From 11fc8b66422588f702ecadfb28039c921e66ff1c Mon Sep 17 00:00:00 2001 From: Koitharu Date: Thu, 5 May 2022 13:44:26 +0300 Subject: [PATCH 1/7] Configure manga tracker for each favourite category --- .../kotatsu/core/backup/BackupRepository.kt | 1 + .../kotatsu/core/backup/RestoreRepository.kt | 1 + .../kotatsu/core/db/DatabaseModule.kt | 2 +- .../koitharu/kotatsu/core/db/MangaDatabase.kt | 40 +++-- .../koitharu/kotatsu/core/db/dao/TracksDao.kt | 3 + .../core/db/migrations/Migration9To10.kt | 11 ++ .../kotatsu/core/github/GithubModule.kt | 2 +- .../kotatsu/core/model/FavouriteCategory.kt | 3 +- .../kotatsu/core/prefs/AppSettings.kt | 9 +- .../kotatsu/favourites/FavouritesModule.kt | 2 +- .../kotatsu/favourites/data/EntityMapping.kt | 1 + .../favourites/data/FavouriteCategoriesDao.kt | 3 + .../data/FavouriteCategoryEntity.kt | 1 + .../kotatsu/favourites/data/FavouritesDao.kt | 3 + .../favourites/domain/FavouritesRepository.kt | 17 ++- .../ui/categories/CategoriesActivity.kt | 12 +- .../FavouritesCategoriesViewModel.kt | 11 +- .../koitharu/kotatsu/history/HistoryModule.kt | 2 +- .../org/koitharu/kotatsu/local/LocalModule.kt | 2 +- .../org/koitharu/kotatsu/main/MainModule.kt | 2 +- .../koitharu/kotatsu/reader/ReaderModule.kt | 2 +- .../koitharu/kotatsu/search/SearchModule.kt | 3 +- .../NotificationSettingsLegacyFragment.kt | 33 +++- .../kotatsu/settings/SettingsModule.kt | 4 +- .../settings/TrackerSettingsFragment.kt | 86 ++++++++++- .../kotatsu/settings/backup/AppBackupAgent.kt | 4 +- .../koitharu/kotatsu/tracker/TrackerModule.kt | 5 +- .../tracker/domain/TrackingRepository.kt | 41 +++-- .../kotatsu/tracker/work/TrackWorker.kt | 105 ++++++++----- .../work/TrackerNotificationChannels.kt | 143 ++++++++++++++++++ .../kotatsu/tracker/work/TrackingItem.kt | 31 ++++ .../res/layout/preference_toggle_header.xml | 64 ++++++++ app/src/main/res/menu/popup_category.xml | 5 + app/src/main/res/values-be/strings.xml | 1 - app/src/main/res/values-de/strings.xml | 1 - app/src/main/res/values-es/strings.xml | 1 - app/src/main/res/values-fi/strings.xml | 1 - app/src/main/res/values-fr/strings.xml | 1 - app/src/main/res/values-it/strings.xml | 1 - app/src/main/res/values-ja/strings.xml | 1 - app/src/main/res/values-nb-rNO/strings.xml | 1 - app/src/main/res/values-pt-rBR/strings.xml | 1 - app/src/main/res/values-pt/strings.xml | 1 - app/src/main/res/values-ru/strings.xml | 5 +- app/src/main/res/values-sv/strings.xml | 1 - app/src/main/res/values-tr/strings.xml | 1 - app/src/main/res/values/strings.xml | 5 +- app/src/main/res/xml/pref_notifications.xml | 21 ++- app/src/main/res/xml/pref_suggestions.xml | 2 +- app/src/main/res/xml/pref_tracker.xml | 17 ++- 50 files changed, 584 insertions(+), 132 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration9To10.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackerNotificationChannels.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackingItem.kt create mode 100644 app/src/main/res/layout/preference_toggle_header.xml diff --git a/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupRepository.kt index 3d2f668a6..4b42b4c60 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupRepository.kt @@ -121,6 +121,7 @@ class BackupRepository(private val db: MangaDatabase) { jo.put("sort_key", sortKey) jo.put("title", title) jo.put("order", order) + jo.put("track", track) return jo } diff --git a/app/src/main/java/org/koitharu/kotatsu/core/backup/RestoreRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/backup/RestoreRepository.kt index ba4a8b6a3..57fd4d6ee 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/backup/RestoreRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/backup/RestoreRepository.kt @@ -104,6 +104,7 @@ class RestoreRepository(private val db: MangaDatabase) { sortKey = json.getInt("sort_key"), title = json.getString("title"), order = json.getStringOrNull("order") ?: SortOrder.NEWEST.name, + track = json.getBooleanOrDefault("track", true), ) private fun parseFavourite(json: JSONObject) = FavouriteEntity( diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/DatabaseModule.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/DatabaseModule.kt index dadbb05eb..215d02259 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/DatabaseModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/DatabaseModule.kt @@ -5,5 +5,5 @@ import org.koin.dsl.module val databaseModule get() = module { - single { MangaDatabase.create(androidContext()) } + single { MangaDatabase(androidContext()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt index 010a2653f..436455014 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt @@ -22,7 +22,7 @@ import org.koitharu.kotatsu.suggestions.data.SuggestionEntity FavouriteCategoryEntity::class, FavouriteEntity::class, MangaPrefsEntity::class, TrackEntity::class, TrackLogEntity::class, SuggestionEntity::class ], - version = 9 + version = 10 ) abstract class MangaDatabase : RoomDatabase() { @@ -43,24 +43,22 @@ abstract class MangaDatabase : RoomDatabase() { abstract val trackLogsDao: TrackLogsDao abstract val suggestionDao: SuggestionDao +} - companion object { - - fun create(context: Context): MangaDatabase = Room.databaseBuilder( - context, - MangaDatabase::class.java, - "kotatsu-db" - ).addMigrations( - Migration1To2(), - Migration2To3(), - Migration3To4(), - Migration4To5(), - Migration5To6(), - Migration6To7(), - Migration7To8(), - Migration8To9(), - ).addCallback( - DatabasePrePopulateCallback(context.resources) - ).build() - } -} \ No newline at end of file +fun MangaDatabase(context: Context): MangaDatabase = Room.databaseBuilder( + context, + MangaDatabase::class.java, + "kotatsu-db" +).addMigrations( + Migration1To2(), + Migration2To3(), + Migration3To4(), + Migration4To5(), + Migration5To6(), + Migration6To7(), + Migration7To8(), + Migration8To9(), + Migration9To10(), +).addCallback( + DatabasePrePopulateCallback(context.resources) +).build() \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TracksDao.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TracksDao.kt index 4bd188966..f8352524b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TracksDao.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TracksDao.kt @@ -10,6 +10,9 @@ abstract class TracksDao { @Query("SELECT * FROM tracks") abstract suspend fun findAll(): List + @Query("SELECT * FROM tracks WHERE manga_id IN (:ids)") + abstract suspend fun findAll(ids: Collection): List + @Query("SELECT * FROM tracks WHERE manga_id = :mangaId") abstract suspend fun find(mangaId: Long): TrackEntity? diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration9To10.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration9To10.kt new file mode 100644 index 000000000..59cba96ef --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration9To10.kt @@ -0,0 +1,11 @@ +package org.koitharu.kotatsu.core.db.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration9To10 : Migration(9, 10) { + + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE favourite_categories ADD COLUMN `track` INTEGER NOT NULL DEFAULT 1") + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/github/GithubModule.kt b/app/src/main/java/org/koitharu/kotatsu/core/github/GithubModule.kt index de5256337..7da9e309f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/github/GithubModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/github/GithubModule.kt @@ -4,7 +4,7 @@ import org.koin.dsl.module val githubModule get() = module { - single { + factory { GithubRepository(get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/FavouriteCategory.kt b/app/src/main/java/org/koitharu/kotatsu/core/model/FavouriteCategory.kt index 655a0eb08..798ec2fbd 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/model/FavouriteCategory.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/model/FavouriteCategory.kt @@ -1,9 +1,9 @@ package org.koitharu.kotatsu.core.model import android.os.Parcelable +import java.util.* import kotlinx.parcelize.Parcelize import org.koitharu.kotatsu.parsers.model.SortOrder -import java.util.* @Parcelize data class FavouriteCategory( @@ -12,4 +12,5 @@ data class FavouriteCategory( val sortKey: Int, val order: SortOrder, val createdAt: Date, + val isTrackingEnabled: Boolean, ) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt index d9178713c..185336542 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -1,5 +1,6 @@ package org.koitharu.kotatsu.core.prefs +import android.annotation.TargetApi import android.content.Context import android.content.SharedPreferences import android.net.ConnectivityManager @@ -78,7 +79,10 @@ class AppSettings(context: Context) { get() = prefs.getLong(KEY_APP_UPDATE, 0L) set(value) = prefs.edit { putLong(KEY_APP_UPDATE, value) } - val trackerNotifications: Boolean + val isTrackerEnabled: Boolean + get() = prefs.getBoolean(KEY_TRACKER_ENABLED, true) + + val isTrackerNotificationsEnabled: Boolean get() = prefs.getBoolean(KEY_TRACKER_NOTIFICATIONS, true) var notificationSound: Uri @@ -269,13 +273,16 @@ class AppSettings(context: Context) { const val KEY_REMOTE_SOURCES = "remote_sources" const val KEY_LOCAL_STORAGE = "local_storage" const val KEY_READER_SWITCHERS = "reader_switchers" + const val KEY_TRACKER_ENABLED = "tracker_enabled" const val KEY_TRACK_SOURCES = "track_sources" + const val KEY_TRACK_CATEGORIES = "track_categories" const val KEY_TRACK_WARNING = "track_warning" const val KEY_TRACKER_NOTIFICATIONS = "tracker_notifications" const val KEY_NOTIFICATIONS_SETTINGS = "notifications_settings" const val KEY_NOTIFICATIONS_SOUND = "notifications_sound" const val KEY_NOTIFICATIONS_VIBRATE = "notifications_vibrate" const val KEY_NOTIFICATIONS_LIGHT = "notifications_light" + const val KEY_NOTIFICATIONS_INFO = "tracker_notifications_info" const val KEY_READER_ANIMATION = "reader_animation" const val KEY_READER_PREFER_RTL = "reader_prefer_rtl" const val KEY_APP_PASSWORD = "app_password" diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/FavouritesModule.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/FavouritesModule.kt index 8222c0f2b..0f10b81ac 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/FavouritesModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/FavouritesModule.kt @@ -10,7 +10,7 @@ import org.koitharu.kotatsu.favourites.ui.list.FavouritesListViewModel val favouritesModule get() = module { - single { FavouritesRepository(get()) } + factory { FavouritesRepository(get(), get()) } viewModel { categoryId -> FavouritesListViewModel(categoryId.get(), get(), get(), get()) diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/data/EntityMapping.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/data/EntityMapping.kt index 801f2566a..c6a65c78e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/data/EntityMapping.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/data/EntityMapping.kt @@ -11,4 +11,5 @@ fun FavouriteCategoryEntity.toFavouriteCategory(id: Long = categoryId.toLong()) sortKey = sortKey, order = SortOrder(order, SortOrder.NEWEST), createdAt = Date(createdAt), + isTrackingEnabled = track, ) \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoriesDao.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoriesDao.kt index 436dc12ea..65d429706 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoriesDao.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoriesDao.kt @@ -30,6 +30,9 @@ abstract class FavouriteCategoriesDao { @Query("UPDATE favourite_categories SET `order` = :order WHERE category_id = :id") abstract suspend fun updateOrder(id: Long, order: String) + @Query("UPDATE favourite_categories SET `track` = :isEnabled WHERE category_id = :id") + abstract suspend fun updateTracking(id: Long, isEnabled: Boolean) + @Query("UPDATE favourite_categories SET sort_key = :sortKey WHERE category_id = :id") abstract suspend fun updateSortKey(id: Long, sortKey: Int) diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoryEntity.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoryEntity.kt index d2b0bc7ed..5fe02f019 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoryEntity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoryEntity.kt @@ -12,4 +12,5 @@ class FavouriteCategoryEntity( @ColumnInfo(name = "sort_key") val sortKey: Int, @ColumnInfo(name = "title") val title: String, @ColumnInfo(name = "order") val order: String, + @ColumnInfo(name = "track") val track: Boolean, ) \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt index 9e5da45f7..89fcc92fb 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt @@ -43,6 +43,9 @@ abstract class FavouritesDao { @Query("SELECT * FROM favourites WHERE category_id = :categoryId GROUP BY manga_id ORDER BY created_at DESC LIMIT :limit OFFSET :offset") abstract suspend fun findAll(categoryId: Long, offset: Int, limit: Int): List + @Query("SELECT * FROM manga WHERE manga_id IN (SELECT manga_id FROM favourites WHERE category_id = :categoryId)") + abstract suspend fun findAllManga(categoryId: Int): List + @Query("SELECT * FROM manga WHERE manga_id IN (SELECT manga_id FROM favourites)") abstract suspend fun findAllManga(): List diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt index 731d7ff5e..35a362e95 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt @@ -13,9 +13,13 @@ import org.koitharu.kotatsu.favourites.data.FavouriteEntity import org.koitharu.kotatsu.favourites.data.toFavouriteCategory import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.SortOrder +import org.koitharu.kotatsu.tracker.work.TrackerNotificationChannels import org.koitharu.kotatsu.utils.ext.mapItems -class FavouritesRepository(private val db: MangaDatabase) { +class FavouritesRepository( + private val db: MangaDatabase, + private val channels: TrackerNotificationChannels, +) { suspend fun getAllManga(): List { val entities = db.favouritesDao.findAll() @@ -65,23 +69,32 @@ class FavouritesRepository(private val db: MangaDatabase) { sortKey = db.favouriteCategoriesDao.getNextSortKey(), categoryId = 0, order = SortOrder.NEWEST.name, + track = true, ) val id = db.favouriteCategoriesDao.insert(entity) - return entity.toFavouriteCategory(id) + val category = entity.toFavouriteCategory(id) + channels.createChannel(category) + return category } suspend fun renameCategory(id: Long, title: String) { db.favouriteCategoriesDao.updateTitle(id, title) + channels.renameChannel(id, title) } suspend fun removeCategory(id: Long) { db.favouriteCategoriesDao.delete(id) + channels.deleteChannel(id) } suspend fun setCategoryOrder(id: Long, order: SortOrder) { db.favouriteCategoriesDao.updateOrder(id, order.name) } + suspend fun setCategoryTracking(id: Long, isEnabled: Boolean) { + db.favouriteCategoriesDao.updateTracking(id, isEnabled) + } + suspend fun reorderCategories(orderedIds: List) { val dao = db.favouriteCategoriesDao db.withTransaction { diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesActivity.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesActivity.kt index 5a2eaf8df..d2615c826 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesActivity.kt @@ -30,7 +30,8 @@ class CategoriesActivity : BaseActivity(), OnListItemClickListener, View.OnClickListener, - CategoriesEditDelegate.CategoriesEditCallback, AllCategoriesToggleListener { + CategoriesEditDelegate.CategoriesEditCallback, + AllCategoriesToggleListener { private val viewModel by viewModel() @@ -63,11 +64,12 @@ class CategoriesActivity : override fun onItemClick(item: FavouriteCategory, view: View) { val menu = PopupMenu(view.context, view) menu.inflate(R.menu.popup_category) - createOrderSubmenu(menu.menu, item) + prepareCategoryMenu(menu.menu, item) menu.setOnMenuItemClickListener { menuItem -> when (menuItem.itemId) { R.id.action_remove -> editDelegate.deleteCategory(item) R.id.action_rename -> editDelegate.renameCategory(item) + R.id.action_tracking -> viewModel.setCategoryTracking(item.id, !item.isTrackingEnabled) R.id.action_order -> return@setOnMenuItemClickListener false else -> { val order = SORT_ORDERS.getOrNull(menuItem.order) ?: return@setOnMenuItemClickListener false @@ -124,7 +126,7 @@ class CategoriesActivity : viewModel.createCategory(name) } - private fun createOrderSubmenu(menu: Menu, category: FavouriteCategory) { + private fun prepareCategoryMenu(menu: Menu, category: FavouriteCategory) { val submenu = menu.findItem(R.id.action_order)?.subMenu ?: return for ((i, item) in SORT_ORDERS.withIndex()) { val menuItem = submenu.add( @@ -137,6 +139,10 @@ class CategoriesActivity : menuItem.isChecked = item == category.order } submenu.setGroupCheckable(R.id.group_order, true, true) + menu.findItem(R.id.action_tracking)?.run { + isVisible = viewModel.isFavouritesTrackerEnabled + isChecked = category.isTrackingEnabled + } } private inner class ReorderHelperCallback : ItemTouchHelper.SimpleCallback( diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt index 7aac74e62..9e84a9d89 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt @@ -1,6 +1,7 @@ package org.koitharu.kotatsu.favourites.ui.categories import androidx.lifecycle.viewModelScope +import java.util.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.* @@ -11,7 +12,6 @@ import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct -import java.util.* class FavouritesCategoriesViewModel( private val repository: FavouritesRepository, @@ -34,6 +34,9 @@ class FavouritesCategoriesViewModel( mapCategories(list, showAll, showAll) }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default) + val isFavouritesTrackerEnabled: Boolean + get() = settings.isTrackerEnabled && AppSettings.TRACK_FAVOURITES in settings.trackSources + fun createCategory(name: String) { launchJob { repository.addCategory(name) @@ -58,6 +61,12 @@ class FavouritesCategoriesViewModel( } } + fun setCategoryTracking(id: Long, isEnabled: Boolean) { + launchJob { + repository.setCategoryTracking(id, isEnabled) + } + } + fun setAllCategoriesVisible(isVisible: Boolean) { settings.isAllFavouritesVisible = isVisible } diff --git a/app/src/main/java/org/koitharu/kotatsu/history/HistoryModule.kt b/app/src/main/java/org/koitharu/kotatsu/history/HistoryModule.kt index 74fb0ef40..246cb3a5f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/HistoryModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/HistoryModule.kt @@ -8,6 +8,6 @@ import org.koitharu.kotatsu.history.ui.HistoryListViewModel val historyModule get() = module { - single { HistoryRepository(get(), get(), get()) } + factory { HistoryRepository(get(), get(), get()) } viewModel { HistoryListViewModel(get(), get(), get(), get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/local/LocalModule.kt b/app/src/main/java/org/koitharu/kotatsu/local/LocalModule.kt index 3366248a4..f8cb28657 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/LocalModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/LocalModule.kt @@ -11,7 +11,7 @@ import org.koitharu.kotatsu.local.ui.LocalListViewModel val localModule get() = module { - single { LocalStorageManager(androidContext(), get()) } + factory { LocalStorageManager(androidContext(), get()) } single { LocalMangaRepository(get()) } factory { DownloadManager.Factory(androidContext(), get(), get(), get(), get(), get()) } diff --git a/app/src/main/java/org/koitharu/kotatsu/main/MainModule.kt b/app/src/main/java/org/koitharu/kotatsu/main/MainModule.kt index e8c69d824..c6e11107b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/MainModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/main/MainModule.kt @@ -11,7 +11,7 @@ import org.koitharu.kotatsu.main.ui.protect.ProtectViewModel val mainModule get() = module { single { AppProtectHelper(get()) } - single { ShortcutsRepository(androidContext(), get(), get(), get()) } + factory { ShortcutsRepository(androidContext(), get(), get(), get()) } viewModel { MainViewModel(get(), get()) } viewModel { ProtectViewModel(get(), get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ReaderModule.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ReaderModule.kt index f27f061c6..c83ad608b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ReaderModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ReaderModule.kt @@ -11,7 +11,7 @@ import org.koitharu.kotatsu.reader.ui.ReaderViewModel val readerModule get() = module { - single { MangaDataRepository(get()) } + factory { MangaDataRepository(get()) } single { PagesCache(get()) } factory { PageSaveHelper(get(), androidContext()) } diff --git a/app/src/main/java/org/koitharu/kotatsu/search/SearchModule.kt b/app/src/main/java/org/koitharu/kotatsu/search/SearchModule.kt index 1f14c6ee3..1d1fb43fc 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/SearchModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/search/SearchModule.kt @@ -13,8 +13,7 @@ import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel val searchModule get() = module { - single { MangaSearchRepository(get(), get(), androidContext(), get()) } - + factory { MangaSearchRepository(get(), get(), androidContext(), get()) } factory { MangaSuggestionsProvider.createSuggestions(androidContext()) } viewModel { params -> diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/NotificationSettingsLegacyFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/NotificationSettingsLegacyFragment.kt index b8852f47e..ee15c796a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/NotificationSettingsLegacyFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/NotificationSettingsLegacyFragment.kt @@ -1,6 +1,7 @@ package org.koitharu.kotatsu.settings import android.content.Context +import android.content.SharedPreferences import android.media.RingtoneManager import android.os.Bundle import android.view.View @@ -11,7 +12,9 @@ import org.koitharu.kotatsu.base.ui.BasePreferenceFragment import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.settings.utils.RingtonePickContract -class NotificationSettingsLegacyFragment : BasePreferenceFragment(R.string.notifications) { +class NotificationSettingsLegacyFragment : + BasePreferenceFragment(R.string.notifications), + SharedPreferences.OnSharedPreferenceChangeListener { private val ringtonePickContract = registerForActivityResult( RingtonePickContract(get().getString(R.string.notification_sound)) @@ -25,15 +28,28 @@ class NotificationSettingsLegacyFragment : BasePreferenceFragment(R.string.notif override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.pref_notifications) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) findPreference(AppSettings.KEY_NOTIFICATIONS_SOUND)?.run { val uri = settings.notificationSound summary = RingtoneManager.getRingtone(context, uri)?.getTitle(context) ?: getString(R.string.silent) } + updateInfo() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + settings.subscribe(this) + } + + override fun onDestroyView() { + settings.unsubscribe(this) + super.onDestroyView() + } + + override fun onSharedPreferenceChanged(prefs: SharedPreferences?, key: String?) { + when (key) { + AppSettings.KEY_TRACKER_NOTIFICATIONS -> updateInfo() + } } override fun onPreferenceTreeClick(preference: Preference): Boolean { @@ -45,4 +61,9 @@ class NotificationSettingsLegacyFragment : BasePreferenceFragment(R.string.notif else -> super.onPreferenceTreeClick(preference) } } -} + + private fun updateInfo() { + findPreference(AppSettings.KEY_NOTIFICATIONS_INFO) + ?.isVisible = !settings.isTrackerNotificationsEnabled + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/SettingsModule.kt b/app/src/main/java/org/koitharu/kotatsu/settings/SettingsModule.kt index b1fd14c50..230ab0d32 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/SettingsModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/SettingsModule.kt @@ -17,8 +17,8 @@ import org.koitharu.kotatsu.settings.sources.SourcesSettingsViewModel val settingsModule get() = module { - single { BackupRepository(get()) } - single { RestoreRepository(get()) } + factory { BackupRepository(get()) } + factory { RestoreRepository(get()) } single(createdAtStart = true) { AppSettings(androidContext()) } viewModel { BackupViewModel(get(), androidContext()) } diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/TrackerSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/TrackerSettingsFragment.kt index 1e993f845..f1637def6 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/TrackerSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/TrackerSettingsFragment.kt @@ -1,21 +1,34 @@ package org.koitharu.kotatsu.settings import android.content.Intent +import android.content.SharedPreferences +import android.net.Uri import android.os.Build import android.os.Bundle import android.provider.Settings import android.text.style.URLSpan +import android.view.View import androidx.core.text.buildSpannedString import androidx.core.text.inSpans import androidx.preference.MultiSelectListPreference import androidx.preference.Preference +import kotlinx.coroutines.launch +import org.koin.android.ext.android.inject import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BasePreferenceFragment import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity import org.koitharu.kotatsu.settings.utils.MultiSummaryProvider -import org.koitharu.kotatsu.tracker.work.TrackWorker +import org.koitharu.kotatsu.tracker.domain.TrackingRepository +import org.koitharu.kotatsu.tracker.work.TrackerNotificationChannels +import org.koitharu.kotatsu.utils.ext.viewLifecycleScope -class TrackerSettingsFragment : BasePreferenceFragment(R.string.check_for_new_chapters) { +class TrackerSettingsFragment : + BasePreferenceFragment(R.string.check_for_new_chapters), + SharedPreferences.OnSharedPreferenceChangeListener { + + private val repository by inject() + private val channels by inject() override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.pref_tracker) @@ -32,22 +45,81 @@ class TrackerSettingsFragment : BasePreferenceFragment(R.string.check_for_new_ch } } } + updateCategoriesEnabled() + } + + override fun onResume() { + super.onResume() + updateCategoriesSummary() + updateNotificationsSummary() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + settings.subscribe(this) + } + + override fun onDestroyView() { + settings.unsubscribe(this) + super.onDestroyView() + } + + override fun onSharedPreferenceChanged(prefs: SharedPreferences, key: String?) { + when (key) { + AppSettings.KEY_TRACKER_NOTIFICATIONS -> updateNotificationsSummary() + AppSettings.KEY_TRACK_SOURCES, + AppSettings.KEY_TRACKER_ENABLED -> updateCategoriesEnabled() + } } override fun onPreferenceTreeClick(preference: Preference): Boolean { return when (preference.key) { - AppSettings.KEY_NOTIFICATIONS_SETTINGS -> { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS) + AppSettings.KEY_NOTIFICATIONS_SETTINGS -> when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> { + val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) .putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName) - .putExtra(Settings.EXTRA_CHANNEL_ID, TrackWorker.CHANNEL_ID) startActivity(intent) true - } else { + } + channels.areNotificationsDisabled -> { + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + .setData(Uri.fromParts("package", requireContext().packageName, null)) + startActivity(intent) + true + } + else -> { super.onPreferenceTreeClick(preference) } } + AppSettings.KEY_TRACK_CATEGORIES -> { + startActivity(CategoriesActivity.newIntent(preference.context)) + true + } else -> super.onPreferenceTreeClick(preference) } } + + private fun updateNotificationsSummary() { + val pref = findPreference(AppSettings.KEY_NOTIFICATIONS_SETTINGS) ?: return + pref.setSummary( + when { + channels.areNotificationsDisabled -> R.string.disabled + channels.isNotificationGroupEnabled() -> R.string.show_notification_new_chapters_on + else -> R.string.show_notification_new_chapters_off + } + ) + } + + private fun updateCategoriesEnabled() { + val pref = findPreference(AppSettings.KEY_TRACK_CATEGORIES) ?: return + pref.isEnabled = settings.isTrackerEnabled && AppSettings.TRACK_FAVOURITES in settings.trackSources + } + + private fun updateCategoriesSummary() { + val pref = findPreference(AppSettings.KEY_TRACK_CATEGORIES) ?: return + viewLifecycleScope.launch { + val count = repository.getCategoriesCount() + pref.summary = getString(R.string.enabled_d_of_d, count[0], count[1]) + } + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/backup/AppBackupAgent.kt b/app/src/main/java/org/koitharu/kotatsu/settings/backup/AppBackupAgent.kt index 278995dab..baa2a5217 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/backup/AppBackupAgent.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/backup/AppBackupAgent.kt @@ -51,7 +51,7 @@ class AppBackupAgent : BackupAgent() { } private fun createBackupFile() = runBlocking { - val repository = BackupRepository(MangaDatabase.create(applicationContext)) + val repository = BackupRepository(MangaDatabase(applicationContext)) BackupZipOutput(this@AppBackupAgent).use { backup -> backup.put(repository.createIndex()) backup.put(repository.dumpHistory()) @@ -63,7 +63,7 @@ class AppBackupAgent : BackupAgent() { } private fun restoreBackupFile(fd: FileDescriptor, size: Long) { - val repository = RestoreRepository(MangaDatabase.create(applicationContext)) + val repository = RestoreRepository(MangaDatabase(applicationContext)) val tempFile = File.createTempFile("backup_", ".tmp") FileInputStream(fd).use { input -> tempFile.outputStream().use { output -> diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/TrackerModule.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/TrackerModule.kt index a08495f54..975e96d66 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/TrackerModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/TrackerModule.kt @@ -1,14 +1,17 @@ package org.koitharu.kotatsu.tracker +import org.koin.android.ext.koin.androidContext import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.tracker.ui.FeedViewModel +import org.koitharu.kotatsu.tracker.work.TrackerNotificationChannels val trackerModule get() = module { - single { TrackingRepository(get()) } + factory { TrackingRepository(get()) } + factory { TrackerNotificationChannels(androidContext(), get()) } viewModel { FeedViewModel(get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt index 661f68bba..aefa9a69a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt @@ -2,15 +2,15 @@ package org.koitharu.kotatsu.tracker.domain import androidx.room.withTransaction import org.koitharu.kotatsu.core.db.MangaDatabase -import org.koitharu.kotatsu.core.db.entity.TrackEntity -import org.koitharu.kotatsu.core.db.entity.TrackLogEntity -import org.koitharu.kotatsu.core.db.entity.toManga -import org.koitharu.kotatsu.core.db.entity.toTrackingLogItem +import org.koitharu.kotatsu.core.db.entity.* +import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.MangaTracking import org.koitharu.kotatsu.core.model.TrackingLogItem +import org.koitharu.kotatsu.favourites.data.toFavouriteCategory import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.util.mapToSet import java.util.* class TrackingRepository( @@ -21,16 +21,29 @@ class TrackingRepository( return db.tracksDao.findNewChapters(mangaId) ?: 0 } - suspend fun getAllTracks(useFavourites: Boolean, useHistory: Boolean): List { - val mangaList = ArrayList() - if (useFavourites) { - db.favouritesDao.findAllManga().mapTo(mangaList) { it.toManga(emptySet()) } + suspend fun getHistoryManga(): List { + return db.historyDao.findAllManga().toMangaList() + } + + suspend fun getFavouritesManga(): Map> { + val categories = db.favouriteCategoriesDao.findAll() + return categories.associateTo(LinkedHashMap(categories.size)) { categoryEntity -> + categoryEntity.toFavouriteCategory() to db.favouritesDao.findAllManga(categoryEntity.categoryId).toMangaList() } - if (useHistory) { - db.historyDao.findAllManga().mapTo(mangaList) { it.toManga(emptySet()) } - } - val tracks = db.tracksDao.findAll().groupBy { it.mangaId } - return mangaList + } + + suspend fun getCategoriesCount(): IntArray { + val categories = db.favouriteCategoriesDao.findAll() + return intArrayOf( + categories.count { it.track }, + categories.size, + ) + } + + suspend fun getTracks(mangaList: Collection): List { + val ids = mangaList.mapToSet { it.id } + val tracks = db.tracksDao.findAll(ids).groupBy { it.mangaId } + return mangaList // TODO optimize .filterNot { it.source == MangaSource.LOCAL } .distinctBy { it.id } .map { manga -> @@ -103,4 +116,6 @@ class TrackingRepository( ) db.tracksDao.upsert(entity) } + + private fun Collection.toMangaList() = map { it.toManga(emptySet()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackWorker.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackWorker.kt index a61b25c47..943572f24 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackWorker.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackWorker.kt @@ -5,7 +5,6 @@ import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.os.Build -import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat import androidx.lifecycle.LiveData @@ -41,26 +40,22 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) : private val coil by inject() private val repository by inject() private val settings by inject() + private val channels by inject() override suspend fun doWork(): Result { - val trackSources = settings.trackSources - if (trackSources.isEmpty()) { - return Result.success() - } - val tracks = repository.getAllTracks( - useFavourites = AppSettings.TRACK_FAVOURITES in trackSources, - useHistory = AppSettings.TRACK_HISTORY in trackSources - ) - if (tracks.isEmpty()) { + if (!settings.isTrackerEnabled) { return Result.success() } if (TAG in tags) { // not expedited trySetForeground() } + val tracks = getAllTracks() + var success = 0 val workData = Data.Builder() .putInt(DATA_TOTAL, tracks.size) - for ((index, track) in tracks.withIndex()) { + for ((index, item) in tracks.withIndex()) { + val (track, channelId) = item val details = runCatching { MangaRepository(track.manga.source).getDetails(track.manga) }.getOrNull() @@ -80,12 +75,12 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) : track.knownChaptersCount == 0 && track.lastChapterId == 0L -> { // manga was empty on last check repository.storeTrackResult( mangaId = track.manga.id, - knownChaptersCount = track.knownChaptersCount, + knownChaptersCount = 0, lastChapterId = 0L, previousTrackChapterId = track.lastNotifiedChapterId, newChapters = chapters ) - showNotification(details, chapters) + showNotification(details, channelId, chapters) } chapters.size == track.knownChaptersCount -> { if (chapters.lastOrNull()?.id == track.lastChapterId) { @@ -114,7 +109,8 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) : ) showNotification( details, - newChapters.takeLastWhile { x -> x.id != track.lastNotifiedChapterId } + channelId, + newChapters.takeLastWhile { x -> x.id != track.lastNotifiedChapterId }, ) } } @@ -126,11 +122,12 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) : knownChaptersCount = track.knownChaptersCount, lastChapterId = track.lastChapterId, previousTrackChapterId = track.lastNotifiedChapterId, - newChapters = newChapters + newChapters = newChapters, ) showNotification( - track.manga, - newChapters.takeLastWhile { x -> x.id != track.lastNotifiedChapterId } + manga = track.manga, + channelId = channelId, + newChapters = newChapters.takeLastWhile { x -> x.id != track.lastNotifiedChapterId }, ) } } @@ -144,13 +141,60 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) : } } - private suspend fun showNotification(manga: Manga, newChapters: List) { - if (newChapters.isEmpty() || !settings.trackerNotifications) { + private suspend fun getAllTracks(): List { + val sources = settings.trackSources + if (sources.isEmpty()) { + return emptyList() + } + val knownIds = HashSet() + val result = ArrayList() + // Favourites + if (AppSettings.TRACK_FAVOURITES in sources) { + val favourites = repository.getFavouritesManga() + channels.updateChannels(favourites.keys) + for ((category, mangaList) in favourites) { + if (!category.isTrackingEnabled || mangaList.isEmpty()) { + continue + } + val categoryTracks = repository.getTracks(mangaList) + val channelId = if (channels.isFavouriteNotificationsEnabled(category)) { + channels.getFavouritesChannelId(category.id) + } else { + null + } + for (track in categoryTracks) { + if (knownIds.add(track.manga)) { + result.add(TrackingItem(track, channelId)) + } + } + } + } + // History + if (AppSettings.TRACK_HISTORY in sources) { + val history = repository.getHistoryManga() + val historyTracks = repository.getTracks(history) + val channelId = if (channels.isHistoryNotificationsEnabled()) { + channels.getHistoryChannelId() + } else { + null + } + for (track in historyTracks) { + if (knownIds.add(track.manga)) { + result.add(TrackingItem(track, channelId)) + } + } + } + result.trimToSize() + return result + } + + private suspend fun showNotification(manga: Manga, channelId: String?, newChapters: List) { + if (newChapters.isEmpty() || channelId == null) { return } val id = manga.url.hashCode() val colorPrimary = ContextCompat.getColor(applicationContext, R.color.blue_primary) - val builder = NotificationCompat.Builder(applicationContext, CHANNEL_ID) + val builder = NotificationCompat.Builder(applicationContext, channelId) val summary = applicationContext.resources.getQuantityString( R.plurals.new_chapters, newChapters.size, newChapters.size @@ -236,7 +280,6 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) : companion object { - const val CHANNEL_ID = "tracking" private const val WORKER_CHANNEL_ID = "track_worker" private const val WORKER_NOTIFICATION_ID = 35 private const val DATA_PROGRESS = "progress" @@ -244,27 +287,7 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) : private const val TAG = "tracking" private const val TAG_ONESHOT = "tracking_oneshot" - @RequiresApi(Build.VERSION_CODES.O) - private fun createNotificationChannel(context: Context) { - val manager = - context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - if (manager.getNotificationChannel(CHANNEL_ID) == null) { - val channel = NotificationChannel( - CHANNEL_ID, - context.getString(R.string.new_chapters), - NotificationManager.IMPORTANCE_DEFAULT - ) - channel.setShowBadge(true) - channel.lightColor = ContextCompat.getColor(context, R.color.blue_primary_dark) - channel.enableLights(true) - manager.createNotificationChannel(channel) - } - } - fun setup(context: Context) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - createNotificationChannel(context) - } val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackerNotificationChannels.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackerNotificationChannels.kt new file mode 100644 index 000000000..81fcb73d5 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackerNotificationChannels.kt @@ -0,0 +1,143 @@ +package org.koitharu.kotatsu.tracker.work + +import android.app.NotificationChannel +import android.app.NotificationChannelGroup +import android.app.NotificationManager +import android.content.Context +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.core.app.NotificationManagerCompat +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.model.FavouriteCategory +import org.koitharu.kotatsu.core.prefs.AppSettings + +class TrackerNotificationChannels( + private val context: Context, + private val settings: AppSettings, +) { + + private val manager = NotificationManagerCompat.from(context) + + val areNotificationsDisabled: Boolean + get() = !manager.areNotificationsEnabled() + + fun updateChannels(categories: Collection) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + return + } + manager.deleteNotificationChannel(OLD_CHANNEL_ID) + val group = createGroup() + val existingChannels = group.channels.associateByTo(HashMap()) { it.id } + for (category in categories) { + val id = getFavouritesChannelId(category.id) + if (existingChannels.remove(id)?.name == category.title) { + continue + } + val channel = NotificationChannel(id, category.title, NotificationManager.IMPORTANCE_DEFAULT) + channel.group = GROUP_ID + manager.createNotificationChannel(channel) + } + existingChannels.remove(CHANNEL_ID_HISTORY) + createHistoryChannel() + for (id in existingChannels.keys) { + manager.deleteNotificationChannel(id) + } + } + + fun createChannel(category: FavouriteCategory) { + renameChannel(category.id, category.title) + } + + fun renameChannel(categoryId: Long, name: String) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + return + } + val id = getFavouritesChannelId(categoryId) + val channel = NotificationChannel(id, name, NotificationManager.IMPORTANCE_DEFAULT) + channel.group = createGroup().id + manager.createNotificationChannel(channel) + } + + fun deleteChannel(categoryId: Long) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + return + } + manager.deleteNotificationChannel(getFavouritesChannelId(categoryId)) + } + + fun isFavouriteNotificationsEnabled(category: FavouriteCategory): Boolean { + if (!manager.areNotificationsEnabled()) { + return false + } + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = manager.getNotificationChannel(getFavouritesChannelId(category.id)) + channel != null && channel.importance != NotificationManager.IMPORTANCE_NONE + } else { + // fallback + settings.isTrackerNotificationsEnabled + } + } + + fun isHistoryNotificationsEnabled(): Boolean { + if (!manager.areNotificationsEnabled()) { + return false + } + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = manager.getNotificationChannel(getHistoryChannelId()) + channel != null && channel.importance != NotificationManager.IMPORTANCE_NONE + } else { + // fallback + settings.isTrackerNotificationsEnabled + } + } + + fun isNotificationGroupEnabled(): Boolean { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + return settings.isTrackerNotificationsEnabled + } + val group = manager.getNotificationChannelGroup(GROUP_ID) ?: return true + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && group.isBlocked) { + return false + } + return group.channels.any { it.importance != NotificationManagerCompat.IMPORTANCE_NONE } + } + + fun getFavouritesChannelId(categoryId: Long): String { + return CHANNEL_ID_PREFIX + categoryId + } + + fun getHistoryChannelId(): String { + return CHANNEL_ID_HISTORY + } + + @RequiresApi(Build.VERSION_CODES.O) + private fun createGroup(): NotificationChannelGroup { + manager.getNotificationChannelGroup(GROUP_ID)?.let { + return it + } + val group = NotificationChannelGroup(GROUP_ID, context.getString(R.string.new_chapters)) + manager.createNotificationChannelGroup(group) + return group + } + + private fun createHistoryChannel() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + return + } + val channel = NotificationChannel( + CHANNEL_ID_HISTORY, + context.getString(R.string.history), + NotificationManager.IMPORTANCE_DEFAULT, + ) + channel.group = GROUP_ID + manager.createNotificationChannel(channel) + } + + companion object { + + const val GROUP_ID = "trackers" + private const val CHANNEL_ID_PREFIX = "track_fav_" + private const val CHANNEL_ID_HISTORY = "track_history" + private const val OLD_CHANNEL_ID = "tracking" + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackingItem.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackingItem.kt new file mode 100644 index 000000000..933918009 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackingItem.kt @@ -0,0 +1,31 @@ +package org.koitharu.kotatsu.tracker.work + +import org.koitharu.kotatsu.core.model.MangaTracking + +class TrackingItem( + val tracking: MangaTracking, + val channelId: String?, +) { + + operator fun component1() = tracking + + operator fun component2() = channelId + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as TrackingItem + + if (tracking != other.tracking) return false + if (channelId != other.channelId) return false + + return true + } + + override fun hashCode(): Int { + var result = tracking.hashCode() + result = 31 * result + channelId.hashCode() + return result + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/preference_toggle_header.xml b/app/src/main/res/layout/preference_toggle_header.xml new file mode 100644 index 000000000..f21e8604e --- /dev/null +++ b/app/src/main/res/layout/preference_toggle_header.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/popup_category.xml b/app/src/main/res/menu/popup_category.xml index 1c4fc96d2..ee78b4b80 100644 --- a/app/src/main/res/menu/popup_category.xml +++ b/app/src/main/res/menu/popup_category.xml @@ -23,4 +23,9 @@ + + \ No newline at end of file diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index cbd9de9a7..bada7be01 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -101,7 +101,6 @@ Паведамленні Уключана %1$d з %2$d Новыя главы - Апавяшчаць пра абнаўленні мангі, якую вы чытаеце Спампаваць Чытаць з пачатку Перазапусціць diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 42fb3a9ee..9bab88eb0 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -46,7 +46,6 @@ Aktualisierungsfeed gelöscht Aktualisierungsfeed löschen Aktualisierungen - Über Aktualisierungen von Manga benachrichtigen, die du liest Benachrichtigung anzeigen, wenn eine Aktualisierung verfügbar ist Anwendungsaktualisierung ist verfügbar Automatisch nach Aktualisierungen suchen diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 9cde0edc3..ef974cffd 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -101,7 +101,6 @@ Notificaciones Activado %1$d de %2$d Nuevos capítulos - Notificar sobre las actualizaciones del manga que estás leyendo Descargar Leer desde el principio Reiniciar diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 69f9c193e..40ee9d1b7 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -118,7 +118,6 @@ Käynnistä uudelleen Lue alusta Lataa - Ilmoita lukemastasi mangan päivityksistä Uusia lukuja Käytössä %1$d / %2$d Ilmoitukset diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 958794036..720c59b4a 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -112,7 +112,6 @@ Paramètres des notifications Lire depuis le début Télécharger - Avertir des mises à jour des mangas que vous lisez Nouveaux chapitres %1$d de %2$d activé(s) Notifications diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index e6196111b..683112a44 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -154,7 +154,6 @@ Riavvia Leggi dall\'inizio Scarica - Notifica gli aggiornamenti dei manga che stai leggendo Nuovi capitoli Abilitato %1$d di %2$d Notifiche diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index d6db95de8..8f9ab14f1 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -143,7 +143,6 @@ 検索履歴をクリア 外部ストレージ Kotatsuの新しい更新が利用可能です - あなたが読んでいる漫画の更新について通知 ここは空っぽです… カテゴリーを使用してお気に入りを整理できます。 «+»を押してカテゴリーを作成出来ます 空のカテゴリー diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 74c07b980..41444062e 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -54,7 +54,6 @@ Fjern «%s»-kategorien fra favorittene\? \nAlle mangaer i den vil bli tapt. Programomstart - Gi merknad om oppdateringer av det du leser %1$d av %2$d påskrudd Denne mangaen har %s. Lagre hele\? Åpne i nettleser diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 32910e0a3..9e654146f 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -232,7 +232,6 @@ Limpar cache de miniaturas Verifique se há novas versões do aplicativo Categorias favoritas - Notificar sobre atualizações de mangá que você está lendo Remover a categoria “%s” dos seus favoritos\? \nTodos os mangás nela serão perdidos. Nenhuma atualização disponível diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index b23edc825..d3794d77a 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -83,7 +83,6 @@ Salve Notificações Novos capítulos - Notifique sobre atualizações do mangá que está lendo Download Ler desde o início Reiniciar diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 41dad2021..87da7d5d0 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -101,7 +101,6 @@ Уведомления Включено %1$d из %2$d Новые главы - Уведомлять об обновлении манги, которую Вы читаете Загрузить Читать с начала Перезапустить @@ -278,4 +277,8 @@ Главы будут удалены в фоновом режиме. Это может занять какое-то время Скрыть Доступны новые источники манги + Проверять новые главы и уведомлять о них + Вы будете получать уведомления об обновлении манги, которую Вы читаете + Вы не будете получать уведомления, но новые главы будут отображаться в списке + Включить уведомления \ No newline at end of file diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 76cfa061a..264ad6c1c 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -105,7 +105,6 @@ Ladda ned Aviseringsinställningar LED-indikator - Avisera om uppdateringar på manga du läser Läs från början Starta om Aviseringsljud diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index ff48edbc1..74d931aab 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -234,7 +234,6 @@ Öntanımlı Bir ad girmelisiniz %s üzerinde oturum açma desteklenmiyor - Okunan manga güncellemeleri hakkında bildirimde bulun Daha fazla oku Bazı aygıtların arka plan görevlerini bozabilecek farklı sistem davranışları vardır. Ekran görüntüsü politikası diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 692597cee..083717035 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -103,7 +103,6 @@ Notifications %1$d of %2$d on New chapters - Notify about updates of manga you are reading Download Read from start Restart @@ -281,4 +280,8 @@ Chapters will be removed in the background. It can take some time Hide New manga sources are available + Check for new chapters and notify about it + You will receive notifications about updates of manga you are reading + You will not receive notifications but new chapters will be highlighted in the lists + Enable notifications \ No newline at end of file diff --git a/app/src/main/res/xml/pref_notifications.xml b/app/src/main/res/xml/pref_notifications.xml index b3c349d72..c3be1d40d 100644 --- a/app/src/main/res/xml/pref_notifications.xml +++ b/app/src/main/res/xml/pref_notifications.xml @@ -1,19 +1,38 @@ + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/pref_suggestions.xml b/app/src/main/res/xml/pref_suggestions.xml index 224185a67..b9712a556 100644 --- a/app/src/main/res/xml/pref_suggestions.xml +++ b/app/src/main/res/xml/pref_suggestions.xml @@ -6,7 +6,7 @@ + + - + Date: Fri, 6 May 2022 16:53:55 +0300 Subject: [PATCH 2/7] Fix crash on first database initialization --- .../koitharu/kotatsu/core/db/DatabasePrePopulateCallback.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/DatabasePrePopulateCallback.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/DatabasePrePopulateCallback.kt index 35c52192c..6257a8456 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/DatabasePrePopulateCallback.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/DatabasePrePopulateCallback.kt @@ -10,8 +10,8 @@ class DatabasePrePopulateCallback(private val resources: Resources) : RoomDataba override fun onCreate(db: SupportSQLiteDatabase) { db.execSQL( - "INSERT INTO favourite_categories (created_at, sort_key, title, `order`) VALUES (?,?,?,?)", - arrayOf(System.currentTimeMillis(), 1, resources.getString(R.string.read_later), SortOrder.NEWEST.name) + "INSERT INTO favourite_categories (created_at, sort_key, title, `order`, track) VALUES (?,?,?,?,?)", + arrayOf(System.currentTimeMillis(), 1, resources.getString(R.string.read_later), SortOrder.NEWEST.name, 1) ) } } \ No newline at end of file From 400a2b14f7b375186d357238f195b53c81a91c47 Mon Sep 17 00:00:00 2001 From: Zakhar Timoshenko Date: Fri, 6 May 2022 16:54:41 +0300 Subject: [PATCH 3/7] Change a bit `preference_toggle_header` view --- .../res/layout/preference_toggle_header.xml | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/app/src/main/res/layout/preference_toggle_header.xml b/app/src/main/res/layout/preference_toggle_header.xml index f21e8604e..b1f816337 100644 --- a/app/src/main/res/layout/preference_toggle_header.xml +++ b/app/src/main/res/layout/preference_toggle_header.xml @@ -1,18 +1,22 @@ - + android:layout_height="wrap_content" + android:layout_marginHorizontal="16dp" + android:layout_marginVertical="8dp" + app:cardCornerRadius="24dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> + android:padding="16dp"> + android:textAppearance="@style/TextAppearance.Material3.TitleLarge" + android:textSize="20sp" + tools:text="Title"/> + android:textAppearance="@style/TextAppearance.Material3.TitleSmall" + tools:text="Subtitle"/> @@ -61,4 +67,4 @@ - + From 930819ffa2084eed7288f4ca29f4bfcc481787a4 Mon Sep 17 00:00:00 2001 From: Zakhar Timoshenko Date: Fri, 6 May 2022 20:59:43 +0300 Subject: [PATCH 4/7] Fix setting tracker on favourites screen --- .../kotatsu/favourites/ui/FavouritesContainerFragment.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt index 4433f978d..45cbba17a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt @@ -150,6 +150,10 @@ class FavouritesContainerFragment : menuItem.isChecked = item == category.order } submenu.setGroupCheckable(R.id.group_order, true, true) + menu.findItem(R.id.action_tracking)?.run { + isVisible = viewModel.isFavouritesTrackerEnabled + isChecked = category.isTrackingEnabled + } } private fun TabLayout.setTabsEnabled(enabled: Boolean) { @@ -168,6 +172,7 @@ class FavouritesContainerFragment : R.id.action_remove -> editDelegate.deleteCategory(category) R.id.action_rename -> editDelegate.renameCategory(category) R.id.action_create -> editDelegate.createCategory() + R.id.action_tracking -> viewModel.setCategoryTracking(category.id, !category.isTrackingEnabled) R.id.action_order -> return@setOnMenuItemClickListener false else -> { val order = CategoriesActivity.SORT_ORDERS.getOrNull(it.order) From 6405523232e25ecb2ea1cfb5c92b13b943d0ac67 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 7 May 2022 08:50:33 +0300 Subject: [PATCH 5/7] Fix CategoryListModel equals/hashcode --- .../kotatsu/favourites/ui/categories/CategoriesAdapter.kt | 5 ++++- .../favourites/ui/categories/adapter/CategoryListModel.kt | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesAdapter.kt index e13b31e00..7a5620158 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesAdapter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesAdapter.kt @@ -40,7 +40,10 @@ class CategoriesAdapter( newItem: CategoryListModel, ): Any? = when { oldItem is CategoryListModel.All && newItem is CategoryListModel.All -> Unit - else -> super.getChangePayload(oldItem, newItem) + oldItem is CategoryListModel.CategoryItem && + newItem is CategoryListModel.CategoryItem && + oldItem.category.title != newItem.category.title -> null + else -> Unit } } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryListModel.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryListModel.kt index 8326f8617..899b73e1c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryListModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryListModel.kt @@ -45,6 +45,7 @@ sealed interface CategoryListModel : ListModel { if (category.id != other.category.id) return false if (category.title != other.category.title) return false if (category.order != other.category.order) return false + if (category.isTrackingEnabled != other.category.isTrackingEnabled) return false return true } @@ -53,6 +54,7 @@ sealed interface CategoryListModel : ListModel { var result = category.id.hashCode() result = 31 * result + category.title.hashCode() result = 31 * result + category.order.hashCode() + result = 31 * result + category.isTrackingEnabled.hashCode() return result } } From 2a97cb34d7121f2e4ab4ea1bb4b6dc92a0ea3700 Mon Sep 17 00:00:00 2001 From: Zakhar Timoshenko Date: Sat, 7 May 2022 14:01:07 +0300 Subject: [PATCH 6/7] Change root view of `preference_toggle_header` --- .../res/layout/preference_toggle_header.xml | 100 ++++++++---------- 1 file changed, 47 insertions(+), 53 deletions(-) diff --git a/app/src/main/res/layout/preference_toggle_header.xml b/app/src/main/res/layout/preference_toggle_header.xml index b1f816337..4b0ed9431 100644 --- a/app/src/main/res/layout/preference_toggle_header.xml +++ b/app/src/main/res/layout/preference_toggle_header.xml @@ -1,70 +1,64 @@ - + style="@style/Widget.Material3.CardView.Filled" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginHorizontal="16dp" + android:layout_marginVertical="8dp" + android:paddingBottom="8dp" + app:cardCornerRadius="24dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> - + android:baselineAligned="false" + android:clipChildren="false" + android:clipToPadding="false" + android:gravity="center_vertical" + android:minHeight="?android:attr/listPreferredItemHeightSmall" + android:padding="16dp"> + android:layout_weight="1" + android:orientation="vertical"> - - - - - - - - - + android:textAppearance="@style/TextAppearance.Material3.TitleLarge" + android:textSize="20sp" + tools:text="Title" /> + + - + - + + + \ No newline at end of file From 33ab7f4d9565d35247d6beb5858556bf14ea8d85 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 7 May 2022 15:06:17 +0300 Subject: [PATCH 7/7] Cleanup preference_toggle_header --- app/src/main/res/layout/preference_toggle_header.xml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/src/main/res/layout/preference_toggle_header.xml b/app/src/main/res/layout/preference_toggle_header.xml index 4b0ed9431..a95b5e211 100644 --- a/app/src/main/res/layout/preference_toggle_header.xml +++ b/app/src/main/res/layout/preference_toggle_header.xml @@ -9,10 +9,7 @@ android:layout_marginHorizontal="16dp" android:layout_marginVertical="8dp" android:paddingBottom="8dp" - app:cardCornerRadius="24dp" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent"> + app:cardCornerRadius="24dp">