Merge "[Partial screen sharing] Handle system-requested media projection stop in SysUI screen recorder" into tm-qpr-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
91066c7d3c
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user