Android使用MediaProjection截图报错:需媒体投影类型前台服务
问题描述
我是Android开发新手,在Android Studio 2024中用Kotlin开发跨应用截图应用,采用MediaProjection方案。应用包含MainActivity与CaptureService,但执行projection = mediaProjectionManager.getMediaProjection(resultCode, data!!)时,触发错误:
Media projections require a foreground service of type ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
导致应用崩溃。已在AndroidManifest中添加相关权限,服务中也配置了通知,但问题仍未解决。
相关代码
MainActivity
package com.example.screenshot import android.annotation.SuppressLint import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.app.Service import android.content.Intent import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION import android.graphics.Color import android.os.Handler import android.os.IBinder import android.os.Looper import android.util.Log import android.widget.Toast import androidx.core.app.ServiceCompat import java.util.* import com.example.screenshot.Capture class MainActivity : Activity() { companion object { private const val REQUEST_CAPTURE = 1 var projection: MediaProjection? = null } private lateinit var mediaProjectionManager: MediaProjectionManager override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val startServiceButton: Button = findViewById(R.id.start_service_button) val stopServiceButton: Button = findViewById(R.id.stop_service_button) mediaProjectionManager = getSystemService(Service.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager startActivityForResult(mediaProjectionManager.createScreenCaptureIntent() , REQUEST_CAPTURE) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { val intent = Intent(this, CaptureService::class.java) if (requestCode == REQUEST_CAPTURE) { if (resultCode == RESULT_OK) { .setAction(CaptureService.ACTION_ENABLE_CAPTURE) startForegroundService(intent) Thread.sleep(100) projection = mediaProjectionManager.getMediaProjection(resultCode, data!!) Toast.makeText(this, "All OK", Toast.LENGTH_SHORT).show() } else { projection = null Toast.makeText(this, "Error", Toast.LENGTH_SHORT).show() } } //finish() } }
CaptureService
package com.example.screenshot import android.annotation.SuppressLint import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.app.Service import android.content.Intent import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION import android.graphics.Color import android.os.Handler import android.os.IBinder import android.os.Looper import android.util.Log import android.widget.Toast import androidx.core.app.ServiceCompat import java.util.* import com.example.screenshot.Capture class CaptureService : Service() { companion object { private val TAG = CaptureService::class.qualifiedName val ACTION_ENABLE_CAPTURE = "enable_capture" } private val notificationId = Random().nextInt() private val capture = Capture(this) override fun onBind(intent: Intent?): IBinder? { return null } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { if (intent != null) { when (intent.action) { ACTION_ENABLE_CAPTURE -> onEnableCapture() } } val channelId = "001" val channelName = "myChannel" val channel = NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_NONE) channel.lightColor = Color.BLUE channel.lockscreenVisibility = Notification.VISIBILITY_PRIVATE val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager manager.createNotificationChannel(channel) val notification: Notification = Notification.Builder(applicationContext, channelId).setOngoing(true) .setSmallIcon(R.mipmap.ic_launcher).setCategory( Notification.CATEGORY_SERVICE ).build() ServiceCompat.startForeground(this,1,notification, FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION) return Service.START_STICKY } private fun enableCapture() { if (MainActivity.projection == null) { println("Phase1") Log.d(TAG, "startActivity(CaptureActivity)") val intent = Intent(this, MainActivity::class.java) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) startActivity(intent) } else { onEnableCapture() } } private fun onEnableCapture() { MainActivity.projection?.run { capture.run(this) { capture.stop() // save bitmap } } } private fun disableCapture() { capture.stop() MainActivity.projection = null } override fun onDestroy() { super.onDestroy() disableCapture() } }
Capture类
package com.example.screenshot import android.content.Context import android.graphics.Bitmap import android.graphics.PixelFormat import android.hardware.display.DisplayManager import android.hardware.display.VirtualDisplay import android.media.ImageReader import android.media.projection.MediaProjection import android.util.Log import android.widget.Toast class Capture(val context: Context) : ImageReader.OnImageAvailableListener { companion object { val TAG = Capture::class.qualifiedName } private var display: VirtualDisplay? = null private var onCaptureListener: ((Bitmap) -> Unit)? = null fun run(mediaProjection: MediaProjection, onCaptureListener: (Bitmap) -> Unit) { this.onCaptureListener = onCaptureListener if (display == null) { display = createDisplay(mediaProjection) } } private fun createDisplay(mediaProjection: MediaProjection): VirtualDisplay { context.resources.displayMetrics.run { val maxImages = 2 val reader = ImageReader.newInstance( widthPixels, heightPixels, PixelFormat.RGBA_8888, maxImages) reader.setOnImageAvailableListener(this@Capture, null) val display = mediaProjection.createVirtualDisplay( "Capture Display", widthPixels, heightPixels, densityDpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, reader.surface, null, null) Log.d(TAG, "createVirtualDisplay") return display } } override fun onImageAvailable(reader: ImageReader) { if (display != null) { onCaptureListener?.invoke(captureImage(reader)) } } private fun captureImage(reader: ImageReader): Bitmap { val image = reader.acquireLatestImage() context.resources.displayMetrics.run { image.planes[0].run { val bitmap = Bitmap.createBitmap( rowStride / pixelStride, heightPixels, Bitmap.Config.ARGB_8888) bitmap.copyPixelsFromBuffer(buffer) image.close() return bitmap } } } fun stop() { display?.release() display = null onCaptureListener = null } }
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <application android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.ScreenShot" tools:targetApi="31"> <activity android:name=".MainActivity" android:exported="true" android:label="@string/app_name" android:theme="@style/Theme.ScreenShot"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <service android:name=".CaptureService" android:enabled="true" android:exported="true" android:stopWithTask="true" android:foregroundServiceType="mediaProjection" /> /> </application> <uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" /> </manifest>
解决方案
核心问题
从Android 12(API 31)开始,调用MediaProjectionManager.getMediaProjection()必须由已启动的FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION类型前台服务发起,你的代码在Activity中调用该方法时,服务还未完成前台状态初始化,导致系统权限校验失败。同时代码存在语法错误和逻辑漏洞,需要一并修复。
具体修复步骤
1. 重构MainActivity:将授权结果传递给服务
移除Activity中获取MediaProjection的逻辑,改为将授权结果传递给服务,由服务处理:
package com.example.screenshot import android.app.Activity import android.app.MediaProjectionManager import android.content.Intent import android.os.Bundle import android.widget.Button import android.widget.Toast class MainActivity : Activity() { companion object { private const val REQUEST_CAPTURE = 1 } private lateinit var mediaProjectionManager: MediaProjectionManager override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val startServiceButton: Button = findViewById(R.id.start_service_button) val stopServiceButton: Button = findViewById(R.id.stop_service_button) mediaProjectionManager = getSystemService(MediaProjectionManager::class.java) startServiceButton.setOnClickListener { startActivityForResult(mediaProjectionManager.createScreenCaptureIntent(), REQUEST_CAPTURE) } stopServiceButton.setOnClickListener { stopService(Intent(this, CaptureService::class.java)) Toast.makeText(this, "服务已停止", Toast.LENGTH_SHORT).show() } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == REQUEST_CAPTURE) { if (resultCode == RESULT_OK && data != null) { val intent = Intent(this, CaptureService::class.java).apply { action = CaptureService.ACTION_ENABLE_CAPTURE putExtra("RESULT_CODE", resultCode) putExtra("DATA", data) } startForegroundService(intent) Toast.makeText(this, "服务已启动", Toast.LENGTH_SHORT).show() } else { Toast.makeText(this, "截图授权失败", Toast.LENGTH_SHORT).show() } } } }
2. 修改CaptureService:在服务内部获取MediaProjection
确保服务先进入前台状态,再调用getMediaProjection():
package com.example.screenshot import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.app.Service import android.content.Intent import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION import android.graphics.Color import android.os.IBinder import android.util.Log import androidx.core.app.ServiceCompat class CaptureService : Service() { companion object { private val TAG = CaptureService::class.qualifiedName const val ACTION_ENABLE_CAPTURE = "enable_capture" } private lateinit var capture: Capture private var mediaProjectionCallback = object : MediaProjectionCallback() { override fun onStop() { super.onStop() capture.stop() stopSelf() } } override fun onCreate() { super.onCreate() capture = Capture(this) } override fun onBind(intent: Intent?): IBinder? { return null } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { intent?.let { when (it.action) { ACTION_ENABLE_CAPTURE -> { val resultCode = it.getIntExtra("RESULT_CODE", -1) val data = it.getParcelableExtra<Intent>("DATA") if (resultCode != -1 && data != null) { startForegroundService() initMediaProjection(resultCode, data) } } } } return Service.START_STICKY } private fun startForegroundService() { val channelId = "media_projection_channel" val channelName = "截图服务" val channel = NotificationChannel( channelId, channelName, NotificationManager.IMPORTANCE_LOW ).apply { lightColor = Color.BLUE lockscreenVisibility = Notification.VISIBILITY_PRIVATE } val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager manager.createNotificationChannel(channel) val notification = Notification.Builder(this, channelId) .setOngoing(true) .setSmallIcon(R.mipmap.ic_launcher) .setContentTitle("截图服务运行中") .setContentText("点击停止服务") .setCategory(Notification.CATEGORY_SERVICE) .build() ServiceCompat.startForeground( this, 1001, notification, FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION ) } private fun initMediaProjection(resultCode: Int, data: Intent) { val mediaProjectionManager = getSystemService(MediaProjectionManager::class.java) val mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, data) mediaProjection.registerCallback(mediaProjectionCallback, null) capture.run(mediaProjection) { bitmap -> Log.d(TAG, "截图成功,尺寸:${bitmap.width}x${bitmap.height}") // 在这里处理截图(如保存到本地) } } override fun onDestroy() { super.onDestroy() capture.stop() } private inner class MediaProjectionCallback : android.media.projection.MediaProjection.Callback() { override fun onStop() { capture.stop() stopSelf() } } }
3. 修复Capture类的截图逻辑
修正Bitmap裁剪错误,避免截图出现拉伸或空白:
package com.example.screenshot import android.content.Context import android.graphics.Bitmap import android.graphics.PixelFormat import android.hardware.display.DisplayManager import android.hardware.display.VirtualDisplay import android.media.Image import android.media.ImageReader import android.media.projection.MediaProjection import android.util.Log import android.view.WindowManager class Capture(val context: Context) : ImageReader.OnImageAvailableListener { companion object { val TAG = Capture::class.qualifiedName } private var display: VirtualDisplay? = null private var onCaptureListener: ((Bitmap) -> Unit)? = null fun run(mediaProjection: MediaProjection, onCaptureListener: (Bitmap) -> Unit) { this.onCaptureListener = onCaptureListener if (display == null) { display = createDisplay(mediaProjection) } } private fun createDisplay(mediaProjection: MediaProjection): VirtualDisplay { val windowManager = context.getSystemService(WindowManager::class.java) val displayMetrics = context.resources.displayMetrics val width = displayMetrics.widthPixels val height = displayMetrics.heightPixels val density = displayMetrics.densityDpi val maxImages = 2 val reader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, maxImages) reader.setOnImageAvailableListener(this, null) return mediaProjection.createVirtualDisplay( "Capture Display", width, height, density, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, reader.surface, null, null ) } override fun onImageAvailable(reader: ImageReader) { val image = reader.acquireLatestImage() ?: return try { val bitmap = imageToBitmap(image) onCaptureListener?.invoke(bitmap) } catch (e: Exception) { Log.e(TAG, "截图失败:${e.message}", e) } finally { image.close() } } private fun imageToBitmap(image: Image): Bitmap { val planes = image.planes val buffer = planes[0].buffer val pixelStride = planes[0].pixelStride val rowStride = planes[0].rowStride val rowPadding = rowStride - pixelStride * image.width val bitmap = Bitmap.createBitmap(
相关产品推荐
相关产品推荐

