Add screen capture protection (#292)

Co-authored-by: Rahul Sharma <rahul.shwork@gmail.com>
This commit is contained in:
Aryan 2026-05-10 10:18:48 +05:30 committed by GitHub
parent 8366d76dcd
commit 6897496423
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 77 additions and 2 deletions

View file

@ -124,4 +124,5 @@ data class ReaderScreenState(
val customAppThemes: List<CustomAppTheme> = emptyList(),
val allTags: List<TagEntity> = emptyList(),
val showTagSelectionDialogFor: Set<String> = emptySet(),
val isScreenCaptureProtectionEnabled: Boolean = false,
)

View file

@ -359,6 +359,15 @@ fun HomeScreen(
onExportLogsClick = { viewModel.exportLogsToFile(context) },
onToggleHideReaderAi = {
saveHideReaderAiFeatures(context, !loadHideReaderAiFeatures(context))
},
onScreenCaptureProtectionChange = { enabled ->
viewModel.setScreenCaptureProtectionEnabled(enabled)
val messageRes = if (enabled) {
R.string.banner_screen_capture_protection_on
} else {
R.string.banner_screen_capture_protection_off
}
viewModel.showBanner(context.getString(messageRes))
}
)
} else {
@ -1019,7 +1028,8 @@ fun DefaultTopAppBar(
onTestSpeechBubbleDetectionClick: () -> Unit,
onLanguageClick: () -> Unit,
onExportLogsClick: () -> Unit,
onToggleHideReaderAi: () -> Unit
onToggleHideReaderAi: () -> Unit,
onScreenCaptureProtectionChange: (Boolean) -> Unit
) {
var showOptionsMenu by remember { mutableStateOf(false) }
var showLimitMenu by remember { mutableStateOf(false) }
@ -1090,6 +1100,19 @@ fun DefaultTopAppBar(
}
})
DropdownMenuItem(
text = { Text(stringResource(R.string.options_screen_capture_protection)) },
onClick = {
onScreenCaptureProtectionChange(!uiState.isScreenCaptureProtectionEnabled)
showOptionsMenu = false
},
trailingIcon = {
if (uiState.isScreenCaptureProtectionEnabled) {
Icon(Icons.Default.Check, contentDescription = stringResource(R.string.content_desc_enabled))
}
}
)
DropdownMenuItem(text = { Text(stringResource(R.string.options_external_file_behavior)) }, onClick = {
onExternalFileBehaviorClick()
showOptionsMenu = false

View file

@ -22,6 +22,7 @@ package com.aryan.reader
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.view.WindowManager
import android.webkit.WebView
import androidx.appcompat.app.AppCompatActivity
import androidx.activity.compose.setContent
@ -34,6 +35,8 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope
import androidx.navigation.compose.rememberNavController
@ -87,6 +90,8 @@ class MainActivity : AppCompatActivity() {
setContent {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
ScreenCaptureProtectionEffect(enabled = uiState.isScreenCaptureProtectionEnabled)
val darkTheme = when (uiState.appThemeMode) {
AppThemeMode.LIGHT -> false
AppThemeMode.DARK -> true
@ -131,4 +136,21 @@ class MainActivity : AppCompatActivity() {
viewModel.onFileSelected(uri, isFromRecent = false, isExternalIntent = true)
}
}
}
}
@Composable
private fun MainActivity.ScreenCaptureProtectionEffect(enabled: Boolean) {
DisposableEffect(enabled) {
if (enabled) {
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
} else {
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
}
onDispose {
if (enabled) {
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
}
}
}
}

View file

@ -544,6 +544,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
activeTabBookId = prefs.getString(KEY_ACTIVE_TAB, null),
externalFileBehavior = prefs.getString(KEY_EXTERNAL_FILE_BEHAVIOR, "ASK") ?: "ASK",
useStrictFileFilter = prefs.getBoolean(KEY_USE_STRICT_FILE_FILTER, false),
isScreenCaptureProtectionEnabled = prefs.getBoolean(KEY_SCREEN_CAPTURE_PROTECTION, false),
appThemeMode = try {
AppThemeMode.valueOf(prefs.getString(KEY_APP_THEME_MODE, AppThemeMode.SYSTEM.name) ?: AppThemeMode.SYSTEM.name)
} catch (_: Exception) { AppThemeMode.SYSTEM },
@ -5227,6 +5228,11 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
_internalState.update { it.copy(useStrictFileFilter = enabled) }
}
fun setScreenCaptureProtectionEnabled(enabled: Boolean) {
prefs.edit { putBoolean(KEY_SCREEN_CAPTURE_PROTECTION, enabled) }
_internalState.update { it.copy(isScreenCaptureProtectionEnabled = enabled) }
}
private fun loadCustomAppThemes(prefs: SharedPreferences): List<CustomAppTheme> {
val jsonString = prefs.getString(KEY_CUSTOM_APP_THEMES, "[]") ?: "[]"
val themes = mutableListOf<CustomAppTheme>()
@ -5506,6 +5512,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
private const val KEY_LAST_OPEN_FILE_TYPE = "last_open_file_type"
private const val KEY_EXTERNAL_FILE_BEHAVIOR = "external_file_behavior"
private const val KEY_USE_STRICT_FILE_FILTER = "use_strict_file_filter"
private const val KEY_SCREEN_CAPTURE_PROTECTION = "screen_capture_protection_enabled"
private const val KEY_APP_THEME_MODE = "app_theme_mode"
private const val KEY_APP_CONTRAST_OPTION = "app_contrast_option"
private const val KEY_APP_SEED_COLOR = "app_seed_color"