diff --git a/app/build.gradle b/app/build.gradle index 0bcefe9d5..426cd1586 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -14,8 +14,8 @@ android { applicationId 'org.koitharu.kotatsu' minSdkVersion 21 targetSdkVersion 32 - versionCode 399 - versionName '3.0-alpha1' + versionCode 400 + versionName '3.0' generatedDensities = [] testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -65,7 +65,7 @@ android { } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) - implementation 'com.github.nv95:kotatsu-parsers:1ba2bba12e' + implementation 'com.github.nv95:kotatsu-parsers:3ea7e92e64' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0' @@ -82,7 +82,6 @@ dependencies { implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01' - implementation 'androidx.asynclayoutinflater:asynclayoutinflater:1.0.0' implementation 'androidx.preference:preference-ktx:1.2.0' implementation 'androidx.work:work-runtime-ktx:2.7.1' implementation 'com.google.android.material:material:1.6.0-beta01' diff --git a/app/src/main/java/org/koitharu/kotatsu/core/ui/AppCrashHandler.kt b/app/src/main/java/org/koitharu/kotatsu/core/ui/AppCrashHandler.kt index 002b8b05e..20a7bf0c3 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/ui/AppCrashHandler.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/ui/AppCrashHandler.kt @@ -3,20 +3,12 @@ package org.koitharu.kotatsu.core.ui import android.content.Context import android.content.Intent import android.util.Log -import java.io.PrintWriter -import java.io.StringWriter import kotlin.system.exitProcess class AppCrashHandler(private val applicationContext: Context) : Thread.UncaughtExceptionHandler { override fun uncaughtException(t: Thread, e: Throwable) { - val crashInfo = buildString { - val writer = StringWriter() - e.printStackTrace(PrintWriter(writer)) - append(writer.toString().trimIndent()) - } - val intent = Intent(applicationContext, CrashActivity::class.java) - intent.putExtra(Intent.EXTRA_TEXT, crashInfo) + val intent = CrashActivity.newIntent(applicationContext, e) intent.flags = (Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) try { applicationContext.startActivity(intent) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/ui/CrashActivity.kt b/app/src/main/java/org/koitharu/kotatsu/core/ui/CrashActivity.kt index 4ea8222bf..7d4d31878 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/ui/CrashActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/ui/CrashActivity.kt @@ -2,6 +2,7 @@ package org.koitharu.kotatsu.core.ui import android.app.Activity import android.content.ActivityNotFoundException +import android.content.Context import android.content.Intent import android.net.Uri import android.os.Bundle @@ -11,6 +12,7 @@ import android.view.View import org.koitharu.kotatsu.R import org.koitharu.kotatsu.databinding.ActivityCrashBinding import org.koitharu.kotatsu.main.ui.MainActivity +import org.koitharu.kotatsu.parsers.util.ellipsize import org.koitharu.kotatsu.utils.ShareHelper class CrashActivity : Activity(), View.OnClickListener { @@ -63,4 +65,19 @@ class CrashActivity : Activity(), View.OnClickListener { } } } + + companion object { + + private const val MAX_TRACE_SIZE = 131071 + + fun newIntent(context: Context, error: Throwable): Intent { + val crashInfo = error + .stackTraceToString() + .trimIndent() + .ellipsize(MAX_TRACE_SIZE) + val intent = Intent(context, CrashActivity::class.java) + intent.putExtra(Intent.EXTRA_TEXT, crashInfo) + return intent + } + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt b/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt index a56ad85c5..3572ebf2b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt +++ b/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt @@ -8,6 +8,7 @@ import coil.ImageLoader import coil.request.ImageRequest import coil.size.Scale import kotlinx.coroutines.* +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flow import okhttp3.OkHttpClient @@ -39,7 +40,7 @@ class DownloadManager( private val localMangaRepository: LocalMangaRepository, ) { - private val connectivityManager = context.getSystemService( + private val connectivityManager = context.applicationContext.getSystemService( Context.CONNECTIVITY_SERVICE ) as ConnectivityManager private val coverWidth = context.resources.getDimensionPixelSize( @@ -49,7 +50,7 @@ class DownloadManager( androidx.core.R.dimen.compat_notification_large_icon_max_height ) - fun downloadManga(manga: Manga, chaptersIds: Set?, startId: Int) = flow { + fun downloadManga(manga: Manga, chaptersIds: Set?, startId: Int): Flow = flow { emit(State.Preparing(startId, manga, null)) var cover: Drawable? = null val destination = localMangaRepository.getOutputDir() @@ -102,13 +103,15 @@ class DownloadManager( } } while (false) - emit(State.Progress( - startId, manga, cover, - totalChapters = chapters.size, - currentChapter = chapterIndex, - totalPages = pages.size, - currentPage = pageIndex, - )) + emit( + State.Progress( + startId, manga, cover, + totalChapters = chapters.size, + currentChapter = chapterIndex, + totalPages = pages.size, + currentPage = pageIndex, + ) + ) } } } @@ -191,7 +194,7 @@ class DownloadManager( val currentChapter: Int, val totalPages: Int, val currentPage: Int, - ): State { + ) : State { val max: Int = totalChapters * totalPages @@ -204,7 +207,7 @@ class DownloadManager( override val startId: Int, override val manga: Manga, override val cover: Drawable?, - ): State + ) : State data class Done( override val startId: Int, @@ -224,7 +227,7 @@ class DownloadManager( override val startId: Int, override val manga: Manga, override val cover: Drawable?, - ): State + ) : State data class PostProcessing( override val startId: Int, @@ -232,4 +235,4 @@ class DownloadManager( override val cover: Drawable?, ) : State } -} +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadNotification.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadNotification.kt index 03352d7be..d51dbba30 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadNotification.kt +++ b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadNotification.kt @@ -11,6 +11,7 @@ import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.toBitmap import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.ui.CrashActivity import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.download.domain.DownloadManager import org.koitharu.kotatsu.download.ui.DownloadsActivity @@ -81,6 +82,14 @@ class DownloadNotification( builder.setSubText(context.getString(R.string.error)) builder.setContentText(message) builder.setAutoCancel(true) + builder.setContentIntent( + PendingIntent.getActivity( + context, + state.manga.hashCode(), + CrashActivity.newIntent(context, state.error), + PendingIntent.FLAG_CANCEL_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + ) + ) builder.setCategory(NotificationCompat.CATEGORY_ERROR) builder.setStyle(NotificationCompat.BigTextStyle().bigText(message)) } diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt index 64b6a2529..79c2295cf 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt +++ b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt @@ -15,6 +15,7 @@ import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.isActive import kotlinx.coroutines.launch @@ -105,6 +106,7 @@ class DownloadService : BaseService() { try { withContext(Dispatchers.Default) { downloadManager.downloadManga(manga, chaptersIds, startId) + .distinctUntilChanged() .collect { state -> stateFlow.value = state notificationManager.notify(startId, notification.create(state)) @@ -181,7 +183,7 @@ class DownloadService : BaseService() { } fun getCancelIntent(startId: Int) = Intent(ACTION_DOWNLOAD_CANCEL) - .putExtra(ACTION_DOWNLOAD_CANCEL, startId) + .putExtra(EXTRA_CANCEL_ID, startId) private fun confirmDataTransfer(context: Context, callback: () -> Unit) { val settings = GlobalContext.get().get() diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt index 4658bc0a0..4c27b1890 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt @@ -24,7 +24,6 @@ import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.databinding.FragmentListBinding import org.koitharu.kotatsu.details.ui.DetailsActivity -import org.koitharu.kotatsu.list.ui.adapter.AsyncViewFactory import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter.Companion.ITEM_TYPE_MANGA_GRID import org.koitharu.kotatsu.list.ui.adapter.MangaListListener @@ -35,12 +34,10 @@ import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.utils.ext.* -private const val PREFETCH_ITEM_LIST = 10 -private const val PREFETCH_ITEM_DETAILED = 8 -private const val PREFETCH_ITEM_GRID = 16 - -abstract class MangaListFragment : BaseFragment(), - PaginationScrollListener.Callback, MangaListListener, +abstract class MangaListFragment : + BaseFragment(), + PaginationScrollListener.Callback, + MangaListListener, SwipeRefreshLayout.OnRefreshListener { private var listAdapter: MangaListAdapter? = null @@ -50,7 +47,6 @@ abstract class MangaListFragment : BaseFragment(), private val listCommitCallback = Runnable { spanSizeLookup.invalidateCache() } - private var asyncViewFactory: AsyncViewFactory? = null open val isSwipeRefreshEnabled = true protected abstract val viewModel: MangaListViewModel @@ -67,12 +63,10 @@ abstract class MangaListFragment : BaseFragment(), override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - asyncViewFactory = AsyncViewFactory(binding.recyclerView) listAdapter = MangaListAdapter( coil = get(), lifecycleOwner = viewLifecycleOwner, listener = this, - viewFactory = checkNotNull(asyncViewFactory), ) paginationListener = PaginationScrollListener(4, this) with(binding.recyclerView) { @@ -97,8 +91,6 @@ abstract class MangaListFragment : BaseFragment(), override fun onDestroyView() { listAdapter = null paginationListener = null - asyncViewFactory?.clear() - asyncViewFactory = null spanSizeLookup.invalidateCache() super.onDestroyView() } @@ -172,7 +164,7 @@ abstract class MangaListFragment : BaseFragment(), @CallSuper protected open fun onLoadingStateChanged(isLoading: Boolean) { binding.swipeRefreshLayout.isEnabled = binding.swipeRefreshLayout.isRefreshing || - isSwipeRefreshEnabled && !isLoading + isSwipeRefreshEnabled && !isLoading if (!isLoading) { binding.swipeRefreshLayout.isRefreshing = false } @@ -219,26 +211,18 @@ abstract class MangaListFragment : BaseFragment(), with(binding.recyclerView) { clearItemDecorations() removeOnLayoutChangeListener(spanResolver) - asyncViewFactory?.clear() - val isListPending = viewModel.isListPending() when (mode) { ListMode.LIST -> { layoutManager = FitHeightLinearLayoutManager(context) val spacing = resources.getDimensionPixelOffset(R.dimen.list_spacing) addItemDecoration(SpacingItemDecoration(spacing)) updatePadding(left = spacing, right = spacing) - if (isListPending) { - asyncViewFactory?.prefetch(R.layout.item_manga_list, PREFETCH_ITEM_LIST) - } } ListMode.DETAILED_LIST -> { layoutManager = FitHeightLinearLayoutManager(context) val spacing = resources.getDimensionPixelOffset(R.dimen.list_spacing) updatePadding(left = spacing, right = spacing) addItemDecoration(SpacingItemDecoration(spacing)) - if (isListPending) { - asyncViewFactory?.prefetch(R.layout.item_manga_list_details, PREFETCH_ITEM_DETAILED) - } } ListMode.GRID -> { layoutManager = FitHeightGridLayoutManager(context, spanResolver.spanCount).also { @@ -248,9 +232,6 @@ abstract class MangaListFragment : BaseFragment(), addItemDecoration(SpacingItemDecoration(spacing)) updatePadding(left = spacing, right = spacing) addOnLayoutChangeListener(spanResolver) - if (isListPending) { - asyncViewFactory?.prefetch(R.layout.item_manga_grid, PREFETCH_ITEM_GRID) - } } } } diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt index bd4dc2feb..20f768c2f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt @@ -44,10 +44,4 @@ abstract class MangaListViewModel( abstract fun onRefresh() abstract fun onRetry() - - fun isListPending(): Boolean { - return content.value?.any { - it is MangaListModel || it is MangaGridModel || it is MangaListDetailedModel - } != true - } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/AsyncViewFactory.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/AsyncViewFactory.kt deleted file mode 100644 index cbd9fb071..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/AsyncViewFactory.kt +++ /dev/null @@ -1,58 +0,0 @@ -package org.koitharu.kotatsu.list.ui.adapter - -import android.util.Log -import android.util.SparseArray -import android.view.View -import android.view.ViewGroup -import androidx.annotation.LayoutRes -import androidx.asynclayoutinflater.view.AsyncLayoutInflater -import androidx.core.util.valueIterator -import org.koitharu.kotatsu.BuildConfig -import java.util.* - -class AsyncViewFactory(private val parent: ViewGroup) : AsyncLayoutInflater.OnInflateFinishedListener { - - private val asyncInflater = AsyncLayoutInflater(parent.context) - private val pool = SparseArray>() - - override fun onInflateFinished(view: View, resid: Int, parent: ViewGroup?) { - var list = pool.get(resid) - if (list != null) { - list.addLast(view) - } else { - list = LinkedList() - list.add(view) - pool.put(resid, list) - } - } - - fun clear() { - if (BuildConfig.DEBUG) { - pool.valueIterator().forEach { - if (it.isNotEmpty()) { - Log.w("AsyncViewFactory", "You have ${it.size} unconsumed prefetched items") - } - } - } - pool.clear() - } - - fun prefetch(@LayoutRes resId: Int, count: Int) { - if (count <= 0) return - repeat(count) { - asyncInflater.inflate(resId, parent, this) - } - } - - operator fun get(@LayoutRes resId: Int): View? { - val result = pool.get(resId)?.removeFirstOrNull() - if (BuildConfig.DEBUG && result == null) { - Log.w("AsyncViewFactory", "Item requested but missing") - } - return result - } - - fun getCount(@LayoutRes resId: Int): Int { - return pool[resId]?.size ?: 0 - } -} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt index 2800b9274..ced1697b0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt @@ -20,15 +20,8 @@ fun mangaGridItemAD( coil: ImageLoader, lifecycleOwner: LifecycleOwner, clickListener: OnListItemClickListener, - viewFactory: AsyncViewFactory, ) = adapterDelegateViewBinding( - { inflater, parent -> - viewFactory[R.layout.item_manga_grid]?.let { - ItemMangaGridBinding.bind(it) - } ?: run { - ItemMangaGridBinding.inflate(inflater, parent, false) - } - } + { inflater, parent -> ItemMangaGridBinding.inflate(inflater, parent, false) } ) { var imageRequest: Disposable? = null diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt index ab424f25c..2b359a8a9 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt @@ -12,20 +12,13 @@ class MangaListAdapter( coil: ImageLoader, lifecycleOwner: LifecycleOwner, listener: MangaListListener, - viewFactory: AsyncViewFactory, ) : AsyncListDifferDelegationAdapter(DiffCallback()) { init { delegatesManager - .addDelegate( - ITEM_TYPE_MANGA_LIST, - mangaListItemAD(coil, lifecycleOwner, listener, viewFactory) - ) - .addDelegate( - ITEM_TYPE_MANGA_LIST_DETAILED, - mangaListDetailedItemAD(coil, lifecycleOwner, listener, viewFactory) - ) - .addDelegate(ITEM_TYPE_MANGA_GRID, mangaGridItemAD(coil, lifecycleOwner, listener, viewFactory)) + .addDelegate(ITEM_TYPE_MANGA_LIST, mangaListItemAD(coil, lifecycleOwner, listener)) + .addDelegate(ITEM_TYPE_MANGA_LIST_DETAILED, mangaListDetailedItemAD(coil, lifecycleOwner, listener)) + .addDelegate(ITEM_TYPE_MANGA_GRID, mangaGridItemAD(coil, lifecycleOwner, listener)) .addDelegate(ITEM_TYPE_LOADING_FOOTER, loadingFooterAD()) .addDelegate(ITEM_TYPE_LOADING_STATE, loadingStateAD()) .addDelegate(ITEM_TYPE_DATE, relatedDateItemAD()) diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt index 3824cab3d..10e9a473d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt @@ -21,15 +21,8 @@ fun mangaListDetailedItemAD( coil: ImageLoader, lifecycleOwner: LifecycleOwner, clickListener: OnListItemClickListener, - viewFactory: AsyncViewFactory, ) = adapterDelegateViewBinding( - { inflater, parent -> - viewFactory[R.layout.item_manga_list_details]?.let { - ItemMangaListDetailsBinding.bind(it) - } ?: run { - ItemMangaListDetailsBinding.inflate(inflater, parent, false) - } - } + { inflater, parent -> ItemMangaListDetailsBinding.inflate(inflater, parent, false) } ) { var imageRequest: Disposable? = null diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt index c955c9f90..18696de6b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt @@ -21,15 +21,8 @@ fun mangaListItemAD( coil: ImageLoader, lifecycleOwner: LifecycleOwner, clickListener: OnListItemClickListener, - viewFactory: AsyncViewFactory, ) = adapterDelegateViewBinding( - { inflater, parent -> - viewFactory[R.layout.item_manga_list]?.let { - ItemMangaListBinding.bind(it) - } ?: run { - ItemMangaListBinding.inflate(inflater, parent, false) - } - } + { inflater, parent -> ItemMangaListBinding.inflate(inflater, parent, false) } ) { var imageRequest: Disposable? = null