Add screen capture protection (#292)
Co-authored-by: Rahul Sharma <rahul.shwork@gmail.com>
This commit is contained in:
parent
8366d76dcd
commit
6897496423
6 changed files with 77 additions and 2 deletions
|
|
@ -124,4 +124,5 @@ data class ReaderScreenState(
|
||||||
val customAppThemes: List<CustomAppTheme> = emptyList(),
|
val customAppThemes: List<CustomAppTheme> = emptyList(),
|
||||||
val allTags: List<TagEntity> = emptyList(),
|
val allTags: List<TagEntity> = emptyList(),
|
||||||
val showTagSelectionDialogFor: Set<String> = emptySet(),
|
val showTagSelectionDialogFor: Set<String> = emptySet(),
|
||||||
|
val isScreenCaptureProtectionEnabled: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -359,6 +359,15 @@ fun HomeScreen(
|
||||||
onExportLogsClick = { viewModel.exportLogsToFile(context) },
|
onExportLogsClick = { viewModel.exportLogsToFile(context) },
|
||||||
onToggleHideReaderAi = {
|
onToggleHideReaderAi = {
|
||||||
saveHideReaderAiFeatures(context, !loadHideReaderAiFeatures(context))
|
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 {
|
} else {
|
||||||
|
|
@ -1019,7 +1028,8 @@ fun DefaultTopAppBar(
|
||||||
onTestSpeechBubbleDetectionClick: () -> Unit,
|
onTestSpeechBubbleDetectionClick: () -> Unit,
|
||||||
onLanguageClick: () -> Unit,
|
onLanguageClick: () -> Unit,
|
||||||
onExportLogsClick: () -> Unit,
|
onExportLogsClick: () -> Unit,
|
||||||
onToggleHideReaderAi: () -> Unit
|
onToggleHideReaderAi: () -> Unit,
|
||||||
|
onScreenCaptureProtectionChange: (Boolean) -> Unit
|
||||||
) {
|
) {
|
||||||
var showOptionsMenu by remember { mutableStateOf(false) }
|
var showOptionsMenu by remember { mutableStateOf(false) }
|
||||||
var showLimitMenu 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 = {
|
DropdownMenuItem(text = { Text(stringResource(R.string.options_external_file_behavior)) }, onClick = {
|
||||||
onExternalFileBehaviorClick()
|
onExternalFileBehaviorClick()
|
||||||
showOptionsMenu = false
|
showOptionsMenu = false
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ package com.aryan.reader
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.WindowManager
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
|
|
@ -34,6 +35,8 @@ import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
|
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
|
||||||
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
|
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
|
@ -87,6 +90,8 @@ class MainActivity : AppCompatActivity() {
|
||||||
setContent {
|
setContent {
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
ScreenCaptureProtectionEffect(enabled = uiState.isScreenCaptureProtectionEnabled)
|
||||||
|
|
||||||
val darkTheme = when (uiState.appThemeMode) {
|
val darkTheme = when (uiState.appThemeMode) {
|
||||||
AppThemeMode.LIGHT -> false
|
AppThemeMode.LIGHT -> false
|
||||||
AppThemeMode.DARK -> true
|
AppThemeMode.DARK -> true
|
||||||
|
|
@ -131,4 +136,21 @@ class MainActivity : AppCompatActivity() {
|
||||||
viewModel.onFileSelected(uri, isFromRecent = false, isExternalIntent = true)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -544,6 +544,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
||||||
activeTabBookId = prefs.getString(KEY_ACTIVE_TAB, null),
|
activeTabBookId = prefs.getString(KEY_ACTIVE_TAB, null),
|
||||||
externalFileBehavior = prefs.getString(KEY_EXTERNAL_FILE_BEHAVIOR, "ASK") ?: "ASK",
|
externalFileBehavior = prefs.getString(KEY_EXTERNAL_FILE_BEHAVIOR, "ASK") ?: "ASK",
|
||||||
useStrictFileFilter = prefs.getBoolean(KEY_USE_STRICT_FILE_FILTER, false),
|
useStrictFileFilter = prefs.getBoolean(KEY_USE_STRICT_FILE_FILTER, false),
|
||||||
|
isScreenCaptureProtectionEnabled = prefs.getBoolean(KEY_SCREEN_CAPTURE_PROTECTION, false),
|
||||||
appThemeMode = try {
|
appThemeMode = try {
|
||||||
AppThemeMode.valueOf(prefs.getString(KEY_APP_THEME_MODE, AppThemeMode.SYSTEM.name) ?: AppThemeMode.SYSTEM.name)
|
AppThemeMode.valueOf(prefs.getString(KEY_APP_THEME_MODE, AppThemeMode.SYSTEM.name) ?: AppThemeMode.SYSTEM.name)
|
||||||
} catch (_: Exception) { AppThemeMode.SYSTEM },
|
} catch (_: Exception) { AppThemeMode.SYSTEM },
|
||||||
|
|
@ -5227,6 +5228,11 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
||||||
_internalState.update { it.copy(useStrictFileFilter = enabled) }
|
_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> {
|
private fun loadCustomAppThemes(prefs: SharedPreferences): List<CustomAppTheme> {
|
||||||
val jsonString = prefs.getString(KEY_CUSTOM_APP_THEMES, "[]") ?: "[]"
|
val jsonString = prefs.getString(KEY_CUSTOM_APP_THEMES, "[]") ?: "[]"
|
||||||
val themes = mutableListOf<CustomAppTheme>()
|
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_LAST_OPEN_FILE_TYPE = "last_open_file_type"
|
||||||
private const val KEY_EXTERNAL_FILE_BEHAVIOR = "external_file_behavior"
|
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_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_THEME_MODE = "app_theme_mode"
|
||||||
private const val KEY_APP_CONTRAST_OPTION = "app_contrast_option"
|
private const val KEY_APP_CONTRAST_OPTION = "app_contrast_option"
|
||||||
private const val KEY_APP_SEED_COLOR = "app_seed_color"
|
private const val KEY_APP_SEED_COLOR = "app_seed_color"
|
||||||
|
|
|
||||||
|
|
@ -1304,4 +1304,7 @@
|
||||||
<string name="action_exit_smart_zoom">Exit Smart Zoom</string>
|
<string name="action_exit_smart_zoom">Exit Smart Zoom</string>
|
||||||
<string name="action_smart_comic_zoom">Smart Comic Zoom</string>
|
<string name="action_smart_comic_zoom">Smart Comic Zoom</string>
|
||||||
<string name="desc_toggle_smart_comic_zoom">Toggle Smart Comic Zoom</string>
|
<string name="desc_toggle_smart_comic_zoom">Toggle Smart Comic Zoom</string>
|
||||||
|
<string name="options_screen_capture_protection">Screen capture protection</string>
|
||||||
|
<string name="banner_screen_capture_protection_on">Screen capture protection is on</string>
|
||||||
|
<string name="banner_screen_capture_protection_off">Screen capture protection is off</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -319,6 +319,25 @@ class MainViewModelTest {
|
||||||
verify { mockEditor.putString("external_file_behavior", "KEEP") }
|
verify { mockEditor.putString("external_file_behavior", "KEEP") }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `screen capture protection persists and updates state`() = runTest {
|
||||||
|
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
|
||||||
|
viewModel.uiState.collect {}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.setScreenCaptureProtectionEnabled(true)
|
||||||
|
|
||||||
|
val enabledState = viewModel.uiState.first { it.isScreenCaptureProtectionEnabled }
|
||||||
|
assertTrue(enabledState.isScreenCaptureProtectionEnabled)
|
||||||
|
verify { mockEditor.putBoolean("screen_capture_protection_enabled", true) }
|
||||||
|
|
||||||
|
viewModel.setScreenCaptureProtectionEnabled(false)
|
||||||
|
|
||||||
|
val disabledState = viewModel.uiState.first { !it.isScreenCaptureProtectionEnabled }
|
||||||
|
assertFalse(disabledState.isScreenCaptureProtectionEnabled)
|
||||||
|
verify { mockEditor.putBoolean("screen_capture_protection_enabled", false) }
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `setSortOrder persists preference and reorders visible home and library lists`() = runTest {
|
fun `setSortOrder persists preference and reorders visible home and library lists`() = runTest {
|
||||||
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
|
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue