Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Screenshot Detection #278

Merged
merged 1 commit into from
Apr 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import androidx.fragment.app.FragmentActivity
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.michaelbel.movies.common.ktx.launchAndCollectIn
import org.michaelbel.movies.ui.ktx.resolveNotificationPreferencesIntent
import org.michaelbel.movies.ui.ktx.setScreenshotBlockEnabled
import org.michaelbel.movies.ui.ktx.supportRegisterScreenCaptureCallback
import org.michaelbel.movies.ui.ktx.supportUnregisterScreenCaptureCallback
import org.michaelbel.movies.ui.shortcuts.installShortcuts

internal class MainActivity: FragmentActivity() {
Expand All @@ -26,8 +29,21 @@ internal class MainActivity: FragmentActivity() {
}
resolveNotificationPreferencesIntent()
viewModel.run {
isScreenshotBlockEnabled.launchAndCollectIn(this@MainActivity) { enabled ->
window.setScreenshotBlockEnabled(enabled)
}
authenticateFlow.launchAndCollectIn(this@MainActivity) { authenticate(this@MainActivity) }
cancelFlow.launchAndCollectIn(this@MainActivity) { finish() }
}
}

override fun onStart() {
super.onStart()
supportRegisterScreenCaptureCallback()
}

override fun onStop() {
super.onStop()
supportUnregisterScreenCaptureCallback()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ internal class MainViewModel(
initialValue = ThemeData.Default
)

val isScreenshotBlockEnabled: StateFlow<Boolean> = interactor.isScreenshotBlockEnabled
.stateIn(
scope = this,
started = SharingStarted.Lazily,
initialValue = false
)

init {
fetchBiometric()
fetchRemoteConfig()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ interface SettingsInteractor {

val isBiometricEnabled: Flow<Boolean>

val isScreenshotBlockEnabled: Flow<Boolean>

suspend fun isBiometricEnabledAsync(): Boolean

suspend fun selectTheme(
Expand Down Expand Up @@ -47,4 +49,8 @@ interface SettingsInteractor {
suspend fun setBiometricEnabled(
enabled: Boolean
)

suspend fun setScreenshotBlockEnabled(
enabled: Boolean
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ internal class SettingsInteractorImpl(

override val isBiometricEnabled: Flow<Boolean> = settingsRepository.isBiometricEnabled

override val isScreenshotBlockEnabled: Flow<Boolean> = settingsRepository.isScreenshotBlockEnabled

override suspend fun isBiometricEnabledAsync(): Boolean {
return settingsRepository.isBiometricEnabledAsync()
}
Expand Down Expand Up @@ -80,4 +82,10 @@ internal class SettingsInteractorImpl(
settingsRepository.setBiometricEnabled(enabled)
}
}

override suspend fun setScreenshotBlockEnabled(enabled: Boolean) {
withContext(dispatchers.main) {
settingsRepository.setScreenshotBlockEnabled(enabled)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ class MoviesPreferences(
val isBiometricEnabledFlow: Flow<Boolean?>
get() = dataStore.data.map { preferences -> preferences[PREFERENCE_BIOMETRIC_KEY] }

val isScreenshotBlockEnabledFlow: Flow<Boolean?>
get() = dataStore.data.map { preferences -> preferences[PREFERENCE_SCREENSHOT_BLOCK_KEY] }

val paletteKeyFlow: Flow<Int?>
get() = dataStore.data.map { preferences -> preferences[PREFERENCE_PALETTE_KEY] }

Expand Down Expand Up @@ -125,6 +128,12 @@ class MoviesPreferences(
}
}

suspend fun setScreenshotBlockEnabled(enabled: Boolean) {
dataStore.edit { preferences ->
preferences[PREFERENCE_SCREENSHOT_BLOCK_KEY] = enabled
}
}

suspend fun setPaletteKey(paletteKey: Int) {
dataStore.edit { preferences ->
preferences[PREFERENCE_PALETTE_KEY] = paletteKey
Expand All @@ -149,5 +158,6 @@ class MoviesPreferences(
private val PREFERENCE_BIOMETRIC_KEY = booleanPreferencesKey("biometric")
private val PREFERENCE_PALETTE_KEY = intPreferencesKey("palette")
private val PREFERENCE_SEED_COLOR_KEY = intPreferencesKey("seed_color")
private val PREFERENCE_SCREENSHOT_BLOCK_KEY = booleanPreferencesKey("screenshot_block")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ interface SettingsRepository {

val isBiometricEnabled: Flow<Boolean>

val isScreenshotBlockEnabled: Flow<Boolean>

suspend fun isBiometricEnabledAsync(): Boolean

suspend fun selectTheme(
Expand Down Expand Up @@ -47,4 +49,8 @@ interface SettingsRepository {
suspend fun setBiometricEnabled(
enabled: Boolean
)

suspend fun setScreenshotBlockEnabled(
enabled: Boolean
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ internal class SettingsRepositoryImpl(
enabled ?: false
}

override val isScreenshotBlockEnabled: Flow<Boolean> = preferences.isScreenshotBlockEnabledFlow.map { enabled ->
enabled ?: false
}

override suspend fun isBiometricEnabledAsync(): Boolean {
return preferences.isBiometricEnabledAsync()
}
Expand Down Expand Up @@ -79,4 +83,8 @@ internal class SettingsRepositoryImpl(
override suspend fun setBiometricEnabled(enabled: Boolean) {
preferences.setBiometricEnabled(enabled)
}

override suspend fun setScreenshotBlockEnabled(enabled: Boolean) {
preferences.setScreenshotBlockEnabled(enabled)
}
}
2 changes: 2 additions & 0 deletions core/ui-kmp/src/androidMain/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
<manifest
xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.DETECT_SCREEN_CAPTURE" />

<application>

<service
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package org.michaelbel.movies.ui.ktx

import android.app.Activity
import android.os.Build

private val screenCaptureCallback: Any
get() {
return if (Build.VERSION.SDK_INT >= 34) {
Activity.ScreenCaptureCallback {}
} else {
Unit
}
}

fun Activity.supportRegisterScreenCaptureCallback() {
if (Build.VERSION.SDK_INT >= 34) {
registerScreenCaptureCallback(mainExecutor, screenCaptureCallback as Activity.ScreenCaptureCallback)
}
}

fun Activity.supportUnregisterScreenCaptureCallback() {
if (Build.VERSION.SDK_INT >= 34) {
unregisterScreenCaptureCallback(screenCaptureCallback as Activity.ScreenCaptureCallback)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.michaelbel.movies.ui.ktx

import android.view.Window
import android.view.WindowManager

fun Window.setScreenshotBlockEnabled(enabled: Boolean) {
if (enabled) {
setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
} else {
clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@
<string name="settings_update">Обнови приложение</string>
<string name="settings_update_description">Используя In-App Update API</string>
<string name="settings_palette_colors">Палитра цветов</string>
<string name="settings_screenshots">Скриншоты</string>
<string name="settings_screenshots_description">Запретить делать скриншоты</string>

<string name="appwidget_description">Отображает предстоящие фильмы</string>
<string name="appwidget_title">Скоро в кино</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@
<string name="settings_update">Update App</string>
<string name="settings_update_description">Using In-App Update API</string>
<string name="settings_palette_colors">Palette Colors</string>
<string name="settings_screenshots">Screenshots</string>
<string name="settings_screenshots_description">Block taking screenshots</string>

<string name="appwidget_description">Display upcoming movies</string>
<string name="appwidget_title">Upcoming Movies</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import androidx.compose.material.icons.outlined.LocalMovies
import androidx.compose.material.icons.outlined.LocationOn
import androidx.compose.material.icons.outlined.Notifications
import androidx.compose.material.icons.outlined.Palette
import androidx.compose.material.icons.outlined.Screenshot
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.Share
Expand Down Expand Up @@ -79,6 +80,7 @@ object MoviesIcons {
val LocalMovies = Icons.Outlined.LocalMovies
val Notifications = Icons.Outlined.Notifications
val Palette = Icons.Outlined.Palette
val Screenshot = Icons.Outlined.Screenshot
val Search = Icons.Outlined.Search
val Settings = Icons.Outlined.Settings
val Share = Icons.Outlined.Share
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ import movies.core.ui_kmp.generated.resources.settings_post_notifications_grante
import movies.core.ui_kmp.generated.resources.settings_post_notifications_should_request
import movies.core.ui_kmp.generated.resources.settings_review
import movies.core.ui_kmp.generated.resources.settings_review_description
import movies.core.ui_kmp.generated.resources.settings_screenshots
import movies.core.ui_kmp.generated.resources.settings_screenshots_description
import movies.core.ui_kmp.generated.resources.settings_theme
import movies.core.ui_kmp.generated.resources.settings_theme_amoled
import movies.core.ui_kmp.generated.resources.settings_theme_dark
Expand Down Expand Up @@ -227,6 +229,8 @@ object MoviesStrings {
val settings_update = Res.string.settings_update
val settings_update_description = Res.string.settings_update_description
val settings_palette_colors = Res.string.settings_palette_colors
val settings_screenshots = Res.string.settings_screenshots
val settings_screenshots_description = Res.string.settings_screenshots_description

@Composable
fun settings_app_version_name(vararg formatArgs: Any): String {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ class SettingsViewModel(
initialValue = false
)

val isScreenshotBlockEnabled: StateFlow<Boolean> = interactor.isScreenshotBlockEnabled
.stateIn(
scope = this,
started = SharingStarted.Lazily,
initialValue = false
)

val appVersionData: StateFlow<AppVersionData> = flowOf(AppVersionData(appService.flavor.name))
.stateIn(
scope = this,
Expand Down Expand Up @@ -120,6 +127,10 @@ class SettingsViewModel(
interactor.setBiometricEnabled(enabled)
}

fun setScreenshotBlockEnabled(enabled: Boolean) = launch {
interactor.setScreenshotBlockEnabled(enabled)
}

fun requestReview(activity: Activity) {
reviewService.requestReview(activity)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ internal actual val isTileFeatureEnabled: Boolean
internal actual val isAppIconFeatureEnabled: Boolean
get() = true

internal actual val isScreenshotFeatureEnabled: Boolean
get() = true

internal actual val isGithubFeatureEnabled: Boolean
get() = true

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import org.michaelbel.movies.settings.model.isLanguageFeatureEnabled
import org.michaelbel.movies.settings.model.isMovieListFeatureEnabled
import org.michaelbel.movies.settings.model.isNotificationsFeatureEnabled
import org.michaelbel.movies.settings.model.isReviewAppFeatureEnabled
import org.michaelbel.movies.settings.model.isScreenshotFeatureEnabled
import org.michaelbel.movies.settings.model.isThemeFeatureEnabled
import org.michaelbel.movies.settings.model.isTileFeatureEnabled
import org.michaelbel.movies.settings.model.isUpdateAppFeatureEnabled
Expand Down Expand Up @@ -81,6 +82,7 @@ fun SettingsRoute(
val currentMovieList by viewModel.currentMovieList.collectAsStateWithLifecycle()
val isBiometricFeatureAvailable by viewModel.isBiometricFeatureEnabled.collectAsStateWithLifecycle()
val isBiometricEnabled by viewModel.isBiometricEnabled.collectAsStateWithLifecycle()
val isScreenshotBlockEnabled by viewModel.isScreenshotBlockEnabled.collectAsStateWithLifecycle()
val appVersionData by viewModel.appVersionData.collectAsStateWithLifecycle()

val context = LocalContext.current
Expand Down Expand Up @@ -248,6 +250,11 @@ fun SettingsRoute(
context.setIcon(icon)
}
),
screenshotData = SettingsData.ChangedData(
isFeatureEnabled = isScreenshotFeatureEnabled,
isEnabled = isScreenshotBlockEnabled,
onChange = viewModel::setScreenshotBlockEnabled
),
githubData = SettingsData.RequestedData(
isFeatureEnabled = isGithubFeatureEnabled,
onRequest = { openUrl(resultContract, toolbarColor, MOVIES_GITHUB_URL) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ internal expect val isBiometricFeatureEnabled: Boolean
internal expect val isWidgetFeatureEnabled: Boolean
internal expect val isTileFeatureEnabled: Boolean
internal expect val isAppIconFeatureEnabled: Boolean
internal expect val isScreenshotFeatureEnabled: Boolean
internal expect val isGithubFeatureEnabled: Boolean
internal expect val isReviewAppFeatureEnabled: Boolean
internal expect val isUpdateAppFeatureEnabled: Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ data class SettingsData(
val widgetData: RequestedData,
val tileData: RequestedData,
val appIconData: ListData<IconAlias>,
val screenshotData: ChangedData,
val githubData: RequestedData,
val reviewAppData: RequestedData,
val updateAppData: RequestedData,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,24 @@ internal fun SettingsScreenContent(
}
}
}
if (settingsData.screenshotData.isFeatureEnabled) {
item {
HorizontalDivider(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp),
thickness = .1.dp,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
}
item {
SettingSwitchItem(
title = stringResource(MoviesStrings.settings_screenshots),
description = stringResource(MoviesStrings.settings_screenshots_description),
icon = MoviesIcons.Screenshot,
checked = settingsData.screenshotData.isEnabled,
onClick = { settingsData.screenshotData.onChange(!settingsData.screenshotData.isEnabled) }
)
}
}
if (settingsData.githubData.isFeatureEnabled) {
item {
HorizontalDivider(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ internal actual val isTileFeatureEnabled: Boolean
internal actual val isAppIconFeatureEnabled: Boolean
get() = false

internal actual val isScreenshotFeatureEnabled: Boolean
get() = false

internal actual val isGithubFeatureEnabled: Boolean
get() = true

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import org.michaelbel.movies.settings.model.isLanguageFeatureEnabled
import org.michaelbel.movies.settings.model.isMovieListFeatureEnabled
import org.michaelbel.movies.settings.model.isNotificationsFeatureEnabled
import org.michaelbel.movies.settings.model.isReviewAppFeatureEnabled
import org.michaelbel.movies.settings.model.isScreenshotFeatureEnabled
import org.michaelbel.movies.settings.model.isThemeFeatureEnabled
import org.michaelbel.movies.settings.model.isTileFeatureEnabled
import org.michaelbel.movies.settings.model.isUpdateAppFeatureEnabled
Expand Down Expand Up @@ -102,6 +103,11 @@ fun SettingsRoute(
current = IconAlias.Red,
onSelect = {}
),
screenshotData = SettingsData.ChangedData(
isFeatureEnabled = isScreenshotFeatureEnabled,
isEnabled = false,
onChange = {}
),
githubData = SettingsData.RequestedData(
isFeatureEnabled = isGithubFeatureEnabled,
onRequest = { openUrl(MOVIES_GITHUB_URL) }
Expand Down
Loading
Loading