Merge "Adding screen recording function."
This commit is contained in:
committed by
Android (Google) Code Review
commit
005489c07e
@@ -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");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
52
core/java/com/android/internal/util/ScreenRecordHelper.java
Normal file
52
core/java/com/android/internal/util/ScreenRecordHelper.java
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
41
packages/SystemUI/res/layout/screen_record_dialog.xml
Normal file
41
packages/SystemUI/res/layout/screen_record_dialog.xml
Normal file
@@ -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>
|
||||
@@ -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] -->
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user