Merge "[Partial Screensharing] Add a temporary entrypoint in SysUI screen recorder" into tm-qpr-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
5837f00409
@@ -148,6 +148,20 @@
|
|||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_weight="1"/>
|
android:layout_weight="1"/>
|
||||||
|
|
||||||
|
<!-- Temporary entrypoint for the partial screensharing used for teamfooding -->
|
||||||
|
<!-- TODO(b/236838395) remove this and use redesigned dialog -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/button_app"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="0"
|
||||||
|
android:layout_gravity="end"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:text="App"
|
||||||
|
style="@style/Widget.Dialog.Button.BorderButton" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/button_start"
|
android:id="@+id/button_start"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import android.media.projection.MediaProjectionManager.EXTRA_MEDIA_PROJECTION
|
|||||||
import android.os.Binder
|
import android.os.Binder
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
|
import android.os.ResultReceiver
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import com.android.internal.app.ChooserActivity
|
import com.android.internal.app.ChooserActivity
|
||||||
import com.android.internal.app.chooser.NotSelectableTargetInfo
|
import com.android.internal.app.chooser.NotSelectableTargetInfo
|
||||||
@@ -103,17 +104,42 @@ class MediaProjectionAppSelectorActivity @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun onTargetActivityLaunched(launchToken: IBinder) {
|
private fun onTargetActivityLaunched(launchToken: IBinder) {
|
||||||
val mediaProjectionBinder = intent.getIBinderExtra(EXTRA_MEDIA_PROJECTION)
|
if (intent.hasExtra(EXTRA_CAPTURE_REGION_RESULT_RECEIVER)) {
|
||||||
val projection = IMediaProjection.Stub.asInterface(mediaProjectionBinder)
|
// The client requested to return the result in the result receiver instead of
|
||||||
|
// activity result, let's send the media projection to the result receiver
|
||||||
|
val resultReceiver = intent
|
||||||
|
.getParcelableExtra(EXTRA_CAPTURE_REGION_RESULT_RECEIVER,
|
||||||
|
ResultReceiver::class.java) as ResultReceiver
|
||||||
|
val captureRegion = MediaProjectionCaptureTarget(launchToken)
|
||||||
|
val data = Bundle().apply {
|
||||||
|
putParcelable(KEY_CAPTURE_TARGET, captureRegion)
|
||||||
|
}
|
||||||
|
resultReceiver.send(RESULT_OK, data)
|
||||||
|
} else {
|
||||||
|
// Return the media projection instance as activity result
|
||||||
|
val mediaProjectionBinder = intent.getIBinderExtra(EXTRA_MEDIA_PROJECTION)
|
||||||
|
val projection = IMediaProjection.Stub.asInterface(mediaProjectionBinder)
|
||||||
|
|
||||||
projection.launchCookie = launchToken
|
projection.launchCookie = launchToken
|
||||||
|
|
||||||
|
val intent = Intent()
|
||||||
|
intent.putExtra(EXTRA_MEDIA_PROJECTION, projection.asBinder())
|
||||||
|
setResult(RESULT_OK, intent)
|
||||||
|
setForceSendResultForMediaProjection()
|
||||||
|
}
|
||||||
|
|
||||||
val intent = Intent()
|
|
||||||
intent.putExtra(EXTRA_MEDIA_PROJECTION, projection.asBinder())
|
|
||||||
setResult(RESULT_OK, intent)
|
|
||||||
setForceSendResultForMediaProjection()
|
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun shouldGetOnlyDefaultActivities() = false
|
override fun shouldGetOnlyDefaultActivities() = false
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* When EXTRA_CAPTURE_REGION_RESULT_RECEIVER is passed as intent extra
|
||||||
|
* the activity will send the [CaptureRegion] to the result receiver
|
||||||
|
* instead of returning media projection instance through activity result.
|
||||||
|
*/
|
||||||
|
const val EXTRA_CAPTURE_REGION_RESULT_RECEIVER = "capture_region_result_receiver"
|
||||||
|
const val KEY_CAPTURE_TARGET = "capture_region"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 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.media
|
||||||
|
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.Parcelable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class that represents an area that should be captured.
|
||||||
|
* Currently it has only a launch cookie that represents a task but
|
||||||
|
* we potentially could add more identifiers e.g. for a pair of tasks.
|
||||||
|
*/
|
||||||
|
data class MediaProjectionCaptureTarget(
|
||||||
|
val launchCookie: IBinder?
|
||||||
|
): Parcelable {
|
||||||
|
|
||||||
|
constructor(parcel: Parcel) : this(parcel.readStrongBinder())
|
||||||
|
|
||||||
|
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||||
|
dest.writeStrongBinder(launchCookie)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun describeContents(): Int = 0
|
||||||
|
|
||||||
|
companion object CREATOR : Parcelable.Creator<MediaProjectionCaptureTarget> {
|
||||||
|
override fun createFromParcel(parcel: Parcel): MediaProjectionCaptureTarget {
|
||||||
|
return MediaProjectionCaptureTarget(parcel)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newArray(size: Int): Array<MediaProjectionCaptureTarget?> {
|
||||||
|
return arrayOfNulls(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,6 +34,7 @@ import com.android.systemui.animation.DialogCuj;
|
|||||||
import com.android.systemui.animation.DialogLaunchAnimator;
|
import com.android.systemui.animation.DialogLaunchAnimator;
|
||||||
import com.android.systemui.dagger.qualifiers.Background;
|
import com.android.systemui.dagger.qualifiers.Background;
|
||||||
import com.android.systemui.dagger.qualifiers.Main;
|
import com.android.systemui.dagger.qualifiers.Main;
|
||||||
|
import com.android.systemui.flags.FeatureFlags;
|
||||||
import com.android.systemui.plugins.ActivityStarter;
|
import com.android.systemui.plugins.ActivityStarter;
|
||||||
import com.android.systemui.plugins.FalsingManager;
|
import com.android.systemui.plugins.FalsingManager;
|
||||||
import com.android.systemui.plugins.qs.QSTile;
|
import com.android.systemui.plugins.qs.QSTile;
|
||||||
@@ -61,6 +62,7 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState>
|
|||||||
private final KeyguardStateController mKeyguardStateController;
|
private final KeyguardStateController mKeyguardStateController;
|
||||||
private final Callback mCallback = new Callback();
|
private final Callback mCallback = new Callback();
|
||||||
private final DialogLaunchAnimator mDialogLaunchAnimator;
|
private final DialogLaunchAnimator mDialogLaunchAnimator;
|
||||||
|
private final FeatureFlags mFlags;
|
||||||
|
|
||||||
private long mMillisUntilFinished = 0;
|
private long mMillisUntilFinished = 0;
|
||||||
|
|
||||||
@@ -71,6 +73,7 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState>
|
|||||||
@Main Handler mainHandler,
|
@Main Handler mainHandler,
|
||||||
FalsingManager falsingManager,
|
FalsingManager falsingManager,
|
||||||
MetricsLogger metricsLogger,
|
MetricsLogger metricsLogger,
|
||||||
|
FeatureFlags flags,
|
||||||
StatusBarStateController statusBarStateController,
|
StatusBarStateController statusBarStateController,
|
||||||
ActivityStarter activityStarter,
|
ActivityStarter activityStarter,
|
||||||
QSLogger qsLogger,
|
QSLogger qsLogger,
|
||||||
@@ -83,6 +86,7 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState>
|
|||||||
statusBarStateController, activityStarter, qsLogger);
|
statusBarStateController, activityStarter, qsLogger);
|
||||||
mController = controller;
|
mController = controller;
|
||||||
mController.observe(this, mCallback);
|
mController.observe(this, mCallback);
|
||||||
|
mFlags = flags;
|
||||||
mKeyguardDismissUtil = keyguardDismissUtil;
|
mKeyguardDismissUtil = keyguardDismissUtil;
|
||||||
mKeyguardStateController = keyguardStateController;
|
mKeyguardStateController = keyguardStateController;
|
||||||
mDialogLaunchAnimator = dialogLaunchAnimator;
|
mDialogLaunchAnimator = dialogLaunchAnimator;
|
||||||
@@ -164,8 +168,8 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState>
|
|||||||
mDialogLaunchAnimator.disableAllCurrentDialogsExitAnimations();
|
mDialogLaunchAnimator.disableAllCurrentDialogsExitAnimations();
|
||||||
getHost().collapsePanels();
|
getHost().collapsePanels();
|
||||||
};
|
};
|
||||||
ScreenRecordDialog dialog = mController.createScreenRecordDialog(mContext,
|
ScreenRecordDialog dialog = mController.createScreenRecordDialog(mContext, mFlags,
|
||||||
onStartRecordingClicked);
|
mDialogLaunchAnimator, mActivityStarter, onStartRecordingClicked);
|
||||||
|
|
||||||
ActivityStarter.OnDismissAction dismissAction = () -> {
|
ActivityStarter.OnDismissAction dismissAction = () -> {
|
||||||
if (shouldAnimateFromView) {
|
if (shouldAnimateFromView) {
|
||||||
|
|||||||
@@ -29,8 +29,11 @@ import androidx.annotation.NonNull;
|
|||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import com.android.internal.annotations.VisibleForTesting;
|
import com.android.internal.annotations.VisibleForTesting;
|
||||||
|
import com.android.systemui.animation.DialogLaunchAnimator;
|
||||||
import com.android.systemui.broadcast.BroadcastDispatcher;
|
import com.android.systemui.broadcast.BroadcastDispatcher;
|
||||||
import com.android.systemui.dagger.SysUISingleton;
|
import com.android.systemui.dagger.SysUISingleton;
|
||||||
|
import com.android.systemui.flags.FeatureFlags;
|
||||||
|
import com.android.systemui.plugins.ActivityStarter;
|
||||||
import com.android.systemui.settings.UserContextProvider;
|
import com.android.systemui.settings.UserContextProvider;
|
||||||
import com.android.systemui.statusbar.policy.CallbackController;
|
import com.android.systemui.statusbar.policy.CallbackController;
|
||||||
|
|
||||||
@@ -94,9 +97,11 @@ public class RecordingController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Create a dialog to show screen recording options to the user. */
|
/** Create a dialog to show screen recording options to the user. */
|
||||||
public ScreenRecordDialog createScreenRecordDialog(Context context,
|
public ScreenRecordDialog createScreenRecordDialog(Context context, FeatureFlags flags,
|
||||||
|
DialogLaunchAnimator dialogLaunchAnimator, ActivityStarter activityStarter,
|
||||||
@Nullable Runnable onStartRecordingClicked) {
|
@Nullable Runnable onStartRecordingClicked) {
|
||||||
return new ScreenRecordDialog(context, this, mUserContextProvider, onStartRecordingClicked);
|
return new ScreenRecordDialog(context, this, activityStarter, mUserContextProvider,
|
||||||
|
flags, dialogLaunchAnimator, onStartRecordingClicked);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
package com.android.systemui.screenrecord;
|
package com.android.systemui.screenrecord;
|
||||||
|
|
||||||
|
import android.annotation.Nullable;
|
||||||
import android.app.Notification;
|
import android.app.Notification;
|
||||||
import android.app.NotificationChannel;
|
import android.app.NotificationChannel;
|
||||||
import android.app.NotificationManager;
|
import android.app.NotificationManager;
|
||||||
@@ -42,6 +43,7 @@ import com.android.internal.logging.UiEventLogger;
|
|||||||
import com.android.systemui.R;
|
import com.android.systemui.R;
|
||||||
import com.android.systemui.dagger.qualifiers.LongRunning;
|
import com.android.systemui.dagger.qualifiers.LongRunning;
|
||||||
import com.android.systemui.dagger.qualifiers.Main;
|
import com.android.systemui.dagger.qualifiers.Main;
|
||||||
|
import com.android.systemui.media.MediaProjectionCaptureTarget;
|
||||||
import com.android.systemui.screenrecord.ScreenMediaRecorder.ScreenMediaRecorderListener;
|
import com.android.systemui.screenrecord.ScreenMediaRecorder.ScreenMediaRecorderListener;
|
||||||
import com.android.systemui.settings.UserContextProvider;
|
import com.android.systemui.settings.UserContextProvider;
|
||||||
import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
|
import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
|
||||||
@@ -67,6 +69,7 @@ public class RecordingService extends Service implements ScreenMediaRecorderList
|
|||||||
private static final String EXTRA_PATH = "extra_path";
|
private static final String EXTRA_PATH = "extra_path";
|
||||||
private static final String EXTRA_AUDIO_SOURCE = "extra_useAudio";
|
private static final String EXTRA_AUDIO_SOURCE = "extra_useAudio";
|
||||||
private static final String EXTRA_SHOW_TAPS = "extra_showTaps";
|
private static final String EXTRA_SHOW_TAPS = "extra_showTaps";
|
||||||
|
private static final String EXTRA_CAPTURE_TARGET = "extra_captureTarget";
|
||||||
|
|
||||||
private static final String ACTION_START = "com.android.systemui.screenrecord.START";
|
private static final String ACTION_START = "com.android.systemui.screenrecord.START";
|
||||||
private static final String ACTION_STOP = "com.android.systemui.screenrecord.STOP";
|
private static final String ACTION_STOP = "com.android.systemui.screenrecord.STOP";
|
||||||
@@ -110,14 +113,18 @@ public class RecordingService extends Service implements ScreenMediaRecorderList
|
|||||||
* @param audioSource The ordinal value of the audio source
|
* @param audioSource The ordinal value of the audio source
|
||||||
* {@link com.android.systemui.screenrecord.ScreenRecordingAudioSource}
|
* {@link com.android.systemui.screenrecord.ScreenRecordingAudioSource}
|
||||||
* @param showTaps True to make touches visible while recording
|
* @param showTaps True to make touches visible while recording
|
||||||
|
* @param captureTarget pass this parameter to capture a specific part instead
|
||||||
|
* of the full screen
|
||||||
*/
|
*/
|
||||||
public static Intent getStartIntent(Context context, int resultCode,
|
public static Intent getStartIntent(Context context, int resultCode,
|
||||||
int audioSource, boolean showTaps) {
|
int audioSource, boolean showTaps,
|
||||||
|
@Nullable MediaProjectionCaptureTarget captureTarget) {
|
||||||
return new Intent(context, RecordingService.class)
|
return new Intent(context, RecordingService.class)
|
||||||
.setAction(ACTION_START)
|
.setAction(ACTION_START)
|
||||||
.putExtra(EXTRA_RESULT_CODE, resultCode)
|
.putExtra(EXTRA_RESULT_CODE, resultCode)
|
||||||
.putExtra(EXTRA_AUDIO_SOURCE, audioSource)
|
.putExtra(EXTRA_AUDIO_SOURCE, audioSource)
|
||||||
.putExtra(EXTRA_SHOW_TAPS, showTaps);
|
.putExtra(EXTRA_SHOW_TAPS, showTaps)
|
||||||
|
.putExtra(EXTRA_CAPTURE_TARGET, captureTarget);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -136,6 +143,9 @@ public class RecordingService extends Service implements ScreenMediaRecorderList
|
|||||||
.values()[intent.getIntExtra(EXTRA_AUDIO_SOURCE, 0)];
|
.values()[intent.getIntExtra(EXTRA_AUDIO_SOURCE, 0)];
|
||||||
Log.d(TAG, "recording with audio source" + mAudioSource);
|
Log.d(TAG, "recording with audio source" + mAudioSource);
|
||||||
mShowTaps = intent.getBooleanExtra(EXTRA_SHOW_TAPS, false);
|
mShowTaps = intent.getBooleanExtra(EXTRA_SHOW_TAPS, false);
|
||||||
|
MediaProjectionCaptureTarget captureTarget =
|
||||||
|
intent.getParcelableExtra(EXTRA_CAPTURE_TARGET,
|
||||||
|
MediaProjectionCaptureTarget.class);
|
||||||
|
|
||||||
mOriginalShowTaps = Settings.System.getInt(
|
mOriginalShowTaps = Settings.System.getInt(
|
||||||
getApplicationContext().getContentResolver(),
|
getApplicationContext().getContentResolver(),
|
||||||
@@ -148,6 +158,7 @@ public class RecordingService extends Service implements ScreenMediaRecorderList
|
|||||||
mMainHandler,
|
mMainHandler,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
mAudioSource,
|
mAudioSource,
|
||||||
|
captureTarget,
|
||||||
this
|
this
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ import android.util.Size;
|
|||||||
import android.view.Surface;
|
import android.view.Surface;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
|
|
||||||
|
import com.android.systemui.media.MediaProjectionCaptureTarget;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -85,6 +86,7 @@ public class ScreenMediaRecorder extends MediaProjection.Callback {
|
|||||||
private ScreenRecordingMuxer mMuxer;
|
private ScreenRecordingMuxer mMuxer;
|
||||||
private ScreenInternalAudioRecorder mAudio;
|
private ScreenInternalAudioRecorder mAudio;
|
||||||
private ScreenRecordingAudioSource mAudioSource;
|
private ScreenRecordingAudioSource mAudioSource;
|
||||||
|
private final MediaProjectionCaptureTarget mCaptureRegion;
|
||||||
private final Handler mHandler;
|
private final Handler mHandler;
|
||||||
|
|
||||||
private Context mContext;
|
private Context mContext;
|
||||||
@@ -92,10 +94,12 @@ public class ScreenMediaRecorder extends MediaProjection.Callback {
|
|||||||
|
|
||||||
public ScreenMediaRecorder(Context context, Handler handler,
|
public ScreenMediaRecorder(Context context, Handler handler,
|
||||||
int user, ScreenRecordingAudioSource audioSource,
|
int user, ScreenRecordingAudioSource audioSource,
|
||||||
|
MediaProjectionCaptureTarget captureRegion,
|
||||||
ScreenMediaRecorderListener listener) {
|
ScreenMediaRecorderListener listener) {
|
||||||
mContext = context;
|
mContext = context;
|
||||||
mHandler = handler;
|
mHandler = handler;
|
||||||
mUser = user;
|
mUser = user;
|
||||||
|
mCaptureRegion = captureRegion;
|
||||||
mListener = listener;
|
mListener = listener;
|
||||||
mAudioSource = audioSource;
|
mAudioSource = audioSource;
|
||||||
}
|
}
|
||||||
@@ -108,9 +112,11 @@ public class ScreenMediaRecorder extends MediaProjection.Callback {
|
|||||||
IMediaProjection proj = null;
|
IMediaProjection proj = null;
|
||||||
proj = mediaService.createProjection(mUser, mContext.getPackageName(),
|
proj = mediaService.createProjection(mUser, mContext.getPackageName(),
|
||||||
MediaProjectionManager.TYPE_SCREEN_CAPTURE, false);
|
MediaProjectionManager.TYPE_SCREEN_CAPTURE, false);
|
||||||
IBinder projection = proj.asBinder();
|
IMediaProjection projection = IMediaProjection.Stub.asInterface(proj.asBinder());
|
||||||
mMediaProjection = new MediaProjection(mContext,
|
if (mCaptureRegion != null) {
|
||||||
IMediaProjection.Stub.asInterface(projection));
|
projection.setLaunchCookie(mCaptureRegion.getLaunchCookie());
|
||||||
|
}
|
||||||
|
mMediaProjection = new MediaProjection(mContext, projection);
|
||||||
mMediaProjection.registerCallback(this, mHandler);
|
mMediaProjection.registerCallback(this, mHandler);
|
||||||
|
|
||||||
File cacheDir = mContext.getCacheDir();
|
File cacheDir = mContext.getCacheDir();
|
||||||
|
|||||||
@@ -16,6 +16,10 @@
|
|||||||
|
|
||||||
package com.android.systemui.screenrecord;
|
package com.android.systemui.screenrecord;
|
||||||
|
|
||||||
|
import static android.app.Activity.RESULT_OK;
|
||||||
|
|
||||||
|
import static com.android.systemui.media.MediaProjectionAppSelectorActivity.EXTRA_CAPTURE_REGION_RESULT_RECEIVER;
|
||||||
|
import static com.android.systemui.media.MediaProjectionAppSelectorActivity.KEY_CAPTURE_TARGET;
|
||||||
import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.INTERNAL;
|
import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.INTERNAL;
|
||||||
import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.MIC;
|
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.MIC_AND_INTERNAL;
|
||||||
@@ -24,8 +28,13 @@ import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.NONE;
|
|||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.PendingIntent;
|
import android.app.PendingIntent;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.os.ResultReceiver;
|
||||||
import android.view.Gravity;
|
import android.view.Gravity;
|
||||||
|
import android.view.View;
|
||||||
import android.view.Window;
|
import android.view.Window;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
import android.widget.ArrayAdapter;
|
import android.widget.ArrayAdapter;
|
||||||
@@ -36,6 +45,13 @@ import android.widget.TextView;
|
|||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import com.android.systemui.R;
|
import com.android.systemui.R;
|
||||||
|
import com.android.systemui.animation.ActivityLaunchAnimator;
|
||||||
|
import com.android.systemui.animation.DialogLaunchAnimator;
|
||||||
|
import com.android.systemui.flags.FeatureFlags;
|
||||||
|
import com.android.systemui.flags.Flags;
|
||||||
|
import com.android.systemui.media.MediaProjectionAppSelectorActivity;
|
||||||
|
import com.android.systemui.media.MediaProjectionCaptureTarget;
|
||||||
|
import com.android.systemui.plugins.ActivityStarter;
|
||||||
import com.android.systemui.settings.UserContextProvider;
|
import com.android.systemui.settings.UserContextProvider;
|
||||||
import com.android.systemui.statusbar.phone.SystemUIDialog;
|
import com.android.systemui.statusbar.phone.SystemUIDialog;
|
||||||
|
|
||||||
@@ -55,15 +71,23 @@ public class ScreenRecordDialog extends SystemUIDialog {
|
|||||||
private final UserContextProvider mUserContextProvider;
|
private final UserContextProvider mUserContextProvider;
|
||||||
@Nullable
|
@Nullable
|
||||||
private final Runnable mOnStartRecordingClicked;
|
private final Runnable mOnStartRecordingClicked;
|
||||||
|
private final ActivityStarter mActivityStarter;
|
||||||
|
private final FeatureFlags mFlags;
|
||||||
|
private final DialogLaunchAnimator mDialogLaunchAnimator;
|
||||||
private Switch mTapsSwitch;
|
private Switch mTapsSwitch;
|
||||||
private Switch mAudioSwitch;
|
private Switch mAudioSwitch;
|
||||||
private Spinner mOptions;
|
private Spinner mOptions;
|
||||||
|
|
||||||
public ScreenRecordDialog(Context context, RecordingController controller,
|
public ScreenRecordDialog(Context context, RecordingController controller,
|
||||||
UserContextProvider userContextProvider, @Nullable Runnable onStartRecordingClicked) {
|
ActivityStarter activityStarter, UserContextProvider userContextProvider,
|
||||||
|
FeatureFlags flags, DialogLaunchAnimator dialogLaunchAnimator,
|
||||||
|
@Nullable Runnable onStartRecordingClicked) {
|
||||||
super(context);
|
super(context);
|
||||||
mController = controller;
|
mController = controller;
|
||||||
mUserContextProvider = userContextProvider;
|
mUserContextProvider = userContextProvider;
|
||||||
|
mActivityStarter = activityStarter;
|
||||||
|
mDialogLaunchAnimator = dialogLaunchAnimator;
|
||||||
|
mFlags = flags;
|
||||||
mOnStartRecordingClicked = onStartRecordingClicked;
|
mOnStartRecordingClicked = onStartRecordingClicked;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,10 +115,36 @@ public class ScreenRecordDialog extends SystemUIDialog {
|
|||||||
mOnStartRecordingClicked.run();
|
mOnStartRecordingClicked.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
requestScreenCapture();
|
// Start full-screen recording
|
||||||
|
requestScreenCapture(/* captureTarget= */ null);
|
||||||
dismiss();
|
dismiss();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (mFlags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING)) {
|
||||||
|
TextView appBtn = findViewById(R.id.button_app);
|
||||||
|
|
||||||
|
appBtn.setVisibility(View.VISIBLE);
|
||||||
|
appBtn.setOnClickListener(v -> {
|
||||||
|
Intent intent = new Intent(getContext(), MediaProjectionAppSelectorActivity.class);
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
|
||||||
|
// We can't start activity for result here so we use result receiver to get
|
||||||
|
// the selected target to capture
|
||||||
|
intent.putExtra(EXTRA_CAPTURE_REGION_RESULT_RECEIVER,
|
||||||
|
new CaptureTargetResultReceiver());
|
||||||
|
|
||||||
|
ActivityLaunchAnimator.Controller animationController =
|
||||||
|
mDialogLaunchAnimator.createActivityLaunchController(appBtn);
|
||||||
|
|
||||||
|
if (animationController == null) {
|
||||||
|
dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
mActivityStarter.startActivity(intent, /* dismissShade= */ true,
|
||||||
|
animationController);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
mAudioSwitch = findViewById(R.id.screenrecord_audio_switch);
|
mAudioSwitch = findViewById(R.id.screenrecord_audio_switch);
|
||||||
mTapsSwitch = findViewById(R.id.screenrecord_taps_switch);
|
mTapsSwitch = findViewById(R.id.screenrecord_taps_switch);
|
||||||
mOptions = findViewById(R.id.screen_recording_options);
|
mOptions = findViewById(R.id.screen_recording_options);
|
||||||
@@ -108,7 +158,12 @@ public class ScreenRecordDialog extends SystemUIDialog {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void requestScreenCapture() {
|
/**
|
||||||
|
* Starts screen capture after some countdown
|
||||||
|
* @param captureTarget target to capture (could be e.g. a task) or
|
||||||
|
* null to record the whole screen
|
||||||
|
*/
|
||||||
|
private void requestScreenCapture(@Nullable MediaProjectionCaptureTarget captureTarget) {
|
||||||
Context userContext = mUserContextProvider.getUserContext();
|
Context userContext = mUserContextProvider.getUserContext();
|
||||||
boolean showTaps = mTapsSwitch.isChecked();
|
boolean showTaps = mTapsSwitch.isChecked();
|
||||||
ScreenRecordingAudioSource audioMode = mAudioSwitch.isChecked()
|
ScreenRecordingAudioSource audioMode = mAudioSwitch.isChecked()
|
||||||
@@ -118,7 +173,7 @@ public class ScreenRecordDialog extends SystemUIDialog {
|
|||||||
RecordingService.REQUEST_CODE,
|
RecordingService.REQUEST_CODE,
|
||||||
RecordingService.getStartIntent(
|
RecordingService.getStartIntent(
|
||||||
userContext, Activity.RESULT_OK,
|
userContext, Activity.RESULT_OK,
|
||||||
audioMode.ordinal(), showTaps),
|
audioMode.ordinal(), showTaps, captureTarget),
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||||
PendingIntent stopIntent = PendingIntent.getService(userContext,
|
PendingIntent stopIntent = PendingIntent.getService(userContext,
|
||||||
RecordingService.REQUEST_CODE,
|
RecordingService.REQUEST_CODE,
|
||||||
@@ -126,4 +181,22 @@ public class ScreenRecordDialog extends SystemUIDialog {
|
|||||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||||
mController.startCountdown(DELAY_MS, INTERVAL_MS, startIntent, stopIntent);
|
mController.startCountdown(DELAY_MS, INTERVAL_MS, startIntent, stopIntent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class CaptureTargetResultReceiver extends ResultReceiver {
|
||||||
|
|
||||||
|
CaptureTargetResultReceiver() {
|
||||||
|
super(new Handler(Looper.getMainLooper()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onReceiveResult(int resultCode, Bundle resultData) {
|
||||||
|
if (resultCode == RESULT_OK) {
|
||||||
|
MediaProjectionCaptureTarget captureTarget = resultData
|
||||||
|
.getParcelable(KEY_CAPTURE_TARGET, MediaProjectionCaptureTarget.class);
|
||||||
|
|
||||||
|
// Start recording of the selected target
|
||||||
|
requestScreenCapture(captureTarget);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import static junit.framework.Assert.assertTrue;
|
|||||||
|
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
@@ -38,6 +39,7 @@ import com.android.systemui.R;
|
|||||||
import com.android.systemui.SysuiTestCase;
|
import com.android.systemui.SysuiTestCase;
|
||||||
import com.android.systemui.animation.DialogLaunchAnimator;
|
import com.android.systemui.animation.DialogLaunchAnimator;
|
||||||
import com.android.systemui.classifier.FalsingManagerFake;
|
import com.android.systemui.classifier.FalsingManagerFake;
|
||||||
|
import com.android.systemui.flags.FeatureFlags;
|
||||||
import com.android.systemui.plugins.ActivityStarter;
|
import com.android.systemui.plugins.ActivityStarter;
|
||||||
import com.android.systemui.plugins.statusbar.StatusBarStateController;
|
import com.android.systemui.plugins.statusbar.StatusBarStateController;
|
||||||
import com.android.systemui.qs.QSTileHost;
|
import com.android.systemui.qs.QSTileHost;
|
||||||
@@ -73,6 +75,8 @@ public class ScreenRecordTileTest extends SysuiTestCase {
|
|||||||
@Mock
|
@Mock
|
||||||
private QSLogger mQSLogger;
|
private QSLogger mQSLogger;
|
||||||
@Mock
|
@Mock
|
||||||
|
private FeatureFlags mFeatureFlags;
|
||||||
|
@Mock
|
||||||
private KeyguardStateController mKeyguardStateController;
|
private KeyguardStateController mKeyguardStateController;
|
||||||
@Mock
|
@Mock
|
||||||
private DialogLaunchAnimator mDialogLaunchAnimator;
|
private DialogLaunchAnimator mDialogLaunchAnimator;
|
||||||
@@ -94,6 +98,7 @@ public class ScreenRecordTileTest extends SysuiTestCase {
|
|||||||
new Handler(mTestableLooper.getLooper()),
|
new Handler(mTestableLooper.getLooper()),
|
||||||
new FalsingManagerFake(),
|
new FalsingManagerFake(),
|
||||||
mMetricsLogger,
|
mMetricsLogger,
|
||||||
|
mFeatureFlags,
|
||||||
mStatusBarStateController,
|
mStatusBarStateController,
|
||||||
mActivityStarter,
|
mActivityStarter,
|
||||||
mQSLogger,
|
mQSLogger,
|
||||||
@@ -125,7 +130,8 @@ public class ScreenRecordTileTest extends SysuiTestCase {
|
|||||||
mTestableLooper.processAllMessages();
|
mTestableLooper.processAllMessages();
|
||||||
|
|
||||||
ArgumentCaptor<Runnable> onStartRecordingClicked = ArgumentCaptor.forClass(Runnable.class);
|
ArgumentCaptor<Runnable> onStartRecordingClicked = ArgumentCaptor.forClass(Runnable.class);
|
||||||
verify(mController).createScreenRecordDialog(any(), onStartRecordingClicked.capture());
|
verify(mController).createScreenRecordDialog(any(), eq(mFeatureFlags),
|
||||||
|
eq(mDialogLaunchAnimator), eq(mActivityStarter), onStartRecordingClicked.capture());
|
||||||
|
|
||||||
// When starting the recording, we collapse the shade and disable the dialog animation.
|
// When starting the recording, we collapse the shade and disable the dialog animation.
|
||||||
assertNotNull(onStartRecordingClicked.getValue());
|
assertNotNull(onStartRecordingClicked.getValue());
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import static org.mockito.Mockito.verify;
|
|||||||
import android.app.Notification;
|
import android.app.Notification;
|
||||||
import android.app.NotificationManager;
|
import android.app.NotificationManager;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.os.Binder;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.RemoteException;
|
import android.os.RemoteException;
|
||||||
import android.testing.AndroidTestingRunner;
|
import android.testing.AndroidTestingRunner;
|
||||||
@@ -38,6 +39,7 @@ import androidx.test.filters.SmallTest;
|
|||||||
|
|
||||||
import com.android.internal.logging.UiEventLogger;
|
import com.android.internal.logging.UiEventLogger;
|
||||||
import com.android.systemui.SysuiTestCase;
|
import com.android.systemui.SysuiTestCase;
|
||||||
|
import com.android.systemui.media.MediaProjectionCaptureTarget;
|
||||||
import com.android.systemui.plugins.ActivityStarter;
|
import com.android.systemui.plugins.ActivityStarter;
|
||||||
import com.android.systemui.settings.UserContextProvider;
|
import com.android.systemui.settings.UserContextProvider;
|
||||||
import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
|
import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
|
||||||
@@ -108,8 +110,17 @@ public class RecordingServiceTest extends SysuiTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testLogStartRecording() {
|
public void testLogStartFullScreenRecording() {
|
||||||
Intent startIntent = RecordingService.getStartIntent(mContext, 0, 0, false);
|
Intent startIntent = RecordingService.getStartIntent(mContext, 0, 0, false, null);
|
||||||
|
mRecordingService.onStartCommand(startIntent, 0, 0);
|
||||||
|
|
||||||
|
verify(mUiEventLogger, times(1)).log(Events.ScreenRecordEvent.SCREEN_RECORD_START);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testLogStartPartialRecording() {
|
||||||
|
MediaProjectionCaptureTarget target = new MediaProjectionCaptureTarget(new Binder());
|
||||||
|
Intent startIntent = RecordingService.getStartIntent(mContext, 0, 0, false, target);
|
||||||
mRecordingService.onStartCommand(startIntent, 0, 0);
|
mRecordingService.onStartCommand(startIntent, 0, 0);
|
||||||
|
|
||||||
verify(mUiEventLogger, times(1)).log(Events.ScreenRecordEvent.SCREEN_RECORD_START);
|
verify(mUiEventLogger, times(1)).log(Events.ScreenRecordEvent.SCREEN_RECORD_START);
|
||||||
@@ -142,7 +153,7 @@ public class RecordingServiceTest extends SysuiTestCase {
|
|||||||
// When the screen recording does not start properly
|
// When the screen recording does not start properly
|
||||||
doThrow(new RuntimeException("fail")).when(mScreenMediaRecorder).start();
|
doThrow(new RuntimeException("fail")).when(mScreenMediaRecorder).start();
|
||||||
|
|
||||||
Intent startIntent = RecordingService.getStartIntent(mContext, 0, 0, false);
|
Intent startIntent = RecordingService.getStartIntent(mContext, 0, 0, false, null);
|
||||||
mRecordingService.onStartCommand(startIntent, 0, 0);
|
mRecordingService.onStartCommand(startIntent, 0, 0);
|
||||||
|
|
||||||
// Then the state is set to not recording
|
// Then the state is set to not recording
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2022 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.testing.AndroidTestingRunner
|
||||||
|
import android.testing.TestableLooper
|
||||||
|
import android.view.View
|
||||||
|
import androidx.test.filters.SmallTest
|
||||||
|
import com.android.systemui.R
|
||||||
|
import com.android.systemui.SysuiTestCase
|
||||||
|
import com.android.systemui.animation.DialogLaunchAnimator
|
||||||
|
import com.android.systemui.flags.FeatureFlags
|
||||||
|
import com.android.systemui.flags.Flags
|
||||||
|
import com.android.systemui.plugins.ActivityStarter
|
||||||
|
import com.android.systemui.settings.UserContextProvider
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.mockito.Mock
|
||||||
|
import org.mockito.MockitoAnnotations
|
||||||
|
import org.mockito.Mockito.`when` as whenever
|
||||||
|
|
||||||
|
@SmallTest
|
||||||
|
@RunWith(AndroidTestingRunner::class)
|
||||||
|
@TestableLooper.RunWithLooper(setAsMainLooper = true)
|
||||||
|
class ScreenRecordDialogTest : SysuiTestCase() {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private lateinit var starter: ActivityStarter
|
||||||
|
@Mock
|
||||||
|
private lateinit var controller: RecordingController
|
||||||
|
@Mock
|
||||||
|
private lateinit var userContextProvider: UserContextProvider
|
||||||
|
@Mock
|
||||||
|
private lateinit var flags: FeatureFlags
|
||||||
|
@Mock
|
||||||
|
private lateinit var dialogLaunchAnimator: DialogLaunchAnimator
|
||||||
|
@Mock
|
||||||
|
private lateinit var onStartRecordingClicked: Runnable
|
||||||
|
|
||||||
|
private lateinit var dialog: ScreenRecordDialog
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
MockitoAnnotations.initMocks(this)
|
||||||
|
|
||||||
|
dialog = ScreenRecordDialog(
|
||||||
|
context, controller, starter, userContextProvider, flags, dialogLaunchAnimator,
|
||||||
|
onStartRecordingClicked
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun teardown() {
|
||||||
|
if (::dialog.isInitialized) {
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testShowDialog_partialScreenSharingDisabled_appButtonIsNotVisible() {
|
||||||
|
whenever(flags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING)).thenReturn(false)
|
||||||
|
|
||||||
|
dialog.show()
|
||||||
|
|
||||||
|
val visibility = dialog.requireViewById<View>(R.id.button_app).visibility
|
||||||
|
assertThat(visibility).isEqualTo(View.GONE)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testShowDialog_partialScreenSharingEnabled_appButtonIsVisible() {
|
||||||
|
whenever(flags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING)).thenReturn(true)
|
||||||
|
|
||||||
|
dialog.show()
|
||||||
|
|
||||||
|
val visibility = dialog.requireViewById<View>(R.id.button_app).visibility
|
||||||
|
assertThat(visibility).isEqualTo(View.VISIBLE)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user