diff --git a/packages/SystemUI/res/layout/screen_record_dialog.xml b/packages/SystemUI/res/layout/screen_record_dialog.xml
index df576d83323f1..fd9936f6b8ea2 100644
--- a/packages/SystemUI/res/layout/screen_record_dialog.xml
+++ b/packages/SystemUI/res/layout/screen_record_dialog.xml
@@ -59,29 +59,16 @@
android:layout_gravity="center"
android:layout_weight="0"
android:layout_marginRight="@dimen/screenrecord_dialog_padding"/>
-
-
-
-
+ android:layout_height="48dp"
+ android:prompt="@string/screenrecord_audio_label"/>
@@ -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"/>
+
diff --git a/packages/SystemUI/res/layout/screen_record_dialog_audio_source.xml b/packages/SystemUI/res/layout/screen_record_dialog_audio_source.xml
new file mode 100644
index 0000000000000..af6f9bb827f66
--- /dev/null
+++ b/packages/SystemUI/res/layout/screen_record_dialog_audio_source.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/screen_record_dialog_audio_source_selected.xml b/packages/SystemUI/res/layout/screen_record_dialog_audio_source_selected.xml
new file mode 100644
index 0000000000000..fabe9e2d44531
--- /dev/null
+++ b/packages/SystemUI/res/layout/screen_record_dialog_audio_source_selected.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml
index 04640f418e975..8156e8dc9bf1c 100644
--- a/packages/SystemUI/res/values/ids.xml
+++ b/packages/SystemUI/res/values/ids.xml
@@ -144,5 +144,10 @@
+
+
+
+
+
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 023c77d5db14f..ec29622c9ba28 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -240,6 +240,8 @@
Screen Recorder
+
+ Processing screen recording
Ongoing notification for a screen record session
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/qualifiers/LongRunning.java b/packages/SystemUI/src/com/android/systemui/dagger/qualifiers/LongRunning.java
new file mode 100644
index 0000000000000..e90781b48f233
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dagger/qualifiers/LongRunning.java
@@ -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 {
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java
index ae0a1c4d98225..b253635e9bfa7 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java
@@ -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) {
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
index 390ac0969b218..cf098d52fa91b 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
@@ -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) {
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenInternalAudioRecorder.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenInternalAudioRecorder.java
new file mode 100644
index 0000000000000..752f4fddf24bd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenInternalAudioRecorder.java
@@ -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;
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java
new file mode 100644
index 0000000000000..c967648c544eb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java
@@ -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;
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java
index 26973d092209c..c247328078a7a 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java
@@ -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 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,
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordingAdapter.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordingAdapter.java
new file mode 100644
index 0000000000000..2e0e746594b4d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordingAdapter.java
@@ -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 {
+ 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 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);
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordingAudioSource.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordingAudioSource.java
new file mode 100644
index 0000000000000..ee11865a03a16
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordingAudioSource.java
@@ -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;
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordingMuxer.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordingMuxer.java
new file mode 100644
index 0000000000000..7ffcfd46a1a48
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordingMuxer.java
@@ -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, Integer> mExtractorIndexToMuxerIndex
+ = new ArrayMap<>();
+ private ArrayList 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 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();
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/concurrency/ConcurrencyModule.java b/packages/SystemUI/src/com/android/systemui/util/concurrency/ConcurrencyModule.java
index 8acfbf2b69961..7729965b56c4e 100644
--- a/packages/SystemUI/src/com/android/systemui/util/concurrency/ConcurrencyModule.java
+++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/ConcurrencyModule.java
@@ -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.
*/