add automatic backup to the app

This commit is contained in:
deniscerri 2025-03-01 17:32:42 +01:00
parent 8254640e5f
commit 1a597240fd
No known key found for this signature in database
GPG key ID: 95C43D517D830350
16 changed files with 779 additions and 533 deletions

View file

@ -12,7 +12,7 @@ def properties = new Properties()
def versionMajor = 1
def versionMinor = 8
def versionPatch = 2
def versionBuild = 2 // bump for dogfood builds, public betas, etc.
def versionBuild = 1 // bump for dogfood builds, public betas, etc.
def isBeta = false
def versionExt = ""

View file

@ -37,6 +37,7 @@ import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.viewmodel.CookieViewModel
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.database.viewmodel.SettingsViewModel
import com.deniscerri.ytdl.ui.BaseActivity
import com.deniscerri.ytdl.ui.HomeFragment
import com.deniscerri.ytdl.ui.downloads.DownloadQueueMainFragment
@ -46,6 +47,7 @@ import com.deniscerri.ytdl.util.CrashListener
import com.deniscerri.ytdl.util.NavbarUtil
import com.deniscerri.ytdl.util.NavbarUtil.applyNavBarStyle
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.dialog.MaterialAlertDialogBuilder
@ -62,6 +64,7 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.io.BufferedReader
import java.io.InputStreamReader
import java.io.Reader
@ -77,6 +80,7 @@ class MainActivity : BaseActivity() {
private lateinit var resultViewModel: ResultViewModel
private lateinit var cookieViewModel: CookieViewModel
private lateinit var downloadViewModel: DownloadViewModel
private lateinit var settingsViewModel: SettingsViewModel
private var navigationView: NavigationView? = null
private var navigationBarView: NavigationBarView? = null
private lateinit var navHostFragment : NavHostFragment
@ -92,6 +96,7 @@ class MainActivity : BaseActivity() {
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
cookieViewModel = ViewModelProvider(this)[CookieViewModel::class.java]
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
settingsViewModel = ViewModelProvider(this)[SettingsViewModel::class.java]
preferences = PreferenceManager.getDefaultSharedPreferences(context)
if (preferences.getBoolean("incognito", false)) {
@ -479,8 +484,17 @@ class MainActivity : BaseActivity() {
private fun checkUpdate() {
if (preferences.getBoolean("update_app", false)) {
val updateUtil = UpdateUtil(this)
CoroutineScope(Dispatchers.IO).launch{
updateUtil.updateApp{}
CoroutineScope(Dispatchers.IO).launch {
val res = updateUtil.tryGetNewVersion()
if (res.isSuccess) {
if (preferences.getBoolean("automatic_backup", false)) {
settingsViewModel.backup()
}
withContext(Dispatchers.Main) {
UiUtil.showNewAppUpdateDialog(res.getOrNull()!!, this@MainActivity, preferences)
}
}
}
}
}

View file

@ -175,6 +175,10 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
return downloadDao.getErroredDownloadsList()
}
fun getSavedDownloads() : List<DownloadItem> {
return downloadDao.getSavedDownloadsList()
}
fun getScheduledDownloadIDs() : List<Long> {
return downloadDao.getScheduledDownloadIDs()
}

View file

@ -1,11 +1,22 @@
package com.deniscerri.ytdl.database.repository
import android.content.SharedPreferences
import androidx.preference.PreferenceManager
import androidx.work.Constraints
import androidx.work.Data
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.dao.ObserveSourcesDao
import com.deniscerri.ytdl.database.models.observeSources.ObserveSourcesItem
import com.deniscerri.ytdl.work.ObserveSourceWorker
import kotlinx.coroutines.flow.Flow
import java.util.Calendar
import java.util.concurrent.TimeUnit
class ObserveSourcesRepository(private val observeSourcesDao: ObserveSourcesDao) {
class ObserveSourcesRepository(private val observeSourcesDao: ObserveSourcesDao, private val workManager: WorkManager, private val sharedPreferences: SharedPreferences) {
val items : Flow<List<ObserveSourcesItem>> = observeSourcesDao.getAllSourcesFlow()
enum class SourceStatus {
ACTIVE, STOPPED
@ -58,4 +69,72 @@ class ObserveSourcesRepository(private val observeSourcesDao: ObserveSourcesDao)
observeSourcesDao.update(item)
}
fun cancelObservationTaskByID(id: Long){
workManager.cancelAllWorkByTag("observation_$id")
}
fun observeTask(it: ObserveSourcesItem){
cancelObservationTaskByID(it.id)
Calendar.getInstance().apply {
timeInMillis = it.startsTime
if (it.everyCategory != ObserveSourcesRepository.EveryCategory.HOUR){
val hourMin = Calendar.getInstance()
hourMin.timeInMillis = it.everyTime
set(Calendar.HOUR_OF_DAY, hourMin.get(Calendar.HOUR_OF_DAY))
set(Calendar.MINUTE, hourMin.get(Calendar.MINUTE))
}
when(it.everyCategory){
ObserveSourcesRepository.EveryCategory.HOUR -> {}
ObserveSourcesRepository.EveryCategory.DAY -> {}
ObserveSourcesRepository.EveryCategory.WEEK -> {
var weekDayNr = get(Calendar.DAY_OF_WEEK) - 1
if (weekDayNr == 0) weekDayNr = 7
val followingWeekDay = it.weeklyConfig?.weekDays?.firstOrNull { it >= weekDayNr }
if (followingWeekDay == null){
add(
Calendar.DAY_OF_MONTH,
it.weeklyConfig?.weekDays?.minBy { it }?.plus((7 - weekDayNr)) ?: 0)
}else{
add(Calendar.DAY_OF_MONTH, followingWeekDay - weekDayNr)
}
}
ObserveSourcesRepository.EveryCategory.MONTH -> {
val currentMonthIndex = get(Calendar.MONTH)
if (it.monthlyConfig?.startsMonth != currentMonthIndex){
set(Calendar.MONTH, it.monthlyConfig?.startsMonth ?: 0)
if (timeInMillis < Calendar.getInstance().timeInMillis){
add(Calendar.YEAR, 1)
}
}
}
}
//schedule for next time
val allowMeteredNetworks = sharedPreferences.getBoolean("metered_networks", true)
val workConstraints = Constraints.Builder()
if (!allowMeteredNetworks) workConstraints.setRequiredNetworkType(NetworkType.UNMETERED)
else {
workConstraints.setRequiredNetworkType(NetworkType.CONNECTED)
}
val workRequest = OneTimeWorkRequestBuilder<ObserveSourceWorker>()
.addTag("observeSources")
.addTag("observation_${it.id}")
.setConstraints(workConstraints.build())
.setInitialDelay(timeInMillis - System.currentTimeMillis(), TimeUnit.MILLISECONDS)
.setInputData(Data.Builder().putLong("id", it.id).build())
workManager.enqueueUniqueWork(
"OBSERVE${it.id}",
ExistingWorkPolicy.REPLACE,
workRequest.build()
)
}
}
}

View file

@ -792,7 +792,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
}
fun getSaved() : List<DownloadItem> {
return dao.getSavedDownloadsList()
return repository.getSavedDownloads()
}
fun getActiveDownloads() : List<DownloadItem>{

View file

@ -30,10 +30,10 @@ class ObserveSourcesViewModel(private val application: Application) : AndroidVie
init {
val dao = DBManager.getInstance(application).observeSourcesDao
repository = ObserveSourcesRepository(dao)
items = repository.items.asLiveData()
workManager = WorkManager.getInstance(application)
preferences = PreferenceManager.getDefaultSharedPreferences(application)
repository = ObserveSourcesRepository(dao, workManager, preferences)
items = repository.items.asLiveData()
}
fun getAll(): List<ObserveSourcesItem> {
@ -51,30 +51,30 @@ class ObserveSourcesViewModel(private val application: Application) : AndroidVie
suspend fun insertUpdate(item: ObserveSourcesItem) : Long {
if (item.id > 0) {
repository.update(item)
observeTask(item)
repository.observeTask(item)
return item.id
}
val id = repository.insert(item)
item.id = id
if (id > 0) observeTask(item)
if (id > 0) repository.observeTask(item)
return id
}
suspend fun stopObserving(item: ObserveSourcesItem) {
item.status = ObserveSourcesRepository.SourceStatus.STOPPED
repository.update(item)
cancelObservationTaskByID(item.id)
repository.cancelObservationTaskByID(item.id)
}
fun delete(item: ObserveSourcesItem) = viewModelScope.launch(Dispatchers.IO) {
runCatching { cancelObservationTaskByID(item.id) }
runCatching { repository.cancelObservationTaskByID(item.id) }
repository.delete(item)
}
fun deleteAll() = viewModelScope.launch(Dispatchers.IO) {
getAll().forEach {
runCatching { cancelObservationTaskByID(it.id) }
runCatching { repository.cancelObservationTaskByID(it.id) }
}
repository.deleteAll()
@ -83,76 +83,4 @@ class ObserveSourcesViewModel(private val application: Application) : AndroidVie
suspend fun update(item: ObserveSourcesItem) {
repository.update(item)
}
private fun cancelObservationTaskByID(id: Long){
workManager.cancelAllWorkByTag("observation_$id")
}
private fun observeTask(it: ObserveSourcesItem){
cancelObservationTaskByID(it.id)
Calendar.getInstance().apply {
timeInMillis = it.startsTime
if (it.everyCategory != ObserveSourcesRepository.EveryCategory.HOUR){
val hourMin = Calendar.getInstance()
hourMin.timeInMillis = it.everyTime
set(Calendar.HOUR_OF_DAY, hourMin.get(Calendar.HOUR_OF_DAY))
set(Calendar.MINUTE, hourMin.get(Calendar.MINUTE))
}
when(it.everyCategory){
ObserveSourcesRepository.EveryCategory.HOUR -> {}
ObserveSourcesRepository.EveryCategory.DAY -> {}
ObserveSourcesRepository.EveryCategory.WEEK -> {
var weekDayNr = get(Calendar.DAY_OF_WEEK) - 1
if (weekDayNr == 0) weekDayNr = 7
val followingWeekDay = it.weeklyConfig?.weekDays?.firstOrNull { it >= weekDayNr }
if (followingWeekDay == null){
add(Calendar.DAY_OF_MONTH,
it.weeklyConfig?.weekDays?.minBy { it }?.plus((7 - weekDayNr)) ?: 0)
}else{
add(Calendar.DAY_OF_MONTH, followingWeekDay - weekDayNr)
}
}
ObserveSourcesRepository.EveryCategory.MONTH -> {
val currentMonthIndex = get(Calendar.MONTH)
if (it.monthlyConfig?.startsMonth != currentMonthIndex){
set(Calendar.MONTH, it.monthlyConfig?.startsMonth ?: 0)
if (timeInMillis < Calendar.getInstance().timeInMillis){
add(Calendar.YEAR, 1)
}
}
}
}
//schedule for next time
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application)
val allowMeteredNetworks = sharedPreferences.getBoolean("metered_networks", true)
val workConstraints = Constraints.Builder()
if (!allowMeteredNetworks) workConstraints.setRequiredNetworkType(NetworkType.UNMETERED)
else {
workConstraints.setRequiredNetworkType(NetworkType.CONNECTED)
}
val workRequest = OneTimeWorkRequestBuilder<ObserveSourceWorker>()
.addTag("observeSources")
.addTag("observation_${it.id}")
.setConstraints(workConstraints.build())
.setInitialDelay(timeInMillis - System.currentTimeMillis(), TimeUnit.MILLISECONDS)
.setInputData(Data.Builder().putLong("id", it.id).build())
workManager.enqueueUniqueWork(
"OBSERVE${it.id}",
ExistingWorkPolicy.REPLACE,
workRequest.build()
)
}
}
}

View file

@ -0,0 +1,235 @@
package com.deniscerri.ytdl.database.viewmodel
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import android.util.Log
import androidx.core.content.edit
import androidx.lifecycle.AndroidViewModel
import androidx.preference.PreferenceManager
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import com.deniscerri.ytdl.BuildConfig
import com.deniscerri.ytdl.database.DBManager
import com.deniscerri.ytdl.database.models.RestoreAppDataItem
import com.deniscerri.ytdl.database.repository.CommandTemplateRepository
import com.deniscerri.ytdl.database.repository.CookieRepository
import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.repository.HistoryRepository
import com.deniscerri.ytdl.database.repository.ObserveSourcesRepository
import com.deniscerri.ytdl.database.repository.SearchHistoryRepository
import com.deniscerri.ytdl.util.BackupSettingsUtil
import com.deniscerri.ytdl.util.FileUtil
import com.google.gson.GsonBuilder
import com.google.gson.JsonObject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.util.Calendar
import java.util.concurrent.TimeUnit
class SettingsViewModel(private val application: Application) : AndroidViewModel(application) {
private val workManager : WorkManager = WorkManager.getInstance(application)
private val preferences : SharedPreferences = PreferenceManager.getDefaultSharedPreferences(application)
private val historyRepository : HistoryRepository
private val downloadRepository : DownloadRepository
private val cookieRepository : CookieRepository
private val commandTemplateRepository : CommandTemplateRepository
private val searchHistoryRepository : SearchHistoryRepository
private val observeSourcesRepository : ObserveSourcesRepository
init {
val dbManager = DBManager.getInstance(application)
historyRepository = HistoryRepository(dbManager.historyDao)
downloadRepository = DownloadRepository(dbManager.downloadDao)
cookieRepository = CookieRepository(dbManager.cookieDao)
commandTemplateRepository = CommandTemplateRepository(dbManager.commandTemplateDao)
searchHistoryRepository = SearchHistoryRepository(dbManager.searchHistoryDao)
observeSourcesRepository = ObserveSourcesRepository(dbManager.observeSourcesDao, workManager, preferences)
}
suspend fun backup(items: List<String> = listOf()) : Result<String> {
var list = items
if (list.isEmpty()) {
list = listOf("settings", "downloads", "queued", "scheduled", "cancelled", "errored", "saved", "cookies", "templates", "shortcuts", "searchHistory", "observeSources")
}
val json = JsonObject()
json.addProperty("app", "YTDLnis_backup")
list.forEach {
runCatching {
when(it){
"settings" -> json.add("settings", BackupSettingsUtil.backupSettings(preferences))
"downloads" -> json.add("downloads", BackupSettingsUtil.backupHistory(historyRepository))
"queued" -> json.add("queued", BackupSettingsUtil.backupQueuedDownloads(downloadRepository))
"scheduled" -> json.add("scheduled", BackupSettingsUtil.backupScheduledDownloads(downloadRepository))
"cancelled" -> json.add("cancelled", BackupSettingsUtil.backupCancelledDownloads(downloadRepository))
"errored" -> json.add("errored", BackupSettingsUtil.backupErroredDownloads(downloadRepository))
"saved" -> json.add("saved", BackupSettingsUtil.backupSavedDownloads(downloadRepository))
"cookies" -> json.add("cookies", BackupSettingsUtil.backupCookies(cookieRepository))
"templates" -> json.add("templates", BackupSettingsUtil.backupCommandTemplates(commandTemplateRepository))
"shortcuts" -> json.add("shortcuts", BackupSettingsUtil.backupShortcuts(commandTemplateRepository))
"searchHistory" -> json.add("search_history", BackupSettingsUtil.backupSearchHistory(searchHistoryRepository))
"observeSources" -> json.add("observe_sources", BackupSettingsUtil.backupObserveSources(observeSourcesRepository))
}
}.onFailure {err ->
return Result.failure(err)
}
}
val currentTime = Calendar.getInstance()
val dir = File(FileUtil.getCachePath(application) + "/Backups")
dir.mkdirs()
val saveFile = File("${dir.absolutePath}/YTDLnis_Backup_${BuildConfig.VERSION_NAME}_${currentTime.get(
Calendar.YEAR)}-${currentTime.get(Calendar.MONTH) + 1}-${currentTime.get(
Calendar.DAY_OF_MONTH)}.json")
saveFile.delete()
withContext(Dispatchers.IO) {
saveFile.createNewFile()
}
saveFile.writeText(GsonBuilder().setPrettyPrinting().create().toJson(json))
val res = withContext(Dispatchers.IO) {
FileUtil.moveFile(saveFile.parentFile!!, application, FileUtil.getBackupPath(application), false) {}
}
return Result.success(res[0])
}
suspend fun restoreData(data: RestoreAppDataItem, context: Context, resetData: Boolean = false) : Boolean {
val result = kotlin.runCatching {
data.settings?.apply {
val prefs = this
PreferenceManager.getDefaultSharedPreferences(context).edit(commit = true){
clear()
prefs.forEach {
val key : String = it.asJsonObject.get("key").toString().replace("\"", "")
when(it.asJsonObject.get("type").toString().replace("\"", "")){
"String" -> {
val value = it.asJsonObject.get("value").toString().replace("\"", "")
putString(key, value)
}
"Boolean" -> {
val value = it.asJsonObject.get("value").toString().replace("\"", "").toBoolean()
Log.e("REST", value.toString())
Log.e("REST", key)
putBoolean(key, value)
}
"Int" -> {
val value = it.asJsonObject.get("value").toString().replace("\"", "").toInt()
putInt(key, value)
}
"HashSet" -> {
val value = it.asJsonObject.get("value").toString().replace("(\")|(\\[)|(])|([ \\t])".toRegex(), "").split(",")
putStringSet(key, value.toHashSet())
}
}
}
}
}
data.downloads?.apply {
withContext(Dispatchers.IO){
if (resetData) historyRepository.deleteAll(false)
data.downloads!!.forEach {
historyRepository.insert(it)
}
}
}
data.queued?.apply {
withContext(Dispatchers.IO){
if (resetData) downloadRepository.deleteQueued()
data.queued!!.forEach {
downloadRepository.insert(it)
}
downloadRepository.startDownloadWorker(listOf(), application)
}
}
data.cancelled?.apply {
withContext(Dispatchers.IO){
if (resetData) downloadRepository.deleteCancelled()
data.cancelled!!.forEach {
downloadRepository.insert(it)
}
}
}
data.errored?.apply {
withContext(Dispatchers.IO){
if (resetData) downloadRepository.deleteErrored()
data.errored!!.forEach {
downloadRepository.insert(it)
}
}
}
data.saved?.apply {
withContext(Dispatchers.IO){
if (resetData) downloadRepository.deleteSaved()
data.saved!!.forEach {
downloadRepository.insert(it)
}
}
}
data.cookies?.apply {
withContext(Dispatchers.IO){
if (resetData) cookieRepository.deleteAll()
data.cookies!!.forEach {
cookieRepository.insert(it)
}
}
}
data.templates?.apply {
withContext(Dispatchers.IO){
if (resetData) commandTemplateRepository.deleteAll()
data.templates!!.forEach {
commandTemplateRepository.insert(it)
}
}
}
data.shortcuts?.apply {
withContext(Dispatchers.IO){
if (resetData) commandTemplateRepository.deleteAllShortcuts()
data.shortcuts!!.forEach {
commandTemplateRepository.insertShortcut(it)
}
}
}
data.searchHistory?.apply {
withContext(Dispatchers.IO){
if (resetData) searchHistoryRepository.deleteAll()
data.searchHistory!!.forEach {
searchHistoryRepository.insert(it.query)
}
}
}
data.observeSources?.apply {
withContext(Dispatchers.IO){
if (resetData) observeSourcesRepository.deleteAll()
data.observeSources!!.forEach {
observeSourcesRepository.insert(it)
}
}
}
}
return result.isSuccess
}
}

View file

@ -25,6 +25,7 @@ import androidx.navigation.fragment.findNavController
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import androidx.preference.SwitchPreferenceCompat
import androidx.work.WorkInfo
import androidx.work.WorkManager
import com.deniscerri.ytdl.BuildConfig
@ -44,6 +45,8 @@ import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel
import com.deniscerri.ytdl.database.viewmodel.ObserveSourcesViewModel
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.database.viewmodel.SettingsViewModel
import com.deniscerri.ytdl.ui.more.settings.FolderSettingsFragment.Companion.COMMAND_PATH_CODE
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.UiUtil
import com.deniscerri.ytdl.util.UpdateUtil
@ -67,22 +70,22 @@ import java.util.Locale
class MainSettingsFragment : PreferenceFragmentCompat() {
private var backup : Preference? = null
private var restore : Preference? = null
private var backupPath : Preference? = null
private var updateUtil: UpdateUtil? = null
private var activeDownloadCount = 0
private lateinit var resultViewModel: ResultViewModel
private lateinit var historyViewModel: HistoryViewModel
private lateinit var downloadViewModel: DownloadViewModel
private lateinit var cookieViewModel: CookieViewModel
private lateinit var commandTemplateViewModel: CommandTemplateViewModel
private lateinit var observeSourcesViewModel: ObserveSourcesViewModel
private lateinit var settingsViewModel: SettingsViewModel
private lateinit var editor: SharedPreferences.Editor
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.root_preferences, rootKey)
val navController = findNavController()
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
editor = preferences.edit()
val appearance = findPreference<Preference>("appearance")
val separator = if (Locale(preferences.getString("app_language", "en")!!).layoutDirection == LayoutDirection.RTL) "،" else ","
appearance?.summary = "${if (Build.VERSION.SDK_INT < 33) getString(R.string.language) + "$separator " else ""}${getString(R.string.Theme)}$separator ${getString(R.string.accents)}$separator ${getString(R.string.preferred_search_engine)}"
@ -135,16 +138,11 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
if (w.state == WorkInfo.State.RUNNING) activeDownloadCount++
}
}
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
historyViewModel = ViewModelProvider(this)[HistoryViewModel::class.java]
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
cookieViewModel = ViewModelProvider(this)[CookieViewModel::class.java]
commandTemplateViewModel = ViewModelProvider(this)[CommandTemplateViewModel::class.java]
observeSourcesViewModel = ViewModelProvider(this)[ObserveSourcesViewModel::class.java]
settingsViewModel = ViewModelProvider(this)[SettingsViewModel::class.java]
backup = findPreference("backup")
restore = findPreference("restore")
backupPath = findPreference("backup_path")
findPreference<Preference>("package_name")?.apply {
summary = BuildConfig.APPLICATION_ID
@ -175,68 +173,38 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
builder.setPositiveButton(
getString(R.string.ok)
) { _: DialogInterface?, _: Int ->
lifecycleScope.launch(Dispatchers.IO) {
lifecycleScope.launch {
if (checkedItems.all { !it }){
withContext(Dispatchers.Main){
Snackbar.make(requireView(), R.string.select_backup_categories, Snackbar.LENGTH_SHORT).show()
}
Snackbar.make(requireView(), R.string.select_backup_categories, Snackbar.LENGTH_SHORT).show()
return@launch
}
val json = JsonObject()
json.addProperty("app", "YTDLnis_backup")
for (i in 0 until checkedItems.size) {
if (checkedItems[i]) {
runCatching {
when(values[i]){
"settings" -> json.add("settings", backupSettings(preferences))
"downloads" -> json.add("downloads", backupHistory())
"queued" -> json.add("queued", backupQueuedDownloads() )
"scheduled" -> json.add("scheduled", backupScheduledDownloads() )
"cancelled" -> json.add("cancelled", backupCancelledDownloads() )
"errored" -> json.add("errored", backupErroredDownloads() )
"saved" -> json.add("saved", backupSavedDownloads() )
"cookies" -> json.add("cookies", backupCookies() )
"templates" -> json.add("templates", backupCommandTemplates() )
"shortcuts" -> json.add("shortcuts", backupShortcuts() )
"searchHistory" -> json.add("search_history", backupSearchHistory() )
"observeSources" -> json.add("observe_sources", backupObserveSources() )
}
}.onFailure {err ->
withContext(Dispatchers.Main){
val snack = Snackbar.make(requireView(), err.message ?: requireContext().getString(R.string.errored), Snackbar.LENGTH_LONG)
val snackbarView: View = snack.view
val snackTextView = snackbarView.findViewById<View>(com.google.android.material.R.id.snackbar_text) as TextView
snackTextView.maxLines = 9999999
snack.setAction(android.R.string.copy){
UiUtil.copyToClipboard(err.message ?: requireContext().getString(R.string.errored), requireActivity())
}
snack.show()
}
}
val selectedItems = values.mapIndexed { idx, it -> Pair<String, Boolean>(it, checkedItems[idx]) }.filter { it.second }.map { it.first }
val pathResult = withContext(Dispatchers.IO){
settingsViewModel.backup(selectedItems)
}
if (pathResult.isFailure) {
val errorMessage = pathResult.exceptionOrNull()?.message ?: requireContext().getString(R.string.errored)
val snack = Snackbar.make(requireView(), errorMessage, Snackbar.LENGTH_LONG)
val snackbarView: View = snack.view
val snackTextView = snackbarView.findViewById<View>(com.google.android.material.R.id.snackbar_text) as TextView
snackTextView.maxLines = 9999999
snack.setAction(android.R.string.copy){
UiUtil.copyToClipboard(errorMessage ?: requireContext().getString(R.string.errored), requireActivity())
}
snack.show()
}else {
val s = Snackbar.make(
requireView(),
getString(R.string.backup_created_successfully),
Snackbar.LENGTH_LONG
)
s.setAction(R.string.Open_File) {
FileUtil.openFileIntent(requireActivity(), pathResult.getOrNull()!!)
}
s.show()
}
val currentTime = Calendar.getInstance()
val dir = File("${FileUtil.getCachePath(requireContext())}/Backups")
dir.mkdirs()
val saveFile = File("${dir.absolutePath}/YTDLnis_Backup_${BuildConfig.VERSION_NAME}_${currentTime.get(
Calendar.YEAR)}-${currentTime.get(Calendar.MONTH) + 1}-${currentTime.get(
Calendar.DAY_OF_MONTH)}_${currentTime.get(Calendar.HOUR_OF_DAY)}-${currentTime.get(Calendar.MINUTE)}-${currentTime.get(Calendar.SECOND)}.json")
saveFile.delete()
saveFile.createNewFile()
saveFile.writeText(GsonBuilder().setPrettyPrinting().create().toJson(json))
val res = withContext(Dispatchers.IO) {
FileUtil.moveFile(saveFile.parentFile!!, requireContext(), FileUtil.getDefaultApplicationPath() + "/Backups", false) {}
}
val s = Snackbar.make(requireView(), getString(R.string.backup_created_successfully), Snackbar.LENGTH_LONG)
s.setAction(R.string.Open_File){
FileUtil.openFileIntent(requireActivity(), res[0])
}
s.show()
}
}
@ -260,179 +228,39 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
true
}
backupPath?.apply {
summary = FileUtil.formatPath(FileUtil.getBackupPath(requireContext()))
setOnPreferenceClickListener {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
backupPathResultLauncher.launch(intent)
true
}
}
}
private fun backupSettings(preferences: SharedPreferences) : JsonArray {
runCatching {
val prefs = preferences.all
prefs.remove("app_language")
val arr = JsonArray()
prefs.forEach {
val obj = JsonObject()
obj.addProperty("key", it.key)
obj.addProperty("value", it.value.toString())
obj.addProperty("type", it.value!!::class.simpleName)
arr.add(obj)
private var backupPathResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.let {
activity?.contentResolver?.takePersistableUriPermission(
it,
Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
}
return arr
val path = result.data!!.data.toString()
backupPath!!.summary = FileUtil.formatPath(path)
editor.putString("backup_path", path)
editor.apply()
}
return JsonArray()
}
private suspend fun backupHistory() : JsonArray {
runCatching {
val historyItems = withContext(Dispatchers.IO) {
historyViewModel.getAll()
}
val arr = JsonArray()
historyItems.forEach {
arr.add(JsonParser.parseString(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
private suspend fun backupQueuedDownloads() : JsonArray {
runCatching {
val items = withContext(Dispatchers.IO) {
downloadViewModel.getQueued()
}
val arr = JsonArray()
items.forEach {
arr.add(JsonParser.parseString(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
private suspend fun backupScheduledDownloads() : JsonArray {
runCatching {
val items = withContext(Dispatchers.IO) {
downloadViewModel.getScheduled()
}
val arr = JsonArray()
items.forEach {
arr.add(JsonParser.parseString(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
private suspend fun backupCancelledDownloads() : JsonArray {
runCatching {
val items = withContext(Dispatchers.IO) {
downloadViewModel.getCancelled()
}
val arr = JsonArray()
items.forEach {
arr.add(JsonParser.parseString(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
private suspend fun backupErroredDownloads() : JsonArray {
runCatching {
val items = withContext(Dispatchers.IO) {
downloadViewModel.getErrored()
}
val arr = JsonArray()
items.forEach {
arr.add(JsonParser.parseString(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
private suspend fun backupSavedDownloads() : JsonArray {
runCatching {
val items = withContext(Dispatchers.IO) {
downloadViewModel.getSaved()
}
val arr = JsonArray()
items.forEach {
arr.add(JsonParser.parseString(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
private suspend fun backupCookies() : JsonArray {
runCatching {
val items = withContext(Dispatchers.IO) {
cookieViewModel.getAll()
}
val arr = JsonArray()
items.forEach {
arr.add(JsonParser.parseString(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
private suspend fun backupCommandTemplates() : JsonArray {
runCatching {
val items = withContext(Dispatchers.IO) {
commandTemplateViewModel.getAll()
}
val arr = JsonArray()
items.forEach {
it.useAsExtraCommand = false
arr.add(JsonParser.parseString(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
private suspend fun backupShortcuts() : JsonArray {
runCatching {
val items = withContext(Dispatchers.IO) {
commandTemplateViewModel.getAllShortcuts()
}
val arr = JsonArray()
items.forEach {
arr.add(JsonParser.parseString(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
private suspend fun backupSearchHistory() : JsonArray {
runCatching {
val historyItems = withContext(Dispatchers.IO) {
resultViewModel.getSearchHistory()
}
val arr = JsonArray()
historyItems.forEach {
arr.add(JsonParser.parseString(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
private suspend fun backupObserveSources() : JsonArray {
runCatching {
val observeSourcesItems = withContext(Dispatchers.IO) {
observeSourcesViewModel.getAll()
}
val arr = JsonArray()
observeSourcesItems.forEach {
arr.add(JsonParser.parseString(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
private var appRestoreResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
@ -590,10 +418,28 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
showAppRestoreInfoDialog(
onMerge = {
restoreData(restoreData,parsedDataMessage.toString())
lifecycleScope.launch {
val res = withContext(Dispatchers.IO){
settingsViewModel.restoreData(restoreData, requireContext())
}
if (res) {
showRestoreFinishedDialog(restoreData, parsedDataMessage.toString())
}else{
throw Error()
}
}
},
onReset = {
restoreData(restoreData,parsedDataMessage.toString(), true)
lifecycleScope.launch {
val res = withContext(Dispatchers.IO){
settingsViewModel.restoreData(restoreData, requireContext(),true)
}
if (res) {
showRestoreFinishedDialog(restoreData, parsedDataMessage.toString())
}else{
throw Error()
}
}
}
)
@ -634,128 +480,7 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
dialog.getButton(AlertDialog.BUTTON_NEUTRAL).gravity = Gravity.START
}
private fun restoreData(data: RestoreAppDataItem, restoreDataMessage: String, resetData: Boolean = false) = lifecycleScope.launch {
data.settings?.apply {
val prefs = this
PreferenceManager.getDefaultSharedPreferences(requireContext()).edit(commit = true){
clear()
prefs.forEach {
val key : String = it.asJsonObject.get("key").toString().replace("\"", "")
when(it.asJsonObject.get("type").toString().replace("\"", "")){
"String" -> {
val value = it.asJsonObject.get("value").toString().replace("\"", "")
putString(key, value)
}
"Boolean" -> {
val value = it.asJsonObject.get("value").toString().replace("\"", "").toBoolean()
Log.e("REST", value.toString())
Log.e("REST", key)
putBoolean(key, value)
}
"Int" -> {
val value = it.asJsonObject.get("value").toString().replace("\"", "").toInt()
putInt(key, value)
}
"HashSet" -> {
val value = it.asJsonObject.get("value").toString().replace("(\")|(\\[)|(])|([ \\t])".toRegex(), "").split(",")
putStringSet(key, value.toHashSet())
}
}
}
}
}
data.downloads?.apply {
withContext(Dispatchers.IO){
if (resetData) historyViewModel.deleteAll(false)
data.downloads!!.forEach {
historyViewModel.insert(it)
}
}
}
data.queued?.apply {
withContext(Dispatchers.IO){
if (resetData) downloadViewModel.deleteQueued()
data.queued!!.forEach {
downloadViewModel.insert(it)
}
downloadViewModel.queueDownloads(listOf())
}
}
data.cancelled?.apply {
withContext(Dispatchers.IO){
if (resetData) downloadViewModel.deleteCancelled()
data.cancelled!!.forEach {
downloadViewModel.insert(it)
}
}
}
data.errored?.apply {
withContext(Dispatchers.IO){
if (resetData) downloadViewModel.deleteErrored()
data.errored!!.forEach {
downloadViewModel.insert(it)
}
}
}
data.saved?.apply {
withContext(Dispatchers.IO){
if (resetData) downloadViewModel.deleteSaved()
data.saved!!.forEach {
downloadViewModel.insert(it)
}
}
}
data.cookies?.apply {
withContext(Dispatchers.IO){
if (resetData) cookieViewModel.deleteAll()
data.cookies!!.forEach {
cookieViewModel.insert(it)
}
}
}
data.templates?.apply {
withContext(Dispatchers.IO){
if (resetData) commandTemplateViewModel.deleteAll()
data.templates!!.forEach {
commandTemplateViewModel.insert(it)
}
}
}
data.shortcuts?.apply {
withContext(Dispatchers.IO){
if (resetData) commandTemplateViewModel.deleteAllShortcuts()
data.shortcuts!!.forEach {
commandTemplateViewModel.insertShortcut(it)
}
}
}
data.searchHistory?.apply {
withContext(Dispatchers.IO){
if (resetData) resultViewModel.deleteAllSearchQueryHistory()
data.searchHistory!!.forEach {
resultViewModel.addSearchQueryToHistory(it.query)
}
}
}
data.observeSources?.apply {
withContext(Dispatchers.IO){
if (resetData) observeSourcesViewModel.deleteAll()
data.observeSources!!.forEach {
observeSourcesViewModel.insertUpdate(it)
}
}
}
private fun showRestoreFinishedDialog(data: RestoreAppDataItem, restoreDataMessage: String) {
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val builder = MaterialAlertDialogBuilder(requireContext())

View file

@ -1,16 +1,24 @@
package com.deniscerri.ytdl.ui.more.settings
import android.annotation.SuppressLint
import android.app.Activity
import android.app.DownloadManager
import android.content.BroadcastReceiver
import android.content.ClipboardManager
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.IntentFilter
import android.content.SharedPreferences
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.os.Handler
import android.os.Looper
import android.text.Editable
import android.text.TextWatcher
import android.text.method.LinkMovementMethod
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
@ -24,9 +32,11 @@ import android.widget.LinearLayout
import android.widget.ListView
import android.widget.PopupMenu
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat
import androidx.core.view.get
import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProvider
@ -38,7 +48,9 @@ import com.deniscerri.ytdl.BuildConfig
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.CommandTemplate
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.database.viewmodel.SettingsViewModel
import com.deniscerri.ytdl.database.viewmodel.YTDLPViewModel
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.UiUtil
import com.deniscerri.ytdl.util.UiUtil.showShortcutsSheet
import com.deniscerri.ytdl.util.UpdateUtil
@ -52,10 +64,14 @@ import com.google.android.material.materialswitch.MaterialSwitch
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputLayout
import com.yausername.youtubedl_android.YoutubeDL
import io.noties.markwon.AbstractMarkwonPlugin
import io.noties.markwon.Markwon
import io.noties.markwon.MarkwonConfiguration
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.util.Locale
@ -67,8 +83,9 @@ class UpdateSettingsFragment : BaseSettingsFragment() {
private var updateUtil: UpdateUtil? = null
private var version: Preference? = null
private lateinit var preferences: SharedPreferences
private lateinit var ytdlpViewModel: YTDLPViewModel
private lateinit var ytdlpViewModel: YTDLPViewModel
private lateinit var settingsViewModel: SettingsViewModel
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.updating_preferences, rootKey)
@ -79,6 +96,7 @@ class UpdateSettingsFragment : BaseSettingsFragment() {
ytdlSource = findPreference("ytdlp_source_label")
ytdlpViewModel = ViewModelProvider(this)[YTDLPViewModel::class.java]
settingsViewModel = ViewModelProvider(this)[SettingsViewModel::class.java]
ytdlSource?.apply {
summary = preferences.getString("ytdlp_source_label", "")!!.ifEmpty { getString(R.string.update_ytdl_stable) }
@ -125,16 +143,19 @@ class UpdateSettingsFragment : BaseSettingsFragment() {
version!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
lifecycleScope.launch{
withContext(Dispatchers.IO){
updateUtil!!.updateApp{ msg ->
lifecycleScope.launch(Dispatchers.Main){
view?.apply {
Snackbar.make(requireView(), msg, Snackbar.LENGTH_LONG).show()
}
val res = withContext(Dispatchers.IO){
updateUtil!!.tryGetNewVersion()
}
if (res.isFailure) {
Snackbar.make(requireView(), res.exceptionOrNull()?.message ?: getString(R.string.network_error), Snackbar.LENGTH_LONG).show()
}else{
if (preferences.getBoolean("automatic_backup", false)) {
withContext(Dispatchers.IO){
settingsViewModel.backup()
}
}
UiUtil.showNewAppUpdateDialog(res.getOrNull()!!, requireActivity(), preferences)
}
}
true
}

View file

@ -0,0 +1,190 @@
package com.deniscerri.ytdl.util
import android.content.SharedPreferences
import com.deniscerri.ytdl.database.repository.CommandTemplateRepository
import com.deniscerri.ytdl.database.repository.CookieRepository
import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.repository.HistoryRepository
import com.deniscerri.ytdl.database.repository.ObserveSourcesRepository
import com.deniscerri.ytdl.database.repository.SearchHistoryRepository
import com.google.gson.Gson
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
object BackupSettingsUtil {
fun backupSettings(preferences: SharedPreferences) : JsonArray {
runCatching {
val prefs = preferences.all
prefs.remove("app_language")
val arr = JsonArray()
prefs.forEach {
val obj = JsonObject()
obj.addProperty("key", it.key)
obj.addProperty("value", it.value.toString())
obj.addProperty("type", it.value!!::class.simpleName)
arr.add(obj)
}
return arr
}
return JsonArray()
}
suspend fun backupHistory(historyRepository: HistoryRepository) : JsonArray {
runCatching {
val historyItems = withContext(Dispatchers.IO) {
historyRepository.getAll()
}
val arr = JsonArray()
historyItems.forEach {
arr.add(JsonParser.parseString(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
suspend fun backupQueuedDownloads(downloadRepository: DownloadRepository) : JsonArray {
runCatching {
val items = withContext(Dispatchers.IO) {
downloadRepository.getQueuedDownloads()
}
val arr = JsonArray()
items.forEach {
arr.add(JsonParser.parseString(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
suspend fun backupScheduledDownloads(downloadRepository: DownloadRepository) : JsonArray {
runCatching {
val items = withContext(Dispatchers.IO) {
downloadRepository.getScheduledDownloads()
}
val arr = JsonArray()
items.forEach {
arr.add(JsonParser.parseString(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
suspend fun backupCancelledDownloads(downloadRepository: DownloadRepository) : JsonArray {
runCatching {
val items = withContext(Dispatchers.IO) {
downloadRepository.getCancelledDownloads()
}
val arr = JsonArray()
items.forEach {
arr.add(JsonParser.parseString(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
suspend fun backupErroredDownloads(downloadRepository: DownloadRepository) : JsonArray {
runCatching {
val items = withContext(Dispatchers.IO) {
downloadRepository.getErroredDownloads()
}
val arr = JsonArray()
items.forEach {
arr.add(JsonParser.parseString(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
suspend fun backupSavedDownloads(downloadRepository: DownloadRepository) : JsonArray {
runCatching {
val items = withContext(Dispatchers.IO) {
downloadRepository.getSavedDownloads()
}
val arr = JsonArray()
items.forEach {
arr.add(JsonParser.parseString(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
suspend fun backupCookies(cookieRepository: CookieRepository) : JsonArray {
runCatching {
val items = withContext(Dispatchers.IO) {
cookieRepository.getAll()
}
val arr = JsonArray()
items.forEach {
arr.add(JsonParser.parseString(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
suspend fun backupCommandTemplates(commandTemplateRepository: CommandTemplateRepository) : JsonArray {
runCatching {
val items = withContext(Dispatchers.IO) {
commandTemplateRepository.getAll()
}
val arr = JsonArray()
items.forEach {
it.useAsExtraCommand = false
arr.add(JsonParser.parseString(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
suspend fun backupShortcuts(commandTemplateRepository: CommandTemplateRepository) : JsonArray {
runCatching {
val items = withContext(Dispatchers.IO) {
commandTemplateRepository.getAllShortCuts()
}
val arr = JsonArray()
items.forEach {
arr.add(JsonParser.parseString(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
suspend fun backupSearchHistory(searchHistoryRepository: SearchHistoryRepository) : JsonArray {
runCatching {
val historyItems = withContext(Dispatchers.IO) {
searchHistoryRepository.getAll()
}
val arr = JsonArray()
historyItems.forEach {
arr.add(JsonParser.parseString(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
suspend fun backupObserveSources(observeSourcesRepository: ObserveSourcesRepository) : JsonArray {
runCatching {
val observeSourcesItems = withContext(Dispatchers.IO) {
observeSourcesRepository.getAll()
}
val arr = JsonArray()
observeSourcesItems.forEach {
arr.add(JsonParser.parseString(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
}

View file

@ -284,9 +284,18 @@ object FileUtil {
return listOf()
}
fun getBackupPath(context: Context) : String {
val preference = PreferenceManager.getDefaultSharedPreferences(context).getString("backup_path", "")
return if (preference.isNullOrBlank() || !File(formatPath(preference)).canWrite()) {
getDefaultApplicationPath() + "/Backups"
}else {
formatPath(preference)
}
}
fun getCachePath(context: Context) : String {
val preference = PreferenceManager.getDefaultSharedPreferences(context).getString("cache_path", "")!!
if (preference.isEmpty() || !File(formatPath(preference)).canWrite()) {
val preference = PreferenceManager.getDefaultSharedPreferences(context).getString("cache_path", "")
if (preference.isNullOrBlank() || !File(formatPath(preference)).canWrite()) {
val externalPath = context.getExternalFilesDir(null)
return if (externalPath == null){
context.cacheDir.absolutePath + "/downloads/"

View file

@ -5,17 +5,22 @@ import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.app.Activity
import android.app.DownloadManager
import android.content.BroadcastReceiver
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.IntentFilter
import android.content.SharedPreferences
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.text.Editable
import android.text.TextWatcher
import android.text.format.DateFormat
import android.text.method.LinkMovementMethod
import android.util.DisplayMetrics
import android.view.Gravity
import android.view.LayoutInflater
@ -51,6 +56,7 @@ import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.CommandTemplate
import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.database.models.Format
import com.deniscerri.ytdl.database.models.GithubRelease
import com.deniscerri.ytdl.database.models.HistoryItem
import com.deniscerri.ytdl.database.models.TemplateShortcut
import com.deniscerri.ytdl.database.models.YoutubePlayerClientItem
@ -86,6 +92,9 @@ import com.google.android.material.textfield.TextInputLayout.END_ICON_NONE
import com.google.android.material.timepicker.MaterialTimePicker
import com.google.android.material.timepicker.TimeFormat
import com.google.gson.Gson
import io.noties.markwon.AbstractMarkwonPlugin
import io.noties.markwon.Markwon
import io.noties.markwon.MarkwonConfiguration
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -2494,4 +2503,86 @@ object UiUtil {
ViewGroup.LayoutParams.MATCH_PARENT
)
}
@SuppressLint("UnspecifiedRegisterReceiverFlag")
fun showNewAppUpdateDialog(v: GithubRelease, context: Activity, preferences: SharedPreferences) {
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)
.setIcon(R.drawable.ic_update_app)
.setNeutralButton(R.string.ignore){ d: DialogInterface?, _:Int ->
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()
}
}
val view = updateDialog.show()
val textView = view.findViewById<TextView>(android.R.id.message)
textView!!.movementMethod = LinkMovementMethod.getInstance()
val mw = Markwon.builder(context).usePlugin(object: AbstractMarkwonPlugin() {
override fun configureConfiguration(builder: MarkwonConfiguration.Builder) {
builder.linkResolver { view, link ->
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(link))
context.startActivity(browserIntent)
}
}
}).build()
mw.setMarkdown(textView, v.body)
}
}

View file

@ -47,7 +47,6 @@ import java.net.URL
class UpdateUtil(var context: Context) {
private val tag = "UpdateUtil"
private val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
private val downloadManager: DownloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
private val channelMap = mapOf(
Pair<String, YoutubeDL.UpdateChannel>("stable", YoutubeDL.UpdateChannel.STABLE),
@ -56,21 +55,20 @@ class UpdateUtil(var context: Context) {
)
@SuppressLint("UnspecifiedRegisterReceiverFlag")
fun updateApp(result: (result: String) -> Unit) {
fun tryGetNewVersion() : Result<GithubRelease> {
try {
val skippedVersions = sharedPreferences.getString("skip_updates", "")?.split(",")?.distinct()?.toMutableList() ?: mutableListOf()
val res = getGithubReleases()
if (res.isEmpty()){
result(context.getString(R.string.network_error))
return
return Result.failure(Error(context.getString(R.string.network_error)))
}
val useBeta = sharedPreferences.getBoolean("update_beta", false)
var v: GithubRelease?
val v: GithubRelease
if (useBeta){
v = res.firstOrNull { it.tag_name.contains("beta", true) && res.indexOf(it) == 0 }
if (v == null) v = res.first()
val tmp = res.firstOrNull { it.tag_name.contains("beta", true) && res.indexOf(it) == 0 }
v = tmp ?: res.first()
}else{
v = res.first { !it.tag_name.contains("beta", true) }
}
@ -88,80 +86,13 @@ class UpdateUtil(var context: Context) {
if (skippedVersions.contains(v.tag_name)) isInLatest = true
if (isInLatest){
result(context.getString(R.string.you_are_in_latest_version))
return
return Result.failure(Error(context.getString(R.string.you_are_in_latest_version)))
}
Handler(Looper.getMainLooper()).post {
val updateDialog = MaterialAlertDialogBuilder(context)
.setTitle(v.tag_name)
.setMessage(v.body)
.setIcon(R.drawable.ic_update_app)
.setNeutralButton(R.string.ignore){ d: DialogInterface?, _:Int ->
skippedVersions.add(v.tag_name)
sharedPreferences.edit().putString("skip_updates", skippedVersions.joinToString(",")).apply()
d?.dismiss()
}
.setNegativeButton(context.resources.getString(R.string.cancel)) { _: DialogInterface?, _: Int -> }
.setPositiveButton(context.resources.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)
}
}
context.registerReceiver(onDownloadComplete, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))
d?.dismiss()
}
}
val view = updateDialog.show()
val textView = view.findViewById<TextView>(android.R.id.message)
textView!!.movementMethod = LinkMovementMethod.getInstance()
val mw = Markwon.builder(context).usePlugin(object: AbstractMarkwonPlugin() {
override fun configureConfiguration(builder: MarkwonConfiguration.Builder) {
builder.linkResolver { view, link ->
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(link))
startActivity(context, browserIntent, Bundle())
}
}
}).build()
mw.setMarkdown(textView, v.body)
}
return
return Result.success(v)
}catch (e: Exception){
e.printStackTrace()
result(e.message.toString())
return
return Result.failure(e)
}
}

View file

@ -40,13 +40,14 @@ class ObserveSourceWorker(
val sourceID = inputData.getLong("id", 0)
if (sourceID == 0L) return Result.failure()
val dbManager = DBManager.getInstance(context)
val repo = ObserveSourcesRepository(dbManager.observeSourcesDao)
val workManager = WorkManager.getInstance(context)
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
val repo = ObserveSourcesRepository(dbManager.observeSourcesDao, workManager, sharedPreferences)
val historyRepo = HistoryRepository(dbManager.historyDao)
val downloadRepo = DownloadRepository(dbManager.downloadDao)
val commandTemplateDao = dbManager.commandTemplateDao
val ytdlpUtil = YTDLPUtil(context, commandTemplateDao)
val resultRepository = ResultRepository(dbManager.resultDao, commandTemplateDao, context)
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
val item = repo.getByID(sourceID)
if (item.status == ObserveSourcesRepository.SourceStatus.STOPPED){

View file

@ -467,4 +467,7 @@
<string name="use_format_sorting">Use Format Importance Ordering</string>
<string name="use_code_highlighter">Use Code Color Highlighter</string>
<string name="reset_preferences_in_screen">Reset all preferences in this screen</string>
<string name="automatic_backup">Automatic backup</string>
<string name="backup_path">Backup Path</string>
<string name="automatic_backup_summary">Automatically make a backup of everything when a new version the application is found</string>
</resources>

View file

@ -41,6 +41,21 @@
app:icon="@drawable/baseline_restore_page_24"
app:key="restore"
app:title="@string/restore"/>
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
app:icon="@drawable/ic_clock"
app:key="automatic_backup"
android:summary="@string/automatic_backup_summary"
app:title="@string/automatic_backup" />
<Preference
app:icon="@drawable/baseline_folder_24"
android:summary=""
app:key="backup_path"
app:title="@string/backup_path" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/about" >