screentinker/android/app/src/main/java/com/remotedisplay/player/ScreenCapturePermissionActivity.kt
ScreenTinker 66ef47239f fix(android): Android 14+ MediaProjection / foreground-service compliance (#5)
On Android 14+ (targetSdk 34) the app could fail to run at all on newer devices
(Pixel 10, onn HD stick). Root cause: the always-on WebSocketService called the
2-arg startForeground(), which claims EVERY foreground-service type declared in
the manifest - including mediaProjection. Android 14 rejects starting a
mediaProjection-typed FGS without a MediaProjection consent token, so the core
service threw on launch and the player never came up. Matches the reporter's
"screen recording policy" hunch - via the FGS type, not the capture trigger.

Fixes:
- WebSocketService now claims ONLY mediaPlayback (explicit
  startForeground(..., FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK), API>=29 guarded;
  2-arg on older). Manifest type narrowed to mediaPlayback.
- New MediaProjectionService (manifest type mediaProjection), started only AFTER
  the user grants consent. It enters the foreground with the mediaProjection type
  BEFORE getMediaProjection() (required on 14+), then drives ScreenCaptureService.
  The consent Activity now hands the result to this service instead of calling
  getMediaProjection() directly (an Activity can't hold that FGS type).
- ScreenCaptureService: register the MediaProjection.Callback BEFORE
  createVirtualDisplay() (Android 14 throws IllegalStateException otherwise).

Verified: Kotlin compiles, manifest merges (WebSocketService=mediaPlayback,
MediaProjectionService=mediaProjection), signed debug APK builds. NOT yet
verified on-device - needs a Pixel 10 / onn-stick run + logcat to confirm the
exact crash is resolved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 17:19:56 -05:00

68 lines
2.6 KiB
Kotlin

package com.remotedisplay.player
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.media.projection.MediaProjectionManager
import android.os.Bundle
import android.util.Log
import com.remotedisplay.player.service.MediaProjectionService
/**
* Transparent activity that requests MediaProjection permission.
* Shows a system dialog asking "Start recording?" - user taps "Start now" once.
*/
class ScreenCapturePermissionActivity : Activity() {
companion object {
private const val REQUEST_CODE = 1001
private const val TAG = "ScreenCapturePermission"
// Store the result intent so the service can use it
var resultCode: Int = RESULT_CANCELED
private set
var resultData: Intent? = null
private set
var hasPermission = false
private set
fun requestPermission(context: Context) {
val intent = Intent(context, ScreenCapturePermissionActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(intent)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val mediaProjectionManager = getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
startActivityForResult(mediaProjectionManager.createScreenCaptureIntent(), REQUEST_CODE)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE) {
if (resultCode == RESULT_OK && data != null) {
Log.i(TAG, "MediaProjection permission granted, starting via service")
// Store the result so the service can create the projection
Companion.resultCode = resultCode
Companion.resultData = data?.clone() as? Intent
Companion.hasPermission = true
// #5: hand the consent to the dedicated mediaProjection foreground
// service. It must enter the foreground with the mediaProjection FGS
// type BEFORE getMediaProjection() on Android 14+ - an Activity can't
// do that, so we can't call getMediaProjection() directly here.
MediaProjectionService.start(this, resultCode, data)
getSharedPreferences("remote_display", MODE_PRIVATE)
.edit().putBoolean("screen_capture_granted", true).apply()
} else {
Log.w(TAG, "MediaProjection permission denied")
}
}
finish()
}
}