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

Commit 231ac9bc authored by Beth Thibodeau's avatar Beth Thibodeau
Browse files

Screen record user switching fixes

Allow opening the dialog in all users
Stop recording when the user changes
Post notifications in the correct user

Still some issues with tile state and status bar but those require more
work to coordinate across users, so may not make it into R
(related bug: b/158004991)

Fixes: 147921212
Bug: 148955577
Test: manual
Test: atest ScreenRecordTileTest
Test: atest com.android.systemui.screenrecord
Change-Id: Idf10931c5ddf3079b903708824fae11135169c1b
parent 2ba2ce12
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -329,6 +329,7 @@

        <activity android:name=".screenrecord.ScreenRecordDialog"
            android:theme="@style/ScreenRecord"
            android:showForAllUsers="true"
            android:excludeFromRecents="true" />
        <service android:name=".screenrecord.RecordingService" />

+12 −10
Original line number Diff line number Diff line
@@ -22,13 +22,13 @@ 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;
import com.android.systemui.qs.QSHost;
import com.android.systemui.qs.tileimpl.QSTileImpl;
import com.android.systemui.screenrecord.RecordingController;
import com.android.systemui.statusbar.phone.KeyguardDismissUtil;

import javax.inject.Inject;

@@ -39,19 +39,17 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState>
        implements RecordingController.RecordingStateChangeCallback {
    private static final String TAG = "ScreenRecordTile";
    private RecordingController mController;
    private ActivityStarter mActivityStarter;
    private KeyguardDismissUtil mKeyguardDismissUtil;
    private long mMillisUntilFinished = 0;
    private Callback mCallback = new Callback();
    private UiEventLogger mUiEventLogger;

    @Inject
    public ScreenRecordTile(QSHost host, RecordingController controller,
            ActivityStarter activityStarter, UiEventLogger uiEventLogger) {
            KeyguardDismissUtil keyguardDismissUtil) {
        super(host);
        mController = controller;
        mController.observe(this, mCallback);
        mActivityStarter = activityStarter;
        mUiEventLogger = uiEventLogger;
        mKeyguardDismissUtil = keyguardDismissUtil;
    }

    @Override
@@ -69,7 +67,7 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState>
        } else if (mController.isRecording()) {
            stopRecording();
        } else {
            startCountdown();
            mUiHandler.post(() -> showPrompt());
        }
        refreshState();
    }
@@ -114,11 +112,15 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState>
        return mContext.getString(R.string.quick_settings_screen_record_label);
    }

    private void startCountdown() {
        // Close QS, otherwise the permission dialog appears beneath it
    private void showPrompt() {
        // Close QS, otherwise the dialog appears beneath it
        getHost().collapsePanels();
        Intent intent = mController.getPromptIntent();
        mActivityStarter.postStartActivityDismissingKeyguard(intent, 0);
        ActivityStarter.OnDismissAction dismissAction = () -> {
            mContext.startActivity(intent);
            return false;
        };
        mKeyguardDismissUtil.executeWhenUnlocked(dismissAction, false);
    }

    private void cancelCountdown() {
+27 −5
Original line number Diff line number Diff line
@@ -17,12 +17,17 @@
package com.android.systemui.screenrecord;

import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.CountDownTimer;
import android.os.UserHandle;
import android.util.Log;

import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.statusbar.policy.CallbackController;

import java.util.ArrayList;
@@ -41,21 +46,30 @@ public class RecordingController
    private static final String SYSUI_SCREENRECORD_LAUNCHER =
            "com.android.systemui.screenrecord.ScreenRecordDialog";

    private final Context mContext;
    private boolean mIsStarting;
    private boolean mIsRecording;
    private PendingIntent mStopIntent;
    private CountDownTimer mCountDownTimer = null;
    private BroadcastDispatcher mBroadcastDispatcher;

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

    /**
     * Create a new RecordingController
     * @param context Context for the controller
     */
    @Inject
    public RecordingController(Context context) {
        mContext = context;
    public RecordingController(BroadcastDispatcher broadcastDispatcher) {
        mBroadcastDispatcher = broadcastDispatcher;
    }

    /**
@@ -99,6 +113,9 @@ public class RecordingController
                }
                try {
                    startIntent.send();
                    IntentFilter userFilter = new IntentFilter(Intent.ACTION_USER_SWITCHED);
                    mBroadcastDispatcher.registerReceiver(mUserChangeReceiver, userFilter, null,
                            UserHandle.ALL);
                    Log.d(TAG, "sent start intent");
                } catch (PendingIntent.CanceledException e) {
                    Log.e(TAG, "Pending intent was cancelled: " + e.getMessage());
@@ -146,11 +163,16 @@ public class RecordingController
     */
    public void stopRecording() {
        try {
            if (mStopIntent != null) {
                mStopIntent.send();
            } else {
                Log.e(TAG, "Stop intent was null");
            }
            updateState(false);
        } catch (PendingIntent.CanceledException e) {
            Log.e(TAG, "Error stopping: " + e.getMessage());
        }
        mBroadcastDispatcher.unregisterReceiver(mUserChangeReceiver);
    }

    /**
+44 −23
Original line number Diff line number Diff line
@@ -32,6 +32,7 @@ import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.UserHandle;
import android.provider.Settings;
import android.util.Log;
import android.widget.Toast;
@@ -40,6 +41,7 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.R;
import com.android.systemui.dagger.qualifiers.LongRunning;
import com.android.systemui.settings.CurrentUserContextTracker;

import java.io.IOException;
import java.util.concurrent.Executor;
@@ -58,7 +60,6 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
    private static final String TAG = "RecordingService";
    private static final String CHANNEL_ID = "screen_record";
    private static final String EXTRA_RESULT_CODE = "extra_resultCode";
    private static final String EXTRA_DATA = "extra_data";
    private static final String EXTRA_PATH = "extra_path";
    private static final String EXTRA_AUDIO_SOURCE = "extra_useAudio";
    private static final String EXTRA_SHOW_TAPS = "extra_showTaps";
@@ -79,14 +80,17 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
    private final Executor mLongExecutor;
    private final UiEventLogger mUiEventLogger;
    private final NotificationManager mNotificationManager;
    private final CurrentUserContextTracker mUserContextTracker;

    @Inject
    public RecordingService(RecordingController controller, @LongRunning Executor executor,
            UiEventLogger uiEventLogger, NotificationManager notificationManager) {
            UiEventLogger uiEventLogger, NotificationManager notificationManager,
            CurrentUserContextTracker userContextTracker) {
        mController = controller;
        mLongExecutor = executor;
        mUiEventLogger = uiEventLogger;
        mNotificationManager = notificationManager;
        mUserContextTracker = userContextTracker;
    }

    /**
@@ -95,8 +99,6 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
     * @param context    Context from the requesting activity
     * @param resultCode The result code from {@link android.app.Activity#onActivityResult(int, int,
     *                   android.content.Intent)}
     * @param data       The data from {@link android.app.Activity#onActivityResult(int, int,
     *                   android.content.Intent)}
     * @param audioSource   The ordinal value of the audio source
     *                      {@link com.android.systemui.screenrecord.ScreenRecordingAudioSource}
     * @param showTaps   True to make touches visible while recording
@@ -118,6 +120,8 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
        String action = intent.getAction();
        Log.d(TAG, "onStartCommand " + action);

        int mCurrentUserId = mUserContextTracker.getCurrentUserContext().getUserId();
        UserHandle currentUser = new UserHandle(mCurrentUserId);
        switch (action) {
            case ACTION_START:
                mAudioSource = ScreenRecordingAudioSource
@@ -132,8 +136,8 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
                setTapsVisible(mShowTaps);

                mRecorder = new ScreenMediaRecorder(
                        getApplicationContext(),
                        getUserId(),
                        mUserContextTracker.getCurrentUserContext(),
                        mCurrentUserId,
                        mAudioSource,
                        this
                );
@@ -148,7 +152,14 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
                } else {
                    mUiEventLogger.log(Events.ScreenRecordEvent.SCREEN_RECORD_END_QS_TILE);
                }
                stopRecording();
                // Check user ID - we may be getting a stop intent after user switch, in which case
                // we want to post the notifications for that user, which is NOT current user
                int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
                if (userId == -1) {
                    userId = mUserContextTracker.getCurrentUserContext().getUserId();
                }
                Log.d(TAG, "notifying for user " + userId);
                stopRecording(userId);
                mNotificationManager.cancel(NOTIFICATION_RECORDING_ID);
                stopSelf();
                break;
@@ -165,7 +176,7 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
                sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));

                // Remove notification
                mNotificationManager.cancel(NOTIFICATION_VIEW_ID);
                mNotificationManager.cancelAsUser(null, NOTIFICATION_VIEW_ID, currentUser);

                startActivity(Intent.createChooser(shareIntent, shareLabel)
                        .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
@@ -184,7 +195,7 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
                        Toast.LENGTH_LONG).show();

                // Remove notification
                mNotificationManager.cancel(NOTIFICATION_VIEW_ID);
                mNotificationManager.cancelAsUser(null, NOTIFICATION_VIEW_ID, currentUser);
                Log.d(TAG, "Deleted recording " + uri);
                break;
        }
@@ -215,11 +226,12 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
            mController.updateState(true);
            createRecordingNotification();
            mUiEventLogger.log(Events.ScreenRecordEvent.SCREEN_RECORD_START);
        } catch (IOException | RemoteException e) {
        } catch (IOException | RemoteException | IllegalStateException e) {
            Toast.makeText(this,
                    R.string.screenrecord_start_error, Toast.LENGTH_LONG)
                    .show();
            e.printStackTrace();
            mController.updateState(false);
        }
    }

@@ -242,7 +254,6 @@ 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);


        Intent stopIntent = getNotificationIntent(this);
        Notification.Builder builder = new Notification.Builder(this, CHANNEL_ID)
                .setSmallIcon(R.drawable.ic_screenrecord)
@@ -254,7 +265,7 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
                .setOngoing(true)
                .setContentIntent(
                        PendingIntent.getService(this, REQUEST_CODE, stopIntent,
                                PendingIntent.FLAG_UPDATE_CURRENT))
                                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE))
                .addExtras(extras);
        startForeground(NOTIFICATION_RECORDING_ID, builder.build());
    }
@@ -265,11 +276,17 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
        String notificationTitle = mAudioSource == ScreenRecordingAudioSource.NONE
                ? res.getString(R.string.screenrecord_ongoing_screen_only)
                : res.getString(R.string.screenrecord_ongoing_screen_and_audio);

        Bundle extras = new Bundle();
        extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME,
                res.getString(R.string.screenrecord_name));

        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);
                .setSmallIcon(R.drawable.ic_screenrecord)
                .addExtras(extras);
        return builder.build();
    }

@@ -287,7 +304,7 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
                        this,
                        REQUEST_CODE,
                        getShareIntent(this, uri.toString()),
                        PendingIntent.FLAG_UPDATE_CURRENT))
                        PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE))
                .build();

        Notification.Action deleteAction = new Notification.Action.Builder(
@@ -297,7 +314,7 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
                        this,
                        REQUEST_CODE,
                        getDeleteIntent(this, uri.toString()),
                        PendingIntent.FLAG_UPDATE_CURRENT))
                        PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE))
                .build();

        Bundle extras = new Bundle();
@@ -328,34 +345,36 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
        return builder.build();
    }

    private void stopRecording() {
    private void stopRecording(int userId) {
        setTapsVisible(mOriginalShowTaps);
        if (getRecorder() != null) {
            getRecorder().end();
            saveRecording();
            saveRecording(userId);
        } else {
            Log.e(TAG, "stopRecording called, but recorder was null");
        }
        mController.updateState(false);
    }

    private void saveRecording() {
        mNotificationManager.notify(NOTIFICATION_PROCESSING_ID, createProcessingNotification());
    private void saveRecording(int userId) {
        UserHandle currentUser = new UserHandle(userId);
        mNotificationManager.notifyAsUser(null, NOTIFICATION_PROCESSING_ID,
                createProcessingNotification(), currentUser);

        mLongExecutor.execute(() -> {
            try {
                Log.d(TAG, "saving recording");
                Notification notification = createSaveNotification(getRecorder().save());
                if (!mController.isRecording()) {
                    Log.d(TAG, "showing saved notification");
                    mNotificationManager.notify(NOTIFICATION_VIEW_ID, notification);
                    mNotificationManager.notifyAsUser(null, NOTIFICATION_VIEW_ID, notification,
                            currentUser);
                }
            } 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 {
                mNotificationManager.cancel(NOTIFICATION_PROCESSING_ID);
                mNotificationManager.cancelAsUser(null, NOTIFICATION_PROCESSING_ID, currentUser);
            }
        });
    }
@@ -371,7 +390,9 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
     * @return
     */
    public static Intent getStopIntent(Context context) {
        return new Intent(context, RecordingService.class).setAction(ACTION_STOP);
        return new Intent(context, RecordingService.class)
                .setAction(ACTION_STOP)
                .putExtra(Intent.EXTRA_USER_HANDLE, context.getUserId());
    }

    /**
+5 −10
Original line number Diff line number Diff line
@@ -16,7 +16,6 @@

package com.android.systemui.screenrecord;

import android.content.Context;
import android.media.AudioAttributes;
import android.media.AudioFormat;
import android.media.AudioPlaybackCaptureConfiguration;
@@ -39,7 +38,6 @@ public class ScreenInternalAudioRecorder {
    private static String TAG = "ScreenAudioRecorder";
    private static final int TIMEOUT = 500;
    private static final float MIC_VOLUME_SCALE = 1.4f;
    private final Context mContext;
    private AudioRecord mAudioRecord;
    private AudioRecord mAudioRecordMic;
    private Config mConfig = new Config();
@@ -49,17 +47,14 @@ public class ScreenInternalAudioRecorder {
    private long mPresentationTime;
    private long mTotalBytes;
    private MediaMuxer mMuxer;
    private String mOutFile;
    private boolean mMic;

    private int mTrackId = -1;

    public ScreenInternalAudioRecorder(String outFile, Context context,
            MediaProjection mp, boolean includeMicInput) throws IOException {
    public ScreenInternalAudioRecorder(String outFile, MediaProjection mp, boolean includeMicInput)
            throws IOException {
        mMic = includeMicInput;
        mOutFile = outFile;
        mMuxer = new MediaMuxer(outFile, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
        mContext = context;
        mMediaProjection = mp;
        Log.d(TAG, "creating audio file " + outFile);
        setupSimple();
@@ -266,8 +261,9 @@ public class ScreenInternalAudioRecorder {

    /**
    * start recording
     * @throws IllegalStateException if recording fails to initialize
    */
    public void start() {
    public void start() throws IllegalStateException {
        if (mThread != null) {
            Log.e(TAG, "a recording is being done in parallel or stop is not called");
        }
@@ -276,8 +272,7 @@ public class ScreenInternalAudioRecorder {
        Log.d(TAG, "channel count " + mAudioRecord.getChannelCount());
        mCodec.start();
        if (mAudioRecord.getRecordingState() != AudioRecord.RECORDSTATE_RECORDING) {
            Log.e(TAG, "Error starting audio recording");
            return;
            throw new IllegalStateException("Audio recording failed to start");
        }
        mThread.start();
    }
Loading