diff --git a/app/build.gradle b/app/build.gradle index 83123b46..79902c28 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 = 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 = "" diff --git a/app/src/main/java/com/deniscerri/ytdl/MainActivity.kt b/app/src/main/java/com/deniscerri/ytdl/MainActivity.kt index 358d82f3..64b00d52 100644 --- a/app/src/main/java/com/deniscerri/ytdl/MainActivity.kt +++ b/app/src/main/java/com/deniscerri/ytdl/MainActivity.kt @@ -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) + } + } + } } } diff --git a/app/src/main/java/com/deniscerri/ytdl/database/repository/DownloadRepository.kt b/app/src/main/java/com/deniscerri/ytdl/database/repository/DownloadRepository.kt index 7d704d99..5d89e3c3 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/repository/DownloadRepository.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/repository/DownloadRepository.kt @@ -175,6 +175,10 @@ class DownloadRepository(private val downloadDao: DownloadDao) { return downloadDao.getErroredDownloadsList() } + fun getSavedDownloads() : List { + return downloadDao.getSavedDownloadsList() + } + fun getScheduledDownloadIDs() : List { return downloadDao.getScheduledDownloadIDs() } diff --git a/app/src/main/java/com/deniscerri/ytdl/database/repository/ObserveSourcesRepository.kt b/app/src/main/java/com/deniscerri/ytdl/database/repository/ObserveSourcesRepository.kt index fc04aa2b..4f573a6d 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/repository/ObserveSourcesRepository.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/repository/ObserveSourcesRepository.kt @@ -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> = 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() + .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() + ) + } + + } + } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/DownloadViewModel.kt b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/DownloadViewModel.kt index 0f2b4303..7a778176 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/DownloadViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/DownloadViewModel.kt @@ -792,7 +792,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel } fun getSaved() : List { - return dao.getSavedDownloadsList() + return repository.getSavedDownloads() } fun getActiveDownloads() : List{ diff --git a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/ObserveSourcesViewModel.kt b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/ObserveSourcesViewModel.kt index dcca4402..670bac55 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/ObserveSourcesViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/ObserveSourcesViewModel.kt @@ -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 { @@ -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() - .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() - ) - } - - } - - } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/SettingsViewModel.kt b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/SettingsViewModel.kt new file mode 100644 index 00000000..5a1d8f99 --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/SettingsViewModel.kt @@ -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 = listOf()) : Result { + 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 + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/MainSettingsFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/MainSettingsFragment.kt index 314c29a8..eb021f4a 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/MainSettingsFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/MainSettingsFragment.kt @@ -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("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("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(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(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(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()) diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/UpdateSettingsFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/UpdateSettingsFragment.kt index 03bf6992..82060787 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/UpdateSettingsFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/UpdateSettingsFragment.kt @@ -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 } diff --git a/app/src/main/java/com/deniscerri/ytdl/util/BackupSettingsUtil.kt b/app/src/main/java/com/deniscerri/ytdl/util/BackupSettingsUtil.kt new file mode 100644 index 00000000..a0249a9d --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdl/util/BackupSettingsUtil.kt @@ -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() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/util/FileUtil.kt b/app/src/main/java/com/deniscerri/ytdl/util/FileUtil.kt index b0e056f1..29809684 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/FileUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/FileUtil.kt @@ -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/" 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 48cb733f..d6ca278e 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt @@ -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(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) + } } \ 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 ecefd718..ca256f54 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/UpdateUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/UpdateUtil.kt @@ -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("stable", YoutubeDL.UpdateChannel.STABLE), @@ -56,21 +55,20 @@ class UpdateUtil(var context: Context) { ) @SuppressLint("UnspecifiedRegisterReceiverFlag") - fun updateApp(result: (result: String) -> Unit) { + fun tryGetNewVersion() : Result { 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(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) } } diff --git a/app/src/main/java/com/deniscerri/ytdl/work/ObserveSourceWorker.kt b/app/src/main/java/com/deniscerri/ytdl/work/ObserveSourceWorker.kt index 04b140f3..a071fba1 100644 --- a/app/src/main/java/com/deniscerri/ytdl/work/ObserveSourceWorker.kt +++ b/app/src/main/java/com/deniscerri/ytdl/work/ObserveSourceWorker.kt @@ -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){ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 32fa6448..cd46f915 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -467,4 +467,7 @@ Use Format Importance Ordering Use Code Color Highlighter Reset all preferences in this screen + Automatic backup + Backup Path + Automatically make a backup of everything when a new version the application is found \ No newline at end of file diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml index 0107b9a1..b90bd697 100644 --- a/app/src/main/res/xml/root_preferences.xml +++ b/app/src/main/res/xml/root_preferences.xml @@ -41,6 +41,21 @@ app:icon="@drawable/baseline_restore_page_24" app:key="restore" app:title="@string/restore"/> + + + + +