diff --git a/app/src/androidTest/java/org/koitharu/kotatsu/core/os/ShortcutsUpdaterTest.kt b/app/src/androidTest/java/org/koitharu/kotatsu/core/os/ShortcutsUpdaterTest.kt new file mode 100644 index 000000000..1dd8925de --- /dev/null +++ b/app/src/androidTest/java/org/koitharu/kotatsu/core/os/ShortcutsUpdaterTest.kt @@ -0,0 +1,56 @@ +package org.koitharu.kotatsu.core.os + +import android.content.pm.ShortcutInfo +import android.content.pm.ShortcutManager +import androidx.core.content.getSystemService +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import kotlinx.coroutines.delay +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.koin.test.KoinTest +import org.koin.test.inject +import org.koitharu.kotatsu.SampleData +import org.koitharu.kotatsu.core.db.MangaDatabase +import org.koitharu.kotatsu.history.domain.HistoryRepository +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +@RunWith(AndroidJUnit4::class) +class ShortcutsUpdaterTest : KoinTest { + + private val historyRepository by inject() + private val shortcutsUpdater by inject() + private val database by inject() + + @Before + fun setUp() { + database.clearAllTables() + } + + @Test + fun testUpdateShortcuts() = runTest { + shortcutsUpdater.await() + assertTrue(getShortcuts().isEmpty()) + historyRepository.addOrUpdate( + manga = SampleData.manga, + chapterId = SampleData.chapter.id, + page = 4, + scroll = 2, + percent = 0.3f + ) + delay(1000) + shortcutsUpdater.await() + + val shortcuts = getShortcuts() + assertEquals(1, shortcuts.size) + } + + private fun getShortcuts(): List { + val context = InstrumentationRegistry.getInstrumentation().targetContext + val manager = checkNotNull(context.getSystemService()) + return manager.dynamicShortcuts.filterNot { it.id == "com.squareup.leakcanary.dynamic_shortcut" } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/os/ShortcutsRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/os/ShortcutsUpdater.kt similarity index 70% rename from app/src/main/java/org/koitharu/kotatsu/core/os/ShortcutsRepository.kt rename to app/src/main/java/org/koitharu/kotatsu/core/os/ShortcutsUpdater.kt index 9e8b18a52..424e312a9 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/os/ShortcutsRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/os/ShortcutsUpdater.kt @@ -6,38 +6,41 @@ import android.content.pm.ShortcutManager import android.media.ThumbnailUtils import android.os.Build import android.util.Size +import androidx.annotation.VisibleForTesting import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.graphics.drawable.IconCompat +import androidx.room.InvalidationTracker import coil.ImageLoader import coil.request.ImageRequest import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.domain.MangaDataRepository +import org.koitharu.kotatsu.core.db.TABLE_HISTORY import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.reader.ui.ReaderActivity +import org.koitharu.kotatsu.utils.ext.processLifecycleScope import org.koitharu.kotatsu.utils.ext.requireBitmap -class ShortcutsRepository( +class ShortcutsUpdater( private val context: Context, private val coil: ImageLoader, private val historyRepository: HistoryRepository, private val mangaRepository: MangaDataRepository, -) { +) : InvalidationTracker.Observer(TABLE_HISTORY) { - private val iconSize by lazy { - getIconSize(context) - } + private val iconSize by lazy { getIconSize(context) } + private var shortcutsUpdateJob: Job? = null - suspend fun updateShortcuts() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) return - val manager = context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager - val shortcuts = historyRepository.getList(0, manager.maxShortcutCountPerActivity) - .filter { x -> x.title.isNotEmpty() } - .map { buildShortcutInfo(it).build().toShortcutInfo() } - manager.dynamicShortcuts = shortcuts + override fun onInvalidated(tables: MutableSet) { + val prevJob = shortcutsUpdateJob + shortcutsUpdateJob = processLifecycleScope.launch(Dispatchers.Default) { + prevJob?.join() + updateShortcutsImpl() + } } suspend fun requestPinShortcut(manga: Manga): Boolean { @@ -48,17 +51,28 @@ class ShortcutsRepository( ) } + @VisibleForTesting + suspend fun await() { + shortcutsUpdateJob?.join() + } + + private suspend fun updateShortcutsImpl() { + val manager = context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager + val shortcuts = historyRepository.getList(0, manager.maxShortcutCountPerActivity) + .filter { x -> x.title.isNotEmpty() } + .map { buildShortcutInfo(it).build().toShortcutInfo() } + manager.dynamicShortcuts = shortcuts + } + private suspend fun buildShortcutInfo(manga: Manga): ShortcutInfoCompat.Builder { val icon = runCatching { - withContext(Dispatchers.IO) { - val bmp = coil.execute( - ImageRequest.Builder(context) - .data(manga.coverUrl) - .size(iconSize.width, iconSize.height) - .build() - ).requireBitmap() - ThumbnailUtils.extractThumbnail(bmp, iconSize.width, iconSize.height, 0) - } + val bmp = coil.execute( + ImageRequest.Builder(context) + .data(manga.coverUrl) + .size(iconSize.width, iconSize.height) + .build() + ).requireBitmap() + ThumbnailUtils.extractThumbnail(bmp, iconSize.width, iconSize.height, 0) }.fold( onSuccess = { IconCompat.createWithAdaptiveBitmap(it) }, onFailure = { IconCompat.createWithResource(context, R.drawable.ic_shortcut_default) } diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt index d4339645c..8e517b73d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt @@ -34,7 +34,7 @@ import org.koitharu.kotatsu.base.ui.BaseActivity import org.koitharu.kotatsu.browser.BrowserActivity import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga -import org.koitharu.kotatsu.core.os.ShortcutsRepository +import org.koitharu.kotatsu.core.os.ShortcutsUpdater import org.koitharu.kotatsu.databinding.ActivityDetailsBinding import org.koitharu.kotatsu.details.ui.adapter.BranchesAdapter import org.koitharu.kotatsu.download.ui.service.DownloadService @@ -224,7 +224,7 @@ class DetailsActivity : R.id.action_shortcut -> { viewModel.manga.value?.let { lifecycleScope.launch { - if (!get().requestPinShortcut(it)) { + if (!get().requestPinShortcut(it)) { binding.snackbar.show(getString(R.string.operation_not_supported)) } } 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 319353b69..029d024f0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/HistoryModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/HistoryModule.kt @@ -10,5 +10,5 @@ val historyModule single { HistoryRepository(get(), get(), get(), getAll()) } - viewModel { HistoryListViewModel(get(), get(), get(), get()) } + viewModel { HistoryListViewModel(get(), get(), get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt index 2e930c529..7cb6d266e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt @@ -9,8 +9,6 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.domain.ReversibleHandle -import org.koitharu.kotatsu.base.domain.plus -import org.koitharu.kotatsu.core.os.ShortcutsRepository import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.core.prefs.observeAsFlow @@ -31,7 +29,6 @@ import java.util.concurrent.TimeUnit class HistoryListViewModel( private val repository: HistoryRepository, private val settings: AppSettings, - private val shortcutsRepository: ShortcutsRepository, private val trackingRepository: TrackingRepository, ) : MangaListViewModel(settings) { @@ -72,7 +69,6 @@ class HistoryListViewModel( fun clearHistory() { launchLoadingJob { repository.clear() - shortcutsRepository.updateShortcuts() } } @@ -81,10 +77,7 @@ class HistoryListViewModel( return } launchJob(Dispatchers.Default) { - val handle = repository.deleteReversible(ids) + ReversibleHandle { - shortcutsRepository.updateShortcuts() - } - shortcutsRepository.updateShortcuts() + val handle = repository.deleteReversible(ids) onItemsRemoved.postCall(handle) } } 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 f8cb28657..b116fdf97 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/LocalModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/LocalModule.kt @@ -16,5 +16,5 @@ val localModule factory { DownloadManager.Factory(androidContext(), get(), get(), get(), get(), get()) } - viewModel { LocalListViewModel(get(), get(), get(), get()) } + viewModel { LocalListViewModel(get(), get(), get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt index e8490f9d9..ebd347913 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt @@ -11,7 +11,6 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.core.os.ShortcutsRepository import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.download.ui.service.DownloadService import org.koitharu.kotatsu.history.domain.HistoryRepository @@ -29,7 +28,6 @@ class LocalListViewModel( private val repository: LocalMangaRepository, private val historyRepository: HistoryRepository, settings: AppSettings, - private val shortcutsRepository: ShortcutsRepository, ) : MangaListViewModel(settings) { val onMangaRemoved = SingleLiveEvent() @@ -107,7 +105,6 @@ class LocalListViewModel( } } } - shortcutsRepository.updateShortcuts() onMangaRemoved.call(Unit) } } 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 c85c04f26..51aa633ec 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/MainModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/main/MainModule.kt @@ -1,12 +1,14 @@ package org.koitharu.kotatsu.main import android.app.Application +import android.os.Build +import androidx.room.InvalidationTracker import org.koin.android.ext.koin.androidContext import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.bind import org.koin.dsl.module import org.koitharu.kotatsu.base.ui.util.ActivityRecreationHandle -import org.koitharu.kotatsu.core.os.ShortcutsRepository +import org.koitharu.kotatsu.core.os.ShortcutsUpdater import org.koitharu.kotatsu.main.ui.MainViewModel import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper import org.koitharu.kotatsu.main.ui.protect.ProtectViewModel @@ -15,7 +17,13 @@ val mainModule get() = module { single { AppProtectHelper(get()) } bind Application.ActivityLifecycleCallbacks::class single { ActivityRecreationHandle() } bind Application.ActivityLifecycleCallbacks::class - factory { ShortcutsRepository(androidContext(), get(), get(), get()) } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { + single { ShortcutsUpdater(androidContext(), get(), get(), get()) } bind InvalidationTracker.Observer::class + } else { + factory { ShortcutsUpdater(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 a27fb9e8d..5b8faf203 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ReaderModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ReaderModule.kt @@ -23,7 +23,6 @@ val readerModule preselectedBranch = params[2], dataRepository = get(), historyRepository = get(), - shortcutsRepository = get(), settings = get(), pageSaveHelper = get(), bookmarksRepository = get(), diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt index 0384e44aa..ea93e7267 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt @@ -17,7 +17,6 @@ import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository import org.koitharu.kotatsu.core.exceptions.MangaNotFoundException -import org.koitharu.kotatsu.core.os.ShortcutsRepository import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.prefs.* import org.koitharu.kotatsu.history.domain.HistoryRepository @@ -46,7 +45,6 @@ class ReaderViewModel( private val preselectedBranch: String?, private val dataRepository: MangaDataRepository, private val historyRepository: HistoryRepository, - private val shortcutsRepository: ShortcutsRepository, private val bookmarksRepository: BookmarksRepository, private val settings: AppSettings, private val pageSaveHelper: PageSaveHelper, @@ -289,7 +287,6 @@ class ReaderViewModel( currentState.value?.let { val percent = computePercent(it.chapterId, it.page) historyRepository.addOrUpdate(manga, it.chapterId, it.page, it.scroll, percent) - shortcutsRepository.updateShortcuts() } content.postValue(ReaderContent(pages, currentState.value))