DeepWatch is an Android application that blocks short-form video content across multiple social media apps using Android's Accessibility Service API and System Alert Window (overlay) permission.
- Language: Kotlin
- UI Framework: Jetpack Compose
- Architecture: MVVM-inspired with reactive state management
- Min SDK: 26 (Android 8.0 Oreo)
- Target SDK: 35
// Compose BOM for version management
implementation(platform(libs.androidx.compose.bom))
// Core Android
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.activity.compose)
// Compose UI
implementation(libs.androidx.ui)
implementation(libs.androidx.material3)
implementation(libs.androidx.navigation.compose)
// Data persistence
implementation(libs.androidx.datastore.preferences)┌─────────────────────────────────────────────────────────────┐
│ MainActivity │
│ (Compose Navigation) │
└────────────────────────────┬────────────────────────────────┘
│
┌────────────────────┼────────────────────┐
│ │ │
▼ ▼ ▼
HomeScreen SettingsScreen OnboardingScreen
│ │
│ │
└────────────────────┼────────────────────┐
│ │
▼ ▼
PreferencesRepository StatsRepository
│ │
└────────────────────┘
│
▼
DataStore (Disk)
┌─────────────────────────────────────────────────────────────┐
│ ShortsAccessibilityService │
│ (Background - Monitors app activity) │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ YouTube │ │ Instagram │ │ TikTok │ ... │
│ │ Detection │ │ Detection │ │ Detection │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└────────────────────────────┬────────────────────────────────┘
│ startService(Intent)
▼
┌─────────────────────────────────────────────────────────────┐
│ OverlayService │
│ (Foreground - Shows blocking UI) │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ BlockingOverlayContent │ │
│ │ (Compose View) │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
- User opens monitored app (YouTube, Instagram, etc.)
- AccessibilityService receives events (window changes, content changes, clicks)
- Detection logic determines if user is in short-form content
- If blocked: OverlayService is started, showing the blocking UI
- User makes choice: Go Back or Proceed
- State is persisted via PreferencesRepository (cooldown) and StatsRepository (statistics)
app/src/main/
├── kotlin/com/deepwatch/
│ ├── DeepWatchApp.kt # Application class, notification channel
│ ├── MainActivity.kt # Entry point, navigation setup
│ ├── data/
│ │ ├── PreferencesRepository.kt # Settings storage (DataStore)
│ │ └── StatsRepository.kt # Statistics storage (DataStore)
│ ├── service/
│ │ ├── ShortsAccessibilityService.kt # Core detection logic
│ │ └── OverlayService.kt # Blocking overlay display
│ └── ui/
│ ├── overlay/
│ │ └── BlockingOverlayContent.kt # Overlay Compose UI
│ ├── screens/
│ │ ├── HomeScreen.kt # Main dashboard
│ │ └── SettingsScreen.kt # Configuration
│ └── theme/
│ ├── Color.kt # Color definitions
│ ├── Theme.kt # Material theme
│ └── Type.kt # Typography
├── res/
│ ├── values/
│ │ └── strings.xml
│ └── xml/
│ └── accessibility_service_config.xml # Service configuration
└── AndroidManifest.xml
The heart of the app. This Android AccessibilityService:
- Runs in the background when enabled
- Receives accessibility events from monitored apps
- Determines if user is viewing short-form content
- Triggers the blocking overlay when appropriate
Key Methods:
// Called when service is first connected
override fun onServiceConnected() {
// Initialize preferences, configure service
}
// Main event handler
override fun onAccessibilityEvent(event: AccessibilityEvent?) {
// Dispatch to app-specific handlers based on package name
}
// App-specific detection
private suspend fun handleYouTubeEvent(event: AccessibilityEvent)
private suspend fun handleInstagramEvent(event: AccessibilityEvent)
private suspend fun handleTikTokEvent(event: AccessibilityEvent)
// ... etc
// Trigger the overlay
private fun triggerOverlay() {
startService(Intent(this, OverlayService::class.java).apply {
action = OverlayService.ACTION_SHOW_OVERLAY
})
}Important Design Decisions:
- WeakReference for static instance: Prevents memory leaks when service is destroyed
- Pre-compiled regex patterns: Performance optimization for text matching
- Coroutine error handling: All async operations wrapped in try-catch
- Handler cleanup in onDestroy: Prevents leaked callbacks
A foreground service that displays the blocking overlay:
- Creates a ComposeView overlay using WindowManager
- Manages countdown timer state
- Handles user interaction (Go Back / Proceed)
- Sets cooldown period when user proceeds
Key Features:
// Thread-safe state management
private val secondsRemaining = AtomicInteger(10)
private val isCountdownComplete = AtomicBoolean(false)
// Overlay display
private fun showOverlay() {
serviceScope.launch {
// Get countdown duration
val countdownDuration = preferencesRepository.countdownSeconds.first()
withContext(Dispatchers.Main) {
// Create and show overlay
createOverlayView()
startCountdown()
}
}
}
// User actions
private fun handleGoBack() {
statsRepository.recordGoBack()
ShortsAccessibilityService.instance?.performBack()
hideOverlay()
}
private fun handleProceed() {
statsRepository.recordProceed()
preferencesRepository.setCooldownUntil(...)
hideOverlay()
}DataStore-based settings storage with Flow-based reactive reads:
class PreferencesRepository(private val context: Context) {
// Reading settings (reactive)
val isEnabled: Flow<Boolean> = context.dataStore.data.map { prefs ->
prefs[Keys.ENABLED] ?: true
}
// Writing settings
suspend fun setEnabled(enabled: Boolean) {
context.dataStore.edit { prefs ->
prefs[Keys.ENABLED] = enabled
}
}
}Similar to PreferencesRepository but for statistics:
- Total interventions
- Today's interventions
- Go back count
- Automatic daily reset
YouTube Shorts are detected via:
- Activity class names: Look for "Shorts", "ShortsActivity", "ReelWatchFragment"
- View IDs: Search for "reel_player_page", "shorts_player"
- Tab clicks: Monitor clicks on the Shorts pivot bar tab
Instagram is more complex due to its single-activity architecture:
- Source Tracking: Track where the user is (Homepage, DM, Story, Reels Tab)
- Click Detection: Monitor navigation button clicks
- Full-Screen Detection: Only block when in actual Reels player
- UI Element Detection: Look for Reels-specific view IDs
Instagram Source Enum:
private enum class InstagramSource {
REELS_TAB, // Dedicated Reels tab
HOMEPAGE, // Main feed
DM, // Direct messages
STORY, // Stories
UNKNOWN
}TikTok is entirely short-form content, so we block on main activities:
- MainActivity
- MainActivityInternal
- LiveStreamActivity
- DetailActivity
Similar patterns for Facebook Reels, Snapchat Spotlight, and Reddit Videos - looking for specific activity names, view IDs, and content descriptions.
All persistent state uses Jetpack DataStore Preferences:
// Define keys
private object Keys {
val ENABLED = booleanPreferencesKey("enabled")
val COUNTDOWN_SECONDS = intPreferencesKey("countdown_seconds")
// ...
}
// Read with Flow
val countdownSeconds: Flow<Int> = context.dataStore.data.map { prefs ->
prefs[Keys.COUNTDOWN_SECONDS] ?: DEFAULT_COUNTDOWN_SECONDS
}
// Write with edit
suspend fun setCountdownSeconds(seconds: Int) {
context.dataStore.edit { prefs ->
prefs[Keys.COUNTDOWN_SECONDS] = seconds.coerceIn(5, 30)
}
}UI components collect DataStore flows as state:
@Composable
fun SettingsScreen() {
val countdownSeconds by preferencesRepository.countdownSeconds.collectAsState(initial = 10)
// Use countdownSeconds in UI
Slider(
value = countdownSeconds.toFloat(),
onValueChange = { scope.launch {
preferencesRepository.setCountdownSeconds(it.toInt())
}}
)
}All UI is built with Jetpack Compose following Material Design 3:
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen(onNavigateToSettings: () -> Unit) {
Scaffold(
topBar = {
TopAppBar(
title = { Text("DeepWatch") },
actions = {
IconButton(onClick = onNavigateToSettings) {
Icon(Icons.Default.Settings, "Settings")
}
}
)
}
) { paddingValues ->
// Content
}
}Simple NavHost-based navigation:
NavHost(navController, startDestination = "home") {
composable("home") {
HomeScreen(onNavigateToSettings = { navController.navigate("settings") })
}
composable("settings") {
SettingsScreen(onNavigateBack = { navController.popBackStack() })
}
}Custom Material 3 theme with:
- Light/Dark mode support
- Custom color palette (Deep Blue primary, Teal accent)
- Overlay-specific colors for dark background
To add support for a new app:
Edit res/xml/accessibility_service_config.xml:
android:packageNames="...,com.newapp.package"In PreferencesRepository.kt:
private object Keys {
// ...
val NEW_APP_ENABLED = booleanPreferencesKey("new_app_enabled")
}
val isNewAppEnabled: Flow<Boolean> = context.dataStore.data.map { prefs ->
prefs[Keys.NEW_APP_ENABLED] ?: false
}
suspend fun setNewAppEnabled(enabled: Boolean) {
context.dataStore.edit { prefs ->
prefs[Keys.NEW_APP_ENABLED] = enabled
}
}In ShortsAccessibilityService.kt:
private const val NEW_APP_PACKAGE = "com.newapp.package"
override fun onAccessibilityEvent(event: AccessibilityEvent?) {
// In the when block:
NEW_APP_PACKAGE -> {
if (preferencesRepository.isNewAppEnabled.first()) {
handleNewAppEvent(event)
}
}
}
private suspend fun handleNewAppEvent(event: AccessibilityEvent) {
when (event.eventType) {
AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED -> {
val className = event.className?.toString() ?: return
// Check for short-form video activities
if (className.contains("ShortVideoActivity")) {
currentApp = NEW_APP_PACKAGE
handleShortFormEntered()
}
}
// Add more event types as needed
}
}In SettingsScreen.kt:
val isNewAppEnabled by preferencesRepository.isNewAppEnabled.collectAsState(initial = false)
SettingsCard {
ToggleRow(
title = "New App",
description = "Block short videos in New App",
checked = isNewAppEnabled,
enabled = isEnabled,
onCheckedChange = { scope.launch { preferencesRepository.setNewAppEnabled(it) } }
)
}Use ADB to verify accessibility service:
# Check if service is enabled
adb shell settings get secure enabled_accessibility_services
# View logcat for DeepWatch
adb logcat -s DeepWatch:*
# Test overlay permission
adb shell appops get com.deepwatch SYSTEM_ALERT_WINDOW- Install debug build
- Enable accessibility service
- Open monitored app
- Navigate to short-form content
- Verify overlay appears
- Check logcat for detection messages
DeepWatch- All app logging- Look for:
"YouTube Shorts detected""Instagram Reels player detected"">>> TRIGGERING BLOCKING OVERLAY <<<"
./gradlew assembleDebug
# APK at: app/build/outputs/apk/debug/app-debug.apk- Create
keystore.propertiesin project root:
storeFile=keystore/your-keystore.jks
storePassword=your-store-password
keyAlias=your-key-alias
keyPassword=your-key-password- Build release:
./gradlew assembleRelease
# APK at: app/build/outputs/apk/release/app-release.apkUpdate in app/build.gradle.kts:
versionCode = 11 // Increment for each release
versionName = "2.0.0" // Semantic versioning- App updates may break detection: When monitored apps change their UI structure, detection may fail until we update
- No root required: We use standard Android APIs, but this means we can't block in all scenarios
- Single-activity apps are harder: Instagram's architecture requires more complex detection
- Battery impact: Accessibility services run continuously, though our implementation is lightweight
- Overlay conflicts: Some apps or system UI may appear over our overlay in edge cases
- Fork the repository
- Create a feature branch
- Make changes following existing code style
- Test thoroughly on multiple devices
- Submit a pull request
- Follow Kotlin coding conventions
- Use meaningful variable/function names
- Add comments for complex logic
- Keep functions focused and small
- Use Compose best practices
[Add your license here]
For questions or issues, contact the development team.