diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt index 11b7fac46..a465ffa4d 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -134,10 +134,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { val isReaderOptimizationEnabled: Boolean get() = prefs.getBoolean(KEY_READER_OPTIMIZE, false) - var isTrafficWarningEnabled: Boolean - get() = prefs.getBoolean(KEY_TRAFFIC_WARNING, true) - set(value) = prefs.edit { putBoolean(KEY_TRAFFIC_WARNING, value) } - val isOfflineCheckDisabled: Boolean get() = prefs.getBoolean(KEY_OFFLINE_DISABLED, false) @@ -328,8 +324,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { } } - val isDownloadsWiFiOnly: Boolean - get() = prefs.getBoolean(KEY_DOWNLOADS_WIFI, false) + var allowDownloadOnMeteredNetwork: TriStateOption + get() = prefs.getEnumValue(KEY_DOWNLOADS_METERED_NETWORK, TriStateOption.ASK) + set(value) = prefs.edit { putEnumValue(KEY_DOWNLOADS_METERED_NETWORK, value) } val preferredDownloadFormat: DownloadFormat get() = prefs.getEnumValue(KEY_DOWNLOADS_FORMAT, DownloadFormat.AUTOMATIC) @@ -573,7 +570,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { const val KEY_THEME = "theme" const val KEY_COLOR_THEME = "color_theme" const val KEY_THEME_AMOLED = "amoled_theme" - const val KEY_TRAFFIC_WARNING = "traffic_warning" const val KEY_OFFLINE_DISABLED = "no_offline" const val KEY_PAGES_CACHE_CLEAR = "pages_cache_clear" const val KEY_HTTP_CACHE_CLEAR = "http_cache_clear" @@ -639,7 +635,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { const val KEY_ANILIST = "anilist" const val KEY_MAL = "mal" const val KEY_KITSU = "kitsu" - const val KEY_DOWNLOADS_WIFI = "downloads_wifi" + const val KEY_DOWNLOADS_METERED_NETWORK = "downloads_metered_network" const val KEY_DOWNLOADS_FORMAT = "downloads_format" const val KEY_ALL_FAVOURITES_VISIBLE = "all_favourites_visible" const val KEY_DOH = "doh" diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/TriStateOption.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/TriStateOption.kt new file mode 100644 index 000000000..143ac3b4b --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/TriStateOption.kt @@ -0,0 +1,9 @@ +package org.koitharu.kotatsu.core.prefs + +import androidx.annotation.Keep + +@Keep +enum class TriStateOption { + + ENABLED, ASK, DISABLED; +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/TwoButtonsAlertDialog.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/BigButtonsAlertDialog.kt similarity index 63% rename from app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/TwoButtonsAlertDialog.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/BigButtonsAlertDialog.kt index 4d15077e1..600fda6de 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/TwoButtonsAlertDialog.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/BigButtonsAlertDialog.kt @@ -6,12 +6,13 @@ import android.view.LayoutInflater import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.appcompat.app.AlertDialog +import androidx.core.view.isGone import androidx.core.view.isVisible import com.google.android.material.button.MaterialButton import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.koitharu.kotatsu.databinding.DialogTwoButtonsBinding -class TwoButtonsAlertDialog private constructor( +class BigButtonsAlertDialog private constructor( private val delegate: AlertDialog ) : DialogInterface by delegate { @@ -51,14 +52,44 @@ class TwoButtonsAlertDialog private constructor( @StringRes textId: Int, listener: DialogInterface.OnClickListener? = null ): Builder { - initButton(binding.button2, DialogInterface.BUTTON_NEGATIVE, textId, listener) + initButton(binding.button3, DialogInterface.BUTTON_NEGATIVE, textId, listener) return this } - fun create(): TwoButtonsAlertDialog { + fun setNeutralButton( + @StringRes textId: Int, + listener: DialogInterface.OnClickListener? = null + ): Builder { + initButton(binding.button2, DialogInterface.BUTTON_NEUTRAL, textId, listener) + return this + } + + fun create(): BigButtonsAlertDialog { + with(binding) { + button1.adjustCorners(isFirst = true, isLast = button2.isGone && button3.isGone) + button2.adjustCorners(isFirst = button1.isGone, isLast = button3.isGone) + button3.adjustCorners(isFirst = button1.isGone && button2.isGone, isLast = true) + } + val dialog = delegate.create() binding.root.tag = dialog - return TwoButtonsAlertDialog(dialog) + return BigButtonsAlertDialog(dialog) + } + + private fun MaterialButton.adjustCorners(isFirst: Boolean, isLast: Boolean) { + if (!isVisible) { + return + } + shapeAppearanceModel = shapeAppearanceModel.toBuilder().apply { + if (!isFirst) { + setTopLeftCornerSize(0f) + setTopRightCornerSize(0f) + } + if (!isLast) { + setBottomLeftCornerSize(0f) + setBottomRightCornerSize(0f) + } + }.build() } private fun initButton( diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/CommonAlertDialogs.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/CommonAlertDialogs.kt index 22b32d910..3eaf94219 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/CommonAlertDialogs.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/CommonAlertDialogs.kt @@ -1,25 +1,58 @@ package org.koitharu.kotatsu.core.ui.dialog import android.content.Context +import android.content.DialogInterface import androidx.annotation.UiContext +import androidx.core.net.ConnectivityManagerCompat +import dagger.Lazy import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.prefs.TriStateOption +import org.koitharu.kotatsu.core.util.ext.connectivityManager +import javax.inject.Inject -object CommonAlertDialogs { +class CommonAlertDialogs @Inject constructor( + private val settings: Lazy, +) { - fun showDownloadConfirmation( + fun askForDownloadOverMeteredNetwork( @UiContext context: Context, - onConfirmed: (startPaused: Boolean) -> Unit, - ) = buildAlertDialog(context, isCentered = true) { - var startPaused = false - setTitle(R.string.save_manga) - setIcon(R.drawable.ic_download) - setMessage(R.string.save_manga_confirm) - setCheckbox(R.string.start_download, true) { _, isChecked -> - startPaused = !isChecked - } - setPositiveButton(R.string.save) { _, _ -> - onConfirmed(startPaused) + onConfirmed: (allow: Boolean) -> Unit + ) { + when (settings.get().allowDownloadOnMeteredNetwork) { + TriStateOption.ENABLED -> onConfirmed(true) + TriStateOption.DISABLED -> onConfirmed(false) + TriStateOption.ASK -> { + if (!ConnectivityManagerCompat.isActiveNetworkMetered(context.connectivityManager)) { + onConfirmed(true) + return + } + val listener = DialogInterface.OnClickListener { _, which -> + when (which) { + DialogInterface.BUTTON_POSITIVE -> { + settings.get().allowDownloadOnMeteredNetwork = TriStateOption.ENABLED + onConfirmed(true) + } + + DialogInterface.BUTTON_NEUTRAL -> { + onConfirmed(true) + } + + DialogInterface.BUTTON_NEGATIVE -> { + settings.get().allowDownloadOnMeteredNetwork = TriStateOption.DISABLED + onConfirmed(false) + } + } + } + BigButtonsAlertDialog.Builder(context) + .setIcon(R.drawable.ic_network_cellular) + .setTitle(R.string.download_cellular_confirm) + .setPositiveButton(R.string.allow_always, listener) + .setNeutralButton(R.string.allow_once, listener) + .setNegativeButton(R.string.dont_allow, listener) + .create() + .show() + } } - setNegativeButton(android.R.string.cancel, null) - }.show() + } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/ChaptersPagesViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/ChaptersPagesViewModel.kt index ae3517dd1..b4e013441 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/ChaptersPagesViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/ChaptersPagesViewModel.kt @@ -33,6 +33,7 @@ import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.details.ui.DetailsViewModel import org.koitharu.kotatsu.details.ui.mapChapters import org.koitharu.kotatsu.details.ui.model.ChapterListItem +import org.koitharu.kotatsu.download.ui.worker.DownloadTask import org.koitharu.kotatsu.download.ui.worker.DownloadWorker import org.koitharu.kotatsu.history.data.HistoryRepository import org.koitharu.kotatsu.local.domain.DeleteLocalMangaUseCase @@ -163,14 +164,18 @@ abstract class ChaptersPagesViewModel( } } - fun download(chaptersIds: Set?) { + fun download(chaptersIds: Set?, allowMeteredNetwork: Boolean) { launchJob(Dispatchers.Default) { - downloadScheduler.schedule( - manga = requireManga(), - chaptersIds = chaptersIds, + val task = DownloadTask( + mangaId = requireManga().id, isPaused = false, isSilent = false, + chaptersIds = chaptersIds?.toLongArray(), + destination = null, + format = null, + allowMeteredNetwork = allowMeteredNetwork, ) + downloadScheduler.schedule(setOf(task)) onDownloadStarted.call(Unit) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/chapters/ChaptersFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/chapters/ChaptersFragment.kt index de808b1b4..0c82e7c83 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/chapters/ChaptersFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/chapters/ChaptersFragment.kt @@ -18,6 +18,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.ui.BaseFragment +import org.koitharu.kotatsu.core.ui.dialog.CommonAlertDialogs import org.koitharu.kotatsu.core.ui.list.ListSelectionController import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.util.PagerNestedScrollHelper @@ -38,6 +39,7 @@ import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder import org.koitharu.kotatsu.reader.ui.ReaderNavigationCallback import org.koitharu.kotatsu.reader.ui.ReaderState +import javax.inject.Inject import kotlin.math.roundToInt @AndroidEntryPoint @@ -47,6 +49,9 @@ class ChaptersFragment : private val viewModel by ChaptersPagesViewModel.ActivityVMLazy(this) + @Inject + lateinit var commonAlertDialogs: CommonAlertDialogs + private var chaptersAdapter: ChaptersAdapter? = null private var selectionController: ListSelectionController? = null @@ -62,7 +67,7 @@ class ChaptersFragment : appCompatDelegate = checkNotNull(findAppCompatDelegate()), decoration = ChaptersSelectionDecoration(binding.root.context), registryOwner = this, - callback = ChaptersSelectionCallback(viewModel, binding.recyclerViewChapters), + callback = ChaptersSelectionCallback(viewModel, commonAlertDialogs, binding.recyclerViewChapters), ) viewModel.isChaptersInGridView.observe(viewLifecycleOwner) { chaptersInGridView -> binding.recyclerViewChapters.layoutManager = if (chaptersInGridView) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/chapters/ChaptersSelectionCallback.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/chapters/ChaptersSelectionCallback.kt index a5cb76c3c..fa9d69111 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/chapters/ChaptersSelectionCallback.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/chapters/ChaptersSelectionCallback.kt @@ -8,6 +8,7 @@ import androidx.recyclerview.widget.RecyclerView import com.google.android.material.snackbar.Snackbar import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.model.LocalMangaSource +import org.koitharu.kotatsu.core.ui.dialog.CommonAlertDialogs import org.koitharu.kotatsu.core.ui.list.BaseListSelectionCallback import org.koitharu.kotatsu.core.ui.list.ListSelectionController import org.koitharu.kotatsu.core.util.ext.toCollection @@ -17,6 +18,7 @@ import org.koitharu.kotatsu.local.ui.LocalChaptersRemoveService class ChaptersSelectionCallback( private val viewModel: ChaptersPagesViewModel, + private val commonAlertDialogs: CommonAlertDialogs, recyclerView: RecyclerView, ) : BaseListSelectionCallback(recyclerView) { @@ -58,8 +60,12 @@ class ChaptersSelectionCallback( override fun onActionItemClicked(controller: ListSelectionController, mode: ActionMode?, item: MenuItem): Boolean { return when (item.itemId) { R.id.action_save -> { - viewModel.download(controller.snapshot()) + val snapshot = controller.snapshot() mode?.finish() + commonAlertDialogs.askForDownloadOverMeteredNetwork( + context = recyclerView.context, + onConfirmed = { viewModel.download(snapshot, it) }, + ) true } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/DownloadDialogFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/DownloadDialogFragment.kt index 007902926..e0f6cee7c 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/DownloadDialogFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/DownloadDialogFragment.kt @@ -22,6 +22,7 @@ import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga import org.koitharu.kotatsu.core.prefs.DownloadFormat import org.koitharu.kotatsu.core.ui.AlertDialogFragment +import org.koitharu.kotatsu.core.ui.dialog.CommonAlertDialogs import org.koitharu.kotatsu.core.ui.widgets.TwoLinesItemView import org.koitharu.kotatsu.core.util.ext.findActivity import org.koitharu.kotatsu.core.util.ext.getDisplayMessage @@ -39,6 +40,7 @@ import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.util.format import org.koitharu.kotatsu.settings.storage.DirectoryModel +import javax.inject.Inject @AndroidEntryPoint class DownloadDialogFragment : AlertDialogFragment(), View.OnClickListener { @@ -46,6 +48,9 @@ class DownloadDialogFragment : AlertDialogFragment(), Vie private val viewModel by viewModels() private var optionViews: Array? = null + @Inject + lateinit var commonAlertDialogs: CommonAlertDialogs + override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?) = DialogDownloadBinding.inflate(inflater, container, false) @@ -104,21 +109,10 @@ class DownloadDialogFragment : AlertDialogFragment(), Vie override fun onClick(v: View) { when (v.id) { R.id.button_cancel -> dialog?.cancel() - R.id.button_confirm -> viewBinding?.run { - val options = viewModel.chaptersSelectOptions.value - viewModel.confirm( - startNow = switchStart.isChecked, - chaptersMacro = when { - optionWholeManga.isChecked -> options.wholeManga - optionWholeBranch.isChecked -> options.wholeBranch ?: return@run - optionFirstChapters.isChecked -> options.firstChapters ?: return@run - optionUnreadChapters.isChecked -> options.unreadChapters ?: return@run - else -> return@run - }, - format = DownloadFormat.entries.getOrNull(spinnerFormat.selectedItemPosition), - destination = viewModel.availableDestinations.value.getOrNull(spinnerDestination.selectedItemPosition), - ) - } + R.id.button_confirm -> commonAlertDialogs.askForDownloadOverMeteredNetwork( + context = context ?: return, + onConfirmed = ::schedule, + ) R.id.textView_more -> { val binding = viewBinding ?: return @@ -138,6 +132,25 @@ class DownloadDialogFragment : AlertDialogFragment(), Vie } } + private fun schedule(allowMeteredNetwork: Boolean) { + viewBinding?.run { + val options = viewModel.chaptersSelectOptions.value + viewModel.confirm( + startNow = switchStart.isChecked, + chaptersMacro = when { + optionWholeManga.isChecked -> options.wholeManga + optionWholeBranch.isChecked -> options.wholeBranch ?: return@run + optionFirstChapters.isChecked -> options.firstChapters ?: return@run + optionUnreadChapters.isChecked -> options.unreadChapters ?: return@run + else -> return@run + }, + format = DownloadFormat.entries.getOrNull(spinnerFormat.selectedItemPosition), + destination = viewModel.availableDestinations.value.getOrNull(spinnerDestination.selectedItemPosition), + allowMetered = allowMeteredNetwork, + ) + } + } + private fun onError(e: Throwable) { MaterialAlertDialogBuilder(context ?: return) .setNegativeButton(R.string.close, null) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/DownloadDialogViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/DownloadDialogViewModel.kt index f8d8e3b77..a385a0515 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/DownloadDialogViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/DownloadDialogViewModel.kt @@ -57,7 +57,6 @@ class DownloadDialogViewModel @Inject constructor( }.awaitAll() } } - val onScheduled = MutableEventFlow() val defaultFormat = MutableStateFlow(null) val availableDestinations = MutableStateFlow(listOf(defaultDestination())) @@ -90,6 +89,7 @@ class DownloadDialogViewModel @Inject constructor( chaptersMacro: ChaptersSelectMacro, format: DownloadFormat?, destination: DirectoryModel?, + allowMetered: Boolean, ) { launchLoadingJob(Dispatchers.Default) { val tasks = mangaDetails.get().map { m -> @@ -102,6 +102,7 @@ class DownloadDialogViewModel @Inject constructor( chaptersIds = chaptersMacro.getChaptersIds(m.id, chapters)?.toLongArray(), destination = destination?.file, format = format, + allowMeteredNetwork = allowMetered, ) } scheduler.schedule(tasks) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadTask.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadTask.kt index 8f6edcc76..f74a67c6e 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadTask.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadTask.kt @@ -15,6 +15,7 @@ class DownloadTask( val chaptersIds: LongArray?, val destination: File?, val format: DownloadFormat?, + val allowMeteredNetwork: Boolean, ) : Parcelable { constructor(data: Data) : this( @@ -24,6 +25,7 @@ class DownloadTask( chaptersIds = data.getLongArray(CHAPTERS)?.takeUnless(LongArray::isEmpty), destination = data.getString(DESTINATION)?.let { File(it) }, format = data.getString(FORMAT)?.let { DownloadFormat.entries.find(it) }, + allowMeteredNetwork = data.getBoolean(ALLOW_METERED, true), ) fun toData(): Data = Data.Builder() @@ -47,6 +49,7 @@ class DownloadTask( if (!(chaptersIds contentEquals other.chaptersIds)) return false if (destination != other.destination) return false if (format != other.format) return false + if (allowMeteredNetwork != other.allowMeteredNetwork) return false return true } @@ -58,6 +61,7 @@ class DownloadTask( result = 31 * result + (chaptersIds?.contentHashCode() ?: 0) result = 31 * result + (destination?.hashCode() ?: 0) result = 31 * result + (format?.hashCode() ?: 0) + result = 31 * result + allowMeteredNetwork.hashCode() return result } @@ -69,5 +73,6 @@ class DownloadTask( const val CHAPTERS = "chapters" const val DESTINATION = "dest" const val FORMAT = "format" + const val ALLOW_METERED = "metered" } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt index 73c7bd556..58f0a8d39 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt @@ -434,48 +434,8 @@ class DownloadWorker @AssistedInject constructor( class Scheduler @Inject constructor( @ApplicationContext private val context: Context, private val workManager: WorkManager, - private val dataRepository: MangaDataRepository, - private val settings: AppSettings, ) { - @Deprecated("") - suspend fun schedule( - manga: Manga, - chaptersIds: Set?, - isPaused: Boolean, - isSilent: Boolean, - ) { - dataRepository.storeManga(manga) - val task = DownloadTask( - mangaId = manga.id, - isPaused = isPaused, - isSilent = isSilent, - chaptersIds = chaptersIds?.toLongArray(), - destination = null, - format = null, - ) - schedule(listOf(task)) - } - - @Deprecated("") - suspend fun schedule( - manga: Collection, - isPaused: Boolean, - ) { - val tasks = manga.map { - dataRepository.storeManga(it) - DownloadTask( - mangaId = it.id, - isPaused = isPaused, - isSilent = false, - chaptersIds = null, - destination = null, - format = null, - ) - } - schedule(tasks) - } - fun observeWorks(): Flow> = workManager .getWorkInfosByTagFlow(TAG) @@ -531,8 +491,8 @@ class DownloadWorker @AssistedInject constructor( workManager.deleteWorks(finishedWorks.mapToSet { it.id }) } - suspend fun updateConstraints() { - val constraints = createConstraints() + suspend fun updateConstraints(allowMeteredNetwork: Boolean) { + val constraints = createConstraints(allowMeteredNetwork) val works = workManager.awaitWorkInfosByTag(TAG) for (work in works) { if (work.state.isFinished) { @@ -551,10 +511,9 @@ class DownloadWorker @AssistedInject constructor( if (tasks.isEmpty()) { return } - val constraints = createConstraints() val requests = tasks.map { task -> OneTimeWorkRequestBuilder() - .setConstraints(constraints) + .setConstraints(createConstraints(task.allowMeteredNetwork)) .addTag(TAG) .keepResultsForAtLeast(30, TimeUnit.DAYS) .setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.SECONDS) @@ -565,8 +524,8 @@ class DownloadWorker @AssistedInject constructor( workManager.enqueue(requests).await() } - private fun createConstraints() = Constraints.Builder() - .setRequiredNetworkType(if (settings.isDownloadsWiFiOnly) NetworkType.UNMETERED else NetworkType.CONNECTED) + private fun createConstraints(allowMeteredNetwork: Boolean) = Constraints.Builder() + .setRequiredNetworkType(if (allowMeteredNetwork) NetworkType.CONNECTED else NetworkType.UNMETERED) .build() } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt index 8313db521..ac44169a7 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt @@ -26,7 +26,7 @@ import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver import org.koitharu.kotatsu.core.model.LocalMangaSource import org.koitharu.kotatsu.core.parser.external.ExternalMangaSource import org.koitharu.kotatsu.core.ui.BaseFragment -import org.koitharu.kotatsu.core.ui.dialog.TwoButtonsAlertDialog +import org.koitharu.kotatsu.core.ui.dialog.BigButtonsAlertDialog import org.koitharu.kotatsu.core.ui.list.ListSelectionController import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.util.RecyclerViewOwner @@ -250,7 +250,7 @@ class ExploreFragment : val listener = DialogInterface.OnClickListener { _, which -> viewModel.respondSuggestionTip(which == DialogInterface.BUTTON_POSITIVE) } - TwoButtonsAlertDialog.Builder(requireContext()) + BigButtonsAlertDialog.Builder(requireContext()) .setIcon(R.drawable.ic_suggestion) .setTitle(R.string.suggestions_enable_prompt) .setPositiveButton(R.string.enable, listener) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/SearchActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/SearchActivity.kt index 095f817f6..781dc822f 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/SearchActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/SearchActivity.kt @@ -17,7 +17,6 @@ import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.ui.BaseActivity -import org.koitharu.kotatsu.core.ui.dialog.CommonAlertDialogs import org.koitharu.kotatsu.core.ui.list.ListSelectionController import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.widgets.TipView @@ -27,7 +26,7 @@ import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.databinding.ActivitySearchBinding import org.koitharu.kotatsu.details.ui.DetailsActivity -import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver +import org.koitharu.kotatsu.download.ui.dialog.DownloadDialogFragment import org.koitharu.kotatsu.favourites.ui.categories.select.FavoriteSheet import org.koitharu.kotatsu.list.domain.ListFilterOption import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration @@ -99,7 +98,8 @@ class SearchActivity : viewModel.list.observe(this, adapter) viewModel.onError.observeEvent(this, SnackbarErrorObserver(viewBinding.recyclerView, null)) - viewModel.onDownloadStarted.observeEvent(this, DownloadStartedObserver(viewBinding.recyclerView)) + + DownloadDialogFragment.registerCallback(this, viewBinding.recyclerView) } override fun onWindowInsetsChanged(insets: Insets) { @@ -185,11 +185,8 @@ class SearchActivity : } R.id.action_save -> { - val itemsSnapshot = collectSelectedItems() - CommonAlertDialogs.showDownloadConfirmation(this) { startPaused -> - mode?.finish() - viewModel.download(itemsSnapshot, isPaused = startPaused) - } + DownloadDialogFragment.show(supportFragmentManager, collectSelectedItems()) + mode?.finish() true } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/SearchViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/SearchViewModel.kt index c0d3d52aa..c4cb96024 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/SearchViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/SearchViewModel.kt @@ -28,10 +28,7 @@ import org.koitharu.kotatsu.core.model.UnknownMangaSource import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.core.ui.BaseViewModel -import org.koitharu.kotatsu.core.util.ext.MutableEventFlow -import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug -import org.koitharu.kotatsu.download.ui.worker.DownloadWorker import org.koitharu.kotatsu.explore.data.MangaSourcesRepository import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.history.data.HistoryRepository @@ -55,14 +52,12 @@ class SearchViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private val mangaListMapper: MangaListMapper, private val mangaRepositoryFactory: MangaRepository.Factory, - private val downloadScheduler: DownloadWorker.Scheduler, private val sourcesRepository: MangaSourcesRepository, private val historyRepository: HistoryRepository, private val localMangaRepository: LocalMangaRepository, private val favouritesRepository: FavouritesRepository, ) : BaseViewModel() { - val onDownloadStarted = MutableEventFlow() val query = savedStateHandle.get(SearchActivity.EXTRA_QUERY).orEmpty() private val retryCounter = MutableStateFlow(0) @@ -109,13 +104,6 @@ class SearchViewModel @Inject constructor( retryCounter.value += 1 } - fun download(items: Set, isPaused: Boolean) { - launchJob(Dispatchers.Default) { - downloadScheduler.schedule(items, isPaused) - onDownloadStarted.call(Unit) - } - } - @CheckResult private fun searchImpl(q: String): Flow> = channelFlow { searchHistory(q)?.let { send(it) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/DownloadsSettingsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/DownloadsSettingsFragment.kt index ffcb1e10f..eece51d8f 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/DownloadsSettingsFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/DownloadsSettingsFragment.kt @@ -17,6 +17,7 @@ import kotlinx.coroutines.withContext import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.DownloadFormat +import org.koitharu.kotatsu.core.prefs.TriStateOption import org.koitharu.kotatsu.core.ui.BasePreferenceFragment import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.resolveFile @@ -54,6 +55,10 @@ class DownloadsSettingsFragment : entryValues = DownloadFormat.entries.names() setDefaultValueCompat(DownloadFormat.AUTOMATIC.name) } + findPreference(AppSettings.KEY_DOWNLOADS_METERED_NETWORK)?.run { + entryValues = TriStateOption.entries.names() + setDefaultValueCompat(TriStateOption.ASK.name) + } dozeHelper.updatePreference() } @@ -80,7 +85,7 @@ class DownloadsSettingsFragment : findPreference(key)?.bindDirectoriesCount() } - AppSettings.KEY_DOWNLOADS_WIFI -> { + AppSettings.KEY_DOWNLOADS_METERED_NETWORK -> { updateDownloadsConstraints() } @@ -156,12 +161,17 @@ class DownloadsSettingsFragment : } private fun updateDownloadsConstraints() { - val preference = findPreference(AppSettings.KEY_DOWNLOADS_WIFI) + val preference = findPreference(AppSettings.KEY_DOWNLOADS_METERED_NETWORK) viewLifecycleScope.launch { try { preference?.isEnabled = false withContext(Dispatchers.Default) { - downloadsScheduler.updateConstraints() + val option = when (settings.allowDownloadOnMeteredNetwork) { + TriStateOption.ENABLED -> true + TriStateOption.ASK -> return@withContext + TriStateOption.DISABLED -> false + } + downloadsScheduler.updateConstraints(option) } } catch (e: Exception) { e.printStackTraceDebug() diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/work/TrackWorker.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/work/TrackWorker.kt index 4b2d3b9c1..f8fe0f4f3 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/work/TrackWorker.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/work/TrackWorker.kt @@ -45,16 +45,18 @@ import org.koitharu.kotatsu.R import org.koitharu.kotatsu.browser.cloudflare.CaptchaNotifier import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException +import org.koitharu.kotatsu.core.model.ids import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.TrackerDownloadStrategy +import org.koitharu.kotatsu.core.prefs.TriStateOption import org.koitharu.kotatsu.core.util.ext.awaitUniqueWorkInfoByName import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission import org.koitharu.kotatsu.core.util.ext.onEachIndexed import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.trySetForeground +import org.koitharu.kotatsu.download.ui.worker.DownloadTask import org.koitharu.kotatsu.download.ui.worker.DownloadWorker import org.koitharu.kotatsu.local.data.LocalMangaRepository -import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.toIntUp import org.koitharu.kotatsu.settings.SettingsActivity @@ -251,12 +253,16 @@ class TrackWorker @AssistedInject constructor( TrackerDownloadStrategy.DOWNLOADED -> { val localManga = localRepositoryLazy.get().findSavedManga(mangaUpdates.manga) if (localManga != null) { - downloadSchedulerLazy.get().schedule( - manga = mangaUpdates.manga, - chaptersIds = mangaUpdates.newChapters.mapToSet { it.id }, + val task = DownloadTask( + mangaId = mangaUpdates.manga.id, isPaused = false, - isSilent = true, + isSilent = false, + chaptersIds = mangaUpdates.newChapters.ids().toLongArray(), + destination = null, + format = null, + allowMeteredNetwork = settings.allowDownloadOnMeteredNetwork != TriStateOption.DISABLED, ) + downloadSchedulerLazy.get().schedule(setOf(task)) } } } diff --git a/app/src/main/res/drawable/ic_network_cellular.xml b/app/src/main/res/drawable/ic_network_cellular.xml new file mode 100644 index 000000000..45146079b --- /dev/null +++ b/app/src/main/res/drawable/ic_network_cellular.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/layout/dialog_two_buttons.xml b/app/src/main/res/layout/dialog_two_buttons.xml index 366e96301..ca17c410a 100644 --- a/app/src/main/res/layout/dialog_two_buttons.xml +++ b/app/src/main/res/layout/dialog_two_buttons.xml @@ -36,7 +36,7 @@ android:minHeight="62dp" android:visibility="gone" app:shapeAppearance="?shapeAppearanceCornerMedium" - app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Material3.Corner.Top" + tools:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Material3.Corner.Top" tools:text="Enable" tools:visibility="visible" /> @@ -48,7 +48,19 @@ android:minHeight="62dp" android:visibility="gone" app:shapeAppearance="?shapeAppearanceCornerMedium" - app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Material3.Corner.Bottom" + tools:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Material3.Corner.None" + tools:text="Ask every time" + tools:visibility="visible" /> + + diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 4a397a422..da63ca086 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -113,4 +113,9 @@ @string/never @string/manga_with_downloaded_chapters + + @string/allow_always + @string/ask_every_time + @string/dont_allow + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f7c6437e1..e4184c482 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -748,4 +748,10 @@ You can select chapters to download by long click on item in the chapter list. All + Downloading over cellular network + Allow downloads over cellular network? + Don\'t allow + Allow always + Allow once + Ask every time diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 81a046d3d..9fa061b69 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -266,6 +266,13 @@ 50% + +