Fix shortcuts

This commit is contained in:
Koitharu
2020-12-17 19:07:22 +02:00
parent abc4ab92a9
commit c132f1d5c4
10 changed files with 111 additions and 136 deletions

View File

@@ -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)
}
}
}
}

View File

@@ -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<ActivityDetailsBinding>(),
R.id.action_shortcut -> {
viewModel.manga.value?.let {
lifecycleScope.launch {
if (!MangaShortcut(it).requestPinShortcut(this@DetailsActivity)) {
if (!get<ShortcutsRepository>().requestPinShortcut(it)) {
Snackbar.make(
binding.pager,
R.string.operation_not_supported,

View File

@@ -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()) }
}

View File

@@ -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<Manga>()
@@ -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()
}
}

View File

@@ -15,5 +15,5 @@ val localModule
single { LocalMangaRepository(androidContext()) }
factory<MangaRepository>(named(MangaSource.LOCAL)) { get<LocalMangaRepository>() }
viewModel { LocalListViewModel(get(), get(), get(), androidContext()) }
viewModel { LocalListViewModel(get(), get(), get(), get(), androidContext()) }
}

View File

@@ -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)
}
}

View File

@@ -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()) }
}

View File

@@ -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())
}
}

View File

@@ -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()
}
}
}

View File

@@ -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<ImageLoader>()
private val mangaRepository by inject<MangaDataRepository>()
@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()
}
}
}