如何实现不依赖Activity生命周期的屏幕时长统计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:
Option A: Room Database (Recommended)
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

