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.media.MediaRecorder;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder; import android.os.IBinder;
import android.os.RemoteException; import android.os.RemoteException;
import android.os.UserHandle; import android.os.UserHandle;
@@ -40,6 +41,8 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.UiEventLogger;
import com.android.systemui.R; import com.android.systemui.R;
import com.android.systemui.dagger.qualifiers.LongRunning; 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.settings.UserContextProvider;
import com.android.systemui.statusbar.phone.KeyguardDismissUtil; 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. * 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; 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_RECORDING_ID = 4274;
private static final int NOTIFICATION_PROCESSING_ID = 4275; private static final int NOTIFICATION_PROCESSING_ID = 4275;
private static final int NOTIFICATION_VIEW_ID = 4273; 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 RecordingController mController;
private final KeyguardDismissUtil mKeyguardDismissUtil; private final KeyguardDismissUtil mKeyguardDismissUtil;
private final Handler mMainHandler;
private ScreenRecordingAudioSource mAudioSource; private ScreenRecordingAudioSource mAudioSource;
private boolean mShowTaps; private boolean mShowTaps;
private boolean mOriginalShowTaps; private boolean mOriginalShowTaps;
@@ -84,10 +89,12 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
@Inject @Inject
public RecordingService(RecordingController controller, @LongRunning Executor executor, public RecordingService(RecordingController controller, @LongRunning Executor executor,
UiEventLogger uiEventLogger, NotificationManager notificationManager, @Main Handler handler, UiEventLogger uiEventLogger,
NotificationManager notificationManager,
UserContextProvider userContextTracker, KeyguardDismissUtil keyguardDismissUtil) { UserContextProvider userContextTracker, KeyguardDismissUtil keyguardDismissUtil) {
mController = controller; mController = controller;
mLongExecutor = executor; mLongExecutor = executor;
mMainHandler = handler;
mUiEventLogger = uiEventLogger; mUiEventLogger = uiEventLogger;
mNotificationManager = notificationManager; mNotificationManager = notificationManager;
mUserContextTracker = userContextTracker; mUserContextTracker = userContextTracker;
@@ -138,6 +145,7 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
mRecorder = new ScreenMediaRecorder( mRecorder = new ScreenMediaRecorder(
mUserContextTracker.getUserContext(), mUserContextTracker.getUserContext(),
mMainHandler,
currentUserId, currentUserId,
mAudioSource, mAudioSource,
this 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 // 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 // we want to post the notifications for that user, which is NOT current user
int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1); int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, USER_ID_NOT_SPECIFIED);
if (userId == -1) { stopService(userId);
userId = mUserContextTracker.getUserContext().getUserId();
}
Log.d(TAG, "notifying for user " + userId);
stopRecording(userId);
mNotificationManager.cancel(NOTIFICATION_RECORDING_ID);
stopSelf();
break; break;
case ACTION_SHARE: case ACTION_SHARE:
@@ -378,15 +380,39 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
return builder.build(); 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); setTapsVisible(mOriginalShowTaps);
if (getRecorder() != null) { if (getRecorder() != null) {
try {
getRecorder().end(); getRecorder().end();
saveRecording(userId); 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 { } else {
Log.e(TAG, "stopRecording called, but recorder was null"); Log.e(TAG, "stopRecording called, but recorder was null");
} }
updateState(false); updateState(false);
stopSelf();
} }
private void saveRecording(int userId) { private void saveRecording(int userId) {
@@ -446,4 +472,12 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
Log.d(TAG, "Media recorder info: " + what); Log.d(TAG, "Media recorder info: " + what);
onStartCommand(getStopIntent(this), 0, 0); 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.MediaProjection;
import android.media.projection.MediaProjectionManager; import android.media.projection.MediaProjectionManager;
import android.net.Uri; import android.net.Uri;
import android.os.Handler;
import android.os.IBinder; import android.os.IBinder;
import android.os.RemoteException; import android.os.RemoteException;
import android.os.ServiceManager; import android.os.ServiceManager;
@@ -51,16 +52,19 @@ import android.view.Surface;
import android.view.WindowManager; import android.view.WindowManager;
import java.io.File; import java.io.File;
import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.nio.file.Files; import java.nio.file.Files;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.List;
/** /**
* Recording screen and mic/internal audio * 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 TOTAL_NUM_TRACKS = 1;
private static final int VIDEO_FRAME_RATE = 30; private static final int VIDEO_FRAME_RATE = 30;
private static final int VIDEO_FRAME_RATE_TO_RESOLUTION_RATIO = 6; private static final int VIDEO_FRAME_RATE_TO_RESOLUTION_RATIO = 6;
@@ -81,14 +85,16 @@ public class ScreenMediaRecorder {
private ScreenRecordingMuxer mMuxer; private ScreenRecordingMuxer mMuxer;
private ScreenInternalAudioRecorder mAudio; private ScreenInternalAudioRecorder mAudio;
private ScreenRecordingAudioSource mAudioSource; private ScreenRecordingAudioSource mAudioSource;
private final Handler mHandler;
private Context mContext; private Context mContext;
MediaRecorder.OnInfoListener mListener; ScreenMediaRecorderListener mListener;
public ScreenMediaRecorder(Context context, public ScreenMediaRecorder(Context context, Handler handler,
int user, ScreenRecordingAudioSource audioSource, int user, ScreenRecordingAudioSource audioSource,
MediaRecorder.OnInfoListener listener) { ScreenMediaRecorderListener listener) {
mContext = context; mContext = context;
mHandler = handler;
mUser = user; mUser = user;
mListener = listener; mListener = listener;
mAudioSource = audioSource; mAudioSource = audioSource;
@@ -105,6 +111,7 @@ public class ScreenMediaRecorder {
IBinder projection = proj.asBinder(); IBinder projection = proj.asBinder();
mMediaProjection = new MediaProjection(mContext, mMediaProjection = new MediaProjection(mContext,
IMediaProjection.Stub.asInterface(projection)); IMediaProjection.Stub.asInterface(projection));
mMediaProjection.registerCallback(this, mHandler);
File cacheDir = mContext.getCacheDir(); File cacheDir = mContext.getCacheDir();
cacheDir.mkdirs(); cacheDir.mkdirs();
@@ -162,10 +169,15 @@ public class ScreenMediaRecorder {
metrics.densityDpi, metrics.densityDpi,
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
mInputSurface, mInputSurface,
null, new VirtualDisplay.Callback() {
null); @Override
public void onStopped() {
onStop();
}
},
mHandler);
mMediaRecorder.setOnInfoListener(mListener); mMediaRecorder.setOnInfoListener((mr, what, extra) -> mListener.onInfo(mr, what, extra));
if (mAudioSource == INTERNAL || if (mAudioSource == INTERNAL ||
mAudioSource == MIC_AND_INTERNAL) { mAudioSource == MIC_AND_INTERNAL) {
mTempAudioFile = File.createTempFile("temp", ".aac", 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() { void end() throws IOException {
mMediaRecorder.stop(); Closer closer = new Closer();
mMediaRecorder.release();
mInputSurface.release(); // MediaRecorder might throw RuntimeException if stopped immediately after starting
mVirtualDisplay.release(); // We should remove the recording in this case as it will be invalid
mMediaProjection.stop(); 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; mMediaRecorder = null;
mMediaProjection = null; mMediaProjection = null;
stopInternalAudioRecording();
Log.d(TAG, "end recording"); Log.d(TAG, "end recording");
} }
@Override
public void onStop() {
Log.d(TAG, "The system notified about stopping the projection");
mListener.onStopped();
}
private void stopInternalAudioRecording() { private void stopInternalAudioRecording() {
if (mAudioSource == INTERNAL || mAudioSource == MIC_AND_INTERNAL) { if (mAudioSource == INTERNAL || mAudioSource == MIC_AND_INTERNAL) {
mAudio.end(); mAudio.end();
@@ -336,6 +361,18 @@ public class ScreenMediaRecorder {
return recording; 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 * Object representing the recording
*/ */
@@ -362,4 +399,66 @@ public class ScreenMediaRecorder {
return mThumbnailBitmap; 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; package com.android.systemui.screenrecord;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import android.app.Notification; import android.app.Notification;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.content.Intent; import android.content.Intent;
import android.os.Handler;
import android.os.RemoteException; import android.os.RemoteException;
import android.testing.AndroidTestingRunner; import android.testing.AndroidTestingRunner;
@@ -66,6 +69,8 @@ public class RecordingServiceTest extends SysuiTestCase {
@Mock @Mock
private Executor mExecutor; private Executor mExecutor;
@Mock @Mock
private Handler mHandler;
@Mock
private UserContextProvider mUserContextTracker; private UserContextProvider mUserContextTracker;
private KeyguardDismissUtil mKeyguardDismissUtil = new KeyguardDismissUtil() { private KeyguardDismissUtil mKeyguardDismissUtil = new KeyguardDismissUtil() {
public void executeWhenUnlocked(ActivityStarter.OnDismissAction action, public void executeWhenUnlocked(ActivityStarter.OnDismissAction action,
@@ -79,8 +84,8 @@ public class RecordingServiceTest extends SysuiTestCase {
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
MockitoAnnotations.initMocks(this); MockitoAnnotations.initMocks(this);
mRecordingService = Mockito.spy(new RecordingService(mController, mExecutor, mUiEventLogger, mRecordingService = Mockito.spy(new RecordingService(mController, mExecutor, mHandler,
mNotificationManager, mUserContextTracker, mKeyguardDismissUtil)); mUiEventLogger, mNotificationManager, mUserContextTracker, mKeyguardDismissUtil));
// Return actual context info // Return actual context info
doReturn(mContext).when(mRecordingService).getApplicationContext(); doReturn(mContext).when(mRecordingService).getApplicationContext();
@@ -143,4 +148,54 @@ public class RecordingServiceTest extends SysuiTestCase {
// Then the state is set to not recording // Then the state is set to not recording
verify(mController).updateState(false); 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();
}
} }