diff options
8 files changed, 184 insertions, 61 deletions
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index 04cb88d5a39d..34b4beeaeb7d 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -155,6 +155,7 @@ android_library { "jsr330", "lottie", "LowLightDreamLib", + "TraceurCommon", "motion_tool_lib", "notification_flags_lib", "PlatformComposeCore", @@ -301,6 +302,7 @@ android_library { "androidx.compose.material_material-icons-extended", "androidx.activity_activity-compose", "androidx.compose.animation_animation-graphics", + "TraceurCommon", ], } diff --git a/packages/SystemUI/res/xml/fileprovider.xml b/packages/SystemUI/res/xml/fileprovider.xml index b67378e638e1..71cc05ddfefa 100644 --- a/packages/SystemUI/res/xml/fileprovider.xml +++ b/packages/SystemUI/res/xml/fileprovider.xml @@ -19,4 +19,5 @@ <cache-path name="leak" path="leak/"/> <external-path name="screenrecord" path="."/> <cache-path name="multi_user" path="multi_user/" /> -</paths>
\ No newline at end of file + <root-path name="traces" path="/data/local/traces"/> +</paths> diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt index 20f3c4d9735b..bd66843e1217 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt @@ -103,14 +103,27 @@ constructor( public override fun handleClick(view: View?) { if (isRecording) { isRecording = false - stopScreenRecord() + stopIssueRecordingService() } else { mUiHandler.post { showPrompt(view) } } refreshState() } - private fun stopScreenRecord() = + private fun startIssueRecordingService(screenRecord: Boolean, winscopeTracing: Boolean) = + PendingIntent.getForegroundService( + userContextProvider.userContext, + RecordingService.REQUEST_CODE, + IssueRecordingService.getStartIntent( + userContextProvider.userContext, + screenRecord, + winscopeTracing + ), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + .send(BroadcastOptions.makeBasic().apply { isInteractive = true }.toBundle()) + + private fun stopIssueRecordingService() = PendingIntent.getService( userContextProvider.userContext, RecordingService.REQUEST_CODE, @@ -124,6 +137,7 @@ constructor( delegateFactory .create { isRecording = true + startIssueRecordingService(it.screenRecord, it.winscopeTracing) refreshState() } .createDialog() diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingConfig.kt b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingConfig.kt new file mode 100644 index 000000000000..bb3b65409a47 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingConfig.kt @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2024 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.recordissue + +data class IssueRecordingConfig(val screenRecord: Boolean, val winscopeTracing: Boolean) diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt index f4872589b3bf..55d7f8e0f740 100644 --- a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt +++ b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt @@ -20,7 +20,9 @@ import android.app.NotificationManager import android.content.Context import android.content.Intent import android.content.res.Resources +import android.net.Uri import android.os.Handler +import android.os.UserHandle import com.android.internal.logging.UiEventLogger import com.android.systemui.dagger.qualifiers.LongRunning import com.android.systemui.dagger.qualifiers.Main @@ -30,6 +32,8 @@ import com.android.systemui.screenrecord.RecordingService import com.android.systemui.screenrecord.RecordingServiceStrings import com.android.systemui.settings.UserContextProvider import com.android.systemui.statusbar.phone.KeyguardDismissUtil +import com.android.traceur.FileSender +import com.android.traceur.TraceUtils import java.util.concurrent.Executor import javax.inject.Inject @@ -60,9 +64,89 @@ constructor( override fun provideRecordingServiceStrings(): RecordingServiceStrings = IrsStrings(resources) + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + when (intent?.action) { + ACTION_START -> { + TraceUtils.traceStart( + contentResolver, + DEFAULT_TRACE_TAGS, + DEFAULT_BUFFER_SIZE, + DEFAULT_IS_INCLUDING_WINSCOPE, + DEFAULT_IS_INCLUDING_APP_TRACE, + DEFAULT_IS_LONG_TRACE, + DEFAULT_ATTACH_TO_BUGREPORT, + DEFAULT_MAX_TRACE_SIZE, + DEFAULT_MAX_TRACE_DURATION_IN_MINUTES + ) + if (!intent.getBooleanExtra(EXTRA_SCREEN_RECORD, false)) { + // If we don't want to record the screen, the ACTION_SHOW_START_NOTIF action + // will circumvent the RecordingService's screen recording start code. + return super.onStartCommand(Intent(ACTION_SHOW_START_NOTIF), flags, startId) + } + } + ACTION_STOP, + ACTION_STOP_NOTIF -> { + TraceUtils.traceStop(contentResolver) + } + ACTION_SHARE -> { + shareRecording(intent) + + // Unlike all other actions, action_share has different behavior for the screen + // recording qs tile than it does for the record issue qs tile. Return sticky to + // avoid running any of the base class' code for this action. + return START_STICKY + } + else -> {} + } + return super.onStartCommand(intent, flags, startId) + } + + private fun shareRecording(intent: Intent) { + val files = TraceUtils.traceDump(contentResolver, TRACE_FILE_NAME).get() + val traceUris: MutableList<Uri> = FileSender.getUriForFiles(this, files, AUTHORITY) + + if ( + intent.hasExtra(EXTRA_PATH) && intent.getStringExtra(EXTRA_PATH)?.isNotEmpty() == true + ) { + traceUris.add(Uri.parse(intent.getStringExtra(EXTRA_PATH))) + } + + val sendIntent = + FileSender.buildSendIntent(this, traceUris).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + + if (mNotificationId != NOTIF_BASE_ID) { + val currentUserId = mUserContextTracker.userContext.userId + mNotificationManager.cancelAsUser(null, mNotificationId, UserHandle(currentUserId)) + } + + // TODO: Debug why the notification shade isn't closing upon starting the BetterBug activity + mKeyguardDismissUtil.executeWhenUnlocked( + { + startActivity(sendIntent) + false + }, + false, + false + ) + } + companion object { private const val TAG = "IssueRecordingService" private const val CHANNEL_ID = "issue_record" + private const val EXTRA_SCREEN_RECORD = "extra_screenRecord" + private const val EXTRA_WINSCOPE_TRACING = "extra_winscopeTracing" + + private val DEFAULT_TRACE_TAGS = listOf<String>() + private const val DEFAULT_BUFFER_SIZE = 16384 + private const val DEFAULT_IS_INCLUDING_WINSCOPE = true + private const val DEFAULT_IS_LONG_TRACE = false + private const val DEFAULT_IS_INCLUDING_APP_TRACE = true + private const val DEFAULT_ATTACH_TO_BUGREPORT = true + private const val DEFAULT_MAX_TRACE_SIZE = 10240 + private const val DEFAULT_MAX_TRACE_DURATION_IN_MINUTES = 30 + + private val TRACE_FILE_NAME = TraceUtils.getOutputFilename(TraceUtils.RecordingType.TRACE) + private const val AUTHORITY = "com.android.systemui.fileprovider" /** * Get an intent to stop the issue recording service. @@ -71,7 +155,7 @@ constructor( * @return */ fun getStopIntent(context: Context): Intent = - Intent(context, RecordingService::class.java) + Intent(context, IssueRecordingService::class.java) .setAction(ACTION_STOP) .putExtra(Intent.EXTRA_USER_HANDLE, context.userId) @@ -80,8 +164,15 @@ constructor( * * @param context Context from the requesting activity */ - fun getStartIntent(context: Context): Intent = - Intent(context, RecordingService::class.java).setAction(ACTION_START) + fun getStartIntent( + context: Context, + screenRecord: Boolean, + winscopeTracing: Boolean + ): Intent = + Intent(context, IssueRecordingService::class.java) + .setAction(ACTION_START) + .putExtra(EXTRA_SCREEN_RECORD, screenRecord) + .putExtra(EXTRA_WINSCOPE_TRACING, winscopeTracing) } } diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt index 1c07d00e4195..ff18a1128802 100644 --- a/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt @@ -17,8 +17,6 @@ package com.android.systemui.recordissue import android.annotation.SuppressLint -import android.app.BroadcastOptions -import android.app.PendingIntent import android.content.Context import android.content.res.ColorStateList import android.graphics.Color @@ -43,8 +41,6 @@ import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDevicePoli import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDialogDelegate import com.android.systemui.qs.tiles.RecordIssueTile import com.android.systemui.res.R -import com.android.systemui.screenrecord.RecordingService -import com.android.systemui.settings.UserContextProvider import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.phone.SystemUIDialog @@ -52,12 +48,12 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import java.util.concurrent.Executor +import java.util.function.Consumer class RecordIssueDialogDelegate @AssistedInject constructor( private val factory: SystemUIDialog.Factory, - private val userContextProvider: UserContextProvider, private val userTracker: UserTracker, private val flags: FeatureFlagsClassic, @Background private val bgExecutor: Executor, @@ -66,14 +62,14 @@ constructor( private val mediaProjectionMetricsLogger: MediaProjectionMetricsLogger, private val userFileManager: UserFileManager, private val screenCaptureDisabledDialogDelegate: ScreenCaptureDisabledDialogDelegate, - @Assisted private val onStarted: Runnable, + @Assisted private val onStarted: Consumer<IssueRecordingConfig>, ) : SystemUIDialog.Delegate { /** To inject dependencies and allow for easier testing */ @AssistedFactory interface Factory { /** Create a dialog object */ - fun create(onStarted: Runnable): RecordIssueDialogDelegate + fun create(onStarted: Consumer<IssueRecordingConfig>): RecordIssueDialogDelegate } @SuppressLint("UseSwitchCompatOrMaterialCode") private lateinit var screenRecordSwitch: Switch @@ -87,10 +83,12 @@ constructor( setIcon(R.drawable.qs_record_issue_icon_off) setNegativeButton(R.string.cancel) { _, _ -> dismiss() } setPositiveButton(R.string.qs_record_issue_start) { _, _ -> - onStarted.run() - if (screenRecordSwitch.isChecked) { - requestScreenCapture() - } + onStarted.accept( + IssueRecordingConfig( + screenRecordSwitch.isChecked, + true /* TODO: Base this on issueType selected */ + ) + ) dismiss() } } @@ -177,13 +175,4 @@ constructor( show() } } - - private fun requestScreenCapture() = - PendingIntent.getForegroundService( - userContextProvider.userContext, - RecordingService.REQUEST_CODE, - IssueRecordingService.getStartIntent(userContextProvider.userContext), - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) - .send(BroadcastOptions.makeBasic().apply { isInteractive = true }.toBundle()) } diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java index b355d2d6b4f8..ac94f39bff5a 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java @@ -60,25 +60,27 @@ public class RecordingService extends Service implements ScreenMediaRecorderList public static final int REQUEST_CODE = 2; private static final int USER_ID_NOT_SPECIFIED = -1; - private static final int NOTIF_BASE_ID = 4273; + protected static final int NOTIF_BASE_ID = 4273; private static final String TAG = "RecordingService"; private static final String CHANNEL_ID = "screen_record"; private static final String GROUP_KEY = "screen_record_saved"; private static final String EXTRA_RESULT_CODE = "extra_resultCode"; - private static final String EXTRA_PATH = "extra_path"; + protected 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"; private static final String EXTRA_CAPTURE_TARGET = "extra_captureTarget"; protected static final String ACTION_START = "com.android.systemui.screenrecord.START"; + protected static final String ACTION_SHOW_START_NOTIF = + "com.android.systemui.screenrecord.START_NOTIF"; protected static final String ACTION_STOP = "com.android.systemui.screenrecord.STOP"; - private static final String ACTION_STOP_NOTIF = + protected static final String ACTION_STOP_NOTIF = "com.android.systemui.screenrecord.STOP_FROM_NOTIF"; - private static final String ACTION_SHARE = "com.android.systemui.screenrecord.SHARE"; + protected 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; + protected final KeyguardDismissUtil mKeyguardDismissUtil; private final Handler mMainHandler; private ScreenRecordingAudioSource mAudioSource; private boolean mShowTaps; @@ -86,9 +88,9 @@ public class RecordingService extends Service implements ScreenMediaRecorderList private ScreenMediaRecorder mRecorder; private final Executor mLongExecutor; private final UiEventLogger mUiEventLogger; - private final NotificationManager mNotificationManager; - private final UserContextProvider mUserContextTracker; - private int mNotificationId = NOTIF_BASE_ID; + protected final NotificationManager mNotificationManager; + protected final UserContextProvider mUserContextTracker; + protected int mNotificationId = NOTIF_BASE_ID; private RecordingServiceStrings mStrings; @Inject @@ -185,7 +187,10 @@ public class RecordingService extends Service implements ScreenMediaRecorderList return Service.START_NOT_STICKY; } break; - + case ACTION_SHOW_START_NOTIF: + createRecordingNotification(); + mUiEventLogger.log(Events.ScreenRecordEvent.SCREEN_RECORD_START); + break; case ACTION_STOP_NOTIF: case ACTION_STOP: // only difference for actions is the log event @@ -232,6 +237,7 @@ public class RecordingService extends Service implements ScreenMediaRecorderList super.onCreate(); } + @Nullable @VisibleForTesting protected ScreenMediaRecorder getRecorder() { return mRecorder; @@ -337,8 +343,9 @@ public class RecordingService extends Service implements ScreenMediaRecorderList } @VisibleForTesting - protected Notification createSaveNotification(ScreenMediaRecorder.SavedRecording recording) { - Uri uri = recording.getUri(); + protected Notification createSaveNotification( + @Nullable ScreenMediaRecorder.SavedRecording recording) { + Uri uri = recording != null ? recording.getUri() : null; Intent viewIntent = new Intent(Intent.ACTION_VIEW) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION) .setDataAndType(uri, "video/mp4"); @@ -349,7 +356,7 @@ public class RecordingService extends Service implements ScreenMediaRecorderList PendingIntent.getService( this, REQUEST_CODE, - getShareIntent(this, uri.toString()), + getShareIntent(this, uri != null ? uri.toString() : null), PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)) .build(); @@ -371,9 +378,10 @@ public class RecordingService extends Service implements ScreenMediaRecorderList .addExtras(extras); // Add thumbnail if available - if (recording.getThumbnail() != null) { + Icon thumbnail = recording != null ? recording.getThumbnail() : null; + if (thumbnail != null) { Notification.BigPictureStyle pictureStyle = new Notification.BigPictureStyle() - .bigPicture(recording.getThumbnail()) + .bigPicture(thumbnail) .showBigPictureWhenCollapsed(true); builder.setStyle(pictureStyle); } @@ -408,27 +416,29 @@ public class RecordingService extends Service implements ScreenMediaRecorderList } Log.d(getTag(), "notifying for user " + userId); setTapsVisible(mOriginalShowTaps); - if (getRecorder() != null) { - try { + try { + if (getRecorder() != null) { getRecorder().end(); - saveRecording(userId); - } catch (RuntimeException exception) { + } + saveRecording(userId); + } catch (RuntimeException exception) { + if (getRecorder() != null) { // RuntimeException could happen if the recording stopped immediately after starting // let's release the recorder and delete all temporary files in this case getRecorder().release(); - showErrorToast(R.string.screenrecord_start_error); - Log.e(getTag(), "stopRecording called, but there was an error when ending" - + "recording"); - exception.printStackTrace(); - createErrorNotification(); - } catch (Throwable throwable) { + } + showErrorToast(R.string.screenrecord_start_error); + Log.e(getTag(), "stopRecording called, but there was an error when ending" + + "recording"); + exception.printStackTrace(); + createErrorNotification(); + } catch (Throwable throwable) { + if (getRecorder() != null) { // Something unexpected happen, SystemUI will crash but let's delete // the temporary files anyway getRecorder().release(); - throw new RuntimeException(throwable); } - } else { - Log.e(getTag(), "stopRecording called, but recorder was null"); + throw new RuntimeException(throwable); } updateState(false); stopForeground(STOP_FOREGROUND_DETACH); @@ -443,7 +453,8 @@ public class RecordingService extends Service implements ScreenMediaRecorderList mLongExecutor.execute(() -> { try { Log.d(getTag(), "saving recording"); - Notification notification = createSaveNotification(getRecorder().save()); + Notification notification = createSaveNotification( + getRecorder() != null ? getRecorder().save() : null); postGroupNotification(currentUser); mNotificationManager.notifyAsUser(null, mNotificationId, notification, currentUser); diff --git a/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt index ada93db537e5..2e8160baa257 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt @@ -37,7 +37,6 @@ import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDi import com.android.systemui.model.SysUiState import com.android.systemui.qs.tiles.RecordIssueTile import com.android.systemui.res.R -import com.android.systemui.settings.UserContextProvider import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.phone.SystemUIDialog @@ -71,12 +70,11 @@ class RecordIssueDialogDelegateTest : SysuiTestCase() { @Mock private lateinit var devicePolicyResolver: ScreenCaptureDevicePolicyResolver @Mock private lateinit var dprLazy: dagger.Lazy<ScreenCaptureDevicePolicyResolver> @Mock private lateinit var mediaProjectionMetricsLogger: MediaProjectionMetricsLogger - @Mock private lateinit var userContextProvider: UserContextProvider @Mock private lateinit var userTracker: UserTracker @Mock private lateinit var userFileManager: UserFileManager @Mock private lateinit var sharedPreferences: SharedPreferences - @Mock private lateinit var screenCaptureDisabledDialogDelegate: - ScreenCaptureDisabledDialogDelegate + @Mock + private lateinit var screenCaptureDisabledDialogDelegate: ScreenCaptureDisabledDialogDelegate @Mock private lateinit var screenCaptureDisabledDialog: SystemUIDialog @Mock private lateinit var sysuiState: SysUiState @@ -96,9 +94,8 @@ class RecordIssueDialogDelegateTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) whenever(dprLazy.get()).thenReturn(devicePolicyResolver) whenever(sysuiState.setFlag(anyInt(), anyBoolean())).thenReturn(sysuiState) - whenever(userContextProvider.userContext).thenReturn(mContext) whenever(screenCaptureDisabledDialogDelegate.createDialog()) - .thenReturn(screenCaptureDisabledDialog) + .thenReturn(screenCaptureDisabledDialog) whenever( userFileManager.getSharedPreferences( eq(RecordIssueTile.TILE_SPEC), @@ -123,7 +120,6 @@ class RecordIssueDialogDelegateTest : SysuiTestCase() { dialog = RecordIssueDialogDelegate( factory, - userContextProvider, userTracker, flags, bgExecutor, |