diff --git a/app/src/main/java/org/koitharu/kotatsu/core/os/ShortcutsRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/os/ShortcutsRepository.kt new file mode 100644 index 000000000..03cfdbbe4 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/os/ShortcutsRepository.kt @@ -0,0 +1,87 @@ +package org.koitharu.kotatsu.core.os + +import android.app.ActivityManager +import android.content.Context +import android.content.pm.ShortcutManager +import android.media.ThumbnailUtils +import android.os.Build +import androidx.core.content.pm.ShortcutInfoCompat +import androidx.core.content.pm.ShortcutManagerCompat +import androidx.core.graphics.drawable.IconCompat +import coil.ImageLoader +import coil.request.ImageRequest +import coil.size.PixelSize +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.domain.MangaDataRepository +import org.koitharu.kotatsu.core.model.Manga +import org.koitharu.kotatsu.history.domain.HistoryRepository +import org.koitharu.kotatsu.reader.ui.ReaderActivity +import org.koitharu.kotatsu.utils.ext.requireBitmap + +class ShortcutsRepository( + private val context: Context, + private val coil: ImageLoader, + private val historyRepository: HistoryRepository, + private val mangaRepository: MangaDataRepository +) { + + private val iconSize by lazy { + getIconSize(context) + } + + 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) + .map { buildShortcutInfo(it).build().toShortcutInfo() } + manager.dynamicShortcuts = shortcuts + } + + suspend fun requestPinShortcut(manga: Manga): Boolean { + return ShortcutManagerCompat.requestPinShortcut( + context, + buildShortcutInfo(manga).build(), + null + ) + } + + 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) + .build() + ).requireBitmap() + ThumbnailUtils.extractThumbnail(bmp, iconSize.width, iconSize.height, 0) + } + }.fold( + onSuccess = { IconCompat.createWithAdaptiveBitmap(it) }, + onFailure = { IconCompat.createWithResource(context, R.drawable.ic_shortcut_default) } + ) + mangaRepository.storeManga(manga) + return ShortcutInfoCompat.Builder(context, manga.id.toString()) + .setShortLabel(manga.title) + .setLongLabel(manga.title) + .setIcon(icon) + .setIntent( + ReaderActivity.newIntent(context, manga.id, null) + .setAction(ReaderActivity.ACTION_MANGA_READ) + ) + } + + private fun getIconSize(context: Context): PixelSize { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { + (context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager).let { + PixelSize(it.iconMaxWidth, it.iconMaxHeight) + } + } else { + (context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager).launcherLargeIconSize.let { + PixelSize(it, it) + } + } + } +} \ No newline at end of file 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 57ffd22ec..7a49906d7 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 @@ -20,6 +20,7 @@ import com.google.android.material.snackbar.Snackbar import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator import kotlinx.coroutines.launch +import org.koin.android.ext.android.get import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf import org.koitharu.kotatsu.BuildConfig @@ -31,10 +32,10 @@ import org.koitharu.kotatsu.browser.cloudflare.CloudFlareDialog import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.MangaSource +import org.koitharu.kotatsu.core.os.ShortcutsRepository import org.koitharu.kotatsu.databinding.ActivityDetailsBinding import org.koitharu.kotatsu.download.DownloadService import org.koitharu.kotatsu.search.ui.global.GlobalSearchActivity -import org.koitharu.kotatsu.utils.MangaShortcut import org.koitharu.kotatsu.utils.ShareHelper import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getThemeColor @@ -197,7 +198,7 @@ class DetailsActivity : BaseActivity(), R.id.action_shortcut -> { viewModel.manga.value?.let { lifecycleScope.launch { - if (!MangaShortcut(it).requestPinShortcut(this@DetailsActivity)) { + if (!get().requestPinShortcut(it)) { Snackbar.make( binding.pager, 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 28587b063..6ae824d2b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/HistoryModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/HistoryModule.kt @@ -1,6 +1,5 @@ package org.koitharu.kotatsu.history -import org.koin.android.ext.koin.androidContext import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module import org.koitharu.kotatsu.history.domain.HistoryRepository @@ -10,5 +9,5 @@ val historyModule get() = module { single { HistoryRepository(get()) } - viewModel { HistoryListViewModel(get(), androidContext(), 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 74f8b58d9..f49ff0522 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 @@ -1,13 +1,12 @@ package org.koitharu.kotatsu.history.ui -import android.content.Context -import android.os.Build import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.* import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.model.Manga +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.ui.DateTimeAgo @@ -15,7 +14,6 @@ import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.history.domain.MangaWithHistory import org.koitharu.kotatsu.list.ui.MangaListViewModel import org.koitharu.kotatsu.list.ui.model.* -import org.koitharu.kotatsu.utils.MangaShortcut import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.ext.asLiveData import org.koitharu.kotatsu.utils.ext.daysDiff @@ -26,8 +24,8 @@ import kotlin.collections.ArrayList class HistoryListViewModel( private val repository: HistoryRepository, - private val context: Context, //todo create ShortcutRepository - private val settings: AppSettings + private val settings: AppSettings, + private val shortcutsRepository: ShortcutsRepository ) : MangaListViewModel(settings) { val onItemRemoved = SingleLiveEvent() @@ -62,9 +60,7 @@ class HistoryListViewModel( fun clearHistory() { launchLoadingJob { repository.clear() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { - MangaShortcut.clearAppShortcuts(context) - } + shortcutsRepository.updateShortcuts() } } @@ -72,9 +68,7 @@ class HistoryListViewModel( launchJob { repository.delete(manga) onItemRemoved.call(manga) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { - MangaShortcut(manga).removeAppShortcut(context) - } + shortcutsRepository.updateShortcuts() } } 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 57ab25975..b2a756cc1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/LocalModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/LocalModule.kt @@ -15,5 +15,5 @@ val localModule single { LocalMangaRepository(androidContext()) } factory(named(MangaSource.LOCAL)) { get() } - viewModel { LocalListViewModel(get(), get(), get(), androidContext()) } + viewModel { LocalListViewModel(get(), get(), get(), get(), androidContext()) } } \ 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 57b634678..1910b3e5f 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 @@ -2,7 +2,6 @@ package org.koitharu.kotatsu.local.ui import android.content.Context import android.net.Uri -import android.os.Build import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers @@ -14,6 +13,7 @@ import kotlinx.coroutines.withContext import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException import org.koitharu.kotatsu.core.model.Manga +import org.koitharu.kotatsu.core.os.ShortcutsRepository import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.list.ui.MangaListViewModel @@ -22,7 +22,6 @@ import org.koitharu.kotatsu.list.ui.model.LoadingState import org.koitharu.kotatsu.list.ui.model.toErrorState import org.koitharu.kotatsu.list.ui.model.toUi import org.koitharu.kotatsu.local.domain.LocalMangaRepository -import org.koitharu.kotatsu.utils.MangaShortcut import org.koitharu.kotatsu.utils.MediaStoreCompat import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.ext.safe @@ -33,6 +32,7 @@ class LocalListViewModel( private val repository: LocalMangaRepository, private val historyRepository: HistoryRepository, private val settings: AppSettings, + private val shortcutsRepository: ShortcutsRepository, private val context: Context ) : MangaListViewModel(settings) { @@ -102,9 +102,7 @@ class LocalListViewModel( historyRepository.deleteOrSwap(manga, original) } } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { - MangaShortcut(manga).removeAppShortcut(context) - } + shortcutsRepository.updateShortcuts() onMangaRemoved.call(manga) } } 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 7414512d9..f8a46d4d6 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/MainModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/main/MainModule.kt @@ -1,7 +1,9 @@ package org.koitharu.kotatsu.main +import org.koin.android.ext.koin.androidContext import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module +import org.koitharu.kotatsu.core.os.ShortcutsRepository import org.koitharu.kotatsu.main.ui.MainViewModel import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper import org.koitharu.kotatsu.main.ui.protect.ProtectViewModel @@ -9,6 +11,7 @@ import org.koitharu.kotatsu.main.ui.protect.ProtectViewModel val mainModule get() = module { single { AppProtectHelper(get()) } + single { ShortcutsRepository(androidContext(), get(), get(), get()) } viewModel { MainViewModel(get(), get()) } viewModel { ProtectViewModel(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 3d26814e2..d031b9aec 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ReaderModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ReaderModule.kt @@ -15,6 +15,6 @@ val readerModule single { PagesCache(get()) } viewModel { (intent: MangaIntent, state: ReaderState?) -> - ReaderViewModel(intent, state, get(), get(), get()) + ReaderViewModel(intent, state, get(), get(), get(), get()) } } \ No newline at end of file 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 dd21b6789..46dc4fa12 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 @@ -21,6 +21,7 @@ import org.koitharu.kotatsu.core.exceptions.MangaNotFoundException import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.MangaChapter import org.koitharu.kotatsu.core.model.MangaPage +import org.koitharu.kotatsu.core.os.ShortcutsRepository import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.ReaderMode import org.koitharu.kotatsu.history.domain.HistoryRepository @@ -35,6 +36,7 @@ class ReaderViewModel( state: ReaderState?, private val dataRepository: MangaDataRepository, private val historyRepository: HistoryRepository, + private val shortcutsRepository: ShortcutsRepository, private val settings: AppSettings ) : BaseViewModel() { @@ -105,6 +107,11 @@ class ReaderViewModel( val pages = loadChapter(requireNotNull(currentState.value).chapterId) content.postValue(ReaderContent(pages, currentState.value)) + // save state + currentState.value?.let { + historyRepository.addOrUpdate(manga, it.chapterId, it.page, it.scroll) + shortcutsRepository.updateShortcuts() + } } } diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/MangaShortcut.kt b/app/src/main/java/org/koitharu/kotatsu/utils/MangaShortcut.kt deleted file mode 100644 index d112091f0..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/utils/MangaShortcut.kt +++ /dev/null @@ -1,114 +0,0 @@ -package org.koitharu.kotatsu.utils - -import android.app.ActivityManager -import android.content.Context -import android.content.pm.ShortcutManager -import android.media.ThumbnailUtils -import android.os.Build -import androidx.annotation.RequiresApi -import androidx.core.content.pm.ShortcutInfoCompat -import androidx.core.content.pm.ShortcutManagerCompat -import androidx.core.graphics.drawable.IconCompat -import coil.ImageLoader -import coil.request.ImageRequest -import coil.size.PixelSize -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.domain.MangaDataRepository -import org.koitharu.kotatsu.core.model.Manga -import org.koitharu.kotatsu.reader.ui.ReaderActivity -import org.koitharu.kotatsu.utils.ext.requireBitmap -import org.koitharu.kotatsu.utils.ext.safe - -class MangaShortcut(private val manga: Manga) : KoinComponent { - - private val shortcutId = manga.id.toString() - private val coil by inject() - private val mangaRepository by inject() - - @RequiresApi(Build.VERSION_CODES.N_MR1) - suspend fun addAppShortcut(context: Context) { - val manager = context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager - val limit = manager.maxShortcutCountPerActivity - val builder = buildShortcutInfo(context, manga) - val shortcuts = manager.dynamicShortcuts - for (shortcut in shortcuts) { - if (shortcut.id == shortcutId) { - builder.setRank(shortcut.rank + 1) - manager.updateShortcuts(listOf(builder.build().toShortcutInfo())) - return - } - } - builder.setRank(1) - if (shortcuts.isNotEmpty() && shortcuts.size >= limit) { - manager.removeDynamicShortcuts(listOf(shortcuts.minByOrNull { it.rank }!!.id)) - } - manager.addDynamicShortcuts(listOf(builder.build().toShortcutInfo())) - } - - suspend fun requestPinShortcut(context: Context): Boolean { - return ShortcutManagerCompat.requestPinShortcut( - context, - buildShortcutInfo(context, manga).build(), - null - ) - } - - @RequiresApi(Build.VERSION_CODES.N_MR1) - fun removeAppShortcut(context: Context) { - val manager = context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager - manager.removeDynamicShortcuts(listOf(shortcutId)) - } - - private suspend fun buildShortcutInfo( - context: Context, - manga: Manga - ): ShortcutInfoCompat.Builder { - val icon = safe { - val size = getIconSize(context) - withContext(Dispatchers.IO) { - val bmp = coil.execute( - ImageRequest.Builder(context) - .data(manga.coverUrl) - .build() - ).requireBitmap() - ThumbnailUtils.extractThumbnail(bmp, size.width, size.height, 0) - } - } - mangaRepository.storeManga(manga) - return ShortcutInfoCompat.Builder(context, manga.id.toString()) - .setShortLabel(manga.title) - .setLongLabel(manga.title) - .setIcon(icon?.let { - IconCompat.createWithAdaptiveBitmap(it) - } ?: IconCompat.createWithResource(context, R.drawable.ic_shortcut_default)) - .setIntent( - ReaderActivity.newIntent(context, manga.id, null) - .setAction(ReaderActivity.ACTION_MANGA_READ) - ) - } - - private fun getIconSize(context: Context): PixelSize { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { - (context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager).let { - PixelSize(it.iconMaxWidth, it.iconMaxHeight) - } - } else { - (context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager).launcherLargeIconSize.let { - PixelSize(it, it) - } - } - } - - companion object { - - @RequiresApi(Build.VERSION_CODES.N_MR1) - fun clearAppShortcuts(context: Context) { - val manager = context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager - manager.removeAllDynamicShortcuts() - } - } -} \ No newline at end of file