Compare commits

..

7 Commits

Author SHA1 Message Date
a0aa33a499 Option to clear updates feed 2020-06-29 13:43:01 +03:00
b27bc86141 Fix search history preference 2020-06-29 13:28:25 +03:00
84ef2af82f Fix MangaDetailsPresenter sharing 2020-06-29 13:26:28 +03:00
a2f09d8763 Restore download after network error 2020-06-29 09:48:17 +03:00
79058440a1 Fix grid span count #11 2020-06-25 19:40:50 +03:00
7f9cfdbf7a Show app update in dialog 2020-06-20 12:10:42 +03:00
85f7477450 Cleaning up traks 2020-06-20 11:35:42 +03:00
39 changed files with 377 additions and 299 deletions

View File

@@ -3,6 +3,7 @@
<words>
<w>chucker</w>
<w>desu</w>
<w>failsafe</w>
<w>koin</w>
<w>kotatsu</w>
<w>manga</w>

View File

@@ -16,7 +16,7 @@ android {
minSdkVersion 21
targetSdkVersion 29
versionCode gitCommits
versionName '0.5-b1'
versionName '0.5-b2'
kapt {
arguments {
@@ -61,17 +61,16 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.6'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.6'
implementation 'androidx.core:core-ktx:1.4.0-alpha01'
implementation 'androidx.appcompat:appcompat:1.3.0-alpha01'
implementation 'androidx.activity:activity-ktx:1.2.0-alpha05'
implementation 'androidx.fragment:fragment-ktx:1.3.0-alpha05'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-alpha03'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta6'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-rc01'
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha03'
implementation 'androidx.core:core-ktx:1.5.0-alpha01'
implementation 'androidx.activity:activity-ktx:1.2.0-alpha06'
implementation 'androidx.fragment:fragment-ktx:1.3.0-alpha06'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-alpha05'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta7'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha04'
implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01'
implementation 'androidx.preference:preference-ktx:1.1.1'
implementation 'androidx.work:work-runtime-ktx:2.4.0-beta01'
implementation 'androidx.work:work-runtime-ktx:2.4.0-rc01'
implementation 'com.google.android.material:material:1.3.0-alpha01'
implementation 'androidx.room:room-runtime:2.2.5'

View File

@@ -19,4 +19,10 @@ interface TrackLogsDao {
@Query("DELETE FROM track_logs WHERE manga_id = :mangaId")
suspend fun removeAll(mangaId: Long)
@Query("DELETE FROM track_logs WHERE manga_id NOT IN (SELECT manga_id FROM tracks)")
suspend fun cleanup()
@Query("SELECT COUNT(*) FROM track_logs")
suspend fun count(): Int
}

View File

@@ -25,11 +25,13 @@ abstract class TracksDao {
@Query("DELETE FROM tracks WHERE manga_id = :mangaId")
abstract suspend fun delete(mangaId: Long)
@Query("DELETE FROM tracks WHERE manga_id NOT IN (SELECT manga_id FROM history UNION SELECT manga_id FROM favourites)")
abstract suspend fun cleanup()
@Transaction
open suspend fun upsert(entity: TrackEntity) {
if (update(entity) == 0) {
insert(entity)
}
}
}

View File

@@ -9,5 +9,6 @@ data class AppVersion(
val name: String,
val url: String,
val apkSize: Long,
val apkUrl: String
val apkUrl: String,
val description: String
) : Parcelable

View File

@@ -22,7 +22,8 @@ class GithubRepository : KoinComponent {
url = json.getString("html_url"),
name = json.getString("name").removePrefix("v"),
apkSize = asset.getLong("size"),
apkUrl = asset.getString("browser_download_url")
apkUrl = asset.getString("browser_download_url"),
description = json.getString("body")
)
}
}

View File

@@ -72,7 +72,6 @@ class LocalMangaRepository : MangaRepository, KoinComponent {
}
}
fun delete(manga: Manga): Boolean {
val file = Uri.parse(manga.url).toFile()
return file.delete()

View File

@@ -13,7 +13,8 @@ import java.util.*
object MangaProviderFactory : KoinComponent {
private val loaderContext by inject<MangaLoaderContext>()
private val cache = EnumMap<MangaSource, WeakReference<MangaRepository>>(MangaSource::class.java)
private val cache =
EnumMap<MangaSource, WeakReference<MangaRepository>>(MangaSource::class.java)
fun getSources(includeHidden: Boolean): List<MangaSource> {
val settings = get<AppSettings>()
@@ -33,24 +34,37 @@ object MangaProviderFactory : KoinComponent {
}
}
fun createLocal(): LocalMangaRepository =
(cache[MangaSource.LOCAL]?.get() as? LocalMangaRepository)
?: LocalMangaRepository().also {
cache[MangaSource.LOCAL] = WeakReference<MangaRepository>(it)
fun createLocal(): LocalMangaRepository {
var instance = cache[MangaSource.LOCAL]?.get()
if (instance == null) {
synchronized(cache) {
instance = cache[MangaSource.LOCAL]?.get()
if (instance == null) {
instance = LocalMangaRepository()
cache[MangaSource.LOCAL] = WeakReference<MangaRepository>(instance)
}
}
}
return instance as LocalMangaRepository
}
@Throws(Throwable::class)
fun create(source: MangaSource): MangaRepository {
cache[source]?.get()?.let {
return it
var instance = cache[source]?.get()
if (instance == null) {
synchronized(cache) {
instance = cache[source]?.get()
if (instance == null) {
instance = try {
source.cls.getDeclaredConstructor(MangaLoaderContext::class.java)
.newInstance(loaderContext)
} catch (e: NoSuchMethodException) {
source.cls.newInstance()
}
cache[source] = WeakReference(instance!!)
}
}
}
val instance = try {
source.cls.getDeclaredConstructor(MangaLoaderContext::class.java)
.newInstance(loaderContext)
} catch (e: NoSuchMethodException) {
source.cls.newInstance()
}
cache[source] = WeakReference<MangaRepository>(instance)
return instance
return instance!!
}
}

View File

@@ -44,6 +44,17 @@ class TrackingRepository : KoinComponent {
}
}
suspend fun count() = db.trackLogsDao.count()
suspend fun clearLogs() = db.trackLogsDao.clear()
suspend fun cleanup() {
db.withTransaction {
db.tracksDao.cleanup()
db.trackLogsDao.cleanup()
}
}
suspend fun storeTrackResult(
mangaId: Long,
knownChaptersCount: Int,

View File

@@ -29,7 +29,7 @@ class BrowserActivity : BaseActivity(), BrowserCallback {
webView.webViewClient = BrowserClient(this)
val url = intent?.dataString
if (url.isNullOrEmpty()) {
finish()
finishAfterTransition()
} else {
webView.loadUrl(url)
}
@@ -43,7 +43,7 @@ class BrowserActivity : BaseActivity(), BrowserCallback {
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
android.R.id.home -> {
webView.stopLoading()
finish()
finishAfterTransition()
true
}
R.id.action_browser -> {

View File

@@ -1,12 +1,10 @@
package org.koitharu.kotatsu.ui.common
import android.view.KeyEvent
import android.view.MenuItem
import android.view.View
import androidx.appcompat.widget.Toolbar
import moxy.MvpAppCompatActivity
import org.koin.core.KoinComponent
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
abstract class BaseActivity : MvpAppCompatActivity(), KoinComponent {
@@ -29,13 +27,4 @@ abstract class BaseActivity : MvpAppCompatActivity(), KoinComponent {
onBackPressed()
true
} else super.onOptionsItemSelected(item)
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
//TODO remove. Just for testing
if (BuildConfig.DEBUG && keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
recreate()
return true
}
return super.onKeyDown(keyCode, event)
}
}

View File

@@ -0,0 +1,25 @@
package org.koitharu.kotatsu.ui.common
import android.util.ArrayMap
import moxy.MvpPresenter
import java.lang.ref.WeakReference
abstract class SharedPresenterHolder<T : MvpPresenter<*>> {
private val cache = ArrayMap<Int, WeakReference<T>>(3)
fun getInstance(key: Int): T {
var instance = cache[key]?.get()
if (instance == null) {
instance = onCreatePresenter(key)
cache[key] = WeakReference(instance)
}
return instance
}
fun clear(key: Int) {
cache.remove(key)
}
protected abstract fun onCreatePresenter(key: Int): T
}

View File

@@ -25,7 +25,9 @@ class ChaptersFragment : BaseFragment(R.layout.fragment_chapters), MangaDetailsV
OnRecyclerItemClickListener<MangaChapter>, ActionMode.Callback {
@Suppress("unused")
private val presenter by moxyPresenter(factory = MangaDetailsPresenter.Companion::getInstance)
private val presenter by moxyPresenter {
MangaDetailsPresenter.getInstance(activity.hashCode())
}
private var manga: Manga? = null

View File

@@ -37,7 +37,9 @@ import org.koitharu.kotatsu.utils.ext.getThemeColor
class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
TabLayoutMediator.TabConfigurationStrategy {
private val presenter by moxyPresenter(factory = MangaDetailsPresenter.Companion::getInstance)
private val presenter by moxyPresenter {
MangaDetailsPresenter.getInstance(hashCode())
}
private var manga: Manga? = null
@@ -52,7 +54,7 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
presenter.loadDetails(it, true)
} ?: intent?.getLongExtra(EXTRA_MANGA_ID, 0)?.takeUnless { it == 0L }?.let {
presenter.findMangaById(it)
} ?: finish()
} ?: finishAfterTransition()
}
}
@@ -73,13 +75,13 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
this, getString(R.string._s_deleted_from_local_storage, manga.title),
Toast.LENGTH_SHORT
).show()
finish()
finishAfterTransition()
}
override fun onError(e: Throwable) {
if (manga == null) {
Toast.makeText(this, e.getDisplayMessage(resources), Toast.LENGTH_LONG).show()
finish()
finishAfterTransition()
} else {
Snackbar.make(pager, e.getDisplayMessage(resources), Snackbar.LENGTH_LONG).show()
}

View File

@@ -29,7 +29,9 @@ class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetai
View.OnLongClickListener {
@Suppress("unused")
private val presenter by moxyPresenter(factory = MangaDetailsPresenter.Companion::getInstance)
private val presenter by moxyPresenter {
MangaDetailsPresenter.getInstance(activity.hashCode())
}
private var manga: Manga? = null
private var history: MangaHistory? = null

View File

@@ -21,13 +21,13 @@ import org.koitharu.kotatsu.domain.history.HistoryRepository
import org.koitharu.kotatsu.domain.history.OnHistoryChangeListener
import org.koitharu.kotatsu.domain.tracking.TrackingRepository
import org.koitharu.kotatsu.ui.common.BasePresenter
import org.koitharu.kotatsu.ui.common.SharedPresenterHolder
import org.koitharu.kotatsu.utils.ext.safe
import java.io.IOException
@InjectViewState
class MangaDetailsPresenter private constructor() : BasePresenter<MangaDetailsView>(),
OnHistoryChangeListener,
OnFavouritesChangeListener {
class MangaDetailsPresenter private constructor(private val key: Int) :
BasePresenter<MangaDetailsView>(), OnHistoryChangeListener, OnFavouritesChangeListener {
private lateinit var historyRepository: HistoryRepository
private lateinit var favouritesRepository: FavouritesRepository
@@ -55,7 +55,7 @@ class MangaDetailsPresenter private constructor() : BasePresenter<MangaDetailsVi
} ?: throw MangaNotFoundException("Cannot find manga by id")
viewState.onMangaUpdated(manga)
loadDetails(manga, true)
} catch (_: CancellationException){
} catch (_: CancellationException) {
} catch (e: Throwable) {
if (BuildConfig.DEBUG) {
e.printStackTrace()
@@ -83,7 +83,7 @@ class MangaDetailsPresenter private constructor() : BasePresenter<MangaDetailsVi
viewState.onMangaUpdated(data)
this@MangaDetailsPresenter.manga = data
viewState.onNewChaptersChanged(trackingRepository.getNewChaptersCount(manga.id))
} catch (_: CancellationException){
} catch (_: CancellationException) {
} catch (e: Throwable) {
if (BuildConfig.DEBUG) {
e.printStackTrace()
@@ -198,18 +198,12 @@ class MangaDetailsPresenter private constructor() : BasePresenter<MangaDetailsVi
override fun onDestroy() {
HistoryRepository.unsubscribe(this)
FavouritesRepository.unsubscribe(this)
instance = null
clear(key)
super.onDestroy()
}
companion object {
companion object Holder : SharedPresenterHolder<MangaDetailsPresenter>() {
private var instance: MangaDetailsPresenter? = null
fun getInstance(): MangaDetailsPresenter = instance ?: synchronized(this) {
MangaDetailsPresenter().also {
instance = it
}
}
override fun onCreatePresenter(key: Int) = MangaDetailsPresenter(key)
}
}

View File

@@ -10,7 +10,9 @@ import org.koitharu.kotatsu.ui.list.MangaListFragment
class RelatedMangaFragment : MangaListFragment<Unit>(), MangaDetailsView {
private val presenter by moxyPresenter(factory = MangaDetailsPresenter.Companion::getInstance)
private val presenter by moxyPresenter {
MangaDetailsPresenter.getInstance(activity.hashCode())
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

View File

@@ -92,6 +92,11 @@ class DownloadNotification(private val context: Context) {
builder.setCategory(NotificationCompat.CATEGORY_PROGRESS)
}
fun setWaitingForNetwork() {
builder.setProgress(0, 0, false)
builder.setContentText(context.getString(R.string.waiting_for_network))
}
fun setPostProcessing() {
builder.setProgress(1, 0, true)
builder.setContentText(context.getString(R.string.processing_))

View File

@@ -13,6 +13,7 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import okhttp3.OkHttpClient
import okhttp3.Request
import okio.IOException
import org.koin.android.ext.android.inject
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
@@ -24,10 +25,7 @@ import org.koitharu.kotatsu.domain.local.MangaZip
import org.koitharu.kotatsu.ui.common.BaseService
import org.koitharu.kotatsu.ui.common.dialog.CheckBoxAlertDialog
import org.koitharu.kotatsu.utils.CacheUtils
import org.koitharu.kotatsu.utils.ext.await
import org.koitharu.kotatsu.utils.ext.retryUntilSuccess
import org.koitharu.kotatsu.utils.ext.safe
import org.koitharu.kotatsu.utils.ext.sub
import org.koitharu.kotatsu.utils.ext.*
import java.io.File
import java.util.concurrent.TimeUnit
import kotlin.collections.set
@@ -37,6 +35,7 @@ class DownloadService : BaseService() {
private lateinit var notification: DownloadNotification
private lateinit var wakeLock: PowerManager.WakeLock
private lateinit var connectivityManager: ConnectivityManager
private val okHttp by inject<OkHttpClient>()
private val cache by inject<PagesCache>()
@@ -47,6 +46,7 @@ class DownloadService : BaseService() {
override fun onCreate() {
super.onCreate()
notification = DownloadNotification(this)
connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager)
.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "kotatsu:downloading")
}
@@ -88,9 +88,11 @@ class DownloadService : BaseService() {
try {
val repo = MangaProviderFactory.create(manga.source)
val cover = safe {
Coil.execute(GetRequestBuilder(this@DownloadService)
.data(manga.coverUrl)
.build()).drawable
Coil.execute(
GetRequestBuilder(this@DownloadService)
.data(manga.coverUrl)
.build()
).drawable
}
withContext(Dispatchers.Main) {
notification.setLargeIcon(cover)
@@ -112,23 +114,30 @@ class DownloadService : BaseService() {
if (chaptersIds == null || chapter.id in chaptersIds) {
val pages = repo.getPages(chapter)
for ((pageIndex, page) in pages.withIndex()) {
val url = repo.getPageFullUrl(page)
val file = cache[url] ?: downloadPage(url, destination)
output.addPage(
chapter,
file,
pageIndex,
MimeTypeMap.getFileExtensionFromUrl(url)
failsafe@ do {
try {
val url = repo.getPageFullUrl(page)
val file = cache[url] ?: downloadPage(url, destination)
output.addPage(
chapter,
file,
pageIndex,
MimeTypeMap.getFileExtensionFromUrl(url)
)
} catch (e: IOException) {
notification.setWaitingForNetwork()
notification.update()
connectivityManager.waitForNetwork()
continue@failsafe
}
} while (false)
notification.setProgress(
chapters.size,
pages.size,
chapterIndex,
pageIndex
)
withContext(Dispatchers.Main) {
notification.setProgress(
chapters.size,
pages.size,
chapterIndex,
pageIndex
)
notification.update()
}
notification.update()
}
}
}

View File

@@ -11,7 +11,6 @@ import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.core.view.isVisible
import androidx.core.view.postDelayed
import androidx.fragment.app.Fragment
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import com.google.android.material.navigation.NavigationView
@@ -26,14 +25,14 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.domain.MangaProviderFactory
import org.koitharu.kotatsu.ui.common.BaseActivity
import org.koitharu.kotatsu.ui.list.favourites.FavouritesContainerFragment
import org.koitharu.kotatsu.ui.list.feed.FeedFragment
import org.koitharu.kotatsu.ui.list.history.HistoryListFragment
import org.koitharu.kotatsu.ui.list.local.LocalListFragment
import org.koitharu.kotatsu.ui.list.remote.RemoteListFragment
import org.koitharu.kotatsu.ui.list.feed.FeedFragment
import org.koitharu.kotatsu.ui.reader.ReaderActivity
import org.koitharu.kotatsu.ui.reader.ReaderState
import org.koitharu.kotatsu.ui.search.SearchHelper
import org.koitharu.kotatsu.ui.settings.AppUpdateService
import org.koitharu.kotatsu.ui.settings.AppUpdateChecker
import org.koitharu.kotatsu.ui.settings.SettingsActivity
import org.koitharu.kotatsu.ui.tracker.TrackWorker
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
@@ -70,10 +69,8 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
} ?: run {
openDefaultSection()
}
drawer.postDelayed(2000) {
AppUpdateService.startIfRequired(applicationContext)
}
TrackWorker.setup(applicationContext)
AppUpdateChecker(this).invoke()
}
override fun onDestroy() {

View File

@@ -41,9 +41,9 @@ abstract class MangaListFragment<E> : BaseFragment(R.layout.fragment_list),
SectionItemDecoration.Callback, SwipeRefreshLayout.OnRefreshListener {
private val settings by inject<AppSettings>()
private val adapterConfig = MergeAdapter.Config.Builder()
private val adapterConfig = ConcatAdapter.Config.Builder()
.setIsolateViewTypes(true)
.setStableIdMode(MergeAdapter.Config.StableIdMode.SHARED_STABLE_IDS)
.setStableIdMode(ConcatAdapter.Config.StableIdMode.SHARED_STABLE_IDS)
.build()
private var adapter: MangaListAdapter? = null
@@ -245,18 +245,17 @@ abstract class MangaListFragment<E> : BaseFragment(R.layout.fragment_list),
adapter?.listMode = mode
recyclerView.layoutManager = when (mode) {
ListMode.GRID -> {
val spanCount = UiUtils.resolveGridSpanCount(ctx)
GridLayoutManager(ctx, spanCount).apply {
GridLayoutManager(ctx, UiUtils.resolveGridSpanCount(ctx)).apply {
spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int) = if (position < getItemsCount())
1 else spanCount
1 else this@apply.spanCount
}
}
}
else -> LinearLayoutManager(ctx)
}
recyclerView.recycledViewPool.clear()
recyclerView.adapter = MergeAdapter(adapterConfig, adapter, progressAdapter)
recyclerView.adapter = ConcatAdapter(adapterConfig, adapter, progressAdapter)
recyclerView.addItemDecoration(
when (mode) {
ListMode.LIST -> DividerItemDecoration(ctx, RecyclerView.VERTICAL)

View File

@@ -36,9 +36,9 @@ abstract class MangaListSheet<E> : BaseBottomSheet(R.layout.sheet_list),
SharedPreferences.OnSharedPreferenceChangeListener, Toolbar.OnMenuItemClickListener {
private val settings by inject<AppSettings>()
private val adapterConfig = MergeAdapter.Config.Builder()
private val adapterConfig = ConcatAdapter.Config.Builder()
.setIsolateViewTypes(true)
.setStableIdMode(MergeAdapter.Config.StableIdMode.SHARED_STABLE_IDS)
.setStableIdMode(ConcatAdapter.Config.StableIdMode.SHARED_STABLE_IDS)
.build()
private var adapter: MangaListAdapter? = null
@@ -181,17 +181,16 @@ abstract class MangaListSheet<E> : BaseBottomSheet(R.layout.sheet_list),
adapter?.listMode = mode
recyclerView.layoutManager = when (mode) {
ListMode.GRID -> {
val spanCount = UiUtils.resolveGridSpanCount(ctx)
GridLayoutManager(ctx, spanCount).apply {
GridLayoutManager(ctx, UiUtils.resolveGridSpanCount(ctx)).apply {
spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int) = if (position < getItemsCount())
1 else spanCount
1 else this@apply.spanCount
}
}
}
else -> LinearLayoutManager(ctx)
}
recyclerView.adapter = MergeAdapter(adapterConfig, adapter, progressAdapter)
recyclerView.adapter = ConcatAdapter(adapterConfig, adapter, progressAdapter)
recyclerView.addItemDecoration(
when (mode) {
ListMode.LIST -> DividerItemDecoration(ctx, RecyclerView.VERTICAL)

View File

@@ -13,7 +13,6 @@ import org.koin.core.get
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.parser.LocalMangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.domain.MangaProviderFactory
@@ -33,16 +32,17 @@ class LocalListPresenter : BasePresenter<MangaListView<File>>() {
private lateinit var repository: LocalMangaRepository
override fun onFirstViewAttach() {
repository = MangaProviderFactory.create(MangaSource.LOCAL) as LocalMangaRepository
repository = MangaProviderFactory.createLocal()
super.onFirstViewAttach()
}
fun loadList(offset: Int) {
if (offset != 0) {
viewState.onListAppended(emptyList())
return
}
presenterScope.launch {
if (offset != 0) {
viewState.onListAppended(emptyList())
return@launch
}
viewState.onLoadingStateChanged(true)
try {
val list = withContext(Dispatchers.IO) {

View File

@@ -75,7 +75,7 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
?: intent.getParcelableExtra<ReaderState>(EXTRA_STATE)
?: let {
Toast.makeText(this, R.string.error_occurred, Toast.LENGTH_SHORT).show()
finish()
finishAfterTransition()
return
}

View File

@@ -19,7 +19,7 @@ class SearchActivity : BaseActivity(), SearchView.OnQueryTextListener {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_search)
source = intent.getParcelableExtra(EXTRA_SOURCE) ?: run {
finish()
finishAfterTransition()
return
}
val query = intent.getStringExtra(EXTRA_QUERY)

View File

@@ -14,7 +14,7 @@ class GlobalSearchActivity : BaseActivity() {
val query = intent.getStringExtra(EXTRA_QUERY)
if (query == null) {
finish()
finishAfterTransition()
return
}

View File

@@ -0,0 +1,128 @@
package org.koitharu.kotatsu.ui.settings
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import androidx.activity.ComponentActivity
import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.core.KoinComponent
import org.koin.core.inject
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.utils.FileSizeUtils
import org.koitharu.kotatsu.utils.ext.byte2HexFormatted
import java.io.ByteArrayInputStream
import java.io.InputStream
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.security.cert.CertificateEncodingException
import java.security.cert.CertificateException
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.util.concurrent.TimeUnit
class AppUpdateChecker(private val activity: ComponentActivity) : KoinComponent {
private val settings by inject<AppSettings>()
operator fun invoke() {
if (isUpdateSupported(activity) && settings.appUpdateAuto && settings.appUpdate + PERIOD < System.currentTimeMillis()) {
launch()
}
}
private fun launch() = activity.lifecycleScope.launch {
try {
val repo = GithubRepository()
val version = withContext(Dispatchers.IO) {
repo.getLatestVersion()
}
val newVersionId = VersionId.parse(version.name)
val currentVersionId = VersionId.parse(BuildConfig.VERSION_NAME)
if (newVersionId > currentVersionId) {
showUpdateDialog(version)
}
settings.appUpdate = System.currentTimeMillis()
} catch (_: CancellationException) {
} catch (e: Throwable) {
if (BuildConfig.DEBUG) {
e.printStackTrace()
}
}
}
private fun showUpdateDialog(version: AppVersion) {
MaterialAlertDialogBuilder(activity)
.setTitle(R.string.app_update_available)
.setMessage(buildString {
append(activity.getString(R.string.new_version_s, version.name))
appendln()
append(activity.getString(R.string.size_s, FileSizeUtils.formatBytes(activity, version.apkSize)))
appendln()
appendln()
append(version.description)
})
.setPositiveButton(R.string.download) { _, _ ->
activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(version.apkUrl)))
}
.setNegativeButton(R.string.close, null)
.create()
.show()
}
companion object {
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"
private val PERIOD = TimeUnit.HOURS.toMillis(6)
fun isUpdateSupported(context: Context): Boolean {
return getCertificateSHA1Fingerprint(context) == CERT_SHA1
}
@Suppress("DEPRECATION")
@SuppressLint("PackageManagerGetSignatures")
private fun getCertificateSHA1Fingerprint(context: Context): String? {
val packageInfo = try {
context.packageManager.getPackageInfo(
context.packageName,
PackageManager.GET_SIGNATURES
)
} catch (e: PackageManager.NameNotFoundException) {
e.printStackTrace()
return null
}
val signatures = packageInfo?.signatures
val cert: ByteArray = signatures?.firstOrNull()?.toByteArray() ?: return null
val input: InputStream = ByteArrayInputStream(cert)
val c = try {
val cf = CertificateFactory.getInstance("X509")
cf.generateCertificate(input) as X509Certificate
} catch (e: CertificateException) {
e.printStackTrace()
return null
}
return try {
val md: MessageDigest = MessageDigest.getInstance("SHA1")
val publicKey: ByteArray = md.digest(c.encoded)
publicKey.byte2HexFormatted()
} catch (e: NoSuchAlgorithmException) {
e.printStackTrace()
null
} catch (e: CertificateEncodingException) {
e.printStackTrace()
null
}
}
}
}

View File

@@ -1,178 +0,0 @@
package org.koitharu.kotatsu.ui.settings
import android.annotation.SuppressLint
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
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.ui.common.BaseService
import org.koitharu.kotatsu.utils.FileSizeUtils
import org.koitharu.kotatsu.utils.ext.byte2HexFormatted
import java.io.ByteArrayInputStream
import java.io.InputStream
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.security.cert.CertificateEncodingException
import java.security.cert.CertificateException
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.util.concurrent.TimeUnit
class AppUpdateService : BaseService() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
launch(Dispatchers.IO) {
try {
val repo = GithubRepository()
val version = repo.getLatestVersion()
val newVersionId = VersionId.parse(version.name)
val currentVersionId = VersionId.parse(BuildConfig.VERSION_NAME)
if (newVersionId > currentVersionId) {
withContext(Dispatchers.Main) {
showUpdateNotification(version)
}
}
AppSettings(this@AppUpdateService).appUpdate = System.currentTimeMillis()
} catch (_: CancellationException) {
} catch (e: Throwable) {
if (BuildConfig.DEBUG) {
e.printStackTrace()
}
} finally {
withContext(Dispatchers.Main) {
stopSelf(startId)
}
}
}
return START_NOT_STICKY
}
private fun showUpdateNotification(newVersion: AppVersion) {
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
&& manager.getNotificationChannel(CHANNEL_ID) == null
) {
val channel = NotificationChannel(
CHANNEL_ID,
getString(R.string.application_update),
NotificationManager.IMPORTANCE_DEFAULT
)
manager.createNotificationChannel(channel)
}
val builder = NotificationCompat.Builder(this, CHANNEL_ID)
builder.setContentTitle(getString(R.string.app_update_available))
builder.setContentText(buildString {
append(newVersion.name)
append(" (")
append(FileSizeUtils.formatBytes(this@AppUpdateService, newVersion.apkSize))
append(')')
})
builder.setContentIntent(
PendingIntent.getActivity(
this,
NOTIFICATION_ID,
Intent(Intent.ACTION_VIEW, Uri.parse(newVersion.url)),
PendingIntent.FLAG_CANCEL_CURRENT
)
)
builder.addAction(
R.drawable.ic_download, getString(R.string.download),
PendingIntent.getActivity(
this,
NOTIFICATION_ID + 1,
Intent(Intent.ACTION_VIEW, Uri.parse(newVersion.apkUrl)),
PendingIntent.FLAG_CANCEL_CURRENT
)
)
builder.setSmallIcon(R.drawable.ic_stat_update)
builder.setAutoCancel(true)
builder.color = ContextCompat.getColor(this, R.color.blue_primary_dark)
builder.setLargeIcon(BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher))
manager.notify(NOTIFICATION_ID, builder.build())
}
companion object {
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"
private const val NOTIFICATION_ID = 202
private const val CHANNEL_ID = "update"
private val PERIOD = TimeUnit.HOURS.toMillis(6)
fun isUpdateSupported(context: Context): Boolean {
return getCertificateSHA1Fingerprint(context) == CERT_SHA1
}
fun startIfRequired(context: Context) {
if (!isUpdateSupported(context)) {
return
}
val settings = AppSettings(context)
if (settings.appUpdateAuto) {
val lastUpdate = settings.appUpdate
if (lastUpdate + PERIOD < System.currentTimeMillis()) {
start(context)
}
}
}
private fun start(context: Context) {
try {
context.startService(Intent(context, AppUpdateService::class.java))
} catch (_: IllegalStateException) {
}
}
@Suppress("DEPRECATION")
@SuppressLint("PackageManagerGetSignatures")
private fun getCertificateSHA1Fingerprint(context: Context): String? {
val packageInfo = try {
context.packageManager.getPackageInfo(
context.packageName,
PackageManager.GET_SIGNATURES
)
} catch (e: PackageManager.NameNotFoundException) {
e.printStackTrace()
return null
}
val signatures = packageInfo?.signatures
val cert: ByteArray = signatures?.firstOrNull()?.toByteArray() ?: return null
val input: InputStream = ByteArrayInputStream(cert)
val c = try {
val cf = CertificateFactory.getInstance("X509")
cf.generateCertificate(input) as X509Certificate
} catch (e: CertificateException) {
e.printStackTrace()
return null
}
return try {
val md: MessageDigest = MessageDigest.getInstance("SHA1")
val publicKey: ByteArray = md.digest(c.encoded)
publicKey.byte2HexFormatted()
} catch (e: NoSuchAlgorithmException) {
e.printStackTrace()
null
} catch (e: CertificateEncodingException) {
e.printStackTrace()
null
}
}
}
}

View File

@@ -9,6 +9,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.local.Cache
import org.koitharu.kotatsu.domain.tracking.TrackingRepository
import org.koitharu.kotatsu.ui.common.BasePreferenceFragment
import org.koitharu.kotatsu.ui.search.MangaSuggestionsProvider
import org.koitharu.kotatsu.utils.CacheUtils
@@ -17,6 +18,10 @@ import org.koitharu.kotatsu.utils.ext.getDisplayMessage
class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cache) {
private val trackerRepo by lazy {
TrackingRepository()
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_history)
findPreference<Preference>(R.string.key_pages_cache_clear)?.let { pref ->
@@ -39,10 +44,16 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
val items = MangaSuggestionsProvider.getItemsCount(p.context)
p.summary = p.context.resources.getQuantityString(R.plurals.items, items, items)
}
findPreference<Preference>(R.string.key_updates_feed_clear)?.let { p ->
lifecycleScope.launchWhenResumed {
val items = trackerRepo.count()
p.summary = p.context.resources.getQuantityString(R.plurals.items, items, items)
}
}
}
override fun onPreferenceTreeClick(preference: Preference): Boolean {
return when(preference.key) {
return when (preference.key) {
getString(R.string.key_pages_cache_clear) -> {
clearCache(preference, Cache.PAGES)
true
@@ -53,8 +64,26 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
}
getString(R.string.key_search_history_clear) -> {
MangaSuggestionsProvider.clearHistory(preference.context)
preference.context.resources.getQuantityString(R.plurals.items, 0, 0)
Snackbar.make(view ?: return true, R.string.search_history_cleared, Snackbar.LENGTH_SHORT).show()
preference.summary = preference.context.resources
.getQuantityString(R.plurals.items, 0, 0)
Snackbar.make(
view ?: return true,
R.string.search_history_cleared,
Snackbar.LENGTH_SHORT
).show()
true
}
getString(R.string.key_updates_feed_clear) -> {
lifecycleScope.launch {
trackerRepo.clearLogs()
preference.summary = preference.context.resources
.getQuantityString(R.plurals.items, 0, 0)
Snackbar.make(
view ?: return@launch,
R.string.updates_feed_cleared,
Snackbar.LENGTH_SHORT
).show()
}
true
}
else -> super.onPreferenceTreeClick(preference)

View File

@@ -42,7 +42,7 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings),
findPreference<MultiSelectListPreference>(R.string.key_reader_switchers)?.summaryProvider =
MultiSummaryProvider(R.string.gestures_only)
findPreference<Preference>(R.string.key_app_update_auto)?.run {
isVisible = AppUpdateService.isUpdateSupported(context)
isVisible = AppUpdateChecker.isUpdateSupported(context)
}
findPreference<Preference>(R.string.key_local_storage)?.run {
summary = settings.getStorageDir(context)?.getStorageName(context)

View File

@@ -118,6 +118,7 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) :
}
success++
}
repo.cleanup()
if (success == 0) {
Result.retry()
} else {

View File

@@ -48,7 +48,7 @@ class ShelfConfigActivity : BaseActivity(), FavouriteCategoriesView,
AppWidgetManager.INVALID_APPWIDGET_ID
) ?: AppWidgetManager.INVALID_APPWIDGET_ID
if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
finish()
finishAfterTransition()
return
}
config = AppWidgetConfig.getInstance(this, appWidgetId)

View File

@@ -0,0 +1,22 @@
package org.koitharu.kotatsu.utils.ext
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkRequest
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
suspend fun ConnectivityManager.waitForNetwork(): Network {
val request = NetworkRequest.Builder().build()
return suspendCancellableCoroutine<Network> { cont ->
val callback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
cont.resume(network)
}
}
registerNetworkCallback(request, callback)
cont.invokeOnCancellation {
unregisterNetworkCallback(callback)
}
}
}

View File

@@ -137,4 +137,9 @@
<string name="text_feed_holder">Здесь будут отображаться обновления манги, которую Вы читаете</string>
<string name="search_results">Результаты поиска</string>
<string name="related">Похожие</string>
<string name="new_version_s">Новая версия: %s</string>
<string name="size_s">Размер: %s</string>
<string name="waiting_for_network">Ожидание подключения…</string>
<string name="clear_updates_feed">Очистить ленту обновлений</string>
<string name="updates_feed_cleared">Лента обновлений очищена</string>
</resources>

View File

@@ -9,6 +9,7 @@
<string name="key_pages_cache_clear">pages_cache_clear</string>
<string name="key_thumbs_cache_clear">thumbs_cache_clear</string>
<string name="key_search_history_clear">search_history_clear</string>
<string name="key_updates_feed_clear">updates_feed_clear</string>
<string name="key_grid_size">grid_size</string>
<string name="key_remote_sources">remote_sources</string>
<string name="key_local_storage">local_storage</string>

View File

@@ -138,4 +138,9 @@
<string name="text_feed_holder">Here you will see the new chapters of the manga you are reading</string>
<string name="search_results">Search results</string>
<string name="related">Related</string>
<string name="new_version_s">New version: %s</string>
<string name="size_s">Size: %s</string>
<string name="waiting_for_network">Waiting for network…</string>
<string name="clear_updates_feed">Clear updates feed</string>
<string name="updates_feed_cleared">Updates feed cleared</string>
</resources>

View File

@@ -9,6 +9,12 @@
android:title="@string/clear_search_history"
app:iconSpaceReserved="false" />
<Preference
android:key="@string/key_updates_feed_clear"
android:persistent="false"
android:title="@string/clear_updates_feed"
app:iconSpaceReserved="false" />
<PreferenceCategory
app:iconSpaceReserved="false"
android:title="@string/cache">

View File

@@ -6,7 +6,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.0-alpha10'
classpath 'com.android.tools.build:gradle:4.2.0-alpha02'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong

View File

@@ -1,6 +1,6 @@
#Sat May 30 09:45:56 EEST 2020
#Sat Jun 20 11:05:53 EEST 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-milestone-1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip