一、问题描述
基于 Android 14平台,下拉快速设置中打开屏幕录制,在 3 秒倒计时完成后,屏幕录制 Tile 变成 STATE_ACTIVE 状态,同时投屏 Tile 也变成STATE_ACTIVE 状态,会造成功能混乱的感觉,需要修改功能单独控制。
https://issuetracker.google.com/issues/328539170
二、问题分析
这个问题的核心在于,系统无法区分屏幕录制发起的 MediaProjection
(屏幕内容捕获请求)和真正的投屏发起的 MediaProjection。当屏幕录制开始时,CastController
(投屏控制器) 错误地认为一个投屏会话已经开始,因此点亮了投屏磁贴。投屏发起者一般是其他应用,包名不是 com.android.systemui
。
ScreenRecordTile
-> RecordingController
-> ScreenRecordDialog
开始录制点击事件
src/com/android/systemui/screenrecord/ScreenRecordDialog.java
TextView startBtn = findViewById(R.id.button_start);
startBtn.setOnClickListener(v -> {
if (mOnStartRecordingClicked != null) {
// Note that it is important to run this callback before dismissing, so that the
// callback can disable the dialog exit animation if it wants to.
mOnStartRecordingClicked.run();
}
// Start full-screen recording
requestScreenCapture(/* captureTarget= */ null);
dismiss();
});
开启屏幕录制请求
/**
* Starts screen capture after some countdown
* @param captureTarget target to capture (could be e.g. a task) or
* null to record the whole screen
*/
private void requestScreenCapture(@Nullable MediaProjectionCaptureTarget captureTarget) {
Context userContext = mUserContextProvider.getUserContext();
boolean showTaps = mTapsSwitch.isChecked();
ScreenRecordingAudioSource audioMode = mAudioSwitch.isChecked()
? (ScreenRecordingAudioSource) mOptions.getSelectedItem()
: NONE;
PendingIntent startIntent = PendingIntent.getForegroundService(userContext,
RecordingService.REQUEST_CODE,
RecordingService.getStartIntent(
userContext, Activity.RESULT_OK,
audioMode.ordinal(), showTaps, captureTarget),
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
PendingIntent stopIntent = PendingIntent.getService(userContext,
RecordingService.REQUEST_CODE,
RecordingService.getStopIntent(userContext),
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
mController.startCountdown(DELAY_MS, INTERVAL_MS, startIntent, stopIntent);
}
src/com/android/systemui/screenrecord/RecordingService.java
/**
* Get an intent to start the recording service.
*
* @param context Context from the requesting activity
* @param resultCode The result code from {@link android.app.Activity#onActivityResult(int, int,
* android.content.Intent)}
* @param audioSource The ordinal value of the audio source
* {@link com.android.systemui.screenrecord.ScreenRecordingAudioSource}
* @param showTaps True to make touches visible while recording
* @param captureTarget pass this parameter to capture a specific part instead
* of the full screen
*/
public static Intent getStartIntent(Context context, int resultCode,
int audioSource, boolean showTaps,
@Nullable MediaProjectionCaptureTarget captureTarget) {
return new Intent(context, RecordingService.class)
.setAction(ACTION_START)
.putExtra(EXTRA_RESULT_CODE, resultCode)
.putExtra(EXTRA_AUDIO_SOURCE, audioSource)
.putExtra(EXTRA_SHOW_TAPS, showTaps)
.putExtra(EXTRA_CAPTURE_TARGET, captureTarget);
}
倒计时 3 秒后开始录制执行 startIntent
try {
startIntent.send(mInteractiveBroadcastOption);
mUserTracker.addCallback(mUserChangedCallback, mMainExecutor);
IntentFilter stateFilter = new IntentFilter(INTENT_UPDATE_STATE);
mBroadcastDispatcher.registerReceiver(mStateChangeReceiver, stateFilter, null,
UserHandle.ALL);
Log.d(TAG, "sent start intent");
} catch (PendingIntent.CanceledException e) {
Log.e(TAG, "Pending intent was cancelled: " + e.getMessage());
}
在 RecordingService
执行具体的录制流程
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent == null) {
return Service.START_NOT_STICKY;
}
String action = intent.getAction();
Log.d(TAG, "onStartCommand " + action);
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID,
getString(R.string.screenrecord_title),
NotificationManager.IMPORTANCE_DEFAULT);
channel.setDescription(getString(R.string.screenrecord_channel_description));
channel.enableVibration(true);
mNotificationManager.createNotificationChannel(channel);
int currentUserId = mUserContextTracker.getUserContext().getUserId();
UserHandle currentUser = new UserHandle(currentUserId);
switch (action) {
case ACTION_START:
// Get a unique ID for this recording's notifications
mNotificationId = NOTIF_BASE_ID + (int) SystemClock.uptimeMillis();
mAudioSource = ScreenRecordingAudioSource
.values()[intent.getIntExtra(EXTRA_AUDIO_SOURCE, 0)];
Log.d(TAG, "recording with audio source " + mAudioSource);
mShowTaps = intent.getBooleanExtra(EXTRA_SHOW_TAPS, false);
MediaProjectionCaptureTarget captureTarget =
intent.getParcelableExtra(EXTRA_CAPTURE_TARGET,
MediaProjectionCaptureTarget.class);
mOriginalShowTaps = Settings.System.getInt(
getApplicationContext().getContentResolver(),
Settings.System.SHOW_TOUCHES, 0) != 0;
setTapsVisible(mShowTaps);
mRecorder = new ScreenMediaRecorder(
mUserContextTracker.getUserContext(),
mMainHandler,
currentUserId,
mAudioSource,
captureTarget,
this
);
if (startRecording()) {
updateState(true);
createRecordingNotification();
mUiEventLogger.log(Events.ScreenRecordEvent.SCREEN_RECORD_START);
} else {
updateState(false);
createErrorNotification();
stopForeground(STOP_FOREGROUND_DETACH);
stopSelf();
return Service.START_NOT_STICKY;
}
break;
...
}
三、解决方案
在投屏启动前根据包名进行拦截处理
src/com/android/systemui/statusbar/policy/CastControllerImpl.java
private final MediaProjectionManager.Callback mProjectionCallback
= new MediaProjectionManager.Callback() {
@Override
public void onStart(MediaProjectionInfo info) {
+ if (info != null && "com.android.systemui".equals(info.getPackageName())) {
+ if (DEBUG) Log.d(TAG, "Ignoring projection from screen recording (com.android.systemui)");
+ return;
+ }
setProjection(info, true);
}
@Override
public void onStop(MediaProjectionInfo info) {
setProjection(info, false);
}
};