feat: Cutout support on pre-Android P

This commit is contained in:
ziro 2024-02-17 08:01:30 +07:00
parent afe3fd64af
commit 9644b71e57
Signed by: null2264
GPG key ID: BA64F8B60AF3EFB6
8 changed files with 119 additions and 71 deletions

View file

@ -6,4 +6,9 @@
## Fixes ## Fixes
## Other ## Other
--> -->
## Additions
- Added cutout support for some pre-Android P devices
## Fixes
- Fixed cutout behaviour for Android P

View file

@ -76,6 +76,7 @@ class ReaderGeneralView @JvmOverloads constructor(context: Context, attrs: Attri
} }
private fun updatePrefs() { private fun updatePrefs() {
binding.cutoutShort.isVisible = DeviceUtil.hasCutout(context) && preferences.fullscreen().get() binding.cutoutShort.isVisible =
DeviceUtil.hasCutout(context as ReaderActivity).ordinal >= DeviceUtil.CutoutSupport.MODERN.ordinal && preferences.fullscreen().get()
} }
} }

View file

@ -3,14 +3,9 @@ package eu.kanade.tachiyomi.ui.reader.settings
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.res.Configuration import android.content.res.Configuration
import android.hardware.display.DisplayManager
import android.os.Build
import android.util.AttributeSet import android.util.AttributeSet
import android.view.Display
import androidx.core.content.getSystemService
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import dev.yokai.domain.ui.settings.ReaderPreferences
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.ReaderPagedLayoutBinding import eu.kanade.tachiyomi.databinding.ReaderPagedLayoutBinding
import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.ui.reader.ReaderActivity
@ -18,7 +13,6 @@ import eu.kanade.tachiyomi.util.bindToPreference
import eu.kanade.tachiyomi.util.lang.addBetaTag import eu.kanade.tachiyomi.util.lang.addBetaTag
import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.widget.BaseReaderSettingsView import eu.kanade.tachiyomi.widget.BaseReaderSettingsView
import uy.kohesive.injekt.injectLazy
class ReaderPagedView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : class ReaderPagedView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
BaseReaderSettingsView<ReaderPagedLayoutBinding>(context, attrs) { BaseReaderSettingsView<ReaderPagedLayoutBinding>(context, attrs) {
@ -110,15 +104,15 @@ class ReaderPagedView @JvmOverloads constructor(context: Context, attrs: Attribu
else -> false else -> false
} }
val ogView = (context as? Activity)?.window?.decorView val ogView = (context as? Activity)?.window?.decorView
val hasCutout = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
ogView?.rootWindowInsets?.displayCutout?.safeInsetTop != null || ogView?.rootWindowInsets?.displayCutout?.safeInsetBottom != null
} else {
false
}
binding.landscapeZoom.isVisible = show && preferences.imageScaleType().get() == SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE binding.landscapeZoom.isVisible = show && preferences.imageScaleType().get() == SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE
binding.extendPastCutout.isVisible = show && isFullFit && hasCutout && preferences.fullscreen().get() binding.extendPastCutout.isVisible =
binding.extendPastCutoutLandscape.isVisible = DeviceUtil.hasCutout(context) && preferences.fullscreen().get() && show && isFullFit
ogView?.resources?.configuration?.orientation == Configuration.ORIENTATION_LANDSCAPE && DeviceUtil.hasCutout(context as? Activity).ordinal >= DeviceUtil.CutoutSupport.LEGACY.ordinal
&& preferences.fullscreen().get()
binding.extendPastCutoutLandscape.isVisible =
DeviceUtil.hasCutout(context as? Activity).ordinal >= DeviceUtil.CutoutSupport.MODERN.ordinal
&& preferences.fullscreen().get()
&& ogView?.resources?.configuration?.orientation == Configuration.ORIENTATION_LANDSCAPE
if (binding.extendPastCutoutLandscape.isVisible) { if (binding.extendPastCutoutLandscape.isVisible) {
binding.filterLinearLayout.removeView(binding.extendPastCutoutLandscape) binding.filterLinearLayout.removeView(binding.extendPastCutoutLandscape)
binding.filterLinearLayout.addView( binding.filterLinearLayout.addView(

View file

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.ui.reader.viewer package eu.kanade.tachiyomi.ui.reader.viewer
import android.app.Activity
import android.content.Context import android.content.Context
import android.graphics.PointF import android.graphics.PointF
import android.graphics.drawable.Animatable import android.graphics.drawable.Animatable
@ -16,7 +17,6 @@ import androidx.annotation.AttrRes
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import androidx.annotation.StyleRes import androidx.annotation.StyleRes
import androidx.appcompat.widget.AppCompatImageView import androidx.appcompat.widget.AppCompatImageView
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import coil.dispose import coil.dispose
import coil.imageLoader import coil.imageLoader
@ -29,6 +29,7 @@ import com.github.chrisbanes.photoview.PhotoView
import dev.yokai.domain.ui.settings.ReaderPreferences.CutoutBehaviour import dev.yokai.domain.ui.settings.ReaderPreferences.CutoutBehaviour
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonSubsamplingImageView import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonSubsamplingImageView
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.GLUtil import eu.kanade.tachiyomi.util.system.GLUtil
import eu.kanade.tachiyomi.util.system.animatorDurationScale import eu.kanade.tachiyomi.util.system.animatorDurationScale
import java.io.InputStream import java.io.InputStream
@ -190,15 +191,16 @@ open class ReaderPageImageView @JvmOverloads constructor(
config.insetInfo.scaleTypeIsFullFit && topInsets + bottomInsets > 0, config.insetInfo.scaleTypeIsFullFit && topInsets + bottomInsets > 0,
) )
if ((config.insetInfo.cutoutBehavior != CutoutBehaviour.IGNORE || !config.insetInfo.scaleTypeIsFullFit) && if ((config.insetInfo.cutoutBehavior != CutoutBehaviour.IGNORE || !config.insetInfo.scaleTypeIsFullFit) &&
android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q &&
config.insetInfo.isFullscreen config.insetInfo.isFullscreen
) { ) {
val insets: WindowInsets? = config.insetInfo.insets val insets: WindowInsets? = config.insetInfo.insets
setExtraSpace( setExtraSpace(
0f, 0f,
insets?.displayCutout?.boundingRectTop?.height()?.toFloat() ?: 0f, DeviceUtil.getCutoutHeight(context as? Activity, config.insetInfo.cutoutSupport).toFloat(),
0f, 0f,
insets?.displayCutout?.boundingRectBottom?.height()?.toFloat() ?: 0f, if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q)
insets?.displayCutout?.boundingRectBottom?.height()?.toFloat() ?: 0f
else 0f,
) )
} }
} }
@ -318,6 +320,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
) )
data class InsetInfo( data class InsetInfo(
val cutoutSupport: DeviceUtil.CutoutSupport,
val cutoutBehavior: CutoutBehaviour, val cutoutBehavior: CutoutBehaviour,
val topCutoutInset: Float, val topCutoutInset: Float,
val bottomCutoutInset: Float, val bottomCutoutInset: Float,

View file

@ -12,6 +12,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.navigation.EdgeNavigation
import eu.kanade.tachiyomi.ui.reader.viewer.navigation.KindlishNavigation import eu.kanade.tachiyomi.ui.reader.viewer.navigation.KindlishNavigation
import eu.kanade.tachiyomi.ui.reader.viewer.navigation.LNavigation import eu.kanade.tachiyomi.ui.reader.viewer.navigation.LNavigation
import eu.kanade.tachiyomi.ui.reader.viewer.navigation.RightAndLeftNavigation import eu.kanade.tachiyomi.ui.reader.viewer.navigation.RightAndLeftNavigation
import eu.kanade.tachiyomi.util.system.DeviceUtil
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn

View file

@ -26,6 +26,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ReaderErrorView
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressBar import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressBar
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig.ZoomType import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig.ZoomType
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.ImageUtil import eu.kanade.tachiyomi.util.system.ImageUtil
import eu.kanade.tachiyomi.util.system.ImageUtil.isPagePadded import eu.kanade.tachiyomi.util.system.ImageUtil.isPagePadded
import eu.kanade.tachiyomi.util.system.ThemeUtil import eu.kanade.tachiyomi.util.system.ThemeUtil
@ -543,12 +544,13 @@ class PagerPageHolder(
zoomStartPosition = viewer.config.imageZoomType, zoomStartPosition = viewer.config.imageZoomType,
landscapeZoom = viewer.config.landscapeZoom, landscapeZoom = viewer.config.landscapeZoom,
insetInfo = InsetInfo( insetInfo = InsetInfo(
cutoutSupport = DeviceUtil.hasCutout(viewer.activity),
cutoutBehavior = viewer.config.cutoutBehavior, cutoutBehavior = viewer.config.cutoutBehavior,
topCutoutInset = viewer.activity.window.decorView.rootWindowInsets?.topCutoutInset()?.toFloat() ?: 0f, topCutoutInset = viewer.activity.window.decorView.rootWindowInsets?.topCutoutInset()?.toFloat() ?: 0f,
bottomCutoutInset = viewer.activity.window.decorView.rootWindowInsets?.bottomCutoutInset()?.toFloat() ?: 0f, bottomCutoutInset = viewer.activity.window.decorView.rootWindowInsets?.bottomCutoutInset()?.toFloat() ?: 0f,
scaleTypeIsFullFit = viewer.config.scaleTypeIsFullFit(), scaleTypeIsFullFit = viewer.config.scaleTypeIsFullFit(),
isFullscreen = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && isFullscreen = viewer.config.isFullscreen
viewer.config.isFullscreen && !viewer.activity.isInMultiWindowMode, && if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) !viewer.activity.isInMultiWindowMode else true,
isSplitScreen = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && viewer.activity.isInMultiWindowMode, isSplitScreen = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && viewer.activity.isInMultiWindowMode,
insets = viewer.activity.window.decorView.rootWindowInsets, insets = viewer.activity.window.decorView.rootWindowInsets,
), ),

View file

@ -121,7 +121,9 @@ class SettingsReaderController : SettingsController() {
// FIXME: Transition from reader to homepage is broken when cutout short is disabled // FIXME: Transition from reader to homepage is broken when cutout short is disabled
title = context.getString(R.string.pref_cutout_short).addBetaTag(context) title = context.getString(R.string.pref_cutout_short).addBetaTag(context)
preferences.fullscreen().changesIn(viewScope) { isVisible = DeviceUtil.hasCutout(activity) && it} preferences.fullscreen().changesIn(viewScope) {
isVisible = DeviceUtil.hasCutout(activity).ordinal >= DeviceUtil.CutoutSupport.MODERN.ordinal && it
}
} }
listPreference(activity) { listPreference(activity) {
bindTo(readerPreferences.landscapeCutoutBehavior()) bindTo(readerPreferences.landscapeCutoutBehavior())
@ -130,7 +132,9 @@ class SettingsReaderController : SettingsController() {
entriesRes = values.map { it.titleResId }.toTypedArray() entriesRes = values.map { it.titleResId }.toTypedArray()
entryValues = values.map { it.name } entryValues = values.map { it.name }
preferences.fullscreen().changesIn(viewScope) { isVisible = DeviceUtil.hasCutout(activity) && it} preferences.fullscreen().changesIn(viewScope) {
isVisible = DeviceUtil.hasCutout(activity).ordinal >= DeviceUtil.CutoutSupport.MODERN.ordinal && it
}
} }
switchPreference { switchPreference {
key = Keys.keepScreenOn key = Keys.keepScreenOn
@ -221,22 +225,13 @@ class SettingsReaderController : SettingsController() {
entryValues = values.map { it.name } entryValues = values.map { it.name }
// Calling this once to show only on cutout // Calling this once to show only on cutout
isVisible = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { isVisible = DeviceUtil.hasCutout(activity).ordinal >= DeviceUtil.CutoutSupport.LEGACY.ordinal
activityBinding?.root?.rootWindowInsets?.displayCutout?.safeInsetTop != null ||
activityBinding?.root?.rootWindowInsets?.displayCutout?.safeInsetBottom != null
} else {
false
}
// Calling this a second time in case activity is recreated while on this page // Calling this a second time in case activity is recreated while on this page
// Keep the first so it shouldn't animate hiding the preference for phones without // Keep the first so it shouldn't animate hiding the preference for phones without
// cutouts // cutouts
activityBinding?.root?.post { activityBinding?.root?.post {
isVisible = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { isVisible = DeviceUtil.hasCutout(activity).ordinal >= DeviceUtil.CutoutSupport.LEGACY.ordinal
activityBinding?.root?.rootWindowInsets?.displayCutout?.safeInsetTop != null ||
activityBinding?.root?.rootWindowInsets?.displayCutout?.safeInsetBottom != null
} else {
false
}
} }
} }

View file

@ -1,15 +1,17 @@
package eu.kanade.tachiyomi.util.system package eu.kanade.tachiyomi.util.system
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.app.Activity
import android.hardware.display.DisplayManager import android.hardware.display.DisplayManager
import android.os.Build import android.os.Build
import android.view.Display import android.view.Display
import android.view.View
import android.view.Window import android.view.Window
import android.view.WindowManager
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.core.view.WindowInsetsCompat
import timber.log.Timber import timber.log.Timber
import java.lang.reflect.InvocationTargetException
object DeviceUtil { object DeviceUtil {
@ -92,59 +94,104 @@ object DeviceUtil {
fun setLegacyCutoutMode(window: Window, mode: LegacyCutoutMode) { fun setLegacyCutoutMode(window: Window, mode: LegacyCutoutMode) {
when (mode) { when (mode) {
LegacyCutoutMode.SHORT_EDGES -> { LegacyCutoutMode.SHORT_EDGES -> {
/* Deprecated method // Vivo doesn't support this, user had to set it from Settings
/*
if (isVivo) { if (isVivo) {
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
var systemUiVisibility = window.decorView.systemUiVisibility
systemUiVisibility = systemUiVisibility or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
systemUiVisibility = systemUiVisibility or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
window.decorView.systemUiVisibility = systemUiVisibility
} }
*/ */
} }
LegacyCutoutMode.NEVER -> { LegacyCutoutMode.NEVER -> {
/* Deprecated method // Vivo doesn't support this, user had to set it from Settings
/*
if (isVivo) { if (isVivo) {
window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
var systemUiVisibility = window.decorView.systemUiVisibility
systemUiVisibility = systemUiVisibility and View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN.inv()
systemUiVisibility = systemUiVisibility and View.SYSTEM_UI_FLAG_LAYOUT_STABLE.inv()
window.decorView.systemUiVisibility = systemUiVisibility
} }
*/ */
} }
} }
} }
fun hasCutout(context: Context? = null): Boolean { fun hasCutout(context: Activity?): CutoutSupport {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (context?.getSystemService<DisplayManager>()
return context?.getSystemService<DisplayManager>() ?.getDisplay(Display.DEFAULT_DISPLAY)?.cutout != null)
?.getDisplay(Display.DEFAULT_DISPLAY)?.cutout != null return CutoutSupport.EXTENDED
} } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// TODO: Actually check for cutout val displayCutout = context?.window?.decorView?.rootWindowInsets?.displayCutout
return true if (displayCutout?.safeInsetTop != null || displayCutout?.safeInsetBottom != null)
} return CutoutSupport.MODERN
/* } else if (isVivo) {
else if (isVivo && context != null) {
// https://swsdl.vivo.com.cn/appstore/developer/uploadfile/20180328/20180328152252602.pdf // https://swsdl.vivo.com.cn/appstore/developer/uploadfile/20180328/20180328152252602.pdf
try { try {
@SuppressLint("PrivateApi") @SuppressLint("PrivateApi")
val ftFeature = context.classLoader val ftFeature = context?.classLoader
.loadClass("android.util.FtFeature") ?.loadClass("android.util.FtFeature")
val isFeatureSupportMethod = ftFeature.getMethod( val isFeatureSupportMethod = ftFeature?.getMethod(
"isFeatureSupport", "isFeatureSupport",
Int::class.javaPrimitiveType Int::class.javaPrimitiveType,
) )
val isNotchOnScreen = 0x00000020 val isNotchOnScreen = 0x00000020
return isFeatureSupportMethod.invoke(ftFeature, isNotchOnScreen) as Boolean val isSupported = isFeatureSupportMethod?.invoke(ftFeature, isNotchOnScreen) as Boolean
if (isSupported) return CutoutSupport.LEGACY
} catch (_: Exception) {
}
} else if (isMiui) {
try {
@SuppressLint("PrivateApi")
val sysProp = context?.classLoader?.loadClass("android.os.SystemProperties")
val method = sysProp?.getMethod("getInt", String::class.java, Int::class.javaPrimitiveType)
val rt = method?.invoke(sysProp, "ro.miui.notch", 0) as Int
if (rt == 1) return CutoutSupport.LEGACY
} catch (_: Exception) { } catch (_: Exception) {
} }
} }
*/ return CutoutSupport.NONE
return false }
fun getCutoutHeight(context: Activity?, cutoutSupport: CutoutSupport): Number {
return when (cutoutSupport) {
CutoutSupport.MODERN -> {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P)
throw IllegalStateException("Modern cutout only available on Android P or higher")
context?.window?.decorView?.rootWindowInsets?.displayCutout?.safeInsetTop ?: 0
}
CutoutSupport.EXTENDED -> {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q)
throw IllegalStateException("Extended cutout only available on Android Q or higher")
context?.window?.decorView?.rootWindowInsets?.displayCutout?.boundingRectTop?.height()?.toFloat() ?: 0f
}
CutoutSupport.LEGACY -> {
if (isVivo) {
val insetCompat = context?.window?.decorView?.rootWindowInsets?.let {
WindowInsetsCompat.toWindowInsetsCompat(it)
}
val statusBarHeight = insetCompat?.getInsets(WindowInsetsCompat.Type.statusBars())?.top
?: 24.dpToPx // 24dp is "standard" height for Android since Marshmallow
var notchHeight = 32.dpToPx
if (notchHeight < statusBarHeight) {
notchHeight = statusBarHeight
}
notchHeight
} else if (isMiui) {
val resourceId = context?.resources?.getIdentifier("notch_height",
"dimen", "android") ?: 0
if (resourceId > 0) {
context?.resources?.getDimensionPixelSize(resourceId) ?: 0
} else {
0
}
} else {
0
}
}
else -> 0
}
}
enum class CutoutSupport {
NONE,
LEGACY, // Pre-Android P, the start of this hell
MODERN, // Android P
EXTENDED, // Android Q
} }
enum class LegacyCutoutMode { enum class LegacyCutoutMode {