Update in-app update checking
This commit is contained in:
@@ -18,7 +18,7 @@ import org.koin.core.context.startKoin
|
||||
import org.koitharu.kotatsu.bookmarks.bookmarksModule
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.db.databaseModule
|
||||
import org.koitharu.kotatsu.core.github.githubModule
|
||||
import org.koitharu.kotatsu.core.github.appUpdateModule
|
||||
import org.koitharu.kotatsu.core.network.networkModule
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.uiModule
|
||||
@@ -61,7 +61,7 @@ class KotatsuApp : Application() {
|
||||
modules(
|
||||
networkModule,
|
||||
databaseModule,
|
||||
githubModule,
|
||||
appUpdateModule,
|
||||
uiModule,
|
||||
mainModule,
|
||||
searchModule,
|
||||
@@ -136,7 +136,7 @@ class KotatsuApp : Application() {
|
||||
StrictMode.ThreadPolicy.Builder()
|
||||
.detectAll()
|
||||
.penaltyLog()
|
||||
.build()
|
||||
.build(),
|
||||
)
|
||||
StrictMode.setVmPolicy(
|
||||
StrictMode.VmPolicy.Builder()
|
||||
@@ -145,7 +145,7 @@ class KotatsuApp : Application() {
|
||||
.setClassInstanceLimit(PagesCache::class.java, 1)
|
||||
.setClassInstanceLimit(MangaLoaderContext::class.java, 1)
|
||||
.penaltyLog()
|
||||
.build()
|
||||
.build(),
|
||||
)
|
||||
FragmentStrictMode.defaultPolicy = FragmentStrictMode.Policy.Builder()
|
||||
.penaltyDeath()
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.koitharu.kotatsu.core.github
|
||||
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.dsl.module
|
||||
|
||||
val appUpdateModule
|
||||
get() = module {
|
||||
single { AppUpdateRepository(androidContext(), get()) }
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package org.koitharu.kotatsu.core.github
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStream
|
||||
import java.security.MessageDigest
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.cert.X509Certificate
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.parsers.util.await
|
||||
import org.koitharu.kotatsu.parsers.util.byte2HexFormatted
|
||||
import org.koitharu.kotatsu.parsers.util.json.mapJSONNotNull
|
||||
import org.koitharu.kotatsu.parsers.util.parseJsonArray
|
||||
import org.koitharu.kotatsu.utils.ext.asArrayList
|
||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||
|
||||
private const val CERT_SHA1 = "2C:19:C7:E8:07:61:2B:8E:94:51:1B:FD:72:67:07:64:5D:C2:58:AE"
|
||||
|
||||
class AppUpdateRepository(
|
||||
private val context: Context,
|
||||
private val okHttp: OkHttpClient,
|
||||
) {
|
||||
|
||||
private val availableUpdate = MutableStateFlow<AppVersion?>(null)
|
||||
|
||||
fun observeAvailableUpdate() = availableUpdate.asStateFlow()
|
||||
|
||||
suspend fun getAvailableVersions(): List<AppVersion> {
|
||||
val request = Request.Builder()
|
||||
.get()
|
||||
.url("https://api.github.com/repos/KotatsuApp/Kotatsu/releases?page=1&per_page=10")
|
||||
val jsonArray = okHttp.newCall(request.build()).await().parseJsonArray()
|
||||
return jsonArray.mapJSONNotNull { json ->
|
||||
val asset = json.optJSONArray("assets")?.optJSONObject(0) ?: return@mapJSONNotNull null
|
||||
AppVersion(
|
||||
id = json.getLong("id"),
|
||||
url = json.getString("html_url"),
|
||||
name = json.getString("name").removePrefix("v"),
|
||||
apkSize = asset.getLong("size"),
|
||||
apkUrl = asset.getString("browser_download_url"),
|
||||
description = json.getString("body"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun fetchUpdate(): AppVersion? {
|
||||
if (!isUpdateSupported()) {
|
||||
return null
|
||||
}
|
||||
return runCatching {
|
||||
val currentVersion = VersionId(BuildConfig.VERSION_NAME)
|
||||
val available = getAvailableVersions().asArrayList()
|
||||
available.sortBy { it.versionId }
|
||||
if (currentVersion.isStable) {
|
||||
available.retainAll { it.versionId.isStable }
|
||||
}
|
||||
available.maxByOrNull { it.versionId }
|
||||
?.takeIf { it.versionId > currentVersion }
|
||||
}.onFailure {
|
||||
it.printStackTraceDebug()
|
||||
}.onSuccess {
|
||||
availableUpdate.value = it
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
fun isUpdateSupported(): Boolean {
|
||||
return BuildConfig.DEBUG || getCertificateSHA1Fingerprint() == CERT_SHA1
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@SuppressLint("PackageManagerGetSignatures")
|
||||
private fun getCertificateSHA1Fingerprint(): String? = runCatching {
|
||||
val packageInfo = context.packageManager.getPackageInfo(context.packageName, PackageManager.GET_SIGNATURES)
|
||||
val signatures = requireNotNull(packageInfo?.signatures)
|
||||
val cert: ByteArray = signatures.first().toByteArray()
|
||||
val input: InputStream = ByteArrayInputStream(cert)
|
||||
val cf = CertificateFactory.getInstance("X509")
|
||||
val c = cf.generateCertificate(input) as X509Certificate
|
||||
val md: MessageDigest = MessageDigest.getInstance("SHA1")
|
||||
val publicKey: ByteArray = md.digest(c.encoded)
|
||||
return publicKey.byte2HexFormatted()
|
||||
}.onFailure { error ->
|
||||
error.printStackTraceDebug()
|
||||
}.getOrNull()
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.koitharu.kotatsu.core.github
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
@@ -10,5 +11,9 @@ data class AppVersion(
|
||||
val url: String,
|
||||
val apkSize: Long,
|
||||
val apkUrl: String,
|
||||
val description: String
|
||||
) : Parcelable
|
||||
val description: String,
|
||||
) : Parcelable {
|
||||
|
||||
@IgnoredOnParcel
|
||||
val versionId = VersionId(name)
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.github
|
||||
|
||||
import org.koin.dsl.module
|
||||
|
||||
val githubModule
|
||||
get() = module {
|
||||
factory { GithubRepository(get()) }
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.github
|
||||
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.koitharu.kotatsu.parsers.util.await
|
||||
import org.koitharu.kotatsu.parsers.util.parseJson
|
||||
|
||||
class GithubRepository(private val okHttp: OkHttpClient) {
|
||||
|
||||
suspend fun getLatestVersion(): AppVersion {
|
||||
val request = Request.Builder()
|
||||
.get()
|
||||
.url("https://api.github.com/repos/KotatsuApp/Kotatsu/releases/latest")
|
||||
val json = okHttp.newCall(request.build()).await().parseJson()
|
||||
val asset = json.getJSONArray("assets").getJSONObject(0)
|
||||
return AppVersion(
|
||||
id = json.getLong("id"),
|
||||
url = json.getString("html_url"),
|
||||
name = json.getString("name").removePrefix("v"),
|
||||
apkSize = asset.getLong("size"),
|
||||
apkUrl = asset.getString("browser_download_url"),
|
||||
description = json.getString("body")
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,9 @@ class VersionId(
|
||||
}
|
||||
}
|
||||
|
||||
val VersionId.isStable: Boolean
|
||||
get() = variantType.isEmpty()
|
||||
|
||||
fun VersionId(versionName: String): VersionId {
|
||||
val parts = versionName.substringBeforeLast('-').split('.')
|
||||
val variant = versionName.substringAfterLast('-', "")
|
||||
@@ -73,4 +76,4 @@ fun VersionId(versionName: String): VersionId {
|
||||
variantType = variant.filter(Char::isLetter),
|
||||
variantNumber = variant.filter(Char::isDigit).toIntOrNull() ?: 0,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,6 @@ val mainModule
|
||||
factory { ShortcutsUpdater(androidContext(), get(), get(), get()) }
|
||||
}
|
||||
|
||||
viewModel { MainViewModel(get(), get()) }
|
||||
viewModel { MainViewModel(get(), get(), get(), get()) }
|
||||
viewModel { ProtectViewModel(get(), get()) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.koitharu.kotatsu.main.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.SparseIntArray
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup.MarginLayoutParams
|
||||
@@ -9,12 +10,14 @@ import androidx.annotation.IdRes
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.util.size
|
||||
import androidx.core.view.*
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentTransaction
|
||||
import androidx.fragment.app.commit
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.transition.TransitionManager
|
||||
import com.google.android.material.R as materialR
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import com.google.android.material.appbar.AppBarLayout.LayoutParams.*
|
||||
import com.google.android.material.navigation.NavigationBarView
|
||||
@@ -40,7 +43,6 @@ import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity
|
||||
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionFragment
|
||||
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
|
||||
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel
|
||||
import org.koitharu.kotatsu.settings.AppUpdateChecker
|
||||
import org.koitharu.kotatsu.settings.newsources.NewSourcesDialogFragment
|
||||
import org.koitharu.kotatsu.settings.onboard.OnboardDialogFragment
|
||||
import org.koitharu.kotatsu.settings.tools.ToolsFragment
|
||||
@@ -50,7 +52,6 @@ import org.koitharu.kotatsu.tracker.ui.FeedFragment
|
||||
import org.koitharu.kotatsu.tracker.work.TrackWorker
|
||||
import org.koitharu.kotatsu.utils.VoiceInputContract
|
||||
import org.koitharu.kotatsu.utils.ext.*
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
private const val TAG_PRIMARY = "primary"
|
||||
private const val TAG_SEARCH = "search"
|
||||
@@ -60,7 +61,8 @@ class MainActivity :
|
||||
AppBarOwner,
|
||||
View.OnClickListener,
|
||||
View.OnFocusChangeListener,
|
||||
SearchSuggestionListener, NavigationBarView.OnItemSelectedListener {
|
||||
SearchSuggestionListener,
|
||||
NavigationBarView.OnItemSelectedListener {
|
||||
|
||||
private val viewModel by viewModel<MainViewModel>()
|
||||
private val searchSuggestionViewModel by viewModel<SearchSuggestionViewModel>()
|
||||
@@ -108,6 +110,7 @@ class MainActivity :
|
||||
viewModel.onError.observe(this, this::onError)
|
||||
viewModel.isLoading.observe(this, this::onLoadingStateChanged)
|
||||
viewModel.isResumeEnabled.observe(this, this::onResumeEnabledChanged)
|
||||
viewModel.counters.observe(this, ::onCountersChanged)
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||
@@ -269,6 +272,20 @@ class MainActivity :
|
||||
Snackbar.make(binding.container, e.getDisplayMessage(resources), Snackbar.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun onCountersChanged(counters: SparseIntArray) {
|
||||
repeat(counters.size) { i ->
|
||||
val id = counters.keyAt(i)
|
||||
val counter = counters.valueAt(i)
|
||||
if (counter == 0) {
|
||||
navBar.getBadge(id)?.isVisible = false
|
||||
} else {
|
||||
val badge = navBar.getOrCreateBadge(id)
|
||||
badge.number = counter
|
||||
badge.isVisible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onLoadingStateChanged(isLoading: Boolean) {
|
||||
binding.fab?.isEnabled = !isLoading
|
||||
}
|
||||
@@ -330,13 +347,9 @@ class MainActivity :
|
||||
|
||||
private fun onFirstStart() {
|
||||
lifecycleScope.launchWhenResumed {
|
||||
val isUpdateSupported = withContext(Dispatchers.Default) {
|
||||
withContext(Dispatchers.Default) {
|
||||
TrackWorker.setup(applicationContext)
|
||||
SuggestionsWorker.setup(applicationContext)
|
||||
AppUpdateChecker.isUpdateSupported(this@MainActivity)
|
||||
}
|
||||
if (isUpdateSupported) {
|
||||
AppUpdateChecker(this@MainActivity).checkIfNeeded()
|
||||
}
|
||||
val settings = get<AppSettings>()
|
||||
when {
|
||||
@@ -378,4 +391,4 @@ class MainActivity :
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,49 +1,54 @@
|
||||
package org.koitharu.kotatsu.main.ui
|
||||
|
||||
import android.util.SparseIntArray
|
||||
import androidx.core.util.set
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
|
||||
import org.koitharu.kotatsu.core.prefs.AppSection
|
||||
import org.koitharu.kotatsu.core.github.AppUpdateRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsLiveData
|
||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
||||
|
||||
class MainViewModel(
|
||||
private val historyRepository: HistoryRepository,
|
||||
private val settings: AppSettings,
|
||||
private val appUpdateRepository: AppUpdateRepository,
|
||||
private val trackingRepository: TrackingRepository,
|
||||
) : BaseViewModel() {
|
||||
|
||||
val onOpenReader = SingleLiveEvent<Manga>()
|
||||
var defaultSection: AppSection
|
||||
get() = settings.defaultSection
|
||||
set(value) {
|
||||
settings.defaultSection = value
|
||||
}
|
||||
|
||||
val isSuggestionsEnabled = settings.observeAsLiveData(
|
||||
context = viewModelScope.coroutineContext + Dispatchers.Default,
|
||||
key = AppSettings.KEY_SUGGESTIONS,
|
||||
valueProducer = { isSuggestionsEnabled },
|
||||
)
|
||||
|
||||
val isTrackerEnabled = settings.observeAsLiveData(
|
||||
context = viewModelScope.coroutineContext + Dispatchers.Default,
|
||||
key = AppSettings.KEY_TRACKER_ENABLED,
|
||||
valueProducer = { isTrackerEnabled },
|
||||
)
|
||||
|
||||
val isResumeEnabled = historyRepository
|
||||
.observeHasItems()
|
||||
.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, false)
|
||||
|
||||
val counters = combine(
|
||||
appUpdateRepository.observeAvailableUpdate(),
|
||||
trackingRepository.observeUpdatedMangaCount(),
|
||||
) { appUpdate, tracks ->
|
||||
val a = SparseIntArray(2)
|
||||
a[R.id.nav_tools] = if (appUpdate != null) 1 else 0
|
||||
a[R.id.nav_feed] = tracks
|
||||
a
|
||||
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, SparseIntArray(0))
|
||||
|
||||
init {
|
||||
launchJob(Dispatchers.Default) {
|
||||
appUpdateRepository.fetchUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
fun openLastReader() {
|
||||
launchLoadingJob {
|
||||
val manga = historyRepository.getLastOrNull() ?: throw EmptyHistoryException()
|
||||
onOpenReader.call(manga)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,31 +7,32 @@ import android.content.pm.PackageManager
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.core.net.toUri
|
||||
import com.google.android.material.R as materialR
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.android.ext.android.get
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.github.AppVersion
|
||||
import org.koitharu.kotatsu.core.github.GithubRepository
|
||||
import org.koitharu.kotatsu.core.github.VersionId
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.parsers.util.byte2HexFormatted
|
||||
import org.koitharu.kotatsu.utils.FileSize
|
||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStream
|
||||
import java.security.MessageDigest
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.concurrent.TimeUnit
|
||||
import com.google.android.material.R as materialR
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.android.ext.android.get
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.github.AppUpdateRepository
|
||||
import org.koitharu.kotatsu.core.github.AppVersion
|
||||
import org.koitharu.kotatsu.core.github.VersionId
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.parsers.util.byte2HexFormatted
|
||||
import org.koitharu.kotatsu.utils.FileSize
|
||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||
|
||||
@Deprecated("")
|
||||
class AppUpdateChecker(private val activity: ComponentActivity) {
|
||||
|
||||
private val settings = activity.get<AppSettings>()
|
||||
private val repo = activity.get<GithubRepository>()
|
||||
private val repo = activity.get<AppUpdateRepository>()
|
||||
|
||||
suspend fun checkIfNeeded(): Boolean? = if (
|
||||
settings.isUpdateCheckingEnabled &&
|
||||
@@ -43,7 +44,7 @@ class AppUpdateChecker(private val activity: ComponentActivity) {
|
||||
}
|
||||
|
||||
suspend fun checkNow() = runCatching {
|
||||
val version = repo.getLatestVersion()
|
||||
val version = repo.fetchUpdate() ?: return@runCatching false
|
||||
val newVersionId = VersionId(version.name)
|
||||
val currentVersionId = VersionId(BuildConfig.VERSION_NAME)
|
||||
val result = newVersionId > currentVersionId
|
||||
@@ -107,4 +108,4 @@ class AppUpdateChecker(private val activity: ComponentActivity) {
|
||||
error.printStackTraceDebug()
|
||||
}.getOrNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,5 +35,5 @@ val settingsModule
|
||||
viewModel { OnboardViewModel(get()) }
|
||||
viewModel { SourcesSettingsViewModel(get()) }
|
||||
viewModel { NewSourcesViewModel(get()) }
|
||||
viewModel { ToolsViewModel(get(), get()) }
|
||||
viewModel { ToolsViewModel(get(), get(), get()) }
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.koitharu.kotatsu.settings.tools
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.res.ColorStateList
|
||||
import android.os.Bundle
|
||||
import android.transition.TransitionManager
|
||||
@@ -10,6 +11,7 @@ import android.widget.CompoundButton
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.core.widget.TextViewCompat
|
||||
@@ -19,9 +21,9 @@ import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseFragment
|
||||
import org.koitharu.kotatsu.base.ui.widgets.SegmentedBarView
|
||||
import org.koitharu.kotatsu.core.github.AppVersion
|
||||
import org.koitharu.kotatsu.databinding.FragmentToolsBinding
|
||||
import org.koitharu.kotatsu.download.ui.DownloadsActivity
|
||||
import org.koitharu.kotatsu.settings.AppUpdateChecker
|
||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
||||
import org.koitharu.kotatsu.settings.tools.model.StorageUsage
|
||||
import org.koitharu.kotatsu.utils.FileSize
|
||||
@@ -32,7 +34,6 @@ class ToolsFragment :
|
||||
CompoundButton.OnCheckedChangeListener,
|
||||
View.OnClickListener {
|
||||
|
||||
private var updateChecker: AppUpdateChecker? = null
|
||||
private val viewModel by viewModel<ToolsViewModel>()
|
||||
|
||||
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): FragmentToolsBinding {
|
||||
@@ -51,12 +52,19 @@ class ToolsFragment :
|
||||
binding.switchIncognito.isChecked = it
|
||||
}
|
||||
viewModel.storageUsage.observe(viewLifecycleOwner, ::onStorageUsageChanged)
|
||||
viewModel.appUpdate.observe(viewLifecycleOwner, ::onAppUpdateAvailable)
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
when (v.id) {
|
||||
R.id.button_settings -> startActivity(SettingsActivity.newIntent(v.context))
|
||||
R.id.button_downloads -> startActivity(DownloadsActivity.newIntent(v.context))
|
||||
R.id.button_download -> {
|
||||
val url = viewModel.appUpdate.value?.apkUrl ?: return
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.data = url.toUri()
|
||||
startActivity(Intent.createChooser(intent, getString(R.string.open_in_browser)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +80,15 @@ class ToolsFragment :
|
||||
)
|
||||
}
|
||||
|
||||
private fun onAppUpdateAvailable(version: AppVersion?) {
|
||||
if (version == null) {
|
||||
binding.cardUpdate.root.isVisible = false
|
||||
return
|
||||
}
|
||||
binding.cardUpdate.textPrimary.text = getString(R.string.new_version_s, version.name)
|
||||
binding.cardUpdate.root.isVisible = true
|
||||
}
|
||||
|
||||
private fun onStorageUsageChanged(usage: StorageUsage) {
|
||||
val storageSegment = SegmentedBarView.Segment(usage.savedManga.percent, segmentColor(1))
|
||||
val pagesSegment = SegmentedBarView.Segment(usage.pagesCache.percent, segmentColor(2))
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package org.koitharu.kotatsu.settings.tools
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.liveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.github.AppUpdateRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsLiveData
|
||||
import org.koitharu.kotatsu.local.data.CacheDir
|
||||
@@ -13,9 +15,13 @@ import org.koitharu.kotatsu.settings.tools.model.StorageUsage
|
||||
|
||||
class ToolsViewModel(
|
||||
private val storageManager: LocalStorageManager,
|
||||
private val appUpdateRepository: AppUpdateRepository,
|
||||
private val settings: AppSettings,
|
||||
) : BaseViewModel() {
|
||||
|
||||
val appUpdate = appUpdateRepository.observeAvailableUpdate()
|
||||
.asLiveData(viewModelScope.coroutineContext)
|
||||
|
||||
val storageUsage: LiveData<StorageUsage> = liveData(
|
||||
context = viewModelScope.coroutineContext + Dispatchers.Default,
|
||||
) {
|
||||
|
||||
@@ -18,6 +18,9 @@ abstract class TracksDao {
|
||||
@Query("SELECT chapters_new FROM tracks WHERE manga_id = :mangaId")
|
||||
abstract suspend fun findNewChapters(mangaId: Long): Int?
|
||||
|
||||
@Query("SELECT chapters_new FROM tracks")
|
||||
abstract fun observeNewChapters(): Flow<List<Int>>
|
||||
|
||||
@Query("SELECT chapters_new FROM tracks WHERE manga_id = :mangaId")
|
||||
abstract fun observeNewChapters(mangaId: Long): Flow<Int?>
|
||||
|
||||
@@ -42,4 +45,4 @@ abstract class TracksDao {
|
||||
insert(entity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.tracker.domain
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.room.withTransaction
|
||||
import java.util.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
@@ -18,7 +19,6 @@ import org.koitharu.kotatsu.tracker.data.toTrackingLogItem
|
||||
import org.koitharu.kotatsu.tracker.domain.model.MangaTracking
|
||||
import org.koitharu.kotatsu.tracker.domain.model.MangaUpdates
|
||||
import org.koitharu.kotatsu.tracker.domain.model.TrackingLogItem
|
||||
import java.util.*
|
||||
|
||||
private const val NO_ID = 0L
|
||||
|
||||
@@ -34,6 +34,10 @@ class TrackingRepository(
|
||||
return db.tracksDao.observeNewChapters(mangaId).map { it ?: 0 }
|
||||
}
|
||||
|
||||
fun observeUpdatedMangaCount(): Flow<Int> {
|
||||
return db.tracksDao.observeNewChapters().map { list -> list.count { it > 0 } }
|
||||
}
|
||||
|
||||
suspend fun getTracks(mangaList: Collection<Manga>): List<MangaTracking> {
|
||||
val ids = mangaList.mapToSet { it.id }
|
||||
val tracks = db.tracksDao.findAll(ids).groupBy { it.mangaId }
|
||||
@@ -47,7 +51,7 @@ class TrackingRepository(
|
||||
result += MangaTracking(
|
||||
manga = item,
|
||||
lastChapterId = track?.lastChapterId ?: NO_ID,
|
||||
lastCheck = track?.lastCheck?.takeUnless { it == 0L }?.let(::Date)
|
||||
lastCheck = track?.lastCheck?.takeUnless { it == 0L }?.let(::Date),
|
||||
)
|
||||
}
|
||||
return result
|
||||
@@ -59,7 +63,7 @@ class TrackingRepository(
|
||||
return MangaTracking(
|
||||
manga = manga,
|
||||
lastChapterId = track?.lastChapterId ?: NO_ID,
|
||||
lastCheck = track?.lastCheck?.takeUnless { it == 0L }?.let(::Date)
|
||||
lastCheck = track?.lastCheck?.takeUnless { it == 0L }?.let(::Date),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -166,4 +170,4 @@ class TrackingRepository(
|
||||
}
|
||||
|
||||
private fun Collection<MangaEntity>.toMangaList() = map { it.toManga(emptySet()) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@ package org.koitharu.kotatsu.utils.ext
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.liveData
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.koitharu.kotatsu.utils.BufferedObserver
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
fun <T> LiveData<T>.requireValue(): T = checkNotNull(value) {
|
||||
"LiveData value is null"
|
||||
@@ -22,12 +22,12 @@ fun <T> LiveData<T>.observeWithPrevious(owner: LifecycleOwner, observer: Buffere
|
||||
}
|
||||
|
||||
fun <T> StateFlow<T>.asLiveDataDistinct(
|
||||
context: CoroutineContext = EmptyCoroutineContext
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
): LiveData<T> = asLiveDataDistinct(context, value)
|
||||
|
||||
fun <T> Flow<T>.asLiveDataDistinct(
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
defaultValue: T
|
||||
defaultValue: T,
|
||||
): LiveData<T> = liveData(context) {
|
||||
if (latestValue == null) {
|
||||
emit(defaultValue)
|
||||
@@ -37,4 +37,4 @@ fun <T> Flow<T>.asLiveDataDistinct(
|
||||
emit(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.core.widget.NestedScrollView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
@@ -14,7 +15,9 @@
|
||||
layout="@layout/layout_app_update"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/margin_normal" />
|
||||
android:layout_margin="@dimen/margin_normal"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<include
|
||||
android:id="@+id/layout_sync"
|
||||
|
||||
@@ -9,10 +9,10 @@ import org.junit.Test
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.parsers.util.await
|
||||
|
||||
class GithubRepositoryTest {
|
||||
class AppUpdateRepositoryTest {
|
||||
|
||||
private val okHttpClient = OkHttpClient()
|
||||
private val repository = GithubRepository(okHttpClient)
|
||||
private val repository = AppUpdateRepository(okHttpClient)
|
||||
|
||||
@Test
|
||||
fun getLatestVersion() = runTest {
|
||||
@@ -23,11 +23,11 @@ class GithubRepositoryTest {
|
||||
Request.Builder()
|
||||
.url(version.apkUrl)
|
||||
.head()
|
||||
.build()
|
||||
.build(),
|
||||
).await()
|
||||
|
||||
Assert.assertTrue(versionId <= VersionId(BuildConfig.VERSION_NAME))
|
||||
Assert.assertTrue(apkHead.isSuccessful)
|
||||
Assert.assertEquals(version.apkSize, apkHead.headersContentLength())
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user