From 5ba67490de94fe990392336894c491101922ee78 Mon Sep 17 00:00:00 2001 From: Robb <35880555+robbwatershed@users.noreply.github.com> Date: Sun, 13 Oct 2024 21:25:00 +0200 Subject: [PATCH 01/44] Debump Balloon --- app/build.gradle | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 427474b2b..e88d73571 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -155,6 +155,7 @@ dependencies { /** * MEDIA */ + /* // Image loader: github.com/bumptech/glide def glide_version = '4.16.0' implementation "com.github.bumptech.glide:glide:$glide_version" @@ -169,6 +170,11 @@ dependencies { implementation "com.github.penfeizhou.android.animation:gif:$APNG4Android_version" implementation "com.github.penfeizhou.android.animation:awebp:$APNG4Android_version" implementation "com.github.penfeizhou.android.animation:glide-plugin:$APNG4Android_version" + */ + + // Image loader and animated pics support -> https://github.com/coil-kt/coil + implementation("io.coil-kt.coil3:coil:3.0.0-rc01") + implementation("io.coil-kt.coil3:coil-network-okhttp:3.0.0-rc01") // Animated GIF creator -> https://github.com/waynejo/android-ndk-gif implementation 'io.github.waynejo:androidndkgif:1.0.1' @@ -197,7 +203,7 @@ dependencies { implementation 'com.github.AppIntro:AppIntro:6.1.0' // Tooltips - implementation 'com.github.skydoves:balloon:1.6.8' + implementation 'com.github.skydoves:balloon:1.6.7' // Popup menus with icons implementation 'com.github.skydoves:powermenu:2.2.4' From 1399da8607a7ed153b0e918d3e2bf53485943741 Mon Sep 17 00:00:00 2001 From: Robb <35880555+robbwatershed@users.noreply.github.com> Date: Sun, 13 Oct 2024 22:57:37 +0200 Subject: [PATCH 02/44] Replace Glide by coil (WIP) --- app/build.gradle | 1 + .../activities/MetadataEditActivity.kt | 10 ++-- .../hentoid/adapters/ImagePagerAdapter.kt | 50 ++++++++---------- .../me/devsaki/hentoid/core/AppStartup.kt | 34 ++++++++++--- .../devsaki/hentoid/core/CustomGlideModule.kt | 41 --------------- .../me/devsaki/hentoid/enums/AlertStatus.kt | 2 +- .../library/LibraryTransformDialogFragment.kt | 10 ++-- .../ReaderContentBottomSheetFragment.kt | 9 ++-- .../reader/ReaderImageBottomSheetFragment.kt | 10 ++-- .../fragments/reader/ReaderPagerFragment.kt | 19 ++++--- .../fragments/web/DuplicateDialogFragment.kt | 10 ++-- .../me/devsaki/hentoid/util/ContentHelper.kt | 5 +- .../me/devsaki/hentoid/util/GlideHelper.kt | 51 ------------------- .../devsaki/hentoid/util/image/CoilHelper.kt | 10 ++++ .../util/image/SmartRotateTransformation.kt | 43 ---------------- .../hentoid/viewholders/ContentItem.kt | 16 +++--- .../hentoid/viewholders/DuplicateItem.kt | 12 ++--- .../hentoid/viewholders/GroupDisplayItem.kt | 12 ++--- .../hentoid/viewholders/ImageFileItem.kt | 17 +++---- .../hentoid/viewholders/SubExpandableItem.kt | 11 ++-- .../hentoid/viewmodels/ReaderViewModel.kt | 7 +-- .../hentoid/workers/ExternalImportWorker.kt | 2 +- .../hentoid/workers/TransformWorker.kt | 8 +-- 23 files changed, 140 insertions(+), 250 deletions(-) delete mode 100644 app/src/main/java/me/devsaki/hentoid/core/CustomGlideModule.kt delete mode 100644 app/src/main/java/me/devsaki/hentoid/util/GlideHelper.kt create mode 100644 app/src/main/java/me/devsaki/hentoid/util/image/CoilHelper.kt delete mode 100644 app/src/main/java/me/devsaki/hentoid/util/image/SmartRotateTransformation.kt diff --git a/app/build.gradle b/app/build.gradle index e88d73571..418dde81a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -175,6 +175,7 @@ dependencies { // Image loader and animated pics support -> https://github.com/coil-kt/coil implementation("io.coil-kt.coil3:coil:3.0.0-rc01") implementation("io.coil-kt.coil3:coil-network-okhttp:3.0.0-rc01") + implementation("io.coil-kt.coil3:coil-gif:3.0.0-rc01") // Animated GIF creator -> https://github.com/waynejo/android-ndk-gif implementation 'io.github.waynejo:androidndkgif:1.0.1' diff --git a/app/src/main/java/me/devsaki/hentoid/activities/MetadataEditActivity.kt b/app/src/main/java/me/devsaki/hentoid/activities/MetadataEditActivity.kt index 5630e3d2a..af615e82b 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/MetadataEditActivity.kt +++ b/app/src/main/java/me/devsaki/hentoid/activities/MetadataEditActivity.kt @@ -12,7 +12,7 @@ import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope -import com.bumptech.glide.Glide +import coil3.load import com.google.android.flexbox.AlignItems import com.google.android.flexbox.FlexWrap import com.google.android.flexbox.FlexboxLayoutManager @@ -42,10 +42,8 @@ import me.devsaki.hentoid.fragments.metadata.GalleryPickerDialogFragment import me.devsaki.hentoid.fragments.metadata.MetaEditBottomSheetFragment import me.devsaki.hentoid.fragments.metadata.MetaRenameDialogFragment import me.devsaki.hentoid.util.applyTheme -import me.devsaki.hentoid.util.bindOnlineCover import me.devsaki.hentoid.util.dimensAsDp import me.devsaki.hentoid.util.getFlagResourceId -import me.devsaki.hentoid.util.glideOptionCenterInside import me.devsaki.hentoid.viewholders.AttributeItem import me.devsaki.hentoid.viewholders.AttributeTypeFilterItem import me.devsaki.hentoid.viewmodels.MetadataEditViewModel @@ -206,17 +204,23 @@ class MetadataEditActivity : BaseActivity(), GalleryPickerDialogFragment.Parent, } else { it.ivCover.visibility = View.VISIBLE if (thumbLocation.startsWith("http")) { + it.ivCover.load(thumbLocation) +/* bindOnlineCover(thumbLocation, contents[0])?.let { glideUrl -> Glide.with(it.ivCover) .load(glideUrl) .apply(glideOptionCenterInside) .into(it.ivCover) } + */ } else // From stored picture + it.ivCover.load(thumbLocation) + /* Glide.with(it.ivCover) .load(Uri.parse(thumbLocation)) .apply(glideOptionCenterInside) .into(it.ivCover) + */ } // Flag (language) diff --git a/app/src/main/java/me/devsaki/hentoid/adapters/ImagePagerAdapter.kt b/app/src/main/java/me/devsaki/hentoid/adapters/ImagePagerAdapter.kt index a79af6eed..2caa9e1a6 100644 --- a/app/src/main/java/me/devsaki/hentoid/adapters/ImagePagerAdapter.kt +++ b/app/src/main/java/me/devsaki/hentoid/adapters/ImagePagerAdapter.kt @@ -4,7 +4,6 @@ import android.annotation.SuppressLint import android.content.Context import android.graphics.Bitmap import android.graphics.Point -import android.graphics.drawable.Drawable import android.net.Uri import android.view.LayoutInflater import android.view.View @@ -18,15 +17,9 @@ import androidx.lifecycle.setViewTreeLifecycleOwner import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView -import com.bumptech.glide.Glide -import com.bumptech.glide.load.DataSource -import com.bumptech.glide.load.MultiTransformation -import com.bumptech.glide.load.Transformation -import com.bumptech.glide.load.engine.GlideException -import com.bumptech.glide.load.resource.UnitTransformation -import com.bumptech.glide.load.resource.bitmap.CenterInside -import com.bumptech.glide.request.RequestListener -import com.bumptech.glide.request.target.Target +import coil3.load +import coil3.request.ErrorResult +import coil3.request.SuccessResult import kotlinx.coroutines.Runnable import me.devsaki.hentoid.R import me.devsaki.hentoid.core.BiConsumer @@ -43,7 +36,6 @@ import me.devsaki.hentoid.gles_renderer.GPUImage import me.devsaki.hentoid.util.Preferences import me.devsaki.hentoid.util.Settings import me.devsaki.hentoid.util.file.getExtension -import me.devsaki.hentoid.util.image.SmartRotateTransformation import me.devsaki.hentoid.util.image.screenHeight import me.devsaki.hentoid.util.image.screenWidth import me.devsaki.hentoid.views.ZoomableRecyclerView @@ -405,7 +397,7 @@ class ImagePagerAdapter(val context: Context) : inner class ImageViewHolder(val rootView: View) : RecyclerView.ViewHolder(rootView), - OnImageEventListener, RequestListener { + OnImageEventListener { val ssiv: CustomSubsamplingScaleImageView = itemView.requireById(R.id.ssiv) private val imageView: ImageView = itemView.requireById(R.id.imageview) val noImgTxt: TextView = itemView.requireById(R.id.viewer_no_page_txt) @@ -442,7 +434,14 @@ class ImagePagerAdapter(val context: Context) : ssiv.setImage(uri(uri)) } else { // ImageView val view = imgView as ImageView - Timber.d("Using Glide") + Timber.d("Using Coil") + view.load(uri) { + listener( + onError = { _, err -> onCoilLoadFailed(err) }, + onSuccess = { _, res -> onCoilLoadSuccess(res) } + ) + } + /* val centerInside: Transformation = CenterInside() val smartRotate90 = if (autoRotate) SmartRotateTransformation( 90f, screenWidth, screenHeight @@ -450,6 +449,7 @@ class ImagePagerAdapter(val context: Context) : Glide.with(view).load(uri) .optionalTransform(MultiTransformation(centerInside, smartRotate90)) .listener(this).into(view) + */ } } @@ -607,28 +607,22 @@ class ImagePagerAdapter(val context: Context) : // Nothing special } - // == GLIDE CALLBACKS - override fun onLoadFailed( - e: GlideException?, model: Any?, target: Target, isFirstResource: Boolean - ): Boolean { + // == COIL CALLBACKS + private fun onCoilLoadFailed(err: ErrorResult) { Timber.d( - e, "Picture %d : Glide loading failed : %s", absoluteAdapterPosition, img!!.fileUri + err.throwable, + "Picture %d : Coil loading failed : %s", + absoluteAdapterPosition, + img!!.fileUri ) if (isImageView) noImgTxt.visibility = View.VISIBLE - return false } - override fun onResourceReady( - resource: Drawable, - model: Any, - target: Target, - dataSource: DataSource, - isFirstResource: Boolean - ): Boolean { + private fun onCoilLoadSuccess(result: SuccessResult): Boolean { noImgTxt.visibility = View.GONE if (Preferences.Constant.VIEWER_ORIENTATION_VERTICAL == viewerOrientation) adjustHeight( - resource.intrinsicWidth, - resource.intrinsicHeight, + result.image.width, + result.image.height, true ) return false diff --git a/app/src/main/java/me/devsaki/hentoid/core/AppStartup.kt b/app/src/main/java/me/devsaki/hentoid/core/AppStartup.kt index d69b85459..288d88cf9 100644 --- a/app/src/main/java/me/devsaki/hentoid/core/AppStartup.kt +++ b/app/src/main/java/me/devsaki/hentoid/core/AppStartup.kt @@ -8,9 +8,14 @@ import android.content.IntentFilter import android.content.pm.PackageManager import android.hardware.usb.UsbManager import android.os.Build +import android.os.Build.VERSION.SDK_INT import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkManager +import coil3.ImageLoader +import coil3.SingletonImageLoader +import coil3.gif.AnimatedImageDecoder +import coil3.gif.GifDecoder import com.google.firebase.analytics.FirebaseAnalytics import com.google.firebase.crashlytics.FirebaseCrashlytics import kotlinx.coroutines.DelicateCoroutinesApi @@ -126,7 +131,7 @@ object AppStartup { this::stopWorkers, this::processAppUpdate, this::loadSiteProperties, - this::initUtils, + this::initNotifications, this::initTLS ) } @@ -139,7 +144,8 @@ object AppStartup { this::createBookmarksJson, this::createPlugReceiver, this::activateTextIntent, - this::checkAchievements + this::checkAchievements, + this::initCoil ) } @@ -169,8 +175,8 @@ object AppStartup { } } - private fun initUtils(context: Context, emitter: (Float) -> Unit) { - Timber.i("Init utils : start") + private fun initNotifications(context: Context, emitter: (Float) -> Unit) { + Timber.i("Init notifications : start") // Init notification channels StartupNotificationChannel.init(context) UpdateNotificationChannel.init(context) @@ -184,7 +190,7 @@ object AppStartup { // Clears all previous notifications val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager manager.cancelAll() - Timber.i("Init utils : done") + Timber.i("Init notifications : done") } private fun processAppUpdate(context: Context, emitter: (Float) -> Unit) { @@ -293,10 +299,26 @@ object AppStartup { Timber.i("Check achievements : done") } + private fun initCoil(context: Context, emitter: (Float) -> Unit) { + Timber.i("Init coil : start") + SingletonImageLoader.setSafe { + ImageLoader.Builder(context) + .components { + if (SDK_INT >= 28) { + add(AnimatedImageDecoder.Factory()) + } else { + add(GifDecoder.Factory()) + } + } + .build() + } + Timber.i("Init coil : done") + } + private fun activateTextIntent(context: Context, emitter: (Float) -> Unit) { Timber.i("Activate text intent : start") - val flags = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) 0 + val flags = if (SDK_INT < Build.VERSION_CODES.R) 0 else PackageManager.SYNCHRONOUS val state = if (Settings.isTextMenuOn) PackageManager.COMPONENT_ENABLED_STATE_ENABLED diff --git a/app/src/main/java/me/devsaki/hentoid/core/CustomGlideModule.kt b/app/src/main/java/me/devsaki/hentoid/core/CustomGlideModule.kt deleted file mode 100644 index 291ed453d..000000000 --- a/app/src/main/java/me/devsaki/hentoid/core/CustomGlideModule.kt +++ /dev/null @@ -1,41 +0,0 @@ -package me.devsaki.hentoid.core - -import android.content.Context -import android.util.Log -import com.bumptech.glide.Glide -import com.bumptech.glide.GlideBuilder -import com.bumptech.glide.Registry -import com.bumptech.glide.annotation.GlideModule -import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader -import com.bumptech.glide.load.engine.executor.GlideExecutor -import com.bumptech.glide.load.model.GlideUrl -import com.bumptech.glide.module.AppGlideModule -import me.devsaki.hentoid.util.network.OkHttpClientSingleton -import java.io.InputStream - -/** - * Startup class to enable Glide over OkHttp and limit Glide parallel threads - */ -@GlideModule -class CustomGlideModule : AppGlideModule() { - override fun registerComponents(context: Context, glide: Glide, registry: Registry) { - val client = OkHttpClientSingleton.getInstance() - val factory = OkHttpUrlLoader.Factory(client) - - registry.replace( - GlideUrl::class.java, - InputStream::class.java, factory - ) - } - - override fun applyOptions(context: Context, builder: GlideBuilder) { - builder // customize builder - .setLogLevel(Log.ERROR) - .setSourceExecutor( // source executor specify the thread it uses to load the image from remote - GlideExecutor - .newSourceBuilder() - .setThreadCount(4) // use the builder to set a desired thread amount - .build() - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/me/devsaki/hentoid/enums/AlertStatus.kt b/app/src/main/java/me/devsaki/hentoid/enums/AlertStatus.kt index 7d66c1a73..b56f26139 100644 --- a/app/src/main/java/me/devsaki/hentoid/enums/AlertStatus.kt +++ b/app/src/main/java/me/devsaki/hentoid/enums/AlertStatus.kt @@ -18,7 +18,7 @@ enum class AlertStatus(@ColorRes val color: Int, @DrawableRes val icon: Int) { // Same as ValueOf with a fallback to NONE // (vital for forward compatibility) fun searchByName(name: String): AlertStatus { - for (s in AlertStatus.values()) if (s.name.equals(name, ignoreCase = true)) return s + for (s in entries) if (s.name.equals(name, ignoreCase = true)) return s return NONE } } diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/library/LibraryTransformDialogFragment.kt b/app/src/main/java/me/devsaki/hentoid/fragments/library/LibraryTransformDialogFragment.kt index 9f300c1be..fa2afd2ea 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/library/LibraryTransformDialogFragment.kt +++ b/app/src/main/java/me/devsaki/hentoid/fragments/library/LibraryTransformDialogFragment.kt @@ -17,7 +17,7 @@ import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequest import androidx.work.WorkManager import androidx.work.workDataOf -import com.bumptech.glide.Glide +import coil3.load import com.google.android.material.textfield.TextInputLayout import com.mikepenz.fastadapter.FastAdapter import com.mikepenz.fastadapter.adapters.ItemAdapter @@ -27,7 +27,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import me.devsaki.hentoid.R -import me.devsaki.hentoid.core.HentoidApp import me.devsaki.hentoid.core.WORK_CLOSEABLE import me.devsaki.hentoid.core.setOnTextChangedListener import me.devsaki.hentoid.database.ObjectBoxDAO @@ -40,7 +39,6 @@ import me.devsaki.hentoid.util.Settings import me.devsaki.hentoid.util.file.formatHumanReadableSize import me.devsaki.hentoid.util.file.getExtensionFromMimeType import me.devsaki.hentoid.util.file.getInputStream -import me.devsaki.hentoid.util.getGlideOptionCenterImage import me.devsaki.hentoid.util.image.TransformParams import me.devsaki.hentoid.util.image.determineEncoder import me.devsaki.hentoid.util.image.getMimeTypeFromPictureBinary @@ -88,8 +86,6 @@ class LibraryTransformDialogFragment : BaseDialogFragment() private val fastAdapter = FastAdapter.with(itemAdapter) - private val glideRequestOptions = getGlideOptionCenterImage(HentoidApp.getInstance()) - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -334,8 +330,8 @@ class LibraryTransformDialogFragment : BaseDialogFragment, var ivReorder: View? = view.findViewById(R.id.ivReorder) var downloadButton: View? = view.findViewById(R.id.ivRedownload) - private val glideRequestOptions = getGlideOptionCenterImage(view.context) private var deleteActionRunnable: Runnable? = null // Extra info to display in stacktraces @@ -392,6 +386,8 @@ class ContentItem : AbstractItem, } ivCover.visibility = View.VISIBLE // Use content's cookies to load image (useful for ExHentai when viewing queue screen) + ivCover.load(thumbLocation) + /* if (thumbLocation.startsWith("http")) { val glideUrl = bindOnlineCover(thumbLocation, content) if (glideUrl != null) { @@ -402,12 +398,15 @@ class ContentItem : AbstractItem, Glide.with(ivCover).load(Uri.parse(thumbLocation)) .signature(ObjectKey(content.uniqueHash())).apply(glideRequestOptions) .into(ivCover) + */ } private fun attachChapterCover(chapter: Chapter) { val thumbLocation = chapter.imageList.firstOrNull()?.usableUri if (thumbLocation != null) { + ivCover.load(thumbLocation) ivCover.visibility = View.VISIBLE + /* val glideRequest = Glide.with(ivCover) val builder = if (thumbLocation.startsWith("http")) glideRequest.load( @@ -417,6 +416,7 @@ class ContentItem : AbstractItem, builder.signature(ObjectKey(chapter.uniqueHash())).apply(glideRequestOptions) .into(ivCover) + */ } else { ivCover.visibility = View.INVISIBLE } @@ -651,7 +651,7 @@ class ContentItem : AbstractItem, deleteActionRunnable = null debugStr = "[no data]" swipeableView.translationX = 0f - if (isValidContextForGlide(ivCover)) Glide.with(ivCover).clear(ivCover) + //if (isValidContextForGlide(ivCover)) Glide.with(ivCover).clear(ivCover) } override fun onDragged() { diff --git a/app/src/main/java/me/devsaki/hentoid/viewholders/DuplicateItem.kt b/app/src/main/java/me/devsaki/hentoid/viewholders/DuplicateItem.kt index 6fc754fd7..711903d13 100644 --- a/app/src/main/java/me/devsaki/hentoid/viewholders/DuplicateItem.kt +++ b/app/src/main/java/me/devsaki/hentoid/viewholders/DuplicateItem.kt @@ -13,7 +13,7 @@ import androidx.annotation.ColorInt import androidx.annotation.DrawableRes import androidx.constraintlayout.widget.Group import androidx.core.content.ContextCompat -import com.bumptech.glide.Glide +import coil3.load import com.google.android.material.materialswitch.MaterialSwitch import com.mikepenz.fastadapter.FastAdapter import com.mikepenz.fastadapter.items.AbstractItem @@ -26,12 +26,9 @@ import me.devsaki.hentoid.enums.Site import me.devsaki.hentoid.enums.StatusContent import me.devsaki.hentoid.ui.BlinkAnimation import me.devsaki.hentoid.util.Preferences -import me.devsaki.hentoid.util.bindOnlineCover import me.devsaki.hentoid.util.formatArtistForDisplay import me.devsaki.hentoid.util.getFlagResourceId -import me.devsaki.hentoid.util.getGlideOptionCenterImage import me.devsaki.hentoid.util.getThemedColor -import me.devsaki.hentoid.util.isValidContextForGlide class DuplicateItem(result: DuplicateEntry, private val viewType: ViewType) : AbstractItem() { @@ -116,8 +113,6 @@ class DuplicateItem(result: DuplicateEntry, private val viewType: ViewType) : private var deleteButton: TextView? = itemView.findViewById(R.id.delete_choice) var keepDeleteSwitch: MaterialSwitch? = itemView.findViewById(R.id.keep_delete) - private val glideRequestOptions = getGlideOptionCenterImage(view.context) - override fun bindView(item: DuplicateItem, payloads: List) { item.content ?: return @@ -156,6 +151,8 @@ class DuplicateItem(result: DuplicateEntry, private val viewType: ViewType) : } it.visibility = View.VISIBLE // Use content's cookies to load image (useful for ExHentai when viewing queue screen) + it.load(thumbLocation) + /* if (thumbLocation.startsWith("http")) { bindOnlineCover(thumbLocation, content)?.let { glideUrl -> Glide.with(it).load(glideUrl).apply(glideRequestOptions).into(it) @@ -164,6 +161,7 @@ class DuplicateItem(result: DuplicateEntry, private val viewType: ViewType) : .load(Uri.parse(thumbLocation)) .apply(glideRequestOptions) .into(it) + */ } } @@ -323,7 +321,7 @@ class DuplicateItem(result: DuplicateEntry, private val viewType: ViewType) : override fun unbindView(item: DuplicateItem) { ivCover?.let { - if (isValidContextForGlide(it)) Glide.with(it).clear(it) + //if (isValidContextForGlide(it)) Glide.with(it).clear(it) } } } diff --git a/app/src/main/java/me/devsaki/hentoid/viewholders/GroupDisplayItem.kt b/app/src/main/java/me/devsaki/hentoid/viewholders/GroupDisplayItem.kt index 1e904b4c5..e2487e89f 100644 --- a/app/src/main/java/me/devsaki/hentoid/viewholders/GroupDisplayItem.kt +++ b/app/src/main/java/me/devsaki/hentoid/viewholders/GroupDisplayItem.kt @@ -8,8 +8,7 @@ import android.widget.TextView import androidx.core.view.isVisible import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView -import com.bumptech.glide.Glide -import com.bumptech.glide.signature.ObjectKey +import coil3.load import com.mikepenz.fastadapter.FastAdapter import com.mikepenz.fastadapter.drag.IExtendedDraggable import com.mikepenz.fastadapter.items.AbstractItem @@ -22,9 +21,7 @@ import me.devsaki.hentoid.database.domains.Content import me.devsaki.hentoid.database.domains.Group import me.devsaki.hentoid.ui.BlinkAnimation import me.devsaki.hentoid.util.Settings -import me.devsaki.hentoid.util.getGlideOptionCenterImage import me.devsaki.hentoid.util.getRatingResourceId -import me.devsaki.hentoid.util.isValidContextForGlide class GroupDisplayItem( val group: Group, @@ -82,8 +79,6 @@ class GroupDisplayItem( private var coverUri = "" - private var glideRequestOptions = getGlideOptionCenterImage(view.context) - override fun bindView(item: GroupDisplayItem, payloads: List) { // Payloads are set when the content stays the same but some properties alone change if (payloads.isNotEmpty()) { @@ -162,6 +157,8 @@ class GroupDisplayItem( return } it.visibility = View.VISIBLE + it.load(uri) + /* if (uri.startsWith("http")) Glide.with(it) .load(uri) .signature(ObjectKey(uri)) @@ -172,6 +169,7 @@ class GroupDisplayItem( .signature(ObjectKey(uri)) .apply(glideRequestOptions) .into(it) + */ } } @@ -182,7 +180,7 @@ class GroupDisplayItem( override fun unbindView(item: GroupDisplayItem) { ivCover?.let { - if (isValidContextForGlide(it)) Glide.with(it).clear(it) + //if (isValidContextForGlide(it)) Glide.with(it).clear(it) } } } diff --git a/app/src/main/java/me/devsaki/hentoid/viewholders/ImageFileItem.kt b/app/src/main/java/me/devsaki/hentoid/viewholders/ImageFileItem.kt index b675af528..272b114f3 100644 --- a/app/src/main/java/me/devsaki/hentoid/viewholders/ImageFileItem.kt +++ b/app/src/main/java/me/devsaki/hentoid/viewholders/ImageFileItem.kt @@ -10,13 +10,7 @@ import android.widget.ImageView import android.widget.TextView import androidx.core.content.ContextCompat import androidx.core.view.ViewCompat -import com.bumptech.glide.Glide -import com.bumptech.glide.load.DataSource -import com.bumptech.glide.load.engine.GlideException -import com.bumptech.glide.request.RequestListener -import com.bumptech.glide.request.target.Target -import com.bumptech.glide.signature.ObjectKey -import com.github.penfeizhou.animation.FrameAnimationDrawable +import coil3.load import com.mikepenz.fastadapter.FastAdapter import com.mikepenz.fastadapter.IExpandable import com.mikepenz.fastadapter.IParentItem @@ -26,8 +20,6 @@ import me.devsaki.hentoid.R import me.devsaki.hentoid.activities.bundles.ImageItemBundle import me.devsaki.hentoid.database.domains.Chapter import me.devsaki.hentoid.database.domains.ImageFile -import me.devsaki.hentoid.util.glideOptionCenterInside -import me.devsaki.hentoid.util.isValidContextForGlide private const val HEART_SYMBOL = "❤" @@ -139,6 +131,9 @@ class ImageFileItem(private val image: ImageFile, private val showChapter: Boole } else chapterOverlay.visibility = View.GONE // Image + // TODO If animated, only load frame zero as a plain bitmap + image.load(item.image.fileUri) + /* Glide.with(image) .load(Uri.parse(item.image.fileUri)) .signature(ObjectKey(item.image.uniqueHash())) @@ -176,6 +171,8 @@ class ImageFileItem(private val image: ImageFile, private val showChapter: Boole }) .apply(glideOptionCenterInside) .into(image) + + */ } private fun updateText(item: ImageFileItem) { @@ -193,7 +190,7 @@ class ImageFileItem(private val image: ImageFile, private val showChapter: Boole } override fun unbindView(item: ImageFileItem) { - if (isValidContextForGlide(image)) Glide.with(image).clear(image) + //if (isValidContextForGlide(image)) Glide.with(image).clear(image) } } } \ No newline at end of file diff --git a/app/src/main/java/me/devsaki/hentoid/viewholders/SubExpandableItem.kt b/app/src/main/java/me/devsaki/hentoid/viewholders/SubExpandableItem.kt index 5b772ae65..cdf0ef54a 100644 --- a/app/src/main/java/me/devsaki/hentoid/viewholders/SubExpandableItem.kt +++ b/app/src/main/java/me/devsaki/hentoid/viewholders/SubExpandableItem.kt @@ -1,6 +1,5 @@ package me.devsaki.hentoid.viewholders -import android.net.Uri import android.view.MotionEvent import android.view.View import android.widget.ImageView @@ -8,7 +7,7 @@ import android.widget.TextView import androidx.annotation.StringRes import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView -import com.bumptech.glide.Glide +import coil3.load import com.mikepenz.fastadapter.ClickListener import com.mikepenz.fastadapter.FastAdapter import com.mikepenz.fastadapter.IAdapter @@ -20,12 +19,9 @@ import com.mikepenz.fastadapter.listeners.TouchEventHook import com.mikepenz.fastadapter.ui.utils.FastAdapterUIUtils import com.mikepenz.fastadapter.ui.utils.StringHolder import me.devsaki.hentoid.R -import me.devsaki.hentoid.core.HentoidApp import me.devsaki.hentoid.core.requireById import me.devsaki.hentoid.core.setMiddleEllipsis import me.devsaki.hentoid.database.domains.ImageFile -import me.devsaki.hentoid.util.bindOnlineCover -import me.devsaki.hentoid.util.getGlideOptionCenterImage /** * Inspired by mikepenz @@ -52,8 +48,6 @@ class SubExpandableItem( private var mOnClickListener: ClickListener>? = null - private val glideRequestOptions = getGlideOptionCenterImage(HentoidApp.getInstance()) - //we define a clickListener in here so we can directly animate /** * we overwrite the item specific click listener so we can automatically animate within the item @@ -179,6 +173,8 @@ class SubExpandableItem( } ivCover.visibility = View.VISIBLE // Use content's cookies to load image (useful for ExHentai when viewing queue screen) + ivCover.load(thumbLocation) + /* if (thumbLocation.startsWith("http")) { bindOnlineCover(thumbLocation, null)?.let { glideUrl -> Glide.with(ivCover).load(glideUrl).apply(glideRequestOptions).into(ivCover) @@ -187,6 +183,7 @@ class SubExpandableItem( Glide.with(ivCover).load(Uri.parse(thumbLocation)) .apply(glideRequestOptions) .into(ivCover) + */ } } diff --git a/app/src/main/java/me/devsaki/hentoid/viewmodels/ReaderViewModel.kt b/app/src/main/java/me/devsaki/hentoid/viewmodels/ReaderViewModel.kt index 8e82db5f9..7308fbaff 100644 --- a/app/src/main/java/me/devsaki/hentoid/viewmodels/ReaderViewModel.kt +++ b/app/src/main/java/me/devsaki/hentoid/viewmodels/ReaderViewModel.kt @@ -14,7 +14,6 @@ import androidx.lifecycle.viewModelScope import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequest import androidx.work.WorkManager -import com.bumptech.glide.Glide import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -57,6 +56,7 @@ import me.devsaki.hentoid.util.file.getOutputStream import me.devsaki.hentoid.util.file.listFiles import me.devsaki.hentoid.util.getPictureFilesFromContent import me.devsaki.hentoid.util.image.PdfManager +import me.devsaki.hentoid.util.image.clearCoilCache import me.devsaki.hentoid.util.image.getMimeTypeFromUri import me.devsaki.hentoid.util.matchFilesToImageList import me.devsaki.hentoid.util.network.HEADER_COOKIE_KEY @@ -2057,8 +2057,9 @@ class ReaderViewModel( ) ) - // Reset Glide cache as it gets confused by the swapping - Glide.get(getApplication()).clearDiskCache() + // Reset Coil cache as it gets confused by the swapping + clearCoilCache(getApplication()) + //Glide.get(getApplication()).clearDiskCache() } /** diff --git a/app/src/main/java/me/devsaki/hentoid/workers/ExternalImportWorker.kt b/app/src/main/java/me/devsaki/hentoid/workers/ExternalImportWorker.kt index 52b6f654c..04e114a98 100644 --- a/app/src/main/java/me/devsaki/hentoid/workers/ExternalImportWorker.kt +++ b/app/src/main/java/me/devsaki/hentoid/workers/ExternalImportWorker.kt @@ -298,7 +298,7 @@ class ExternalImportWorker(context: Context, parameters: WorkerParameters) : .split(File.separator) val parentNames = getFullPathFromUri(context, deltaPlusRoot.uri) .split(File.separator).toMutableList() - for (i in extRootElts.indices - 1) parentNames.removeFirst() + for (i in extRootElts.indices - 1) parentNames.removeAt(parentNames.lastIndex) Timber.d(" parents : $parentNames") deltaPlusPairs.values.forEach { docs -> diff --git a/app/src/main/java/me/devsaki/hentoid/workers/TransformWorker.kt b/app/src/main/java/me/devsaki/hentoid/workers/TransformWorker.kt index 7394ce29d..a2148da83 100644 --- a/app/src/main/java/me/devsaki/hentoid/workers/TransformWorker.kt +++ b/app/src/main/java/me/devsaki/hentoid/workers/TransformWorker.kt @@ -7,7 +7,7 @@ import androidx.core.net.toUri import androidx.documentfile.provider.DocumentFile import androidx.work.Data import androidx.work.WorkerParameters -import com.bumptech.glide.Glide +import coil3.SingletonImageLoader import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import kotlinx.coroutines.CoroutineScope @@ -31,6 +31,7 @@ import me.devsaki.hentoid.util.file.getInputStream import me.devsaki.hentoid.util.file.getOrCreateCacheFolder import me.devsaki.hentoid.util.file.saveBinary import me.devsaki.hentoid.util.image.TransformParams +import me.devsaki.hentoid.util.image.clearCoilCache import me.devsaki.hentoid.util.image.determineEncoder import me.devsaki.hentoid.util.image.isImageLossless import me.devsaki.hentoid.util.image.transform @@ -67,8 +68,9 @@ class TransformWorker(context: Context, parameters: WorkerParameters) : dao.cleanup() upscaler?.cleanup() - // Reset Glide cache as it gets confused by the resizing - Glide.get(applicationContext).clearDiskCache() + // Reset Coil cache as it gets confused by the resizing + clearCoilCache(applicationContext) + //Glide.get(applicationContext).clearDiskCache() } override fun getToWork(input: Data) { From 35b85ca16ff2bb08550a24444254f7f21fb50eb4 Mon Sep 17 00:00:00 2001 From: Robb <35880555+robbwatershed@users.noreply.github.com> Date: Mon, 14 Oct 2024 21:11:00 +0200 Subject: [PATCH 03/44] Fix unarchive issue --- .../main/java/me/devsaki/hentoid/util/file/ArchiveHelper.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/me/devsaki/hentoid/util/file/ArchiveHelper.kt b/app/src/main/java/me/devsaki/hentoid/util/file/ArchiveHelper.kt index 2bce56c02..902371057 100644 --- a/app/src/main/java/me/devsaki/hentoid/util/file/ArchiveHelper.kt +++ b/app/src/main/java/me/devsaki/hentoid/util/file/ArchiveHelper.kt @@ -202,7 +202,7 @@ fun Context.extractArchiveEntries( return extractArchiveEntries( uri, fileCreator = { targetFileName -> File(targetFolder.absolutePath + File.separator + targetFileName) }, - fileFinder = { targetFileName -> Uri.fromFile(findFile(targetFolder, targetFileName)) }, + fileFinder = { targetFileName -> findFile(targetFolder, targetFileName)?.let { Uri.fromFile(it)} }, entriesToExtract, interrupt, onExtract, onComplete ) } @@ -236,7 +236,7 @@ fun Context.extractArchiveEntriesBlocking( extractArchiveEntries( uri, fileCreator = { targetFileName -> File(targetFolder.absolutePath + File.separator + targetFileName) }, - fileFinder = { targetFileName -> Uri.fromFile(findFile(targetFolder, targetFileName)) }, + fileFinder = { targetFileName -> findFile(targetFolder, targetFileName)?.let { Uri.fromFile(it)} }, entriesToExtract, null, callback, null ) From 8e5b99478093c29051e767d3cba724550bf0654d Mon Sep 17 00:00:00 2001 From: Robb <35880555+robbwatershed@users.noreply.github.com> Date: Tue, 15 Oct 2024 23:38:59 +0200 Subject: [PATCH 04/44] Tweak to generated high-speed GIFs to improve their compatibility --- .../main/java/me/devsaki/hentoid/util/image/ImageHelper.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/me/devsaki/hentoid/util/image/ImageHelper.kt b/app/src/main/java/me/devsaki/hentoid/util/image/ImageHelper.kt index 457ededf3..b488d3ec7 100644 --- a/app/src/main/java/me/devsaki/hentoid/util/image/ImageHelper.kt +++ b/app/src/main/java/me/devsaki/hentoid/util/image/ImageHelper.kt @@ -29,6 +29,7 @@ import java.io.IOException import java.io.InputStream import java.nio.charset.StandardCharsets import kotlin.math.abs +import kotlin.math.max import kotlin.math.min import kotlin.math.pow @@ -366,7 +367,9 @@ fun assembleGif( getInputStream(context, frame.first).use { input -> BitmapFactory.decodeStream(input, null, options)?.let { b -> try { - gifEncoder.encodeFrame(b, frame.second) + // Min cap of 11ms to make the GIF compatible with most players + // (see https://android.googlesource.com/platform/frameworks/base/+/2be87bb707e2c6d75f668c4aff6697b85fbf5b15) + gifEncoder.encodeFrame(b, max(11, frame.second)) } finally { b.recycle() } From b292c86c6a2c1dddb4e1fbd7b130a2fa57b5219d Mon Sep 17 00:00:00 2001 From: Robb <35880555+robbwatershed@users.noreply.github.com> Date: Wed, 16 Oct 2024 13:30:06 +0200 Subject: [PATCH 05/44] 11ms won't cut it as the GIF format precision is 1/100th of a second --- .../main/java/me/devsaki/hentoid/util/image/ImageHelper.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/me/devsaki/hentoid/util/image/ImageHelper.kt b/app/src/main/java/me/devsaki/hentoid/util/image/ImageHelper.kt index b488d3ec7..c9f875926 100644 --- a/app/src/main/java/me/devsaki/hentoid/util/image/ImageHelper.kt +++ b/app/src/main/java/me/devsaki/hentoid/util/image/ImageHelper.kt @@ -367,9 +367,9 @@ fun assembleGif( getInputStream(context, frame.first).use { input -> BitmapFactory.decodeStream(input, null, options)?.let { b -> try { - // Min cap of 11ms to make the GIF compatible with most players + // Warning : if frame.second is <= 10, GIFs will be read slower on most readers // (see https://android.googlesource.com/platform/frameworks/base/+/2be87bb707e2c6d75f668c4aff6697b85fbf5b15) - gifEncoder.encodeFrame(b, max(11, frame.second)) + gifEncoder.encodeFrame(b, frame.second) } finally { b.recycle() } From fcd134e52694f4dfb099a79b58eee0055621c97d Mon Sep 17 00:00:00 2001 From: Robb <35880555+robbwatershed@users.noreply.github.com> Date: Thu, 17 Oct 2024 21:20:50 +0200 Subject: [PATCH 06/44] Fix crash [#1193] --- .../hentoid/viewmodels/ReaderViewModel.kt | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/me/devsaki/hentoid/viewmodels/ReaderViewModel.kt b/app/src/main/java/me/devsaki/hentoid/viewmodels/ReaderViewModel.kt index 7308fbaff..a8819da73 100644 --- a/app/src/main/java/me/devsaki/hentoid/viewmodels/ReaderViewModel.kt +++ b/app/src/main/java/me/devsaki/hentoid/viewmodels/ReaderViewModel.kt @@ -816,9 +816,8 @@ class ReaderViewModel( */ fun deletePage(pageViewerIndex: Int, onError: (Throwable) -> Unit) { val imageFiles = viewerImagesInternal - if (imageFiles.size > pageViewerIndex && pageViewerIndex > -1) deletePages( - listOf(imageFiles[pageViewerIndex]), onError - ) + if (imageFiles.size > pageViewerIndex && pageViewerIndex > -1) + deletePages(listOf(imageFiles[pageViewerIndex]), onError) } /** @@ -999,7 +998,9 @@ class ReaderViewModel( } fun clearForceReload() { - viewerImagesInternal.forEach { it.isForceRefresh = false } + synchronized(viewerImagesInternal) { + viewerImagesInternal.forEach { it.isForceRefresh = false } + } } /** @@ -1068,18 +1069,20 @@ class ReaderViewModel( // Populate what's already cached val cachedIndexes = HashSet() var nbProcessed = 0 - indexesToLoad.forEach { idx -> - nbProcessed++ - viewerImagesInternal[idx].let { img -> - val key = formatCacheKey(img) - if (DiskCache.peekFile(key)) { - updateImgWithExtractedUri( - img, - idx, - DiskCache.getFile(key)!!, - 0 == cachedIndexes.size % 4 || nbProcessed == indexesToLoad.size - ) - cachedIndexes.add(idx) + synchronized(viewerImagesInternal) { + indexesToLoad.forEach { idx -> + nbProcessed++ + viewerImagesInternal[idx].let { img -> + val key = formatCacheKey(img) + if (DiskCache.peekFile(key)) { + updateImgWithExtractedUri( + img, + idx, + DiskCache.getFile(key)!!, + 0 == cachedIndexes.size % 4 || nbProcessed == indexesToLoad.size + ) + cachedIndexes.add(idx) + } } } } From b1fae53b4f8ae35e18b5278c7e4625317893fd02 Mon Sep 17 00:00:00 2001 From: Robb <35880555+robbwatershed@users.noreply.github.com> Date: Thu, 17 Oct 2024 21:21:30 +0200 Subject: [PATCH 07/44] Glide -> Coil (WIP) --- app/build.gradle | 8 +-- .../hentoid/adapters/ImagePagerAdapter.kt | 1 + .../me/devsaki/hentoid/core/AppStartup.kt | 12 ++-- .../me/devsaki/hentoid/util/ContentHelper.kt | 31 +++++++- .../devsaki/hentoid/util/image/CoilHelper.kt | 71 +++++++++++++++++++ .../hentoid/viewholders/ContentItem.kt | 8 ++- .../hentoid/viewholders/DuplicateItem.kt | 22 +++--- .../hentoid/viewholders/GroupDisplayItem.kt | 7 +- .../hentoid/viewholders/ImageFileItem.kt | 2 + 9 files changed, 134 insertions(+), 28 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 418dde81a..fe1b0fe38 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -163,14 +163,14 @@ dependencies { implementation("com.github.bumptech.glide:okhttp3-integration:$glide_version") { exclude group: 'glide-parent' } + */ // Animated pics support (APNG, AWEBP and GIF) -> https://github.com/penfeizhou/APNG4Android def APNG4Android_version = '3.0.1' implementation "com.github.penfeizhou.android.animation:apng:$APNG4Android_version" - implementation "com.github.penfeizhou.android.animation:gif:$APNG4Android_version" - implementation "com.github.penfeizhou.android.animation:awebp:$APNG4Android_version" - implementation "com.github.penfeizhou.android.animation:glide-plugin:$APNG4Android_version" - */ + //implementation "com.github.penfeizhou.android.animation:gif:$APNG4Android_version" + //implementation "com.github.penfeizhou.android.animation:awebp:$APNG4Android_version" + //implementation "com.github.penfeizhou.android.animation:glide-plugin:$APNG4Android_version" // Image loader and animated pics support -> https://github.com/coil-kt/coil implementation("io.coil-kt.coil3:coil:3.0.0-rc01") diff --git a/app/src/main/java/me/devsaki/hentoid/adapters/ImagePagerAdapter.kt b/app/src/main/java/me/devsaki/hentoid/adapters/ImagePagerAdapter.kt index 2caa9e1a6..64c13d365 100644 --- a/app/src/main/java/me/devsaki/hentoid/adapters/ImagePagerAdapter.kt +++ b/app/src/main/java/me/devsaki/hentoid/adapters/ImagePagerAdapter.kt @@ -435,6 +435,7 @@ class ImagePagerAdapter(val context: Context) : } else { // ImageView val view = imgView as ImageView Timber.d("Using Coil") + Timber.d("Using uri $uri") view.load(uri) { listener( onError = { _, err -> onCoilLoadFailed(err) }, diff --git a/app/src/main/java/me/devsaki/hentoid/core/AppStartup.kt b/app/src/main/java/me/devsaki/hentoid/core/AppStartup.kt index 288d88cf9..7340b869f 100644 --- a/app/src/main/java/me/devsaki/hentoid/core/AppStartup.kt +++ b/app/src/main/java/me/devsaki/hentoid/core/AppStartup.kt @@ -50,6 +50,7 @@ import me.devsaki.hentoid.util.file.DiskCache import me.devsaki.hentoid.util.file.findFile import me.devsaki.hentoid.util.file.getDocumentFromTreeUriString import me.devsaki.hentoid.util.file.readStreamAsString +import me.devsaki.hentoid.util.image.AnimatedPngDecoder import me.devsaki.hentoid.util.jsonToObject import me.devsaki.hentoid.util.updateBookmarksJson import me.devsaki.hentoid.workers.StartupWorker @@ -132,7 +133,8 @@ object AppStartup { this::processAppUpdate, this::loadSiteProperties, this::initNotifications, - this::initTLS + this::initTLS, + this::initCoil ) } @@ -144,8 +146,7 @@ object AppStartup { this::createBookmarksJson, this::createPlugReceiver, this::activateTextIntent, - this::checkAchievements, - this::initCoil + this::checkAchievements ) } @@ -305,10 +306,11 @@ object AppStartup { ImageLoader.Builder(context) .components { if (SDK_INT >= 28) { - add(AnimatedImageDecoder.Factory()) + add(AnimatedImageDecoder.Factory(false)) } else { - add(GifDecoder.Factory()) + add(GifDecoder.Factory(false)) } + add(AnimatedPngDecoder.Factory()) } .build() } diff --git a/app/src/main/java/me/devsaki/hentoid/util/ContentHelper.kt b/app/src/main/java/me/devsaki/hentoid/util/ContentHelper.kt index 336f4618c..f8503db59 100644 --- a/app/src/main/java/me/devsaki/hentoid/util/ContentHelper.kt +++ b/app/src/main/java/me/devsaki/hentoid/util/ContentHelper.kt @@ -1049,13 +1049,13 @@ fun shareContent( */ fun parseDownloadParams(downloadParamsStr: String?): Map { // Handle empty and {} - if (null == downloadParamsStr || downloadParamsStr.trim().length <= 2) return HashMap() + if (null == downloadParamsStr || downloadParamsStr.trim().length <= 2) return emptyMap() try { return jsonToObject(downloadParamsStr, MAP_STRINGS)!! } catch (e: IOException) { Timber.w(e) } - return HashMap() + return emptyMap() } @@ -1746,9 +1746,34 @@ fun bindOnlineCover( } return null } - */ +fun getContentHeaders(content: Content?): List> { + if (getWebViewAvailable()) { + var cookieStr: String? = null + var referer: String? = null + + // Quickly skip JSON deserialization if there are no cookies in downloadParams + if (content != null) { + val downloadParamsStr = content.downloadParams + if (downloadParamsStr.contains(HEADER_COOKIE_KEY)) { + val downloadParams = parseDownloadParams(downloadParamsStr) + cookieStr = downloadParams[HEADER_COOKIE_KEY] + referer = downloadParams[HEADER_REFERER_KEY] + } + if (null == cookieStr) cookieStr = getCookies(content.galleryUrl) + if (null == referer) referer = content.galleryUrl + + val results: MutableList> = ArrayList() + results.add(Pair(HEADER_COOKIE_KEY, cookieStr)) + results.add(Pair(HEADER_REFERER_KEY, referer)) + results.add(Pair(HEADER_USER_AGENT, content.site.userAgent)) + return results + } + } + return emptyList() +} + /** * Find the best match for the given Content inside the library and queue * diff --git a/app/src/main/java/me/devsaki/hentoid/util/image/CoilHelper.kt b/app/src/main/java/me/devsaki/hentoid/util/image/CoilHelper.kt index 61f5e10f7..d34b3d0c1 100644 --- a/app/src/main/java/me/devsaki/hentoid/util/image/CoilHelper.kt +++ b/app/src/main/java/me/devsaki/hentoid/util/image/CoilHelper.kt @@ -1,10 +1,81 @@ package me.devsaki.hentoid.util.image import android.content.Context +import android.view.View +import android.widget.ImageView +import coil3.ImageLoader +import coil3.SingletonImageLoader +import coil3.asImage +import coil3.decode.DecodeResult +import coil3.decode.Decoder +import coil3.decode.ImageSource +import coil3.fetch.SourceFetchResult import coil3.imageLoader +import coil3.network.NetworkHeaders +import coil3.network.httpHeaders +import coil3.request.ImageRequest +import coil3.request.Options +import coil3.request.target +import com.github.penfeizhou.animation.apng.APNGDrawable +import com.github.penfeizhou.animation.apng.decode.APNGParser +import me.devsaki.hentoid.database.domains.Content +import me.devsaki.hentoid.util.getContentHeaders + fun clearCoilCache(context: Context, memory: Boolean = true, file: Boolean = true) { val imageLoader = context.imageLoader if (memory) imageLoader.memoryCache?.clear() if (file) imageLoader.diskCache?.clear() +} + +fun ImageView.loadCover(content: Content) { + // TODO If animated, only load frame zero as a plain bitmap + val thumbLocation = content.cover.usableUri + if (thumbLocation.isEmpty()) { + this.visibility = View.INVISIBLE + return + } + this.visibility = View.VISIBLE + + // Use content's cookies to load image (useful for ExHentai when viewing queue screen) + val networkHeaders = if (thumbLocation.startsWith("http")) { + val headers = NetworkHeaders.Builder() + getContentHeaders(content).forEach { + headers.add(it.first, it.second) + } + headers.build() + } else { + NetworkHeaders.EMPTY + } + + val request = ImageRequest.Builder(context) + .data(thumbLocation) + .target(this) + .httpHeaders(networkHeaders) + + SingletonImageLoader.get(this.context).enqueue(request.build()) +} + +class AnimatedPngDecoder(private val source: ImageSource) : Decoder { + + override suspend fun decode(): DecodeResult { + return DecodeResult( + image = APNGDrawable.fromFile(source.file().toString()).asImage(), + isSampled = false + ) + } + + class Factory : Decoder.Factory { + override fun create( + result: SourceFetchResult, + options: Options, + imageLoader: ImageLoader + ): Decoder? { + return if (APNGParser.isAPNG(result.source.file().toString())) { + AnimatedPngDecoder(result.source) + } else { + null + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/me/devsaki/hentoid/viewholders/ContentItem.kt b/app/src/main/java/me/devsaki/hentoid/viewholders/ContentItem.kt index 07a5e5574..6f21ed445 100644 --- a/app/src/main/java/me/devsaki/hentoid/viewholders/ContentItem.kt +++ b/app/src/main/java/me/devsaki/hentoid/viewholders/ContentItem.kt @@ -15,6 +15,7 @@ import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView +import coil3.dispose import coil3.load import com.google.android.material.progressindicator.CircularProgressIndicator import com.mikepenz.fastadapter.FastAdapter @@ -50,6 +51,7 @@ import me.devsaki.hentoid.util.generateIdForPlaceholder import me.devsaki.hentoid.util.getFlagResourceId import me.devsaki.hentoid.util.getRatingResourceId import me.devsaki.hentoid.util.getThemedColor +import me.devsaki.hentoid.util.image.loadCover import me.devsaki.hentoid.util.isInQueue import timber.log.Timber import java.util.Locale @@ -379,15 +381,16 @@ class ContentItem : AbstractItem, } private fun attachContentCover(content: Content) { + ivCover.loadCover(content) + /* val thumbLocation = content.cover.usableUri if (thumbLocation.isEmpty()) { ivCover.visibility = View.INVISIBLE return } ivCover.visibility = View.VISIBLE + // Use content's cookies to load image (useful for ExHentai when viewing queue screen) - ivCover.load(thumbLocation) - /* if (thumbLocation.startsWith("http")) { val glideUrl = bindOnlineCover(thumbLocation, content) if (glideUrl != null) { @@ -651,6 +654,7 @@ class ContentItem : AbstractItem, deleteActionRunnable = null debugStr = "[no data]" swipeableView.translationX = 0f + ivCover.dispose() //if (isValidContextForGlide(ivCover)) Glide.with(ivCover).clear(ivCover) } diff --git a/app/src/main/java/me/devsaki/hentoid/viewholders/DuplicateItem.kt b/app/src/main/java/me/devsaki/hentoid/viewholders/DuplicateItem.kt index 711903d13..571e921ec 100644 --- a/app/src/main/java/me/devsaki/hentoid/viewholders/DuplicateItem.kt +++ b/app/src/main/java/me/devsaki/hentoid/viewholders/DuplicateItem.kt @@ -3,7 +3,6 @@ package me.devsaki.hentoid.viewholders import android.graphics.BlendMode import android.graphics.BlendModeColorFilter import android.graphics.PorterDuff -import android.net.Uri import android.os.Build import android.os.Bundle import android.view.View @@ -13,7 +12,7 @@ import androidx.annotation.ColorInt import androidx.annotation.DrawableRes import androidx.constraintlayout.widget.Group import androidx.core.content.ContextCompat -import coil3.load +import coil3.dispose import com.google.android.material.materialswitch.MaterialSwitch import com.mikepenz.fastadapter.FastAdapter import com.mikepenz.fastadapter.items.AbstractItem @@ -29,6 +28,7 @@ import me.devsaki.hentoid.util.Preferences import me.devsaki.hentoid.util.formatArtistForDisplay import me.devsaki.hentoid.util.getFlagResourceId import me.devsaki.hentoid.util.getThemedColor +import me.devsaki.hentoid.util.image.loadCover class DuplicateItem(result: DuplicateEntry, private val viewType: ViewType) : AbstractItem() { @@ -143,6 +143,8 @@ class DuplicateItem(result: DuplicateEntry, private val viewType: ViewType) : } private fun attachCover(content: Content) { + ivCover?.loadCover(content) + /* ivCover?.let { val thumbLocation = content.cover.usableUri if (thumbLocation.isEmpty()) { @@ -150,9 +152,6 @@ class DuplicateItem(result: DuplicateEntry, private val viewType: ViewType) : return } it.visibility = View.VISIBLE - // Use content's cookies to load image (useful for ExHentai when viewing queue screen) - it.load(thumbLocation) - /* if (thumbLocation.startsWith("http")) { bindOnlineCover(thumbLocation, content)?.let { glideUrl -> Glide.with(it).load(glideUrl).apply(glideRequestOptions).into(it) @@ -161,8 +160,8 @@ class DuplicateItem(result: DuplicateEntry, private val viewType: ViewType) : .load(Uri.parse(thumbLocation)) .apply(glideRequestOptions) .into(it) - */ - } + } + */ } private fun attachFlag(content: Content) { @@ -320,9 +319,12 @@ class DuplicateItem(result: DuplicateEntry, private val viewType: ViewType) : get() = ivSite override fun unbindView(item: DuplicateItem) { - ivCover?.let { - //if (isValidContextForGlide(it)) Glide.with(it).clear(it) - } + ivCover?.dispose() + /* + ivCover?.let { + //if (isValidContextForGlide(it)) Glide.with(it).clear(it) + } + */ } } } \ No newline at end of file diff --git a/app/src/main/java/me/devsaki/hentoid/viewholders/GroupDisplayItem.kt b/app/src/main/java/me/devsaki/hentoid/viewholders/GroupDisplayItem.kt index e2487e89f..f6309f816 100644 --- a/app/src/main/java/me/devsaki/hentoid/viewholders/GroupDisplayItem.kt +++ b/app/src/main/java/me/devsaki/hentoid/viewholders/GroupDisplayItem.kt @@ -1,6 +1,5 @@ package me.devsaki.hentoid.viewholders -import android.net.Uri import android.os.Bundle import android.view.View import android.widget.ImageView @@ -8,6 +7,7 @@ import android.widget.TextView import androidx.core.view.isVisible import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView +import coil3.dispose import coil3.load import com.mikepenz.fastadapter.FastAdapter import com.mikepenz.fastadapter.drag.IExtendedDraggable @@ -179,9 +179,8 @@ class GroupDisplayItem( get() = ivRating override fun unbindView(item: GroupDisplayItem) { - ivCover?.let { - //if (isValidContextForGlide(it)) Glide.with(it).clear(it) - } + ivCover?.dispose() + //if (isValidContextForGlide(it)) Glide.with(it).clear(it) } } } \ No newline at end of file diff --git a/app/src/main/java/me/devsaki/hentoid/viewholders/ImageFileItem.kt b/app/src/main/java/me/devsaki/hentoid/viewholders/ImageFileItem.kt index 272b114f3..7838ae2ec 100644 --- a/app/src/main/java/me/devsaki/hentoid/viewholders/ImageFileItem.kt +++ b/app/src/main/java/me/devsaki/hentoid/viewholders/ImageFileItem.kt @@ -10,6 +10,7 @@ import android.widget.ImageView import android.widget.TextView import androidx.core.content.ContextCompat import androidx.core.view.ViewCompat +import coil3.dispose import coil3.load import com.mikepenz.fastadapter.FastAdapter import com.mikepenz.fastadapter.IExpandable @@ -190,6 +191,7 @@ class ImageFileItem(private val image: ImageFile, private val showChapter: Boole } override fun unbindView(item: ImageFileItem) { + image.dispose() //if (isValidContextForGlide(image)) Glide.with(image).clear(image) } } From 81b6c2ebcf29b31f0da0d0a19164556c946d4cce Mon Sep 17 00:00:00 2001 From: Robb <35880555+robbwatershed@users.noreply.github.com> Date: Sun, 20 Oct 2024 15:53:46 +0200 Subject: [PATCH 08/44] Rename .java to .kt --- .../decoder/{SkiaDecoderHelper.java => SkiaDecoderHelper.kt} | 0 .../decoder/{SkiaImageDecoder.java => SkiaImageDecoder.kt} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/{SkiaDecoderHelper.java => SkiaDecoderHelper.kt} (100%) rename app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/{SkiaImageDecoder.java => SkiaImageDecoder.kt} (100%) diff --git a/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/SkiaDecoderHelper.java b/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/SkiaDecoderHelper.kt similarity index 100% rename from app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/SkiaDecoderHelper.java rename to app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/SkiaDecoderHelper.kt diff --git a/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/SkiaImageDecoder.java b/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/SkiaImageDecoder.kt similarity index 100% rename from app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/SkiaImageDecoder.java rename to app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/SkiaImageDecoder.kt From 58f151fbc7d8dba6cbec00a16da3c3a25a991b00 Mon Sep 17 00:00:00 2001 From: Robb <35880555+robbwatershed@users.noreply.github.com> Date: Sun, 20 Oct 2024 15:53:47 +0200 Subject: [PATCH 09/44] APNG support + more Kotlin in SSIV --- app/customssiv/build.gradle | 1 + .../customssiv/decoder/SkiaDecoderHelper.kt | 72 ++++----- .../customssiv/decoder/SkiaImageDecoder.kt | 145 ++++++++++-------- .../decoder/SkiaImageRegionDecoder.java | 4 +- .../decoder/SkiaPooledImageRegionDecoder.java | 3 +- .../devsaki/hentoid/customssiv/util/Helper.kt | 11 ++ .../hentoid/customssiv/util/ImageHelper.kt | 27 ++-- .../hentoid/adapters/ImagePagerAdapter.kt | 4 +- .../java/me/devsaki/hentoid/util/Helper.kt | 11 ++ .../devsaki/hentoid/util/file/FileHelper.kt | 4 +- .../devsaki/hentoid/util/image/CoilHelper.kt | 37 ++++- .../devsaki/hentoid/util/image/ImageHelper.kt | 30 ++-- 12 files changed, 200 insertions(+), 149 deletions(-) diff --git a/app/customssiv/build.gradle b/app/customssiv/build.gradle index f84d59b12..733c47bd2 100644 --- a/app/customssiv/build.gradle +++ b/app/customssiv/build.gradle @@ -44,6 +44,7 @@ dependencies { implementation 'com.jakewharton.timber:timber:5.0.1' implementation 'androidx.core:core-ktx:1.13.1' + implementation("com.squareup.okio:okio:3.9.1") implementation project(path: ':app:gles-renderer') } diff --git a/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/SkiaDecoderHelper.kt b/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/SkiaDecoderHelper.kt index bfc720dc1..15fec8e75 100644 --- a/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/SkiaDecoderHelper.kt +++ b/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/SkiaDecoderHelper.kt @@ -1,47 +1,35 @@ -package me.devsaki.hentoid.customssiv.decoder; - -import android.content.Context; -import android.content.pm.PackageManager; -import android.content.res.Resources; -import android.net.Uri; -import android.text.TextUtils; - -import androidx.annotation.NonNull; - -import java.util.List; - -public class SkiaDecoderHelper { - - private SkiaDecoderHelper() { - throw new IllegalStateException("Utility class"); +package me.devsaki.hentoid.customssiv.decoder + +import android.content.Context +import android.content.pm.PackageManager +import android.content.res.Resources +import android.net.Uri +import android.text.TextUtils + +@Throws(PackageManager.NameNotFoundException::class) +fun getResourceId(context: Context, uri: Uri): Int { + val res: Resources + val packageName = uri.authority + if (context.packageName == packageName) { + res = context.resources + } else { + val pm = context.packageManager + res = pm.getResourcesForApplication(packageName!!) } - - public static int getResourceId(@NonNull final Context context, @NonNull final Uri uri) throws PackageManager.NameNotFoundException { - Resources res; - String packageName = uri.getAuthority(); - if (context.getPackageName().equals(packageName)) { - res = context.getResources(); - } else { - PackageManager pm = context.getPackageManager(); - res = pm.getResourcesForApplication(packageName); - } - - int result = 0; - List segments = uri.getPathSegments(); - int size = segments.size(); - if (size == 2 && segments.get(0).equals("drawable")) { - String resName = segments.get(1); - result = res.getIdentifier(resName, "drawable", packageName); - } else if (size == 1 && TextUtils.isDigitsOnly(segments.get(0))) { - try { - result = Integer.parseInt(segments.get(0)); - } catch (NumberFormatException ignored) { - // Ignored exception - } + var result = 0 + val segments = uri.pathSegments + val size = segments.size + if (size == 2 && segments[0] == "drawable") { + val resName = segments[1] + result = res.getIdentifier(resName, "drawable", packageName) + } else if (size == 1 && TextUtils.isDigitsOnly(segments[0])) { + try { + result = segments[0].toInt() + } catch (ignored: NumberFormatException) { + // Ignored exception } - - return result; } -} + return result +} \ No newline at end of file diff --git a/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/SkiaImageDecoder.kt b/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/SkiaImageDecoder.kt index 5f166cd29..a54989c9a 100644 --- a/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/SkiaImageDecoder.kt +++ b/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/SkiaImageDecoder.kt @@ -1,24 +1,21 @@ -package me.devsaki.hentoid.customssiv.decoder; +package me.devsaki.hentoid.customssiv.decoder + +import android.content.ContentResolver +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.ColorSpace +import android.net.Uri +import me.devsaki.hentoid.customssiv.exception.UnsupportedContentException +import me.devsaki.hentoid.customssiv.util.copy +import me.devsaki.hentoid.customssiv.util.isImageAnimated +import timber.log.Timber +import java.io.IOException +import java.io.InputStream +import java.nio.ByteBuffer +import java.nio.MappedByteBuffer +import kotlin.math.min -import static me.devsaki.hentoid.customssiv.util.HelperKt.copy; -import static me.devsaki.hentoid.customssiv.util.ImageHelperKt.isImageAnimated; - -import android.content.ContentResolver; -import android.content.Context; -import android.content.pm.PackageManager; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.ColorSpace; -import android.net.Uri; - -import androidx.annotation.NonNull; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; - -import me.devsaki.hentoid.customssiv.exception.UnsupportedContentException; /** * Default implementation of {@link ImageDecoder} @@ -26,71 +23,83 @@ import me.devsaki.hentoid.customssiv.exception.UnsupportedContentException; * works well in most circumstances and has reasonable performance, however it has some problems * with grayscale, indexed and CMYK images. */ -public class SkiaImageDecoder implements ImageDecoder { - private static final String FILE_PREFIX = "file://"; - private static final String ASSET_PREFIX = FILE_PREFIX + "/android_asset/"; - private static final String RESOURCE_PREFIX = ContentResolver.SCHEME_ANDROID_RESOURCE + "://"; +private const val FILE_PREFIX = "file://" +private const val ASSET_PREFIX = "$FILE_PREFIX/android_asset/" +private const val RESOURCE_PREFIX = ContentResolver.SCHEME_ANDROID_RESOURCE + "://" - private final Bitmap.Config bitmapConfig; +class ByteBufferBackedInputStream(private var buf: ByteBuffer) : InputStream() { + @Throws(IOException::class) + override fun read(): Int { + if (!buf.hasRemaining()) { + return -1 + } + return buf.get().toInt() and 0xFF + } + + @Throws(IOException::class) + override fun read(bytes: ByteArray, off: Int, len: Int): Int { + if (!buf.hasRemaining()) return -1 + val theLen = min(len, buf.remaining()) + buf[bytes, off, theLen] + return theLen + } - public SkiaImageDecoder(@NonNull Bitmap.Config bitmapConfig) { - this.bitmapConfig = bitmapConfig; + override fun close() { + buf.clear() + super.close() } +} + +class SkiaImageDecoder(private val bitmapConfig: Bitmap.Config) : ImageDecoder { + override fun decode(context: Context, uri: Uri): Bitmap { + val uriString = uri.toString() + val options = BitmapFactory.Options() + var bitmap: Bitmap? = null + options.inPreferredConfig = bitmapConfig - @Override - @NonNull - public Bitmap decode(@NonNull final Context context, @NonNull final Uri uri) throws IOException, PackageManager.NameNotFoundException, UnsupportedContentException { - String uriString = uri.toString(); - BitmapFactory.Options options = new BitmapFactory.Options(); - Bitmap bitmap = null; - options.inPreferredConfig = bitmapConfig; // If that is not set, some PNGs are read with a ColorSpace of code "Unknown" (-1), // which makes resizing buggy (generates a black picture) - options.inPreferredColorSpace = ColorSpace.get(ColorSpace.Named.SRGB); + options.inPreferredColorSpace = ColorSpace.get(ColorSpace.Named.SRGB) if (uriString.startsWith(RESOURCE_PREFIX)) { - int id = SkiaDecoderHelper.getResourceId(context, uri); - bitmap = BitmapFactory.decodeResource(context.getResources(), id, options); + val id = getResourceId(context, uri) + bitmap = BitmapFactory.decodeResource(context.resources, id, options) } else if (uriString.startsWith(ASSET_PREFIX)) { - String assetName = uriString.substring(ASSET_PREFIX.length()); - bitmap = BitmapFactory.decodeStream(context.getAssets().open(assetName), null, options); + val assetName = uriString.substring(ASSET_PREFIX.length) + bitmap = BitmapFactory.decodeStream(context.assets.open(assetName), null, options) } else { - InputStream fileStream = null; - try (InputStream input = context.getContentResolver().openInputStream(uri)) { - if (input == null) - throw new RuntimeException("Content resolver returned null stream. Unable to initialise with uri."); - - // First examine header - byte[] header = new byte[400]; - if (input.read(header) > 0) { - if (isImageAnimated(header)) - throw new UnsupportedContentException("SSIV doesn't handle animated pictures"); - - // If it passes, load the whole picture - try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { - baos.write(header, 0, 400); - - copy(input, baos); - - fileStream = new ByteArrayInputStream(baos.toByteArray()); + var fileStream: InputStream? = null + var size = 0 + context.contentResolver.openFileDescriptor(uri, "r")?.use { + size = it.statSize.toInt() + } + context.contentResolver.openInputStream(uri)?.use { input -> + if (size > 0) { + // First examine header + val header = ByteArray(400) + if (input.read(header) > 0) { + if (isImageAnimated(header)) + throw UnsupportedContentException("SSIV doesn't handle animated pictures") + val bb = MappedByteBuffer.allocate(size) + bb.put(header) + copy(input, bb) + fileStream = ByteBufferBackedInputStream(bb.asReadOnlyBuffer()) } + } else { + Timber.e("Size is zero!") } } - - if (fileStream != null) { - try { - bitmap = BitmapFactory.decodeStream(fileStream, null, options); - } finally { - fileStream.close(); - } + ?: throw RuntimeException("Content resolver returned null stream. Unable to initialise with uri.") + fileStream?.use { + bitmap = BitmapFactory.decodeStream(fileStream, null, options) } } - if (bitmap == null) { - throw new RuntimeException("Skia image region decoder returned null bitmap - image format may not be supported"); + bitmap?.let { + return it + } ?: run { + throw RuntimeException("Skia image region decoder returned null bitmap - image format may not be supported") } - - return bitmap; } } \ No newline at end of file diff --git a/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/SkiaImageRegionDecoder.java b/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/SkiaImageRegionDecoder.java index 727e79416..af2159d1b 100644 --- a/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/SkiaImageRegionDecoder.java +++ b/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/SkiaImageRegionDecoder.java @@ -1,5 +1,7 @@ package me.devsaki.hentoid.customssiv.decoder; +import static me.devsaki.hentoid.customssiv.decoder.SkiaDecoderHelperKt.getResourceId; + import android.content.ContentResolver; import android.content.Context; import android.content.pm.PackageManager; @@ -52,7 +54,7 @@ public SkiaImageRegionDecoder(@NonNull Bitmap.Config bitmapConfig) { public Point init(Context context, @NonNull Uri uri) throws IOException, PackageManager.NameNotFoundException { String uriString = uri.toString(); if (uriString.startsWith(RESOURCE_PREFIX)) { - int id = SkiaDecoderHelper.getResourceId(context, uri); + int id = getResourceId(context, uri); decoder = BitmapRegionDecoder.newInstance(context.getResources().openRawResource(id), false); } else if (uriString.startsWith(ASSET_PREFIX)) { String assetName = uriString.substring(ASSET_PREFIX.length()); diff --git a/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/SkiaPooledImageRegionDecoder.java b/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/SkiaPooledImageRegionDecoder.java index 0ea4b7e0d..ac1171374 100644 --- a/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/SkiaPooledImageRegionDecoder.java +++ b/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/SkiaPooledImageRegionDecoder.java @@ -1,6 +1,7 @@ package me.devsaki.hentoid.customssiv.decoder; import static android.content.Context.ACTIVITY_SERVICE; +import static me.devsaki.hentoid.customssiv.decoder.SkiaDecoderHelperKt.getResourceId; import android.app.ActivityManager; import android.content.ContentResolver; @@ -143,7 +144,7 @@ private void initialiseDecoder() throws IOException, PackageManager.NameNotFound BitmapRegionDecoder decoder; long localFileLength = Long.MAX_VALUE; if (uriString.startsWith(RESOURCE_PREFIX)) { - int id = SkiaDecoderHelper.getResourceId(context, uri); + int id = getResourceId(context, uri); try (AssetFileDescriptor descriptor = context.getResources().openRawResourceFd(id)) { localFileLength = descriptor.getLength(); } catch (Exception e) { diff --git a/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/util/Helper.kt b/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/util/Helper.kt index 9ebfb6ec8..f6c66a48f 100644 --- a/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/util/Helper.kt +++ b/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/util/Helper.kt @@ -7,6 +7,7 @@ import android.view.WindowManager import java.io.IOException import java.io.InputStream import java.io.OutputStream +import java.nio.ByteBuffer import kotlin.math.abs @@ -36,6 +37,16 @@ fun copy(`in`: InputStream, out: OutputStream) { out.flush() } +@Throws(IOException::class) +fun copy(`in`: InputStream, out: ByteBuffer) { + // Transfer bytes from in to out + val buf = ByteArray(FILE_IO_BUFFER_SIZE) + var len: Int + while ((`in`.read(buf).also { len = it }) > 0) { + out.put(buf, 0, len) + } +} + fun getScreenDpi(context: Context): Float { val metrics = context.resources.displayMetrics val averageDpi = (metrics.xdpi + metrics.ydpi) / 2 diff --git a/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/util/ImageHelper.kt b/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/util/ImageHelper.kt index 38dfde330..fc67a2610 100644 --- a/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/util/ImageHelper.kt +++ b/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/util/ImageHelper.kt @@ -41,36 +41,37 @@ fun ByteArray.startsWith(data: ByteArray): Boolean { /** * Determine the MIME-type of the given binary data if it's a picture * - * @param binary Picture binary data to determine the MIME-type for + * @param data Picture binary data to determine the MIME-type for * @return MIME-type of the given binary data; empty string if not supported */ -fun getMimeTypeFromPictureBinary(binary: ByteArray): String { - if (binary.size < 12) return "" +fun getMimeTypeFromPictureBinary(data: ByteArray, limit: Int = -1): String { + if (data.size < 12) return "" + val theLimit = if (-1 == limit) min(data.size * 0.2f, 1000f).toInt() else limit - return if (binary.startsWith(JPEG_SIGNATURE)) MIME_IMAGE_JPEG + return if (data.startsWith(JPEG_SIGNATURE)) MIME_IMAGE_JPEG // WEBP : byte comparison is non-contiguous - else if (binary.startsWith(WEBP_SIGNATURE) && 0x57.toByte() == binary[8] && 0x45.toByte() == binary[9] && 0x42.toByte() == binary[10] && 0x50.toByte() == binary[11] + else if (data.startsWith(WEBP_SIGNATURE) && 0x57.toByte() == data[8] && 0x45.toByte() == data[9] && 0x42.toByte() == data[10] && 0x50.toByte() == data[11] ) MIME_IMAGE_WEBP - else if (binary.startsWith(PNG_SIGNATURE)) { + else if (data.startsWith(PNG_SIGNATURE)) { // Detect animated PNG : To be recognized as APNG an 'acTL' chunk must appear in the stream before any 'IDAT' chunks val acTlPos = findSequencePosition( - binary, + data, 0, PNG_ACTL, - (binary.size * 0.2).toInt() + theLimit ) if (acTlPos > -1) { val idatPos = findSequencePosition( - binary, + data, acTlPos, PNG_IDAT, - (binary.size * 0.1).toInt() + theLimit ).toLong() if (idatPos > -1) return MIME_IMAGE_APNG } MIME_IMAGE_PNG - } else if (binary.startsWith(GIF_SIGNATURE)) MIME_IMAGE_GIF - else if (binary.startsWith(BMP_SIGNATURE)) MIME_IMAGE_BMP + } else if (data.startsWith(GIF_SIGNATURE)) MIME_IMAGE_GIF + else if (data.startsWith(BMP_SIGNATURE)) MIME_IMAGE_BMP else MIME_IMAGE_GENERIC } @@ -85,7 +86,7 @@ fun isImageAnimated(data: ByteArray): Boolean { return if (data.size < 400) false else { val limit = min(data.size, 1000) - when (getMimeTypeFromPictureBinary(data)) { + when (getMimeTypeFromPictureBinary(data, limit)) { MIME_IMAGE_APNG -> true MIME_IMAGE_GIF -> findSequencePosition( data, diff --git a/app/src/main/java/me/devsaki/hentoid/adapters/ImagePagerAdapter.kt b/app/src/main/java/me/devsaki/hentoid/adapters/ImagePagerAdapter.kt index 64c13d365..500709f60 100644 --- a/app/src/main/java/me/devsaki/hentoid/adapters/ImagePagerAdapter.kt +++ b/app/src/main/java/me/devsaki/hentoid/adapters/ImagePagerAdapter.kt @@ -588,11 +588,11 @@ class ImagePagerAdapter(val context: Context) : override fun onImageLoadError(e: Throwable) { Timber.d( e, - "Picture %d : SSIV loading failed; reloading with Glide : %s", + "Picture %d : SSIV loading failed; reloading on ImageVIew : %s", absoluteAdapterPosition, img!!.fileUri ) - // Fall back to Glide + // Fall back to ImageView forceImageView() // Reload adapter notifyItemChanged(layoutPosition) diff --git a/app/src/main/java/me/devsaki/hentoid/util/Helper.kt b/app/src/main/java/me/devsaki/hentoid/util/Helper.kt index cf0dad6a9..a5cbc530e 100644 --- a/app/src/main/java/me/devsaki/hentoid/util/Helper.kt +++ b/app/src/main/java/me/devsaki/hentoid/util/Helper.kt @@ -41,6 +41,7 @@ import java.io.InputStream import java.io.OutputStream import java.io.PrintWriter import java.io.StringWriter +import java.nio.ByteBuffer import java.time.Instant import java.time.ZoneId import java.time.format.DateTimeFormatter @@ -243,6 +244,16 @@ fun copy(`in`: InputStream, out: OutputStream) { out.flush() } +@Throws(IOException::class) +fun copy(`in`: InputStream, out: ByteBuffer) { + // Transfer bytes from in to out + val buf = ByteArray(FILE_IO_BUFFER_SIZE) + var len: Int + while ((`in`.read(buf).also { len = it }) > 0) { + out.put(buf, 0, len) + } +} + /** * Generate an ID for a RecyclerView ViewHolder without any ID to assign to * diff --git a/app/src/main/java/me/devsaki/hentoid/util/file/FileHelper.kt b/app/src/main/java/me/devsaki/hentoid/util/file/FileHelper.kt index 03dc167b8..4c03098c3 100644 --- a/app/src/main/java/me/devsaki/hentoid/util/file/FileHelper.kt +++ b/app/src/main/java/me/devsaki/hentoid/util/file/FileHelper.kt @@ -683,14 +683,14 @@ fun getFileNameWithoutExtension(filePath: String): String { * @throws IOException In case something horrible happens during I/O */ @Throws(IOException::class) -fun saveBinary(context: Context, uri: Uri, binaryData: ByteArray?) { +fun saveBinary(context: Context, uri: Uri, binaryData: ByteArray) { getOutputStream(context, uri)?.let { saveBinary(it, binaryData) } } @Throws(IOException::class) -fun saveBinary(out: OutputStream, binaryData: ByteArray?) { +fun saveBinary(out: OutputStream, binaryData: ByteArray) { val buffer = ByteArray(FILE_IO_BUFFER_SIZE) var count: Int ByteArrayInputStream(binaryData).use { input -> diff --git a/app/src/main/java/me/devsaki/hentoid/util/image/CoilHelper.kt b/app/src/main/java/me/devsaki/hentoid/util/image/CoilHelper.kt index d34b3d0c1..aeb6f7924 100644 --- a/app/src/main/java/me/devsaki/hentoid/util/image/CoilHelper.kt +++ b/app/src/main/java/me/devsaki/hentoid/util/image/CoilHelper.kt @@ -17,9 +17,13 @@ import coil3.request.ImageRequest import coil3.request.Options import coil3.request.target import com.github.penfeizhou.animation.apng.APNGDrawable -import com.github.penfeizhou.animation.apng.decode.APNGParser +import com.github.penfeizhou.animation.io.ByteBufferReader import me.devsaki.hentoid.database.domains.Content import me.devsaki.hentoid.util.getContentHeaders +import okio.BufferedSource +import timber.log.Timber +import java.io.InputStream +import java.nio.ByteBuffer fun clearCoilCache(context: Context, memory: Boolean = true, file: Boolean = true) { @@ -59,19 +63,42 @@ fun ImageView.loadCover(content: Content) { class AnimatedPngDecoder(private val source: ImageSource) : Decoder { override suspend fun decode(): DecodeResult { + // We must buffer the source into memory as APNGDrawable decodes + // the image lazily at draw time which is prohibited by Coil. + val buffer = source.source().squashToDirectByteBuffer() return DecodeResult( - image = APNGDrawable.fromFile(source.file().toString()).asImage(), - isSampled = false + image = APNGDrawable { ByteBufferReader(buffer) }.asImage(), + isSampled = false, ) } + private fun BufferedSource.squashToDirectByteBuffer(): ByteBuffer { + // Squash bytes to BufferedSource inner buffer then we know total byteCount. + request(Long.MAX_VALUE) + + val byteBuffer = ByteBuffer.allocateDirect(buffer.size.toInt()) + while (!buffer.exhausted()) buffer.read(byteBuffer) + byteBuffer.flip() + return byteBuffer + } + class Factory : Decoder.Factory { + + private fun isApng(input: InputStream): Boolean { + val data = ByteArray(1000) + input.read(data, 0, 1000) + return getMimeTypeFromPictureBinary(data, 1000) == MIME_IMAGE_APNG + } + override fun create( result: SourceFetchResult, options: Options, - imageLoader: ImageLoader + imageLoader: ImageLoader, ): Decoder? { - return if (APNGParser.isAPNG(result.source.file().toString())) { + Timber.i("what do we have here?") + val stream = result.source.source().peek().inputStream() + return if (isApng(stream)) { + Timber.i("we got an APNG") AnimatedPngDecoder(result.source) } else { null diff --git a/app/src/main/java/me/devsaki/hentoid/util/image/ImageHelper.kt b/app/src/main/java/me/devsaki/hentoid/util/image/ImageHelper.kt index c9f875926..1fe6db2b8 100644 --- a/app/src/main/java/me/devsaki/hentoid/util/image/ImageHelper.kt +++ b/app/src/main/java/me/devsaki/hentoid/util/image/ImageHelper.kt @@ -29,7 +29,6 @@ import java.io.IOException import java.io.InputStream import java.nio.charset.StandardCharsets import kotlin.math.abs -import kotlin.math.max import kotlin.math.min import kotlin.math.pow @@ -41,7 +40,7 @@ const val MIME_IMAGE_JPEG = "image/jpeg" const val MIME_IMAGE_GIF = "image/gif" private const val MIME_IMAGE_BMP = "image/bmp" const val MIME_IMAGE_PNG = "image/png" -private const val MIME_IMAGE_APNG = "image/apng" +const val MIME_IMAGE_APNG = "image/apng" // In Java and Kotlin, byte type is signed ! // => Converting all raw values to byte to be sure they are evaluated as expected @@ -106,36 +105,37 @@ fun ByteArray.startsWith(data: ByteArray): Boolean { /** * Determine the MIME-type of the given binary data if it's a picture * - * @param binary Picture binary data to determine the MIME-type for + * @param data Picture binary data to determine the MIME-type for * @return MIME-type of the given binary data; empty string if not supported */ -fun getMimeTypeFromPictureBinary(binary: ByteArray): String { - if (binary.size < 12) return "" +fun getMimeTypeFromPictureBinary(data: ByteArray, limit: Int = -1): String { + if (data.size < 12) return "" + val theLimit = if (-1 == limit) min(data.size * 0.2f, 1000f).toInt() else limit - return if (binary.startsWith(JPEG_SIGNATURE)) MIME_IMAGE_JPEG + return if (data.startsWith(JPEG_SIGNATURE)) MIME_IMAGE_JPEG // WEBP : byte comparison is non-contiguous - else if (binary.startsWith(WEBP_SIGNATURE) && 0x57.toByte() == binary[8] && 0x45.toByte() == binary[9] && 0x42.toByte() == binary[10] && 0x50.toByte() == binary[11] + else if (data.startsWith(WEBP_SIGNATURE) && 0x57.toByte() == data[8] && 0x45.toByte() == data[9] && 0x42.toByte() == data[10] && 0x50.toByte() == data[11] ) MIME_IMAGE_WEBP - else if (binary.startsWith(PNG_SIGNATURE)) { + else if (data.startsWith(PNG_SIGNATURE)) { // Detect animated PNG : To be recognized as APNG an 'acTL' chunk must appear in the stream before any 'IDAT' chunks val acTlPos = findSequencePosition( - binary, + data, 0, PNG_ACTL, - (binary.size * 0.2).toInt() + theLimit ) if (acTlPos > -1) { val idatPos = findSequencePosition( - binary, + data, acTlPos, PNG_IDAT, - (binary.size * 0.1).toInt() + theLimit ).toLong() if (idatPos > -1) return MIME_IMAGE_APNG } MIME_IMAGE_PNG - } else if (binary.startsWith(GIF_SIGNATURE)) MIME_IMAGE_GIF - else if (binary.startsWith(BMP_SIGNATURE)) MIME_IMAGE_BMP + } else if (data.startsWith(GIF_SIGNATURE)) MIME_IMAGE_GIF + else if (data.startsWith(BMP_SIGNATURE)) MIME_IMAGE_BMP else MIME_IMAGE_GENERIC } @@ -150,7 +150,7 @@ fun isImageAnimated(data: ByteArray): Boolean { return if (data.size < 400) false else { val limit = min(data.size, 1000) - when (getMimeTypeFromPictureBinary(data)) { + when (getMimeTypeFromPictureBinary(data, limit)) { MIME_IMAGE_APNG -> true MIME_IMAGE_GIF -> findSequencePosition( data, From 57d1077c438b00359c74f0a925319ae1096eae09 Mon Sep 17 00:00:00 2001 From: Robb <35880555+robbwatershed@users.noreply.github.com> Date: Mon, 21 Oct 2024 13:22:57 +0200 Subject: [PATCH 10/44] Remove useless import --- app/customssiv/build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/app/customssiv/build.gradle b/app/customssiv/build.gradle index 733c47bd2..f84d59b12 100644 --- a/app/customssiv/build.gradle +++ b/app/customssiv/build.gradle @@ -44,7 +44,6 @@ dependencies { implementation 'com.jakewharton.timber:timber:5.0.1' implementation 'androidx.core:core-ktx:1.13.1' - implementation("com.squareup.okio:okio:3.9.1") implementation project(path: ':app:gles-renderer') } From 4a4a0982578286b1109865c644a6c2f5b8bd00e3 Mon Sep 17 00:00:00 2001 From: Robb <35880555+robbwatershed@users.noreply.github.com> Date: Mon, 21 Oct 2024 13:30:51 +0200 Subject: [PATCH 11/44] Load covers and gallery items as still images --- .../hentoid/adapters/ImagePagerAdapter.kt | 2 +- .../devsaki/hentoid/util/image/CoilHelper.kt | 25 +++++++++--- .../hentoid/viewholders/ContentItem.kt | 38 ++----------------- .../hentoid/viewholders/DuplicateItem.kt | 25 +----------- .../hentoid/viewholders/ImageFileItem.kt | 8 +--- 5 files changed, 26 insertions(+), 72 deletions(-) diff --git a/app/src/main/java/me/devsaki/hentoid/adapters/ImagePagerAdapter.kt b/app/src/main/java/me/devsaki/hentoid/adapters/ImagePagerAdapter.kt index 500709f60..1d373debb 100644 --- a/app/src/main/java/me/devsaki/hentoid/adapters/ImagePagerAdapter.kt +++ b/app/src/main/java/me/devsaki/hentoid/adapters/ImagePagerAdapter.kt @@ -462,7 +462,7 @@ class ImagePagerAdapter(val context: Context) : } // ImageView - // TODO doesn't work for Glide as it doesn't use ImageView's scaling + // TODO doesn't work for Coil as it doesn't use ImageView's scaling var absoluteScale: Float get() { return if (!isImageView) { diff --git a/app/src/main/java/me/devsaki/hentoid/util/image/CoilHelper.kt b/app/src/main/java/me/devsaki/hentoid/util/image/CoilHelper.kt index aeb6f7924..c5c134b5c 100644 --- a/app/src/main/java/me/devsaki/hentoid/util/image/CoilHelper.kt +++ b/app/src/main/java/me/devsaki/hentoid/util/image/CoilHelper.kt @@ -16,15 +16,17 @@ import coil3.network.httpHeaders import coil3.request.ImageRequest import coil3.request.Options import coil3.request.target +import coil3.serviceLoaderEnabled import com.github.penfeizhou.animation.apng.APNGDrawable import com.github.penfeizhou.animation.io.ByteBufferReader +import me.devsaki.hentoid.core.HentoidApp import me.devsaki.hentoid.database.domains.Content import me.devsaki.hentoid.util.getContentHeaders import okio.BufferedSource -import timber.log.Timber import java.io.InputStream import java.nio.ByteBuffer +private val stillImageLoader: ImageLoader by lazy { initStillImageLoader() } fun clearCoilCache(context: Context, memory: Boolean = true, file: Boolean = true) { val imageLoader = context.imageLoader @@ -32,8 +34,19 @@ fun clearCoilCache(context: Context, memory: Boolean = true, file: Boolean = tru if (file) imageLoader.diskCache?.clear() } -fun ImageView.loadCover(content: Content) { - // TODO If animated, only load frame zero as a plain bitmap +private fun initStillImageLoader(): ImageLoader { + return ImageLoader.Builder(HentoidApp.getInstance()).serviceLoaderEnabled(false).build() +} + +fun ImageView.loadStill(data: Any) { + val request = ImageRequest.Builder(context) + .data(data) + .target(this) + + stillImageLoader.enqueue(request.build()) +} + +fun ImageView.loadCover(content: Content, disableAnimation: Boolean = false) { val thumbLocation = content.cover.usableUri if (thumbLocation.isEmpty()) { this.visibility = View.INVISIBLE @@ -57,7 +70,9 @@ fun ImageView.loadCover(content: Content) { .target(this) .httpHeaders(networkHeaders) - SingletonImageLoader.get(this.context).enqueue(request.build()) + val loader = if (disableAnimation) stillImageLoader + else SingletonImageLoader.get(this.context) + loader.enqueue(request.build()) } class AnimatedPngDecoder(private val source: ImageSource) : Decoder { @@ -95,10 +110,8 @@ class AnimatedPngDecoder(private val source: ImageSource) : Decoder { options: Options, imageLoader: ImageLoader, ): Decoder? { - Timber.i("what do we have here?") val stream = result.source.source().peek().inputStream() return if (isApng(stream)) { - Timber.i("we got an APNG") AnimatedPngDecoder(result.source) } else { null diff --git a/app/src/main/java/me/devsaki/hentoid/viewholders/ContentItem.kt b/app/src/main/java/me/devsaki/hentoid/viewholders/ContentItem.kt index 6f21ed445..ee3566626 100644 --- a/app/src/main/java/me/devsaki/hentoid/viewholders/ContentItem.kt +++ b/app/src/main/java/me/devsaki/hentoid/viewholders/ContentItem.kt @@ -16,7 +16,6 @@ import androidx.core.view.isVisible import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView import coil3.dispose -import coil3.load import com.google.android.material.progressindicator.CircularProgressIndicator import com.mikepenz.fastadapter.FastAdapter import com.mikepenz.fastadapter.drag.IExtendedDraggable @@ -52,6 +51,7 @@ import me.devsaki.hentoid.util.getFlagResourceId import me.devsaki.hentoid.util.getRatingResourceId import me.devsaki.hentoid.util.getThemedColor import me.devsaki.hentoid.util.image.loadCover +import me.devsaki.hentoid.util.image.loadStill import me.devsaki.hentoid.util.isInQueue import timber.log.Timber import java.util.Locale @@ -381,45 +381,14 @@ class ContentItem : AbstractItem, } private fun attachContentCover(content: Content) { - ivCover.loadCover(content) - /* - val thumbLocation = content.cover.usableUri - if (thumbLocation.isEmpty()) { - ivCover.visibility = View.INVISIBLE - return - } - ivCover.visibility = View.VISIBLE - - // Use content's cookies to load image (useful for ExHentai when viewing queue screen) - if (thumbLocation.startsWith("http")) { - val glideUrl = bindOnlineCover(thumbLocation, content) - if (glideUrl != null) { - Glide.with(ivCover).load(glideUrl).signature(ObjectKey(content.uniqueHash())) - .apply(glideRequestOptions).into(ivCover) - } - } else // From stored picture - Glide.with(ivCover).load(Uri.parse(thumbLocation)) - .signature(ObjectKey(content.uniqueHash())).apply(glideRequestOptions) - .into(ivCover) - */ + ivCover.loadCover(content, true) } private fun attachChapterCover(chapter: Chapter) { val thumbLocation = chapter.imageList.firstOrNull()?.usableUri if (thumbLocation != null) { - ivCover.load(thumbLocation) + ivCover.loadStill(thumbLocation) ivCover.visibility = View.VISIBLE - /* - val glideRequest = Glide.with(ivCover) - - val builder = if (thumbLocation.startsWith("http")) glideRequest.load( - bindOnlineCover(thumbLocation, null) - ) - else glideRequest.load(Uri.parse(thumbLocation)) - - builder.signature(ObjectKey(chapter.uniqueHash())).apply(glideRequestOptions) - .into(ivCover) - */ } else { ivCover.visibility = View.INVISIBLE } @@ -655,7 +624,6 @@ class ContentItem : AbstractItem, debugStr = "[no data]" swipeableView.translationX = 0f ivCover.dispose() - //if (isValidContextForGlide(ivCover)) Glide.with(ivCover).clear(ivCover) } override fun onDragged() { diff --git a/app/src/main/java/me/devsaki/hentoid/viewholders/DuplicateItem.kt b/app/src/main/java/me/devsaki/hentoid/viewholders/DuplicateItem.kt index 571e921ec..462c27bb2 100644 --- a/app/src/main/java/me/devsaki/hentoid/viewholders/DuplicateItem.kt +++ b/app/src/main/java/me/devsaki/hentoid/viewholders/DuplicateItem.kt @@ -143,25 +143,7 @@ class DuplicateItem(result: DuplicateEntry, private val viewType: ViewType) : } private fun attachCover(content: Content) { - ivCover?.loadCover(content) - /* - ivCover?.let { - val thumbLocation = content.cover.usableUri - if (thumbLocation.isEmpty()) { - it.visibility = View.INVISIBLE - return - } - it.visibility = View.VISIBLE - if (thumbLocation.startsWith("http")) { - bindOnlineCover(thumbLocation, content)?.let { glideUrl -> - Glide.with(it).load(glideUrl).apply(glideRequestOptions).into(it) - } - } else Glide.with(it) - .load(Uri.parse(thumbLocation)) - .apply(glideRequestOptions) - .into(it) - } - */ + ivCover?.loadCover(content, true) } private fun attachFlag(content: Content) { @@ -320,11 +302,6 @@ class DuplicateItem(result: DuplicateEntry, private val viewType: ViewType) : override fun unbindView(item: DuplicateItem) { ivCover?.dispose() - /* - ivCover?.let { - //if (isValidContextForGlide(it)) Glide.with(it).clear(it) - } - */ } } } \ No newline at end of file diff --git a/app/src/main/java/me/devsaki/hentoid/viewholders/ImageFileItem.kt b/app/src/main/java/me/devsaki/hentoid/viewholders/ImageFileItem.kt index 7838ae2ec..ec4361a22 100644 --- a/app/src/main/java/me/devsaki/hentoid/viewholders/ImageFileItem.kt +++ b/app/src/main/java/me/devsaki/hentoid/viewholders/ImageFileItem.kt @@ -1,9 +1,6 @@ package me.devsaki.hentoid.viewholders import android.graphics.Typeface -import android.graphics.drawable.BitmapDrawable -import android.graphics.drawable.Drawable -import android.net.Uri import android.os.Bundle import android.view.View import android.widget.ImageView @@ -11,7 +8,6 @@ import android.widget.TextView import androidx.core.content.ContextCompat import androidx.core.view.ViewCompat import coil3.dispose -import coil3.load import com.mikepenz.fastadapter.FastAdapter import com.mikepenz.fastadapter.IExpandable import com.mikepenz.fastadapter.IParentItem @@ -21,6 +17,7 @@ import me.devsaki.hentoid.R import me.devsaki.hentoid.activities.bundles.ImageItemBundle import me.devsaki.hentoid.database.domains.Chapter import me.devsaki.hentoid.database.domains.ImageFile +import me.devsaki.hentoid.util.image.loadStill private const val HEART_SYMBOL = "❤" @@ -132,8 +129,7 @@ class ImageFileItem(private val image: ImageFile, private val showChapter: Boole } else chapterOverlay.visibility = View.GONE // Image - // TODO If animated, only load frame zero as a plain bitmap - image.load(item.image.fileUri) + image.loadStill(item.image.fileUri) /* Glide.with(image) .load(Uri.parse(item.image.fileUri)) From 00e643fcf990eace60f05215789fcc51f37a3260 Mon Sep 17 00:00:00 2001 From: Robb <35880555+robbwatershed@users.noreply.github.com> Date: Mon, 21 Oct 2024 13:33:32 +0200 Subject: [PATCH 12/44] Cleanup --- .../activities/MetadataEditActivity.kt | 14 ------- .../hentoid/adapters/ImagePagerAdapter.kt | 2 +- .../ReaderContentBottomSheetFragment.kt | 15 ------- .../reader/ReaderImageBottomSheetFragment.kt | 6 --- .../fragments/reader/ReaderPagerFragment.kt | 13 ------ .../fragments/web/DuplicateDialogFragment.kt | 5 --- .../me/devsaki/hentoid/util/ContentHelper.kt | 40 ------------------ .../hentoid/viewholders/GroupDisplayItem.kt | 17 +------- .../hentoid/viewholders/ImageFileItem.kt | 41 ------------------- .../hentoid/viewholders/SubExpandableItem.kt | 10 ----- .../hentoid/viewmodels/ReaderViewModel.kt | 1 - .../hentoid/workers/TransformWorker.kt | 1 - 12 files changed, 3 insertions(+), 162 deletions(-) diff --git a/app/src/main/java/me/devsaki/hentoid/activities/MetadataEditActivity.kt b/app/src/main/java/me/devsaki/hentoid/activities/MetadataEditActivity.kt index af615e82b..1eb13578e 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/MetadataEditActivity.kt +++ b/app/src/main/java/me/devsaki/hentoid/activities/MetadataEditActivity.kt @@ -205,22 +205,8 @@ class MetadataEditActivity : BaseActivity(), GalleryPickerDialogFragment.Parent, it.ivCover.visibility = View.VISIBLE if (thumbLocation.startsWith("http")) { it.ivCover.load(thumbLocation) -/* - bindOnlineCover(thumbLocation, contents[0])?.let { glideUrl -> - Glide.with(it.ivCover) - .load(glideUrl) - .apply(glideOptionCenterInside) - .into(it.ivCover) - } - */ } else // From stored picture it.ivCover.load(thumbLocation) - /* - Glide.with(it.ivCover) - .load(Uri.parse(thumbLocation)) - .apply(glideOptionCenterInside) - .into(it.ivCover) - */ } // Flag (language) diff --git a/app/src/main/java/me/devsaki/hentoid/adapters/ImagePagerAdapter.kt b/app/src/main/java/me/devsaki/hentoid/adapters/ImagePagerAdapter.kt index 1d373debb..b5782e298 100644 --- a/app/src/main/java/me/devsaki/hentoid/adapters/ImagePagerAdapter.kt +++ b/app/src/main/java/me/devsaki/hentoid/adapters/ImagePagerAdapter.kt @@ -63,7 +63,7 @@ class ImagePagerAdapter(val context: Context) : ListAdapter(IMAGE_DIFF_CALLBACK) { enum class ImageType(val value: Int) { - IMG_TYPE_OTHER(0), // PNGs, JPEGs and WEBPs -> use CustomSubsamplingScaleImageView; will fallback to Glide if animation detected + IMG_TYPE_OTHER(0), // PNGs, JPEGs and WEBPs -> use CustomSubsamplingScaleImageView; will fallback to Coil if animation detected IMG_TYPE_GIF(1), // Static and animated GIFs -> use APNG4Android library IMG_TYPE_APNG(2), // Animated PNGs -> use APNG4Android library IMG_TYPE_AWEBP(3) // Animated WEBPs -> use APNG4Android library diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/reader/ReaderContentBottomSheetFragment.kt b/app/src/main/java/me/devsaki/hentoid/fragments/reader/ReaderContentBottomSheetFragment.kt index 1126ff6d6..48f1e645c 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/reader/ReaderContentBottomSheetFragment.kt +++ b/app/src/main/java/me/devsaki/hentoid/fragments/reader/ReaderContentBottomSheetFragment.kt @@ -117,21 +117,6 @@ class ReaderContentBottomSheetFragment : BottomSheetDialogFragment() { } else { ivCover.visibility = View.VISIBLE ivCover.load(thumbLocation) - /* - if (thumbLocation.startsWith("http")) { - val glideUrl = bindOnlineCover(thumbLocation, content) - if (glideUrl != null) { - Glide.with(ivCover) - .load(glideUrl) - .apply(glideRequestOptions) - .into(ivCover) - } - } else Glide.with(ivCover) - .load(Uri.parse(thumbLocation)) - .apply(glideRequestOptions) - .into(ivCover) - - */ } if (openOnTap) ivCover.setOnClickListener { openReader( diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/reader/ReaderImageBottomSheetFragment.kt b/app/src/main/java/me/devsaki/hentoid/fragments/reader/ReaderImageBottomSheetFragment.kt index 59d22614b..485cc9f16 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/reader/ReaderImageBottomSheetFragment.kt +++ b/app/src/main/java/me/devsaki/hentoid/fragments/reader/ReaderImageBottomSheetFragment.kt @@ -146,12 +146,6 @@ class ReaderImageBottomSheetFragment : BottomSheetDialogFragment() { sizeStr ) binding.ivThumb.load(it.fileUri) - /* - Glide.with(binding.ivThumb) - .load(Uri.parse(it.fileUri)) - .apply(glideRequestOptions) - .into(binding.ivThumb) - */ } else { binding.imageStats.setText(R.string.image_not_found) binding.imgActionFavourite.imageTintList = ColorStateList.valueOf(grayColor) diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/reader/ReaderPagerFragment.kt b/app/src/main/java/me/devsaki/hentoid/fragments/reader/ReaderPagerFragment.kt index a3298a757..06523ddd8 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/reader/ReaderPagerFragment.kt +++ b/app/src/main/java/me/devsaki/hentoid/fragments/reader/ReaderPagerFragment.kt @@ -1259,24 +1259,11 @@ class ReaderPagerFragment : Fragment(R.layout.fragment_reader_pager), val nextImg = adapter.getImageAt(absIndex + 1) if (previousImg != null) { previousImageView.load(previousImg.fileUri) - /* - Glide.with(previousImageView).load(Uri.parse(previousImg.fileUri)) - .apply(glideOptionCenterInside).into(previousImageView) - */ previousImageView.visibility = View.VISIBLE } else previousImageView.visibility = View.INVISIBLE if (currentImg != null) controlsOverlay.imagePreviewCenter.load(currentImg.fileUri) - /* - Glide.with(controlsOverlay.imagePreviewCenter) - .load(Uri.parse(currentImg.fileUri)).apply(glideOptionCenterInside) - .into(controlsOverlay.imagePreviewCenter) - */ if (nextImg != null) { nextImageView.load(nextImg.fileUri) - /* - Glide.with(nextImageView).load(Uri.parse(nextImg.fileUri)) - .apply(glideOptionCenterInside).into(nextImageView) - */ nextImageView.visibility = View.VISIBLE } else nextImageView.visibility = View.INVISIBLE } diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/web/DuplicateDialogFragment.kt b/app/src/main/java/me/devsaki/hentoid/fragments/web/DuplicateDialogFragment.kt index de97cafb4..a0fa572ab 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/web/DuplicateDialogFragment.kt +++ b/app/src/main/java/me/devsaki/hentoid/fragments/web/DuplicateDialogFragment.kt @@ -114,11 +114,6 @@ class DuplicateDialogFragment : BaseDialogFragment> { if (getWebViewAvailable()) { var cookieStr: String? = null diff --git a/app/src/main/java/me/devsaki/hentoid/viewholders/GroupDisplayItem.kt b/app/src/main/java/me/devsaki/hentoid/viewholders/GroupDisplayItem.kt index f6309f816..e677f59da 100644 --- a/app/src/main/java/me/devsaki/hentoid/viewholders/GroupDisplayItem.kt +++ b/app/src/main/java/me/devsaki/hentoid/viewholders/GroupDisplayItem.kt @@ -8,7 +8,6 @@ import androidx.core.view.isVisible import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView import coil3.dispose -import coil3.load import com.mikepenz.fastadapter.FastAdapter import com.mikepenz.fastadapter.drag.IExtendedDraggable import com.mikepenz.fastadapter.items.AbstractItem @@ -22,6 +21,7 @@ import me.devsaki.hentoid.database.domains.Group import me.devsaki.hentoid.ui.BlinkAnimation import me.devsaki.hentoid.util.Settings import me.devsaki.hentoid.util.getRatingResourceId +import me.devsaki.hentoid.util.image.loadStill class GroupDisplayItem( val group: Group, @@ -157,19 +157,7 @@ class GroupDisplayItem( return } it.visibility = View.VISIBLE - it.load(uri) - /* - if (uri.startsWith("http")) Glide.with(it) - .load(uri) - .signature(ObjectKey(uri)) - .apply(glideRequestOptions) - .into(it) - else Glide.with(it) - .load(Uri.parse(uri)) - .signature(ObjectKey(uri)) - .apply(glideRequestOptions) - .into(it) - */ + it.loadStill(uri) } } @@ -180,7 +168,6 @@ class GroupDisplayItem( override fun unbindView(item: GroupDisplayItem) { ivCover?.dispose() - //if (isValidContextForGlide(it)) Glide.with(it).clear(it) } } } \ No newline at end of file diff --git a/app/src/main/java/me/devsaki/hentoid/viewholders/ImageFileItem.kt b/app/src/main/java/me/devsaki/hentoid/viewholders/ImageFileItem.kt index ec4361a22..a61c94780 100644 --- a/app/src/main/java/me/devsaki/hentoid/viewholders/ImageFileItem.kt +++ b/app/src/main/java/me/devsaki/hentoid/viewholders/ImageFileItem.kt @@ -130,46 +130,6 @@ class ImageFileItem(private val image: ImageFile, private val showChapter: Boole // Image image.loadStill(item.image.fileUri) - /* - Glide.with(image) - .load(Uri.parse(item.image.fileUri)) - .signature(ObjectKey(item.image.uniqueHash())) - .addListener(object : RequestListener { - override fun onLoadFailed( - e: GlideException?, - model: Any?, - target: Target, - isFirstResource: Boolean - ): Boolean { - return false - } - - override fun onResourceReady( - resource: Drawable, - model: Any, - target: Target?, - dataSource: DataSource, - isFirstResource: Boolean - ): Boolean { - var handled = false - // If animated, only load frame zero as a plain bitmap - if (target != null && resource is FrameAnimationDrawable<*>) { - target.onResourceReady( - BitmapDrawable( - image.resources, - resource.frameSeqDecoder.getFrameBitmap(0) - ), null - ) - resource.stop() - handled = true - } - return handled - } - }) - .apply(glideOptionCenterInside) - .into(image) - - */ } private fun updateText(item: ImageFileItem) { @@ -188,7 +148,6 @@ class ImageFileItem(private val image: ImageFile, private val showChapter: Boole override fun unbindView(item: ImageFileItem) { image.dispose() - //if (isValidContextForGlide(image)) Glide.with(image).clear(image) } } } \ No newline at end of file diff --git a/app/src/main/java/me/devsaki/hentoid/viewholders/SubExpandableItem.kt b/app/src/main/java/me/devsaki/hentoid/viewholders/SubExpandableItem.kt index cdf0ef54a..3a05ae0f1 100644 --- a/app/src/main/java/me/devsaki/hentoid/viewholders/SubExpandableItem.kt +++ b/app/src/main/java/me/devsaki/hentoid/viewholders/SubExpandableItem.kt @@ -174,16 +174,6 @@ class SubExpandableItem( ivCover.visibility = View.VISIBLE // Use content's cookies to load image (useful for ExHentai when viewing queue screen) ivCover.load(thumbLocation) - /* - if (thumbLocation.startsWith("http")) { - bindOnlineCover(thumbLocation, null)?.let { glideUrl -> - Glide.with(ivCover).load(glideUrl).apply(glideRequestOptions).into(ivCover) - } - } else // From stored picture - Glide.with(ivCover).load(Uri.parse(thumbLocation)) - .apply(glideRequestOptions) - .into(ivCover) - */ } } diff --git a/app/src/main/java/me/devsaki/hentoid/viewmodels/ReaderViewModel.kt b/app/src/main/java/me/devsaki/hentoid/viewmodels/ReaderViewModel.kt index a8819da73..45a482589 100644 --- a/app/src/main/java/me/devsaki/hentoid/viewmodels/ReaderViewModel.kt +++ b/app/src/main/java/me/devsaki/hentoid/viewmodels/ReaderViewModel.kt @@ -2062,7 +2062,6 @@ class ReaderViewModel( // Reset Coil cache as it gets confused by the swapping clearCoilCache(getApplication()) - //Glide.get(getApplication()).clearDiskCache() } /** diff --git a/app/src/main/java/me/devsaki/hentoid/workers/TransformWorker.kt b/app/src/main/java/me/devsaki/hentoid/workers/TransformWorker.kt index a2148da83..f7be65322 100644 --- a/app/src/main/java/me/devsaki/hentoid/workers/TransformWorker.kt +++ b/app/src/main/java/me/devsaki/hentoid/workers/TransformWorker.kt @@ -70,7 +70,6 @@ class TransformWorker(context: Context, parameters: WorkerParameters) : // Reset Coil cache as it gets confused by the resizing clearCoilCache(applicationContext) - //Glide.get(applicationContext).clearDiskCache() } override fun getToWork(input: Data) { From eea30390f70b1d78dc69c3a734996cb12c32f34c Mon Sep 17 00:00:00 2001 From: Robb <35880555+robbwatershed@users.noreply.github.com> Date: Mon, 21 Oct 2024 13:55:01 +0200 Subject: [PATCH 13/44] Implement SmartRotateTransformation as a Coil Transformation --- .../hentoid/adapters/ImagePagerAdapter.kt | 18 +++++----- .../hentoid/util/image/NullTransformation.kt | 13 +++++++ .../util/image/SmartRotateTransformation.kt | 35 +++++++++++++++++++ 3 files changed, 57 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/me/devsaki/hentoid/util/image/NullTransformation.kt create mode 100644 app/src/main/java/me/devsaki/hentoid/util/image/SmartRotateTransformation.kt diff --git a/app/src/main/java/me/devsaki/hentoid/adapters/ImagePagerAdapter.kt b/app/src/main/java/me/devsaki/hentoid/adapters/ImagePagerAdapter.kt index b5782e298..023b2f847 100644 --- a/app/src/main/java/me/devsaki/hentoid/adapters/ImagePagerAdapter.kt +++ b/app/src/main/java/me/devsaki/hentoid/adapters/ImagePagerAdapter.kt @@ -20,6 +20,7 @@ import androidx.recyclerview.widget.RecyclerView import coil3.load import coil3.request.ErrorResult import coil3.request.SuccessResult +import coil3.request.transformations import kotlinx.coroutines.Runnable import me.devsaki.hentoid.R import me.devsaki.hentoid.core.BiConsumer @@ -36,6 +37,8 @@ import me.devsaki.hentoid.gles_renderer.GPUImage import me.devsaki.hentoid.util.Preferences import me.devsaki.hentoid.util.Settings import me.devsaki.hentoid.util.file.getExtension +import me.devsaki.hentoid.util.image.NullTransformation +import me.devsaki.hentoid.util.image.SmartRotateTransformation import me.devsaki.hentoid.util.image.screenHeight import me.devsaki.hentoid.util.image.screenWidth import me.devsaki.hentoid.views.ZoomableRecyclerView @@ -436,21 +439,18 @@ class ImagePagerAdapter(val context: Context) : val view = imgView as ImageView Timber.d("Using Coil") Timber.d("Using uri $uri") + val transformation = if (autoRotate) SmartRotateTransformation( + 90f, + screenWidth, + screenHeight + ) else NullTransformation() view.load(uri) { + transformations(transformation) listener( onError = { _, err -> onCoilLoadFailed(err) }, onSuccess = { _, res -> onCoilLoadSuccess(res) } ) } - /* - val centerInside: Transformation = CenterInside() - val smartRotate90 = if (autoRotate) SmartRotateTransformation( - 90f, screenWidth, screenHeight - ) else UnitTransformation.get() - Glide.with(view).load(uri) - .optionalTransform(MultiTransformation(centerInside, smartRotate90)) - .listener(this).into(view) - */ } } diff --git a/app/src/main/java/me/devsaki/hentoid/util/image/NullTransformation.kt b/app/src/main/java/me/devsaki/hentoid/util/image/NullTransformation.kt new file mode 100644 index 000000000..eb985e1a1 --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/util/image/NullTransformation.kt @@ -0,0 +1,13 @@ +package me.devsaki.hentoid.util.image + +import android.graphics.Bitmap +import coil3.size.Size +import coil3.transform.Transformation + +class NullTransformation : Transformation() { + override val cacheKey = "${this::class.qualifiedName}" + + override suspend fun transform(input: Bitmap, size: Size): Bitmap { + return input + } +} \ No newline at end of file diff --git a/app/src/main/java/me/devsaki/hentoid/util/image/SmartRotateTransformation.kt b/app/src/main/java/me/devsaki/hentoid/util/image/SmartRotateTransformation.kt new file mode 100644 index 000000000..4aa733b43 --- /dev/null +++ b/app/src/main/java/me/devsaki/hentoid/util/image/SmartRotateTransformation.kt @@ -0,0 +1,35 @@ +package me.devsaki.hentoid.util.image + +import android.graphics.Bitmap +import android.graphics.Matrix +import coil3.size.Size +import coil3.transform.Transformation + +class SmartRotateTransformation( + private val rotateRotationAngle: Float, + private val screenWidth: Int, + private val screenHeight: Int +) : Transformation() { + + override val cacheKey = "${this::class.qualifiedName}" + + override suspend fun transform(input: Bitmap, size: Size): Bitmap { + val matrix = Matrix() + if (needsRotating( + screenWidth, + screenHeight, + input.width, + input.height + ) + ) matrix.postRotate(rotateRotationAngle) + return Bitmap.createBitmap( + input, + 0, + 0, + input.width, + input.height, + matrix, + true + ) + } +} \ No newline at end of file From 0790fbb19db93046f1985da17a5be379dd378080 Mon Sep 17 00:00:00 2001 From: Robb <35880555+robbwatershed@users.noreply.github.com> Date: Mon, 21 Oct 2024 13:59:42 +0200 Subject: [PATCH 14/44] Cleanup --- app/build.gradle | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index fe1b0fe38..3ff856963 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -155,32 +155,19 @@ dependencies { /** * MEDIA */ - /* - // Image loader: github.com/bumptech/glide - def glide_version = '4.16.0' - implementation "com.github.bumptech.glide:glide:$glide_version" - ksp "com.github.bumptech.glide:ksp:$glide_version" - implementation("com.github.bumptech.glide:okhttp3-integration:$glide_version") { - exclude group: 'glide-parent' - } - */ - - // Animated pics support (APNG, AWEBP and GIF) -> https://github.com/penfeizhou/APNG4Android - def APNG4Android_version = '3.0.1' - implementation "com.github.penfeizhou.android.animation:apng:$APNG4Android_version" - //implementation "com.github.penfeizhou.android.animation:gif:$APNG4Android_version" - //implementation "com.github.penfeizhou.android.animation:awebp:$APNG4Android_version" - //implementation "com.github.penfeizhou.android.animation:glide-plugin:$APNG4Android_version" - // Image loader and animated pics support -> https://github.com/coil-kt/coil implementation("io.coil-kt.coil3:coil:3.0.0-rc01") implementation("io.coil-kt.coil3:coil-network-okhttp:3.0.0-rc01") implementation("io.coil-kt.coil3:coil-gif:3.0.0-rc01") + // Animated pics that Coil doesn't support (APNG) -> https://github.com/penfeizhou/APNG4Android + def APNG4Android_version = '3.0.1' + implementation "com.github.penfeizhou.android.animation:apng:$APNG4Android_version" + // Animated GIF creator -> https://github.com/waynejo/android-ndk-gif implementation 'io.github.waynejo:androidndkgif:1.0.1' - // PDF creator + // PDF creator/extractor implementation 'com.itextpdf:itext7-core:8.0.5' /** From 8c16ffb1b6e68b8307f1d6bf70ab396120fa3419 Mon Sep 17 00:00:00 2001 From: Robb <35880555+robbwatershed@users.noreply.github.com> Date: Fri, 25 Oct 2024 21:21:00 +0200 Subject: [PATCH 15/44] Back to dev --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 0d592998b..4269130d4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -37,7 +37,7 @@ android { minSdkVersion 26 targetSdkVersion 35 versionCode 130 // is updated automatically by BitRise; only used when building locally - versionName '1.20.0' + versionName '1.20.1-dev' def includeObjectBoxBrowser = System.getenv("INCLUDE_OBJECTBOX_BROWSER") ?: "false" def includeLeakCanary = System.getenv("INCLUDE_LEAK_CANARY") ?: "false" From 1893a9d4d78384c32a176636983c6f87d157a0e8 Mon Sep 17 00:00:00 2001 From: Robb <35880555+robbwatershed@users.noreply.github.com> Date: Fri, 25 Oct 2024 21:27:45 +0200 Subject: [PATCH 16/44] Glide -> Coil --- .../java/me/devsaki/hentoid/workers/SplitMergeWorker.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/me/devsaki/hentoid/workers/SplitMergeWorker.kt b/app/src/main/java/me/devsaki/hentoid/workers/SplitMergeWorker.kt index ed6306438..aaf246ad1 100644 --- a/app/src/main/java/me/devsaki/hentoid/workers/SplitMergeWorker.kt +++ b/app/src/main/java/me/devsaki/hentoid/workers/SplitMergeWorker.kt @@ -7,7 +7,6 @@ import androidx.annotation.IdRes import androidx.documentfile.provider.DocumentFile import androidx.work.Data import androidx.work.WorkerParameters -import com.bumptech.glide.Glide import me.devsaki.hentoid.BuildConfig import me.devsaki.hentoid.R import me.devsaki.hentoid.database.CollectionDAO @@ -35,6 +34,7 @@ import me.devsaki.hentoid.util.file.getOutputStream import me.devsaki.hentoid.util.file.listFiles import me.devsaki.hentoid.util.getLocation import me.devsaki.hentoid.util.getOrCreateContentDownloadDir +import me.devsaki.hentoid.util.image.clearCoilCache import me.devsaki.hentoid.util.mergeContents import me.devsaki.hentoid.util.moveContentToCustomGroup import me.devsaki.hentoid.util.network.UriParts @@ -444,8 +444,8 @@ abstract class BaseSplitMergeWorker( if (finalContent != null) persistJson(applicationContext, finalContent) progressDone(nbMax) - // Reset Glide cache as it gets confused by the swapping - Glide.get(applicationContext).clearDiskCache() + // Reset Coil cache as it gets confused by the swapping + clearCoilCache(applicationContext) } /** From d74bc68fec771e564e0a121820dfb75566fd7f2c Mon Sep 17 00:00:00 2001 From: Robb <35880555+robbwatershed@users.noreply.github.com> Date: Sat, 26 Oct 2024 16:07:31 +0200 Subject: [PATCH 17/44] Rename .java to .kt --- .../customssiv/decoder/{ImageDecoder.java => ImageDecoder.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/{ImageDecoder.java => ImageDecoder.kt} (100%) diff --git a/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/ImageDecoder.java b/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/ImageDecoder.kt similarity index 100% rename from app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/ImageDecoder.java rename to app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/ImageDecoder.kt From 5ec0fb6b5ca299119eaa814064e9b8556c25a13f Mon Sep 17 00:00:00 2001 From: Robb <35880555+robbwatershed@users.noreply.github.com> Date: Sat, 26 Oct 2024 16:07:31 +0200 Subject: [PATCH 18/44] ImageDecoder -> Kt --- .../customssiv/decoder/DecoderFactory.java | 25 --------------- .../customssiv/decoder/ImageDecoder.kt | 31 ++++++++----------- 2 files changed, 13 insertions(+), 43 deletions(-) delete mode 100644 app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/DecoderFactory.java diff --git a/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/DecoderFactory.java b/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/DecoderFactory.java deleted file mode 100644 index b4ac03f1f..000000000 --- a/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/DecoderFactory.java +++ /dev/null @@ -1,25 +0,0 @@ -package me.devsaki.hentoid.customssiv.decoder; - - -import androidx.annotation.NonNull; - -import java.lang.reflect.InvocationTargetException; - -/** - * Interface for {@link ImageDecoder} and {@link ImageRegionDecoder} factories. - * @param the class of decoder that will be produced. - */ -public interface DecoderFactory { - - /** - * Produce a new instance of a decoder with type {@link T}. - * @return a new instance of your decoder. - * @throws IllegalAccessException if the factory class cannot be instantiated. - * @throws InstantiationException if the factory class cannot be instantiated. - * @throws NoSuchMethodException if the factory class cannot be instantiated. - * @throws InvocationTargetException if the factory class cannot be instantiated. - */ - @NonNull - T make() throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException; - -} \ No newline at end of file diff --git a/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/ImageDecoder.kt b/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/ImageDecoder.kt index 8004d2c44..56a909ec4 100644 --- a/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/ImageDecoder.kt +++ b/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/ImageDecoder.kt @@ -1,33 +1,28 @@ -package me.devsaki.hentoid.customssiv.decoder; +package me.devsaki.hentoid.customssiv.decoder - -import android.content.Context; -import android.graphics.Bitmap; -import android.net.Uri; - -import androidx.annotation.NonNull; +import android.content.Context +import android.graphics.Bitmap +import android.net.Uri /** * Interface for image decoding classes, allowing the default {@link android.graphics.BitmapFactory} * based on the Skia library to be replaced with a custom class. */ -public interface ImageDecoder { - +interface ImageDecoder { /** * Decode an image. The URI can be in one of the following formats: - *
- * File: file:///scard/picture.jpg - *
- * Asset: file:///android_asset/picture.png - *
- * Resource: android.resource://com.example.app/drawable/picture + *

+ * File: `file:///scard/picture.jpg` + *

+ * Asset: `file:///android_asset/picture.png` + *

+ * Resource: `android.resource://com.example.app/drawable/picture` * * @param context Application context * @param uri URI of the image * @return the decoded bitmap * @throws Exception if decoding fails. */ - @NonNull - Bitmap decode(Context context, @NonNull Uri uri) throws Exception; - + @Throws(Exception::class) + fun decode(context: Context, uri: Uri): Bitmap } \ No newline at end of file From 2955f72e30f224bf5630c4345472794cf18094ba Mon Sep 17 00:00:00 2001 From: Robb <35880555+robbwatershed@users.noreply.github.com> Date: Sat, 26 Oct 2024 16:10:09 +0200 Subject: [PATCH 19/44] Rename .java to .kt --- .../decoder/{ImageRegionDecoder.java => ImageRegionDecoder.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/{ImageRegionDecoder.java => ImageRegionDecoder.kt} (100%) diff --git a/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/ImageRegionDecoder.java b/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/ImageRegionDecoder.kt similarity index 100% rename from app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/ImageRegionDecoder.java rename to app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/ImageRegionDecoder.kt From fea5ff368bb229c7bd6e9827189f7913c970bd0c Mon Sep 17 00:00:00 2001 From: Robb <35880555+robbwatershed@users.noreply.github.com> Date: Sat, 26 Oct 2024 16:10:09 +0200 Subject: [PATCH 20/44] ImageRegionDecoder -> Kt --- .../CustomSubsamplingScaleImageView.kt | 4 +- .../customssiv/decoder/ImageRegionDecoder.kt | 57 +++++++++---------- 2 files changed, 29 insertions(+), 32 deletions(-) diff --git a/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/CustomSubsamplingScaleImageView.kt b/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/CustomSubsamplingScaleImageView.kt index 89ea7d87e..b5e42192a 100644 --- a/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/CustomSubsamplingScaleImageView.kt +++ b/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/CustomSubsamplingScaleImageView.kt @@ -2090,10 +2090,10 @@ open class CustomSubsamplingScaleImageView(context: Context, attr: AttributeSet? tile: Tile ): Tile { assertNonUiThread() - if (decoder.isReady && tile.visible) { + if (decoder.isReady() && tile.visible) { view.decoderLock.readLock().lock() try { - if (decoder.isReady) { + if (decoder.isReady()) { tile.loading = true // Update tile's file sRect according to rotation tile.sRect?.let { sRect -> diff --git a/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/ImageRegionDecoder.kt b/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/ImageRegionDecoder.kt index 42336d2db..12f2dddaf 100644 --- a/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/ImageRegionDecoder.kt +++ b/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/ImageRegionDecoder.kt @@ -1,64 +1,61 @@ -package me.devsaki.hentoid.customssiv.decoder; +package me.devsaki.hentoid.customssiv.decoder -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.Point; -import android.graphics.Rect; -import android.net.Uri; - -import androidx.annotation.NonNull; +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Point +import android.graphics.Rect +import android.net.Uri /** * Interface for image decoding classes, allowing the default {@link android.graphics.BitmapRegionDecoder} * based on the Skia library to be replaced with a custom class. */ -public interface ImageRegionDecoder { - +interface ImageRegionDecoder { /** * Initialise the decoder. When possible, perform initial setup work once in this method. The * dimensions of the image must be returned. The URI can be in one of the following formats: - *
- * File: file:///scard/picture.jpg - *
- * Asset: file:///android_asset/picture.png - *
- * Resource: android.resource://com.example.app/drawable/picture + *

+ * File: `file:///scard/picture.jpg` + *

+ * Asset: `file:///android_asset/picture.png` + *

+ * Resource: `android.resource://com.example.app/drawable/picture` * @param context Application context. A reference may be held, but must be cleared on recycle. * @param uri URI of the image. * @return Dimensions of the image. * @throws Exception if initialisation fails. */ - @NonNull - Point init(Context context, @NonNull Uri uri) throws Exception; + @Throws(Exception::class) + fun init(context: Context, uri: Uri): Point /** - *

+ * + * * Decode a region of the image with the given sample size. This method is called off the UI * thread so it can safely load the image on the current thread. It is called from - * {@link android.os.AsyncTask}s running in an executor that may have multiple threads, so - * implementations must be thread safe. Adding synchronized to the method signature - * is the simplest way to achieve this, but bear in mind the {@link #recycle()} method can be + * [android.os.AsyncTask]s running in an executor that may have multiple threads, so + * implementations must be thread safe. Adding `synchronized` to the method signature + * is the simplest way to achieve this, but bear in mind the [.recycle] method can be * called concurrently. - *

- * See {@link SkiaImageRegionDecoder} and {@link SkiaPooledImageRegionDecoder} for examples of + * + * + * See [SkiaImageRegionDecoder] and [SkiaPooledImageRegionDecoder] for examples of * internal locking and synchronization. - *

+ * * @param sRect Source image rectangle to decode. * @param sampleSize Sample size. * @return The decoded region. It is safe to return null if decoding fails. */ - @NonNull - Bitmap decodeRegion(@NonNull Rect sRect, int sampleSize); + fun decodeRegion(sRect: Rect, sampleSize: Int): Bitmap /** * Status check. Should return false before initialisation and after recycle. * @return true if the decoder is ready to be used. */ - boolean isReady(); + fun isReady(): Boolean /** * This method will be called when the decoder is no longer required. It should clean up any resources still in use. */ - void recycle(); - + fun recycle() } \ No newline at end of file From 3a75b6a720c97ab51c50312313c9305fa5a0b640 Mon Sep 17 00:00:00 2001 From: Robb <35880555+robbwatershed@users.noreply.github.com> Date: Sat, 26 Oct 2024 16:15:51 +0200 Subject: [PATCH 21/44] Rename .java to .kt --- .../{SkiaImageRegionDecoder.java => SkiaImageRegionDecoder.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/{SkiaImageRegionDecoder.java => SkiaImageRegionDecoder.kt} (100%) diff --git a/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/SkiaImageRegionDecoder.java b/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/SkiaImageRegionDecoder.kt similarity index 100% rename from app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/SkiaImageRegionDecoder.java rename to app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/SkiaImageRegionDecoder.kt From c910761d8f517d8ea4cd07751953a597e921b05d Mon Sep 17 00:00:00 2001 From: Robb <35880555+robbwatershed@users.noreply.github.com> Date: Sat, 26 Oct 2024 16:15:51 +0200 Subject: [PATCH 22/44] SkiaImageRegionDecoder -> Kt --- .../decoder/SkiaImageRegionDecoder.kt | 148 ++++++++---------- 1 file changed, 68 insertions(+), 80 deletions(-) diff --git a/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/SkiaImageRegionDecoder.kt b/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/SkiaImageRegionDecoder.kt index af2159d1b..82c1e2491 100644 --- a/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/SkiaImageRegionDecoder.kt +++ b/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/SkiaImageRegionDecoder.kt @@ -1,26 +1,20 @@ -package me.devsaki.hentoid.customssiv.decoder; - -import static me.devsaki.hentoid.customssiv.decoder.SkiaDecoderHelperKt.getResourceId; - -import android.content.ContentResolver; -import android.content.Context; -import android.content.pm.PackageManager; -import android.content.res.AssetManager; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.BitmapRegionDecoder; -import android.graphics.ColorSpace; -import android.graphics.Point; -import android.graphics.Rect; -import android.net.Uri; - -import androidx.annotation.NonNull; - -import java.io.IOException; -import java.io.InputStream; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReadWriteLock; -import java.util.concurrent.locks.ReentrantReadWriteLock; +package me.devsaki.hentoid.customssiv.decoder + +import android.content.ContentResolver +import android.content.Context +import android.content.pm.PackageManager +import android.content.res.AssetManager +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.BitmapRegionDecoder +import android.graphics.ColorSpace +import android.graphics.Point +import android.graphics.Rect +import android.net.Uri +import java.io.IOException +import java.util.concurrent.locks.Lock +import java.util.concurrent.locks.ReadWriteLock +import java.util.concurrent.locks.ReentrantReadWriteLock /** * Default implementation of {@link ImageRegionDecoder} @@ -33,86 +27,80 @@ import java.util.concurrent.locks.ReentrantReadWriteLock; * tiles are being loaded before recycling the decoder. In practice, {@link BitmapRegionDecoder} is * synchronized internally so this has no real impact on performance. */ -public class SkiaImageRegionDecoder implements ImageRegionDecoder { - - private BitmapRegionDecoder decoder; - private final ReadWriteLock decoderLock = new ReentrantReadWriteLock(true); +private const val FILE_PREFIX = "file://" +private const val ASSET_PREFIX = "$FILE_PREFIX/android_asset/" +private const val RESOURCE_PREFIX = ContentResolver.SCHEME_ANDROID_RESOURCE + "://" - private static final String FILE_PREFIX = "file://"; - private static final String ASSET_PREFIX = FILE_PREFIX + "/android_asset/"; - private static final String RESOURCE_PREFIX = ContentResolver.SCHEME_ANDROID_RESOURCE + "://"; - - private final Bitmap.Config bitmapConfig; - - - public SkiaImageRegionDecoder(@NonNull Bitmap.Config bitmapConfig) { - this.bitmapConfig = bitmapConfig; - } +class SkiaImageRegionDecoder(private val bitmapConfig: Bitmap.Config) : ImageRegionDecoder { + private var decoder: BitmapRegionDecoder? = null + private val decoderLock: ReadWriteLock = ReentrantReadWriteLock(true) - @Override - @NonNull - public Point init(Context context, @NonNull Uri uri) throws IOException, PackageManager.NameNotFoundException { - String uriString = uri.toString(); + @Throws(IOException::class, PackageManager.NameNotFoundException::class) + override fun init(context: Context, uri: Uri): Point { + val uriString = uri.toString() if (uriString.startsWith(RESOURCE_PREFIX)) { - int id = getResourceId(context, uri); - decoder = BitmapRegionDecoder.newInstance(context.getResources().openRawResource(id), false); + val id = getResourceId(context, uri) + decoder = BitmapRegionDecoder.newInstance(context.resources.openRawResource(id), false) } else if (uriString.startsWith(ASSET_PREFIX)) { - String assetName = uriString.substring(ASSET_PREFIX.length()); - decoder = BitmapRegionDecoder.newInstance(context.getAssets().open(assetName, AssetManager.ACCESS_RANDOM), false); + val assetName = uriString.substring(ASSET_PREFIX.length) + decoder = BitmapRegionDecoder.newInstance( + context.assets.open( + assetName, + AssetManager.ACCESS_RANDOM + ), false + ) } else if (uriString.startsWith(FILE_PREFIX)) { - decoder = BitmapRegionDecoder.newInstance(uriString.substring(FILE_PREFIX.length()), false); + decoder = + BitmapRegionDecoder.newInstance(uriString.substring(FILE_PREFIX.length), false) } else { - try (InputStream input = context.getContentResolver().openInputStream(uri)) { - if (input == null) - throw new RuntimeException("Content resolver returned null stream. Unable to initialise with uri."); - decoder = BitmapRegionDecoder.newInstance(input, false); + context.contentResolver.openInputStream(uri).use { input -> + if (input == null) throw RuntimeException("Content resolver returned null stream. Unable to initialise with uri.") + decoder = BitmapRegionDecoder.newInstance(input, false) } } - if (decoder != null && !decoder.isRecycled()) - return new Point(decoder.getWidth(), decoder.getHeight()); - else return new Point(-1, -1); + return if (decoder != null && !decoder!!.isRecycled) Point( + decoder!!.width, + decoder!!.height + ) + else Point(-1, -1) } - @Override - @NonNull - public Bitmap decodeRegion(@NonNull Rect sRect, int sampleSize) { - getDecodeLock().lock(); + override fun decodeRegion(sRect: Rect, sampleSize: Int): Bitmap { + getDecodeLock().lock() try { - if (decoder != null && !decoder.isRecycled()) { - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inSampleSize = sampleSize; - options.inPreferredConfig = bitmapConfig; + if (decoder != null && !decoder!!.isRecycled) { + val options = BitmapFactory.Options() + options.inSampleSize = sampleSize + options.inPreferredConfig = bitmapConfig // If that is not set, some PNGs are read with a ColorSpace of code "Unknown" (-1), // which makes resizing buggy (generates a black picture) - options.inPreferredColorSpace = ColorSpace.get(ColorSpace.Named.SRGB); + options.inPreferredColorSpace = ColorSpace.get(ColorSpace.Named.SRGB) - Bitmap bitmap = decoder.decodeRegion(sRect, options); - if (bitmap == null) { - throw new RuntimeException("Skia image decoder returned null bitmap - image format may not be supported"); - } + val bitmap = decoder!!.decodeRegion(sRect, options) + ?: throw RuntimeException("Skia image decoder returned null bitmap - image format may not be supported") - return bitmap; + return bitmap } else { - throw new IllegalStateException("Cannot decode region after decoder has been recycled"); + throw IllegalStateException("Cannot decode region after decoder has been recycled") } } finally { - getDecodeLock().unlock(); + getDecodeLock().unlock() } } - @Override - public synchronized boolean isReady() { - return decoder != null && !decoder.isRecycled(); + @Synchronized + override fun isReady(): Boolean { + return decoder != null && !decoder!!.isRecycled } - @Override - public synchronized void recycle() { - decoderLock.writeLock().lock(); + @Synchronized + override fun recycle() { + decoderLock.writeLock().lock() try { - if (decoder != null) decoder.recycle(); - decoder = null; + if (decoder != null) decoder!!.recycle() + decoder = null } finally { - decoderLock.writeLock().unlock(); + decoderLock.writeLock().unlock() } } @@ -121,7 +109,7 @@ public class SkiaImageRegionDecoder implements ImageRegionDecoder { * regions from multiple threads with one decoder instance causes a segfault. For old versions * use the write lock to enforce single threaded decoding. */ - private Lock getDecodeLock() { - return decoderLock.readLock(); + private fun getDecodeLock(): Lock { + return decoderLock.readLock() } } \ No newline at end of file From f4d7537e8cbbab4477cd28db331e6edae28f6157 Mon Sep 17 00:00:00 2001 From: Robb <35880555+robbwatershed@users.noreply.github.com> Date: Sat, 26 Oct 2024 16:20:59 +0200 Subject: [PATCH 23/44] Rename .java to .kt --- ...ledImageRegionDecoder.java => SkiaPooledImageRegionDecoder.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/{SkiaPooledImageRegionDecoder.java => SkiaPooledImageRegionDecoder.kt} (100%) diff --git a/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/SkiaPooledImageRegionDecoder.java b/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/SkiaPooledImageRegionDecoder.kt similarity index 100% rename from app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/SkiaPooledImageRegionDecoder.java rename to app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/SkiaPooledImageRegionDecoder.kt From ff18063a095dcd9331d215e06ef4f0e02a49e64f Mon Sep 17 00:00:00 2001 From: Robb <35880555+robbwatershed@users.noreply.github.com> Date: Sat, 26 Oct 2024 16:20:59 +0200 Subject: [PATCH 24/44] SkiaPooledImageRegionDecoder -> Kt --- .../decoder/SkiaPooledImageRegionDecoder.kt | 434 +++++++++--------- 1 file changed, 210 insertions(+), 224 deletions(-) diff --git a/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/SkiaPooledImageRegionDecoder.kt b/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/SkiaPooledImageRegionDecoder.kt index ac1171374..6b6bb2c36 100644 --- a/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/SkiaPooledImageRegionDecoder.kt +++ b/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/decoder/SkiaPooledImageRegionDecoder.kt @@ -1,37 +1,26 @@ -package me.devsaki.hentoid.customssiv.decoder; - -import static android.content.Context.ACTIVITY_SERVICE; -import static me.devsaki.hentoid.customssiv.decoder.SkiaDecoderHelperKt.getResourceId; - -import android.app.ActivityManager; -import android.content.ContentResolver; -import android.content.Context; -import android.content.pm.PackageManager; -import android.content.res.AssetFileDescriptor; -import android.content.res.AssetManager; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.BitmapRegionDecoder; -import android.graphics.ColorSpace; -import android.graphics.Point; -import android.graphics.Rect; -import android.net.Uri; -import android.util.Log; - -import androidx.annotation.Keep; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.Executor; -import java.util.concurrent.Semaphore; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.locks.ReadWriteLock; -import java.util.concurrent.locks.ReentrantReadWriteLock; +package me.devsaki.hentoid.customssiv.decoder + +import android.app.ActivityManager +import android.content.ContentResolver +import android.content.Context +import android.content.pm.PackageManager +import android.content.res.AssetManager +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.BitmapRegionDecoder +import android.graphics.ColorSpace +import android.graphics.Point +import android.graphics.Rect +import android.net.Uri +import androidx.annotation.Keep +import timber.log.Timber +import java.io.File +import java.io.IOException +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.Semaphore +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.locks.ReadWriteLock +import java.util.concurrent.locks.ReentrantReadWriteLock /** *

@@ -51,32 +40,22 @@ import java.util.concurrent.locks.ReentrantReadWriteLock; * {@link SkiaImageRegionDecoder} on old or low powered devices you could not test. *

*/ -public class SkiaPooledImageRegionDecoder implements ImageRegionDecoder { - - private static final String TAG = SkiaPooledImageRegionDecoder.class.getSimpleName(); - - private static boolean debug = false; - - private DecoderPool decoderPool = new DecoderPool(); - private final ReadWriteLock decoderLock = new ReentrantReadWriteLock(true); +private const val FILE_PREFIX = "file://" +private const val ASSET_PREFIX = "$FILE_PREFIX/android_asset/" +private const val RESOURCE_PREFIX = ContentResolver.SCHEME_ANDROID_RESOURCE + "://" - private static final String FILE_PREFIX = "file://"; - private static final String ASSET_PREFIX = FILE_PREFIX + "/android_asset/"; - private static final String RESOURCE_PREFIX = ContentResolver.SCHEME_ANDROID_RESOURCE + "://"; +class SkiaPooledImageRegionDecoder(private val bitmapConfig: Bitmap.Config) : ImageRegionDecoder { + private var debug: Boolean = false - private final Bitmap.Config bitmapConfig; + private var decoderPool: DecoderPool? = DecoderPool() + private val decoderLock: ReadWriteLock = ReentrantReadWriteLock(true) - private Context context; - private Uri uri; + private var context: Context? = null + private var uri: Uri? = null - private long fileLength = Long.MAX_VALUE; - private final Point imageDimensions = new Point(0, 0); - private final AtomicBoolean lazyInited = new AtomicBoolean(false); - - - public SkiaPooledImageRegionDecoder(@NonNull Bitmap.Config bitmapConfig) { - this.bitmapConfig = bitmapConfig; - } + private var fileLength = Long.MAX_VALUE + private val imageDimensions = Point(0, 0) + private val lazyInited = AtomicBoolean(false) /** * Controls logging of debug messages. All instances are affected. @@ -84,162 +63,173 @@ public class SkiaPooledImageRegionDecoder implements ImageRegionDecoder { * @param debug true to enable debug logging, false to disable. */ @Keep - @SuppressWarnings("unused") - public static void setDebug(boolean debug) { - SkiaPooledImageRegionDecoder.debug = debug; + @Suppress("unused") + fun setDebug(debug: Boolean) { + this.debug = debug } /** * Initialises the decoder pool. This method creates one decoder on the current thread and uses * it to decode the bounds, then spawns an independent thread to populate the pool with an - * additional three decoders. The thread will abort if {@link #recycle()} is called. + * additional three decoders. The thread will abort if [.recycle] is called. */ - @Override - @NonNull - public Point init(final Context context, @NonNull final Uri uri) throws Exception { - this.context = context; - this.uri = uri; - initialiseDecoder(); - return this.imageDimensions; + @Throws(Exception::class) + override fun init(context: Context, uri: Uri): Point { + this.context = context + this.uri = uri + initialiseDecoder() + return this.imageDimensions } /** - * Initialises extra decoders for as long as {@link #allowAdditionalDecoder(int, long)} returns + * Initialises extra decoders for as long as [.allowAdditionalDecoder] returns * true and the pool has not been recycled. */ - private void lazyInit() { + private fun lazyInit() { if (lazyInited.compareAndSet(false, true) && fileLength < Long.MAX_VALUE) { - debug("Starting lazy init of additional decoders"); - Thread thread = new Thread() { - @Override - public void run() { - while (decoderPool != null && allowAdditionalDecoder(decoderPool.size(), fileLength)) { + debug("Starting lazy init of additional decoders") + val thread: Thread = object : Thread() { + override fun run() { + while (decoderPool != null && allowAdditionalDecoder( + decoderPool!!.size(), + fileLength + ) + ) { // New decoders can be created while reading tiles but this read lock prevents // them being initialised while the pool is being recycled. try { if (decoderPool != null) { - long start = System.currentTimeMillis(); - debug("Starting decoder"); - initialiseDecoder(); - long end = System.currentTimeMillis(); - debug("Started decoder, took " + (end - start) + "ms"); + val start = System.currentTimeMillis() + debug("Starting decoder") + initialiseDecoder() + val end = System.currentTimeMillis() + debug("Started decoder, took " + (end - start) + "ms") } - } catch (Exception e) { + } catch (e: Exception) { // A decoder has already been successfully created so we can ignore this - debug("Failed to start decoder: " + e.getMessage()); + debug("Failed to start decoder: " + e.message) } } } - }; - thread.start(); + } + thread.start() } } /** - * Initialises a new {@link BitmapRegionDecoder} and adds it to the pool, unless the pool has + * Initialises a new [BitmapRegionDecoder] and adds it to the pool, unless the pool has * been recycled while it was created. */ - private void initialiseDecoder() throws IOException, PackageManager.NameNotFoundException { - String uriString = uri.toString(); - BitmapRegionDecoder decoder; - long localFileLength = Long.MAX_VALUE; + @Throws(IOException::class, PackageManager.NameNotFoundException::class) + private fun initialiseDecoder() { + val uriString = uri.toString() + var decoder: BitmapRegionDecoder? + var localFileLength = Long.MAX_VALUE if (uriString.startsWith(RESOURCE_PREFIX)) { - int id = getResourceId(context, uri); - try (AssetFileDescriptor descriptor = context.getResources().openRawResourceFd(id)) { - localFileLength = descriptor.getLength(); - } catch (Exception e) { + val id = getResourceId(context!!, uri!!) + try { + context!!.resources.openRawResourceFd(id).use { descriptor -> + localFileLength = descriptor.length + } + } catch (e: Exception) { // Pooling disabled } - decoder = BitmapRegionDecoder.newInstance(context.getResources().openRawResource(id), false); + decoder = + BitmapRegionDecoder.newInstance(context!!.resources.openRawResource(id), false) } else if (uriString.startsWith(ASSET_PREFIX)) { - String assetName = uriString.substring(ASSET_PREFIX.length()); - try (AssetFileDescriptor descriptor = context.getAssets().openFd(assetName)) { - localFileLength = descriptor.getLength(); - } catch (Exception e) { + val assetName = uriString.substring(ASSET_PREFIX.length) + try { + context!!.assets.openFd(assetName).use { descriptor -> + localFileLength = descriptor.length + } + } catch (e: Exception) { // Pooling disabled } - decoder = BitmapRegionDecoder.newInstance(context.getAssets().open(assetName, AssetManager.ACCESS_RANDOM), false); + decoder = BitmapRegionDecoder.newInstance( + context!!.assets.open( + assetName, + AssetManager.ACCESS_RANDOM + ), false + ) } else if (uriString.startsWith(FILE_PREFIX)) { - decoder = BitmapRegionDecoder.newInstance(uriString.substring(FILE_PREFIX.length()), false); + decoder = + BitmapRegionDecoder.newInstance(uriString.substring(FILE_PREFIX.length), false) try { - File file = new File(uriString); + val file = File(uriString) if (file.exists()) { - localFileLength = file.length(); + localFileLength = file.length() } - } catch (Exception e) { + } catch (e: Exception) { // Pooling disabled } } else { - ContentResolver contentResolver = context.getContentResolver(); - try (InputStream input = contentResolver.openInputStream(uri)) { - if (input == null) - throw new RuntimeException("Content resolver returned null stream. Unable to initialise with uri."); - decoder = BitmapRegionDecoder.newInstance(input, false); - try (AssetFileDescriptor descriptor = contentResolver.openAssetFileDescriptor(uri, "r")) { - if (descriptor != null) { - localFileLength = descriptor.getLength(); + val contentResolver = context!!.contentResolver + contentResolver.openInputStream(uri!!).use { input -> + if (input == null) throw RuntimeException("Content resolver returned null stream. Unable to initialise with uri.") + decoder = BitmapRegionDecoder.newInstance(input, false) + try { + contentResolver.openAssetFileDescriptor(uri!!, "r").use { descriptor -> + if (descriptor != null) { + localFileLength = descriptor.length + } } - } catch (Exception e) { + } catch (e: Exception) { // Stick with MAX_LENGTH } } } - this.fileLength = localFileLength; - this.imageDimensions.set(decoder.getWidth(), decoder.getHeight()); - decoderLock.writeLock().lock(); + this.fileLength = localFileLength + imageDimensions[decoder!!.width] = decoder!!.height + decoderLock.writeLock().lock() try { if (decoderPool != null) { - decoderPool.add(decoder); + decoderPool!!.add(decoder) } } finally { - decoderLock.writeLock().unlock(); + decoderLock.writeLock().unlock() } } /** * Acquire a read lock to prevent decoding overlapping with recycling, then check the pool still * exists and acquire a decoder to load the requested region. There is no check whether the pool - * currently has decoders, because it's guaranteed to have one decoder after {@link #init(Context, Uri)} - * is called and be null once {@link #recycle()} is called. In practice the view can't call this - * method until after {@link #init(Context, Uri)}, so there will be no blocking on an empty pool. + * currently has decoders, because it's guaranteed to have one decoder after [.init] + * is called and be null once [.recycle] is called. In practice the view can't call this + * method until after [.init], so there will be no blocking on an empty pool. */ - @Override - @NonNull - public Bitmap decodeRegion(@NonNull Rect sRect, int sampleSize) { - debug("Decode region " + sRect + " on thread " + Thread.currentThread().getName()); + override fun decodeRegion(sRect: Rect, sampleSize: Int): Bitmap { + debug("Decode region " + sRect + " on thread " + Thread.currentThread().name) if (sRect.width() < imageDimensions.x || sRect.height() < imageDimensions.y) { - lazyInit(); + lazyInit() } - decoderLock.readLock().lock(); + decoderLock.readLock().lock() try { if (decoderPool != null) { - BitmapRegionDecoder decoder = decoderPool.acquire(); + val decoder = decoderPool!!.acquire() try { // Decoder can't be null or recycled in practice - if (decoder != null && !decoder.isRecycled()) { - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inSampleSize = sampleSize; - options.inPreferredConfig = bitmapConfig; + if (decoder != null && !decoder.isRecycled) { + val options = BitmapFactory.Options() + options.inSampleSize = sampleSize + options.inPreferredConfig = bitmapConfig // If that is not set, some PNGs are read with a ColorSpace of code "Unknown" (-1), // which makes resizing buggy (generates a black picture) - options.inPreferredColorSpace = ColorSpace.get(ColorSpace.Named.SRGB); + options.inPreferredColorSpace = ColorSpace.get(ColorSpace.Named.SRGB) - Bitmap bitmap = decoder.decodeRegion(sRect, options); - if (bitmap == null) { - throw new RuntimeException("Skia image decoder returned null bitmap - image format may not be supported"); - } - return bitmap; + val bitmap = decoder.decodeRegion(sRect, options) + ?: throw RuntimeException("Skia image decoder returned null bitmap - image format may not be supported") + return bitmap } } finally { if (decoder != null) { - decoderPool.release(decoder); + decoderPool!!.release(decoder) } } } - throw new IllegalStateException("Cannot decode region after decoder has been recycled"); + throw IllegalStateException("Cannot decode region after decoder has been recycled") } finally { - decoderLock.readLock().unlock(); + decoderLock.readLock().unlock() } } @@ -247,27 +237,27 @@ public class SkiaPooledImageRegionDecoder implements ImageRegionDecoder { * Holding a read lock to avoid returning true while the pool is being recycled, this returns * true if the pool has at least one decoder available. */ - @Override - public synchronized boolean isReady() { - return decoderPool != null && !decoderPool.isEmpty(); + @Synchronized + override fun isReady(): Boolean { + return !(decoderPool?.isEmpty ?: true) } /** - * Wait until all read locks held by {@link #decodeRegion(Rect, int)} are released, then recycle + * Wait until all read locks held by [.decodeRegion] are released, then recycle * and destroy the pool. Elsewhere, when a read lock is acquired, we must check the pool is not null. */ - @Override - public synchronized void recycle() { - decoderLock.writeLock().lock(); + @Synchronized + override fun recycle() { + decoderLock.writeLock().lock() try { if (decoderPool != null) { - decoderPool.recycle(); - decoderPool = null; - context = null; - uri = null; + decoderPool!!.recycle() + decoderPool = null + context = null + uri = null } } finally { - decoderLock.writeLock().unlock(); + decoderLock.writeLock().unlock() } } @@ -280,136 +270,132 @@ public class SkiaPooledImageRegionDecoder implements ImageRegionDecoder { * @param fileLength the size of the image file in bytes. Creating another decoder will use approximately this much native memory. * @return true if another decoder can be created. */ - @SuppressWarnings("WeakerAccess") - protected boolean allowAdditionalDecoder(int numberOfDecoders, long fileLength) { + protected fun allowAdditionalDecoder(numberOfDecoders: Int, fileLength: Long): Boolean { if (numberOfDecoders >= 4) { - debug("No additional decoders allowed, reached hard limit (4)"); - return false; + debug("No additional decoders allowed, reached hard limit (4)") + return false } else if (numberOfDecoders * fileLength > 20 * 1024 * 1024) { - debug("No additional encoders allowed, reached hard memory limit (20Mb)"); - return false; + debug("No additional encoders allowed, reached hard memory limit (20Mb)") + return false } else if (numberOfDecoders >= getNumberOfCores()) { - debug("No additional encoders allowed, limited by CPU cores (" + getNumberOfCores() + ")"); - return false; + debug("No additional encoders allowed, limited by CPU cores (" + getNumberOfCores() + ")") + return false } else if (isLowMemory()) { - debug("No additional encoders allowed, memory is low"); - return false; + debug("No additional encoders allowed, memory is low") + return false } - debug("Additional decoder allowed, current count is " + numberOfDecoders + ", estimated native memory " + ((fileLength * numberOfDecoders) / (1024 * 1024)) + "Mb"); - return true; + debug("Additional decoder allowed, current count is " + numberOfDecoders + ", estimated native memory " + ((fileLength * numberOfDecoders) / (1024 * 1024)) + "Mb") + return true } /** - * A simple pool of {@link BitmapRegionDecoder} instances, all loading from the same source. + * A simple pool of [BitmapRegionDecoder] instances, all loading from the same source. */ - private static class DecoderPool { - private final Semaphore available = new Semaphore(0, true); - private final Map decoders = new ConcurrentHashMap<>(); + private class DecoderPool { + private val available = Semaphore(0, true) + private val decoders: MutableMap = ConcurrentHashMap() - /** - * Returns false if there is at least one decoder in the pool. - */ - private synchronized boolean isEmpty() { - return decoders.isEmpty(); - } + @get:Synchronized + val isEmpty: Boolean + /** + * Returns false if there is at least one decoder in the pool. + */ + get() = decoders.isEmpty() /** * Returns number of encoders. */ - private synchronized int size() { - return decoders.size(); + @Synchronized + fun size(): Int { + return decoders.size } /** * Acquire a decoder. Blocks until one is available. */ - @Nullable - private BitmapRegionDecoder acquire() { - available.acquireUninterruptibly(); - return getNextAvailable(); + fun acquire(): BitmapRegionDecoder? { + available.acquireUninterruptibly() + return nextAvailable } /** * Release a decoder back to the pool. */ - private void release(BitmapRegionDecoder decoder) { + fun release(decoder: BitmapRegionDecoder) { if (markAsUnused(decoder)) { - available.release(); + available.release() } } /** * Adds a newly created decoder to the pool, releasing an additional permit. */ - private synchronized void add(BitmapRegionDecoder decoder) { - decoders.put(decoder, false); - available.release(); + @Synchronized + fun add(decoder: BitmapRegionDecoder?) { + decoders[decoder] = false + available.release() } /** * While there are decoders in the map, wait until each is available before acquiring, - * recycling and removing it. After this is called, any call to {@link #acquire()} will + * recycling and removing it. After this is called, any call to [.acquire] will * block forever, so this call should happen within a write lock, and all calls to - * {@link #acquire()} should be made within a read lock so they cannot end up blocking on + * [.acquire] should be made within a read lock so they cannot end up blocking on * the semaphore when it has no permits. */ - private synchronized void recycle() { - while (!decoders.isEmpty()) { - BitmapRegionDecoder decoder = acquire(); + @Synchronized + fun recycle() { + while (decoders.isNotEmpty()) { + val decoder = acquire() if (decoder != null) { - decoder.recycle(); - decoders.remove(decoder); + decoder.recycle() + decoders.remove(decoder) } } } - @Nullable - private synchronized BitmapRegionDecoder getNextAvailable() { - for (Map.Entry entry : decoders.entrySet()) { - if (!entry.getValue()) { - entry.setValue(true); - return entry.getKey(); + @get:Synchronized + private val nextAvailable: BitmapRegionDecoder? + get() { + for (entry in decoders.entries) { + if (!entry.value) { + entry.setValue(true) + return entry.key + } } + return null } - return null; - } - private synchronized boolean markAsUnused(BitmapRegionDecoder decoder) { - for (Map.Entry entry : decoders.entrySet()) { - if (decoder == entry.getKey()) { - if (entry.getValue()) { - entry.setValue(false); - return true; + @Synchronized + private fun markAsUnused(decoder: BitmapRegionDecoder): Boolean { + for (entry in decoders.entries) { + if (decoder == entry.key) { + if (entry.value) { + entry.setValue(false) + return true } else { - return false; + return false } } } - return false; + return false } - } - private int getNumberOfCores() { - return Runtime.getRuntime().availableProcessors(); + private fun getNumberOfCores(): Int { + return Runtime.getRuntime().availableProcessors() } - private boolean isLowMemory() { - ActivityManager activityManager = (ActivityManager) context.getSystemService(ACTIVITY_SERVICE); - if (activityManager != null) { - ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo(); - activityManager.getMemoryInfo(memoryInfo); - return memoryInfo.lowMemory; - } else { - return true; - } + private fun isLowMemory(): Boolean { + val activityManager = + context!!.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + val memoryInfo = ActivityManager.MemoryInfo() + activityManager.getMemoryInfo(memoryInfo) + return memoryInfo.lowMemory } - private void debug(String message) { - if (debug) { - Log.d(TAG, message); - } + private fun debug(message: String) { + if (debug) Timber.d(message) } - } \ No newline at end of file From 7d909039e56a47c9df3094c1958b8ce6210890b9 Mon Sep 17 00:00:00 2001 From: Robb <35880555+robbwatershed@users.noreply.github.com> Date: Sat, 26 Oct 2024 16:28:07 +0200 Subject: [PATCH 25/44] Rename .java to .kt --- .../{PixivUserIllustMetadata.java => PixivUserIllustMetadata.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/main/java/me/devsaki/hentoid/json/sources/{PixivUserIllustMetadata.java => PixivUserIllustMetadata.kt} (100%) diff --git a/app/src/main/java/me/devsaki/hentoid/json/sources/PixivUserIllustMetadata.java b/app/src/main/java/me/devsaki/hentoid/json/sources/PixivUserIllustMetadata.kt similarity index 100% rename from app/src/main/java/me/devsaki/hentoid/json/sources/PixivUserIllustMetadata.java rename to app/src/main/java/me/devsaki/hentoid/json/sources/PixivUserIllustMetadata.kt From cf32c530a7db7cc255ccc358eab7722735fb9bc1 Mon Sep 17 00:00:00 2001 From: Robb <35880555+robbwatershed@users.noreply.github.com> Date: Sat, 26 Oct 2024 16:28:07 +0200 Subject: [PATCH 26/44] PixivUserIllustMetadata -> Kt --- .../json/sources/PixivUserIllustMetadata.kt | 40 +++++++++---------- .../hentoid/parsers/images/PixivParser.kt | 12 +++--- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/me/devsaki/hentoid/json/sources/PixivUserIllustMetadata.kt b/app/src/main/java/me/devsaki/hentoid/json/sources/PixivUserIllustMetadata.kt index 67fde4138..76dfe8d55 100644 --- a/app/src/main/java/me/devsaki/hentoid/json/sources/PixivUserIllustMetadata.kt +++ b/app/src/main/java/me/devsaki/hentoid/json/sources/PixivUserIllustMetadata.kt @@ -1,32 +1,32 @@ -package me.devsaki.hentoid.json.sources; +package me.devsaki.hentoid.json.sources + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass -import java.util.Collections; -import java.util.List; /** * Data structure for Pixiv's "user illusts" mobile endpoint */ -@SuppressWarnings({"unused, MismatchedQueryAndUpdateOfCollection", "squid:S1172", "squid:S1068"}) -public class PixivUserIllustMetadata { - - private Boolean error; - private String message; - private PixivUserIllusts body; - - public List getIllustIds() { - if (null == body || null == body.user_illust_ids) return Collections.emptyList(); - return body.user_illust_ids; +data class PixivUserIllustMetadata( + private val error: Boolean? = null, + private val message: String? = null, + private val body: PixivUserIllusts? = null +) { + fun getIllustIds(): List { + return body?.userIllustIds ?: emptyList() } - public boolean isError() { - return error; + fun isError(): Boolean { + return error ?: false } - public String getMessage() { - return message; + fun getMessage(): String { + return message ?: "" } - private static class PixivUserIllusts { - private List user_illust_ids; - } + @JsonClass(generateAdapter = true) + data class PixivUserIllusts( + @Json(name = "user_illust_ids") + val userIllustIds: List? = null + ) } diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/images/PixivParser.kt b/app/src/main/java/me/devsaki/hentoid/parsers/images/PixivParser.kt index 4fe77f6da..197599ddf 100644 --- a/app/src/main/java/me/devsaki/hentoid/parsers/images/PixivParser.kt +++ b/app/src/main/java/me/devsaki/hentoid/parsers/images/PixivParser.kt @@ -103,9 +103,9 @@ class PixivParser : BaseImageListParser() { PixivServer.api.getIllustPages(content.uniqueSiteId, cookieStr, acceptAll, userAgent) .execute().body() if (null == galleryMetadata || galleryMetadata.error == true) { - var message: String? = "" - if (galleryMetadata != null) message = galleryMetadata.message - throw EmptyResultException(message!!) + var message = "" + if (galleryMetadata != null) message = galleryMetadata.message ?: "" + throw EmptyResultException(message) } return urlsToImageFiles( galleryMetadata.getPageUrls(), @@ -247,14 +247,14 @@ class PixivParser : BaseImageListParser() { ) } val userIllustsMetadata = userIllustResp.body() - if (null == userIllustsMetadata || userIllustsMetadata.isError) { + if (null == userIllustsMetadata || userIllustsMetadata.isError()) { var message: String? = "Unreachable user illusts" - if (userIllustsMetadata != null) message = userIllustsMetadata.message + if (userIllustsMetadata != null) message = userIllustsMetadata.getMessage() throw IllegalArgumentException(message) } // Detect extra chapters - var illustIds = userIllustsMetadata.illustIds + var illustIds = userIllustsMetadata.getIllustIds() var storedChapters: List? = null if (storedContent != null) { storedChapters = storedContent.chapters From 9cbc1d4daa46c93421fae3d51b7fcbbf3ca8c1a3 Mon Sep 17 00:00:00 2001 From: Robb <35880555+robbwatershed@users.noreply.github.com> Date: Sat, 26 Oct 2024 16:33:27 +0200 Subject: [PATCH 27/44] Rename .java to .kt --- .../json/sources/{EHentaiImageQuery.java => EHentaiImageQuery.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/main/java/me/devsaki/hentoid/json/sources/{EHentaiImageQuery.java => EHentaiImageQuery.kt} (100%) diff --git a/app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiImageQuery.java b/app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiImageQuery.kt similarity index 100% rename from app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiImageQuery.java rename to app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiImageQuery.kt From 21d0e0349eefdaff91752d28527597f4505d2288 Mon Sep 17 00:00:00 2001 From: Robb <35880555+robbwatershed@users.noreply.github.com> Date: Sat, 26 Oct 2024 16:33:27 +0200 Subject: [PATCH 28/44] EHentaiImageQuery -> Kt --- .../hentoid/json/sources/EHentaiImageQuery.kt | 28 ++++++------------- .../hentoid/parsers/images/EHentaiParser.kt | 2 +- 2 files changed, 9 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiImageQuery.kt b/app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiImageQuery.kt index d779faf63..95245d890 100644 --- a/app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiImageQuery.kt +++ b/app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiImageQuery.kt @@ -1,21 +1,9 @@ -package me.devsaki.hentoid.json.sources; +package me.devsaki.hentoid.json.sources -@SuppressWarnings({"unused, MismatchedQueryAndUpdateOfCollection", "squid:S1172", "squid:S1068", "FieldCanBeLocal"}) -public class EHentaiImageQuery { - private final String method = "imagedispatch"; - private final Integer gid; - private final String imgkey; - private final String mpvkey; - private final Integer page; - - public EHentaiImageQuery( - int gid, - String imgKey, - String mpvKey, - int page) { - this.gid = gid; - this.imgkey = imgKey; - this.mpvkey = mpvKey; - this.page = page; - } -} +data class EHentaiImageQuery( + val gid: Int, + val imgkey: String, + val mpvkey: String, + val page: Int, + val method: String = "imagedispatch" +) \ No newline at end of file diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/images/EHentaiParser.kt b/app/src/main/java/me/devsaki/hentoid/parsers/images/EHentaiParser.kt index fe553c427..404af4d33 100644 --- a/app/src/main/java/me/devsaki/hentoid/parsers/images/EHentaiParser.kt +++ b/app/src/main/java/me/devsaki/hentoid/parsers/images/EHentaiParser.kt @@ -558,7 +558,7 @@ class EHentaiParser : ImageListParser { chapter ) } - + override fun clear() { // No need for that here } From 1d6a805a58643a6a4bbff0f995f70d0daca1e23a Mon Sep 17 00:00:00 2001 From: Robb <35880555+robbwatershed@users.noreply.github.com> Date: Sat, 26 Oct 2024 16:36:33 +0200 Subject: [PATCH 29/44] Rename .java to .kt --- .../{EHentaiImageMetadata.java => EHentaiImageMetadata.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/main/java/me/devsaki/hentoid/json/sources/{EHentaiImageMetadata.java => EHentaiImageMetadata.kt} (100%) diff --git a/app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiImageMetadata.java b/app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiImageMetadata.kt similarity index 100% rename from app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiImageMetadata.java rename to app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiImageMetadata.kt From a5887015468d28ad67b638fe453612488295fe0a Mon Sep 17 00:00:00 2001 From: Robb <35880555+robbwatershed@users.noreply.github.com> Date: Sat, 26 Oct 2024 16:36:33 +0200 Subject: [PATCH 30/44] EHentaiImageMetadata -> Kt --- .../json/sources/EHentaiImageMetadata.kt | 19 +++++++++---------- .../hentoid/parsers/images/EHentaiParser.kt | 2 +- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiImageMetadata.kt b/app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiImageMetadata.kt index 676dfc9a6..b7695e120 100644 --- a/app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiImageMetadata.kt +++ b/app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiImageMetadata.kt @@ -1,12 +1,11 @@ -package me.devsaki.hentoid.json.sources; +package me.devsaki.hentoid.json.sources -@SuppressWarnings({"unused, MismatchedQueryAndUpdateOfCollection", "squid:S1172", "squid:S1068"}) -public class EHentaiImageMetadata { - private String n; - private String k; - private String t; - - public String getKey() { - return k; +data class EHentaiImageMetadata( + val n: String, + val k: String, + val t: String +) { + fun getKey(): String { + return k } -} +} \ No newline at end of file diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/images/EHentaiParser.kt b/app/src/main/java/me/devsaki/hentoid/parsers/images/EHentaiParser.kt index 404af4d33..a96c07187 100644 --- a/app/src/main/java/me/devsaki/hentoid/parsers/images/EHentaiParser.kt +++ b/app/src/main/java/me/devsaki/hentoid/parsers/images/EHentaiParser.kt @@ -230,7 +230,7 @@ class EHentaiParser : ImageListParser { ): EHentaiImageResponse { val query = EHentaiImageQuery( imageInfo.gid, - imageInfo.image.key, + imageInfo.image.getKey(), imageInfo.mpvkey, imageInfo.pageNum ) From 14f2a50b16bdc4aa8b759b3cfe907c430f92be89 Mon Sep 17 00:00:00 2001 From: Robb <35880555+robbwatershed@users.noreply.github.com> Date: Sat, 26 Oct 2024 16:42:36 +0200 Subject: [PATCH 31/44] Rename .java to .kt --- .../sources/{EHentaiGalleryQuery.java => EHentaiGalleryQuery.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/main/java/me/devsaki/hentoid/json/sources/{EHentaiGalleryQuery.java => EHentaiGalleryQuery.kt} (100%) diff --git a/app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiGalleryQuery.java b/app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiGalleryQuery.kt similarity index 100% rename from app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiGalleryQuery.java rename to app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiGalleryQuery.kt From 5d774223155a73b491d4f85a18c0b293a0f57988 Mon Sep 17 00:00:00 2001 From: Robb <35880555+robbwatershed@users.noreply.github.com> Date: Sat, 26 Oct 2024 16:42:37 +0200 Subject: [PATCH 32/44] EHentaiGalleryQuery -> Kt --- .../json/sources/EHentaiGalleryQuery.kt | 26 +++++++------------ 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiGalleryQuery.kt b/app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiGalleryQuery.kt index 92fcd06a1..27aa301a6 100644 --- a/app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiGalleryQuery.kt +++ b/app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiGalleryQuery.kt @@ -1,19 +1,11 @@ -package me.devsaki.hentoid.json.sources; +package me.devsaki.hentoid.json.sources -import java.util.ArrayList; -import java.util.List; - -@SuppressWarnings({"unused, MismatchedQueryAndUpdateOfCollection", "squid:S1172", "squid:S1068"}) -public class EHentaiGalleryQuery { - private String method = "gdata"; - private final List> gidlist; - private String namespace = "1"; - - public EHentaiGalleryQuery(String galleryId, String galleryKey) { - gidlist = new ArrayList<>(); - List galleryIds = new ArrayList<>(); - galleryIds.add(galleryId); - galleryIds.add(galleryKey); - gidlist.add(galleryIds); - } +data class EHentaiGalleryQuery( + val gidlist: List> = ArrayList(), + val method: String = "gdata", + val namespace: String = "1" +) { + constructor(galleryId: String, galleryKey: String) : this( + listOf(listOf(galleryId, galleryKey)) + ) } From 299f5ea0cec75ee584666a39bec4123792675823 Mon Sep 17 00:00:00 2001 From: Robb <35880555+robbwatershed@users.noreply.github.com> Date: Sat, 26 Oct 2024 16:51:15 +0200 Subject: [PATCH 33/44] Prefs -> Settings --- .../activities/DuplicateDetectorActivity.kt | 3 +- .../hentoid/activities/LibraryActivity.kt | 8 +- .../hentoid/activities/QueueActivity.kt | 3 +- .../hentoid/activities/ReaderActivity.kt | 3 +- .../activities/StoragePreferenceActivity.kt | 33 ++-- .../hentoid/activities/UnlockActivity.kt | 5 +- .../activities/sources/BaseWebActivity.kt | 2 +- .../activities/sources/ExHentaiActivity.kt | 3 +- .../me/devsaki/hentoid/core/AppStartup.kt | 6 +- .../devsaki/hentoid/database/ObjectBoxDAO.kt | 2 +- .../devsaki/hentoid/database/ObjectBoxDB.kt | 4 +- .../fragments/intro/ImportIntroFragment.kt | 5 +- .../library/LibraryContentFragment.kt | 36 ++--- .../library/LibraryGroupsFragment.kt | 8 +- .../fragments/library/MergeDialogFragment.kt | 6 +- .../pin/ActivatePinDialogFragment.kt | 5 +- .../pin/DeactivatePinDialogFragment.kt | 5 +- .../fragments/pin/LockPreferenceFragment.kt | 2 +- .../fragments/pin/ResetPinDialogFragment.kt | 6 +- .../fragments/pin/UnlockPinDialogFragment.kt | 4 +- .../DownloadStrategyDialogFragment.kt | 20 ++- .../preferences/LibRefreshDialogFragment.kt | 11 +- .../preferences/PreferencesFragment.kt | 10 +- .../preferences/StorageUsageDialogFragment.kt | 3 +- .../fragments/tools/LogsDialogFragment.kt | 4 +- .../tools/MassOperationsDialogFragment.kt | 8 +- .../me/devsaki/hentoid/util/ContentHelper.kt | 24 +-- .../me/devsaki/hentoid/util/GroupHelper.kt | 2 +- .../java/me/devsaki/hentoid/util/Helper.kt | 4 +- .../me/devsaki/hentoid/util/ImportHelper.kt | 26 +-- .../java/me/devsaki/hentoid/util/LogHelper.kt | 2 +- .../me/devsaki/hentoid/util/Preferences.java | 150 +----------------- .../java/me/devsaki/hentoid/util/Settings.kt | 109 ++++++++++--- .../hentoid/util/download/DownloadHelper.kt | 14 +- .../hentoid/util/file/StorageHelper.kt | 6 +- .../hentoid/viewholders/ContentItem.kt | 3 +- .../hentoid/viewholders/DuplicateItem.kt | 4 +- .../viewmodels/PreferencesViewModel.kt | 4 +- .../hentoid/workers/ExternalImportWorker.kt | 10 +- .../hentoid/workers/MetadataImportWorker.kt | 6 +- .../hentoid/workers/PrimaryImportWorker.kt | 5 +- 41 files changed, 242 insertions(+), 332 deletions(-) diff --git a/app/src/main/java/me/devsaki/hentoid/activities/DuplicateDetectorActivity.kt b/app/src/main/java/me/devsaki/hentoid/activities/DuplicateDetectorActivity.kt index 83fc5dd3e..24966f775 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/DuplicateDetectorActivity.kt +++ b/app/src/main/java/me/devsaki/hentoid/activities/DuplicateDetectorActivity.kt @@ -17,6 +17,7 @@ import me.devsaki.hentoid.events.CommunicationEvent import me.devsaki.hentoid.fragments.tools.DuplicateDetailsFragment import me.devsaki.hentoid.fragments.tools.DuplicateMainFragment import me.devsaki.hentoid.util.Preferences +import me.devsaki.hentoid.util.Settings import me.devsaki.hentoid.util.applyTheme import me.devsaki.hentoid.viewmodels.DuplicateViewModel import me.devsaki.hentoid.viewmodels.ViewModelFactory @@ -44,7 +45,7 @@ class DuplicateDetectorActivity : BaseActivity() { val vmFactory = ViewModelFactory(application) viewModel = ViewModelProvider(this, vmFactory)[DuplicateViewModel::class.java] - if (!Preferences.getRecentVisibility()) { + if (!Settings.recentVisibility) { window.setFlags( WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE diff --git a/app/src/main/java/me/devsaki/hentoid/activities/LibraryActivity.kt b/app/src/main/java/me/devsaki/hentoid/activities/LibraryActivity.kt index cf407d7f0..11889f90b 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/LibraryActivity.kt +++ b/app/src/main/java/me/devsaki/hentoid/activities/LibraryActivity.kt @@ -312,7 +312,7 @@ class LibraryActivity : BaseActivity(), LibraryArchiveDialogFragment.Parent { searchRecords.addAll(records) } - if (!Preferences.getRecentVisibility()) { + if (!Settings.recentVisibility) { window.setFlags( WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE @@ -461,7 +461,7 @@ class LibraryActivity : BaseActivity(), LibraryArchiveDialogFragment.Parent { } private fun considerRefreshExtLib() { - if (Preferences.getExternalLibraryUri().isNullOrEmpty()) return + if (Settings.externalLibraryUri.isEmpty()) return runExternalImport(this, true) } @@ -889,7 +889,7 @@ class LibraryActivity : BaseActivity(), LibraryArchiveDialogFragment.Parent { hasChangedDisplaySettings = true } - Preferences.Key.PRIMARY_STORAGE_URI, Preferences.Key.EXTERNAL_LIBRARY_URI -> { + Settings.Key.PRIMARY_STORAGE_URI, Settings.Key.EXTERNAL_LIBRARY_URI -> { updateDisplay(Grouping.FLAT.id) viewModel.setGrouping(Grouping.FLAT.id) } @@ -1115,7 +1115,7 @@ class LibraryActivity : BaseActivity(), LibraryArchiveDialogFragment.Parent { } else { // Flat view editMenu?.isVisible = !hasProcessed deleteMenu?.isVisible = - !hasProcessed && ((selectedLocalCount > 0 || selectedStreamedCount > 0) && 0L == selectedExternalCount || selectedExternalCount > 0 && Preferences.isDeleteExternalLibrary()) + !hasProcessed && ((selectedLocalCount > 0 || selectedStreamedCount > 0) && 0L == selectedExternalCount || selectedExternalCount > 0 && Settings.isDeleteExternalLibrary) completedMenu?.isVisible = true resetReadStatsMenu?.isVisible = true rateMenu?.isVisible = isMultipleSelection diff --git a/app/src/main/java/me/devsaki/hentoid/activities/QueueActivity.kt b/app/src/main/java/me/devsaki/hentoid/activities/QueueActivity.kt index 907e04583..775d09dac 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/QueueActivity.kt +++ b/app/src/main/java/me/devsaki/hentoid/activities/QueueActivity.kt @@ -42,6 +42,7 @@ import me.devsaki.hentoid.fragments.queue.QueueFragment import me.devsaki.hentoid.util.Debouncer import me.devsaki.hentoid.util.Preferences import me.devsaki.hentoid.util.QueuePosition +import me.devsaki.hentoid.util.Settings import me.devsaki.hentoid.util.applyTheme import me.devsaki.hentoid.util.network.CloudflareHelper import me.devsaki.hentoid.util.network.WebkitPackageHelper @@ -136,7 +137,7 @@ class QueueActivity : BaseActivity(), SelectSiteDialogFragment.Parent { viewModel.getQueue().observe(this) { onQueueChanged(it) } viewModel.getErrors().observe(this) { onErrorsChanged(it) } - if (!Preferences.getRecentVisibility()) { + if (!Settings.recentVisibility) { window.setFlags( WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE diff --git a/app/src/main/java/me/devsaki/hentoid/activities/ReaderActivity.kt b/app/src/main/java/me/devsaki/hentoid/activities/ReaderActivity.kt index d70e133a5..447b28943 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/ReaderActivity.kt +++ b/app/src/main/java/me/devsaki/hentoid/activities/ReaderActivity.kt @@ -12,6 +12,7 @@ import me.devsaki.hentoid.activities.bundles.ReaderActivityBundle import me.devsaki.hentoid.fragments.reader.ReaderGalleryFragment import me.devsaki.hentoid.fragments.reader.ReaderPagerFragment import me.devsaki.hentoid.util.Preferences +import me.devsaki.hentoid.util.Settings import me.devsaki.hentoid.util.file.RQST_STORAGE_PERMISSION import me.devsaki.hentoid.util.file.requestExternalStorageReadPermission import me.devsaki.hentoid.util.toast @@ -73,7 +74,7 @@ open class ReaderActivity : BaseActivity() { .add(android.R.id.content, fragment) .commit() } - if (!Preferences.getRecentVisibility()) window.setFlags( + if (!Settings.recentVisibility) window.setFlags( WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE ) diff --git a/app/src/main/java/me/devsaki/hentoid/activities/StoragePreferenceActivity.kt b/app/src/main/java/me/devsaki/hentoid/activities/StoragePreferenceActivity.kt index af2d6b5fc..957fc4030 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/StoragePreferenceActivity.kt +++ b/app/src/main/java/me/devsaki/hentoid/activities/StoragePreferenceActivity.kt @@ -30,7 +30,6 @@ import me.devsaki.hentoid.fragments.ProgressDialogFragment import me.devsaki.hentoid.fragments.preferences.DownloadStrategyDialogFragment import me.devsaki.hentoid.fragments.preferences.LibRefreshDialogFragment import me.devsaki.hentoid.fragments.preferences.StorageUsageDialogFragment -import me.devsaki.hentoid.util.Preferences import me.devsaki.hentoid.util.Settings import me.devsaki.hentoid.util.applyTheme import me.devsaki.hentoid.util.dimensAsDp @@ -129,11 +128,11 @@ class StoragePreferenceActivity : BaseActivity(), DownloadStrategyDialogFragment getPrefsIndex( resources, R.array.pref_memory_alert_values, - Preferences.getMemoryAlertThreshold().toString() + Settings.memoryAlertThreshold.toString() ) ) { dialog, which -> val array = resources.getStringArray(R.array.pref_memory_alert_values) - Preferences.setMemoryAlertThreshold(array[which].toInt()) + Settings.memoryAlertThreshold = array[which].toInt() refreshDisplay() dialog.dismiss() } @@ -157,27 +156,27 @@ class StoragePreferenceActivity : BaseActivity(), DownloadStrategyDialogFragment browseModeImg.isVisible = Settings.isBrowserMode primaryVolume1.isVisible = - Preferences.getStorageUri(StorageLocation.PRIMARY_1).isNotEmpty() + Settings.getStorageUri(StorageLocation.PRIMARY_1).isNotEmpty() if (primaryVolume1.isVisible) { if (null == binding1) binding1 = IncludePrefsStorageVolumeBinding.bind(primaryVolume1) bindLocation( binding1, StorageLocation.PRIMARY_1, - Preferences.getStorageUri(StorageLocation.PRIMARY_1) + Settings.getStorageUri(StorageLocation.PRIMARY_1) ) } addPrimary1.isVisible = !primaryVolume1.isVisible primaryVolume2.isVisible = - Preferences.getStorageUri(StorageLocation.PRIMARY_2).isNotEmpty() + Settings.getStorageUri(StorageLocation.PRIMARY_2).isNotEmpty() if (primaryVolume2.isVisible) { if (null == binding2) binding2 = IncludePrefsStorageVolumeBinding.bind(primaryVolume2) bindLocation( binding2, StorageLocation.PRIMARY_2, - Preferences.getStorageUri(StorageLocation.PRIMARY_2) + Settings.getStorageUri(StorageLocation.PRIMARY_2) ) } addPrimary2.isVisible = primaryVolume1.isVisible && !primaryVolume2.isVisible @@ -187,32 +186,32 @@ class StoragePreferenceActivity : BaseActivity(), DownloadStrategyDialogFragment alertDesc.text = textArray[getPrefsIndex( resources, R.array.pref_memory_alert_values, - Preferences.getMemoryAlertThreshold().toString() + Settings.memoryAlertThreshold.toString() )] strategyPanel.isVisible = (primaryVolume1.isVisible && primaryVolume2.isVisible) textArray = resources.getStringArray(R.array.pref_storage_strategy_name) strategyTitle.text = resources.getString( R.string.storage_strategy_title, - textArray[Preferences.getStorageDownloadStrategy()] + textArray[Settings.storageDownloadStrategy] ) textArray = resources.getStringArray(R.array.pref_storage_strategy_desc) strategyDesc.text = String.format( - textArray[Preferences.getStorageDownloadStrategy()], - Preferences.getStorageSwitchThresholdPc() + textArray[Settings.storageDownloadStrategy], + Settings.storageSwitchThresholdPc ) - externalVolume.isVisible = Preferences.getExternalLibraryUri().isNotEmpty() + externalVolume.isVisible = Settings.externalLibraryUri.isNotEmpty() if (externalVolume.isVisible) { if (null == bindingExt) bindingExt = IncludePrefsStorageVolumeBinding.bind(externalVolume) bindLocation( bindingExt, StorageLocation.EXTERNAL, - Preferences.getExternalLibraryUri() + Settings.externalLibraryUri ) } - addExternal.isVisible = Preferences.getExternalLibraryUri().isEmpty() + addExternal.isVisible = Settings.externalLibraryUri.isEmpty() } } @@ -356,7 +355,7 @@ class StoragePreferenceActivity : BaseActivity(), DownloadStrategyDialogFragment val folder = getDocumentFromTreeUriString( this, - Preferences.getStorageUri(location) + Settings.getStorageUri(location) ) if (folder != null) openFile(this, folder) } @@ -369,7 +368,7 @@ class StoragePreferenceActivity : BaseActivity(), DownloadStrategyDialogFragment private fun onDetachSelected(location: StorageLocation) { if (StorageLocation.PRIMARY_1 == location - && Preferences.getStorageUri(StorageLocation.PRIMARY_2).isNotBlank() + && Settings.getStorageUri(StorageLocation.PRIMARY_2).isNotBlank() ) { Snackbar.make( findViewById(android.R.id.content), @@ -416,7 +415,7 @@ class StoragePreferenceActivity : BaseActivity(), DownloadStrategyDialogFragment } var location1free = -1L - val root1 = Preferences.getStorageUri(StorageLocation.PRIMARY_1) + val root1 = Settings.getStorageUri(StorageLocation.PRIMARY_1) if (root1.isNotEmpty()) { val root1Folder = getDocumentFromTreeUriString(this, root1) if (root1Folder != null) { diff --git a/app/src/main/java/me/devsaki/hentoid/activities/UnlockActivity.kt b/app/src/main/java/me/devsaki/hentoid/activities/UnlockActivity.kt index 11451e3c0..e866a8b56 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/UnlockActivity.kt +++ b/app/src/main/java/me/devsaki/hentoid/activities/UnlockActivity.kt @@ -21,7 +21,6 @@ import me.devsaki.hentoid.core.startBiometric import me.devsaki.hentoid.database.domains.Content import me.devsaki.hentoid.enums.Site import me.devsaki.hentoid.fragments.pin.UnlockPinDialogFragment -import me.devsaki.hentoid.util.Preferences import me.devsaki.hentoid.util.Settings import me.devsaki.hentoid.util.applyTheme import me.devsaki.hentoid.util.pause @@ -34,9 +33,7 @@ class UnlockActivity : AppCompatActivity(), UnlockPinDialogFragment.Parent { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) applyTheme() - if (Preferences.getAppLockPin().length != 4) { - Preferences.setAppLockPin("") - } + if (Settings.appLockPin.length != 4) Settings.appLockPin = "" if (0 == Settings.lockType || isUnlocked()) { goToNextActivity() return diff --git a/app/src/main/java/me/devsaki/hentoid/activities/sources/BaseWebActivity.kt b/app/src/main/java/me/devsaki/hentoid/activities/sources/BaseWebActivity.kt index ad5cf118b..2c40ca991 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/sources/BaseWebActivity.kt +++ b/app/src/main/java/me/devsaki/hentoid/activities/sources/BaseWebActivity.kt @@ -277,7 +277,7 @@ abstract class BaseWebActivity : BaseActivity(), CustomWebViewClient.CustomWebAc initWebview() initSwipeLayout() webView.loadUrl(getStartUrl()) - if (!Preferences.getRecentVisibility()) { + if (!Settings.recentVisibility) { window.setFlags( WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE diff --git a/app/src/main/java/me/devsaki/hentoid/activities/sources/ExHentaiActivity.kt b/app/src/main/java/me/devsaki/hentoid/activities/sources/ExHentaiActivity.kt index 6c81372f5..61aee158d 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/sources/ExHentaiActivity.kt +++ b/app/src/main/java/me/devsaki/hentoid/activities/sources/ExHentaiActivity.kt @@ -17,6 +17,7 @@ import me.devsaki.hentoid.parsers.content.ExhentaiContent import me.devsaki.hentoid.parsers.images.EHentaiParser import me.devsaki.hentoid.parsers.images.EHentaiParser.EhAuthState import me.devsaki.hentoid.util.Preferences +import me.devsaki.hentoid.util.Settings import me.devsaki.hentoid.util.file.findOrCreateDocumentFile import me.devsaki.hentoid.util.file.getDocumentFromTreeUriString import me.devsaki.hentoid.util.file.saveBinary @@ -74,7 +75,7 @@ class ExHentaiActivity : BaseWebActivity() { try { val root = getDocumentFromTreeUriString( application, - Preferences.getStorageUri(StorageLocation.PRIMARY_1) + Settings.getStorageUri(StorageLocation.PRIMARY_1) ) if (root != null) { val cookiesLog = findOrCreateDocumentFile( diff --git a/app/src/main/java/me/devsaki/hentoid/core/AppStartup.kt b/app/src/main/java/me/devsaki/hentoid/core/AppStartup.kt index 7340b869f..61c1cf2ad 100644 --- a/app/src/main/java/me/devsaki/hentoid/core/AppStartup.kt +++ b/app/src/main/java/me/devsaki/hentoid/core/AppStartup.kt @@ -235,10 +235,10 @@ object AppStartup { "color_theme", Preferences.getColorTheme().toString() ) FirebaseAnalytics.getInstance(context).setUserProperty( - "endless", Preferences.getEndlessScroll().toString() + "endless", Settings.endlessScroll.toString() ) FirebaseCrashlytics.getInstance().setCustomKey( - "Library display mode", if (Preferences.getEndlessScroll()) "endless" else "paged" + "Library display mode", if (Settings.endlessScroll) "endless" else "paged" ) } catch (e: IllegalStateException) { // Happens during unit tests Timber.e(e, "fail@init Crashlytics") @@ -250,7 +250,7 @@ object AppStartup { private fun createBookmarksJson(context: Context, emitter: (Float) -> Unit) { Timber.i("Create bookmarks JSON : start") val appRoot = getDocumentFromTreeUriString( - context, Preferences.getStorageUri(StorageLocation.PRIMARY_1) + context, Settings.getStorageUri(StorageLocation.PRIMARY_1) ) if (appRoot != null) { val bookmarksJson = findFile(context, appRoot, BOOKMARKS_JSON_FILE_NAME) diff --git a/app/src/main/java/me/devsaki/hentoid/database/ObjectBoxDAO.kt b/app/src/main/java/me/devsaki/hentoid/database/ObjectBoxDAO.kt index 6e578402e..09116b49e 100644 --- a/app/src/main/java/me/devsaki/hentoid/database/ObjectBoxDAO.kt +++ b/app/src/main/java/me/devsaki/hentoid/database/ObjectBoxDAO.kt @@ -254,7 +254,7 @@ class ObjectBoxDAO : CollectionDAO { searchBundle, metadata ) else getPagedContentByQuery(isUniversal, searchBundle, metadata) - val nbPages = Preferences.getContentPageQuantity() + val nbPages = Settings.contentPageQuantity var initialLoad = nbPages * 3 if (searchBundle.loadAll) { // Trump Android's algorithm by setting a number of pages higher that the actual number of results diff --git a/app/src/main/java/me/devsaki/hentoid/database/ObjectBoxDB.kt b/app/src/main/java/me/devsaki/hentoid/database/ObjectBoxDB.kt index 61e6d5108..0a080f1ed 100644 --- a/app/src/main/java/me/devsaki/hentoid/database/ObjectBoxDB.kt +++ b/app/src/main/java/me/devsaki/hentoid/database/ObjectBoxDB.kt @@ -1418,7 +1418,7 @@ object ObjectBoxDB { return when (location) { Location.PRIMARY -> qc.and(Content_.status.notEqual(StatusContent.EXTERNAL.code)) Location.PRIMARY_1 -> { - var root = Preferences.getStorageUri(StorageLocation.PRIMARY_1) + var root = Settings.getStorageUri(StorageLocation.PRIMARY_1) if (root.isEmpty()) root = "FAIL" // Auto-fails condition qc.and( Content_.storageUri.startsWith( @@ -1429,7 +1429,7 @@ object ObjectBoxDB { } Location.PRIMARY_2 -> { - var root = Preferences.getStorageUri(StorageLocation.PRIMARY_2) + var root = Settings.getStorageUri(StorageLocation.PRIMARY_2) if (root.isEmpty()) root = "FAIL" // Auto-fails condition qc.and( Content_.storageUri.startsWith( diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/intro/ImportIntroFragment.kt b/app/src/main/java/me/devsaki/hentoid/fragments/intro/ImportIntroFragment.kt index dd9329ce2..c0f84193a 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/intro/ImportIntroFragment.kt +++ b/app/src/main/java/me/devsaki/hentoid/fragments/intro/ImportIntroFragment.kt @@ -25,7 +25,6 @@ import me.devsaki.hentoid.events.ProcessEvent import me.devsaki.hentoid.ui.BlinkAnimation import me.devsaki.hentoid.util.PickFolderContract import me.devsaki.hentoid.util.PickerResult -import me.devsaki.hentoid.util.Preferences import me.devsaki.hentoid.util.ProcessFolderResult import me.devsaki.hentoid.util.Settings import me.devsaki.hentoid.util.file.getFullPathFromUri @@ -77,7 +76,7 @@ class ImportIntroFragment : Fragment(R.layout.intro_slide_04) { */ fun reset() { if (!isDone) return - Preferences.setStorageUri(StorageLocation.PRIMARY_1, "") + Settings.setStorageUri(StorageLocation.PRIMARY_1, "") mergedBinding?.apply { importStep1Button.visibility = View.VISIBLE @@ -234,7 +233,7 @@ class ImportIntroFragment : Fragment(R.layout.intro_slide_04) { importStep1Button.visibility = View.INVISIBLE importStep1Folder.text = getFullPathFromUri( requireContext(), - Uri.parse(Preferences.getStorageUri(StorageLocation.PRIMARY_1)) + Uri.parse(Settings.getStorageUri(StorageLocation.PRIMARY_1)) ) importStep1Check.visibility = View.VISIBLE importStep2.visibility = View.VISIBLE diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/library/LibraryContentFragment.kt b/app/src/main/java/me/devsaki/hentoid/fragments/library/LibraryContentFragment.kt index 48fd1070c..be46de682 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/library/LibraryContentFragment.kt +++ b/app/src/main/java/me/devsaki/hentoid/fragments/library/LibraryContentFragment.kt @@ -425,7 +425,7 @@ class LibraryContentFragment : Fragment(), ChangeGroupDialogFragment.Parent, // Hide FAB when scrolling up binding?.topFab?.apply { scrollListener.setDeltaYListener(lifecycleScope) { i: Int -> - isVisible = (Preferences.isTopFabEnabled() && i > 0) + isVisible = (Settings.topFabEnabled && i > 0) } // Top FAB @@ -433,7 +433,7 @@ class LibraryContentFragment : Fragment(), ChangeGroupDialogFragment.Parent, llm?.scrollToPositionWithOffset(0, 0) } setOnLongClickListener { - Preferences.setTopFabEnabled(false) + Settings.topFabEnabled = false visibility = View.GONE true } @@ -458,7 +458,7 @@ class LibraryContentFragment : Fragment(), ChangeGroupDialogFragment.Parent, // Pager pager.initUI(rootView) - setPagingMethod(Preferences.getEndlessScroll(), false) + setPagingMethod(Settings.endlessScroll, false) addCustomBackControl() } @@ -660,10 +660,10 @@ class LibraryContentFragment : Fragment(), ChangeGroupDialogFragment.Parent, private fun deleteSelectedItems() { val selectedItems: Set = selectExtension!!.selectedItems if (selectedItems.isNotEmpty()) { - var selectedContent = selectedItems.mapNotNull { ci -> ci.content } + var selectedContent = selectedItems.mapNotNull { it.content } // Remove external items if they can't be deleted - if (!Preferences.isDeleteExternalLibrary()) selectedContent = selectedContent - .filterNot { c: Content? -> c!!.status == StatusContent.EXTERNAL } + if (!Settings.isDeleteExternalLibrary) selectedContent = selectedContent + .filterNot { it.status == StatusContent.EXTERNAL } if (selectedContent.isNotEmpty()) activity.get()!!.askDeleteItems( selectedContent, emptyList(), { refreshIfNeeded() }, selectExtension!! @@ -1004,7 +1004,7 @@ class LibraryContentFragment : Fragment(), ChangeGroupDialogFragment.Parent, CommunicationEvent.Type.DISABLE -> onDisable() CommunicationEvent.Type.UNSELECT -> leaveSelectionMode() CommunicationEvent.Type.UPDATE_EDIT_MODE -> setPagingMethod( - Preferences.getEndlessScroll(), activity.get()!!.isEditMode() + Settings.endlessScroll, activity.get()!!.isEditMode() ) CommunicationEvent.Type.SCROLL_TOP -> { @@ -1065,16 +1065,16 @@ class LibraryContentFragment : Fragment(), ChangeGroupDialogFragment.Parent, Timber.v("Prefs change detected : %s", key) when (key) { - Preferences.Key.TOP_FAB -> - binding?.topFab?.isVisible = Preferences.isTopFabEnabled() + Settings.Key.TOP_FAB -> + binding?.topFab?.isVisible = Settings.topFabEnabled - Preferences.Key.ENDLESS_SCROLL -> { + Settings.Key.ENDLESS_SCROLL -> { setPagingMethod( - Preferences.getEndlessScroll(), activity.get()!!.isEditMode() + Settings.endlessScroll, activity.get()!!.isEditMode() ) FirebaseCrashlytics.getInstance().setCustomKey( "Library display mode", - if (Preferences.getEndlessScroll()) "endless" else "paged" + if (Settings.endlessScroll) "endless" else "paged" ) viewModel.searchContent() // Trigger a blank search } @@ -1344,8 +1344,8 @@ class LibraryContentFragment : Fragment(), ChangeGroupDialogFragment.Parent, * @return Min and max index of the books to display on the given page */ private fun getShelfBound(shelfNumber: Int, librarySize: Int): Pair { - val minIndex = (shelfNumber - 1) * Preferences.getContentPageQuantity() - val maxIndex = min(minIndex + Preferences.getContentPageQuantity(), librarySize) + val minIndex = (shelfNumber - 1) * Settings.contentPageQuantity + val maxIndex = min(minIndex + Settings.contentPageQuantity, librarySize) return Pair(minIndex, maxIndex) } @@ -1384,7 +1384,7 @@ class LibraryContentFragment : Fragment(), ChangeGroupDialogFragment.Parent, * @param shelfNumber Number of the shelf to display */ private fun populateBookshelf(iLibrary: PagedList, shelfNumber: Int) { - if (Preferences.getEndlessScroll()) return + if (Settings.endlessScroll) return val bounds = getShelfBound(shelfNumber, iLibrary.size) val minIndex = bounds.first @@ -1516,7 +1516,7 @@ class LibraryContentFragment : Fragment(), ChangeGroupDialogFragment.Parent, if (newSearch) topItemPosition = 0 // Update displayed books - if (Preferences.getEndlessScroll() && !activity.get()!! + if (Settings.endlessScroll && !activity.get()!! .isEditMode() && pagedItemAdapter != null ) { pagedItemAdapter?.submitList(result) { differEndCallback() } @@ -1525,7 +1525,7 @@ class LibraryContentFragment : Fragment(), ChangeGroupDialogFragment.Parent, } else { // Paged mode if (newSearch) pager.setCurrentPage(1) pager.setPageCount( - ceil(result.size * 1.0 / Preferences.getContentPageQuantity()).toInt() + ceil(result.size * 1.0 / Settings.contentPageQuantity).toInt() ) loadBookshelf(result) } @@ -1899,7 +1899,7 @@ class LibraryContentFragment : Fragment(), ChangeGroupDialogFragment.Parent, * @param scrollPosition New 0-based scroll position */ private fun onScrollPositionChange(scrollPosition: Int) { - if (Preferences.isTopFabEnabled()) { + if (Settings.topFabEnabled) { binding?.topFab?.isVisible = (scrollPosition > 2) } } diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/library/LibraryGroupsFragment.kt b/app/src/main/java/me/devsaki/hentoid/fragments/library/LibraryGroupsFragment.kt index 1a19259c1..58ead4268 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/library/LibraryGroupsFragment.kt +++ b/app/src/main/java/me/devsaki/hentoid/fragments/library/LibraryGroupsFragment.kt @@ -371,15 +371,15 @@ class LibraryGroupsFragment : Fragment(), private fun deleteSelectedItems() { val selectedItems: Set = selectExtension!!.selectedItems if (selectedItems.isNotEmpty()) { - var selectedGroups = selectedItems.map { gi -> gi.group }.toMutableList() - val selectedContentLists = selectedGroups.map { g -> viewModel.getGroupContents(g) } + var selectedGroups = selectedItems.map { it.group }.toMutableList() + val selectedContentLists = selectedGroups.map { viewModel.getGroupContents(it) } var selectedContent: MutableList = ArrayList() for (list in selectedContentLists) selectedContent.addAll(list) // Remove external items if they can't be deleted - if (!Preferences.isDeleteExternalLibrary()) { + if (!Settings.isDeleteExternalLibrary) { val contentToDelete = - selectedContent.filterNot { c -> c.status == StatusContent.EXTERNAL } + selectedContent.filterNot { it.status == StatusContent.EXTERNAL } val diff = selectedContent.size - contentToDelete.size // Remove undeletable books from the list if (diff > 0) { diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/library/MergeDialogFragment.kt b/app/src/main/java/me/devsaki/hentoid/fragments/library/MergeDialogFragment.kt index f7912df56..105adfc40 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/library/MergeDialogFragment.kt +++ b/app/src/main/java/me/devsaki/hentoid/fragments/library/MergeDialogFragment.kt @@ -21,7 +21,7 @@ import me.devsaki.hentoid.databinding.DialogLibraryMergeBinding import me.devsaki.hentoid.enums.StatusContent import me.devsaki.hentoid.fragments.BaseDialogFragment import me.devsaki.hentoid.util.InnerNameNumberContentComparator -import me.devsaki.hentoid.util.Preferences +import me.devsaki.hentoid.util.Settings import me.devsaki.hentoid.viewholders.ContentItem import me.devsaki.hentoid.viewholders.IDraggableViewHolder @@ -120,8 +120,8 @@ class MergeDialogFragment : BaseDialogFragment(), It list.adapter = fastAdapter if (isExternal) { - mergeDeleteSwitch.isEnabled = Preferences.isDeleteExternalLibrary() - mergeDeleteSwitch.isChecked = Preferences.isDeleteExternalLibrary() && deleteDefault + mergeDeleteSwitch.isEnabled = Settings.isDeleteExternalLibrary + mergeDeleteSwitch.isChecked = Settings.isDeleteExternalLibrary && deleteDefault } else { mergeDeleteSwitch.isEnabled = true mergeDeleteSwitch.isChecked = deleteDefault diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/pin/ActivatePinDialogFragment.kt b/app/src/main/java/me/devsaki/hentoid/fragments/pin/ActivatePinDialogFragment.kt index f92407699..c72facc24 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/pin/ActivatePinDialogFragment.kt +++ b/app/src/main/java/me/devsaki/hentoid/fragments/pin/ActivatePinDialogFragment.kt @@ -5,7 +5,6 @@ import android.content.DialogInterface import android.os.Bundle import android.view.View import me.devsaki.hentoid.R -import me.devsaki.hentoid.util.Preferences import me.devsaki.hentoid.util.Settings class ActivatePinDialogFragment : PinDialogFragment() { @@ -30,12 +29,14 @@ class ActivatePinDialogFragment : PinDialogFragment() { clearPin() setHeaderText(R.string.pin_new_confirm) } + pin -> { - Preferences.setAppLockPin(proposedPin) + Settings.appLockPin = pin Settings.lockType = 1 dismiss() parent?.onPinActivateSuccess() } + else -> { proposedPin = null setHeaderText(R.string.pin_new) diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/pin/DeactivatePinDialogFragment.kt b/app/src/main/java/me/devsaki/hentoid/fragments/pin/DeactivatePinDialogFragment.kt index 4a56b8d0e..b08c4649b 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/pin/DeactivatePinDialogFragment.kt +++ b/app/src/main/java/me/devsaki/hentoid/fragments/pin/DeactivatePinDialogFragment.kt @@ -5,7 +5,6 @@ import android.content.DialogInterface import android.os.Bundle import android.view.View import me.devsaki.hentoid.R -import me.devsaki.hentoid.util.Preferences import me.devsaki.hentoid.util.Settings class DeactivatePinDialogFragment : PinDialogFragment() { @@ -21,9 +20,9 @@ class DeactivatePinDialogFragment : PinDialogFragment() { } override fun onPinAccept(pin: String) { - if (Preferences.getAppLockPin() == pin) { + if (Settings.appLockPin == pin) { Settings.lockType = 0 - Preferences.setAppLockPin("") + Settings.appLockPin = "" dismiss() parent?.onPinDeactivateSuccess() } else { diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/pin/LockPreferenceFragment.kt b/app/src/main/java/me/devsaki/hentoid/fragments/pin/LockPreferenceFragment.kt index 7f88d7665..801965ec9 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/pin/LockPreferenceFragment.kt +++ b/app/src/main/java/me/devsaki/hentoid/fragments/pin/LockPreferenceFragment.kt @@ -108,7 +108,7 @@ class LockPreferenceFragment : Fragment(), DeactivatePinDialogFragment.Parent, private fun onBiometricsActivateSuccess(result: Boolean) { if (result) { Settings.lockType = 2 - Preferences.setAppLockPin("") + Settings.appLockPin = "" } refresh() } diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/pin/ResetPinDialogFragment.kt b/app/src/main/java/me/devsaki/hentoid/fragments/pin/ResetPinDialogFragment.kt index 17ea65c7e..a94d802ed 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/pin/ResetPinDialogFragment.kt +++ b/app/src/main/java/me/devsaki/hentoid/fragments/pin/ResetPinDialogFragment.kt @@ -4,7 +4,7 @@ import android.content.Context import android.os.Bundle import android.view.View import me.devsaki.hentoid.R -import me.devsaki.hentoid.util.Preferences +import me.devsaki.hentoid.util.Settings import java.security.InvalidParameterException class ResetPinDialogFragment : PinDialogFragment() { @@ -34,7 +34,7 @@ class ResetPinDialogFragment : PinDialogFragment() { } private fun step0(pin: String) { - if (Preferences.getAppLockPin() == pin) { + if (Settings.appLockPin == pin) { step = 1 setHeaderText(R.string.pin_new) } else { @@ -52,7 +52,7 @@ class ResetPinDialogFragment : PinDialogFragment() { private fun step2(pin: String) { if (proposedPin == pin) { - Preferences.setAppLockPin(pin) + Settings.appLockPin = pin dismiss() parent?.onPinResetSuccess() } else { diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/pin/UnlockPinDialogFragment.kt b/app/src/main/java/me/devsaki/hentoid/fragments/pin/UnlockPinDialogFragment.kt index e2b51686c..5f88f8cc7 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/pin/UnlockPinDialogFragment.kt +++ b/app/src/main/java/me/devsaki/hentoid/fragments/pin/UnlockPinDialogFragment.kt @@ -5,7 +5,7 @@ import android.os.Bundle import android.view.View import androidx.fragment.app.FragmentManager import me.devsaki.hentoid.R -import me.devsaki.hentoid.util.Preferences +import me.devsaki.hentoid.util.Settings class UnlockPinDialogFragment : PinDialogFragment() { companion object { @@ -29,7 +29,7 @@ class UnlockPinDialogFragment : PinDialogFragment() { } override fun onPinAccept(pin: String) { - if (Preferences.getAppLockPin() == pin) { + if (Settings.appLockPin == pin) { dismiss() parent?.onUnlockSuccess() } else { diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/preferences/DownloadStrategyDialogFragment.kt b/app/src/main/java/me/devsaki/hentoid/fragments/preferences/DownloadStrategyDialogFragment.kt index 0b7813277..e07c2ded3 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/preferences/DownloadStrategyDialogFragment.kt +++ b/app/src/main/java/me/devsaki/hentoid/fragments/preferences/DownloadStrategyDialogFragment.kt @@ -10,7 +10,7 @@ import androidx.fragment.app.FragmentActivity import me.devsaki.hentoid.R import me.devsaki.hentoid.databinding.DialogPrefsDlStrategyBinding import me.devsaki.hentoid.fragments.BaseDialogFragment -import me.devsaki.hentoid.util.Preferences +import me.devsaki.hentoid.util.Settings import me.devsaki.hentoid.util.coerceIn class DownloadStrategyDialogFragment : BaseDialogFragment() { @@ -50,14 +50,14 @@ class DownloadStrategyDialogFragment : BaseDialogFragment choiceBalance.id + when (Settings.storageDownloadStrategy) { + Settings.Value.STORAGE_FILL_BALANCE_FREE -> choiceBalance.id else -> choiceFallover.id } ) @@ -71,7 +71,7 @@ class DownloadStrategyDialogFragment : BaseDialogFragment Preferences.Constant.STORAGE_FILL_BALANCE_FREE - else -> Preferences.Constant.STORAGE_FILL_FALLOVER + choiceBalance.id -> Settings.Value.STORAGE_FILL_BALANCE_FREE + else -> Settings.Value.STORAGE_FILL_FALLOVER } - ) } parent?.onStrategySelected() diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/preferences/LibRefreshDialogFragment.kt b/app/src/main/java/me/devsaki/hentoid/fragments/preferences/LibRefreshDialogFragment.kt index 528375411..5ffc43ea0 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/preferences/LibRefreshDialogFragment.kt +++ b/app/src/main/java/me/devsaki/hentoid/fragments/preferences/LibRefreshDialogFragment.kt @@ -29,7 +29,6 @@ import me.devsaki.hentoid.fragments.BaseDialogFragment import me.devsaki.hentoid.util.ImportOptions import me.devsaki.hentoid.util.PickFolderContract import me.devsaki.hentoid.util.PickerResult -import me.devsaki.hentoid.util.Preferences import me.devsaki.hentoid.util.ProcessFolderResult import me.devsaki.hentoid.util.Settings import me.devsaki.hentoid.util.file.RQST_STORAGE_PERMISSION @@ -151,7 +150,7 @@ class LibRefreshDialogFragment : BaseDialogFragment onPrefColorThemeChanged() Preferences.Key.DL_THREADS_QUANTITY_LISTS, - Preferences.Key.APP_PREVIEW, + Settings.Key.APP_PREVIEW, Settings.Key.FORCE_ENGLISH, Settings.Key.TEXT_SELECT_MENU, Settings.Key.ANALYTICS_PREFERENCE -> onPrefRequiringRestartChanged() - Preferences.Key.EXTERNAL_LIBRARY_URI -> onExternalFolderChanged() + Settings.Key.EXTERNAL_LIBRARY_URI -> onExternalFolderChanged() Preferences.Key.BROWSER_DNS_OVER_HTTPS -> onDoHChanged() Settings.Key.WEB_AUGMENTED_BROWSER -> onAugmentedBrowserChanged() } @@ -143,7 +143,7 @@ class PreferencesFragment : PreferenceFragmentCompat(), true } - Preferences.Key.APP_LOCK -> { + Settings.Key.APP_LOCK -> { requireContext().startLocalActivity() true } @@ -201,11 +201,11 @@ class PreferencesFragment : PreferenceFragmentCompat(), private fun onExternalFolderChanged() { val storageFolderPref: Preference? = findPreference(Preferences.Key.EXTERNAL_LIBRARY) as Preference? - val uri = Uri.parse(Preferences.getExternalLibraryUri()) + val uri = Uri.parse(Settings.externalLibraryUri) storageFolderPref?.summary = getFullPathFromUri(requireContext(), uri) // Enable/disable sub-prefs val deleteExternalLibrary: Preference? = - findPreference(Preferences.Key.EXTERNAL_LIBRARY_DELETE) as Preference? + findPreference(Settings.Key.EXTERNAL_LIBRARY_DELETE) as Preference? deleteExternalLibrary?.isEnabled = (uri.toString().isNotEmpty()) val detachExternalLibrary: Preference? = findPreference(Preferences.Key.EXTERNAL_LIBRARY_DETACH) as Preference? diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/preferences/StorageUsageDialogFragment.kt b/app/src/main/java/me/devsaki/hentoid/fragments/preferences/StorageUsageDialogFragment.kt index c2d808c44..8e31a9a41 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/preferences/StorageUsageDialogFragment.kt +++ b/app/src/main/java/me/devsaki/hentoid/fragments/preferences/StorageUsageDialogFragment.kt @@ -17,6 +17,7 @@ import me.devsaki.hentoid.enums.Site import me.devsaki.hentoid.enums.StorageLocation import me.devsaki.hentoid.fragments.BaseDialogFragment import me.devsaki.hentoid.util.Preferences +import me.devsaki.hentoid.util.Settings import me.devsaki.hentoid.util.file.MemoryUsageFigures import me.devsaki.hentoid.util.file.formatHumanReadableSize import me.devsaki.hentoid.util.file.getDocumentFromTreeUriString @@ -133,7 +134,7 @@ class StorageUsageDialogFragment : BaseDialogFragment() { } private fun getStats(location: StorageLocation): Pair { - val root = Preferences.getStorageUri(location) + val root = Settings.getStorageUri(location) if (root.isNotEmpty()) { val rootFolder = getDocumentFromTreeUriString(requireActivity(), root) if (rootFolder != null) { diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/tools/LogsDialogFragment.kt b/app/src/main/java/me/devsaki/hentoid/fragments/tools/LogsDialogFragment.kt index 842de6c74..438ac72b2 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/tools/LogsDialogFragment.kt +++ b/app/src/main/java/me/devsaki/hentoid/fragments/tools/LogsDialogFragment.kt @@ -22,7 +22,7 @@ import me.devsaki.hentoid.R import me.devsaki.hentoid.databinding.DialogToolsAppLogsBinding import me.devsaki.hentoid.enums.StorageLocation import me.devsaki.hentoid.fragments.BaseDialogFragment -import me.devsaki.hentoid.util.Preferences +import me.devsaki.hentoid.util.Settings import me.devsaki.hentoid.util.dimensAsDp import me.devsaki.hentoid.util.file.getDocumentFromTreeUriString import me.devsaki.hentoid.util.file.listFiles @@ -96,7 +96,7 @@ class LogsDialogFragment : BaseDialogFragment() { val rootFolder = getDocumentFromTreeUriString( requireContext(), - Preferences.getStorageUri(StorageLocation.PRIMARY_1) + Settings.getStorageUri(StorageLocation.PRIMARY_1) ) ?: return@withContext emptyList() var files = listFiles( diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/tools/MassOperationsDialogFragment.kt b/app/src/main/java/me/devsaki/hentoid/fragments/tools/MassOperationsDialogFragment.kt index 1940356ff..f00edf244 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/tools/MassOperationsDialogFragment.kt +++ b/app/src/main/java/me/devsaki/hentoid/fragments/tools/MassOperationsDialogFragment.kt @@ -15,7 +15,6 @@ import me.devsaki.hentoid.activities.ToolsActivity import me.devsaki.hentoid.database.ObjectBoxDAO import me.devsaki.hentoid.databinding.DialogToolsMassOperationsBinding import me.devsaki.hentoid.fragments.BaseDialogFragment -import me.devsaki.hentoid.util.Preferences import me.devsaki.hentoid.util.Settings import me.devsaki.hentoid.widget.ContentSearchManager @@ -99,7 +98,8 @@ class MassOperationsDialogFragment : BaseDialogFragment favGroupsContent.contains(e) }.count()) } else { Pair(allCount, scope.count()) @@ -116,8 +116,8 @@ class MassOperationsDialogFragment : BaseDialogFragment result += title - Preferences.Constant.FOLDER_NAMING_CONTENT_AUTH_TITLE_ID -> result += "$author - $title" - Preferences.Constant.FOLDER_NAMING_CONTENT_TITLE_AUTH_ID -> result += "$title - $author" + when (Settings.folderNameFormat) { + Settings.Value.FOLDER_NAMING_CONTENT_TITLE_ID -> result += title + Settings.Value.FOLDER_NAMING_CONTENT_AUTH_TITLE_ID -> result += "$author - $title" + Settings.Value.FOLDER_NAMING_CONTENT_TITLE_AUTH_ID -> result += "$title - $author" else -> {} } result += " - " @@ -975,7 +975,7 @@ fun getOrCreateSiteDownloadDir( location: StorageLocation, site: Site ): DocumentFile? { - val appUriStr = Preferences.getStorageUri(location) + val appUriStr = Settings.getStorageUri(location) if (appUriStr.isEmpty()) { Timber.e("No storage URI defined for location %s", location.name) return null @@ -2064,7 +2064,7 @@ fun mergeContents( // External library root for external content if (mergedContent.status == StatusContent.EXTERNAL) { val externalRootFolder = - getDocumentFromTreeUriString(context, Preferences.getExternalLibraryUri()) + getDocumentFromTreeUriString(context, Settings.externalLibraryUri) if (null == externalRootFolder || !externalRootFolder.exists()) throw ContentNotProcessedException( mergedContent, "Could not create target directory : external root unreachable" @@ -2086,7 +2086,7 @@ fun mergeContents( } else { // Primary folder for non-external content; using download strategy val location = selectDownloadLocation(context) targetFolder = getOrCreateContentDownloadDir(context, mergedContent, location, true) - parentFolder = getDocumentFromTreeUriString(context, Preferences.getStorageUri(location)) + parentFolder = getDocumentFromTreeUriString(context, Settings.getStorageUri(location)) } if (null == targetFolder || !targetFolder.exists()) throw ContentNotProcessedException(mergedContent, "Could not create target directory") @@ -2266,7 +2266,7 @@ fun mergeContents( fun getLocation(content: Content): StorageLocation { for (location in StorageLocation.entries) { - val rootUri = Preferences.getStorageUri(location) + val rootUri = Settings.getStorageUri(location) if (rootUri.isNotEmpty() && content.storageUri.startsWith(rootUri)) return location } return StorageLocation.NONE diff --git a/app/src/main/java/me/devsaki/hentoid/util/GroupHelper.kt b/app/src/main/java/me/devsaki/hentoid/util/GroupHelper.kt index dc70f7f38..aaa113477 100644 --- a/app/src/main/java/me/devsaki/hentoid/util/GroupHelper.kt +++ b/app/src/main/java/me/devsaki/hentoid/util/GroupHelper.kt @@ -103,7 +103,7 @@ fun updateGroupsJson(context: Context, dao: CollectionDAO): Boolean { contentCollection.replaceGroups(Grouping.DL_DATE, editedDateGroups) val rootFolder = - getDocumentFromTreeUriString(context, Preferences.getStorageUri(StorageLocation.PRIMARY_1)) + getDocumentFromTreeUriString(context, Settings.getStorageUri(StorageLocation.PRIMARY_1)) ?: return false try { diff --git a/app/src/main/java/me/devsaki/hentoid/util/Helper.kt b/app/src/main/java/me/devsaki/hentoid/util/Helper.kt index 891b63db4..a94764a81 100644 --- a/app/src/main/java/me/devsaki/hentoid/util/Helper.kt +++ b/app/src/main/java/me/devsaki/hentoid/util/Helper.kt @@ -387,7 +387,7 @@ fun updateBookmarksJson(context: Context, dao: CollectionDAO): Boolean { contentCollection.replaceBookmarks(bookmarks) val rootFolder = - getDocumentFromTreeUriString(context, Preferences.getStorageUri(StorageLocation.PRIMARY_1)) + getDocumentFromTreeUriString(context, Settings.getStorageUri(StorageLocation.PRIMARY_1)) ?: return false try { @@ -427,7 +427,7 @@ fun updateRenamingRulesJson(context: Context, dao: CollectionDAO): Boolean { contentCollection.replaceRenamingRules(rules) val rootFolder = - getDocumentFromTreeUriString(context, Preferences.getStorageUri(StorageLocation.PRIMARY_1)) + getDocumentFromTreeUriString(context, Settings.getStorageUri(StorageLocation.PRIMARY_1)) ?: return false try { diff --git a/app/src/main/java/me/devsaki/hentoid/util/ImportHelper.kt b/app/src/main/java/me/devsaki/hentoid/util/ImportHelper.kt index a992995ba..5b399a117 100644 --- a/app/src/main/java/me/devsaki/hentoid/util/ImportHelper.kt +++ b/app/src/main/java/me/devsaki/hentoid/util/ImportHelper.kt @@ -183,10 +183,10 @@ private fun getFolderPickerIntent(context: Context, location: StorageLocation): intent.putExtra("android.content.extra.SHOW_ADVANCED", true) // Start the SAF at the specified location - if (Preferences.getStorageUri(location).isNotEmpty()) { + if (Settings.getStorageUri(location).isNotEmpty()) { val file = getDocumentFromTreeUriString( context, - Preferences.getStorageUri(location) + Settings.getStorageUri(location) ) if (file != null) intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, file.uri) } @@ -258,8 +258,8 @@ fun setAndScanPrimaryFolder( // Check if selected folder is separate from Hentoid's other primary location val otherLocationUriStr: String = - if (location == StorageLocation.PRIMARY_1) Preferences.getStorageUri(StorageLocation.PRIMARY_2) - else Preferences.getStorageUri(StorageLocation.PRIMARY_1) + if (location == StorageLocation.PRIMARY_1) Settings.getStorageUri(StorageLocation.PRIMARY_2) + else Settings.getStorageUri(StorageLocation.PRIMARY_1) if (otherLocationUriStr.isNotEmpty()) { val treeFullPath = getFullPathFromUri(context, treeUri) @@ -282,7 +282,7 @@ fun setAndScanPrimaryFolder( } // Check if selected folder is separate from Hentoid's external location - val extLocationStr = Preferences.getStorageUri(StorageLocation.EXTERNAL) + val extLocationStr = Settings.getStorageUri(StorageLocation.EXTERNAL) if (extLocationStr.isNotEmpty()) { val treeFullPath = getFullPathFromUri(context, treeUri) val extFullPath = getFullPathFromUri(context, Uri.parse(extLocationStr)) @@ -329,7 +329,7 @@ fun setAndScanPrimaryFolder( // => Don't run the import worker and settle things here // In case that Location was previously populated, drop all books - if (Preferences.getStorageUri(location).isNotEmpty()) { + if (Settings.getStorageUri(location).isNotEmpty()) { val dao: CollectionDAO = ObjectBoxDAO() try { detachAllPrimaryContent(dao, location) @@ -337,7 +337,7 @@ fun setAndScanPrimaryFolder( dao.cleanup() } } - Preferences.setStorageUri(location, hentoidFolder.uri.toString()) + Settings.setStorageUri(location, hentoidFolder.uri.toString()) Pair(ProcessFolderResult.OK_EMPTY_FOLDER, hentoidFolder.uri.toString()) } } @@ -370,8 +370,8 @@ fun setAndScanExternalFolder( } // Check if selected folder is separate from one of Hentoid's primary locations - var primaryUri1 = Preferences.getStorageUri(StorageLocation.PRIMARY_1) - var primaryUri2 = Preferences.getStorageUri(StorageLocation.PRIMARY_2) + var primaryUri1 = Settings.getStorageUri(StorageLocation.PRIMARY_1) + var primaryUri2 = Settings.getStorageUri(StorageLocation.PRIMARY_2) if (primaryUri1.isNotEmpty()) primaryUri1 = getFullPathFromUri(context, Uri.parse(primaryUri1)) if (primaryUri2.isNotEmpty()) primaryUri2 = @@ -398,7 +398,7 @@ fun setAndScanExternalFolder( // Set the folder as the app's external library folder val folderUri = docFile.uri.toString() - Preferences.setExternalLibraryUri(folderUri) + Settings.externalLibraryUri = folderUri // Start the import return if (runExternalImport(context)) Pair(ProcessFolderResult.OK_LIBRARY_DETECTED, folderUri) @@ -418,9 +418,9 @@ fun persistLocationCredentials( location: List ) { val uri = location - .mapNotNull { l -> Preferences.getStorageUri(l) } - .filterNot { obj -> obj.isEmpty() } - .map { uri -> Uri.parse(uri) } + .map { Settings.getStorageUri(it) } + .filterNot { it.isEmpty() } + .map { Uri.parse(it) } persistNewUriPermission(context, treeUri, uri) } diff --git a/app/src/main/java/me/devsaki/hentoid/util/LogHelper.kt b/app/src/main/java/me/devsaki/hentoid/util/LogHelper.kt index dcbc04636..5b5c022be 100644 --- a/app/src/main/java/me/devsaki/hentoid/util/LogHelper.kt +++ b/app/src/main/java/me/devsaki/hentoid/util/LogHelper.kt @@ -240,7 +240,7 @@ fun Context.writeLog(logInfo: LogInfo): DocumentFile? { // Save the log; use primary folder by default val folder = getDocumentFromTreeUriString( - this, Preferences.getStorageUri(StorageLocation.PRIMARY_1) + this, Settings.getStorageUri(StorageLocation.PRIMARY_1) ) if (folder != null) { val logDocumentFile = findOrCreateDocumentFile( diff --git a/app/src/main/java/me/devsaki/hentoid/util/Preferences.java b/app/src/main/java/me/devsaki/hentoid/util/Preferences.java index e9bda0107..704ec4263 100644 --- a/app/src/main/java/me/devsaki/hentoid/util/Preferences.java +++ b/app/src/main/java/me/devsaki/hentoid/util/Preferences.java @@ -9,10 +9,8 @@ import java.util.HashMap; import java.util.Map; -import me.devsaki.hentoid.BuildConfig; import me.devsaki.hentoid.database.domains.DownloadMode; import me.devsaki.hentoid.enums.Grouping; -import me.devsaki.hentoid.enums.StorageLocation; import me.devsaki.hentoid.enums.Theme; import me.devsaki.hentoid.util.network.Source; import timber.log.Timber; @@ -51,8 +49,8 @@ public static void performHousekeeping() { sharedPreferences.edit().remove(Key.VIEWER_FLING_FACTOR).apply(); } // PIN activation -> Lock type (v1.18.4) - if (sharedPreferences.contains(Key.APP_LOCK)) { - if (!getAppLockPin().isEmpty()) Settings.INSTANCE.setLockType(1); + if (sharedPreferences.contains(Settings.Key.APP_LOCK)) { + if (!Settings.INSTANCE.getAppLockPin().isEmpty()) Settings.INSTANCE.setLockType(1); } } @@ -70,8 +68,8 @@ public static Map extractPortableInformation() { // Remove non-exportable settings that make no sense on another instance result.remove(Settings.Key.FIRST_RUN); result.remove(Settings.Key.WELCOME_DONE); - result.remove(Key.PRIMARY_STORAGE_URI); - result.remove(Key.EXTERNAL_LIBRARY_URI); + result.remove(Settings.Key.PRIMARY_STORAGE_URI); + result.remove(Settings.Key.EXTERNAL_LIBRARY_URI); result.remove(Key.LAST_KNOWN_APP_VERSION_CODE); result.remove(Settings.Key.REFRESH_JSON_1_DONE); result.remove(Settings.Key.LOCK_TYPE); @@ -102,11 +100,6 @@ private static int getIntPref(@NonNull String key, int defaultValue) { return Integer.parseInt(sharedPreferences.getString(key, Integer.toString(defaultValue))); } - private static void setIntPref(@NonNull String key, int value) { - if (null == sharedPreferences) return; - sharedPreferences.edit().putString(key, Integer.toString(value)).apply(); - } - private static long getLongPref(@NonNull String key, long defaultValue) { if (null == sharedPreferences) return defaultValue; return Long.parseLong(sharedPreferences.getString(key, Long.toString(defaultValue))); @@ -119,110 +112,6 @@ private static boolean getBoolPref(@NonNull String key, boolean defaultValue) { // ======= PROPERTIES GETTERS / SETTERS - public static int getContentPageQuantity() { - return getIntPref(Key.QUANTITY_PER_PAGE_LISTS, Default.QUANTITY_PER_PAGE); - } - - public static String getAppLockPin() { - return sharedPreferences.getString(Key.APP_LOCK, ""); - } - - public static void setAppLockPin(String pin) { - sharedPreferences.edit().putString(Key.APP_LOCK, pin).apply(); - } - - public static boolean getEndlessScroll() { - return getBoolPref(Key.ENDLESS_SCROLL, Default.ENDLESS_SCROLL); - } - - public static boolean isTopFabEnabled() { - return getBoolPref(Key.TOP_FAB, Default.TOP_FAB); - } - - public static void setTopFabEnabled(boolean value) { - sharedPreferences.edit().putBoolean(Key.TOP_FAB, value).apply(); - } - - public static boolean getRecentVisibility() { - return getBoolPref(Key.APP_PREVIEW, BuildConfig.DEBUG); - } - - private static String getStorageUri() { - return sharedPreferences.getString(Key.PRIMARY_STORAGE_URI, ""); - } - - private static void setStorageUri(String uri) { - sharedPreferences.edit().putString(Key.PRIMARY_STORAGE_URI, uri).apply(); - } - - private static String getStorageUri2() { - return sharedPreferences.getString(Key.PRIMARY_STORAGE_URI_2, ""); - } - - private static void setStorageUri2(String uri) { - sharedPreferences.edit().putString(Key.PRIMARY_STORAGE_URI_2, uri).apply(); - } - - public static String getStorageUri(StorageLocation location) { - return switch (location) { - case PRIMARY_1 -> getStorageUri(); - case PRIMARY_2 -> getStorageUri2(); - case EXTERNAL -> getExternalLibraryUri(); - default -> ""; - }; - } - - public static void setStorageUri(StorageLocation location, String uri) { - switch (location) { - case PRIMARY_1 -> setStorageUri(uri); - case PRIMARY_2 -> setStorageUri2(uri); - case EXTERNAL -> setExternalLibraryUri(uri); - default -> { - // Nothing - } - } - } - - public static int getStorageDownloadStrategy() { - return getIntPref(Key.PRIMARY_STORAGE_FILL_METHOD, Default.PRIMARY_STORAGE_FILL_METHOD); - } - - public static void setStorageDownloadStrategy(int value) { - setIntPref(Key.PRIMARY_STORAGE_FILL_METHOD, value); - } - - public static int getStorageSwitchThresholdPc() { - return getIntPref(Key.PRIMARY_STORAGE_SWITCH_THRESHOLD_PC, Default.PRIMARY_STORAGE_SWITCH_THRESHOLD_PC); - } - - public static void setStorageSwitchThresholdPc(int value) { - setIntPref(Key.PRIMARY_STORAGE_SWITCH_THRESHOLD_PC, value); - } - - public static int getMemoryAlertThreshold() { - return getIntPref(Key.MEMORY_ALERT, Default.MEMORY_ALERT); - } - - public static void setMemoryAlertThreshold(int value) { - setIntPref(Key.MEMORY_ALERT, value); - } - - public static String getExternalLibraryUri() { - return sharedPreferences.getString(Key.EXTERNAL_LIBRARY_URI, ""); - } - - public static void setExternalLibraryUri(String uri) { - sharedPreferences.edit().putString(Key.EXTERNAL_LIBRARY_URI, uri).apply(); - } - - public static boolean isDeleteExternalLibrary() { - return getBoolPref(Key.EXTERNAL_LIBRARY_DELETE, Default.EXTERNAL_LIBRARY_DELETE); - } - - static int getFolderNameFormat() { - return getIntPref(Key.FOLDER_NAMING_CONTENT_LISTS, Default.FOLDER_NAMING_CONTENT); - } - public static int getWebViewInitialZoom() { return getIntPref(Key.WEBVIEW_INITIAL_ZOOM_LISTS, Default.WEBVIEW_INITIAL_ZOOM); } @@ -689,25 +578,12 @@ private Key() { throw new IllegalStateException("Utility class"); } - public static final String APP_LOCK = "pref_app_lock"; - public static final String APP_PREVIEW = "pref_app_preview"; public static final String CHECK_UPDATE_MANUAL = "pref_check_updates_manual"; static final String VERSION_KEY = "prefs_version"; - static final String QUANTITY_PER_PAGE_LISTS = "pref_quantity_per_page_lists"; public static final String DRAWER_SOURCES = "pref_drawer_sources"; - public static final String ENDLESS_SCROLL = "pref_endless_scroll"; - public static final String TOP_FAB = "pref_top_fab"; - public static final String PRIMARY_STORAGE_URI = "pref_sd_storage_uri"; - public static final String PRIMARY_STORAGE_URI_2 = "pref_sd_storage_uri_2"; - public static final String PRIMARY_STORAGE_FILL_METHOD = "pref_storage_fill_method"; - public static final String PRIMARY_STORAGE_SWITCH_THRESHOLD_PC = "pref_storage_switch_threshold_pc"; public static final String EXTERNAL_LIBRARY = "pref_external_library"; - public static final String EXTERNAL_LIBRARY_URI = "pref_external_library_uri"; - public static final String EXTERNAL_LIBRARY_DELETE = "pref_external_library_delete"; public static final String EXTERNAL_LIBRARY_DETACH = "pref_detach_external_library"; - static final String FOLDER_NAMING_CONTENT_LISTS = "pref_folder_naming_content_lists"; public static final String STORAGE_MANAGEMENT = "storage_mgt"; - public static final String MEMORY_ALERT = "pref_memory_alert"; static final String WEBVIEW_OVERRIDE_OVERVIEW_LISTS = "pref_webview_override_overview_lists"; static final String WEBVIEW_INITIAL_ZOOM_LISTS = "pref_webview_initial_zoom_lists"; static final String BROWSER_RESUME_LAST = "pref_browser_resume_last"; @@ -805,15 +681,6 @@ private Default() { throw new IllegalStateException("Utility class"); } - static final int PRIMARY_STORAGE_FILL_METHOD = Constant.STORAGE_FILL_BALANCE_FREE; - static final int PRIMARY_STORAGE_SWITCH_THRESHOLD_PC = 90; - - static final int QUANTITY_PER_PAGE = 20; - static final boolean ENDLESS_SCROLL = true; - static final boolean TOP_FAB = true; - static final int MEMORY_ALERT = 110; - static final boolean EXTERNAL_LIBRARY_DELETE = false; - static final int FOLDER_NAMING_CONTENT = Constant.FOLDER_NAMING_CONTENT_AUTH_TITLE_ID; static final boolean WEBVIEW_OVERRIDE_OVERVIEW = false; public static final int WEBVIEW_INITIAL_ZOOM = 20; static final boolean BROWSER_RESUME_LAST = false; @@ -903,19 +770,10 @@ private Constant() { throw new IllegalStateException("Utility class"); } - public static final int STORAGE_FILL_BALANCE_FREE = 0; - public static final int STORAGE_FILL_FALLOVER = 1; - - public static final int DOWNLOAD_THREAD_COUNT_AUTO = 0; public static final int ORDER_CONTENT_FAVOURITE = -2; // Artificial order created for clarity purposes - static final int FOLDER_NAMING_CONTENT_ID = 0; - static final int FOLDER_NAMING_CONTENT_TITLE_ID = 1; - static final int FOLDER_NAMING_CONTENT_AUTH_TITLE_ID = 2; - static final int FOLDER_NAMING_CONTENT_TITLE_AUTH_ID = 3; - public static final int QUEUE_NEW_DOWNLOADS_POSITION_TOP = 0; public static final int QUEUE_NEW_DOWNLOADS_POSITION_BOTTOM = 1; public static final int QUEUE_NEW_DOWNLOADS_POSITION_ASK = 2; diff --git a/app/src/main/java/me/devsaki/hentoid/util/Settings.kt b/app/src/main/java/me/devsaki/hentoid/util/Settings.kt index 02f69ce83..c398bea11 100644 --- a/app/src/main/java/me/devsaki/hentoid/util/Settings.kt +++ b/app/src/main/java/me/devsaki/hentoid/util/Settings.kt @@ -5,8 +5,10 @@ import android.content.SharedPreferences import android.content.SharedPreferences.OnSharedPreferenceChangeListener import android.text.TextUtils import androidx.preference.PreferenceManager +import me.devsaki.hentoid.BuildConfig import me.devsaki.hentoid.enums.PictureEncoder import me.devsaki.hentoid.enums.Site +import me.devsaki.hentoid.enums.StorageLocation import me.devsaki.hentoid.util.Settings.Value.SEARCH_ORDER_ATTRIBUTES_COUNT import kotlin.reflect.KProperty @@ -24,7 +26,7 @@ object Settings { val isImportQueueEmptyBooks: Boolean by BoolSetting(Key.IMPORT_QUEUE_EMPTY, false) // LIBRARY - var libraryDisplay: Int by IntSetting(Key.LIBRARY_DISPLAY, Value.LIBRARY_DISPLAY_DEFAULT) + var libraryDisplay: Int by IntSettingStr(Key.LIBRARY_DISPLAY, Value.LIBRARY_DISPLAY_DEFAULT) var libraryDisplayGridFav: Boolean by BoolSetting(Key.LIBRARY_DISPLAY_GRID_FAV, true) var libraryDisplayGridRating: Boolean by BoolSetting(Key.LIBRARY_DISPLAY_GRID_RATING, true) var libraryDisplayGridSource: Boolean by BoolSetting(Key.LIBRARY_DISPLAY_GRID_SOURCE, true) @@ -34,18 +36,22 @@ object Settings { ) var libraryDisplayGridTitle: Boolean by BoolSetting(Key.LIBRARY_DISPLAY_GRID_TITLE, true) var libraryDisplayGridLanguage: Boolean by BoolSetting(Key.LIBRARY_DISPLAY_GRID_LANG, true) - var libraryGridCardWidthDP: Int by IntSetting(Key.LIBRARY_GRID_CARD_WIDTH, 150) + var libraryGridCardWidthDP: Int by IntSettingStr(Key.LIBRARY_GRID_CARD_WIDTH, 150) var activeSites: List by ListSiteSetting("active_sites", Value.ACTIVE_SITES) - var contentSortField: Int by IntSetting2( + var contentSortField: Int by IntSetting( "pref_order_content_field", Default.ORDER_CONTENT_FIELD ) var isContentSortDesc: Boolean by BoolSetting("pref_order_content_desc", false) - var groupSortField: Int by IntSetting2("pref_order_group_field", Default.ORDER_GROUP_FIELD) + var groupSortField: Int by IntSetting("pref_order_group_field", Default.ORDER_GROUP_FIELD) var isGroupSortDesc: Boolean by BoolSetting("pref_order_group_desc", false) + var contentPageQuantity: Int by IntSettingStr("pref_quantity_per_page_lists", 20) + var appLockPin: String by StringSetting(Key.APP_LOCK, "") + val endlessScroll: Boolean by BoolSetting(Key.ENDLESS_SCROLL, true) + var topFabEnabled: Boolean by BoolSetting(Key.TOP_FAB, true) // ADV SEARCH - val searchAttributesSortOrder: Int by IntSetting( + val searchAttributesSortOrder: Int by IntSettingStr( "pref_order_attribute_lists", SEARCH_ORDER_ATTRIBUTES_COUNT ) @@ -54,33 +60,33 @@ object Settings { // DOWNLOADER // LOCK - var lockType: Int by IntSetting(Key.LOCK_TYPE, 0) + var lockType: Int by IntSettingStr(Key.LOCK_TYPE, 0) // MASS OPERATIONS - var massOperation: Int by IntSetting("MASS_OPERATION", 0) - var massOperationScope: Int by IntSetting("MASS_SCOPE", 0) + var massOperation: Int by IntSettingStr("MASS_OPERATION", 0) + var massOperationScope: Int by IntSettingStr("MASS_SCOPE", 0) // TRANSFORM var isResizeEnabled: Boolean by BoolSetting("TRANSFORM_RESIZE_ENABLED", false) - var resizeMethod: Int by IntSetting("TRANSFORM_RESIZE_METHOD", 0) - var resizeMethod1Ratio: Int by IntSetting("TRANSFORM_RESIZE_1_RATIO", 120) - var resizeMethod2Height: Int by IntSetting("TRANSFORM_RESIZE_2_HEIGHT", 0) - var resizeMethod2Width: Int by IntSetting("TRANSFORM_RESIZE_2_WIDTH", 0) - var resizeMethod3Ratio: Int by IntSetting("TRANSFORM_RESIZE_3_RATIO", 80) - var transcodeMethod: Int by IntSetting("TRANSFORM_TRANSCODE_METHOD", 0) - var transcodeEncoderAll: Int by IntSetting( + var resizeMethod: Int by IntSettingStr("TRANSFORM_RESIZE_METHOD", 0) + var resizeMethod1Ratio: Int by IntSettingStr("TRANSFORM_RESIZE_1_RATIO", 120) + var resizeMethod2Height: Int by IntSettingStr("TRANSFORM_RESIZE_2_HEIGHT", 0) + var resizeMethod2Width: Int by IntSettingStr("TRANSFORM_RESIZE_2_WIDTH", 0) + var resizeMethod3Ratio: Int by IntSettingStr("TRANSFORM_RESIZE_3_RATIO", 80) + var transcodeMethod: Int by IntSettingStr("TRANSFORM_TRANSCODE_METHOD", 0) + var transcodeEncoderAll: Int by IntSettingStr( "TRANSFORM_TRANSCODE_ENC_ALL", PictureEncoder.PNG.value ) - var transcodeEncoderLossless: Int by IntSetting( + var transcodeEncoderLossless: Int by IntSettingStr( "TRANSFORM_TRANSCODE_ENC_LOSSLESS", PictureEncoder.PNG.value ) - var transcodeEncoderLossy: Int by IntSetting( + var transcodeEncoderLossy: Int by IntSettingStr( "TRANSFORM_TRANSCODE_ENC_LOSSY", PictureEncoder.JPEG.value ) - var transcodeQuality: Int by IntSetting("TRANSFORM_TRANSCODE_QUALITY", 90) + var transcodeQuality: Int by IntSettingStr("TRANSFORM_TRANSCODE_QUALITY", 90) // ARCHIVES var archiveTargetFolder: String by StringSetting( @@ -88,8 +94,8 @@ object Settings { Value.ARCHIVE_TARGET_FOLDER_DOWNLOADS ) var latestTargetFolderUri: String by StringSetting("ARCHIVE_TARGET_FOLDER_LATEST", "") - var archiveTargetFormat: Int by IntSetting("ARCHIVE_TARGET_FORMAT", 0) - var pdfBackgroundColor: Int by IntSetting("ARCHIVE_PDF_BGCOLOR", 0) + var archiveTargetFormat: Int by IntSettingStr("ARCHIVE_TARGET_FORMAT", 0) + var pdfBackgroundColor: Int by IntSettingStr("ARCHIVE_PDF_BGCOLOR", 0) var isArchiveOverwrite: Boolean by BoolSetting("ARCHIVE_OVERWRITE", true) var isArchiveDeleteOnSuccess: Boolean by BoolSetting("ARCHIVE_DELETE_ON_SUCCESS", false) @@ -102,15 +108,49 @@ object Settings { var blockedTags: List by ListStringSetting(Key.DL_BLOCKED_TAGS) // READER - var colorDepth: Int by IntSetting(Key.READER_COLOR_DEPTH, 0) + var colorDepth: Int by IntSettingStr(Key.READER_COLOR_DEPTH, 0) // METADATA & RULES EDITOR - var ruleSortField: Int by IntSetting2("pref_order_rule_field", Value.ORDER_FIELD_SOURCE_NAME) + var ruleSortField: Int by IntSetting("pref_order_rule_field", Value.ORDER_FIELD_SOURCE_NAME) var isRuleSortDesc: Boolean by BoolSetting("pref_order_rule_desc", false) // ACHIEVEMENTS var achievements: ULong by ULongSetting(Key.ACHIEVEMENTS, 0UL) - var nbAIRescale: Int by IntSetting(Key.ACHIEVEMENTS_NB_AI_RESCALE, 0) + var nbAIRescale: Int by IntSettingStr(Key.ACHIEVEMENTS_NB_AI_RESCALE, 0) + + // STORAGE + private var storageUri: String by StringSetting(Key.PRIMARY_STORAGE_URI, "") + private var storageUri2: String by StringSetting(Key.PRIMARY_STORAGE_URI_2, "") + var externalLibraryUri: String by StringSetting(Key.EXTERNAL_LIBRARY_URI, "") + fun getStorageUri(location: StorageLocation): String { + return when (location) { + StorageLocation.PRIMARY_1 -> storageUri + StorageLocation.PRIMARY_2 -> storageUri2 + StorageLocation.EXTERNAL -> externalLibraryUri + else -> "" + } + } + + fun setStorageUri(location: StorageLocation, uri: String) { + when (location) { + StorageLocation.PRIMARY_1 -> storageUri = uri + StorageLocation.PRIMARY_2 -> storageUri2 = uri + StorageLocation.EXTERNAL -> externalLibraryUri = uri + else -> {} + } + } + + val folderNameFormat: Int by IntSettingStr( + "pref_folder_naming_content_lists", + Value.FOLDER_NAMING_CONTENT_AUTH_TITLE_ID + ) + var storageDownloadStrategy: Int by IntSettingStr( + Key.PRIMARY_STORAGE_FILL_METHOD, + Value.STORAGE_FILL_BALANCE_FREE + ) + var storageSwitchThresholdPc: Int by IntSettingStr(Key.PRIMARY_STORAGE_SWITCH_THRESHOLD_PC, 90) + var memoryAlertThreshold: Int by IntSettingStr("pref_memory_alert", 110) + val isDeleteExternalLibrary: Boolean by BoolSetting(Key.EXTERNAL_LIBRARY_DELETE, false) // APP-WIDE var isFirstRun: Boolean by BoolSetting(Key.FIRST_RUN, true) @@ -121,6 +161,7 @@ object Settings { var isBrowserMode: Boolean by BoolSetting(Key.BROWSER_MODE, false) val isForceEnglishLocale: Boolean by BoolSetting(Key.FORCE_ENGLISH, false) var isTextMenuOn: Boolean by BoolSetting(Key.TEXT_SELECT_MENU, false) + val recentVisibility: Boolean by BoolSetting(Key.APP_PREVIEW, BuildConfig.DEBUG) // Public Helpers @@ -146,7 +187,7 @@ object Settings { } } - private class IntSetting(val key: String, val default: Int) { + private class IntSettingStr(val key: String, val default: Int) { operator fun getValue(thisRef: Any?, property: KProperty<*>): Int { return (sharedPreferences.getString(key, default.toString()) + "").toInt() } @@ -156,7 +197,7 @@ object Settings { } } - private class IntSetting2(val key: String, val default: Int) { + private class IntSetting(val key: String, val default: Int) { operator fun getValue(thisRef: Any?, property: KProperty<*>): Int { return (sharedPreferences.getInt(key, default)) } @@ -246,6 +287,16 @@ object Settings { const val WEB_FORCE_LIGHTMODE = "WEB_FORCE_LIGHTMODE" const val DL_BLOCKED_TAGS = "pref_dl_blocked_tags" const val TEXT_SELECT_MENU = "TEXT_SELECT_MENU" + const val APP_LOCK = "pref_app_lock" + const val ENDLESS_SCROLL = "pref_endless_scroll" + const val TOP_FAB = "pref_top_fab" + const val APP_PREVIEW = "pref_app_preview" + const val PRIMARY_STORAGE_URI = "pref_sd_storage_uri" + const val PRIMARY_STORAGE_URI_2 = "pref_sd_storage_uri_2" + const val EXTERNAL_LIBRARY_URI = "pref_external_library_uri" + const val PRIMARY_STORAGE_FILL_METHOD = "pref_storage_fill_method" + const val PRIMARY_STORAGE_SWITCH_THRESHOLD_PC = "pref_storage_switch_threshold_pc" + const val EXTERNAL_LIBRARY_DELETE = "pref_external_library_delete" } object Default { @@ -294,5 +345,13 @@ object Settings { const val ORDER_FIELD_TARGET_NAME = 12 // Rules only const val ORDER_FIELD_CUSTOM = 98 const val ORDER_FIELD_RANDOM = 99 + + const val STORAGE_FILL_BALANCE_FREE = 0 + const val STORAGE_FILL_FALLOVER = 1 + + const val FOLDER_NAMING_CONTENT_ID = 0 + const val FOLDER_NAMING_CONTENT_TITLE_ID = 1 + const val FOLDER_NAMING_CONTENT_AUTH_TITLE_ID = 2 + const val FOLDER_NAMING_CONTENT_TITLE_AUTH_ID = 3 } } \ No newline at end of file diff --git a/app/src/main/java/me/devsaki/hentoid/util/download/DownloadHelper.kt b/app/src/main/java/me/devsaki/hentoid/util/download/DownloadHelper.kt index 3392c2e2d..9f10fae0d 100644 --- a/app/src/main/java/me/devsaki/hentoid/util/download/DownloadHelper.kt +++ b/app/src/main/java/me/devsaki/hentoid/util/download/DownloadHelper.kt @@ -9,7 +9,7 @@ import me.devsaki.hentoid.database.domains.Content import me.devsaki.hentoid.enums.Site import me.devsaki.hentoid.enums.StorageLocation import me.devsaki.hentoid.events.DownloadEvent -import me.devsaki.hentoid.util.Preferences +import me.devsaki.hentoid.util.Settings import me.devsaki.hentoid.util.assertNonUiThread import me.devsaki.hentoid.util.download.DownloadSpeedLimiter.take import me.devsaki.hentoid.util.exception.DownloadInterruptedException @@ -327,8 +327,8 @@ fun getCanonicalUrl(doc: Document): String { } fun selectDownloadLocation(context: Context): StorageLocation { - val uriStr1 = Preferences.getStorageUri(StorageLocation.PRIMARY_1).trim { it <= ' ' } - val uriStr2 = Preferences.getStorageUri(StorageLocation.PRIMARY_2).trim { it <= ' ' } + val uriStr1 = Settings.getStorageUri(StorageLocation.PRIMARY_1).trim() + val uriStr2 = Settings.getStorageUri(StorageLocation.PRIMARY_2).trim() // Obvious cases if (uriStr1.isEmpty() && uriStr2.isEmpty()) return StorageLocation.NONE @@ -345,9 +345,9 @@ fun selectDownloadLocation(context: Context): StorageLocation { // Apply download strategy val memUsage1 = MemoryUsageFigures(context, root1) val memUsage2 = MemoryUsageFigures(context, root2!!) - val strategy = Preferences.getStorageDownloadStrategy() - return if (Preferences.Constant.STORAGE_FILL_FALLOVER == strategy) { - if (100 - memUsage1.freeUsageRatio100 > Preferences.getStorageSwitchThresholdPc()) StorageLocation.PRIMARY_2 else StorageLocation.PRIMARY_1 + val strategy = Settings.storageDownloadStrategy + return if (Settings.Value.STORAGE_FILL_FALLOVER == strategy) { + if (100 - memUsage1.freeUsageRatio100 > Settings.storageSwitchThresholdPc) StorageLocation.PRIMARY_2 else StorageLocation.PRIMARY_1 } else { if (memUsage1.getfreeUsageBytes() > memUsage2.getfreeUsageBytes()) StorageLocation.PRIMARY_1 else StorageLocation.PRIMARY_2 } @@ -373,7 +373,7 @@ fun getDownloadLocation(context: Context, content: Content): Pair, this.touchHelper = touchHelper this.deleteAction = deleteAction isSwipeable = - !content.isBeingProcessed && (content.status != StatusContent.EXTERNAL || Preferences.isDeleteExternalLibrary()) && viewType != ViewType.MERGE + !content.isBeingProcessed && (content.status != StatusContent.EXTERNAL || Settings.isDeleteExternalLibrary) && viewType != ViewType.MERGE identifier = content.uniqueHash() } diff --git a/app/src/main/java/me/devsaki/hentoid/viewholders/DuplicateItem.kt b/app/src/main/java/me/devsaki/hentoid/viewholders/DuplicateItem.kt index 462c27bb2..3cf9f9127 100644 --- a/app/src/main/java/me/devsaki/hentoid/viewholders/DuplicateItem.kt +++ b/app/src/main/java/me/devsaki/hentoid/viewholders/DuplicateItem.kt @@ -24,7 +24,7 @@ import me.devsaki.hentoid.database.domains.DuplicateEntry import me.devsaki.hentoid.enums.Site import me.devsaki.hentoid.enums.StatusContent import me.devsaki.hentoid.ui.BlinkAnimation -import me.devsaki.hentoid.util.Preferences +import me.devsaki.hentoid.util.Settings import me.devsaki.hentoid.util.formatArtistForDisplay import me.devsaki.hentoid.util.getFlagResourceId import me.devsaki.hentoid.util.getThemedColor @@ -67,7 +67,7 @@ class DuplicateItem(result: DuplicateEntry, private val viewType: ViewType) : isBeingDeleted = result.isBeingDeleted } content?.let { - canDelete = it.status != StatusContent.EXTERNAL || Preferences.isDeleteExternalLibrary() + canDelete = it.status != StatusContent.EXTERNAL || Settings.isDeleteExternalLibrary } nbDuplicates = result.nbDuplicates isReferenceItem = titleScore > 1f diff --git a/app/src/main/java/me/devsaki/hentoid/viewmodels/PreferencesViewModel.kt b/app/src/main/java/me/devsaki/hentoid/viewmodels/PreferencesViewModel.kt index e14e214e2..bdb5d3d36 100644 --- a/app/src/main/java/me/devsaki/hentoid/viewmodels/PreferencesViewModel.kt +++ b/app/src/main/java/me/devsaki/hentoid/viewmodels/PreferencesViewModel.kt @@ -8,7 +8,7 @@ import me.devsaki.hentoid.R import me.devsaki.hentoid.database.CollectionDAO import me.devsaki.hentoid.enums.StorageLocation import me.devsaki.hentoid.events.ProcessEvent -import me.devsaki.hentoid.util.Preferences +import me.devsaki.hentoid.util.Settings import me.devsaki.hentoid.util.detachAllExternalContent import me.devsaki.hentoid.util.detachAllPrimaryContent import me.devsaki.hentoid.util.file.Beholder @@ -51,7 +51,7 @@ class PreferencesViewModel(application: Application, val dao: CollectionDAO) : // Nothing } } - Preferences.setStorageUri(location, "") + Settings.setStorageUri(location, "") } suspend fun merge2to1(nbBooks: Int) { diff --git a/app/src/main/java/me/devsaki/hentoid/workers/ExternalImportWorker.kt b/app/src/main/java/me/devsaki/hentoid/workers/ExternalImportWorker.kt index eb33de9ff..6928e59fe 100644 --- a/app/src/main/java/me/devsaki/hentoid/workers/ExternalImportWorker.kt +++ b/app/src/main/java/me/devsaki/hentoid/workers/ExternalImportWorker.kt @@ -17,7 +17,7 @@ import me.devsaki.hentoid.events.ProcessEvent import me.devsaki.hentoid.notification.import_.ImportCompleteNotification import me.devsaki.hentoid.notification.import_.ImportProgressNotification import me.devsaki.hentoid.notification.import_.ImportStartNotification -import me.devsaki.hentoid.util.Preferences +import me.devsaki.hentoid.util.Settings import me.devsaki.hentoid.util.addContent import me.devsaki.hentoid.util.createJsonFileFor import me.devsaki.hentoid.util.existsInCollection @@ -108,14 +108,14 @@ class ExternalImportWorker(context: Context, parameters: WorkerParameters) : */ private fun startImport(context: Context) { logNoDataMessage = "No content detected." - val rootFolder = getDocumentFromTreeUriString(context, Preferences.getExternalLibraryUri()) + val rootFolder = getDocumentFromTreeUriString(context, Settings.externalLibraryUri) if (null == rootFolder) { - Timber.e("External folder is not defined (%s)", Preferences.getExternalLibraryUri()) + Timber.e("External folder is not defined (%s)", Settings.externalLibraryUri) return } try { Beholder.clearSnapshot(context) - FileExplorer(context, Uri.parse(Preferences.getExternalLibraryUri())).use { explorer -> + FileExplorer(context, Uri.parse(Settings.externalLibraryUri)).use { explorer -> val detectedContent: MutableList = ArrayList() // Deep recursive search starting from the place the user has selected var dao: CollectionDAO = ObjectBoxDAO() @@ -294,7 +294,7 @@ class ExternalImportWorker(context: Context, parameters: WorkerParameters) : // Forge parent names using folder root path minus ext library root path val extRootElts = - getFullPathFromUri(context, Uri.parse(Preferences.getExternalLibraryUri())) + getFullPathFromUri(context, Uri.parse(Settings.externalLibraryUri)) .split(File.separator) val parentNames = getFullPathFromUri(context, deltaPlusRoot.uri) .split(File.separator).toMutableList() diff --git a/app/src/main/java/me/devsaki/hentoid/workers/MetadataImportWorker.kt b/app/src/main/java/me/devsaki/hentoid/workers/MetadataImportWorker.kt index ffdb4fa1d..57b3f399f 100644 --- a/app/src/main/java/me/devsaki/hentoid/workers/MetadataImportWorker.kt +++ b/app/src/main/java/me/devsaki/hentoid/workers/MetadataImportWorker.kt @@ -31,7 +31,7 @@ import me.devsaki.hentoid.json.JsonContentCollection import me.devsaki.hentoid.notification.import_.ImportCompleteNotification import me.devsaki.hentoid.notification.import_.ImportProgressNotification import me.devsaki.hentoid.notification.import_.ImportStartNotification -import me.devsaki.hentoid.util.Preferences +import me.devsaki.hentoid.util.Settings import me.devsaki.hentoid.util.addContent import me.devsaki.hentoid.util.createImageListFromFolder import me.devsaki.hentoid.util.file.findFile @@ -315,9 +315,9 @@ class MetadataImportWorker(val context: Context, val params: WorkerParameters) : val result: MutableMap> = EnumMap( Site::class.java ) - var storageUri = Preferences.getStorageUri(StorageLocation.PRIMARY_1) + var storageUri = Settings.getStorageUri(StorageLocation.PRIMARY_1) if (storageUri.isNotEmpty()) mapSiteFolders(context, result, storageUri) - storageUri = Preferences.getStorageUri(StorageLocation.PRIMARY_2) + storageUri = Settings.getStorageUri(StorageLocation.PRIMARY_2) if (storageUri.isNotEmpty()) mapSiteFolders(context, result, storageUri) return result } diff --git a/app/src/main/java/me/devsaki/hentoid/workers/PrimaryImportWorker.kt b/app/src/main/java/me/devsaki/hentoid/workers/PrimaryImportWorker.kt index 890051641..bc4098e41 100644 --- a/app/src/main/java/me/devsaki/hentoid/workers/PrimaryImportWorker.kt +++ b/app/src/main/java/me/devsaki/hentoid/workers/PrimaryImportWorker.kt @@ -43,7 +43,6 @@ import me.devsaki.hentoid.notification.import_.ImportProgressNotification import me.devsaki.hentoid.notification.import_.ImportStartNotification import me.devsaki.hentoid.util.LogEntry import me.devsaki.hentoid.util.LogInfo -import me.devsaki.hentoid.util.Preferences import me.devsaki.hentoid.util.Settings import me.devsaki.hentoid.util.addContent import me.devsaki.hentoid.util.createImageListFromFiles @@ -195,9 +194,9 @@ class PrimaryImportWorker(context: Context, parameters: WorkerParameters) : // Stop downloads; it can get messy if downloading _and_ refresh / import happen at the same time EventBus.getDefault().post(DownloadCommandEvent(DownloadCommandEvent.Type.EV_PAUSE, null)) - var previousUriStr = Preferences.getStorageUri(location) + var previousUriStr = Settings.getStorageUri(location) if (previousUriStr.isEmpty()) previousUriStr = "FAIL" // Auto-fails if location is not set - Preferences.setStorageUri(location, targetRootUri) + Settings.setStorageUri(location, targetRootUri) val rootFolder = getDocumentFromTreeUriString(context, targetRootUri) if (null == rootFolder) { From 49d5e7de1b637ae3cf37d957cb2cd93cbfb4c824 Mon Sep 17 00:00:00 2001 From: Robb <35880555+robbwatershed@users.noreply.github.com> Date: Sun, 27 Oct 2024 09:36:03 +0100 Subject: [PATCH 34/44] Library screen : Fix delete and merge menus not showing up for external archives & PDF --- .../devsaki/hentoid/fragments/library/LibraryContentFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/library/LibraryContentFragment.kt b/app/src/main/java/me/devsaki/hentoid/fragments/library/LibraryContentFragment.kt index be46de682..7c8a6f056 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/library/LibraryContentFragment.kt +++ b/app/src/main/java/me/devsaki/hentoid/fragments/library/LibraryContentFragment.kt @@ -1721,7 +1721,7 @@ class LibraryContentFragment : Fragment(), ChangeGroupDialogFragment.Parent, val selectedNonArchivePdfExternalCount = contentList.count { it.status == StatusContent.EXTERNAL && !it.isArchive && !it.isPdf } val selectedArchivePdfExternalCount = - contentList.count { it.status == StatusContent.EXTERNAL && it.isArchive && it.isPdf } + contentList.count { it.status == StatusContent.EXTERNAL && (it.isArchive || it.isPdf) } activity.get()?.updateSelectionToolbar( selectedCount.toLong(), selectedProcessedCount.toLong(), From dc685331dcaf91e1f01da8ad179bd48ac1540355 Mon Sep 17 00:00:00 2001 From: Robb <35880555+robbwatershed@users.noreply.github.com> Date: Sun, 27 Oct 2024 10:13:46 +0100 Subject: [PATCH 35/44] Library screen : Actually opens the containing folder of an archive or a PDF --- .../fragments/library/LibraryContentFragment.kt | 16 +++++++++------- .../me/devsaki/hentoid/util/file/FileHelper.kt | 11 +++++++++++ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/library/LibraryContentFragment.kt b/app/src/main/java/me/devsaki/hentoid/fragments/library/LibraryContentFragment.kt index 7c8a6f056..6d9caf3c9 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/library/LibraryContentFragment.kt +++ b/app/src/main/java/me/devsaki/hentoid/fragments/library/LibraryContentFragment.kt @@ -88,7 +88,8 @@ import me.devsaki.hentoid.util.dimensAsDp import me.devsaki.hentoid.util.dimensAsPx import me.devsaki.hentoid.util.file.formatHumanReadableSizeInt import me.devsaki.hentoid.util.file.getDocumentFromTreeUriString -import me.devsaki.hentoid.util.file.openFile +import me.devsaki.hentoid.util.file.getParent +import me.devsaki.hentoid.util.file.openUri import me.devsaki.hentoid.util.formatEpochToDate import me.devsaki.hentoid.util.getIdForCurrentTheme import me.devsaki.hentoid.util.isNumeric @@ -754,14 +755,15 @@ class LibraryContentFragment : Fragment(), ChangeGroupDialogFragment.Parent, toast(R.string.folder_undefined) return } - val folder = - getDocumentFromTreeUriString(context, c.storageUri) + val folder = getDocumentFromTreeUriString(context, c.storageUri) if (folder != null) { - selectExtension?.apply { - deselect(selections.toMutableSet()) - } + selectExtension?.apply { deselect(selections.toMutableSet()) } activity.get()?.getSelectionToolbar()?.visibility = View.GONE - openFile(context, folder) + + val uri = if (c.isArchive || c.isPdf) + getParent(context, Uri.parse(Settings.externalLibraryUri), folder) + else folder.uri + uri?.let { openUri(context, it) } } } } diff --git a/app/src/main/java/me/devsaki/hentoid/util/file/FileHelper.kt b/app/src/main/java/me/devsaki/hentoid/util/file/FileHelper.kt index 4c03098c3..696ca1a26 100644 --- a/app/src/main/java/me/devsaki/hentoid/util/file/FileHelper.kt +++ b/app/src/main/java/me/devsaki/hentoid/util/file/FileHelper.kt @@ -1295,6 +1295,17 @@ fun getFileUriCompat(context: Context, file: File): Uri { return FileProvider.getUriForFile(context, AUTHORITY, file) } +/** + * Get the parent Uri of the given DocumentFile using the given root + * NB : doc must be a child/grandchild of the document represented by root + */ +fun getParent(context: Context, root: Uri, doc: DocumentFile): Uri? { + val parentsRoots = + DocumentsContract.findDocumentPath(context.contentResolver, doc.uri) ?: return null + // NB : that call is expensive; consider implementing that within FileExplorer if needed inside a loop + return DocumentsContract.buildDocumentUriUsingTree(root, parentsRoots.path[0]) +} + /** * Remove all illegal characters from the given string to make it a valid Android file name * From cb243d1b27356cea059e4ee35cf8b5fb2cb4cdc9 Mon Sep 17 00:00:00 2001 From: Robb <35880555+robbwatershed@users.noreply.github.com> Date: Sun, 27 Oct 2024 20:28:42 +0100 Subject: [PATCH 36/44] Cleanup --- .../hentoid/adapters/ImagePagerAdapter.kt | 242 +++++++++--------- 1 file changed, 125 insertions(+), 117 deletions(-) diff --git a/app/src/main/java/me/devsaki/hentoid/adapters/ImagePagerAdapter.kt b/app/src/main/java/me/devsaki/hentoid/adapters/ImagePagerAdapter.kt index f3897d915..cafdca349 100644 --- a/app/src/main/java/me/devsaki/hentoid/adapters/ImagePagerAdapter.kt +++ b/app/src/main/java/me/devsaki/hentoid/adapters/ImagePagerAdapter.kt @@ -17,6 +17,7 @@ import androidx.lifecycle.setViewTreeLifecycleOwner import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView +import coil3.dispose import coil3.load import coil3.request.ErrorResult import coil3.request.SuccessResult @@ -177,102 +178,14 @@ class ImagePagerAdapter(val context: Context) : return ImageViewHolder(view) } - @SuppressLint("ClickableViewAccessibility") - fun reset(holder: ImageViewHolder) { - holder.apply { - val image = rootView.findViewById(R.id.imageview) - image.isClickable = true - image.isFocusable = true - image.scaleType = ImageView.ScaleType.FIT_CENTER - image.setOnTouchListener(null) - - val ssiv = rootView.findViewById(R.id.ssiv) - ssiv.setIgnoreTouchEvents(false) - ssiv.setDirection(CustomSubsamplingScaleImageView.Direction.HORIZONTAL) - ssiv.setPreferredBitmapConfig(colorDepth) - ssiv.setDoubleTapZoomDuration(500) - ssiv.setOnTouchListener(null) - - rootView.minimumHeight = 0 - noImgTxt.isVisible = false - } - } - - // TODO make all that method less ugly override fun onBindViewHolder(holder: ImageViewHolder, position: Int) { Timber.d("Picture %d : BindViewHolder", position) val displayParams = getDisplayParamsForPosition(position) ?: return val imgViewType = getImageViewType(displayParams) - reset(holder) - - holder.apply { - val imageView = rootView.findViewById(R.id.imageview) - viewerOrientation = displayParams.orientation - displayMode = displayParams.displayMode - isSmoothRendering = displayParams.isSmoothRendering - - if (ViewType.DEFAULT == imgViewType) { - // ImageView shouldn't react to click events when in vertical mode (controlled by ZoomableFrame / ZoomableRecyclerView) - if (Preferences.Constant.VIEWER_ORIENTATION_VERTICAL == viewerOrientation) { - imageView.isClickable = false - imageView.isFocusable = false - } - } else if (ViewType.IMAGEVIEW_STRETCH == imgViewType) { - imageView.scaleType = ImageView.ScaleType.FIT_XY - } else if (ViewType.SSIV_VERTICAL == imgViewType) { - val ssiv = rootView.findViewById(R.id.ssiv) - ssiv.setIgnoreTouchEvents(true) - ssiv.setDirection(CustomSubsamplingScaleImageView.Direction.VERTICAL) - } - - // Avoid stacking 0-px tall images on screen and load all of them at the same time - if (Preferences.Constant.VIEWER_ORIENTATION_VERTICAL == viewerOrientation) - rootView.minimumHeight = pageMinHeight - - val imageType = getImageType(getImageAt(position)) - if (forceImageView != null) { // ImageView has been forced - switchImageView(forceImageView!!, true) - forceImageView = null // Reset force flag - } else if (ImageType.IMG_TYPE_GIF == imageType || ImageType.IMG_TYPE_APNG == imageType) { - animationAlertListener?.run() - switchImageView(isImageView = true, isClickThrough = true) - } else switchImageView(imgViewType == ViewType.IMAGEVIEW_STRETCH) - - // Initialize SSIV when required - if (imgViewType == ViewType.DEFAULT && Preferences.Constant.VIEWER_ORIENTATION_HORIZONTAL == viewerOrientation && !isImageView) { - if (isSmoothRendering) ssiv.setGlEsRenderer(glEsRenderer) - else ssiv.setGlEsRenderer(null) - ssiv.setPreloadDimensions(itemView.width, imgView.height) - if (!Preferences.isReaderZoomTransitions()) ssiv.setDoubleTapZoomDuration(10) - - val scrollLTR = - Preferences.Constant.VIEWER_DIRECTION_LTR == displayParams.direction && isScrollLTR - ssiv.setOffsetLeftSide(scrollLTR) - ssiv.setScaleListener { s -> onAbsoluteScaleChanged(position, s.toFloat()) } - } - val layoutStyle = - if (Preferences.Constant.VIEWER_ORIENTATION_VERTICAL == viewerOrientation) ViewGroup.LayoutParams.WRAP_CONTENT else ViewGroup.LayoutParams.MATCH_PARENT - val layoutParams = imgView.layoutParams - layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT - layoutParams.height = layoutStyle - imgView.layoutParams = layoutParams - - var imageAvailable = true - val img = getImageAt(position) - if (img != null && img.fileUri.isNotEmpty()) setImage(img) - else imageAvailable = false - - val isStreaming = img != null && !imageAvailable && img.status == StatusContent.ONLINE - val isExtracting = img != null && !imageAvailable && !img.url.startsWith("http") - - @StringRes var text: Int = R.string.image_not_found - if (isStreaming) text = R.string.image_streaming - else if (isExtracting) text = R.string.image_extracting - noImgTxt.setText(text) - noImgTxt.isVisible = !imageAvailable - } + holder.reset() + holder.bind(displayParams, imgViewType, position) } override fun onViewRecycled(holder: ImageViewHolder) { @@ -287,7 +200,7 @@ class ImagePagerAdapter(val context: Context) : } // Free the SSIV's resources - if (!holder.isImageView) holder.ssiv.clear() + holder.clear() super.onViewRecycled(holder) } @@ -396,44 +309,100 @@ class ImagePagerAdapter(val context: Context) : fun setGestureListenerForPosition(position: Int) { recyclerView?.lifecycleScope?.launch { - (recyclerView?.findViewHolderForAdapterPosition(position) as ImageViewHolder?)?.apply { - withContext(Dispatchers.Default) { - var iterations = 0 // Wait for 5 secs max - while (isLoading.get() && iterations++ < 33) pause(150) - } - // ImageView or vertical mode => ZoomableRecycleView handles gestures - if (isImageView || Preferences.Constant.VIEWER_ORIENTATION_VERTICAL == viewerOrientation) { - recyclerView?.setTapListener(itemTouchListener) - imgView.setOnTouchListener(null) - } else { // Horizontal SSIV => SSIV handles gestures - recyclerView?.setTapListener(null) - imgView.setOnTouchListener(itemTouchListener) - } - } + (recyclerView?.findViewHolderForAdapterPosition(position) as ImageViewHolder?)?.setTapListener() } } inner class ImageViewHolder(val rootView: View) : RecyclerView.ViewHolder(rootView), OnImageEventListener { - val ssiv: CustomSubsamplingScaleImageView = itemView.requireById(R.id.ssiv) + private val ssiv: CustomSubsamplingScaleImageView = itemView.requireById(R.id.ssiv) private val imageView: ImageView = itemView.requireById(R.id.imageview) - val noImgTxt: TextView = itemView.requireById(R.id.viewer_no_page_txt) + private val noImgTxt: TextView = itemView.requireById(R.id.viewer_no_page_txt) - lateinit var imgView: View + private lateinit var imgView: View - var displayMode: Int = 0 - var viewerOrientation: Int = 0 - var isSmoothRendering: Boolean = false + private var displayMode = 0 + var viewerOrientation = 0 + private var isSmoothRendering = false - var isImageView = false - var forceImageView: Boolean? = null + private var isImageView = false + private var forceImageView: Boolean? = null private var img: ImageFile? = null private var scaleMultiplier = 1f // When used with ZoomableFrame in vertical mode - var isLoading = AtomicBoolean(false) + private var isLoading = AtomicBoolean(false) + + fun bind( + displayParams: ReaderPagerFragment.DisplayParams, + imgViewType: ViewType, + position: Int + ) { + viewerOrientation = displayParams.orientation + displayMode = displayParams.displayMode + isSmoothRendering = displayParams.isSmoothRendering + + if (ViewType.DEFAULT == imgViewType) { + // ImageView shouldn't react to click events when in vertical mode (controlled by ZoomableFrame / ZoomableRecyclerView) + if (Preferences.Constant.VIEWER_ORIENTATION_VERTICAL == viewerOrientation) { + imageView.isClickable = false + imageView.isFocusable = false + } + } else if (ViewType.IMAGEVIEW_STRETCH == imgViewType) { + imageView.scaleType = ImageView.ScaleType.FIT_XY + } else if (ViewType.SSIV_VERTICAL == imgViewType) { + ssiv.setIgnoreTouchEvents(true) + ssiv.setDirection(CustomSubsamplingScaleImageView.Direction.VERTICAL) + } + + // Avoid stacking 0-px tall images on screen and load all of them at the same time + if (Preferences.Constant.VIEWER_ORIENTATION_VERTICAL == viewerOrientation) + rootView.minimumHeight = pageMinHeight + + val imageType = getImageType(getImageAt(position)) + if (forceImageView != null) { // ImageView has been forced + switchImageView(forceImageView!!, true) + forceImageView = null // Reset force flag + } else if (ImageType.IMG_TYPE_GIF == imageType || ImageType.IMG_TYPE_APNG == imageType) { + animationAlertListener?.run() + switchImageView(isImageView = true, isClickThrough = true) + } else switchImageView(imgViewType == ViewType.IMAGEVIEW_STRETCH) + + // Initialize SSIV when required + if (imgViewType == ViewType.DEFAULT && Preferences.Constant.VIEWER_ORIENTATION_HORIZONTAL == viewerOrientation && !isImageView) { + if (isSmoothRendering) ssiv.setGlEsRenderer(glEsRenderer) + else ssiv.setGlEsRenderer(null) + ssiv.setPreloadDimensions(itemView.width, imgView.height) + if (!Preferences.isReaderZoomTransitions()) ssiv.setDoubleTapZoomDuration(10) + + val scrollLTR = + Preferences.Constant.VIEWER_DIRECTION_LTR == displayParams.direction && isScrollLTR + ssiv.setOffsetLeftSide(scrollLTR) + ssiv.setScaleListener { s -> onAbsoluteScaleChanged(position, s.toFloat()) } + } + val layoutStyle = + if (Preferences.Constant.VIEWER_ORIENTATION_VERTICAL == viewerOrientation) ViewGroup.LayoutParams.WRAP_CONTENT else ViewGroup.LayoutParams.MATCH_PARENT + val layoutParams = imgView.layoutParams + layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT + layoutParams.height = layoutStyle + imgView.layoutParams = layoutParams + + var imageAvailable = true + val img = getImageAt(position) + if (img != null && img.fileUri.isNotEmpty()) setImage(img) + else imageAvailable = false + + val isStreaming = img != null && !imageAvailable && img.status == StatusContent.ONLINE + val isExtracting = img != null && !imageAvailable && !img.url.startsWith("http") + + @StringRes var text: Int = R.string.image_not_found + if (isStreaming) text = R.string.image_streaming + else if (isExtracting) text = R.string.image_extracting + noImgTxt.setText(text) + noImgTxt.isVisible = !imageAvailable + } - fun setImage(img: ImageFile) { + private fun setImage(img: ImageFile) { this.img = img val imgType = getImageType(img) val uri = Uri.parse(img.fileUri) @@ -456,7 +425,6 @@ class ImagePagerAdapter(val context: Context) : } else { // ImageView val view = imgView as ImageView Timber.d("Picture $absoluteAdapterPosition : Using Coil") - Timber.d("Using uri $uri") val transformation = if (autoRotate) SmartRotateTransformation( 90f, screenWidth, @@ -472,6 +440,23 @@ class ImagePagerAdapter(val context: Context) : } } + suspend fun setTapListener() { + withContext(Dispatchers.Default) { + var iterations = 0 // Wait for 5 secs max + while (isLoading.get() && iterations++ < 33) pause(150) + } + // ImageView or vertical mode => ZoomableRecycleView handles gestures + if (isImageView || Preferences.Constant.VIEWER_ORIENTATION_VERTICAL == viewerOrientation) { + Timber.d("setTapListener on recyclerView") + recyclerView?.setTapListener(itemTouchListener) + imgView.setOnTouchListener(null) + } else { // Horizontal SSIV => SSIV handles gestures + Timber.d("setTapListener on imageView") + recyclerView?.setTapListener(null) + imgView.setOnTouchListener(itemTouchListener) + } + } + private val scaleType: CustomSubsamplingScaleImageView.ScaleType get() = if (Preferences.Constant.VIEWER_DISPLAY_FILL == displayMode) { CustomSubsamplingScaleImageView.ScaleType.SMART_FILL @@ -561,7 +546,7 @@ class ImagePagerAdapter(val context: Context) : } } - fun switchImageView(isImageView: Boolean, isClickThrough: Boolean = false) { + private fun switchImageView(isImageView: Boolean, isClickThrough: Boolean = false) { Timber.d( "Picture %d : switching to %s (%s)", absoluteAdapterPosition, @@ -584,6 +569,29 @@ class ImagePagerAdapter(val context: Context) : forceImageView = true } + fun clear() { + if (isImageView) imageView.dispose() + else ssiv.clear() + } + + @SuppressLint("ClickableViewAccessibility") + fun reset() { + clear() + imageView.isClickable = true + imageView.isFocusable = true + imageView.scaleType = ImageView.ScaleType.FIT_CENTER + imageView.setOnTouchListener(null) + + ssiv.setIgnoreTouchEvents(false) + ssiv.setDirection(CustomSubsamplingScaleImageView.Direction.HORIZONTAL) + ssiv.setPreferredBitmapConfig(colorDepth) + ssiv.setDoubleTapZoomDuration(500) + ssiv.setOnTouchListener(null) + + rootView.minimumHeight = 0 + noImgTxt.isVisible = false + } + // == SUBSAMPLINGSCALEVIEW CALLBACKS override fun onReady() { if (Preferences.Constant.VIEWER_ORIENTATION_VERTICAL == viewerOrientation) { From 97208babad715ba4367ecf257b2a05d592356cd6 Mon Sep 17 00:00:00 2001 From: Robb <35880555+robbwatershed@users.noreply.github.com> Date: Sun, 27 Oct 2024 20:55:18 +0100 Subject: [PATCH 37/44] Reader : animate Coil images even with transformations --- .../customssiv/CustomSubsamplingScaleImageView.kt | 3 +-- .../devsaki/hentoid/adapters/ImagePagerAdapter.kt | 15 +++++++++------ .../hentoid/util/image/NullTransformation.kt | 13 ------------- 3 files changed, 10 insertions(+), 21 deletions(-) delete mode 100644 app/src/main/java/me/devsaki/hentoid/util/image/NullTransformation.kt diff --git a/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/CustomSubsamplingScaleImageView.kt b/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/CustomSubsamplingScaleImageView.kt index b5e42192a..9f93d2624 100644 --- a/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/CustomSubsamplingScaleImageView.kt +++ b/app/customssiv/src/main/java/me/devsaki/hentoid/customssiv/CustomSubsamplingScaleImageView.kt @@ -554,7 +554,7 @@ open class CustomSubsamplingScaleImageView(context: Context, attr: AttributeSet? * @param previewSource Optional source for a preview image to be displayed and allow interaction while the full size image loads. * @param state State to be restored. Nullable. */ - fun setImage(imageSource: ImageSource, previewSource: ImageSource?, state: ImageViewState?) { + private fun setImage(imageSource: ImageSource, previewSource: ImageSource?, state: ImageViewState?) { reset(true) if (state != null) restoreState(state) val targetScale = if ((null == state)) 1f else getVirtualScale() @@ -575,7 +575,6 @@ open class CustomSubsamplingScaleImageView(context: Context, attr: AttributeSet? Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.packageName + "/" + previewSource.getResource()) } if (previewSourceUri != null) { - lifecycleScope?.launch { try { val bmp = withContext(Dispatchers.IO) { diff --git a/app/src/main/java/me/devsaki/hentoid/adapters/ImagePagerAdapter.kt b/app/src/main/java/me/devsaki/hentoid/adapters/ImagePagerAdapter.kt index cafdca349..474b6f033 100644 --- a/app/src/main/java/me/devsaki/hentoid/adapters/ImagePagerAdapter.kt +++ b/app/src/main/java/me/devsaki/hentoid/adapters/ImagePagerAdapter.kt @@ -21,6 +21,7 @@ import coil3.dispose import coil3.load import coil3.request.ErrorResult import coil3.request.SuccessResult +import coil3.request.allowConversionToBitmap import coil3.request.transformations import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Runnable @@ -42,7 +43,6 @@ import me.devsaki.hentoid.gles_renderer.GPUImage import me.devsaki.hentoid.util.Preferences import me.devsaki.hentoid.util.Settings import me.devsaki.hentoid.util.file.getExtension -import me.devsaki.hentoid.util.image.NullTransformation import me.devsaki.hentoid.util.image.SmartRotateTransformation import me.devsaki.hentoid.util.image.screenHeight import me.devsaki.hentoid.util.image.screenWidth @@ -425,17 +425,20 @@ class ImagePagerAdapter(val context: Context) : } else { // ImageView val view = imgView as ImageView Timber.d("Picture $absoluteAdapterPosition : Using Coil") - val transformation = if (autoRotate) SmartRotateTransformation( - 90f, - screenWidth, - screenHeight - ) else NullTransformation() + val transformation = if (autoRotate) listOf( + SmartRotateTransformation( + 90f, + screenWidth, + screenHeight + ) + ) else emptyList() view.load(uri) { transformations(transformation) listener( onError = { _, err -> onCoilLoadFailed(err) }, onSuccess = { _, res -> onCoilLoadSuccess(res) } ) + allowConversionToBitmap(false) } } } diff --git a/app/src/main/java/me/devsaki/hentoid/util/image/NullTransformation.kt b/app/src/main/java/me/devsaki/hentoid/util/image/NullTransformation.kt deleted file mode 100644 index eb985e1a1..000000000 --- a/app/src/main/java/me/devsaki/hentoid/util/image/NullTransformation.kt +++ /dev/null @@ -1,13 +0,0 @@ -package me.devsaki.hentoid.util.image - -import android.graphics.Bitmap -import coil3.size.Size -import coil3.transform.Transformation - -class NullTransformation : Transformation() { - override val cacheKey = "${this::class.qualifiedName}" - - override suspend fun transform(input: Bitmap, size: Size): Bitmap { - return input - } -} \ No newline at end of file From 8afa9795283ca66eb458cd27ce10e97b05d08755 Mon Sep 17 00:00:00 2001 From: Robb <35880555+robbwatershed@users.noreply.github.com> Date: Mon, 28 Oct 2024 07:42:30 +0100 Subject: [PATCH 38/44] Add adapters --- .../me/devsaki/hentoid/json/sources/EHentaiGalleryQuery.kt | 3 +++ .../me/devsaki/hentoid/json/sources/EHentaiImageMetadata.kt | 3 +++ .../java/me/devsaki/hentoid/json/sources/EHentaiImageQuery.kt | 3 +++ 3 files changed, 9 insertions(+) diff --git a/app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiGalleryQuery.kt b/app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiGalleryQuery.kt index 27aa301a6..91d16a25e 100644 --- a/app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiGalleryQuery.kt +++ b/app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiGalleryQuery.kt @@ -1,5 +1,8 @@ package me.devsaki.hentoid.json.sources +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) data class EHentaiGalleryQuery( val gidlist: List> = ArrayList(), val method: String = "gdata", diff --git a/app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiImageMetadata.kt b/app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiImageMetadata.kt index b7695e120..1a7f08147 100644 --- a/app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiImageMetadata.kt +++ b/app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiImageMetadata.kt @@ -1,5 +1,8 @@ package me.devsaki.hentoid.json.sources +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) data class EHentaiImageMetadata( val n: String, val k: String, diff --git a/app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiImageQuery.kt b/app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiImageQuery.kt index 95245d890..4a25e09bd 100644 --- a/app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiImageQuery.kt +++ b/app/src/main/java/me/devsaki/hentoid/json/sources/EHentaiImageQuery.kt @@ -1,5 +1,8 @@ package me.devsaki.hentoid.json.sources +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) data class EHentaiImageQuery( val gid: Int, val imgkey: String, From 3add9646ebef79d230b094c105558f5039a695c1 Mon Sep 17 00:00:00 2001 From: Robb <35880555+robbwatershed@users.noreply.github.com> Date: Mon, 28 Oct 2024 21:29:42 +0100 Subject: [PATCH 39/44] Reader / stretched : Fix touch + enable zoom --- .../hentoid/adapters/ImagePagerAdapter.kt | 51 ++++++++++++------- .../fragments/reader/ReaderPagerFragment.kt | 24 ++++----- 2 files changed, 45 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/me/devsaki/hentoid/adapters/ImagePagerAdapter.kt b/app/src/main/java/me/devsaki/hentoid/adapters/ImagePagerAdapter.kt index 474b6f033..0c28ce6f2 100644 --- a/app/src/main/java/me/devsaki/hentoid/adapters/ImagePagerAdapter.kt +++ b/app/src/main/java/me/devsaki/hentoid/adapters/ImagePagerAdapter.kt @@ -32,7 +32,6 @@ import me.devsaki.hentoid.core.BiConsumer import me.devsaki.hentoid.core.requireById import me.devsaki.hentoid.customssiv.CustomSubsamplingScaleImageView import me.devsaki.hentoid.customssiv.CustomSubsamplingScaleImageView.OnImageEventListener -import me.devsaki.hentoid.customssiv.exception.UnsupportedContentException import me.devsaki.hentoid.customssiv.uri import me.devsaki.hentoid.customssiv.util.lifecycleScope import me.devsaki.hentoid.database.domains.ImageFile @@ -87,7 +86,7 @@ class ImagePagerAdapter(val context: Context) : private var itemTouchListener: OnZoneTapListener? = null private var scaleListener: BiConsumer? = null - private var animationAlertListener: Runnable? = null + private var ssivAlertListener: Runnable? = null private var recyclerView: ZoomableRecyclerView? = null private val initialAbsoluteScales: MutableMap = HashMap() @@ -147,8 +146,8 @@ class ImagePagerAdapter(val context: Context) : this.itemTouchListener = itemTouchListener } - fun setAnimationAlertListener(animationAlertListener: Runnable) { - this.animationAlertListener = animationAlertListener + fun setSsivAlertListener(ssivAlertListener: Runnable) { + this.ssivAlertListener = ssivAlertListener } private fun getImageType(img: ImageFile?): ImageType { @@ -211,7 +210,7 @@ class ImagePagerAdapter(val context: Context) : fun destroy() { if (isGlInit) glEsRenderer.clear() scaleListener = null - animationAlertListener = null + ssivAlertListener = null itemTouchListener = null } @@ -236,8 +235,8 @@ class ImagePagerAdapter(val context: Context) : } fun getDimensionsAtPosition(position: Int): Point { - (recyclerView?.findViewHolderForAdapterPosition(position) as ImageViewHolder?)?.let { holder -> - return holder.dimensions + (recyclerView?.findViewHolderForAdapterPosition(position) as ImageViewHolder?)?.let { + return it.dimensions } return Point() } @@ -246,8 +245,8 @@ class ImagePagerAdapter(val context: Context) : if (absoluteScales.containsKey(position)) { val result = absoluteScales[position] if (result != null) return result - } else (recyclerView?.findViewHolderForAdapterPosition(position) as ImageViewHolder?)?.let { holder -> - return holder.absoluteScale + } else (recyclerView?.findViewHolderForAdapterPosition(position) as ImageViewHolder?)?.let { + return it.absoluteScale } return 0f } @@ -273,6 +272,16 @@ class ImagePagerAdapter(val context: Context) : } } + fun getSSivAtPosition(position: Int): Boolean { + var res = false + recyclerView?.lifecycleScope?.launch { + res = + (recyclerView?.findViewHolderForAdapterPosition(position) as ImageViewHolder?)?.isImageView() + ?: false + } + return res + } + fun resetScaleAtPosition(position: Int) { (recyclerView?.findViewHolderForAdapterPosition(position) as ImageViewHolder?)?.resetScale() } @@ -313,6 +322,7 @@ class ImagePagerAdapter(val context: Context) : } } + // ====================== VIEWHOLDER inner class ImageViewHolder(val rootView: View) : RecyclerView.ViewHolder(rootView), OnImageEventListener { @@ -364,9 +374,12 @@ class ImagePagerAdapter(val context: Context) : switchImageView(forceImageView!!, true) forceImageView = null // Reset force flag } else if (ImageType.IMG_TYPE_GIF == imageType || ImageType.IMG_TYPE_APNG == imageType) { - animationAlertListener?.run() + ssivAlertListener?.run() switchImageView(isImageView = true, isClickThrough = true) - } else switchImageView(imgViewType == ViewType.IMAGEVIEW_STRETCH) + } else switchImageView( + imgViewType == ViewType.IMAGEVIEW_STRETCH, + imgViewType == ViewType.IMAGEVIEW_STRETCH + ) // Initialize SSIV when required if (imgViewType == ViewType.DEFAULT && Preferences.Constant.VIEWER_ORIENTATION_HORIZONTAL == viewerOrientation && !isImageView) { @@ -444,12 +457,8 @@ class ImagePagerAdapter(val context: Context) : } suspend fun setTapListener() { - withContext(Dispatchers.Default) { - var iterations = 0 // Wait for 5 secs max - while (isLoading.get() && iterations++ < 33) pause(150) - } // ImageView or vertical mode => ZoomableRecycleView handles gestures - if (isImageView || Preferences.Constant.VIEWER_ORIENTATION_VERTICAL == viewerOrientation) { + if (isImageView() || Preferences.Constant.VIEWER_ORIENTATION_VERTICAL == viewerOrientation) { Timber.d("setTapListener on recyclerView") recyclerView?.setTapListener(itemTouchListener) imgView.setOnTouchListener(null) @@ -460,6 +469,14 @@ class ImagePagerAdapter(val context: Context) : } } + suspend fun isImageView(): Boolean { + withContext(Dispatchers.Default) { + var iterations = 0 // Wait for 5 secs max + while (isLoading.get() && iterations++ < 33) pause(150) + } + return isImageView + } + private val scaleType: CustomSubsamplingScaleImageView.ScaleType get() = if (Preferences.Constant.VIEWER_DISPLAY_FILL == displayMode) { CustomSubsamplingScaleImageView.ScaleType.SMART_FILL @@ -626,8 +643,6 @@ class ImagePagerAdapter(val context: Context) : forceImageView() // Reload adapter notifyItemChanged(layoutPosition) - // Notify the listener of the presence of an animation - if (e is UnsupportedContentException) animationAlertListener?.run() } override fun onTileLoadError(e: Throwable) { diff --git a/app/src/main/java/me/devsaki/hentoid/fragments/reader/ReaderPagerFragment.kt b/app/src/main/java/me/devsaki/hentoid/fragments/reader/ReaderPagerFragment.kt index 791fad368..91ff6558f 100644 --- a/app/src/main/java/me/devsaki/hentoid/fragments/reader/ReaderPagerFragment.kt +++ b/app/src/main/java/me/devsaki/hentoid/fragments/reader/ReaderPagerFragment.kt @@ -126,7 +126,7 @@ class ReaderPagerFragment : Fragment(R.layout.fragment_reader_pager), private var latestSlideshowTick: Long = -1 private var displayParams: DisplayParams? = null - private var isImageAnimated = false + private var useSsiv = false // Properties // Preferences of current book; to feed the book prefs dialog @@ -438,10 +438,10 @@ class ReaderPagerFragment : Fragment(R.layout.fragment_reader_pager), adapter.setOnScaleListener { position, scale -> if (position == absImageIndex) adapterRescaleDebouncer.submit(scale) } - adapter.setAnimationAlertListener { - isImageAnimated = true + adapter.setSsivAlertListener { + useSsiv = false displayParams?.apply { - if (!hasAnimation) { + if (useSsiv) { onDisplayParamsChange( DisplayParams(browseMode, displayMode, isSmoothRendering, true) ) @@ -760,11 +760,13 @@ class ReaderPagerFragment : Fragment(R.layout.fragment_reader_pager), binding?.apply { adapter.reset() // Required to communicate tapListener to the Adapter before images are binded + /* images.firstOrNull()?.let { it.content.reach(it)?.apply { adjustDisplay(bookPreferences) } } + */ adapter.submitList(images) { differEndCallback() } if (images.isEmpty()) { setSystemBarsVisible(true) @@ -954,7 +956,7 @@ class ReaderPagerFragment : Fragment(R.layout.fragment_reader_pager), private fun onPageChanged(absImageIndex: Int, scrollDirection: Int) { currentImg?.let { it.content.reach(it)?.apply { - adjustDisplay(bookPreferences) + adjustDisplay(bookPreferences, absImageIndex) } } viewModel.onPageChange(absImageIndex, scrollDirection) @@ -1034,12 +1036,12 @@ class ReaderPagerFragment : Fragment(R.layout.fragment_reader_pager), } } - private fun adjustDisplay(bookPreferences: Map) { + private fun adjustDisplay(bookPreferences: Map, absImageIndex: Int) { val newDisplayParams = DisplayParams( Preferences.getContentBrowseMode(bookPreferences), Preferences.getContentDisplayMode(bookPreferences), Preferences.isContentSmoothRendering(bookPreferences), - displayParams?.hasAnimation ?: false + adapter.getSSivAtPosition(absImageIndex) ) if (null == displayParams || newDisplayParams != displayParams) onDisplayParamsChange(newDisplayParams) @@ -1061,7 +1063,7 @@ class ReaderPagerFragment : Fragment(R.layout.fragment_reader_pager), val isDirectionChange = (null == currentDisplayParams || currentDisplayParams.direction != newDisplayParams.direction) val isAnimationChange = - (null == currentDisplayParams || currentDisplayParams.hasAnimation != newDisplayParams.hasAnimation) + (null == currentDisplayParams || currentDisplayParams.useSsiv != newDisplayParams.useSsiv) displayParams = newDisplayParams var isZoomFrameEnabled = false @@ -1069,9 +1071,7 @@ class ReaderPagerFragment : Fragment(R.layout.fragment_reader_pager), binding?.apply { // Animated picture appears in horizontal mode => enable zoom frame if (isAnimationChange && VIEWER_ORIENTATION_HORIZONTAL == newDisplayParams.orientation) { - if (newDisplayParams.hasAnimation || isImageAnimated) { - isZoomFrameEnabled = true - } + if (!newDisplayParams.useSsiv || !useSsiv) isZoomFrameEnabled = true } // Orientation changes @@ -1590,7 +1590,7 @@ class ReaderPagerFragment : Fragment(R.layout.fragment_reader_pager), val browseMode: Int, val displayMode: Int, val isSmoothRendering: Boolean, - val hasAnimation: Boolean + val useSsiv: Boolean ) { val orientation = if (browseMode == VIEWER_BROWSE_TTB) VIEWER_ORIENTATION_VERTICAL else VIEWER_ORIENTATION_HORIZONTAL From f9d4f56378ace511d1ed6bea5913443f15163bd0 Mon Sep 17 00:00:00 2001 From: Robb <35880555+robbwatershed@users.noreply.github.com> Date: Wed, 30 Oct 2024 22:28:54 +0100 Subject: [PATCH 40/44] FileHelper.copyFiles (WIP) --- .../activities/sources/ExHentaiActivity.kt | 1 - .../hentoid/parsers/images/MangagoParser.kt | 4 +- .../me/devsaki/hentoid/util/ContentHelper.kt | 38 +++--- .../hentoid/util/download/DownloadHelper.kt | 3 +- .../devsaki/hentoid/util/file/FileHelper.kt | 127 ++++++++++++++---- .../viewmodels/PreferencesViewModel.kt | 26 ++-- .../hentoid/viewmodels/ReaderViewModel.kt | 1 - .../hentoid/workers/ContentDownloadWorker.kt | 8 +- .../hentoid/workers/SplitMergeWorker.kt | 32 ++--- .../hentoid/workers/TransformWorker.kt | 1 - 10 files changed, 156 insertions(+), 85 deletions(-) diff --git a/app/src/main/java/me/devsaki/hentoid/activities/sources/ExHentaiActivity.kt b/app/src/main/java/me/devsaki/hentoid/activities/sources/ExHentaiActivity.kt index 61aee158d..d923d34df 100644 --- a/app/src/main/java/me/devsaki/hentoid/activities/sources/ExHentaiActivity.kt +++ b/app/src/main/java/me/devsaki/hentoid/activities/sources/ExHentaiActivity.kt @@ -16,7 +16,6 @@ import me.devsaki.hentoid.parsers.content.ContentParser import me.devsaki.hentoid.parsers.content.ExhentaiContent import me.devsaki.hentoid.parsers.images.EHentaiParser import me.devsaki.hentoid.parsers.images.EHentaiParser.EhAuthState -import me.devsaki.hentoid.util.Preferences import me.devsaki.hentoid.util.Settings import me.devsaki.hentoid.util.file.findOrCreateDocumentFile import me.devsaki.hentoid.util.file.getDocumentFromTreeUriString diff --git a/app/src/main/java/me/devsaki/hentoid/parsers/images/MangagoParser.kt b/app/src/main/java/me/devsaki/hentoid/parsers/images/MangagoParser.kt index 7056dc7fb..c0ada224d 100644 --- a/app/src/main/java/me/devsaki/hentoid/parsers/images/MangagoParser.kt +++ b/app/src/main/java/me/devsaki/hentoid/parsers/images/MangagoParser.kt @@ -17,7 +17,7 @@ import me.devsaki.hentoid.parsers.getImgSrc import me.devsaki.hentoid.parsers.urlToImageFile import me.devsaki.hentoid.util.download.getDownloadLocation import me.devsaki.hentoid.util.exception.EmptyResultException -import me.devsaki.hentoid.util.file.getFileFromSingleUriString +import me.devsaki.hentoid.util.file.getFileFromSingleUri import me.devsaki.hentoid.util.getOrCreateContentDownloadDir import me.devsaki.hentoid.util.image.MIME_IMAGE_GENERIC import me.devsaki.hentoid.util.network.UriParts @@ -190,7 +190,7 @@ class MangagoParser : BaseChapteredImageListParser() { img.computeName(5) img.setChapter(chp) // Enrich physical properties - getFileFromSingleUriString(context, fileUri.toString())?.let { + getFileFromSingleUri(context, fileUri)?.let { img.size = it.length() img.mimeType = it.type ?: MIME_IMAGE_GENERIC } diff --git a/app/src/main/java/me/devsaki/hentoid/util/ContentHelper.kt b/app/src/main/java/me/devsaki/hentoid/util/ContentHelper.kt index 7c31a4e77..061aa6c31 100644 --- a/app/src/main/java/me/devsaki/hentoid/util/ContentHelper.kt +++ b/app/src/main/java/me/devsaki/hentoid/util/ContentHelper.kt @@ -1536,22 +1536,23 @@ fun purgeFiles( val tempFiles: MutableList = ArrayList() var tempFolder: File? = null if (filesToKeep.isNotEmpty()) { - tempFolder = getOrCreateCacheFolder(context, "tmp" + content.id) - for (file in filesToKeep) { - try { - val uri = copyFile( - context, - file.uri, - Uri.fromFile(tempFolder), - file.type ?: "", - file.name ?: "" - ) - if (uri != null) { - val tmpFile = legacyFileFromUri(uri) - if (tmpFile != null) tempFiles.add(tmpFile) + getOrCreateCacheFolder(context, "tmp" + content.id)?.let { tmp -> + tempFolder = tmp + for (file in filesToKeep) { + try { + val uri = copyFile( + context, + file.uri, + tmp, + file.name ?: "" + ) + if (uri != null) { + val tmpFile = legacyFileFromUri(uri) + if (tmpFile != null) tempFiles.add(tmpFile) + } + } catch (e: IOException) { + Timber.w(e) } - } catch (e: IOException) { - Timber.w(e) } } } @@ -1582,7 +1583,7 @@ fun purgeFiles( val newUri = copyFile( context, Uri.fromFile(file), - bookFolder.uri, + bookFolder, mimeType, file.name ) @@ -2208,9 +2209,10 @@ fun mergeContents( val newUri = copyFile( context, Uri.parse(img.fileUri), - targetFolder.uri, + targetFolder, newImg.mimeType, - newImg.name + "." + getExtensionFromUri(img.fileUri) + newImg.name + "." + getExtensionFromUri(img.fileUri), + true ) if (newUri != null) newImg.fileUri = newUri.toString() else Timber.w("Could not move file %s", img.fileUri) diff --git a/app/src/main/java/me/devsaki/hentoid/util/download/DownloadHelper.kt b/app/src/main/java/me/devsaki/hentoid/util/download/DownloadHelper.kt index 9f10fae0d..b03fcf291 100644 --- a/app/src/main/java/me/devsaki/hentoid/util/download/DownloadHelper.kt +++ b/app/src/main/java/me/devsaki/hentoid/util/download/DownloadHelper.kt @@ -21,6 +21,7 @@ import me.devsaki.hentoid.util.file.fileSizeFromUri import me.devsaki.hentoid.util.file.findOrCreateDocumentFile import me.devsaki.hentoid.util.file.formatHumanReadableSize import me.devsaki.hentoid.util.file.getDocumentFromTreeUriString +import me.devsaki.hentoid.util.file.getDocumentFromTreeUri import me.devsaki.hentoid.util.file.getExtensionFromMimeType import me.devsaki.hentoid.util.file.getOutputStream import me.devsaki.hentoid.util.file.isUriPermissionPersisted @@ -281,7 +282,7 @@ fun createFile( throw IOException("Could not create file $targetFileNameFinal : $targetFolderUri has no path") } } else { - getDocumentFromTreeUriString(context, targetFolderUri.toString())?.let { targetFolder -> + getDocumentFromTreeUri(context, targetFolderUri)?.let { targetFolder -> val file = findOrCreateDocumentFile( context, targetFolder, diff --git a/app/src/main/java/me/devsaki/hentoid/util/file/FileHelper.kt b/app/src/main/java/me/devsaki/hentoid/util/file/FileHelper.kt index 696ca1a26..069be43d4 100644 --- a/app/src/main/java/me/devsaki/hentoid/util/file/FileHelper.kt +++ b/app/src/main/java/me/devsaki/hentoid/util/file/FileHelper.kt @@ -62,7 +62,7 @@ private const val ILLEGAL_FILENAME_CHARS = "[\"*/:<>\\?\\\\|]" const val URI_ELEMENTS_SEPARATOR = "%3A" -const val FILE_IO_BUFFER_SIZE = 32 * 1024 +const val FILE_IO_BUFFER_SIZE = 64 * 1024 /** @@ -74,7 +74,11 @@ const val FILE_IO_BUFFER_SIZE = 32 * 1024 */ fun getFileFromSingleUriString(context: Context, uriStr: String): DocumentFile? { if (uriStr.isEmpty()) return null - val result = DocumentFile.fromSingleUri(context, Uri.parse(uriStr)) + return getFileFromSingleUri(context, Uri.parse(uriStr)) +} + +fun getFileFromSingleUri(context: Context, uri: Uri): DocumentFile? { + val result = DocumentFile.fromSingleUri(context, uri) return if (null == result || !result.exists()) null else result } @@ -88,7 +92,11 @@ fun getFileFromSingleUriString(context: Context, uriStr: String): DocumentFile? */ fun getDocumentFromTreeUriString(context: Context, treeUriStr: String): DocumentFile? { if (treeUriStr.isEmpty()) return null - val result = DocumentFile.fromTreeUri(context, Uri.parse(treeUriStr)) + return getDocumentFromTreeUri(context, Uri.parse(treeUriStr)) +} + +fun getDocumentFromTreeUri(context: Context, treeUri: Uri): DocumentFile? { + val result = DocumentFile.fromTreeUri(context, treeUri) return if (null == result || !result.exists()) null else result } @@ -316,7 +324,7 @@ fun getOutputStream(context: Context, fileUri: Uri): OutputStream? { val path = fileUri.path if (null != path) return getOutputStream(File(path)) } else { - val doc = getFileFromSingleUriString(context, fileUri.toString()) + val doc = getFileFromSingleUri(context, fileUri) if (doc != null) return getOutputStream(context, doc) } throw IOException("Couldn't find document for Uri : $fileUri") @@ -370,7 +378,7 @@ fun removeFile(context: Context, fileUri: Uri) { val path = fileUri.path if (null != path) removeFile(File(path)) } else { - val doc = getFileFromSingleUriString(context, fileUri.toString()) + val doc = getFileFromSingleUri(context, fileUri) doc?.delete() } } @@ -499,7 +507,11 @@ fun listFoldersFilter( * @param filter Name filter to use to filter the files to list * @return Files of the given parent folder matching the given name filter */ -fun listFiles(context: Context, parent: DocumentFile, filter: NameFilter?): List { +fun listFiles( + context: Context, + parent: DocumentFile, + filter: NameFilter? = null +): List { var result = emptyList() try { FileExplorer(context, parent).use { fe -> @@ -809,35 +821,79 @@ fun findSequencePosition(data: ByteArray, initialPos: Int, sequence: ByteArray, * @param context Context to use * @param sourceFileUri Uri of the source file to copy * @param targetFolderUri Uri of the folder where to copy the source file - * @param mimeType Mime-type of the source file + * @param forceMime Mime-type of the source file * @param newName Filename to give of the copy * @return Uri of the copied file, if successful; null if failed * @throws IOException If something terrible happens */ @Throws(IOException::class) -fun copyFile( +fun copyFiles( context: Context, - sourceFileUri: Uri, + operations: List>, // source Uri, target name targetFolderUri: Uri, - mimeType: String, - newName: String -): Uri? { - if (!fileExists(context, sourceFileUri)) return null + forceMime: String? = null, + isCanceled: (() -> Boolean)? = null, + onProgress: (Float, Uri, Uri?) -> Unit, +): List { + val result = ArrayList() + val nbElts = operations.size - val targetFileUri: Uri if (ContentResolver.SCHEME_FILE == targetFolderUri.scheme) { val targetFolder = legacyFileFromUri(targetFolderUri) - if (null == targetFolder || !targetFolder.exists()) return null - val targetFile = File(targetFolder, newName) - if (!targetFile.exists() && !targetFile.createNewFile()) return null - targetFileUri = Uri.fromFile(targetFile) + if (null == targetFolder || !targetFolder.exists()) return emptyList() + operations.forEachIndexed { index, op -> + val targetFile = File(targetFolder, op.second) + val newUri = if (targetFile.exists() || targetFile.createNewFile()) { + getOutputStream(targetFile).use { output -> + getInputStream(context, op.first) + .use { input -> copy(input, output) } + } + Uri.fromFile(targetFile) + } else null + if (newUri != null) result.add(newUri) + onProgress(index * 1f / nbElts, op.first, newUri) + if (isCanceled?.invoke() == true) return@forEachIndexed + } } else { val targetFolder = DocumentFile.fromTreeUri(context, targetFolderUri) - if (null == targetFolder || !targetFolder.exists()) return null - val targetFile = findOrCreateDocumentFile(context, targetFolder, mimeType, newName) - if (null == targetFile || !targetFile.exists()) return null - targetFileUri = targetFile.uri + if (null == targetFolder || !targetFolder.exists()) return emptyList() + + val existingFiles = listFiles(context, targetFolder) + .groupBy { it.name ?: "" } + .mapValues { it.value.first() } + + operations.forEachIndexed { index, op -> + val mime = + if (forceMime.isNullOrEmpty()) getMimeTypeFromFileName(op.second) else forceMime + var newUri: Uri? = null + existingFiles[op.second] ?: targetFolder.createFile(mime, op.second)?.let { out -> + getOutputStream(context, out)?.use { output -> + getInputStream(context, op.first) + .use { input -> copy(input, output) } + } + newUri = out.uri + } + newUri?.let { result.add(it) } + onProgress(index * 1f / nbElts, op.first, newUri) + if (isCanceled?.invoke() == true) return@forEachIndexed + } } + return result +} + +@Throws(IOException::class) +fun copyFile( + context: Context, + sourceFileUri: Uri, + targetFolder: File, + newName: String +): Uri? { + if (!targetFolder.exists()) return null + if (!fileExists(context, sourceFileUri)) return null + + val targetFile = File(targetFolder, newName) + if (!targetFile.exists() && !targetFile.createNewFile()) return null + val targetFileUri = Uri.fromFile(targetFile) getOutputStream(context, targetFileUri)?.use { output -> getInputStream(context, sourceFileUri) @@ -846,6 +902,29 @@ fun copyFile( return targetFileUri } +@Throws(IOException::class) +fun copyFile( + context: Context, + sourceFileUri: Uri, + targetFolder: DocumentFile, + mimeType: String, + newName: String, + forceCreate: Boolean = false +): Uri? { + if (!targetFolder.exists()) return null + if (!fileExists(context, sourceFileUri)) return null + + val targetFile = if (forceCreate) targetFolder.createFile(mimeType, newName) + else findOrCreateDocumentFile(context, targetFolder, mimeType, newName) + if (null == targetFile || !targetFile.exists()) return null + + getOutputStream(context, targetFile.uri)?.use { output -> + getInputStream(context, sourceFileUri) + .use { input -> copy(input, output) } + } + return targetFile.uri +} + /** * Get the device's Downloads folder * @@ -1253,7 +1332,7 @@ fun fileExists(context: Context, fileUri: Uri): Boolean { return if (path != null) File(path).exists() else false } else { - val doc = getFileFromSingleUriString(context, fileUri.toString()) + val doc = getFileFromSingleUri(context, fileUri) return (doc != null) } } @@ -1278,7 +1357,7 @@ fun fileSizeFromUri(context: Context, fileUri: Uri): Long { val path = fileUri.path if (path != null) return File(path).length() } else { - val doc = getFileFromSingleUriString(context, fileUri.toString()) + val doc = getFileFromSingleUri(context, fileUri) if (doc != null) return doc.length() } return -1 diff --git a/app/src/main/java/me/devsaki/hentoid/viewmodels/PreferencesViewModel.kt b/app/src/main/java/me/devsaki/hentoid/viewmodels/PreferencesViewModel.kt index bdb5d3d36..13d0c3143 100644 --- a/app/src/main/java/me/devsaki/hentoid/viewmodels/PreferencesViewModel.kt +++ b/app/src/main/java/me/devsaki/hentoid/viewmodels/PreferencesViewModel.kt @@ -12,12 +12,13 @@ import me.devsaki.hentoid.util.Settings import me.devsaki.hentoid.util.detachAllExternalContent import me.devsaki.hentoid.util.detachAllPrimaryContent import me.devsaki.hentoid.util.file.Beholder -import me.devsaki.hentoid.util.file.copyFile +import me.devsaki.hentoid.util.file.copyFiles import me.devsaki.hentoid.util.file.getDocumentFromTreeUriString import me.devsaki.hentoid.util.file.listFiles import me.devsaki.hentoid.util.getOrCreateContentDownloadDir import me.devsaki.hentoid.util.getPathRoot import org.greenrobot.eventbus.EventBus +import timber.log.Timber import java.util.concurrent.atomic.AtomicInteger class PreferencesViewModel(application: Application, val dao: CollectionDAO) : @@ -103,20 +104,17 @@ class PreferencesViewModel(application: Application, val dao: CollectionDAO) : if (sourceFolder != null) { val files = listFiles(getApplication(), sourceFolder, null) // TODO secondary progress for pages - files.forEach { it1 -> - val newUri = copyFile( - getApplication(), - it1.uri, - targetFolder.uri, - it1.type ?: "", - it1.name ?: "" - ) - c.imageFiles.forEach { it2 -> - if (it1.uri.toString() == it2.fileUri) { - it2.fileUri = newUri.toString() - } + copyFiles( + getApplication(), + files.map { Pair(it.uri, it.name ?: "") }, + targetFolder.uri, + onProgress = { _, oldUri, newUri -> + if (newUri != null) { + c.imageFiles.firstOrNull { it.fileUri == oldUri.toString() }?.fileUri = + newUri.toString() + } else Timber.w("Could not move file $oldUri") } - } + ) // Update Content Uris c.setStorageDoc(targetFolder) dao.insertContentCore(c) diff --git a/app/src/main/java/me/devsaki/hentoid/viewmodels/ReaderViewModel.kt b/app/src/main/java/me/devsaki/hentoid/viewmodels/ReaderViewModel.kt index c4ba292ef..028ee8033 100644 --- a/app/src/main/java/me/devsaki/hentoid/viewmodels/ReaderViewModel.kt +++ b/app/src/main/java/me/devsaki/hentoid/viewmodels/ReaderViewModel.kt @@ -52,7 +52,6 @@ import me.devsaki.hentoid.util.file.getDocumentFromTreeUriString import me.devsaki.hentoid.util.file.getFileFromSingleUriString import me.devsaki.hentoid.util.getPictureFilesFromContent import me.devsaki.hentoid.util.image.PdfManager -import me.devsaki.hentoid.util.image.clearCoilCache import me.devsaki.hentoid.util.image.getMimeTypeFromUri import me.devsaki.hentoid.util.matchFilesToImageList import me.devsaki.hentoid.util.network.HEADER_COOKIE_KEY diff --git a/app/src/main/java/me/devsaki/hentoid/workers/ContentDownloadWorker.kt b/app/src/main/java/me/devsaki/hentoid/workers/ContentDownloadWorker.kt index 8a401705a..16f8cafa9 100644 --- a/app/src/main/java/me/devsaki/hentoid/workers/ContentDownloadWorker.kt +++ b/app/src/main/java/me/devsaki/hentoid/workers/ContentDownloadWorker.kt @@ -74,7 +74,7 @@ import me.devsaki.hentoid.util.file.extractArchiveEntries import me.devsaki.hentoid.util.file.fileSizeFromUri import me.devsaki.hentoid.util.file.formatHumanReadableSize import me.devsaki.hentoid.util.file.getDocumentFromTreeUriString -import me.devsaki.hentoid.util.file.getFileFromSingleUriString +import me.devsaki.hentoid.util.file.getFileFromSingleUri import me.devsaki.hentoid.util.file.getOrCreateCacheFolder import me.devsaki.hentoid.util.getOrCreateContentDownloadDir import me.devsaki.hentoid.util.image.MIME_IMAGE_GENERIC @@ -1191,9 +1191,7 @@ class ContentDownloadWorker(context: Context, parameters: WorkerParameters) : // This is run on the I/O thread pool spawned by the downloader private fun onRequestSuccess(request: RequestOrder, fileUri: Uri) { val img = request.img - val imgFile = getFileFromSingleUriString( - applicationContext, fileUri.toString() - ) + val imgFile = getFileFromSingleUri(applicationContext, fileUri) if (imgFile != null) { img.size = imgFile.length() img.mimeType = imgFile.type ?: MIME_IMAGE_GENERIC @@ -1401,7 +1399,7 @@ class ContentDownloadWorker(context: Context, parameters: WorkerParameters) : val finalImgUri = copyFile( applicationContext, ugoiraGifFile, - dir.uri, + dir, MIME_IMAGE_GIF, img.name + ".gif" ) ?: throw IOException("Couldn't copy result ugoira file") diff --git a/app/src/main/java/me/devsaki/hentoid/workers/SplitMergeWorker.kt b/app/src/main/java/me/devsaki/hentoid/workers/SplitMergeWorker.kt index aaf246ad1..e31291771 100644 --- a/app/src/main/java/me/devsaki/hentoid/workers/SplitMergeWorker.kt +++ b/app/src/main/java/me/devsaki/hentoid/workers/SplitMergeWorker.kt @@ -27,7 +27,7 @@ import me.devsaki.hentoid.util.copy import me.devsaki.hentoid.util.createJson import me.devsaki.hentoid.util.exception.ContentNotProcessedException import me.devsaki.hentoid.util.file.Beholder -import me.devsaki.hentoid.util.file.copyFile +import me.devsaki.hentoid.util.file.copyFiles import me.devsaki.hentoid.util.file.getDocumentFromTreeUriString import me.devsaki.hentoid.util.file.getInputStream import me.devsaki.hentoid.util.file.getOutputStream @@ -38,7 +38,6 @@ import me.devsaki.hentoid.util.image.clearCoilCache import me.devsaki.hentoid.util.mergeContents import me.devsaki.hentoid.util.moveContentToCustomGroup import me.devsaki.hentoid.util.network.UriParts -import me.devsaki.hentoid.util.network.getExtensionFromUri import me.devsaki.hentoid.util.notification.BaseNotification import me.devsaki.hentoid.util.persistJson import me.devsaki.hentoid.util.removeContent @@ -155,24 +154,21 @@ abstract class BaseSplitMergeWorker( Beholder.ignoreFolder(targetFolder) // Copy the corresponding images to that folder - val splitContentImages = splitContent.imageList - for (img in splitContentImages) { - if (isStopped) break - if (img.status == StatusContent.DOWNLOADED) { - val extension = getExtensionFromUri(img.fileUri) - val newUri = copyFile( - applicationContext, - Uri.parse(img.fileUri), - targetFolder.uri, - img.mimeType, - img.name + "." + extension - ) - if (newUri != null) img.fileUri = newUri.toString() - else Timber.w("Could not move file %s", img.fileUri) - + val splitContentImages = + splitContent.imageList.filter { it.status == StatusContent.DOWNLOADED } + copyFiles( + applicationContext, + splitContentImages.map { Pair(Uri.parse(it.fileUri), it.name) }, + targetFolder.uri, + isCanceled = this::isStopped, + onProgress = { _, oldUri, newUri -> + if (newUri != null) { + splitContentImages.firstOrNull { it.fileUri == oldUri.toString() }?.fileUri = + newUri.toString() + } else Timber.w("Could not move file $oldUri") progressPlus(chap.name) } - } + ) if (isStopped) break // Save the JSON for the new book diff --git a/app/src/main/java/me/devsaki/hentoid/workers/TransformWorker.kt b/app/src/main/java/me/devsaki/hentoid/workers/TransformWorker.kt index f7be65322..293f649e4 100644 --- a/app/src/main/java/me/devsaki/hentoid/workers/TransformWorker.kt +++ b/app/src/main/java/me/devsaki/hentoid/workers/TransformWorker.kt @@ -7,7 +7,6 @@ import androidx.core.net.toUri import androidx.documentfile.provider.DocumentFile import androidx.work.Data import androidx.work.WorkerParameters -import coil3.SingletonImageLoader import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import kotlinx.coroutines.CoroutineScope From bbfaaefddcf3e38c8851e6b1d347427bbfb4de75 Mon Sep 17 00:00:00 2001 From: Robb <35880555+robbwatershed@users.noreply.github.com> Date: Wed, 30 Oct 2024 23:11:50 +0100 Subject: [PATCH 41/44] Use downloadToFile to download the APK --- .../hentoid/workers/UpdateDownloadWorker.kt | 96 +++++++++++-------- 1 file changed, 56 insertions(+), 40 deletions(-) diff --git a/app/src/main/java/me/devsaki/hentoid/workers/UpdateDownloadWorker.kt b/app/src/main/java/me/devsaki/hentoid/workers/UpdateDownloadWorker.kt index 9b8f5b7c2..cb028c985 100644 --- a/app/src/main/java/me/devsaki/hentoid/workers/UpdateDownloadWorker.kt +++ b/app/src/main/java/me/devsaki/hentoid/workers/UpdateDownloadWorker.kt @@ -1,22 +1,21 @@ package me.devsaki.hentoid.workers import android.content.Context +import android.net.Uri import androidx.documentfile.provider.DocumentFile import androidx.work.Data import androidx.work.WorkerParameters import me.devsaki.hentoid.R +import me.devsaki.hentoid.enums.Site import me.devsaki.hentoid.notification.appUpdate.UpdateFailedNotification import me.devsaki.hentoid.notification.appUpdate.UpdateInstallNotification import me.devsaki.hentoid.notification.appUpdate.UpdateProgressNotification -import me.devsaki.hentoid.util.file.FILE_IO_BUFFER_SIZE -import me.devsaki.hentoid.util.file.getFileUriCompat -import me.devsaki.hentoid.util.file.getOutputStream -import me.devsaki.hentoid.util.network.getOnlineResource +import me.devsaki.hentoid.util.download.downloadToFile import me.devsaki.hentoid.util.notification.BaseNotification import me.devsaki.hentoid.workers.data.UpdateDownloadData import timber.log.Timber -import java.io.File import java.io.IOException +import java.util.concurrent.atomic.AtomicBoolean import kotlin.math.roundToInt class UpdateDownloadWorker(context: Context, parameters: WorkerParameters) : @@ -54,41 +53,58 @@ class UpdateDownloadWorker(context: Context, parameters: WorkerParameters) : @Throws(IOException::class) private fun downloadUpdate(apkUrl: String) { - val context = applicationContext - Timber.w(context.resources.getString(R.string.starting_download)) - val file = File(context.externalCacheDir, "hentoid.apk") - file.createNewFile() - val response = getOnlineResource(apkUrl, null, false, false, false) - Timber.d("DOWNLOADING APK - RESPONSE %s", response.code) - if (response.code >= 300) throw IOException("Network error " + response.code) - val body = response.body - ?: throw IOException("Could not read response : empty body for $apkUrl") - var size = body.contentLength() - if (size < 1) size = 1 - Timber.d("WRITING DOWNLOADED APK TO %s (size %.2f KB)", file.absolutePath, size / 1024.0) - val buffer = ByteArray(FILE_IO_BUFFER_SIZE) - var len: Int - var processed: Long = 0 - var iteration = 0 - body.byteStream().use { `in` -> - getOutputStream(file).use { out -> - while (`in`.read(buffer).also { len = it } > -1) { - processed += len.toLong() - if (0 == ++iteration % 50) // Notify every 200KB - updateNotificationProgress((processed * 100f / size).roundToInt()) - out.write(buffer, 0, len) - } - out.flush() - } + Timber.d("DOWNLOADING APK") + val apk = downloadToFile( + applicationContext, + Site.NONE, + apkUrl, + emptyList(), + Uri.fromFile(applicationContext.externalCacheDir), + "hentoid.apk", + AtomicBoolean(), + resourceId = 0 + ) { f -> + Timber.v("Download progress: %s%%", f.roundToInt()) + notificationManager.notify(UpdateProgressNotification(f.roundToInt())) } - Timber.d("Download successful") - notificationManager.notifyLast( - UpdateInstallNotification(getFileUriCompat(context, file)) - ) - } - - private fun updateNotificationProgress(processPc: Int) { - Timber.v("Download progress: %s%%", processPc) - notificationManager.notify(UpdateProgressNotification(processPc)) + apk.first?.let { + Timber.d("Download successful") + notificationManager.notifyLast(UpdateInstallNotification(it)) + } ?: run { + Timber.d("Download failed") + } + /* + val context = applicationContext + Timber.w(context.resources.getString(R.string.starting_download)) + val file = File(context.externalCacheDir, "hentoid.apk") + file.createNewFile() + val response = getOnlineResource(apkUrl, null, false, false, false) + Timber.d("DOWNLOADING APK - RESPONSE %s", response.code) + if (response.code >= 300) throw IOException("Network error " + response.code) + val body = response.body + ?: throw IOException("Could not read response : empty body for $apkUrl") + var size = body.contentLength() + if (size < 1) size = 1 + Timber.d("WRITING DOWNLOADED APK TO %s (size %.2f KB)", file.absolutePath, size / 1024.0) + val buffer = ByteArray(FILE_IO_BUFFER_SIZE) + var len: Int + var processed: Long = 0 + var iteration = 0 + body.byteStream().use { `in` -> + getOutputStream(file).use { out -> + while (`in`.read(buffer).also { len = it } > -1) { + processed += len.toLong() + if (0 == ++iteration % 50) // Notify every 200KB + updateNotificationProgress((processed * 100f / size).roundToInt()) + out.write(buffer, 0, len) + } + out.flush() + } + } + Timber.d("Download successful") + notificationManager.notifyLast( + UpdateInstallNotification(getFileUriCompat(context, file)) + ) + */ } } \ No newline at end of file From 5da586af0b830d896b3b8be222c30b43dd779231 Mon Sep 17 00:00:00 2001 From: RobbWatershed <35880555+RobbWatershed@users.noreply.github.com> Date: Thu, 31 Oct 2024 20:49:13 +0100 Subject: [PATCH 42/44] Update update.json --- app/update.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/update.json b/app/update.json index 039cf6767..2d3968c25 100644 --- a/app/update.json +++ b/app/update.json @@ -1,7 +1,7 @@ { "updateURL": "https://github.com/avluis/Hentoid/releases/download/v1.20.0/hentoid1200-747.apk", "versionCode": "747", - "updateURL.beta": "https://github.com/avluis/Hentoid/releases/download/test1/v415.apk", + "updateURL.beta": "https://github.com/avluis/Hentoid/releases/download/v1.20.0/hentoid1200-747.apk", "versionCode.beta": "999", "sourceAlerts":[ {"sourceName":"PURURIN", "status":"RED", "fixedByBuild":"204" }, From 2f85895b3198c544262af84f42ac552eb4e337e9 Mon Sep 17 00:00:00 2001 From: Robb <35880555+robbwatershed@users.noreply.github.com> Date: Thu, 31 Oct 2024 21:07:36 +0100 Subject: [PATCH 43/44] Use downloadToFile to download the APK (cont'ed) --- .../appUpdate/UpdateInstallNotification.kt | 6 +-- .../hentoid/workers/UpdateDownloadWorker.kt | 46 +++++-------------- 2 files changed, 13 insertions(+), 39 deletions(-) diff --git a/app/src/main/java/me/devsaki/hentoid/notification/appUpdate/UpdateInstallNotification.kt b/app/src/main/java/me/devsaki/hentoid/notification/appUpdate/UpdateInstallNotification.kt index 7236277a8..1bec00b4c 100644 --- a/app/src/main/java/me/devsaki/hentoid/notification/appUpdate/UpdateInstallNotification.kt +++ b/app/src/main/java/me/devsaki/hentoid/notification/appUpdate/UpdateInstallNotification.kt @@ -9,14 +9,10 @@ import android.webkit.MimeTypeMap import androidx.core.app.NotificationCompat import me.devsaki.hentoid.R import me.devsaki.hentoid.util.notification.BaseNotification +import me.devsaki.hentoid.workers.APK_MIMETYPE class UpdateInstallNotification(private val apkUri: Uri) : BaseNotification() { - companion object { - private val APK_MIMETYPE = MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk") - } - - override fun onCreateNotification(context: Context): android.app.Notification = NotificationCompat.Builder(context, UpdateNotificationChannel.ID) .setSmallIcon(R.drawable.ic_hentoid_shape) diff --git a/app/src/main/java/me/devsaki/hentoid/workers/UpdateDownloadWorker.kt b/app/src/main/java/me/devsaki/hentoid/workers/UpdateDownloadWorker.kt index cb028c985..1d91da6ae 100644 --- a/app/src/main/java/me/devsaki/hentoid/workers/UpdateDownloadWorker.kt +++ b/app/src/main/java/me/devsaki/hentoid/workers/UpdateDownloadWorker.kt @@ -2,6 +2,7 @@ package me.devsaki.hentoid.workers import android.content.Context import android.net.Uri +import android.webkit.MimeTypeMap import androidx.documentfile.provider.DocumentFile import androidx.work.Data import androidx.work.WorkerParameters @@ -11,6 +12,8 @@ import me.devsaki.hentoid.notification.appUpdate.UpdateFailedNotification import me.devsaki.hentoid.notification.appUpdate.UpdateInstallNotification import me.devsaki.hentoid.notification.appUpdate.UpdateProgressNotification import me.devsaki.hentoid.util.download.downloadToFile +import me.devsaki.hentoid.util.file.getFileUriCompat +import me.devsaki.hentoid.util.file.legacyFileFromUri import me.devsaki.hentoid.util.notification.BaseNotification import me.devsaki.hentoid.workers.data.UpdateDownloadData import timber.log.Timber @@ -18,6 +21,8 @@ import java.io.IOException import java.util.concurrent.atomic.AtomicBoolean import kotlin.math.roundToInt +val APK_MIMETYPE = MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk") + class UpdateDownloadWorker(context: Context, parameters: WorkerParameters) : BaseWorker(context, parameters, R.id.update_download_service, null) { @@ -62,6 +67,7 @@ class UpdateDownloadWorker(context: Context, parameters: WorkerParameters) : Uri.fromFile(applicationContext.externalCacheDir), "hentoid.apk", AtomicBoolean(), + forceMimeType = APK_MIMETYPE, resourceId = 0 ) { f -> Timber.v("Download progress: %s%%", f.roundToInt()) @@ -69,42 +75,14 @@ class UpdateDownloadWorker(context: Context, parameters: WorkerParameters) : } apk.first?.let { Timber.d("Download successful") - notificationManager.notifyLast(UpdateInstallNotification(it)) + legacyFileFromUri(it)?.let { file -> + notificationManager.notifyLast( + // Must use getFileUriCompat to avoid being molested by Android + UpdateInstallNotification(getFileUriCompat(applicationContext, file)) + ) + } } ?: run { Timber.d("Download failed") } - /* - val context = applicationContext - Timber.w(context.resources.getString(R.string.starting_download)) - val file = File(context.externalCacheDir, "hentoid.apk") - file.createNewFile() - val response = getOnlineResource(apkUrl, null, false, false, false) - Timber.d("DOWNLOADING APK - RESPONSE %s", response.code) - if (response.code >= 300) throw IOException("Network error " + response.code) - val body = response.body - ?: throw IOException("Could not read response : empty body for $apkUrl") - var size = body.contentLength() - if (size < 1) size = 1 - Timber.d("WRITING DOWNLOADED APK TO %s (size %.2f KB)", file.absolutePath, size / 1024.0) - val buffer = ByteArray(FILE_IO_BUFFER_SIZE) - var len: Int - var processed: Long = 0 - var iteration = 0 - body.byteStream().use { `in` -> - getOutputStream(file).use { out -> - while (`in`.read(buffer).also { len = it } > -1) { - processed += len.toLong() - if (0 == ++iteration % 50) // Notify every 200KB - updateNotificationProgress((processed * 100f / size).roundToInt()) - out.write(buffer, 0, len) - } - out.flush() - } - } - Timber.d("Download successful") - notificationManager.notifyLast( - UpdateInstallNotification(getFileUriCompat(context, file)) - ) - */ } } \ No newline at end of file From 04d23a0ae971e5e98e69dd0e71d5f366831d0ff1 Mon Sep 17 00:00:00 2001 From: Robb <35880555+robbwatershed@users.noreply.github.com> Date: Thu, 31 Oct 2024 21:09:33 +0100 Subject: [PATCH 44/44] v1.20.1 --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index d39ab9b55..3ffaf7969 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -37,7 +37,7 @@ android { minSdkVersion 26 targetSdkVersion 35 versionCode 130 // is updated automatically by BitRise; only used when building locally - versionName '1.20.1-dev' + versionName '1.20.1' def includeObjectBoxBrowser = System.getenv("INCLUDE_OBJECTBOX_BROWSER") ?: "false" def includeLeakCanary = System.getenv("INCLUDE_LEAK_CANARY") ?: "false"