You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现不依赖Activity生命周期的屏幕时长统计Broadcast Receiver?

Solution for Background Screen-On Time Tracking with Broadcast Receiver

Let's break down your problem step by step—you need to track daily screen-on time without tying the Broadcast Receiver to an Activity's lifecycle, while following Google's modern background execution rules.

1. Why Static Registration Isn't an Option

You’re correct: starting from Android 7.0 (API 24), ACTION_SCREEN_ON and ACTION_SCREEN_OFF can’t be registered in the manifest. Dynamic registration is mandatory here, but we need to decouple it from Activity lifecycle to enable background tracking.

2. The Right Approach: Foreground Service + Dynamic Broadcast Registration

Google discourages plain background services because they’re prone to being killed by the system (especially on Android O+). Instead, a Foreground Service is your best bet—it runs with higher priority (backed by a persistent notification) and won’t be terminated arbitrarily. Here’s how to set it up:

Step 1: Create the Foreground Service

This service will handle registering/unregistering the Broadcast Receiver and managing screen time tracking logic.

class ScreenTimeTrackingService : Service() {
    private lateinit var screenReceiver: ScreenReceiver
    private var screenOnStartTime: Long = 0

    override fun onCreate() {
        super.onCreate()
        // Initialize and register the Broadcast Receiver
        screenReceiver = ScreenReceiver()
        val intentFilter = IntentFilter().apply {
            addAction(Intent.ACTION_SCREEN_ON)
            addAction(Intent.ACTION_SCREEN_OFF)
        }
        registerReceiver(screenReceiver, intentFilter)

        // Start as foreground service (required for Android O+)
        val notification = NotificationCompat.Builder(this, "SCREEN_TRACKING_CHANNEL")
            .setContentTitle("Tracking Screen Time")
            .setContentText("Monitoring your screen usage")
            .setSmallIcon(R.drawable.ic_notification)
            .build()

        // Create notification channel first (Android O+)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                "SCREEN_TRACKING_CHANNEL",
                "Screen Time Tracking",
                NotificationManager.IMPORTANCE_LOW
            )
            getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
        }

        startForeground(1, notification)
    }

    override fun onDestroy() {
        super.onDestroy()
        // Unregister receiver to avoid memory leaks
        unregisterReceiver(screenReceiver)
    }

    override fun onBind(intent: Intent): IBinder? {
        return null // No bound service needed for this use case
    }

    // Inner Broadcast Receiver class
    inner class ScreenReceiver : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
            when (intent?.action) {
                Intent.ACTION_SCREEN_ON -> {
                    // Record start time when screen turns on
                    screenOnStartTime = System.currentTimeMillis()
                }
                Intent.ACTION_SCREEN_OFF -> {
                    // Calculate duration and update daily total
                    val duration = System.currentTimeMillis() - screenOnStartTime
                    if (duration > 0) {
                        ScreenTimeStorage.updateDailyTotal(duration)
                    }
                }
            }
        }
    }
}

Step 2: Start the Service

Launch the service once (e.g., when the user opens your main Activity or on app startup via Application.onCreate()):

// In your MainActivity's onCreate
if (!isServiceRunning(ScreenTimeTrackingService::class.java)) {
    val serviceIntent = Intent(this, ScreenTimeTrackingService::class.java)
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        startForegroundService(serviceIntent)
    } else {
        startService(serviceIntent)
    }
}

// Helper method to check if service is running
private fun isServiceRunning(serviceClass: Class<*>): Boolean {
    val manager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
    return manager.getRunningServices(Int.MAX_VALUE)
        .any { it.service.className == serviceClass.name }
}

Step 3: Handle Device Reboots

If the device restarts, your service will stop. Fix this with a static BOOT_COMPLETED receiver (allowed by Android):

<!-- Add to AndroidManifest.xml -->
<receiver
    android:name=".BootCompletedReceiver"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />
    </intent-filter>
</receiver>

Implement the receiver to restart the foreground service:

class BootCompletedReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        if (intent?.action == Intent.ACTION_BOOT_COMPLETED) {
            val serviceIntent = Intent(context, ScreenTimeTrackingService::class.java)
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                context?.startForegroundService(serviceIntent)
            } else {
                context?.startService(serviceIntent)
            }
        }
    }
}

3. Best Storage Options for Screen Time Data

Choose based on your needs:

Perfect if you need detailed session data for generating charts. It’s structured, scalable, and integrates well with lifecycle components.

Example Entity:

@Entity(tableName = "screen_time")
data class ScreenTimeEntry(
    @PrimaryKey(autoGenerate = true) val id: Int = 0,
    val date: String, // Use ISO format like "2024-05-20"
    val totalDuration: Long, // In milliseconds
    val lastUpdated: Long = System.currentTimeMillis()
)

Create a DAO to insert/update daily totals and query data for your UI.

Option B: SharedPreferences

Simpler for basic daily total tracking without detailed sessions:

object ScreenTimeStorage {
    private const val PREF_NAME = "ScreenTimePrefs"
    private const val KEY_DAILY_TOTAL = "daily_total_"

    fun updateDailyTotal(addDuration: Long) {
        val dateKey = KEY_DAILY_TOTAL + getCurrentDateString()
        val prefs = ContextCompat.getSharedPreferences(
            MyApp.instance, PREF_NAME, Context.MODE_PRIVATE
        )
        val currentTotal = prefs.getLong(dateKey, 0)
        prefs.edit().putLong(dateKey, currentTotal + addDuration).apply()
    }

    fun getDailyTotal(date: String): Long {
        val dateKey = KEY_DAILY_TOTAL + date
        val prefs = ContextCompat.getSharedPreferences(
            MyApp.instance, PREF_NAME, Context.MODE_PRIVATE
        )
        return prefs.getLong(dateKey, 0)
    }

    private fun getCurrentDateString(): String {
        return SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date())
    }
}

4. Required Permissions

Add these to your AndroidManifest.xml:

<!-- For foreground service -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- For BOOT_COMPLETED receiver -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<!-- Android 13+ requires notification permission -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

On Android 13+, request POST_NOTIFICATIONS runtime permission before starting the foreground service.

5. Notes on System Restrictions

  • Doze Mode: Foreground services are partially exempt, but long-term unused apps may still face restrictions. This rarely affects screen time tracking since the system sends screen state broadcasts regardless.
  • Battery Optimization: If you notice tracking gaps, ask users to exclude your app from battery optimization (explain why clearly to avoid user frustration).

内容的提问来源于stack exchange,提问作者Rafa

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 06:50:59