Merge "Screen recording sound options" into rvc-dev
This commit is contained in:
@@ -59,29 +59,16 @@
|
||||
android:layout_gravity="center"
|
||||
android:layout_weight="0"
|
||||
android:layout_marginRight="@dimen/screenrecord_dialog_padding"/>
|
||||
<LinearLayout
|
||||
<Spinner
|
||||
android:id="@+id/screen_recording_options"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_weight="1">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:text="@string/screenrecord_audio_label"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"/>
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/audio_type"
|
||||
android:text="@string/screenrecord_mic_label"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"/>
|
||||
</LinearLayout>
|
||||
android:layout_height="48dp"
|
||||
android:prompt="@string/screenrecord_audio_label"/>
|
||||
<Switch
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="48dp"
|
||||
android:layout_weight="0"
|
||||
android:layout_weight="1"
|
||||
android:layout_gravity="end"
|
||||
android:id="@+id/screenrecord_audio_switch"/>
|
||||
</LinearLayout>
|
||||
|
||||
@@ -102,7 +89,8 @@
|
||||
android:id="@+id/screenrecord_taps_switch"
|
||||
android:text="@string/screenrecord_taps_label"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"/>
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"/>
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (C) 2020 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.
|
||||
-->
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:orientation="vertical"
|
||||
android:padding="10dp"
|
||||
android:layout_weight="1">
|
||||
<TextView
|
||||
android:id="@+id/screen_recording_dialog_source_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="?android:attr/textColorPrimary"/>
|
||||
<TextView
|
||||
android:id="@+id/screen_recording_dialog_source_description"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="?android:attr/textColorSecondary"/>
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (C) 2020 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.
|
||||
-->
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:orientation="vertical"
|
||||
android:layout_weight="1">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:text="@string/screenrecord_audio_label"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="?android:attr/textColorPrimary"/>
|
||||
<TextView
|
||||
android:id="@+id/screen_recording_dialog_source_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"/>
|
||||
</LinearLayout>
|
||||
@@ -144,5 +144,10 @@
|
||||
|
||||
<!-- NotificationPanelView -->
|
||||
<item type="id" name="notification_panel" />
|
||||
|
||||
<!-- Screen Recording -->
|
||||
<item type="id" name="screen_recording_options" />
|
||||
<item type="id" name="screen_recording_dialog_source_text" />
|
||||
<item type="id" name="screen_recording_dialog_source_description" />
|
||||
</resources>
|
||||
|
||||
|
||||
@@ -240,6 +240,8 @@
|
||||
|
||||
<!-- Notification title displayed for screen recording [CHAR LIMIT=50]-->
|
||||
<string name="screenrecord_name">Screen Recorder</string>
|
||||
<!-- Processing screen recoding video in the background [CHAR LIMIT=30]-->
|
||||
<string name="screenrecord_background_processing_label">Processing 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>
|
||||
<!-- Title for the screen prompting the user to begin recording their screen [CHAR LIMIT=NONE]-->
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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.dagger.qualifiers;
|
||||
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
|
||||
import javax.inject.Qualifier;
|
||||
|
||||
@Qualifier
|
||||
@Documented
|
||||
@Retention(RUNTIME)
|
||||
public @interface LongRunning {
|
||||
}
|
||||
@@ -137,7 +137,7 @@ public class RecordingController
|
||||
* Check if the recording is ongoing
|
||||
* @return
|
||||
*/
|
||||
public boolean isRecording() {
|
||||
public synchronized boolean isRecording() {
|
||||
return mIsRecording;
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ public class RecordingController
|
||||
* Update the current status
|
||||
* @param isRecording
|
||||
*/
|
||||
public void updateState(boolean isRecording) {
|
||||
public synchronized void updateState(boolean isRecording) {
|
||||
mIsRecording = isRecording;
|
||||
for (RecordingStateChangeCallback cb : mListeners) {
|
||||
if (isRecording) {
|
||||
|
||||
@@ -22,41 +22,27 @@ import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
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.projection.IMediaProjection;
|
||||
import android.media.projection.IMediaProjectionManager;
|
||||
import android.media.projection.MediaProjection;
|
||||
import android.media.projection.MediaProjectionManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
import android.os.ServiceManager;
|
||||
import android.provider.MediaStore;
|
||||
import android.provider.Settings;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.util.Size;
|
||||
import android.view.Surface;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.android.systemui.R;
|
||||
import com.android.systemui.dagger.qualifiers.LongRunning;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
@@ -66,13 +52,15 @@ import javax.inject.Inject;
|
||||
public class RecordingService extends Service implements MediaRecorder.OnInfoListener {
|
||||
public static final int REQUEST_CODE = 2;
|
||||
|
||||
private static final int NOTIFICATION_ID = 1;
|
||||
private static final int NOTIFICATION_RECORDING_ID = 4274;
|
||||
private static final int NOTIFICATION_PROCESSING_ID = 4275;
|
||||
private static final int NOTIFICATION_VIEW_ID = 4273;
|
||||
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_AUDIO_SOURCE = "extra_useAudio";
|
||||
private static final String EXTRA_SHOW_TAPS = "extra_showTaps";
|
||||
|
||||
private static final String ACTION_START = "com.android.systemui.screenrecord.START";
|
||||
@@ -80,29 +68,19 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
|
||||
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 int VIDEO_BIT_RATE = 10000000;
|
||||
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 int MAX_DURATION_MS = 60 * 60 * 1000;
|
||||
private static final long MAX_FILESIZE_BYTES = 5000000000L;
|
||||
|
||||
private final RecordingController mController;
|
||||
private MediaProjection mMediaProjection;
|
||||
private Surface mInputSurface;
|
||||
private VirtualDisplay mVirtualDisplay;
|
||||
private MediaRecorder mMediaRecorder;
|
||||
private Notification.Builder mRecordingNotificationBuilder;
|
||||
|
||||
private boolean mUseAudio;
|
||||
private ScreenRecordingAudioSource mAudioSource;
|
||||
private boolean mShowTaps;
|
||||
private boolean mOriginalShowTaps;
|
||||
private File mTempFile;
|
||||
private ScreenMediaRecorder mRecorder;
|
||||
private final Executor mLongExecutor;
|
||||
|
||||
@Inject
|
||||
public RecordingService(RecordingController controller) {
|
||||
public RecordingService(RecordingController controller, @LongRunning Executor executor) {
|
||||
mController = controller;
|
||||
mLongExecutor = executor;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,16 +91,16 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
|
||||
* 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 audioSource The ordinal value of the audio source
|
||||
* {@link com.android.systemui.screenrecord.ScreenRecordingAudioSource}
|
||||
* @param showTaps True to make touches visible while recording
|
||||
*/
|
||||
public static Intent getStartIntent(Context context, int resultCode, Intent data,
|
||||
boolean useAudio, boolean showTaps) {
|
||||
public static Intent getStartIntent(Context context, int resultCode,
|
||||
int audioSource, 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_AUDIO_SOURCE, audioSource)
|
||||
.putExtra(EXTRA_SHOW_TAPS, showTaps);
|
||||
}
|
||||
|
||||
@@ -139,36 +117,31 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
|
||||
|
||||
switch (action) {
|
||||
case ACTION_START:
|
||||
mUseAudio = intent.getBooleanExtra(EXTRA_USE_AUDIO, false);
|
||||
mAudioSource = ScreenRecordingAudioSource
|
||||
.values()[intent.getIntExtra(EXTRA_AUDIO_SOURCE, 0)];
|
||||
Log.d(TAG, "recording with audio source" + mAudioSource);
|
||||
mShowTaps = intent.getBooleanExtra(EXTRA_SHOW_TAPS, false);
|
||||
try {
|
||||
IBinder b = ServiceManager.getService(MEDIA_PROJECTION_SERVICE);
|
||||
IMediaProjectionManager mediaService =
|
||||
IMediaProjectionManager.Stub.asInterface(b);
|
||||
IMediaProjection proj = mediaService.createProjection(getUserId(),
|
||||
getPackageName(),
|
||||
MediaProjectionManager.TYPE_SCREEN_CAPTURE, false);
|
||||
IBinder projection = proj.asBinder();
|
||||
if (projection == null) {
|
||||
Log.e(TAG, "Projection was null");
|
||||
Toast.makeText(this, R.string.screenrecord_start_error, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
return Service.START_NOT_STICKY;
|
||||
}
|
||||
mMediaProjection = new MediaProjection(getApplicationContext(),
|
||||
IMediaProjection.Stub.asInterface(projection));
|
||||
startRecording();
|
||||
} catch (RemoteException e) {
|
||||
e.printStackTrace();
|
||||
Toast.makeText(this, R.string.screenrecord_start_error, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
return Service.START_NOT_STICKY;
|
||||
}
|
||||
|
||||
mOriginalShowTaps = Settings.System.getInt(
|
||||
getApplicationContext().getContentResolver(),
|
||||
Settings.System.SHOW_TOUCHES, 0) != 0;
|
||||
|
||||
setTapsVisible(mShowTaps);
|
||||
|
||||
mRecorder = new ScreenMediaRecorder(
|
||||
getApplicationContext(),
|
||||
getUserId(),
|
||||
mAudioSource,
|
||||
this
|
||||
);
|
||||
startRecording();
|
||||
break;
|
||||
|
||||
case ACTION_STOP:
|
||||
stopRecording();
|
||||
notificationManager.cancel(NOTIFICATION_RECORDING_ID);
|
||||
saveRecording(notificationManager);
|
||||
stopSelf();
|
||||
break;
|
||||
|
||||
case ACTION_SHARE:
|
||||
@@ -183,10 +156,10 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
|
||||
sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
|
||||
|
||||
// Remove notification
|
||||
notificationManager.cancel(NOTIFICATION_ID);
|
||||
notificationManager.cancel(NOTIFICATION_RECORDING_ID);
|
||||
|
||||
startActivity(Intent.createChooser(shareIntent, shareLabel)
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
|
||||
break;
|
||||
case ACTION_DELETE:
|
||||
// Close quick shade
|
||||
@@ -202,7 +175,7 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
|
||||
Toast.LENGTH_LONG).show();
|
||||
|
||||
// Remove notification
|
||||
notificationManager.cancel(NOTIFICATION_ID);
|
||||
notificationManager.cancel(NOTIFICATION_RECORDING_ID);
|
||||
Log.d(TAG, "Deleted recording " + uri);
|
||||
break;
|
||||
}
|
||||
@@ -224,70 +197,15 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
|
||||
*/
|
||||
private void startRecording() {
|
||||
try {
|
||||
File cacheDir = getCacheDir();
|
||||
cacheDir.mkdirs();
|
||||
mTempFile = File.createTempFile("temp", ".mp4", cacheDir);
|
||||
Log.d(TAG, "Writing video output to: " + mTempFile.getAbsolutePath());
|
||||
|
||||
mOriginalShowTaps = 1 == Settings.System.getInt(
|
||||
getApplicationContext().getContentResolver(),
|
||||
Settings.System.SHOW_TOUCHES, 0);
|
||||
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 = new DisplayMetrics();
|
||||
WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
|
||||
wm.getDefaultDisplay().getRealMetrics(metrics);
|
||||
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);
|
||||
mMediaRecorder.setMaxDuration(MAX_DURATION_MS);
|
||||
mMediaRecorder.setMaxFileSize(MAX_FILESIZE_BYTES);
|
||||
|
||||
// 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.setOnInfoListener(this);
|
||||
mMediaRecorder.start();
|
||||
mRecorder.start();
|
||||
mController.updateState(true);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Error starting screen recording: " + e.getMessage());
|
||||
createRecordingNotification();
|
||||
} catch (IOException | RemoteException e) {
|
||||
Toast.makeText(this,
|
||||
R.string.screenrecord_start_error, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
createRecordingNotification();
|
||||
}
|
||||
|
||||
private void createRecordingNotification() {
|
||||
@@ -306,7 +224,7 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
|
||||
extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME,
|
||||
res.getString(R.string.screenrecord_name));
|
||||
|
||||
String notificationTitle = mUseAudio
|
||||
String notificationTitle = mAudioSource == ScreenRecordingAudioSource.NONE
|
||||
? res.getString(R.string.screenrecord_ongoing_screen_and_audio)
|
||||
: res.getString(R.string.screenrecord_ongoing_screen_only);
|
||||
|
||||
@@ -323,9 +241,10 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
|
||||
this, REQUEST_CODE, getStopIntent(this),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
.addExtras(extras);
|
||||
notificationManager.notify(NOTIFICATION_ID, mRecordingNotificationBuilder.build());
|
||||
notificationManager.notify(NOTIFICATION_RECORDING_ID,
|
||||
mRecordingNotificationBuilder.build());
|
||||
Notification notification = mRecordingNotificationBuilder.build();
|
||||
startForeground(NOTIFICATION_ID, notification);
|
||||
startForeground(NOTIFICATION_RECORDING_ID, notification);
|
||||
}
|
||||
|
||||
private Notification createSaveNotification(Uri uri) {
|
||||
@@ -392,47 +311,38 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis
|
||||
|
||||
private void stopRecording() {
|
||||
setTapsVisible(mOriginalShowTaps);
|
||||
mMediaRecorder.stop();
|
||||
mMediaRecorder.release();
|
||||
mMediaRecorder = null;
|
||||
mMediaProjection.stop();
|
||||
mMediaProjection = null;
|
||||
mInputSurface.release();
|
||||
mVirtualDisplay.release();
|
||||
stopSelf();
|
||||
mRecorder.end();
|
||||
mController.updateState(false);
|
||||
}
|
||||
|
||||
private void saveRecording(NotificationManager notificationManager) {
|
||||
String fileName = new SimpleDateFormat("'screen-'yyyyMMdd-HHmmss'.mp4'")
|
||||
.format(new Date());
|
||||
Resources res = getApplicationContext().getResources();
|
||||
String notificationTitle = mAudioSource == ScreenRecordingAudioSource.NONE
|
||||
? res.getString(R.string.screenrecord_ongoing_screen_only)
|
||||
: res.getString(R.string.screenrecord_ongoing_screen_and_audio);
|
||||
Notification.Builder builder = new Notification.Builder(getApplicationContext(), CHANNEL_ID)
|
||||
.setContentTitle(notificationTitle)
|
||||
.setContentText(
|
||||
getResources().getString(R.string.screenrecord_background_processing_label))
|
||||
.setSmallIcon(R.drawable.ic_screenrecord);
|
||||
notificationManager.notify(NOTIFICATION_PROCESSING_ID, builder.build());
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(MediaStore.Video.Media.DISPLAY_NAME, fileName);
|
||||
values.put(MediaStore.Video.Media.MIME_TYPE, "video/mp4");
|
||||
values.put(MediaStore.Video.Media.DATE_ADDED, System.currentTimeMillis());
|
||||
values.put(MediaStore.Video.Media.DATE_TAKEN, System.currentTimeMillis());
|
||||
|
||||
ContentResolver resolver = getContentResolver();
|
||||
Uri collectionUri = MediaStore.Video.Media.getContentUri(
|
||||
MediaStore.VOLUME_EXTERNAL_PRIMARY);
|
||||
Uri itemUri = resolver.insert(collectionUri, values);
|
||||
|
||||
try {
|
||||
// Add to the mediastore
|
||||
OutputStream os = resolver.openOutputStream(itemUri, "w");
|
||||
Files.copy(mTempFile.toPath(), os);
|
||||
os.close();
|
||||
|
||||
Notification notification = createSaveNotification(itemUri);
|
||||
notificationManager.notify(NOTIFICATION_ID, notification);
|
||||
|
||||
mTempFile.delete();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Error saving screen recording: " + e.getMessage());
|
||||
Toast.makeText(this, R.string.screenrecord_delete_error, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
}
|
||||
mLongExecutor.execute(() -> {
|
||||
try {
|
||||
Log.d(TAG, "saving recording");
|
||||
Notification notification = createSaveNotification(mRecorder.save());
|
||||
if (!mController.isRecording()) {
|
||||
Log.d(TAG, "showing saved notification");
|
||||
notificationManager.notify(NOTIFICATION_VIEW_ID, notification);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Error saving screen recording: " + e.getMessage());
|
||||
Toast.makeText(this, R.string.screenrecord_delete_error, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
} finally {
|
||||
notificationManager.cancel(NOTIFICATION_PROCESSING_ID);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setTapsVisible(boolean turnOn) {
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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.content.Context;
|
||||
import android.media.AudioAttributes;
|
||||
import android.media.AudioFormat;
|
||||
import android.media.AudioPlaybackCaptureConfiguration;
|
||||
import android.media.AudioRecord;
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaCodecInfo;
|
||||
import android.media.MediaFormat;
|
||||
import android.media.MediaMuxer;
|
||||
import android.media.MediaRecorder;
|
||||
import android.media.projection.MediaProjection;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* Recording internal audio
|
||||
*/
|
||||
public class ScreenInternalAudioRecorder {
|
||||
private static String TAG = "ScreenAudioRecorder";
|
||||
private static final int TIMEOUT = 500;
|
||||
private final Context mContext;
|
||||
private AudioRecord mAudioRecord;
|
||||
private AudioRecord mAudioRecordMic;
|
||||
private Config mConfig = new Config();
|
||||
private Thread mThread;
|
||||
private MediaProjection mMediaProjection;
|
||||
private MediaCodec mCodec;
|
||||
private long mPresentationTime;
|
||||
private long mTotalBytes;
|
||||
private MediaMuxer mMuxer;
|
||||
private String mOutFile;
|
||||
private boolean mMic;
|
||||
|
||||
private int mTrackId = -1;
|
||||
|
||||
public ScreenInternalAudioRecorder(String outFile, Context context,
|
||||
MediaProjection mp, boolean includeMicInput) throws IOException {
|
||||
mMic = includeMicInput;
|
||||
mOutFile = outFile;
|
||||
mMuxer = new MediaMuxer(outFile, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
|
||||
mContext = context;
|
||||
mMediaProjection = mp;
|
||||
Log.d(TAG, "creating audio file " + outFile);
|
||||
setupSimple();
|
||||
}
|
||||
/**
|
||||
* Audio recoding configuration
|
||||
*/
|
||||
public static class Config {
|
||||
public int channelOutMask = AudioFormat.CHANNEL_OUT_MONO;
|
||||
public int channelInMask = AudioFormat.CHANNEL_IN_MONO;
|
||||
public int encoding = AudioFormat.ENCODING_PCM_16BIT;
|
||||
public int sampleRate = 44100;
|
||||
public int bitRate = 196000;
|
||||
public int bufferSizeBytes = 1 << 17;
|
||||
public boolean privileged = true;
|
||||
public boolean legacy_app_looback = false;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "channelMask=" + channelOutMask
|
||||
+ "\n encoding=" + encoding
|
||||
+ "\n sampleRate=" + sampleRate
|
||||
+ "\n bufferSize=" + bufferSizeBytes
|
||||
+ "\n privileged=" + privileged
|
||||
+ "\n legacy app looback=" + legacy_app_looback;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void setupSimple() throws IOException {
|
||||
int size = AudioRecord.getMinBufferSize(
|
||||
mConfig.sampleRate, mConfig.channelInMask,
|
||||
mConfig.encoding) * 2;
|
||||
|
||||
Log.d(TAG, "audio buffer size: " + size);
|
||||
|
||||
AudioFormat format = new AudioFormat.Builder()
|
||||
.setEncoding(mConfig.encoding)
|
||||
.setSampleRate(mConfig.sampleRate)
|
||||
.setChannelMask(mConfig.channelOutMask)
|
||||
.build();
|
||||
|
||||
AudioPlaybackCaptureConfiguration playbackConfig =
|
||||
new AudioPlaybackCaptureConfiguration.Builder(mMediaProjection)
|
||||
.addMatchingUsage(AudioAttributes.USAGE_MEDIA)
|
||||
.addMatchingUsage(AudioAttributes.USAGE_UNKNOWN)
|
||||
.addMatchingUsage(AudioAttributes.USAGE_GAME)
|
||||
.build();
|
||||
|
||||
mAudioRecord = new AudioRecord.Builder()
|
||||
.setAudioFormat(format)
|
||||
.setAudioPlaybackCaptureConfig(playbackConfig)
|
||||
.build();
|
||||
|
||||
if (mMic) {
|
||||
mAudioRecordMic = new AudioRecord(MediaRecorder.AudioSource.VOICE_COMMUNICATION,
|
||||
mConfig.sampleRate, AudioFormat.CHANNEL_IN_MONO, mConfig.encoding, size);
|
||||
}
|
||||
|
||||
mCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC);
|
||||
MediaFormat medFormat = MediaFormat.createAudioFormat(
|
||||
MediaFormat.MIMETYPE_AUDIO_AAC, mConfig.sampleRate, 1);
|
||||
medFormat.setInteger(MediaFormat.KEY_AAC_PROFILE,
|
||||
MediaCodecInfo.CodecProfileLevel.AACObjectLC);
|
||||
medFormat.setInteger(MediaFormat.KEY_BIT_RATE, mConfig.bitRate);
|
||||
medFormat.setInteger(MediaFormat.KEY_PCM_ENCODING, mConfig.encoding);
|
||||
mCodec.configure(medFormat,
|
||||
null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
|
||||
|
||||
mThread = new Thread(() -> {
|
||||
short[] bufferInternal = null;
|
||||
short[] bufferMic = null;
|
||||
byte[] buffer = null;
|
||||
|
||||
if (mMic) {
|
||||
bufferInternal = new short[size / 2];
|
||||
bufferMic = new short[size / 2];
|
||||
} else {
|
||||
buffer = new byte[size];
|
||||
}
|
||||
|
||||
while (true) {
|
||||
int readBytes = 0;
|
||||
int readShortsInternal = 0;
|
||||
int readShortsMic = 0;
|
||||
if (mMic) {
|
||||
readShortsInternal = mAudioRecord.read(bufferInternal, 0,
|
||||
bufferInternal.length);
|
||||
readShortsMic = mAudioRecordMic.read(bufferMic, 0, bufferMic.length);
|
||||
readBytes = Math.min(readShortsInternal, readShortsMic) * 2;
|
||||
buffer = addAndConvertBuffers(bufferInternal, readShortsInternal, bufferMic,
|
||||
readShortsMic);
|
||||
} else {
|
||||
readBytes = mAudioRecord.read(buffer, 0, buffer.length);
|
||||
}
|
||||
|
||||
//exit the loop when at end of stream
|
||||
if (readBytes < 0) {
|
||||
Log.e(TAG, "read error " + readBytes +
|
||||
", shorts internal: " + readShortsInternal +
|
||||
", shorts mic: " + readShortsMic);
|
||||
break;
|
||||
}
|
||||
encode(buffer, readBytes);
|
||||
}
|
||||
endStream();
|
||||
});
|
||||
}
|
||||
|
||||
private byte[] addAndConvertBuffers(short[] a1, int a1Limit, short[] a2, int a2Limit) {
|
||||
int size = Math.max(a1Limit, a2Limit);
|
||||
if (size < 0) return new byte[0];
|
||||
byte[] buff = new byte[size * 2];
|
||||
for (int i = 0; i < size; i++) {
|
||||
int sum;
|
||||
if (i > a1Limit) {
|
||||
sum = a2[i];
|
||||
} else if (i > a2Limit) {
|
||||
sum = a1[i];
|
||||
} else {
|
||||
sum = (int) a1[i] + (int) a2[i];
|
||||
}
|
||||
|
||||
if (sum > Short.MAX_VALUE) sum = Short.MAX_VALUE;
|
||||
if (sum < Short.MIN_VALUE) sum = Short.MIN_VALUE;
|
||||
int byteIndex = i * 2;
|
||||
buff[byteIndex] = (byte) (sum & 0xff);
|
||||
buff[byteIndex + 1] = (byte) ((sum >> 8) & 0xff);
|
||||
}
|
||||
return buff;
|
||||
}
|
||||
|
||||
private void encode(byte[] buffer, int readBytes) {
|
||||
int offset = 0;
|
||||
while (readBytes > 0) {
|
||||
int totalBytesRead = 0;
|
||||
int bufferIndex = mCodec.dequeueInputBuffer(TIMEOUT);
|
||||
if (bufferIndex < 0) {
|
||||
writeOutput();
|
||||
return;
|
||||
}
|
||||
ByteBuffer buff = mCodec.getInputBuffer(bufferIndex);
|
||||
buff.clear();
|
||||
int bufferSize = buff.capacity();
|
||||
int bytesToRead = readBytes > bufferSize ? bufferSize : readBytes;
|
||||
totalBytesRead += bytesToRead;
|
||||
readBytes -= bytesToRead;
|
||||
buff.put(buffer, offset, bytesToRead);
|
||||
offset += bytesToRead;
|
||||
mCodec.queueInputBuffer(bufferIndex, 0, bytesToRead, mPresentationTime, 0);
|
||||
mTotalBytes += totalBytesRead;
|
||||
mPresentationTime = 1000000L * (mTotalBytes / 2) / mConfig.sampleRate;
|
||||
|
||||
writeOutput();
|
||||
}
|
||||
}
|
||||
|
||||
private void endStream() {
|
||||
int bufferIndex = mCodec.dequeueInputBuffer(TIMEOUT);
|
||||
mCodec.queueInputBuffer(bufferIndex, 0, 0, mPresentationTime,
|
||||
MediaCodec.BUFFER_FLAG_END_OF_STREAM);
|
||||
writeOutput();
|
||||
}
|
||||
|
||||
private void writeOutput() {
|
||||
while (true) {
|
||||
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
||||
int bufferIndex = mCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT);
|
||||
if (bufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
|
||||
mTrackId = mMuxer.addTrack(mCodec.getOutputFormat());
|
||||
mMuxer.start();
|
||||
continue;
|
||||
}
|
||||
if (bufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
|
||||
break;
|
||||
}
|
||||
if (mTrackId < 0) return;
|
||||
ByteBuffer buff = mCodec.getOutputBuffer(bufferIndex);
|
||||
|
||||
if (!((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0
|
||||
&& bufferInfo.size != 0)) {
|
||||
mMuxer.writeSampleData(mTrackId, buff, bufferInfo);
|
||||
}
|
||||
mCodec.releaseOutputBuffer(bufferIndex, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* start recording
|
||||
*/
|
||||
public void start() {
|
||||
if (mThread != null) {
|
||||
Log.e(TAG, "a recording is being done in parallel or stop is not called");
|
||||
}
|
||||
mAudioRecord.startRecording();
|
||||
if (mMic) mAudioRecordMic.startRecording();
|
||||
Log.d(TAG, "channel count " + mAudioRecord.getChannelCount());
|
||||
mCodec.start();
|
||||
if (mAudioRecord.getRecordingState() != AudioRecord.RECORDSTATE_RECORDING) {
|
||||
Log.e(TAG, "Error starting audio recording");
|
||||
return;
|
||||
}
|
||||
mThread.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* end recording
|
||||
*/
|
||||
public void end() {
|
||||
mAudioRecord.stop();
|
||||
if (mMic) {
|
||||
mAudioRecordMic.stop();
|
||||
}
|
||||
mAudioRecord.release();
|
||||
if (mMic) {
|
||||
mAudioRecordMic.release();
|
||||
}
|
||||
try {
|
||||
mThread.join();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
mCodec.stop();
|
||||
mCodec.release();
|
||||
mMuxer.stop();
|
||||
mMuxer.release();
|
||||
mThread = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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 static android.content.Context.MEDIA_PROJECTION_SERVICE;
|
||||
|
||||
import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.INTERNAL;
|
||||
import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.MIC;
|
||||
import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.MIC_AND_INTERNAL;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.hardware.display.DisplayManager;
|
||||
import android.hardware.display.VirtualDisplay;
|
||||
import android.media.MediaMuxer;
|
||||
import android.media.MediaRecorder;
|
||||
import android.media.projection.IMediaProjection;
|
||||
import android.media.projection.IMediaProjectionManager;
|
||||
import android.media.projection.MediaProjection;
|
||||
import android.media.projection.MediaProjectionManager;
|
||||
import android.net.Uri;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
import android.os.ServiceManager;
|
||||
import android.provider.MediaStore;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.view.Surface;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Recording screen and mic/internal audio
|
||||
*/
|
||||
public class ScreenMediaRecorder {
|
||||
private static final int TOTAL_NUM_TRACKS = 1;
|
||||
private static final int VIDEO_BIT_RATE = 10000000;
|
||||
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 int MAX_DURATION_MS = 60 * 60 * 1000;
|
||||
private static final long MAX_FILESIZE_BYTES = 5000000000L;
|
||||
private static final String TAG = "ScreenMediaRecorder";
|
||||
|
||||
|
||||
private File mTempVideoFile;
|
||||
private File mTempAudioFile;
|
||||
private MediaProjection mMediaProjection;
|
||||
private Surface mInputSurface;
|
||||
private VirtualDisplay mVirtualDisplay;
|
||||
private MediaRecorder mMediaRecorder;
|
||||
private int mUser;
|
||||
private ScreenRecordingMuxer mMuxer;
|
||||
private ScreenInternalAudioRecorder mAudio;
|
||||
private ScreenRecordingAudioSource mAudioSource;
|
||||
|
||||
private Context mContext;
|
||||
MediaRecorder.OnInfoListener mListener;
|
||||
|
||||
public ScreenMediaRecorder(Context context,
|
||||
int user, ScreenRecordingAudioSource audioSource,
|
||||
MediaRecorder.OnInfoListener listener) {
|
||||
mContext = context;
|
||||
mUser = user;
|
||||
mListener = listener;
|
||||
mAudioSource = audioSource;
|
||||
}
|
||||
|
||||
private void prepare() throws IOException, RemoteException {
|
||||
//Setup media projection
|
||||
IBinder b = ServiceManager.getService(MEDIA_PROJECTION_SERVICE);
|
||||
IMediaProjectionManager mediaService =
|
||||
IMediaProjectionManager.Stub.asInterface(b);
|
||||
IMediaProjection proj = null;
|
||||
proj = mediaService.createProjection(mUser, mContext.getPackageName(),
|
||||
MediaProjectionManager.TYPE_SCREEN_CAPTURE, false);
|
||||
IBinder projection = proj.asBinder();
|
||||
mMediaProjection = new MediaProjection(mContext,
|
||||
IMediaProjection.Stub.asInterface(projection));
|
||||
|
||||
File cacheDir = mContext.getCacheDir();
|
||||
cacheDir.mkdirs();
|
||||
mTempVideoFile = File.createTempFile("temp", ".mp4", cacheDir);
|
||||
|
||||
// Set up media recorder
|
||||
mMediaRecorder = new MediaRecorder();
|
||||
|
||||
// Set up audio source
|
||||
if (mAudioSource == MIC) {
|
||||
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
|
||||
}
|
||||
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
|
||||
|
||||
mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
|
||||
|
||||
|
||||
// Set up video
|
||||
DisplayMetrics metrics = new DisplayMetrics();
|
||||
WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
|
||||
wm.getDefaultDisplay().getRealMetrics(metrics);
|
||||
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);
|
||||
mMediaRecorder.setMaxDuration(MAX_DURATION_MS);
|
||||
mMediaRecorder.setMaxFileSize(MAX_FILESIZE_BYTES);
|
||||
|
||||
// Set up audio
|
||||
if (mAudioSource == MIC) {
|
||||
mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.HE_AAC);
|
||||
mMediaRecorder.setAudioChannels(TOTAL_NUM_TRACKS);
|
||||
mMediaRecorder.setAudioEncodingBitRate(AUDIO_BIT_RATE);
|
||||
mMediaRecorder.setAudioSamplingRate(AUDIO_SAMPLE_RATE);
|
||||
}
|
||||
|
||||
mMediaRecorder.setOutputFile(mTempVideoFile);
|
||||
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.setOnInfoListener(mListener);
|
||||
if (mAudioSource == INTERNAL ||
|
||||
mAudioSource == MIC_AND_INTERNAL) {
|
||||
mTempAudioFile = File.createTempFile("temp", ".aac",
|
||||
mContext.getCacheDir());
|
||||
mAudio = new ScreenInternalAudioRecorder(mTempAudioFile.getAbsolutePath(), mContext,
|
||||
mMediaProjection, mAudioSource == MIC_AND_INTERNAL);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Start screen recording
|
||||
*/
|
||||
void start() throws IOException, RemoteException {
|
||||
Log.d(TAG, "start recording");
|
||||
prepare();
|
||||
mMediaRecorder.start();
|
||||
recordInternalAudio();
|
||||
}
|
||||
|
||||
/**
|
||||
* End screen recording
|
||||
*/
|
||||
void end() {
|
||||
mMediaRecorder.stop();
|
||||
mMediaProjection.stop();
|
||||
mMediaRecorder.release();
|
||||
mMediaRecorder = null;
|
||||
mMediaProjection = null;
|
||||
mInputSurface.release();
|
||||
mVirtualDisplay.release();
|
||||
stopInternalAudioRecording();
|
||||
|
||||
Log.d(TAG, "end recording");
|
||||
}
|
||||
|
||||
private void stopInternalAudioRecording() {
|
||||
if (mAudioSource == INTERNAL || mAudioSource == MIC_AND_INTERNAL) {
|
||||
mAudio.end();
|
||||
mAudio = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void recordInternalAudio() {
|
||||
if (mAudioSource == INTERNAL || mAudioSource == MIC_AND_INTERNAL) {
|
||||
mAudio.start();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store recorded video
|
||||
*/
|
||||
Uri save() throws IOException {
|
||||
String fileName = new SimpleDateFormat("'screen-'yyyyMMdd-HHmmss'.mp4'")
|
||||
.format(new Date());
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(MediaStore.Video.Media.DISPLAY_NAME, fileName);
|
||||
values.put(MediaStore.Video.Media.MIME_TYPE, "video/mp4");
|
||||
values.put(MediaStore.Video.Media.DATE_ADDED, System.currentTimeMillis());
|
||||
values.put(MediaStore.Video.Media.DATE_TAKEN, System.currentTimeMillis());
|
||||
|
||||
ContentResolver resolver = mContext.getContentResolver();
|
||||
Uri collectionUri = MediaStore.Video.Media.getContentUri(
|
||||
MediaStore.VOLUME_EXTERNAL_PRIMARY);
|
||||
Uri itemUri = resolver.insert(collectionUri, values);
|
||||
|
||||
Log.d(TAG, itemUri.toString());
|
||||
if (mAudioSource == MIC_AND_INTERNAL || mAudioSource == INTERNAL) {
|
||||
try {
|
||||
Log.d(TAG, "muxing recording");
|
||||
File file = File.createTempFile("temp", ".mp4",
|
||||
mContext.getCacheDir());
|
||||
mMuxer = new ScreenRecordingMuxer(MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4,
|
||||
file.getAbsolutePath(),
|
||||
mTempVideoFile.getAbsolutePath(),
|
||||
mTempAudioFile.getAbsolutePath());
|
||||
mMuxer.mux();
|
||||
mTempVideoFile.delete();
|
||||
mTempVideoFile = file;
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "muxing recording " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
// Add to the mediastore
|
||||
OutputStream os = resolver.openOutputStream(itemUri, "w");
|
||||
Files.copy(mTempVideoFile.toPath(), os);
|
||||
os.close();
|
||||
mTempVideoFile.delete();
|
||||
if (mTempAudioFile != null) mTempAudioFile.delete();
|
||||
return itemUri;
|
||||
}
|
||||
}
|
||||
@@ -16,17 +16,30 @@
|
||||
|
||||
package com.android.systemui.screenrecord;
|
||||
|
||||
import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.INTERNAL;
|
||||
import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.MIC;
|
||||
import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.MIC_AND_INTERNAL;
|
||||
import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.NONE;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.PendingIntent;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.Window;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Button;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.Switch;
|
||||
|
||||
import com.android.systemui.R;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
/**
|
||||
@@ -35,10 +48,15 @@ import javax.inject.Inject;
|
||||
public class ScreenRecordDialog extends Activity {
|
||||
private static final long DELAY_MS = 3000;
|
||||
private static final long INTERVAL_MS = 1000;
|
||||
private static final String TAG = "ScreenRecordDialog";
|
||||
|
||||
private final RecordingController mController;
|
||||
private Switch mAudioSwitch;
|
||||
private Switch mTapsSwitch;
|
||||
private Switch mAudioSwitch;
|
||||
private Spinner mOptions;
|
||||
private List<ScreenRecordingAudioSource> mModes;
|
||||
private int mSelected;
|
||||
|
||||
|
||||
@Inject
|
||||
public ScreenRecordDialog(RecordingController controller) {
|
||||
@@ -68,17 +86,32 @@ public class ScreenRecordDialog extends Activity {
|
||||
finish();
|
||||
});
|
||||
|
||||
mModes = new ArrayList<>();
|
||||
mModes.add(INTERNAL);
|
||||
mModes.add(MIC);
|
||||
mModes.add(MIC_AND_INTERNAL);
|
||||
|
||||
mAudioSwitch = findViewById(R.id.screenrecord_audio_switch);
|
||||
mTapsSwitch = findViewById(R.id.screenrecord_taps_switch);
|
||||
mOptions = findViewById(R.id.screen_recording_options);
|
||||
ArrayAdapter a = new ScreenRecordingAdapter(getApplicationContext(),
|
||||
android.R.layout.simple_spinner_dropdown_item,
|
||||
mModes);
|
||||
a.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
mOptions.setAdapter(a);
|
||||
|
||||
}
|
||||
|
||||
private void requestScreenCapture() {
|
||||
boolean useAudio = mAudioSwitch.isChecked();
|
||||
boolean showTaps = mTapsSwitch.isChecked();
|
||||
ScreenRecordingAudioSource audioMode = mAudioSwitch.isChecked()
|
||||
? (ScreenRecordingAudioSource) mOptions.getSelectedItem()
|
||||
: NONE;
|
||||
PendingIntent startIntent = PendingIntent.getForegroundService(this,
|
||||
RecordingService.REQUEST_CODE,
|
||||
RecordingService.getStartIntent(
|
||||
ScreenRecordDialog.this, RESULT_OK, null, useAudio, showTaps),
|
||||
ScreenRecordDialog.this, RESULT_OK,
|
||||
audioMode.ordinal(), showTaps),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
PendingIntent stopIntent = PendingIntent.getService(this,
|
||||
RecordingService.REQUEST_CODE,
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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 static com.android.systemui.screenrecord.ScreenRecordingAudioSource.INTERNAL;
|
||||
import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.MIC;
|
||||
import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.MIC_AND_INTERNAL;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.android.systemui.R;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Screen recording view adapter
|
||||
*/
|
||||
public class ScreenRecordingAdapter extends ArrayAdapter<ScreenRecordingAudioSource> {
|
||||
private LinearLayout mSelectedMic;
|
||||
private LinearLayout mSelectedInternal;
|
||||
private LinearLayout mSelectedMicAndInternal;
|
||||
private LinearLayout mMicOption;
|
||||
private LinearLayout mMicAndInternalOption;
|
||||
private LinearLayout mInternalOption;
|
||||
|
||||
public ScreenRecordingAdapter(Context context, int resource,
|
||||
List<ScreenRecordingAudioSource> objects) {
|
||||
super(context, resource, objects);
|
||||
initViews();
|
||||
}
|
||||
|
||||
private void initViews() {
|
||||
mSelectedInternal = getSelected(R.string.screenrecord_device_audio_label);
|
||||
mSelectedMic = getSelected(R.string.screenrecord_mic_label);
|
||||
mSelectedMicAndInternal = getSelected(R.string.screenrecord_device_audio_and_mic_label);
|
||||
|
||||
mMicOption = getOption(R.string.screenrecord_mic_label, Resources.ID_NULL);
|
||||
mMicOption.removeViewAt(1);
|
||||
|
||||
mMicAndInternalOption = getOption(
|
||||
R.string.screenrecord_device_audio_and_mic_label, Resources.ID_NULL);
|
||||
mMicAndInternalOption.removeViewAt(1);
|
||||
|
||||
mInternalOption = getOption(R.string.screenrecord_device_audio_label,
|
||||
R.string.screenrecord_device_audio_description);
|
||||
}
|
||||
|
||||
private LinearLayout getOption(int label, int description) {
|
||||
LayoutInflater inflater = (LayoutInflater) getContext()
|
||||
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
LinearLayout layout = (LinearLayout) inflater
|
||||
.inflate(R.layout.screen_record_dialog_audio_source, null, false);
|
||||
((TextView) layout.findViewById(R.id.screen_recording_dialog_source_text))
|
||||
.setText(label);
|
||||
if (description != Resources.ID_NULL)
|
||||
((TextView) layout.findViewById(R.id.screen_recording_dialog_source_description))
|
||||
.setText(description);
|
||||
return layout;
|
||||
}
|
||||
|
||||
private LinearLayout getSelected(int label) {
|
||||
LayoutInflater inflater = LayoutInflater.from(getContext());
|
||||
LinearLayout layout = (LinearLayout) inflater
|
||||
.inflate(R.layout.screen_record_dialog_audio_source_selected, null, false);
|
||||
((TextView) layout.findViewById(R.id.screen_recording_dialog_source_text))
|
||||
.setText(label);
|
||||
return layout;
|
||||
}
|
||||
|
||||
private void setDescription(LinearLayout layout, int description) {
|
||||
if (description != Resources.ID_NULL) {
|
||||
((TextView) layout.getChildAt(1)).setText(description);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getDropDownView(int position, View convertView, ViewGroup parent) {
|
||||
switch (getItem(position)) {
|
||||
case INTERNAL:
|
||||
return mInternalOption;
|
||||
case MIC_AND_INTERNAL:
|
||||
return mMicAndInternalOption;
|
||||
case MIC:
|
||||
return mMicOption;
|
||||
default:
|
||||
return super.getDropDownView(position, convertView, parent);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
switch (getItem(position)) {
|
||||
case INTERNAL:
|
||||
return mSelectedInternal;
|
||||
case MIC_AND_INTERNAL:
|
||||
return mSelectedMicAndInternal;
|
||||
case MIC:
|
||||
return mSelectedMic;
|
||||
default:
|
||||
return super.getView(position, convertView, parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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;
|
||||
|
||||
/**
|
||||
* Audio sources
|
||||
*/
|
||||
public enum ScreenRecordingAudioSource {
|
||||
NONE,
|
||||
INTERNAL,
|
||||
MIC,
|
||||
MIC_AND_INTERNAL;
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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.media.MediaCodec;
|
||||
import android.media.MediaExtractor;
|
||||
import android.media.MediaMuxer;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Mixing audio and video tracks
|
||||
*/
|
||||
public class ScreenRecordingMuxer {
|
||||
// size of a memory page for cache coherency
|
||||
private static final int BUFFER_SIZE = 1024 * 4096;
|
||||
private String[] mFiles;
|
||||
private String mOutFile;
|
||||
private int mFormat;
|
||||
private ArrayMap<Pair<MediaExtractor, Integer>, Integer> mExtractorIndexToMuxerIndex
|
||||
= new ArrayMap<>();
|
||||
private ArrayList<MediaExtractor> mExtractors = new ArrayList<>();
|
||||
|
||||
private static String TAG = "ScreenRecordingMuxer";
|
||||
public ScreenRecordingMuxer(@MediaMuxer.Format int format, String outfileName,
|
||||
String... inputFileNames) {
|
||||
mFiles = inputFileNames;
|
||||
mOutFile = outfileName;
|
||||
mFormat = format;
|
||||
Log.d(TAG, "out: " + mOutFile + " , in: " + mFiles[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* RUN IN THE BACKGROUND THREAD!
|
||||
*/
|
||||
public void mux() throws IOException {
|
||||
MediaMuxer muxer = null;
|
||||
muxer = new MediaMuxer(mOutFile, mFormat);
|
||||
// Add extractors
|
||||
for (String file: mFiles) {
|
||||
MediaExtractor extractor = new MediaExtractor();
|
||||
try {
|
||||
extractor.setDataSource(file);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "error creating extractor: " + file);
|
||||
e.printStackTrace();
|
||||
continue;
|
||||
}
|
||||
Log.d(TAG, file + " track count: " + extractor.getTrackCount());
|
||||
mExtractors.add(extractor);
|
||||
for (int i = 0; i < extractor.getTrackCount(); i++) {
|
||||
int muxId = muxer.addTrack(extractor.getTrackFormat(i));
|
||||
Log.d(TAG, "created extractor format" + extractor.getTrackFormat(i).toString());
|
||||
mExtractorIndexToMuxerIndex.put(Pair.create(extractor, i), muxId);
|
||||
}
|
||||
}
|
||||
|
||||
muxer.start();
|
||||
for (Pair<MediaExtractor, Integer> pair: mExtractorIndexToMuxerIndex.keySet()) {
|
||||
MediaExtractor extractor = pair.first;
|
||||
extractor.selectTrack(pair.second);
|
||||
int muxId = mExtractorIndexToMuxerIndex.get(pair);
|
||||
Log.d(TAG, "track format: " + extractor.getTrackFormat(pair.second));
|
||||
extractor.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC);
|
||||
ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
|
||||
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
|
||||
int offset;
|
||||
while (true) {
|
||||
offset = buffer.arrayOffset();
|
||||
info.size = extractor.readSampleData(buffer, offset);
|
||||
if (info.size < 0) break;
|
||||
info.presentationTimeUs = extractor.getSampleTime();
|
||||
info.flags = extractor.getSampleFlags();
|
||||
muxer.writeSampleData(muxId, buffer, info);
|
||||
extractor.advance();
|
||||
}
|
||||
}
|
||||
|
||||
for (MediaExtractor extractor: mExtractors) {
|
||||
extractor.release();
|
||||
}
|
||||
muxer.stop();
|
||||
muxer.release();
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import android.os.Looper;
|
||||
import android.os.Process;
|
||||
|
||||
import com.android.systemui.dagger.qualifiers.Background;
|
||||
import com.android.systemui.dagger.qualifiers.LongRunning;
|
||||
import com.android.systemui.dagger.qualifiers.Main;
|
||||
import com.android.systemui.dagger.qualifiers.UiBackground;
|
||||
|
||||
@@ -50,6 +51,17 @@ public abstract class ConcurrencyModule {
|
||||
return thread.getLooper();
|
||||
}
|
||||
|
||||
/** Long running tasks Looper */
|
||||
@Provides
|
||||
@Singleton
|
||||
@LongRunning
|
||||
public static Looper provideLongRunningLooper() {
|
||||
HandlerThread thread = new HandlerThread("SysUiLng",
|
||||
Process.THREAD_PRIORITY_BACKGROUND);
|
||||
thread.start();
|
||||
return thread.getLooper();
|
||||
}
|
||||
|
||||
/** Main Looper */
|
||||
@Provides
|
||||
@Main
|
||||
@@ -88,6 +100,16 @@ public abstract class ConcurrencyModule {
|
||||
return new ExecutorImpl(looper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a Long running Executor by default.
|
||||
*/
|
||||
@Provides
|
||||
@Singleton
|
||||
@LongRunning
|
||||
public static Executor provideLongRunningExecutor(@LongRunning Looper looper) {
|
||||
return new ExecutorImpl(looper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a Background-Thread Executor.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user