diff options
| author | 2018-11-01 23:14:14 +0000 | |
|---|---|---|
| committer | 2018-11-01 23:14:14 +0000 | |
| commit | 005489c07e92951a7b92df88c94de906a4699620 (patch) | |
| tree | 7c5a3621c26b69530742a654eb9747730abd53be | |
| parent | 4c2ca62956e59f36d95b0243f7ff7e50d9c80588 (diff) | |
| parent | 5898ac47b2b61c64416bfedaa47afecb5da2a33d (diff) | |
Merge "Adding screen recording function."
10 files changed, 746 insertions, 1 deletions
diff --git a/core/java/android/util/FeatureFlagUtils.java b/core/java/android/util/FeatureFlagUtils.java index fb44eda5bc73..2d67d79062b9 100644 --- a/core/java/android/util/FeatureFlagUtils.java +++ b/core/java/android/util/FeatureFlagUtils.java @@ -36,6 +36,7 @@ public class FeatureFlagUtils { public static final String PERSIST_PREFIX = "persist." + FFLAG_OVERRIDE_PREFIX; public static final String HEARING_AID_SETTINGS = "settings_bluetooth_hearing_aid"; public static final String EMERGENCY_DIAL_SHORTCUTS = "settings_emergency_dial_shortcuts"; + public static final String SCREENRECORD_LONG_PRESS = "settings_screenrecord_long_press"; private static final Map<String, String> DEFAULT_FLAGS; static { @@ -50,6 +51,7 @@ public class FeatureFlagUtils { DEFAULT_FLAGS.put(HEARING_AID_SETTINGS, "false"); DEFAULT_FLAGS.put(EMERGENCY_DIAL_SHORTCUTS, "true"); DEFAULT_FLAGS.put("settings_network_and_internet_v2", "false"); + DEFAULT_FLAGS.put(SCREENRECORD_LONG_PRESS, "false"); } /** diff --git a/core/java/com/android/internal/util/ScreenRecordHelper.java b/core/java/com/android/internal/util/ScreenRecordHelper.java new file mode 100644 index 000000000000..64d089848b7d --- /dev/null +++ b/core/java/com/android/internal/util/ScreenRecordHelper.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2018 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.internal.util; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; + +/** + * Helper class to initiate a screen recording + */ +public class ScreenRecordHelper { + private static final String SYSUI_PACKAGE = "com.android.systemui"; + private static final String SYSUI_SCREENRECORD_LAUNCHER = + "com.android.systemui.screenrecord.ScreenRecordDialog"; + + private final Context mContext; + + /** + * Create a new ScreenRecordHelper for the given context + * @param context + */ + public ScreenRecordHelper(Context context) { + mContext = context; + } + + /** + * Show dialog of screen recording options to user. + */ + public void launchRecordPrompt() { + final ComponentName launcherComponent = new ComponentName(SYSUI_PACKAGE, + SYSUI_SCREENRECORD_LAUNCHER); + final Intent intent = new Intent(); + intent.setComponent(launcherComponent); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + mContext.startActivity(intent); + } +} diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 44bc3f212654..d9b20637e6b4 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -136,6 +136,10 @@ <!-- Screen Capturing --> <uses-permission android:name="android.permission.MANAGE_MEDIA_PROJECTION" /> + <!-- Screen Recording --> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> + <uses-permission android:name="android.permission.RECORD_AUDIO" /> + <!-- Assist --> <uses-permission android:name="android.permission.ACCESS_VOICE_INTERACTION_SERVICE" /> @@ -267,6 +271,10 @@ </intent-filter> </receiver> + <activity android:name=".screenrecord.ScreenRecordDialog" + android:theme="@style/ScreenRecord" /> + <service android:name=".screenrecord.RecordingService" /> + <receiver android:name=".SysuiRestartReceiver" android:exported="false"> <intent-filter> diff --git a/packages/SystemUI/res/layout/screen_record_dialog.xml b/packages/SystemUI/res/layout/screen_record_dialog.xml new file mode 100644 index 000000000000..6c5c7fac3ed6 --- /dev/null +++ b/packages/SystemUI/res/layout/screen_record_dialog.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:clipChildren="false" + android:clipToPadding="false" + android:gravity="top" + android:orientation="vertical"> + + <Space + android:layout_width="match_parent" + android:layout_height="10dp"/> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:background="@android:color/white"> + <CheckBox + android:id="@+id/checkbox_mic" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/screenrecord_mic_label"/> + <CheckBox + android:id="@+id/checkbox_taps" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/screenrecord_taps_label"/> + <Button + android:id="@+id/record_button" + android:layout_width="match_parent" + android:layout_height="50dp" + android:text="@string/screenrecord_start_label" + /> + </LinearLayout> + + <Space + android:layout_width="match_parent" + android:layout_height="10dp"/> + +</LinearLayout> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 3e928a4227cd..d67841213c7e 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -185,6 +185,39 @@ <string name="screenshot_failed_to_capture_text">Taking screenshots isn\'t allowed by the app or your organization</string> + <!-- Notification title displayed for screen recording [CHAR LIMIT=50]--> + <string name="screenrecord_name">Screen Recording</string> + <!-- Description of the screen recording notification channel [CHAR LIMIT=NONE]--> + <string name="screenrecord_channel_description">Ongoing notification for a screen record session</string> + <!-- Label for the button to begin screen recording [CHAR LIMIT=NONE]--> + <string name="screenrecord_start_label">Start Recording</string> + <!-- Label for the checkbox to enable microphone input during screen recording [CHAR LIMIT=NONE]--> + <string name="screenrecord_mic_label">Record voiceover</string> + <!-- Label for the checkbox to enable showing location of touches during screen recording [CHAR LIMIT=NONE]--> + <string name="screenrecord_taps_label">Show taps</string> + <!-- Label for notification action to stop and save the screen recording [CHAR LIMIT=35] --> + <string name="screenrecord_stop_label">Stop</string> + <!-- Label for notification action to pause screen recording [CHAR LIMIT=35] --> + <string name="screenrecord_pause_label">Pause</string> + <!-- Label for notification action to resume screen recording [CHAR LIMIT=35] --> + <string name="screenrecord_resume_label">Resume</string> + <!-- Label for notification action to cancel and discard screen recording [CHAR LIMIT=35] --> + <string name="screenrecord_cancel_label">Cancel</string> + <!-- Label for notification action to share screen recording [CHAR LIMIT=35] --> + <string name="screenrecord_share_label">Share</string> + <!-- Label for notification action to delete a screen recording file [CHAR LIMIT=35] --> + <string name="screenrecord_delete_label">Delete</string> + <!-- A toast message shown after successfully canceling a screen recording [CHAR LIMIT=NONE] --> + <string name="screenrecord_cancel_success">Screen recording canceled</string> + <!-- Notification text shown after saving a screen recording to prompt the user to view it [CHAR LIMIT=100] --> + <string name="screenrecord_save_message">Screen recording saved, tap to view</string> + <!-- A toast message shown after successfully deleting a screen recording [CHAR LIMIT=NONE] --> + <string name="screenrecord_delete_description">Screen recording deleted</string> + <!-- A toast message shown when there is an error deleting a screen recording [CHAR LIMIT=NONE] --> + <string name="screenrecord_delete_error">Error deleting screen recording</string> + <!-- A toast message shown when the screen recording cannot be started due to insufficient permissions [CHAR LIMIT=NONE] --> + <string name="screenrecord_permission_error">Failed to get permissions</string> + <!-- Title for the USB function chooser in UsbPreferenceActivity. [CHAR LIMIT=30] --> <string name="usb_preference_title">USB file transfer options</string> <!-- Label for the MTP USB function in UsbPreferenceActivity. [CHAR LIMIT=50] --> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index 6bc2a6352e3d..6244e1c4a509 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -503,4 +503,13 @@ <item name="chargingAnimColor">@android:color/white</item> <item name="android:textColor">@android:color/white</item> </style> + + <!-- Screen recording --> + <style name="ScreenRecord" parent="Theme.SystemUI.Dialog.GlobalActions"> + <item name="android:windowIsTranslucent">true</item> + <item name="android:windowBackground">@android:color/transparent</item> + <item name="android:windowIsFloating">true</item> + <item name="android:backgroundDimEnabled">true</item> + <item name="android:windowCloseOnTouchOutside">true</item> + </style> </resources> diff --git a/packages/SystemUI/res/xml/fileprovider.xml b/packages/SystemUI/res/xml/fileprovider.xml index 4aaa90fb5c53..fa6468fefe04 100644 --- a/packages/SystemUI/res/xml/fileprovider.xml +++ b/packages/SystemUI/res/xml/fileprovider.xml @@ -17,4 +17,5 @@ <paths xmlns:android="http://schemas.android.com/apk/res/android"> <cache-path name="leak" path="leak/"/> + <external-path name="screenrecord" path="."/> </paths>
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java index 512cd82731c2..dc7b1ef9a092 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java @@ -82,6 +82,7 @@ import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.telephony.TelephonyIntents; import com.android.internal.telephony.TelephonyProperties; import com.android.internal.util.EmergencyAffordanceManager; +import com.android.internal.util.ScreenRecordHelper; import com.android.internal.util.ScreenshotHelper; import com.android.internal.widget.LockPatternUtils; import com.android.systemui.Dependency; @@ -158,6 +159,7 @@ class GlobalActionsDialog implements DialogInterface.OnDismissListener, private final boolean mShowSilentToggle; private final EmergencyAffordanceManager mEmergencyAffordanceManager; private final ScreenshotHelper mScreenshotHelper; + private final ScreenRecordHelper mScreenRecordHelper; /** * @param context everything needs a context :( @@ -199,6 +201,7 @@ class GlobalActionsDialog implements DialogInterface.OnDismissListener, mEmergencyAffordanceManager = new EmergencyAffordanceManager(context); mScreenshotHelper = new ScreenshotHelper(context); + mScreenRecordHelper = new ScreenRecordHelper(context); Dependency.get(ConfigurationController.class).addCallback(this); } @@ -522,7 +525,7 @@ class GlobalActionsDialog implements DialogInterface.OnDismissListener, } - private class ScreenshotAction extends SinglePressAction { + private class ScreenshotAction extends SinglePressAction implements LongPressAction { public ScreenshotAction() { super(R.drawable.ic_screenshot, R.string.global_action_screenshot); } @@ -552,6 +555,16 @@ class GlobalActionsDialog implements DialogInterface.OnDismissListener, public boolean showBeforeProvisioning() { return false; } + + @Override + public boolean onLongPress() { + if (FeatureFlagUtils.isEnabled(mContext, FeatureFlagUtils.SCREENRECORD_LONG_PRESS)) { + mScreenRecordHelper.launchRecordPrompt(); + } else { + onPress(); + } + return true; + } } private class BugReportAction extends SinglePressAction implements LongPressAction { diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java new file mode 100644 index 000000000000..a9896f51369c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java @@ -0,0 +1,451 @@ +/* + * Copyright (C) 2018 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 android.app.Activity; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.drawable.Icon; +import android.hardware.display.DisplayManager; +import android.hardware.display.VirtualDisplay; +import android.media.MediaRecorder; +import android.media.ThumbnailUtils; +import android.media.projection.MediaProjection; +import android.media.projection.MediaProjectionManager; +import android.net.Uri; +import android.os.Environment; +import android.os.IBinder; +import android.provider.MediaStore; +import android.provider.Settings; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.Surface; +import android.widget.Toast; + +import androidx.core.content.FileProvider; + +import com.android.systemui.R; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * A service which records the device screen and optionally microphone input. + */ +public class RecordingService extends Service { + private static final int NOTIFICATION_ID = 1; + 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_USE_AUDIO = "extra_useAudio"; + private static final String EXTRA_SHOW_TAPS = "extra_showTaps"; + private static final int REQUEST_CODE = 2; + + 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_PAUSE = "com.android.systemui.screenrecord.PAUSE"; + private static final String ACTION_RESUME = "com.android.systemui.screenrecord.RESUME"; + private static final String ACTION_CANCEL = "com.android.systemui.screenrecord.CANCEL"; + private static final String ACTION_SHARE = "com.android.systemui.screenrecord.SHARE"; + private static final String ACTION_DELETE = "com.android.systemui.screenrecord.DELETE"; + + private static final int TOTAL_NUM_TRACKS = 1; + private static final String RECORD_DIR = "Captures"; // TODO: use a translatable string + private static final int VIDEO_BIT_RATE = 6000000; + private static final int VIDEO_FRAME_RATE = 30; + private static final int AUDIO_BIT_RATE = 16; + private static final int AUDIO_SAMPLE_RATE = 44100; + private static final String FILE_PROVIDER = "com.android.systemui.fileprovider"; + + private MediaProjectionManager mMediaProjectionManager; + private MediaProjection mMediaProjection; + private Surface mInputSurface; + private VirtualDisplay mVirtualDisplay; + private MediaRecorder mMediaRecorder; + private Notification.Builder mRecordingNotificationBuilder; + + private boolean mUseAudio; + private boolean mShowTaps; + private File mTempFile; + + /** + * Get an intent to start the recording service. + * + * @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 useAudio True to enable microphone input while recording + * @param showTaps True to make touches visible while recording + */ + public static Intent getStartIntent(Context context, int resultCode, Intent data, + boolean useAudio, boolean showTaps) { + return new Intent(context, RecordingService.class) + .setAction(ACTION_START) + .putExtra(EXTRA_RESULT_CODE, resultCode) + .putExtra(EXTRA_DATA, data) + .putExtra(EXTRA_USE_AUDIO, useAudio) + .putExtra(EXTRA_SHOW_TAPS, showTaps); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Log.d(TAG, "RecordingService is starting"); + if (intent == null) { + return Service.START_NOT_STICKY; + } + String action = intent.getAction(); + + NotificationManager notificationManager = + (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + + switch (action) { + case ACTION_START: + int resultCode = intent.getIntExtra(EXTRA_RESULT_CODE, Activity.RESULT_CANCELED); + mUseAudio = intent.getBooleanExtra(EXTRA_USE_AUDIO, false); + mShowTaps = intent.getBooleanExtra(EXTRA_SHOW_TAPS, false); + Intent data = intent.getParcelableExtra(EXTRA_DATA); + if (data != null) { + mMediaProjection = mMediaProjectionManager.getMediaProjection(resultCode, data); + startRecording(); + } + break; + + case ACTION_CANCEL: + stopRecording(); + + // Delete temp file + if (!mTempFile.delete()) { + Log.e(TAG, "Error canceling screen recording!"); + Toast.makeText(this, R.string.screenrecord_delete_error, Toast.LENGTH_LONG) + .show(); + } else { + Toast.makeText(this, R.string.screenrecord_cancel_success, Toast.LENGTH_LONG) + .show(); + } + + // Close quick shade + sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); + break; + + case ACTION_STOP: + stopRecording(); + + // Move temp file to user directory + File recordDir = new File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES), + RECORD_DIR); + recordDir.mkdirs(); + + String fileName = new SimpleDateFormat("'screen-'yyyyMMdd-HHmmss'.mp4'") + .format(new Date()); + Path path = new File(recordDir, fileName).toPath(); + + try { + Files.move(mTempFile.toPath(), path); + Notification notification = createSaveNotification(path); + notificationManager.notify(NOTIFICATION_ID, notification); + } catch (IOException e) { + e.printStackTrace(); + Toast.makeText(this, R.string.screenrecord_delete_error, Toast.LENGTH_LONG) + .show(); + } + break; + + case ACTION_PAUSE: + mMediaRecorder.pause(); + setNotificationActions(true, notificationManager); + break; + + case ACTION_RESUME: + mMediaRecorder.resume(); + setNotificationActions(false, notificationManager); + break; + + case ACTION_SHARE: + File shareFile = new File(intent.getStringExtra(EXTRA_PATH)); + Uri shareUri = FileProvider.getUriForFile(this, FILE_PROVIDER, shareFile); + + Intent shareIntent = new Intent(Intent.ACTION_SEND) + .setType("video/mp4") + .putExtra(Intent.EXTRA_STREAM, shareUri); + String shareLabel = getResources().getString(R.string.screenrecord_share_label); + + // Close quick shade + sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); + + // Remove notification + notificationManager.cancel(NOTIFICATION_ID); + + startActivity(Intent.createChooser(shareIntent, shareLabel) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); + break; + case ACTION_DELETE: + // Close quick shade + sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); + + File file = new File(intent.getStringExtra(EXTRA_PATH)); + if (file.delete()) { + Toast.makeText( + this, + R.string.screenrecord_delete_description, + Toast.LENGTH_LONG).show(); + + // Remove notification + notificationManager.cancel(NOTIFICATION_ID); + } else { + Log.e(TAG, "Error deleting screen recording!"); + Toast.makeText(this, R.string.screenrecord_delete_error, Toast.LENGTH_LONG) + .show(); + } + break; + } + return Service.START_STICKY; + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public void onCreate() { + super.onCreate(); + + mMediaProjectionManager = + (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE); + } + + /** + * Begin the recording session + */ + private void startRecording() { + try { + mTempFile = File.createTempFile("temp", ".mp4"); + Log.d(TAG, "Writing video output to: " + mTempFile.getAbsolutePath()); + + setTapsVisible(mShowTaps); + + // Set up media recorder + mMediaRecorder = new MediaRecorder(); + if (mUseAudio) { + mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); + } + mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); + mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); + + // Set up video + DisplayMetrics metrics = getResources().getDisplayMetrics(); + int screenWidth = metrics.widthPixels; + int screenHeight = metrics.heightPixels; + mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); + mMediaRecorder.setVideoSize(screenWidth, screenHeight); + mMediaRecorder.setVideoFrameRate(VIDEO_FRAME_RATE); + mMediaRecorder.setVideoEncodingBitRate(VIDEO_BIT_RATE); + + // Set up audio + if (mUseAudio) { + mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); + mMediaRecorder.setAudioChannels(TOTAL_NUM_TRACKS); + mMediaRecorder.setAudioEncodingBitRate(AUDIO_BIT_RATE); + mMediaRecorder.setAudioSamplingRate(AUDIO_SAMPLE_RATE); + } + + mMediaRecorder.setOutputFile(mTempFile); + mMediaRecorder.prepare(); + + // Create surface + mInputSurface = mMediaRecorder.getSurface(); + mVirtualDisplay = mMediaProjection.createVirtualDisplay( + "Recording Display", + screenWidth, + screenHeight, + metrics.densityDpi, + DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, + mInputSurface, + null, + null); + + mMediaRecorder.start(); + } catch (IOException e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + + createRecordingNotification(); + } + + private void createRecordingNotification() { + NotificationChannel channel = new NotificationChannel( + CHANNEL_ID, + getString(R.string.screenrecord_name), + NotificationManager.IMPORTANCE_HIGH); + channel.setDescription(getString(R.string.screenrecord_channel_description)); + channel.enableVibration(true); + NotificationManager notificationManager = + (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.createNotificationChannel(channel); + + mRecordingNotificationBuilder = new Notification.Builder(this, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_android) + .setContentTitle(getResources().getString(R.string.screenrecord_name)) + .setUsesChronometer(true) + .setOngoing(true); + setNotificationActions(false, notificationManager); + Notification notification = mRecordingNotificationBuilder.build(); + startForeground(NOTIFICATION_ID, notification); + } + + private void setNotificationActions(boolean isPaused, NotificationManager notificationManager) { + String pauseString = getResources() + .getString(isPaused ? R.string.screenrecord_resume_label + : R.string.screenrecord_pause_label); + Intent pauseIntent = isPaused ? getResumeIntent(this) : getPauseIntent(this); + + mRecordingNotificationBuilder.setActions( + new Notification.Action.Builder( + Icon.createWithResource(this, R.drawable.ic_android), + getResources().getString(R.string.screenrecord_stop_label), + PendingIntent + .getService(this, REQUEST_CODE, getStopIntent(this), + PendingIntent.FLAG_UPDATE_CURRENT)) + .build(), + new Notification.Action.Builder( + Icon.createWithResource(this, R.drawable.ic_android), pauseString, + PendingIntent.getService(this, REQUEST_CODE, pauseIntent, + PendingIntent.FLAG_UPDATE_CURRENT)) + .build(), + new Notification.Action.Builder( + Icon.createWithResource(this, R.drawable.ic_android), + getResources().getString(R.string.screenrecord_cancel_label), + PendingIntent + .getService(this, REQUEST_CODE, getCancelIntent(this), + PendingIntent.FLAG_UPDATE_CURRENT)) + .build()); + notificationManager.notify(NOTIFICATION_ID, mRecordingNotificationBuilder.build()); + } + + private Notification createSaveNotification(Path path) { + Uri saveUri = FileProvider.getUriForFile(this, FILE_PROVIDER, path.toFile()); + Log.d(TAG, "Screen recording saved to " + path.toString()); + + Intent viewIntent = new Intent(Intent.ACTION_VIEW) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION) + .setDataAndType(saveUri, "video/mp4"); + + Notification.Action shareAction = new Notification.Action.Builder( + Icon.createWithResource(this, R.drawable.ic_android), + getResources().getString(R.string.screenrecord_share_label), + PendingIntent.getService( + this, + REQUEST_CODE, + getShareIntent(this, path.toString()), + PendingIntent.FLAG_UPDATE_CURRENT)) + .build(); + + Notification.Action deleteAction = new Notification.Action.Builder( + Icon.createWithResource(this, R.drawable.ic_android), + getResources().getString(R.string.screenrecord_delete_label), + PendingIntent.getService( + this, + REQUEST_CODE, + getDeleteIntent(this, path.toString()), + PendingIntent.FLAG_UPDATE_CURRENT)) + .build(); + + Notification.Builder builder = new Notification.Builder(this, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_android) + .setContentTitle(getResources().getString(R.string.screenrecord_name)) + .setContentText(getResources().getString(R.string.screenrecord_save_message)) + .setContentIntent(PendingIntent.getActivity( + this, + REQUEST_CODE, + viewIntent, + Intent.FLAG_GRANT_READ_URI_PERMISSION)) + .addAction(shareAction) + .addAction(deleteAction) + .setAutoCancel(true); + + // Add thumbnail if available + Bitmap thumbnailBitmap = ThumbnailUtils.createVideoThumbnail(path.toString(), + MediaStore.Video.Thumbnails.MINI_KIND); + if (thumbnailBitmap != null) { + Notification.BigPictureStyle pictureStyle = new Notification.BigPictureStyle() + .bigPicture(thumbnailBitmap) + .bigLargeIcon((Bitmap) null); + builder.setLargeIcon(thumbnailBitmap).setStyle(pictureStyle); + } + return builder.build(); + } + + private void stopRecording() { + setTapsVisible(false); + mMediaRecorder.stop(); + mMediaRecorder.release(); + mMediaRecorder = null; + mMediaProjection.stop(); + mMediaProjection = null; + mInputSurface.release(); + mVirtualDisplay.release(); + stopSelf(); + } + + private void setTapsVisible(boolean turnOn) { + int value = turnOn ? 1 : 0; + Settings.System.putInt(getApplicationContext().getContentResolver(), + Settings.System.SHOW_TOUCHES, value); + } + + private static Intent getStopIntent(Context context) { + return new Intent(context, RecordingService.class).setAction(ACTION_STOP); + } + + private static Intent getPauseIntent(Context context) { + return new Intent(context, RecordingService.class).setAction(ACTION_PAUSE); + } + + private static Intent getResumeIntent(Context context) { + return new Intent(context, RecordingService.class).setAction(ACTION_RESUME); + } + + private static Intent getCancelIntent(Context context) { + return new Intent(context, RecordingService.class).setAction(ACTION_CANCEL); + } + + private static Intent getShareIntent(Context context, String path) { + return new Intent(context, RecordingService.class).setAction(ACTION_SHARE) + .putExtra(EXTRA_PATH, path); + } + + private static Intent getDeleteIntent(Context context, String path) { + return new Intent(context, RecordingService.class).setAction(ACTION_DELETE) + .putExtra(EXTRA_PATH, path); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java new file mode 100644 index 000000000000..27e9fbab3161 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2018 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 android.Manifest; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.media.projection.MediaProjectionManager; +import android.os.Bundle; +import android.util.Log; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.Toast; + +import com.android.systemui.R; + +/** + * Activity to select screen recording options + */ +public class ScreenRecordDialog extends Activity { + private static final String TAG = "ScreenRecord"; + private static final int REQUEST_CODE_VIDEO_ONLY = 200; + private static final int REQUEST_CODE_VIDEO_TAPS = 201; + private static final int REQUEST_CODE_PERMISSIONS = 299; + private static final int REQUEST_CODE_VIDEO_AUDIO = 300; + private static final int REQUEST_CODE_VIDEO_AUDIO_TAPS = 301; + private static final int REQUEST_CODE_PERMISSIONS_AUDIO = 399; + private boolean mUseAudio; + private boolean mShowTaps; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.screen_record_dialog); + + final CheckBox micCheckBox = findViewById(R.id.checkbox_mic); + final CheckBox tapsCheckBox = findViewById(R.id.checkbox_taps); + + final Button recordButton = findViewById(R.id.record_button); + recordButton.setOnClickListener(v -> { + mUseAudio = micCheckBox.isChecked(); + mShowTaps = tapsCheckBox.isChecked(); + Log.d(TAG, "Record button clicked: audio " + mUseAudio + ", taps " + mShowTaps); + + if (mUseAudio && checkSelfPermission(Manifest.permission.RECORD_AUDIO) + != PackageManager.PERMISSION_GRANTED) { + Log.d(TAG, "Requesting permission for audio"); + requestPermissions(new String[]{Manifest.permission.RECORD_AUDIO}, + REQUEST_CODE_PERMISSIONS_AUDIO); + } else { + requestScreenCapture(); + } + }); + } + + private void requestScreenCapture() { + MediaProjectionManager mediaProjectionManager = (MediaProjectionManager) getSystemService( + Context.MEDIA_PROJECTION_SERVICE); + Intent permissionIntent = mediaProjectionManager.createScreenCaptureIntent(); + + if (mUseAudio) { + startActivityForResult(permissionIntent, + mShowTaps ? REQUEST_CODE_VIDEO_AUDIO_TAPS : REQUEST_CODE_VIDEO_AUDIO); + } else { + startActivityForResult(permissionIntent, + mShowTaps ? REQUEST_CODE_VIDEO_TAPS : REQUEST_CODE_VIDEO_ONLY); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + mShowTaps = (requestCode == REQUEST_CODE_VIDEO_TAPS + || requestCode == REQUEST_CODE_VIDEO_AUDIO_TAPS); + switch (requestCode) { + case REQUEST_CODE_VIDEO_TAPS: + case REQUEST_CODE_VIDEO_AUDIO_TAPS: + case REQUEST_CODE_VIDEO_ONLY: + case REQUEST_CODE_VIDEO_AUDIO: + if (resultCode == RESULT_OK) { + mUseAudio = (requestCode == REQUEST_CODE_VIDEO_AUDIO + || requestCode == REQUEST_CODE_VIDEO_AUDIO_TAPS); + startForegroundService( + RecordingService.getStartIntent(this, resultCode, data, mUseAudio, + mShowTaps)); + } else { + Toast.makeText(this, + getResources().getString(R.string.screenrecord_permission_error), + Toast.LENGTH_SHORT).show(); + } + finish(); + break; + case REQUEST_CODE_PERMISSIONS: + int permission = checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE); + if (permission != PackageManager.PERMISSION_GRANTED) { + Toast.makeText(this, + getResources().getString(R.string.screenrecord_permission_error), + Toast.LENGTH_SHORT).show(); + finish(); + } else { + requestScreenCapture(); + } + break; + case REQUEST_CODE_PERMISSIONS_AUDIO: + int videoPermission = checkSelfPermission( + Manifest.permission.WRITE_EXTERNAL_STORAGE); + int audioPermission = checkSelfPermission(Manifest.permission.RECORD_AUDIO); + if (videoPermission != PackageManager.PERMISSION_GRANTED + || audioPermission != PackageManager.PERMISSION_GRANTED) { + Toast.makeText(this, + getResources().getString(R.string.screenrecord_permission_error), + Toast.LENGTH_SHORT).show(); + finish(); + } else { + requestScreenCapture(); + } + break; + } + } +} |