Global screenshot policy #920 #138

This commit is contained in:
Koitharu
2024-06-06 10:58:29 +03:00
parent 173087ee19
commit 124f31ebe1
15 changed files with 114 additions and 42 deletions

View File

@@ -48,6 +48,7 @@ import org.koitharu.kotatsu.local.data.LocalStorageChanges
import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.main.domain.CoverRestoreInterceptor
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
import org.koitharu.kotatsu.main.ui.protect.ScreenshotPolicyHelper
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider
import org.koitharu.kotatsu.settings.backup.BackupObserver
@@ -152,10 +153,12 @@ interface AppModule {
appProtectHelper: AppProtectHelper,
activityRecreationHandle: ActivityRecreationHandle,
acraScreenLogger: AcraScreenLogger,
screenshotPolicyHelper: ScreenshotPolicyHelper,
): Set<@JvmSuppressWildcards Application.ActivityLifecycleCallbacks> = arraySetOf(
appProtectHelper,
activityRecreationHandle,
acraScreenLogger,
screenshotPolicyHelper,
)
@Provides

View File

@@ -298,10 +298,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
get() = prefs.getBoolean(KEY_PAGES_NUMBERS, false)
val screenshotsPolicy: ScreenshotsPolicy
get() = runCatching {
val key = prefs.getString(KEY_SCREENSHOTS_POLICY, null)?.uppercase(Locale.ROOT)
if (key == null) ScreenshotsPolicy.ALLOW else ScreenshotsPolicy.valueOf(key)
}.getOrDefault(ScreenshotsPolicy.ALLOW)
get() = prefs.getEnumValue(KEY_SCREENSHOTS_POLICY, ScreenshotsPolicy.ALLOW)
var userSpecifiedMangaDirectories: Set<File>
get() {

View File

@@ -3,5 +3,5 @@ package org.koitharu.kotatsu.core.prefs
enum class ScreenshotsPolicy {
// Do not rename this
ALLOW, BLOCK_NSFW, BLOCK_ALL;
}
ALLOW, BLOCK_NSFW, BLOCK_INCOGNITO, BLOCK_ALL;
}

View File

@@ -19,6 +19,8 @@ import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
@@ -26,10 +28,12 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.util.ActionModeDelegate
import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate
import org.koitharu.kotatsu.core.util.ext.isWebViewUnavailable
import org.koitharu.kotatsu.main.ui.protect.ScreenshotPolicyHelper
@Suppress("LeakingThis")
abstract class BaseActivity<B : ViewBinding> :
AppCompatActivity(),
ScreenshotPolicyHelper.ContentContainer,
WindowInsetsDelegate.WindowInsetsListener {
private var isAmoledTheme = false
@@ -151,6 +155,8 @@ abstract class BaseActivity<B : ViewBinding> :
}
}
override fun isNsfwContent(): Flow<Boolean> = flowOf(false)
private fun putDataToExtras(intent: Intent?) {
intent?.putExtra(EXTRA_DATA, intent.data)
}

View File

@@ -34,10 +34,12 @@ import coil.util.CoilUtils
import com.google.android.material.chip.Chip
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.core.model.FavouriteCategory
@@ -197,6 +199,8 @@ class DetailsActivity :
addMenuProvider(menuProvider)
}
override fun isNsfwContent(): Flow<Boolean> = viewModel.manga.map { it?.isNsfw == true }
override fun onClick(v: View) {
when (v.id) {
R.id.button_read -> openReader(isIncognitoMode = false)

View File

@@ -0,0 +1,61 @@
package org.koitharu.kotatsu.main.ui.protect
import android.app.Activity
import android.os.Bundle
import android.view.WindowManager
import androidx.annotation.MainThread
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ScreenshotsPolicy
import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.core.ui.DefaultActivityLifecycleCallbacks
import javax.inject.Inject
class ScreenshotPolicyHelper @Inject constructor(
private val settings: AppSettings,
) : DefaultActivityLifecycleCallbacks {
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
(activity as? ContentContainer)?.setupScreenshotPolicy(activity)
}
private fun ContentContainer.setupScreenshotPolicy(activity: Activity) =
lifecycleScope.launch(Dispatchers.Default) {
settings.observeAsFlow(AppSettings.KEY_SCREENSHOTS_POLICY) { screenshotsPolicy }
.flatMapLatest { policy ->
when (policy) {
ScreenshotsPolicy.ALLOW -> flowOf(false)
ScreenshotsPolicy.BLOCK_NSFW -> withContext(Dispatchers.Main) {
isNsfwContent()
}.distinctUntilChanged()
ScreenshotsPolicy.BLOCK_ALL -> flowOf(true)
ScreenshotsPolicy.BLOCK_INCOGNITO -> settings.observeAsFlow(AppSettings.KEY_INCOGNITO_MODE) {
isIncognitoModeEnabled
}
}
}.collect { isSecure ->
withContext(Dispatchers.Main) {
if (isSecure) {
activity.window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
} else {
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
}
}
}
}
interface ContentContainer : LifecycleOwner {
@MainThread
fun isNsfwContent(): Flow<Boolean>
}
}

View File

@@ -28,6 +28,7 @@ import androidx.lifecycle.lifecycleScope
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.BuildConfig
@@ -142,7 +143,6 @@ class ReaderActivity :
viewModel.content.observe(this) {
onLoadingStateChanged(viewModel.isLoading.value)
}
viewModel.isScreenshotsBlockEnabled.observe(this, this::setWindowSecure)
viewModel.isKeepScreenOnEnabled.observe(this, this::setKeepScreenOn)
viewModel.isInfoBarEnabled.observe(this, ::onReaderBarChanged)
viewModel.isBookmarkAdded.observe(this, MenuInvalidator(this))
@@ -179,6 +179,8 @@ class ReaderActivity :
viewModel.onPause()
}
override fun isNsfwContent(): Flow<Boolean> = viewModel.isMangaNsfw
override fun onIdle() {
viewModel.saveCurrentState(readerManager.currentReader?.getCurrentState())
}
@@ -297,14 +299,6 @@ class ReaderActivity :
.show()
}
private fun setWindowSecure(isSecure: Boolean) {
if (isSecure) {
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
} else {
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
}
}
private fun setKeepScreenOn(isKeep: Boolean) {
if (isKeep) {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)

View File

@@ -17,6 +17,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
@@ -38,7 +39,6 @@ import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.MangaIntent
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.core.prefs.ScreenshotsPolicy
import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
import org.koitharu.kotatsu.core.ui.BaseViewModel
@@ -166,13 +166,9 @@ class ReaderViewModel @Inject constructor(
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null),
)
val isScreenshotsBlockEnabled = combine(
mangaFlow,
settings.observeAsFlow(AppSettings.KEY_SCREENSHOTS_POLICY) { screenshotsPolicy },
) { manga, policy ->
policy == ScreenshotsPolicy.BLOCK_ALL ||
(policy == ScreenshotsPolicy.BLOCK_NSFW && manga != null && manga.isNsfw)
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, false)
val isMangaNsfw = mangaFlow.map {
it?.isNsfw == true
}
val isBookmarkAdded = currentState.flatMapLatest { state ->
val manga = mangaData.value?.toManga()

View File

@@ -15,11 +15,14 @@ import androidx.fragment.app.commit
import com.google.android.material.appbar.AppBarLayout
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.model.isNsfw
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaTags
import org.koitharu.kotatsu.core.parser.MangaIntent
@@ -58,6 +61,8 @@ class MangaListActivity :
"Cannot find FilterOwner fragment in ${supportFragmentManager.fragments}"
}.filter
private var source: MangaSource? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityMangaListBinding.inflate(layoutInflater))
@@ -66,16 +71,19 @@ class MangaListActivity :
if (viewBinding.containerFilterHeader != null) {
viewBinding.appbar.addOnOffsetChangedListener(this)
}
val source = intent.getStringExtra(EXTRA_SOURCE)?.let(::MangaSource) ?: tags?.firstOrNull()?.source
if (source == null) {
source = intent.getStringExtra(EXTRA_SOURCE)?.let(::MangaSource) ?: tags?.firstOrNull()?.source
val src = source
if (src == null) {
finishAfterTransition()
return
} else {
viewBinding.buttonOrder?.setOnClickListener(this)
title = if (src == MangaSource.LOCAL) getString(R.string.local_storage) else src.title
initList(src, tags)
}
viewBinding.buttonOrder?.setOnClickListener(this)
title = if (source == MangaSource.LOCAL) getString(R.string.local_storage) else source.title
initList(source, tags)
}
override fun isNsfwContent(): Flow<Boolean> = flowOf(source?.isNsfw() == true)
override fun onWindowInsetsChanged(insets: Insets) {
viewBinding.root.updatePadding(
left = insets.left,

View File

@@ -10,6 +10,7 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.view.postDelayed
import androidx.fragment.app.viewModels
import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.Preference
import androidx.preference.TwoStatePreference
@@ -22,6 +23,7 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
import org.koitharu.kotatsu.core.os.AppShortcutManager
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ScreenshotsPolicy
import org.koitharu.kotatsu.core.prefs.SearchSuggestionType
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.ui.util.ActivityRecreationHandle
@@ -29,6 +31,7 @@ import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
import org.koitharu.kotatsu.core.util.FileSize
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.setDefaultValueCompat
import org.koitharu.kotatsu.core.util.ext.tryLaunch
import org.koitharu.kotatsu.local.data.CacheDir
import org.koitharu.kotatsu.parsers.util.mapToSet
@@ -63,6 +66,10 @@ class UserDataSettingsFragment : BasePreferenceFragment(R.string.data_and_privac
appShortcutManager.isDynamicShortcutsAvailable()
findPreference<TwoStatePreference>(AppSettings.KEY_PROTECT_APP)
?.isChecked = !settings.appPassword.isNullOrEmpty()
findPreference<ListPreference>(AppSettings.KEY_SCREENSHOTS_POLICY)?.run {
entryValues = ScreenshotsPolicy.entries.names()
setDefaultValueCompat(ScreenshotsPolicy.ALLOW.name)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

View File

@@ -23,6 +23,7 @@
<string-array name="screenshots_policy" translatable="false">
<item>@string/screenshots_allow</item>
<item>@string/screenshots_block_nsfw</item>
<item>@string/screenshots_block_incognito</item>
<item>@string/screenshots_block_all</item>
</string-array>
<string-array name="network_policy" translatable="false">

View File

@@ -30,11 +30,6 @@
<string-array name="values_track_sources_default" translatable="false">
<item>favourites</item>
</string-array>
<string-array name="values_screenshots_policy" translatable="false">
<item>allow</item>
<item>block_nsfw</item>
<item>block_all</item>
</string-array>
<string-array name="values_network_policy" translatable="false">
<item>1</item>
<item>2</item>

View File

@@ -653,4 +653,5 @@
<!-- In plural, used for filter -->
<string name="_new">New</string>
<string name="all_languages">All languages</string>
<string name="screenshots_block_incognito">Block when incognito mode</string>
</resources>

View File

@@ -125,14 +125,6 @@
android:title="@string/keep_screen_on"
app:allowDividerAbove="true" />
<ListPreference
android:defaultValue="allow"
android:entries="@array/screenshots_policy"
android:entryValues="@array/values_screenshots_policy"
android:key="screenshots_policy"
android:title="@string/screenshots_policy"
app:useSimpleSummaryProvider="true" />
<ListPreference
android:defaultValue="2"
android:entries="@array/network_policy"

View File

@@ -9,6 +9,13 @@
android:summary="@string/protect_application_summary"
android:title="@string/protect_application" />
<ListPreference
android:defaultValue="allow"
android:entries="@array/screenshots_policy"
android:key="screenshots_policy"
android:title="@string/screenshots_policy"
app:useSimpleSummaryProvider="true" />
<SwitchPreferenceCompat
android:key="history_exclude_nsfw"
android:summary="@string/exclude_nsfw_from_history_summary"