1.第一个碰到的问题是
Caused by: android.view.WindowManager$BadTokenException: Unable to add window android.view.ViewRootImpl$W@73e5526 -- permission denied for window type 2003
java.lang.RuntimeException: Unable to create service com.example.liuw.camera2demo.service.ScreenRecordService: android.view.WindowManager$BadTokenException: Unable to add window android.view.ViewRootImpl$W@73e5526 -- permission denied for window type 2003
经过不断排查发现是权限问题
项目添加 <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<service
android:foregroundServiceType="mediaProjection"
android:name=".service.ScreenRecordService"
android:enabled="true"
android:exported="true" />
这里在说明悬浮窗添加于应用上层时需要申请权限在android6以上
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (!Settings.canDrawOverlays(ScreenRecordActivity.this)) {
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
startActivity(intent);
Toast.makeText(this, "勾选允许后,再次点击", Toast.LENGTH_SHORT)
.show();
return;
}
}
然后ScreenRecordActivity添加
@Override
protected void onActivityResult(int requestCode, final int resultCode, final Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (REQUEST_CODE == requestCode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (!Settings.canDrawOverlays(ScreenRecordActivity.this)) {
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
startActivity(intent);
Toast.makeText(this, "勾选允许后,再次点击", Toast.LENGTH_SHORT)
.show();
return;
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
Intent service = new Intent(this, ScreenRecordService.class);
service.putExtra("resultCode", resultCode);
service.putExtra("data", data);
service.putExtra("mWidthPixels", mWidthPixels);
service.putExtra("mHeightPixels", mHeightPixels);
service.putExtra("mDensityDpi", mDensityDpi);
startForegroundService(service);
Log.d(TAG,"VERSION_CODES.Q---onActivityResult");
}else{
Intent service = new Intent(this, ScreenRecordService.class);
service.putExtra("resultCode", resultCode);
service.putExtra("data", data);
service.putExtra("mWidthPixels", mWidthPixels);
service.putExtra("mHeightPixels", mHeightPixels);
service.putExtra("mDensityDpi", mDensityDpi);
startService(service);
}
}
}
接着运行出现一个新的异常
java.lang.RuntimeException: Unable to start service com.example.liuw.camera2demo.service.ScreenRecordService@73e5526 with Intent { cmp=com.example.liuw.camera2demo/.service.ScreenRecordService (has extras) }: java.lang.SecurityException: Media projections require a foreground service of type ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
Caused by: java.lang.SecurityException: Media projections require a foreground service of type ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
继续修改代码针对android10进行兼容
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
Log.d(TAG, "Build.VERSION_CODES.Q onStartCommand created: " );
createNotificationChannel();//android10必须有个自定义的通知
int resultCode = intent.getIntExtra("resultCode", -1);
Intent data = intent.getParcelableExtra("data");
mWidthPixels = intent.getIntExtra("mWidthPixels", 720);
mHeightPixels = intent.getIntExtra("mHeightPixels", 1080);
mScreenDensity = intent.getIntExtra("mDensityDpi", 1);
Log.d(TAG,"mResultCode: "+resultCode+"\tmResultData: "+data);
Log.d(TAG, "initData --> mScreenDensity: " + mScreenDensity + " mWidthPixels: " + mWidthPixels + " mHeightPixels: " + mHeightPixels);
//mResultData = intent.getSelector();
MediaProjectionManager mMediaProjectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
mMediaProjection = mMediaProjectionManager.getMediaProjection(resultCode, Objects.requireNonNull(data));
Log.e(TAG, "mMediaProjection created: " + mMediaProjection);
}else{
int resultCode = intent.getIntExtra("resultCode", -1);
Intent data = intent.getParcelableExtra("data");
Log.d(TAG,"mResultCode: "+resultCode+"\tmResultData: "+data);
mWidthPixels = intent.getIntExtra("mWidthPixels", 720);
mHeightPixels = intent.getIntExtra("mHeightPixels", 1080);
mScreenDensity = intent.getIntExtra("mDensityDpi", 1);
Log.d(TAG, "initData --> mScreenDensity: " + mScreenDensity + " mWidthPixels: " + mWidthPixels + " mHeightPixels: " + mHeightPixels);
mMediaProjection = ((MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE))
.getMediaProjection(resultCode, data);
}
return super.onStartCommand(intent, flags, startId);
}
private void createNotificationChannel() {
Log.d(TAG, "-----------createNotificationChannel----");
Notification.Builder builder = new Notification.Builder(this.getApplicationContext()); //获取一个Notification构造器
Intent nfIntent = new Intent(this, ScreenRecordService.class); //点击后跳转的界面,可以设置跳转数据
builder.setContentIntent(PendingIntent.getActivity(this, 0, nfIntent, 0)) // 设置PendingIntent
.setLargeIcon(BitmapFactory.decodeResource(this.getResources(), R.mipmap.ic_launcher)) // 设置下拉列表中的图标(大图标)
//.setContentTitle("SMI InstantView") // 设置下拉列表里的标题
.setSmallIcon(R.mipmap.ic_launcher) // 设置状态栏内的小图标
.setContentTitle("Starting Service")
.setContentText("is running......") // 设置上下文内容
.setWhen(System.currentTimeMillis()); // 设置该通知发生的时间
/*以下是对Android 8.0的适配*/
//普通notification适配
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder.setChannelId("notification_id");
}
//前台服务notification适配
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationManager notificationManager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
NotificationChannel channel = new NotificationChannel("notification_id", "notification_name", NotificationManager.IMPORTANCE_LOW);
notificationManager.createNotificationChannel(channel);
}
Notification notification = builder.build(); // 获取构建好的Notification
notification.defaults = Notification.DEFAULT_SOUND; //设置为默认的声音
startForeground(110, notification);//种类的id必须大于1 由于调用了startForegroundService 5S内必须调用startForeground
}
最后出现一个纠结很久的问题
java.lang.RuntimeException: Unable to start service com.example.liuw.camera2demo.service.ScreenRecordService@809dd7f with null: java.lang.NullPointerException: Attempt to invoke virtual method 'int android.content.Intent.getIntExtra(java.lang.String, int)' on a null object reference
Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'int android.content.Intent.getIntExtra(java.lang.String, int)' on a null object reference
开始一直以为是intent 对象为空 排查得知这个异常
2023-10-13 16:42:47.532 3150-3150/com.example.liuw.camera2demo E/ScreenRecordService: /storage/emulated/0/Movies/Pax_2023-10-13-16-42-47.mp4: open failed: EACCES (Permission denied)
还是android10权限问题
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNA" />
在application 中添加如下代码 运行ok
android:requestLegacyExternalStorage=“true”
总结:排查问题须一步步来
完整代码
public class ScreenRecordActivity extends AppCompatActivity {
private final int REQUEST_CODE = 0x001;
private int mDensityDpi, mWidthPixels, mHeightPixels;
private static final String TAG = "ScreenRecordActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// setContentView(R.layout.activity_screen_record);
if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED
|| ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.RECORD_AUDIO, Manifest.permission.WRITE_EXTERNAL_STORAGE}, 10);
}
initData();
Button button = new Button(this);
setContentView(button);
button.setText("启动录屏服务");
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
MediaProjectionManager manager = (MediaProjectionManager)
getSystemService(Context.MEDIA_PROJECTION_SERVICE);
Intent intent = new Intent(manager.createScreenCaptureIntent());
startActivityForResult(intent, REQUEST_CODE);
}
});
}
private void initData() {
DisplayMetrics dm = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(dm);
mDensityDpi = dm.densityDpi; // 屏幕密度(每寸像素:120/160/240/320)
mWidthPixels = dm.widthPixels;
mHeightPixels = dm.heightPixels;
}
@Override
protected void onActivityResult(int requestCode, final int resultCode, final Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (REQUEST_CODE == requestCode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (!Settings.canDrawOverlays(ScreenRecordActivity.this)) {
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
startActivity(intent);
Toast.makeText(this, "勾选允许后,再次点击", Toast.LENGTH_SHORT)
.show();
return;
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
Intent service = new Intent(this, ScreenRecordService.class);
service.putExtra("resultCode", resultCode);
service.putExtra("data", data);
service.putExtra("mWidthPixels", mWidthPixels);
service.putExtra("mHeightPixels", mHeightPixels);
service.putExtra("mDensityDpi", mDensityDpi);
startForegroundService(service);
Log.d(TAG,"VERSION_CODES.Q---onActivityResult");
}else{
Intent service = new Intent(this, ScreenRecordService.class);
service.putExtra("resultCode", resultCode);
service.putExtra("data", data);
service.putExtra("mWidthPixels", mWidthPixels);
service.putExtra("mHeightPixels", mHeightPixels);
service.putExtra("mDensityDpi", mDensityDpi);
startService(service);
}
}
}
}
ScreenRecordService
public class ScreenRecordService extends Service {
private static final String TAG = "ScreenRecordService";
private WindowManager mWindowManager;
private WindowManager.LayoutParams wmParams;
private LinearLayout mFloatLayout;
private MediaProjection mMediaProjection;
private MediaRecorder mMediaRecorder;
private VirtualDisplay mVirtualDisplay;
private boolean isScreenRecord;
private int mScreenDensity, mWidthPixels, mHeightPixels;
public ScreenRecordService() {
}
@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("Not yet implemented");
}
@Override
public void onCreate() {
super.onCreate();
initView();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
Log.d(TAG, "Build.VERSION_CODES.Q onStartCommand created: " );
createNotificationChannel();
int resultCode = intent.getIntExtra("resultCode", -1);
Intent data = intent.getParcelableExtra("data");
mWidthPixels = intent.getIntExtra("mWidthPixels", 720);
mHeightPixels = intent.getIntExtra("mHeightPixels", 1080);
mScreenDensity = intent.getIntExtra("mDensityDpi", 1);
Log.d(TAG,"mResultCode: "+resultCode+"\tmResultData: "+data);
Log.d(TAG, "initData --> mScreenDensity: " + mScreenDensity + " mWidthPixels: " + mWidthPixels + " mHeightPixels: " + mHeightPixels);
//mResultData = intent.getSelector();
MediaProjectionManager mMediaProjectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
mMediaProjection = mMediaProjectionManager.getMediaProjection(resultCode, Objects.requireNonNull(data));
Log.e(TAG, "mMediaProjection created: " + mMediaProjection);
}else{
int resultCode = intent.getIntExtra("resultCode", -1);
Intent data = intent.getParcelableExtra("data");
Log.d(TAG,"mResultCode: "+resultCode+"\tmResultData: "+data);
mWidthPixels = intent.getIntExtra("mWidthPixels", 720);
mHeightPixels = intent.getIntExtra("mHeightPixels", 1080);
mScreenDensity = intent.getIntExtra("mDensityDpi", 1);
Log.d(TAG, "initData --> mScreenDensity: " + mScreenDensity + " mWidthPixels: " + mWidthPixels + " mHeightPixels: " + mHeightPixels);
mMediaProjection = ((MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE))
.getMediaProjection(resultCode, data);
}
return super.onStartCommand(intent, flags, startId);
}
private void createNotificationChannel() {
Log.d(TAG, "-----------createNotificationChannel----");
Notification.Builder builder = new Notification.Builder(this.getApplicationContext()); //获取一个Notification构造器
Intent nfIntent = new Intent(this, ScreenRecordService.class); //点击后跳转的界面,可以设置跳转数据
builder.setContentIntent(PendingIntent.getActivity(this, 0, nfIntent, 0)) // 设置PendingIntent
.setLargeIcon(BitmapFactory.decodeResource(this.getResources(), R.mipmap.ic_launcher)) // 设置下拉列表中的图标(大图标)
//.setContentTitle("SMI InstantView") // 设置下拉列表里的标题
.setSmallIcon(R.mipmap.ic_launcher) // 设置状态栏内的小图标
.setContentTitle("Starting Service")
.setContentText("is running......") // 设置上下文内容
.setWhen(System.currentTimeMillis()); // 设置该通知发生的时间
/*以下是对Android 8.0的适配*/
//普通notification适配
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder.setChannelId("notification_id");
}
//前台服务notification适配
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationManager notificationManager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
NotificationChannel channel = new NotificationChannel("notification_id", "notification_name", NotificationManager.IMPORTANCE_LOW);
notificationManager.createNotificationChannel(channel);
}
Notification notification = builder.build(); // 获取构建好的Notification
notification.defaults = Notification.DEFAULT_SOUND; //设置为默认的声音
startForeground(110, notification);
}
private void startRecorder() {
Log.d(TAG, "startRecord");
isScreenRecord = true;
createMediaRecorder();
createVirtualDisplay(); // 在mediaRecorder.prepare() 之后调用,否则报错 failed to get surface
mMediaRecorder.start();
}
private void createVirtualDisplay() {
Log.d(TAG, "----------createVirtualDisplay-------------: " );
mVirtualDisplay = mMediaProjection.createVirtualDisplay(TAG, mWidthPixels, mHeightPixels, mScreenDensity,
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, mMediaRecorder.getSurface(), null, null);
}
private void createMediaRecorder() {
Log.d(TAG, "----------createMediaRecorder-------------: " );
// 生成文件名
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");
String curTime = formatter.format(new Date(System.currentTimeMillis()));
mMediaRecorder = new MediaRecorder();
//设置视频源: DEFAULT,Surface,Camera
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
//设置视频输出格式: amr_nb,amr_wb,default,mpeg_4,raw_amr,three_gpp
mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
//设置视频编码格式: default, H263, H264, MPEG_4_SP
mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
// //设置音频源
// mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.DEFAULT);
//
// //设置音频编码格式: default,AAC,AMR_NB,AMR_WB
// mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
//设置视频尺寸大小
mMediaRecorder.setVideoSize(mWidthPixels, mHeightPixels);
//设置视频编码的码率
mMediaRecorder.setVideoEncodingBitRate(5 * mWidthPixels * mHeightPixels); // mWidthPixels * mHeightPixels
//设置视频编码的帧率
mMediaRecorder.setVideoFrameRate(60); // 30
//设置视频输出路径
mMediaRecorder.setOutputFile(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES) + "/Pax_" + curTime + ".mp4");
try {
mMediaRecorder.prepare();
} catch (IOException e) {
e.printStackTrace();
Log.e(TAG, e.getMessage());
}
Log.d(TAG, "VideoSize: " + mWidthPixels + " X " + mHeightPixels + " VideoEncodingBitRate: "
+ (5 * mWidthPixels * mHeightPixels) + " +VideoFrameRate: " + "60");
}
private void initView() {
Log.d(TAG, "----------initView-------------: " );
mWindowManager = (WindowManager) getApplication().getSystemService(getApplication().WINDOW_SERVICE);
wmParams = new WindowManager.LayoutParams();
//修改的
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
wmParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;// 设置window type为TYPE_SYSTEM_ALERT
} else {
wmParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
}
wmParams.format = PixelFormat.RGBA_8888;// 设置图片格式,效果为背景透明
wmParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;// 设置浮动窗口不可聚焦(实现操作除浮动窗口外的其他可见窗口的操作)
wmParams.gravity = Gravity.LEFT | Gravity.TOP;// 默认位置:右下角
wmParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
wmParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
wmParams.x = (ScreenUtils.getScreenWidth(getApplicationContext()) - wmParams.width) / 2;// 设置x、y初始值,相对于gravity
wmParams.y = 10;
// 浮动窗口布局
mFloatLayout = (LinearLayout) LayoutInflater.from(getApplication()).inflate(R.layout.activity_screen_record, null);
mWindowManager.addView(mFloatLayout, wmParams);
final LinearLayout screenrecord = (LinearLayout) mFloatLayout.findViewById(R.id.screenrecord);
final TextView title = (TextView) mFloatLayout.findViewById(R.id.screenrecord_title);
final TextView close = (TextView) mFloatLayout.findViewById(R.id.screenrecord_close);
final ImageView image = (ImageView) mFloatLayout.findViewById(R.id.play_stop);
mFloatLayout.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
// 设置监听浮动窗口的触摸移动
View.OnTouchListener mOnTouchListener = new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
// getRawX是触摸位置相对于屏幕的坐标,getX是相对于按钮的坐标
wmParams.x = (int) event.getRawX() - mFloatLayout.getMeasuredWidth() / 2;
Log.i(TAG, "RawX" + event.getRawX() + " X" + event.getX());
wmParams.y = (int) event.getRawY() - mFloatLayout.getMeasuredHeight() / 2 - 25;// 减25为状态栏的高度
Log.i(TAG, "RawY" + event.getRawY() + " Y" + event.getY());
mWindowManager.updateViewLayout(mFloatLayout, wmParams);// 刷新
return false;
}
};
mFloatLayout.setOnTouchListener(mOnTouchListener);
screenrecord.setOnTouchListener(mOnTouchListener);
screenrecord.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (isScreenRecord) {
image.setImageResource(R.mipmap.screen_record_play);
title.setText("开始");
stopRecord();
} else {
image.setImageResource(R.mipmap.screen_record_stop);
title.setText("停止");
startRecorder();
}
}
});
close.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Log.d(TAG, "stopSelf");
ScreenRecordService.this.stopSelf();
}
});
}
private void stopRecord() {
Log.d(TAG, "stopRecord");
isScreenRecord = false;
mMediaRecorder.stop();
mMediaRecorder.reset();
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d(TAG, "onDestroy");
if (isScreenRecord) {
stopRecord();
}
mVirtualDisplay.release();
mMediaProjection.stop();
stopForeground(true);
mWindowManager.removeView(mFloatLayout);
}
}