Loading packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java +1 −1 Original line number Diff line number Diff line Loading @@ -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); Loading packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java +28 −3 Original line number Diff line number Diff line Loading @@ -54,15 +54,32 @@ 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(); } }; @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"); } } } }; Loading Loading @@ -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()); Loading Loading @@ -174,7 +195,6 @@ public class RecordingController } catch (PendingIntent.CanceledException e) { Log.e(TAG, "Error stopping: " + e.getMessage()); } mBroadcastDispatcher.unregisterReceiver(mUserChangeReceiver); } /** Loading @@ -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) { Loading packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java +68 −16 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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 Loading @@ -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: Loading Loading @@ -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 Loading Loading @@ -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) { Loading @@ -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); } Loading packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java +2 −2 Original line number Diff line number Diff line Loading @@ -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 = Loading Loading @@ -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(); Loading packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java +31 −0 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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() { Loading Loading
packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java +1 −1 Original line number Diff line number Diff line Loading @@ -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); Loading
packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java +28 −3 Original line number Diff line number Diff line Loading @@ -54,15 +54,32 @@ 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(); } }; @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"); } } } }; Loading Loading @@ -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()); Loading Loading @@ -174,7 +195,6 @@ public class RecordingController } catch (PendingIntent.CanceledException e) { Log.e(TAG, "Error stopping: " + e.getMessage()); } mBroadcastDispatcher.unregisterReceiver(mUserChangeReceiver); } /** Loading @@ -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) { Loading
packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java +68 −16 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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 Loading @@ -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: Loading Loading @@ -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 Loading Loading @@ -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) { Loading @@ -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); } Loading
packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java +2 −2 Original line number Diff line number Diff line Loading @@ -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 = Loading Loading @@ -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(); Loading
packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java +31 −0 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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() { Loading