Enable secondary user to update controller status

Also improved error handling for recorder, which currently crashes when
secondary user tries to record audio

Fixes: 158004991
Bug: 170771496
Test: manual
Test: atest ScreenRecordTileTest RecordingControllerTest RecordingServiceTest
Change-Id: I9c0687f0798170e0bfd2ee6a8234bf0a44952212
This commit is contained in:
Beth Thibodeau
2020-11-19 15:54:45 -05:00
parent 4f8866ea20
commit 4e16d7ac68
6 changed files with 149 additions and 22 deletions

View File

@@ -134,7 +134,7 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState>
getHost().collapsePanels();
Intent intent = mController.getPromptIntent();
ActivityStarter.OnDismissAction dismissAction = () -> {
mContext.startActivity(intent);
mHost.getUserContext().startActivity(intent);
return false;
};
mKeyguardDismissUtil.executeWhenUnlocked(dismissAction, false);

View File

@@ -54,14 +54,31 @@ public class RecordingController
private CountDownTimer mCountDownTimer = null;
private BroadcastDispatcher mBroadcastDispatcher;
protected static final String INTENT_UPDATE_STATE =
"com.android.systemui.screenrecord.UPDATE_STATE";
protected static final String EXTRA_STATE = "extra_state";
private ArrayList<RecordingStateChangeCallback> mListeners = new ArrayList<>();
@VisibleForTesting
protected final BroadcastReceiver mUserChangeReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (mStopIntent != null) {
stopRecording();
stopRecording();
}
};
@VisibleForTesting
protected final BroadcastReceiver mStateChangeReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent != null && INTENT_UPDATE_STATE.equals(intent.getAction())) {
if (intent.hasExtra(EXTRA_STATE)) {
boolean state = intent.getBooleanExtra(EXTRA_STATE, false);
updateState(state);
} else {
Log.e(TAG, "Received update intent with no state");
}
}
}
};
@@ -118,6 +135,10 @@ public class RecordingController
IntentFilter userFilter = new IntentFilter(Intent.ACTION_USER_SWITCHED);
mBroadcastDispatcher.registerReceiver(mUserChangeReceiver, userFilter, null,
UserHandle.ALL);
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());
@@ -174,7 +195,6 @@ public class RecordingController
} catch (PendingIntent.CanceledException e) {
Log.e(TAG, "Error stopping: " + e.getMessage());
}
mBroadcastDispatcher.unregisterReceiver(mUserChangeReceiver);
}
/**
@@ -182,6 +202,11 @@ public class RecordingController
* @param isRecording
*/
public synchronized void updateState(boolean isRecording) {
if (!isRecording && mIsRecording) {
// Unregister receivers if we have stopped recording
mBroadcastDispatcher.unregisterReceiver(mUserChangeReceiver);
mBroadcastDispatcher.unregisterReceiver(mStateChangeReceiver);
}
mIsRecording = isRecording;
for (RecordingStateChangeCallback cb : mListeners) {
if (isRecording) {

View File

@@ -69,6 +69,7 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
private static final String ACTION_STOP_NOTIF =
"com.android.systemui.screenrecord.STOP_FROM_NOTIF";
private static final String ACTION_SHARE = "com.android.systemui.screenrecord.SHARE";
private static final String PERMISSION_SELF = "com.android.systemui.permission.SELF";
private final RecordingController mController;
private final KeyguardDismissUtil mKeyguardDismissUtil;
@@ -120,8 +121,8 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
String action = intent.getAction();
Log.d(TAG, "onStartCommand " + action);
int mCurrentUserId = mUserContextTracker.getUserContext().getUserId();
UserHandle currentUser = new UserHandle(mCurrentUserId);
int currentUserId = mUserContextTracker.getUserContext().getUserId();
UserHandle currentUser = new UserHandle(currentUserId);
switch (action) {
case ACTION_START:
mAudioSource = ScreenRecordingAudioSource
@@ -137,11 +138,22 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
mRecorder = new ScreenMediaRecorder(
mUserContextTracker.getUserContext(),
mCurrentUserId,
currentUserId,
mAudioSource,
this
);
startRecording();
if (startRecording()) {
updateState(true);
createRecordingNotification();
mUiEventLogger.log(Events.ScreenRecordEvent.SCREEN_RECORD_START);
} else {
updateState(false);
createErrorNotification();
stopForeground(true);
stopSelf();
return Service.START_NOT_STICKY;
}
break;
case ACTION_STOP_NOTIF:
@@ -201,22 +213,63 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
return mRecorder;
}
private void updateState(boolean state) {
int userId = mUserContextTracker.getUserContext().getUserId();
if (userId == UserHandle.USER_SYSTEM) {
// Main user has a reference to the correct controller, so no need to use a broadcast
mController.updateState(state);
} else {
Intent intent = new Intent(RecordingController.INTENT_UPDATE_STATE);
intent.putExtra(RecordingController.EXTRA_STATE, state);
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
sendBroadcast(intent, PERMISSION_SELF);
}
}
/**
* Begin the recording session
* @return true if successful, false if something went wrong
*/
private void startRecording() {
private boolean startRecording() {
try {
getRecorder().start();
mController.updateState(true);
createRecordingNotification();
mUiEventLogger.log(Events.ScreenRecordEvent.SCREEN_RECORD_START);
} catch (IOException | RemoteException | IllegalStateException e) {
Toast.makeText(this,
R.string.screenrecord_start_error, Toast.LENGTH_LONG)
.show();
return true;
} catch (IOException | RemoteException | RuntimeException e) {
showErrorToast(R.string.screenrecord_start_error);
e.printStackTrace();
mController.updateState(false);
}
return false;
}
/**
* Simple error notification, needed since startForeground must be called to avoid errors
*/
@VisibleForTesting
protected void createErrorNotification() {
Resources res = getResources();
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID,
getString(R.string.screenrecord_name),
NotificationManager.IMPORTANCE_DEFAULT);
channel.setDescription(getString(R.string.screenrecord_channel_description));
channel.enableVibration(true);
mNotificationManager.createNotificationChannel(channel);
Bundle extras = new Bundle();
extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME,
res.getString(R.string.screenrecord_name));
String notificationTitle = res.getString(R.string.screenrecord_start_error);
Notification.Builder builder = new Notification.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_screenrecord)
.setContentTitle(notificationTitle)
.addExtras(extras);
startForeground(NOTIFICATION_RECORDING_ID, builder.build());
}
@VisibleForTesting
protected void showErrorToast(int stringId) {
Toast.makeText(this, stringId, Toast.LENGTH_LONG).show();
}
@VisibleForTesting
@@ -326,7 +379,7 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
} else {
Log.e(TAG, "stopRecording called, but recorder was null");
}
mController.updateState(false);
updateState(false);
}
private void saveRecording(int userId) {
@@ -344,8 +397,7 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
}
} catch (IOException e) {
Log.e(TAG, "Error saving screen recording: " + e.getMessage());
Toast.makeText(this, R.string.screenrecord_delete_error, Toast.LENGTH_LONG)
.show();
showErrorToast(R.string.screenrecord_delete_error);
} finally {
mNotificationManager.cancelAsUser(null, NOTIFICATION_PROCESSING_ID, currentUser);
}

View File

@@ -94,7 +94,7 @@ public class ScreenMediaRecorder {
mAudioSource = audioSource;
}
private void prepare() throws IOException, RemoteException {
private void prepare() throws IOException, RemoteException, RuntimeException {
//Setup media projection
IBinder b = ServiceManager.getService(MEDIA_PROJECTION_SERVICE);
IMediaProjectionManager mediaService =
@@ -257,7 +257,7 @@ public class ScreenMediaRecorder {
/**
* Start screen recording
*/
void start() throws IOException, RemoteException, IllegalStateException {
void start() throws IOException, RemoteException, RuntimeException {
Log.d(TAG, "start recording");
prepare();
mMediaRecorder.start();

View File

@@ -19,6 +19,8 @@ package com.android.systemui.screenrecord;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import android.app.PendingIntent;
@@ -128,6 +130,35 @@ public class RecordingControllerTest extends SysuiTestCase {
verify(mCallback).onRecordingEnd();
}
// Test that broadcast will update state
@Test
public void testUpdateStateBroadcast() {
if (Looper.myLooper() == null) {
Looper.prepare();
}
// When a recording has started
PendingIntent startIntent = Mockito.mock(PendingIntent.class);
mController.startCountdown(0, 0, startIntent, null);
verify(mCallback).onCountdownEnd();
// then the receiver was registered
verify(mBroadcastDispatcher).registerReceiver(eq(mController.mStateChangeReceiver),
any(), any(), any());
// When the receiver gets an update
Intent intent = new Intent(RecordingController.INTENT_UPDATE_STATE);
intent.putExtra(RecordingController.EXTRA_STATE, false);
mController.mStateChangeReceiver.onReceive(mContext, intent);
// then the state is updated
assertFalse(mController.isRecording());
verify(mCallback).onRecordingEnd();
// and the receiver is unregistered
verify(mBroadcastDispatcher).unregisterReceiver(eq(mController.mStateChangeReceiver));
}
// Test that switching users will stop an ongoing recording
@Test
public void testUserChange() {

View File

@@ -17,15 +17,18 @@
package com.android.systemui.screenrecord;
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.times;
import static org.mockito.Mockito.verify;
import android.app.Notification;
import android.app.NotificationManager;
import android.content.Intent;
import android.os.RemoteException;
import android.testing.AndroidTestingRunner;
import androidx.test.filters.SmallTest;
@@ -43,6 +46,7 @@ import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import java.io.IOException;
import java.util.concurrent.Executor;
@RunWith(AndroidTestingRunner.class)
@@ -88,6 +92,9 @@ public class RecordingServiceTest extends SysuiTestCase {
doNothing().when(mRecordingService).createRecordingNotification();
doReturn(mNotification).when(mRecordingService).createProcessingNotification();
doReturn(mNotification).when(mRecordingService).createSaveNotification(any());
doNothing().when(mRecordingService).createErrorNotification();
doNothing().when(mRecordingService).showErrorToast(anyInt());
doNothing().when(mRecordingService).stopForeground(anyBoolean());
doNothing().when(mRecordingService).startForeground(anyInt(), any());
doReturn(mScreenMediaRecorder).when(mRecordingService).getRecorder();
@@ -124,4 +131,16 @@ public class RecordingServiceTest extends SysuiTestCase {
.log(Events.ScreenRecordEvent.SCREEN_RECORD_END_NOTIFICATION);
verify(mUiEventLogger, times(0)).log(Events.ScreenRecordEvent.SCREEN_RECORD_END_QS_TILE);
}
@Test
public void testErrorUpdatesState() throws IOException, RemoteException {
// When the screen recording does not start properly
doThrow(new RuntimeException("fail")).when(mScreenMediaRecorder).start();
Intent startIntent = RecordingService.getStartIntent(mContext, 0, 0, false);
mRecordingService.onStartCommand(startIntent, 0, 0);
// Then the state is set to not recording
verify(mController).updateState(false);
}
}