Merge "[Partial screen sharing] Handle system-requested media projection stop in SysUI screen recorder" into tm-qpr-dev

This commit is contained in:
Nick Chameyev
2022-07-20 15:46:22 +00:00
committed by Android (Google) Code Review
3 changed files with 218 additions and 30 deletions

View File

@@ -29,6 +29,7 @@ import android.graphics.drawable.Icon;
import android.media.MediaRecorder;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.UserHandle;
@@ -40,6 +41,8 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.R;
import com.android.systemui.dagger.qualifiers.LongRunning;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.screenrecord.ScreenMediaRecorder.ScreenMediaRecorderListener;
import com.android.systemui.settings.UserContextProvider;
import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
@@ -51,9 +54,10 @@ import javax.inject.Inject;
/**
* A service which records the device screen and optionally microphone input.
*/
public class RecordingService extends Service implements MediaRecorder.OnInfoListener {
public class RecordingService extends Service implements ScreenMediaRecorderListener {
public static final int REQUEST_CODE = 2;
private static final int USER_ID_NOT_SPECIFIED = -1;
private static final int NOTIFICATION_RECORDING_ID = 4274;
private static final int NOTIFICATION_PROCESSING_ID = 4275;
private static final int NOTIFICATION_VIEW_ID = 4273;
@@ -73,6 +77,7 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
private final RecordingController mController;
private final KeyguardDismissUtil mKeyguardDismissUtil;
private final Handler mMainHandler;
private ScreenRecordingAudioSource mAudioSource;
private boolean mShowTaps;
private boolean mOriginalShowTaps;
@@ -84,10 +89,12 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
@Inject
public RecordingService(RecordingController controller, @LongRunning Executor executor,
UiEventLogger uiEventLogger, NotificationManager notificationManager,
@Main Handler handler, UiEventLogger uiEventLogger,
NotificationManager notificationManager,
UserContextProvider userContextTracker, KeyguardDismissUtil keyguardDismissUtil) {
mController = controller;
mLongExecutor = executor;
mMainHandler = handler;
mUiEventLogger = uiEventLogger;
mNotificationManager = notificationManager;
mUserContextTracker = userContextTracker;
@@ -138,6 +145,7 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
mRecorder = new ScreenMediaRecorder(
mUserContextTracker.getUserContext(),
mMainHandler,
currentUserId,
mAudioSource,
this
@@ -166,14 +174,8 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
}
// Check user ID - we may be getting a stop intent after user switch, in which case
// we want to post the notifications for that user, which is NOT current user
int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
if (userId == -1) {
userId = mUserContextTracker.getUserContext().getUserId();
}
Log.d(TAG, "notifying for user " + userId);
stopRecording(userId);
mNotificationManager.cancel(NOTIFICATION_RECORDING_ID);
stopSelf();
int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, USER_ID_NOT_SPECIFIED);
stopService(userId);
break;
case ACTION_SHARE:
@@ -378,15 +380,39 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
return builder.build();
}
private void stopRecording(int userId) {
private void stopService() {
stopService(USER_ID_NOT_SPECIFIED);
}
private void stopService(int userId) {
if (userId == USER_ID_NOT_SPECIFIED) {
userId = mUserContextTracker.getUserContext().getUserId();
}
Log.d(TAG, "notifying for user " + userId);
setTapsVisible(mOriginalShowTaps);
if (getRecorder() != null) {
getRecorder().end();
saveRecording(userId);
try {
getRecorder().end();
saveRecording(userId);
} catch (RuntimeException exception) {
// RuntimeException could happen if the recording stopped immediately after starting
// let's release the recorder and delete all temporary files in this case
getRecorder().release();
showErrorToast(R.string.screenrecord_start_error);
Log.e(TAG, "stopRecording called, but there was an error when ending"
+ "recording");
exception.printStackTrace();
} catch (Throwable throwable) {
// Something unexpected happen, SystemUI will crash but let's delete
// the temporary files anyway
getRecorder().release();
throw new RuntimeException(throwable);
}
} else {
Log.e(TAG, "stopRecording called, but recorder was null");
}
updateState(false);
stopSelf();
}
private void saveRecording(int userId) {
@@ -446,4 +472,12 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
Log.d(TAG, "Media recorder info: " + what);
onStartCommand(getStopIntent(this), 0, 0);
}
@Override
public void onStopped() {
if (mController.isRecording()) {
Log.d(TAG, "Stopping recording because the system requested the stop");
stopService();
}
}
}

View File

@@ -40,6 +40,7 @@ import android.media.projection.IMediaProjectionManager;
import android.media.projection.MediaProjection;
import android.media.projection.MediaProjectionManager;
import android.net.Uri;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceManager;
@@ -51,16 +52,19 @@ import android.view.Surface;
import android.view.WindowManager;
import java.io.File;
import java.io.Closeable;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* Recording screen and mic/internal audio
*/
public class ScreenMediaRecorder {
public class ScreenMediaRecorder extends MediaProjection.Callback {
private static final int TOTAL_NUM_TRACKS = 1;
private static final int VIDEO_FRAME_RATE = 30;
private static final int VIDEO_FRAME_RATE_TO_RESOLUTION_RATIO = 6;
@@ -81,14 +85,16 @@ public class ScreenMediaRecorder {
private ScreenRecordingMuxer mMuxer;
private ScreenInternalAudioRecorder mAudio;
private ScreenRecordingAudioSource mAudioSource;
private final Handler mHandler;
private Context mContext;
MediaRecorder.OnInfoListener mListener;
ScreenMediaRecorderListener mListener;
public ScreenMediaRecorder(Context context,
public ScreenMediaRecorder(Context context, Handler handler,
int user, ScreenRecordingAudioSource audioSource,
MediaRecorder.OnInfoListener listener) {
ScreenMediaRecorderListener listener) {
mContext = context;
mHandler = handler;
mUser = user;
mListener = listener;
mAudioSource = audioSource;
@@ -105,6 +111,7 @@ public class ScreenMediaRecorder {
IBinder projection = proj.asBinder();
mMediaProjection = new MediaProjection(mContext,
IMediaProjection.Stub.asInterface(projection));
mMediaProjection.registerCallback(this, mHandler);
File cacheDir = mContext.getCacheDir();
cacheDir.mkdirs();
@@ -162,10 +169,15 @@ public class ScreenMediaRecorder {
metrics.densityDpi,
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
mInputSurface,
null,
null);
new VirtualDisplay.Callback() {
@Override
public void onStopped() {
onStop();
}
},
mHandler);
mMediaRecorder.setOnInfoListener(mListener);
mMediaRecorder.setOnInfoListener((mr, what, extra) -> mListener.onInfo(mr, what, extra));
if (mAudioSource == INTERNAL ||
mAudioSource == MIC_AND_INTERNAL) {
mTempAudioFile = File.createTempFile("temp", ".aac",
@@ -259,21 +271,34 @@ public class ScreenMediaRecorder {
}
/**
* End screen recording
* End screen recording, throws an exception if stopping recording failed
*/
void end() {
mMediaRecorder.stop();
mMediaRecorder.release();
mInputSurface.release();
mVirtualDisplay.release();
mMediaProjection.stop();
void end() throws IOException {
Closer closer = new Closer();
// MediaRecorder might throw RuntimeException if stopped immediately after starting
// We should remove the recording in this case as it will be invalid
closer.register(mMediaRecorder::stop);
closer.register(mMediaRecorder::release);
closer.register(mInputSurface::release);
closer.register(mVirtualDisplay::release);
closer.register(mMediaProjection::stop);
closer.register(this::stopInternalAudioRecording);
closer.close();
mMediaRecorder = null;
mMediaProjection = null;
stopInternalAudioRecording();
Log.d(TAG, "end recording");
}
@Override
public void onStop() {
Log.d(TAG, "The system notified about stopping the projection");
mListener.onStopped();
}
private void stopInternalAudioRecording() {
if (mAudioSource == INTERNAL || mAudioSource == MIC_AND_INTERNAL) {
mAudio.end();
@@ -336,6 +361,18 @@ public class ScreenMediaRecorder {
return recording;
}
/**
* Release the resources without saving the data
*/
protected void release() {
if (mTempVideoFile != null) {
mTempVideoFile.delete();
}
if (mTempAudioFile != null) {
mTempAudioFile.delete();
}
}
/**
* Object representing the recording
*/
@@ -362,4 +399,66 @@ public class ScreenMediaRecorder {
return mThumbnailBitmap;
}
}
interface ScreenMediaRecorderListener {
/**
* Called to indicate an info or a warning during recording.
* See {@link MediaRecorder.OnInfoListener} for the full description.
*/
void onInfo(MediaRecorder mr, int what, int extra);
/**
* Called when the recording stopped by the system.
* For example, this might happen when doing partial screen sharing of an app
* and the app that is being captured is closed.
*/
void onStopped();
}
/**
* Allows to register multiple {@link Closeable} objects and close them all by calling
* {@link Closer#close}. If there is an exception thrown during closing of one
* of the registered closeables it will continue trying closing the rest closeables.
* If there are one or more exceptions thrown they will be re-thrown at the end.
* In case of multiple exceptions only the first one will be thrown and all the rest
* will be printed.
*/
private static class Closer implements Closeable {
private final List<Closeable> mCloseables = new ArrayList<>();
void register(Closeable closeable) {
mCloseables.add(closeable);
}
@Override
public void close() throws IOException {
Throwable throwable = null;
for (int i = 0; i < mCloseables.size(); i++) {
Closeable closeable = mCloseables.get(i);
try {
closeable.close();
} catch (Throwable e) {
if (throwable == null) {
throwable = e;
} else {
e.printStackTrace();
}
}
}
if (throwable != null) {
if (throwable instanceof IOException) {
throw (IOException) throwable;
}
if (throwable instanceof RuntimeException) {
throw (RuntimeException) throwable;
}
throw (Error) throwable;
}
}
}
}

View File

@@ -16,18 +16,21 @@
package com.android.systemui.screenrecord;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import android.app.Notification;
import android.app.NotificationManager;
import android.content.Intent;
import android.os.Handler;
import android.os.RemoteException;
import android.testing.AndroidTestingRunner;
@@ -66,6 +69,8 @@ public class RecordingServiceTest extends SysuiTestCase {
@Mock
private Executor mExecutor;
@Mock
private Handler mHandler;
@Mock
private UserContextProvider mUserContextTracker;
private KeyguardDismissUtil mKeyguardDismissUtil = new KeyguardDismissUtil() {
public void executeWhenUnlocked(ActivityStarter.OnDismissAction action,
@@ -79,8 +84,8 @@ public class RecordingServiceTest extends SysuiTestCase {
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mRecordingService = Mockito.spy(new RecordingService(mController, mExecutor, mUiEventLogger,
mNotificationManager, mUserContextTracker, mKeyguardDismissUtil));
mRecordingService = Mockito.spy(new RecordingService(mController, mExecutor, mHandler,
mUiEventLogger, mNotificationManager, mUserContextTracker, mKeyguardDismissUtil));
// Return actual context info
doReturn(mContext).when(mRecordingService).getApplicationContext();
@@ -143,4 +148,54 @@ public class RecordingServiceTest extends SysuiTestCase {
// Then the state is set to not recording
verify(mController).updateState(false);
}
@Test
public void testOnSystemRequestedStop_recordingInProgress_endsRecording() throws IOException {
doReturn(true).when(mController).isRecording();
mRecordingService.onStopped();
verify(mScreenMediaRecorder).end();
}
@Test
public void testOnSystemRequestedStop_recordingInProgress_updatesState() {
doReturn(true).when(mController).isRecording();
mRecordingService.onStopped();
verify(mController).updateState(false);
}
@Test
public void testOnSystemRequestedStop_recordingIsNotInProgress_doesNotEndRecording()
throws IOException {
doReturn(false).when(mController).isRecording();
mRecordingService.onStopped();
verify(mScreenMediaRecorder, never()).end();
}
@Test
public void testOnSystemRequestedStop_recorderEndThrowsRuntimeException_releasesRecording()
throws IOException {
doReturn(true).when(mController).isRecording();
doThrow(new RuntimeException()).when(mScreenMediaRecorder).end();
mRecordingService.onStopped();
verify(mScreenMediaRecorder).release();
}
@Test
public void testOnSystemRequestedStop_recorderEndThrowsOOMError_releasesRecording()
throws IOException {
doReturn(true).when(mController).isRecording();
doThrow(new OutOfMemoryError()).when(mScreenMediaRecorder).end();
assertThrows(Throwable.class, () -> mRecordingService.onStopped());
verify(mScreenMediaRecorder).release();
}
}