Adding screen recording function.
Screen recording can be launched via long press on screenshot in the global actions menu if the local feature flag is enabled. Otherwise, long press on screenshot will also trigger a screenshot. Demo videos: https://drive.google.com/open?id=1oJzfzJb8aGXSUqn4CZ_Yn7qWmJ2dvRd5 Test: manual Change-Id: I373d38ad86291ff6f26f7dca3195001bd8f5ee16 Bug: 111395687 Bug: 118826991
This commit is contained in:
@@ -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