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:
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user