Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit abb338f1 authored by Beth Thibodeau's avatar Beth Thibodeau
Browse files

Add logging for screen recording

Log recording start, end from qs tile, and end from notification

Fixes: 147503450
Test: atest RecordingServiceTest ScreenRecordTileTest
Test: manual with go/aster-event-viewing, verified correct IDs logged
Change-Id: I1b631cfb82c087ad4c08f074795ae3a8b170e714
parent e30c1fe9
Loading
Loading
Loading
Loading
+4 −3
Original line number Diff line number Diff line
@@ -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();
    }

+45 −0
Original line number Diff line number Diff line
/*
 * 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;
        }
    }
}
+72 −39
Original line number Diff line number Diff line
@@ -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());
    }

    @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();
    }

    private Notification createSaveNotification(ScreenMediaRecorder.SavedRecording recording) {
    @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);
+4 −1
Original line number Diff line number Diff line
@@ -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
+114 −0
Original line number Diff line number Diff line
/*
 * 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);
    }
}