Skip to content

Instantly share code, notes, and snippets.

@vishal-nagarajan
Created January 2, 2026 12:05
Show Gist options
  • Select an option

  • Save vishal-nagarajan/c73ea3e748eafce24ee5e86711be4549 to your computer and use it in GitHub Desktop.

Select an option

Save vishal-nagarajan/c73ea3e748eafce24ee5e86711be4549 to your computer and use it in GitHub Desktop.

DeepWatch Developer Guide

Overview

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.

Tech Stack

  • Language: Kotlin
  • UI Framework: Jetpack Compose
  • Architecture: MVVM-inspired with reactive state management
  • Min SDK: 26 (Android 8.0 Oreo)
  • Target SDK: 35

Key Dependencies

// 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)

Architecture Overview

┌─────────────────────────────────────────────────────────────┐
│                        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)                         │  │
│  └───────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────┘

Data Flow

  1. User opens monitored app (YouTube, Instagram, etc.)
  2. AccessibilityService receives events (window changes, content changes, clicks)
  3. Detection logic determines if user is in short-form content
  4. If blocked: OverlayService is started, showing the blocking UI
  5. User makes choice: Go Back or Proceed
  6. State is persisted via PreferencesRepository (cooldown) and StatsRepository (statistics)

Project Structure

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

Core Components

ShortsAccessibilityService

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:

  1. WeakReference for static instance: Prevents memory leaks when service is destroyed
  2. Pre-compiled regex patterns: Performance optimization for text matching
  3. Coroutine error handling: All async operations wrapped in try-catch
  4. Handler cleanup in onDestroy: Prevents leaked callbacks

OverlayService

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()
}

PreferencesRepository

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
        }
    }
}

StatsRepository

Similar to PreferencesRepository but for statistics:

  • Total interventions
  • Today's interventions
  • Go back count
  • Automatic daily reset

Detection Strategies

YouTube Detection

YouTube Shorts are detected via:

  1. Activity class names: Look for "Shorts", "ShortsActivity", "ReelWatchFragment"
  2. View IDs: Search for "reel_player_page", "shorts_player"
  3. Tab clicks: Monitor clicks on the Shorts pivot bar tab

Instagram Detection

Instagram is more complex due to its single-activity architecture:

  1. Source Tracking: Track where the user is (Homepage, DM, Story, Reels Tab)
  2. Click Detection: Monitor navigation button clicks
  3. Full-Screen Detection: Only block when in actual Reels player
  4. 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 Detection

TikTok is entirely short-form content, so we block on main activities:

  • MainActivity
  • MainActivityInternal
  • LiveStreamActivity
  • DetailActivity

Other Apps

Similar patterns for Facebook Reels, Snapchat Spotlight, and Reddit Videos - looking for specific activity names, view IDs, and content descriptions.


State Management

DataStore Usage

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 State

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())
        }}
    )
}

UI Architecture

Jetpack Compose

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
    }
}

Navigation

Simple NavHost-based navigation:

NavHost(navController, startDestination = "home") {
    composable("home") {
        HomeScreen(onNavigateToSettings = { navController.navigate("settings") })
    }
    composable("settings") {
        SettingsScreen(onNavigateBack = { navController.popBackStack() })
    }
}

Theme System

Custom Material 3 theme with:

  • Light/Dark mode support
  • Custom color palette (Deep Blue primary, Teal accent)
  • Overlay-specific colors for dark background

Adding New Apps

To add support for a new app:

1. Add package name to service config

Edit res/xml/accessibility_service_config.xml:

android:packageNames="...,com.newapp.package"

2. Add preference keys

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
    }
}

3. Add detection handler

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
    }
}

4. Add UI toggle

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) } }
    )
}

Testing

Manual Testing

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

Testing Detection

  1. Install debug build
  2. Enable accessibility service
  3. Open monitored app
  4. Navigate to short-form content
  5. Verify overlay appears
  6. Check logcat for detection messages

Common Debug Tags

  • DeepWatch - All app logging
  • Look for:
    • "YouTube Shorts detected"
    • "Instagram Reels player detected"
    • ">>> TRIGGERING BLOCKING OVERLAY <<<"

Building & Release

Debug Build

./gradlew assembleDebug
# APK at: app/build/outputs/apk/debug/app-debug.apk

Release Build

  1. Create keystore.properties in project root:
storeFile=keystore/your-keystore.jks
storePassword=your-store-password
keyAlias=your-key-alias
keyPassword=your-key-password
  1. Build release:
./gradlew assembleRelease
# APK at: app/build/outputs/apk/release/app-release.apk

Version Management

Update in app/build.gradle.kts:

versionCode = 11  // Increment for each release
versionName = "2.0.0"  // Semantic versioning

Known Limitations

  1. App updates may break detection: When monitored apps change their UI structure, detection may fail until we update
  2. No root required: We use standard Android APIs, but this means we can't block in all scenarios
  3. Single-activity apps are harder: Instagram's architecture requires more complex detection
  4. Battery impact: Accessibility services run continuously, though our implementation is lightweight
  5. Overlay conflicts: Some apps or system UI may appear over our overlay in edge cases

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Make changes following existing code style
  4. Test thoroughly on multiple devices
  5. Submit a pull request

Code Style

  • Follow Kotlin coding conventions
  • Use meaningful variable/function names
  • Add comments for complex logic
  • Keep functions focused and small
  • Use Compose best practices

License

[Add your license here]


Contact

For questions or issues, contact the development team.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment