Update app shortcuts using InvalidationTracker.Callback

pull/203/head
Koitharu 4 years ago
parent aa5fd530d3
commit 62088b36a4
No known key found for this signature in database
GPG Key ID: 8E861F8CE6E7CE27

@ -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<HistoryRepository>()
private val shortcutsUpdater by inject<ShortcutsUpdater>()
private val database by inject<MangaDatabase>()
@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<ShortcutInfo> {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val manager = checkNotNull(context.getSystemService<ShortcutManager>())
return manager.dynamicShortcuts.filterNot { it.id == "com.squareup.leakcanary.dynamic_shortcut" }
}
}

@ -6,38 +6,41 @@ import android.content.pm.ShortcutManager
import android.media.ThumbnailUtils import android.media.ThumbnailUtils
import android.os.Build import android.os.Build
import android.util.Size import android.util.Size
import androidx.annotation.VisibleForTesting
import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import androidx.room.InvalidationTracker
import coil.ImageLoader import coil.ImageLoader
import coil.request.ImageRequest import coil.request.ImageRequest
import kotlinx.coroutines.Dispatchers 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.R
import org.koitharu.kotatsu.base.domain.MangaDataRepository 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.history.domain.HistoryRepository
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.reader.ui.ReaderActivity import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
import org.koitharu.kotatsu.utils.ext.requireBitmap import org.koitharu.kotatsu.utils.ext.requireBitmap
class ShortcutsRepository( class ShortcutsUpdater(
private val context: Context, private val context: Context,
private val coil: ImageLoader, private val coil: ImageLoader,
private val historyRepository: HistoryRepository, private val historyRepository: HistoryRepository,
private val mangaRepository: MangaDataRepository, private val mangaRepository: MangaDataRepository,
) { ) : InvalidationTracker.Observer(TABLE_HISTORY) {
private val iconSize by lazy { private val iconSize by lazy { getIconSize(context) }
getIconSize(context) private var shortcutsUpdateJob: Job? = null
}
suspend fun updateShortcuts() { override fun onInvalidated(tables: MutableSet<String>) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) return val prevJob = shortcutsUpdateJob
val manager = context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager shortcutsUpdateJob = processLifecycleScope.launch(Dispatchers.Default) {
val shortcuts = historyRepository.getList(0, manager.maxShortcutCountPerActivity) prevJob?.join()
.filter { x -> x.title.isNotEmpty() } updateShortcutsImpl()
.map { buildShortcutInfo(it).build().toShortcutInfo() } }
manager.dynamicShortcuts = shortcuts
} }
suspend fun requestPinShortcut(manga: Manga): Boolean { suspend fun requestPinShortcut(manga: Manga): Boolean {
@ -48,9 +51,21 @@ 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 { private suspend fun buildShortcutInfo(manga: Manga): ShortcutInfoCompat.Builder {
val icon = runCatching { val icon = runCatching {
withContext(Dispatchers.IO) {
val bmp = coil.execute( val bmp = coil.execute(
ImageRequest.Builder(context) ImageRequest.Builder(context)
.data(manga.coverUrl) .data(manga.coverUrl)
@ -58,7 +73,6 @@ class ShortcutsRepository(
.build() .build()
).requireBitmap() ).requireBitmap()
ThumbnailUtils.extractThumbnail(bmp, iconSize.width, iconSize.height, 0) ThumbnailUtils.extractThumbnail(bmp, iconSize.width, iconSize.height, 0)
}
}.fold( }.fold(
onSuccess = { IconCompat.createWithAdaptiveBitmap(it) }, onSuccess = { IconCompat.createWithAdaptiveBitmap(it) },
onFailure = { IconCompat.createWithResource(context, R.drawable.ic_shortcut_default) } onFailure = { IconCompat.createWithResource(context, R.drawable.ic_shortcut_default) }

@ -34,7 +34,7 @@ import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.browser.BrowserActivity import org.koitharu.kotatsu.browser.BrowserActivity
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga 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.databinding.ActivityDetailsBinding
import org.koitharu.kotatsu.details.ui.adapter.BranchesAdapter import org.koitharu.kotatsu.details.ui.adapter.BranchesAdapter
import org.koitharu.kotatsu.download.ui.service.DownloadService import org.koitharu.kotatsu.download.ui.service.DownloadService
@ -224,7 +224,7 @@ class DetailsActivity :
R.id.action_shortcut -> { R.id.action_shortcut -> {
viewModel.manga.value?.let { viewModel.manga.value?.let {
lifecycleScope.launch { lifecycleScope.launch {
if (!get<ShortcutsRepository>().requestPinShortcut(it)) { if (!get<ShortcutsUpdater>().requestPinShortcut(it)) {
binding.snackbar.show(getString(R.string.operation_not_supported)) binding.snackbar.show(getString(R.string.operation_not_supported))
} }
} }

@ -10,5 +10,5 @@ val historyModule
single { HistoryRepository(get(), get(), get(), getAll()) } single { HistoryRepository(get(), get(), get(), getAll()) }
viewModel { HistoryListViewModel(get(), get(), get(), get()) } viewModel { HistoryListViewModel(get(), get(), get()) }
} }

@ -9,8 +9,6 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.ReversibleHandle 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.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.core.prefs.observeAsFlow import org.koitharu.kotatsu.core.prefs.observeAsFlow
@ -31,7 +29,6 @@ import java.util.concurrent.TimeUnit
class HistoryListViewModel( class HistoryListViewModel(
private val repository: HistoryRepository, private val repository: HistoryRepository,
private val settings: AppSettings, private val settings: AppSettings,
private val shortcutsRepository: ShortcutsRepository,
private val trackingRepository: TrackingRepository, private val trackingRepository: TrackingRepository,
) : MangaListViewModel(settings) { ) : MangaListViewModel(settings) {
@ -72,7 +69,6 @@ class HistoryListViewModel(
fun clearHistory() { fun clearHistory() {
launchLoadingJob { launchLoadingJob {
repository.clear() repository.clear()
shortcutsRepository.updateShortcuts()
} }
} }
@ -81,10 +77,7 @@ class HistoryListViewModel(
return return
} }
launchJob(Dispatchers.Default) { launchJob(Dispatchers.Default) {
val handle = repository.deleteReversible(ids) + ReversibleHandle { val handle = repository.deleteReversible(ids)
shortcutsRepository.updateShortcuts()
}
shortcutsRepository.updateShortcuts()
onItemsRemoved.postCall(handle) onItemsRemoved.postCall(handle)
} }
} }

@ -16,5 +16,5 @@ val localModule
factory { DownloadManager.Factory(androidContext(), get(), get(), get(), get(), get()) } factory { DownloadManager.Factory(androidContext(), get(), get(), get(), get(), get()) }
viewModel { LocalListViewModel(get(), get(), get(), get()) } viewModel { LocalListViewModel(get(), get(), get()) }
} }

@ -11,7 +11,6 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.os.ShortcutsRepository
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.download.ui.service.DownloadService import org.koitharu.kotatsu.download.ui.service.DownloadService
import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.history.domain.HistoryRepository
@ -29,7 +28,6 @@ class LocalListViewModel(
private val repository: LocalMangaRepository, private val repository: LocalMangaRepository,
private val historyRepository: HistoryRepository, private val historyRepository: HistoryRepository,
settings: AppSettings, settings: AppSettings,
private val shortcutsRepository: ShortcutsRepository,
) : MangaListViewModel(settings) { ) : MangaListViewModel(settings) {
val onMangaRemoved = SingleLiveEvent<Unit>() val onMangaRemoved = SingleLiveEvent<Unit>()
@ -107,7 +105,6 @@ class LocalListViewModel(
} }
} }
} }
shortcutsRepository.updateShortcuts()
onMangaRemoved.call(Unit) onMangaRemoved.call(Unit)
} }
} }

@ -1,12 +1,14 @@
package org.koitharu.kotatsu.main package org.koitharu.kotatsu.main
import android.app.Application import android.app.Application
import android.os.Build
import androidx.room.InvalidationTracker
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.bind import org.koin.dsl.bind
import org.koin.dsl.module import org.koin.dsl.module
import org.koitharu.kotatsu.base.ui.util.ActivityRecreationHandle 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.MainViewModel
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
import org.koitharu.kotatsu.main.ui.protect.ProtectViewModel import org.koitharu.kotatsu.main.ui.protect.ProtectViewModel
@ -15,7 +17,13 @@ val mainModule
get() = module { get() = module {
single { AppProtectHelper(get()) } bind Application.ActivityLifecycleCallbacks::class single { AppProtectHelper(get()) } bind Application.ActivityLifecycleCallbacks::class
single { ActivityRecreationHandle() } 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 { MainViewModel(get(), get()) }
viewModel { ProtectViewModel(get(), get()) } viewModel { ProtectViewModel(get(), get()) }
} }

@ -23,7 +23,6 @@ val readerModule
preselectedBranch = params[2], preselectedBranch = params[2],
dataRepository = get(), dataRepository = get(),
historyRepository = get(), historyRepository = get(),
shortcutsRepository = get(),
settings = get(), settings = get(),
pageSaveHelper = get(), pageSaveHelper = get(),
bookmarksRepository = get(), bookmarksRepository = get(),

@ -17,7 +17,6 @@ import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
import org.koitharu.kotatsu.core.exceptions.MangaNotFoundException 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.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.* import org.koitharu.kotatsu.core.prefs.*
import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.history.domain.HistoryRepository
@ -46,7 +45,6 @@ class ReaderViewModel(
private val preselectedBranch: String?, private val preselectedBranch: String?,
private val dataRepository: MangaDataRepository, private val dataRepository: MangaDataRepository,
private val historyRepository: HistoryRepository, private val historyRepository: HistoryRepository,
private val shortcutsRepository: ShortcutsRepository,
private val bookmarksRepository: BookmarksRepository, private val bookmarksRepository: BookmarksRepository,
private val settings: AppSettings, private val settings: AppSettings,
private val pageSaveHelper: PageSaveHelper, private val pageSaveHelper: PageSaveHelper,
@ -289,7 +287,6 @@ class ReaderViewModel(
currentState.value?.let { currentState.value?.let {
val percent = computePercent(it.chapterId, it.page) val percent = computePercent(it.chapterId, it.page)
historyRepository.addOrUpdate(manga, it.chapterId, it.page, it.scroll, percent) historyRepository.addOrUpdate(manga, it.chapterId, it.page, it.scroll, percent)
shortcutsRepository.updateShortcuts()
} }
content.postValue(ReaderContent(pages, currentState.value)) content.postValue(ReaderContent(pages, currentState.value))

Loading…
Cancel
Save