Merge "Add logging for screen recording" into rvc-dev

This commit is contained in:
TreeHugger Robot
2020-06-09 01:10:59 +00:00
committed by Android (Google) Code Review
5 changed files with 239 additions and 43 deletions

View File

@@ -22,6 +22,7 @@ import android.text.TextUtils;
import android.util.Log;
import android.widget.Switch;
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.R;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.qs.QSTile;
@@ -41,14 +42,16 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState>
private ActivityStarter mActivityStarter;
private long mMillisUntilFinished = 0;
private Callback mCallback = new Callback();
private UiEventLogger mUiEventLogger;
@Inject
public ScreenRecordTile(QSHost host, RecordingController controller,
ActivityStarter activityStarter) {
ActivityStarter activityStarter, UiEventLogger uiEventLogger) {
super(host);
mController = controller;
mController.observe(this, mCallback);
mActivityStarter = activityStarter;
mUiEventLogger = uiEventLogger;
}
@Override
@@ -112,7 +115,6 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState>
}
private void startCountdown() {
Log.d(TAG, "Starting countdown");
// Close QS, otherwise the permission dialog appears beneath it
getHost().collapsePanels();
Intent intent = mController.getPromptIntent();
@@ -125,7 +127,6 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState>
}
private void stopRecording() {
Log.d(TAG, "Stopping recording from tile");
mController.stopRecording();
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.screenrecord;
import com.android.internal.logging.UiEvent;
import com.android.internal.logging.UiEventLogger;
/**
* Events related to the SystemUI screen recorder
*/
public class Events {
public enum ScreenRecordEvent implements UiEventLogger.UiEventEnum {
@UiEvent(doc = "Screen recording was started")
SCREEN_RECORD_START(299),
@UiEvent(doc = "Screen recording was stopped from the quick settings tile")
SCREEN_RECORD_END_QS_TILE(300),
@UiEvent(doc = "Screen recording was stopped from the notification")
SCREEN_RECORD_END_NOTIFICATION(301);
private final int mId;
ScreenRecordEvent(int id) {
mId = id;
}
@Override
public int getId() {
return mId;
}
}
}

View File

@@ -36,6 +36,8 @@ import android.provider.Settings;
import android.util.Log;
import android.widget.Toast;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.R;
import com.android.systemui.dagger.qualifiers.LongRunning;
@@ -63,22 +65,28 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
private static final String ACTION_START = "com.android.systemui.screenrecord.START";
private static final String ACTION_STOP = "com.android.systemui.screenrecord.STOP";
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 ACTION_DELETE = "com.android.systemui.screenrecord.DELETE";
private final RecordingController mController;
private Notification.Builder mRecordingNotificationBuilder;
private ScreenRecordingAudioSource mAudioSource;
private boolean mShowTaps;
private boolean mOriginalShowTaps;
private ScreenMediaRecorder mRecorder;
private final Executor mLongExecutor;
private final UiEventLogger mUiEventLogger;
private final NotificationManager mNotificationManager;
@Inject
public RecordingService(RecordingController controller, @LongRunning Executor executor) {
public RecordingService(RecordingController controller, @LongRunning Executor executor,
UiEventLogger uiEventLogger, NotificationManager notificationManager) {
mController = controller;
mLongExecutor = executor;
mUiEventLogger = uiEventLogger;
mNotificationManager = notificationManager;
}
/**
@@ -110,9 +118,6 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
String action = intent.getAction();
Log.d(TAG, "onStartCommand " + action);
NotificationManager notificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
switch (action) {
case ACTION_START:
mAudioSource = ScreenRecordingAudioSource
@@ -135,10 +140,16 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
startRecording();
break;
case ACTION_STOP_NOTIF:
case ACTION_STOP:
// only difference for actions is the log event
if (ACTION_STOP_NOTIF.equals(action)) {
mUiEventLogger.log(Events.ScreenRecordEvent.SCREEN_RECORD_END_NOTIFICATION);
} else {
mUiEventLogger.log(Events.ScreenRecordEvent.SCREEN_RECORD_END_QS_TILE);
}
stopRecording();
notificationManager.cancel(NOTIFICATION_RECORDING_ID);
saveRecording(notificationManager);
mNotificationManager.cancel(NOTIFICATION_RECORDING_ID);
stopSelf();
break;
@@ -154,7 +165,7 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
// Remove notification
notificationManager.cancel(NOTIFICATION_VIEW_ID);
mNotificationManager.cancel(NOTIFICATION_VIEW_ID);
startActivity(Intent.createChooser(shareIntent, shareLabel)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
@@ -173,7 +184,7 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
Toast.LENGTH_LONG).show();
// Remove notification
notificationManager.cancel(NOTIFICATION_VIEW_ID);
mNotificationManager.cancel(NOTIFICATION_VIEW_ID);
Log.d(TAG, "Deleted recording " + uri);
break;
}
@@ -190,14 +201,20 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
super.onCreate();
}
@VisibleForTesting
protected ScreenMediaRecorder getRecorder() {
return mRecorder;
}
/**
* Begin the recording session
*/
private void startRecording() {
try {
mRecorder.start();
getRecorder().start();
mController.updateState(true);
createRecordingNotification();
mUiEventLogger.log(Events.ScreenRecordEvent.SCREEN_RECORD_START);
} catch (IOException | RemoteException e) {
Toast.makeText(this,
R.string.screenrecord_start_error, Toast.LENGTH_LONG)
@@ -206,7 +223,8 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
}
}
private void createRecordingNotification() {
@VisibleForTesting
protected void createRecordingNotification() {
Resources res = getResources();
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID,
@@ -214,9 +232,7 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
NotificationManager.IMPORTANCE_DEFAULT);
channel.setDescription(getString(R.string.screenrecord_channel_description));
channel.enableVibration(true);
NotificationManager notificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.createNotificationChannel(channel);
mNotificationManager.createNotificationChannel(channel);
Bundle extras = new Bundle();
extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME,
@@ -226,7 +242,9 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
? res.getString(R.string.screenrecord_ongoing_screen_only)
: res.getString(R.string.screenrecord_ongoing_screen_and_audio);
mRecordingNotificationBuilder = new Notification.Builder(this, CHANNEL_ID)
Intent stopIntent = getNotificationIntent(this);
Notification.Builder builder = new Notification.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_screenrecord)
.setContentTitle(notificationTitle)
.setContentText(getResources().getString(R.string.screenrecord_stop_text))
@@ -235,17 +253,28 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
.setColor(getResources().getColor(R.color.GM2_red_700))
.setOngoing(true)
.setContentIntent(
PendingIntent.getService(
this, REQUEST_CODE, getStopIntent(this),
PendingIntent.getService(this, REQUEST_CODE, stopIntent,
PendingIntent.FLAG_UPDATE_CURRENT))
.addExtras(extras);
notificationManager.notify(NOTIFICATION_RECORDING_ID,
mRecordingNotificationBuilder.build());
Notification notification = mRecordingNotificationBuilder.build();
startForeground(NOTIFICATION_RECORDING_ID, notification);
startForeground(NOTIFICATION_RECORDING_ID, builder.build());
}
private Notification createSaveNotification(ScreenMediaRecorder.SavedRecording recording) {
@VisibleForTesting
protected Notification createProcessingNotification() {
Resources res = getApplicationContext().getResources();
String notificationTitle = mAudioSource == ScreenRecordingAudioSource.NONE
? res.getString(R.string.screenrecord_ongoing_screen_only)
: res.getString(R.string.screenrecord_ongoing_screen_and_audio);
Notification.Builder builder = new Notification.Builder(getApplicationContext(), CHANNEL_ID)
.setContentTitle(notificationTitle)
.setContentText(
getResources().getString(R.string.screenrecord_background_processing_label))
.setSmallIcon(R.drawable.ic_screenrecord);
return builder.build();
}
@VisibleForTesting
protected Notification createSaveNotification(ScreenMediaRecorder.SavedRecording recording) {
Uri uri = recording.getUri();
Intent viewIntent = new Intent(Intent.ACTION_VIEW)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION)
@@ -301,44 +330,39 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
private void stopRecording() {
setTapsVisible(mOriginalShowTaps);
mRecorder.end();
if (getRecorder() != null) {
getRecorder().end();
saveRecording();
} else {
Log.e(TAG, "stopRecording called, but recorder was null");
}
mController.updateState(false);
}
private void saveRecording(NotificationManager notificationManager) {
Resources res = getApplicationContext().getResources();
String notificationTitle = mAudioSource == ScreenRecordingAudioSource.NONE
? res.getString(R.string.screenrecord_ongoing_screen_only)
: res.getString(R.string.screenrecord_ongoing_screen_and_audio);
Notification.Builder builder = new Notification.Builder(getApplicationContext(), CHANNEL_ID)
.setContentTitle(notificationTitle)
.setContentText(
getResources().getString(R.string.screenrecord_background_processing_label))
.setSmallIcon(R.drawable.ic_screenrecord);
notificationManager.notify(NOTIFICATION_PROCESSING_ID, builder.build());
private void saveRecording() {
mNotificationManager.notify(NOTIFICATION_PROCESSING_ID, createProcessingNotification());
mLongExecutor.execute(() -> {
try {
Log.d(TAG, "saving recording");
Notification notification = createSaveNotification(mRecorder.save());
Notification notification = createSaveNotification(getRecorder().save());
if (!mController.isRecording()) {
Log.d(TAG, "showing saved notification");
notificationManager.notify(NOTIFICATION_VIEW_ID, notification);
mNotificationManager.notify(NOTIFICATION_VIEW_ID, notification);
}
} catch (IOException e) {
Log.e(TAG, "Error saving screen recording: " + e.getMessage());
Toast.makeText(this, R.string.screenrecord_delete_error, Toast.LENGTH_LONG)
.show();
} finally {
notificationManager.cancel(NOTIFICATION_PROCESSING_ID);
mNotificationManager.cancel(NOTIFICATION_PROCESSING_ID);
}
});
}
private void setTapsVisible(boolean turnOn) {
int value = turnOn ? 1 : 0;
Settings.System.putInt(getApplicationContext().getContentResolver(),
Settings.System.SHOW_TOUCHES, value);
Settings.System.putInt(getContentResolver(), Settings.System.SHOW_TOUCHES, value);
}
/**
@@ -350,6 +374,15 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
return new Intent(context, RecordingService.class).setAction(ACTION_STOP);
}
/**
* Get the recording notification content intent
* @param context
* @return
*/
protected static Intent getNotificationIntent(Context context) {
return new Intent(context, RecordingService.class).setAction(ACTION_STOP_NOTIF);
}
private static Intent getShareIntent(Context context, String path) {
return new Intent(context, RecordingService.class).setAction(ACTION_SHARE)
.putExtra(EXTRA_PATH, path);

View File

@@ -29,6 +29,7 @@ import android.testing.TestableLooper;
import androidx.test.filters.SmallTest;
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.Dependency;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
@@ -53,6 +54,8 @@ public class ScreenRecordTileTest extends SysuiTestCase {
private ActivityStarter mActivityStarter;
@Mock
private QSTileHost mHost;
@Mock
private UiEventLogger mUiEventLogger;
private TestableLooper mTestableLooper;
private ScreenRecordTile mTile;
@@ -68,7 +71,7 @@ public class ScreenRecordTileTest extends SysuiTestCase {
when(mHost.getContext()).thenReturn(mContext);
mTile = new ScreenRecordTile(mHost, mController, mActivityStarter);
mTile = new ScreenRecordTile(mHost, mController, mActivityStarter, mUiEventLogger);
}
// Test that the tile is inactive and labeled correctly when the controller is neither starting

View File

@@ -0,0 +1,114 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.screenrecord;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
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.testing.AndroidTestingRunner;
import androidx.test.filters.SmallTest;
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.SysuiTestCase;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import java.util.concurrent.Executor;
@RunWith(AndroidTestingRunner.class)
@SmallTest
public class RecordingServiceTest extends SysuiTestCase {
@Mock
private UiEventLogger mUiEventLogger;
@Mock
private RecordingController mController;
@Mock
private NotificationManager mNotificationManager;
@Mock
private ScreenMediaRecorder mScreenMediaRecorder;
@Mock
private Notification mNotification;
@Mock
private Executor mExecutor;
private RecordingService mRecordingService;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mRecordingService = Mockito.spy(new RecordingService(mController, mExecutor, mUiEventLogger,
mNotificationManager));
// Return actual context info
doReturn(mContext).when(mRecordingService).getApplicationContext();
doReturn(mContext.getUserId()).when(mRecordingService).getUserId();
doReturn(mContext.getPackageName()).when(mRecordingService).getPackageName();
doReturn(mContext.getContentResolver()).when(mRecordingService).getContentResolver();
// Mock notifications
doNothing().when(mRecordingService).createRecordingNotification();
doReturn(mNotification).when(mRecordingService).createProcessingNotification();
doReturn(mNotification).when(mRecordingService).createSaveNotification(any());
doNothing().when(mRecordingService).startForeground(anyInt(), any());
doReturn(mScreenMediaRecorder).when(mRecordingService).getRecorder();
}
@Test
public void testLogStartRecording() {
Intent startIntent = RecordingService.getStartIntent(mContext, 0, 0, false);
mRecordingService.onStartCommand(startIntent, 0, 0);
verify(mUiEventLogger, times(1)).log(Events.ScreenRecordEvent.SCREEN_RECORD_START);
}
@Test
public void testLogStopFromQsTile() {
Intent stopIntent = RecordingService.getStopIntent(mContext);
mRecordingService.onStartCommand(stopIntent, 0, 0);
// Verify that we log the correct event
verify(mUiEventLogger, times(1)).log(Events.ScreenRecordEvent.SCREEN_RECORD_END_QS_TILE);
verify(mUiEventLogger, times(0))
.log(Events.ScreenRecordEvent.SCREEN_RECORD_END_NOTIFICATION);
}
@Test
public void testLogStopFromNotificationIntent() {
Intent stopIntent = RecordingService.getNotificationIntent(mContext);
mRecordingService.onStartCommand(stopIntent, 0, 0);
// Verify that we log the correct event
verify(mUiEventLogger, times(1))
.log(Events.ScreenRecordEvent.SCREEN_RECORD_END_NOTIFICATION);
verify(mUiEventLogger, times(0)).log(Events.ScreenRecordEvent.SCREEN_RECORD_END_QS_TILE);
}
}