Merge changes from topic "record-dialog-animation" into sc-v2-dev

* changes:
  Animate the Internet dialog height changes
  Animate the screen recording dialog (1/2)
  Animate the Data Saver QS dialog
  Wrap animated dialog view inside another view
This commit is contained in:
Jordan Demeulenaere
2021-11-09 15:36:12 +00:00
committed by Android (Google) Code Review
13 changed files with 362 additions and 180 deletions

View File

@@ -356,10 +356,6 @@
android:exported="false"
android:finishOnTaskLaunch="true" />
<activity android:name=".screenrecord.ScreenRecordDialog"
android:theme="@style/ScreenRecord"
android:showForAllUsers="true"
android:excludeFromRecents="true" />
<service android:name=".screenrecord.RecordingService" />
<receiver android:name=".SysuiRestartReceiver"

View File

@@ -16,11 +16,16 @@
package com.android.systemui.animation
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
import android.app.Dialog
import android.content.Context
import android.graphics.Color
import android.graphics.Rect
import android.os.Looper
import android.util.Log
import android.util.MathUtils
import android.view.GhostView
import android.view.Gravity
import android.view.View
@@ -32,6 +37,7 @@ import android.view.WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR
import android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
import android.view.WindowManagerPolicyConstants
import android.widget.FrameLayout
import kotlin.math.roundToInt
private const val TAG = "DialogLaunchAnimator"
@@ -52,7 +58,8 @@ class DialogLaunchAnimator(
private val currentAnimations = hashSetOf<DialogLaunchAnimation>()
/**
* Show [dialog] by expanding it from [view].
* Show [dialog] by expanding it from [view]. If [animateBackgroundBoundsChange] is true, then
* the background of the dialog will be animated when the dialog bounds change.
*
* Caveats: When calling this function, the dialog content view will actually be stolen and
* attached to a different dialog (and thus a different window) which means that the actual
@@ -60,7 +67,12 @@ class DialogLaunchAnimator(
* must call dismiss(), hide() and show() on the [Dialog] returned by this function to actually
* dismiss, hide or show the dialog.
*/
fun showFromView(dialog: Dialog, view: View): Dialog {
@JvmOverloads
fun showFromView(
dialog: Dialog,
view: View,
animateBackgroundBoundsChange: Boolean = false
): Dialog {
if (Looper.myLooper() != Looper.getMainLooper()) {
throw IllegalStateException(
"showFromView must be called from the main thread and dialog must be created in " +
@@ -78,7 +90,8 @@ class DialogLaunchAnimator(
val launchAnimation = DialogLaunchAnimation(
context, launchAnimator, hostDialogProvider, view,
onDialogDismissed = { currentAnimations.remove(it) }, originalDialog = dialog)
onDialogDismissed = { currentAnimations.remove(it) }, originalDialog = dialog,
animateBackgroundBoundsChange)
val hostDialog = launchAnimation.hostDialog
currentAnimations.add(launchAnimation)
@@ -208,7 +221,10 @@ private class DialogLaunchAnimation(
private val onDialogDismissed: (DialogLaunchAnimation) -> Unit,
/** The original dialog whose content will be shown and animate in/out in [hostDialog]. */
private val originalDialog: Dialog
private val originalDialog: Dialog,
/** Whether we should animate the dialog background when its bounds change. */
private val animateBackgroundBoundsChange: Boolean
) {
/**
* The fullscreen dialog to which we will add the content view [originalDialogView] of
@@ -221,10 +237,11 @@ private class DialogLaunchAnimation(
private val hostDialogRoot = FrameLayout(context)
/**
* The content view of [originalDialog], which will be stolen from that dialog and added to
* [hostDialogRoot].
* The parent of the original dialog content view, that serves as a fake window that will have
* the same size as the original dialog window and to which we will set the original dialog
* window background.
*/
private var originalDialogView: View? = null
private val dialogContentParent = FrameLayout(context)
/**
* The background color of [originalDialogView], taking into consideration the [originalDialog]
@@ -246,6 +263,11 @@ private class DialogLaunchAnimation(
private var isTouchSurfaceGhostDrawn = false
private var isOriginalDialogViewLaidOut = false
private var backgroundLayoutListener = if (animateBackgroundBoundsChange) {
AnimatedBoundsLayoutListener()
} else {
null
}
fun start() {
// Show the host (fullscreen) dialog, to which we will add the stolen dialog view.
@@ -374,9 +396,6 @@ private class DialogLaunchAnimation(
}
private fun showDialogFromView(dialogView: View) {
// Save the dialog view for later as we will need it for the close animation.
this.originalDialogView = dialogView
// Close the dialog when clicking outside of it.
hostDialogRoot.setOnClickListener { hostDialog.dismiss() }
dialogView.isClickable = true
@@ -394,17 +413,13 @@ private class DialogLaunchAnimation(
throw IllegalStateException("Dialogs with no backgrounds on window are not supported")
}
dialogView.setBackgroundResource(backgroundRes)
originalDialogBackgroundColor =
GhostedViewLaunchAnimatorController.findGradientDrawable(dialogView.background!!)
?.color
?.defaultColor ?: Color.BLACK
// Add the dialog view to the host (fullscreen) dialog and make it invisible to make sure
// it's not drawn yet.
(dialogView.parent as? ViewGroup)?.removeView(dialogView)
// Add a parent view to the original dialog view to which we will set the original dialog
// window background. This View serves as a fake window with background, so that we are sure
// that we don't override the dialog view paddings with the window background that usually
// has insets.
dialogContentParent.setBackgroundResource(backgroundRes)
hostDialogRoot.addView(
dialogView,
dialogContentParent,
// We give it the size of its original dialog window.
FrameLayout.LayoutParams(
@@ -413,10 +428,31 @@ private class DialogLaunchAnimation(
Gravity.CENTER
)
)
dialogView.visibility = View.INVISIBLE
// Make the dialog view parent invisible for now, to make sure it's not drawn yet.
dialogContentParent.visibility = View.INVISIBLE
val background = dialogContentParent.background!!
originalDialogBackgroundColor =
GhostedViewLaunchAnimatorController.findGradientDrawable(background)
?.color
?.defaultColor ?: Color.BLACK
// Add the dialog view to its parent (that has the original window background).
(dialogView.parent as? ViewGroup)?.removeView(dialogView)
dialogContentParent.addView(
dialogView,
// It should match its parent size, which is sized the same as the original dialog
// window.
FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
)
// Start the animation when the dialog is laid out in the center of the host dialog.
dialogView.addOnLayoutChangeListener(object : View.OnLayoutChangeListener {
dialogContentParent.addOnLayoutChangeListener(object : View.OnLayoutChangeListener {
override fun onLayoutChange(
view: View,
left: Int,
@@ -428,7 +464,7 @@ private class DialogLaunchAnimation(
oldRight: Int,
oldBottom: Int
) {
dialogView.removeOnLayoutChangeListener(this)
dialogContentParent.removeOnLayoutChangeListener(this)
isOriginalDialogViewLaidOut = true
maybeStartLaunchAnimation()
@@ -479,6 +515,13 @@ private class DialogLaunchAnimation(
if (dismissRequested) {
hostDialog.dismiss()
}
// If necessary, we animate the dialog background when its bounds change. We do it
// at the end of the launch animation, because the lauch animation already correctly
// handles bounds changes.
if (backgroundLayoutListener != null) {
dialogContentParent.addOnLayoutChangeListener(backgroundLayoutListener)
}
}
)
}
@@ -548,7 +591,11 @@ private class DialogLaunchAnimation(
}
touchSurface.visibility = View.VISIBLE
originalDialogView!!.visibility = View.INVISIBLE
dialogContentParent.visibility = View.INVISIBLE
if (backgroundLayoutListener != null) {
dialogContentParent.removeOnLayoutChangeListener(backgroundLayoutListener)
}
// The animated ghost was just removed. We create a temporary ghost that will be
// removed only once we draw the touch surface, to avoid flickering that would
@@ -578,12 +625,10 @@ private class DialogLaunchAnimation(
onLaunchAnimationStart: () -> Unit = {},
onLaunchAnimationEnd: () -> Unit = {}
) {
val dialogView = this.originalDialogView!!
// Create 2 ghost controllers to animate both the dialog and the touch surface in the host
// dialog.
val startView = if (isLaunching) touchSurface else dialogView
val endView = if (isLaunching) dialogView else touchSurface
val startView = if (isLaunching) touchSurface else dialogContentParent
val endView = if (isLaunching) dialogContentParent else touchSurface
val startViewController = GhostedViewLaunchAnimatorController(startView)
val endViewController = GhostedViewLaunchAnimatorController(endView)
startViewController.launchContainer = hostDialogRoot
@@ -662,4 +707,81 @@ private class DialogLaunchAnimation(
return (touchSurface.parent as? View)?.isShown ?: true
}
/** A layout listener to animate the change of bounds of the dialog background. */
class AnimatedBoundsLayoutListener : View.OnLayoutChangeListener {
companion object {
private const val ANIMATION_DURATION = 500L
}
private var lastBounds: Rect? = null
private var currentAnimator: ValueAnimator? = null
override fun onLayoutChange(
view: View,
left: Int,
top: Int,
right: Int,
bottom: Int,
oldLeft: Int,
oldTop: Int,
oldRight: Int,
oldBottom: Int
) {
// Don't animate if bounds didn't actually change.
if (left == oldLeft && top == oldTop && right == oldRight && bottom == oldBottom) {
// Make sure that we that the last bounds set by the animator were not overridden.
lastBounds?.let { bounds ->
view.left = bounds.left
view.top = bounds.top
view.right = bounds.right
view.bottom = bounds.bottom
}
return
}
if (lastBounds == null) {
lastBounds = Rect(oldLeft, oldTop, oldRight, oldBottom)
}
val bounds = lastBounds!!
val startLeft = bounds.left
val startTop = bounds.top
val startRight = bounds.right
val startBottom = bounds.bottom
currentAnimator?.cancel()
currentAnimator = null
val animator = ValueAnimator.ofFloat(0f, 1f).apply {
duration = ANIMATION_DURATION
interpolator = Interpolators.STANDARD
addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
currentAnimator = null
}
})
addUpdateListener { animatedValue ->
val progress = animatedValue.animatedFraction
// Compute new bounds.
bounds.left = MathUtils.lerp(startLeft, left, progress).roundToInt()
bounds.top = MathUtils.lerp(startTop, top, progress).roundToInt()
bounds.right = MathUtils.lerp(startRight, right, progress).roundToInt()
bounds.bottom = MathUtils.lerp(startBottom, bottom, progress).roundToInt()
// Set the new bounds.
view.left = bounds.left
view.top = bounds.top
view.right = bounds.right
view.bottom = bounds.bottom
}
}
currentAnimator = animator
animator.start()
}
}
}

View File

@@ -16,78 +16,74 @@
~ limitations under the License.
-->
<FrameLayout
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:sysui="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="24dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
>
<TextView
android:id="@+id/title"
android:layout_height="wrap_content"
android:padding="24dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
>
<TextView
android:id="@+id/title"
android:layout_height="wrap_content"
android:layout_width="0dp"
android:textAlignment="center"
android:text="@string/qs_user_switch_dialog_title"
android:textAppearance="@style/TextAppearance.QSDialog.Title"
android:layout_marginBottom="32dp"
sysui:layout_constraintTop_toTopOf="parent"
sysui:layout_constraintStart_toStartOf="parent"
sysui:layout_constraintEnd_toEndOf="parent"
sysui:layout_constraintBottom_toTopOf="@id/grid"
/>
<com.android.systemui.qs.PseudoGridView
android:id="@+id/grid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="28dp"
sysui:verticalSpacing="4dp"
sysui:horizontalSpacing="4dp"
sysui:fixedChildWidth="80dp"
sysui:layout_constraintTop_toBottomOf="@id/title"
sysui:layout_constraintStart_toStartOf="parent"
sysui:layout_constraintEnd_toEndOf="parent"
sysui:layout_constraintBottom_toTopOf="@id/barrier"
android:layout_width="0dp"
android:textAlignment="center"
android:text="@string/qs_user_switch_dialog_title"
android:textAppearance="@style/TextAppearance.QSDialog.Title"
android:layout_marginBottom="32dp"
sysui:layout_constraintTop_toTopOf="parent"
sysui:layout_constraintStart_toStartOf="parent"
sysui:layout_constraintEnd_toEndOf="parent"
sysui:layout_constraintBottom_toTopOf="@id/grid"
/>
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
sysui:barrierDirection="top"
sysui:constraint_referenced_ids="settings,done"
/>
<com.android.systemui.qs.PseudoGridView
android:id="@+id/grid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="28dp"
sysui:verticalSpacing="4dp"
sysui:horizontalSpacing="4dp"
sysui:fixedChildWidth="80dp"
sysui:layout_constraintTop_toBottomOf="@id/title"
sysui:layout_constraintStart_toStartOf="parent"
sysui:layout_constraintEnd_toEndOf="parent"
sysui:layout_constraintBottom_toTopOf="@id/barrier"
/>
<Button
android:id="@+id/settings"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:text="@string/quick_settings_more_user_settings"
sysui:layout_constraintTop_toBottomOf="@id/barrier"
sysui:layout_constraintBottom_toBottomOf="parent"
sysui:layout_constraintStart_toStartOf="parent"
sysui:layout_constraintEnd_toStartOf="@id/done"
sysui:layout_constraintHorizontal_chainStyle="spread_inside"
style="@style/Widget.QSDialog.Button.BorderButton"
/>
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
sysui:barrierDirection="top"
sysui:constraint_referenced_ids="settings,done"
/>
<Button
android:id="@+id/done"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:text="@string/quick_settings_done"
sysui:layout_constraintTop_toBottomOf="@id/barrier"
sysui:layout_constraintBottom_toBottomOf="parent"
sysui:layout_constraintStart_toEndOf="@id/settings"
sysui:layout_constraintEnd_toEndOf="parent"
style="@style/Widget.QSDialog.Button"
/>
<Button
android:id="@+id/settings"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:text="@string/quick_settings_more_user_settings"
sysui:layout_constraintTop_toBottomOf="@id/barrier"
sysui:layout_constraintBottom_toBottomOf="parent"
sysui:layout_constraintStart_toStartOf="parent"
sysui:layout_constraintEnd_toStartOf="@id/done"
sysui:layout_constraintHorizontal_chainStyle="spread_inside"
style="@style/Widget.QSDialog.Button.BorderButton"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>
<Button
android:id="@+id/done"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:text="@string/quick_settings_done"
sysui:layout_constraintTop_toBottomOf="@id/barrier"
sysui:layout_constraintBottom_toBottomOf="parent"
sysui:layout_constraintStart_toEndOf="@id/settings"
sysui:layout_constraintEnd_toEndOf="parent"
style="@style/Widget.QSDialog.Button"
/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -17,8 +17,7 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/rounded_bg_full">
android:orientation="vertical">
<!-- Scrollview is necessary to fit everything in landscape layout -->
<ScrollView

View File

@@ -22,7 +22,6 @@ import com.android.systemui.ForegroundServicesDialog;
import com.android.systemui.keyguard.WorkLockActivity;
import com.android.systemui.people.PeopleSpaceActivity;
import com.android.systemui.people.widget.LaunchConversationActivity;
import com.android.systemui.screenrecord.ScreenRecordDialog;
import com.android.systemui.screenshot.LongScreenshotActivity;
import com.android.systemui.sensorprivacy.SensorUseStartedActivity;
import com.android.systemui.sensorprivacy.television.TvUnblockSensorActivity;
@@ -67,12 +66,6 @@ public abstract class DefaultActivityBinder {
@ClassKey(BrightnessDialog.class)
public abstract Activity bindBrightnessDialog(BrightnessDialog activity);
/** Inject into ScreenRecordDialog */
@Binds
@IntoMap
@ClassKey(ScreenRecordDialog.class)
public abstract Activity bindScreenRecordDialog(ScreenRecordDialog activity);
/** Inject into UsbDebuggingActivity. */
@Binds
@IntoMap

View File

@@ -14,7 +14,6 @@
package com.android.systemui.qs.tiles;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.os.Handler;
import android.os.Looper;
@@ -29,6 +28,7 @@ import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.systemui.Prefs;
import com.android.systemui.R;
import com.android.systemui.animation.DialogLaunchAnimator;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.ActivityStarter;
@@ -47,6 +47,7 @@ public class DataSaverTile extends QSTileImpl<BooleanState> implements
DataSaverController.Listener{
private final DataSaverController mDataSaverController;
private final DialogLaunchAnimator mDialogLaunchAnimator;
@Inject
public DataSaverTile(
@@ -58,11 +59,13 @@ public class DataSaverTile extends QSTileImpl<BooleanState> implements
StatusBarStateController statusBarStateController,
ActivityStarter activityStarter,
QSLogger qsLogger,
DataSaverController dataSaverController
DataSaverController dataSaverController,
DialogLaunchAnimator dialogLaunchAnimator
) {
super(host, backgroundLooper, mainHandler, falsingManager, metricsLogger,
statusBarStateController, activityStarter, qsLogger);
mDataSaverController = dataSaverController;
mDialogLaunchAnimator = dialogLaunchAnimator;
mDataSaverController.observe(getLifecycle(), this);
}
@@ -83,18 +86,27 @@ public class DataSaverTile extends QSTileImpl<BooleanState> implements
toggleDataSaver();
return;
}
// Shows dialog first
SystemUIDialog dialog = new SystemUIDialog(mContext);
dialog.setTitle(com.android.internal.R.string.data_saver_enable_title);
dialog.setMessage(com.android.internal.R.string.data_saver_description);
dialog.setPositiveButton(com.android.internal.R.string.data_saver_enable_button,
(OnClickListener) (dialogInterface, which) -> {
toggleDataSaver();
Prefs.putBoolean(mContext, Prefs.Key.QS_DATA_SAVER_DIALOG_SHOWN, true);
});
dialog.setNegativeButton(com.android.internal.R.string.cancel, null);
dialog.setShowForAllUsers(true);
dialog.show();
// Show a dialog to confirm first. Dialogs shown by the DialogLaunchAnimator must be created
// and shown on the main thread, so we post it to the UI handler.
mUiHandler.post(() -> {
SystemUIDialog dialog = new SystemUIDialog(mContext);
dialog.setTitle(com.android.internal.R.string.data_saver_enable_title);
dialog.setMessage(com.android.internal.R.string.data_saver_description);
dialog.setPositiveButton(com.android.internal.R.string.data_saver_enable_button,
(dialogInterface, which) -> {
toggleDataSaver();
Prefs.putBoolean(mContext, Prefs.Key.QS_DATA_SAVER_DIALOG_SHOWN, true);
});
dialog.setNegativeButton(com.android.internal.R.string.cancel, null);
dialog.setShowForAllUsers(true);
if (view != null) {
mDialogLaunchAnimator.showFromView(dialog, view);
} else {
dialog.show();
}
});
}
private void toggleDataSaver() {

View File

@@ -29,6 +29,7 @@ import androidx.annotation.Nullable;
import com.android.internal.logging.MetricsLogger;
import com.android.systemui.R;
import com.android.systemui.animation.DialogLaunchAnimator;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.ActivityStarter;
@@ -39,7 +40,9 @@ import com.android.systemui.qs.QSHost;
import com.android.systemui.qs.logging.QSLogger;
import com.android.systemui.qs.tileimpl.QSTileImpl;
import com.android.systemui.screenrecord.RecordingController;
import com.android.systemui.screenrecord.ScreenRecordDialog;
import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import javax.inject.Inject;
@@ -49,10 +52,13 @@ import javax.inject.Inject;
public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState>
implements RecordingController.RecordingStateChangeCallback {
private static final String TAG = "ScreenRecordTile";
private RecordingController mController;
private KeyguardDismissUtil mKeyguardDismissUtil;
private final RecordingController mController;
private final KeyguardDismissUtil mKeyguardDismissUtil;
private final KeyguardStateController mKeyguardStateController;
private final Callback mCallback = new Callback();
private final DialogLaunchAnimator mDialogLaunchAnimator;
private long mMillisUntilFinished = 0;
private Callback mCallback = new Callback();
@Inject
public ScreenRecordTile(
@@ -65,13 +71,17 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState>
ActivityStarter activityStarter,
QSLogger qsLogger,
RecordingController controller,
KeyguardDismissUtil keyguardDismissUtil
KeyguardDismissUtil keyguardDismissUtil,
KeyguardStateController keyguardStateController,
DialogLaunchAnimator dialogLaunchAnimator
) {
super(host, backgroundLooper, mainHandler, falsingManager, metricsLogger,
statusBarStateController, activityStarter, qsLogger);
mController = controller;
mController.observe(this, mCallback);
mKeyguardDismissUtil = keyguardDismissUtil;
mKeyguardStateController = keyguardStateController;
mDialogLaunchAnimator = dialogLaunchAnimator;
}
@Override
@@ -89,7 +99,7 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState>
} else if (mController.isRecording()) {
stopRecording();
} else {
mUiHandler.post(() -> showPrompt());
mUiHandler.post(() -> showPrompt(view));
}
refreshState();
}
@@ -136,15 +146,33 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState>
return mContext.getString(R.string.quick_settings_screen_record_label);
}
private void showPrompt() {
// Close QS, otherwise the dialog appears beneath it
getHost().collapsePanels();
Intent intent = mController.getPromptIntent();
private void showPrompt(@Nullable View view) {
// We animate from the touched view only if we are not on the keyguard, given that if we
// are we will dismiss it which will also collapse the shade.
boolean shouldAnimateFromView = view != null && !mKeyguardStateController.isShowing();
// Create the recording dialog that will collapse the shade only if we start the recording.
Runnable onStartRecordingClicked = () -> {
// We dismiss the shade. Since starting the recording will also dismiss the dialog, we
// disable the exit animation which looks weird when it happens at the same time as the
// shade collapsing.
mDialogLaunchAnimator.disableAllCurrentDialogsExitAnimations();
getHost().collapsePanels();
};
ScreenRecordDialog dialog = mController.createScreenRecordDialog(mContext,
onStartRecordingClicked);
ActivityStarter.OnDismissAction dismissAction = () -> {
mHost.getUserContext().startActivity(intent);
if (shouldAnimateFromView) {
mDialogLaunchAnimator.showFromView(dialog, view);
} else {
dialog.show();
}
return false;
};
mKeyguardDismissUtil.executeWhenUnlocked(dismissAction, false, false);
mKeyguardDismissUtil.executeWhenUnlocked(dismissAction, false /* requiresShadeOpen */,
true /* afterKeyguardDone */);
}
private void cancelCountdown() {

View File

@@ -63,7 +63,8 @@ class InternetDialogFactory @Inject constructor(
canConfigMobileData, canConfigWifi, aboveStatusBar, uiEventLogger, handler,
executor)
if (view != null) {
dialogLaunchAnimator.showFromView(internetDialog!!, view)
dialogLaunchAnimator.showFromView(internetDialog!!, view,
animateBackgroundBoundsChange = true)
} else {
internetDialog?.show()
}

View File

@@ -18,7 +18,6 @@ package com.android.systemui.screenrecord;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
@@ -27,10 +26,12 @@ import android.os.UserHandle;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.settings.UserContextProvider;
import com.android.systemui.statusbar.policy.CallbackController;
import java.util.concurrent.CopyOnWriteArrayList;
@@ -44,15 +45,13 @@ import javax.inject.Inject;
public class RecordingController
implements CallbackController<RecordingController.RecordingStateChangeCallback> {
private static final String TAG = "RecordingController";
private static final String SYSUI_PACKAGE = "com.android.systemui";
private static final String SYSUI_SCREENRECORD_LAUNCHER =
"com.android.systemui.screenrecord.ScreenRecordDialog";
private boolean mIsStarting;
private boolean mIsRecording;
private PendingIntent mStopIntent;
private CountDownTimer mCountDownTimer = null;
private BroadcastDispatcher mBroadcastDispatcher;
private UserContextProvider mUserContextProvider;
protected static final String INTENT_UPDATE_STATE =
"com.android.systemui.screenrecord.UPDATE_STATE";
@@ -88,20 +87,16 @@ public class RecordingController
* Create a new RecordingController
*/
@Inject
public RecordingController(BroadcastDispatcher broadcastDispatcher) {
public RecordingController(BroadcastDispatcher broadcastDispatcher,
UserContextProvider userContextProvider) {
mBroadcastDispatcher = broadcastDispatcher;
mUserContextProvider = userContextProvider;
}
/**
* Get an intent to show screen recording options to the user.
*/
public Intent getPromptIntent() {
final ComponentName launcherComponent = new ComponentName(SYSUI_PACKAGE,
SYSUI_SCREENRECORD_LAUNCHER);
final Intent intent = new Intent();
intent.setComponent(launcherComponent);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
return intent;
/** Create a dialog to show screen recording options to the user. */
public ScreenRecordDialog createScreenRecordDialog(Context context,
@Nullable Runnable onStartRecordingClicked) {
return new ScreenRecordDialog(context, this, mUserContextProvider, onStartRecordingClicked);
}
/**

View File

@@ -26,7 +26,6 @@ import android.app.PendingIntent;
import android.content.Context;
import android.os.Bundle;
import android.view.Gravity;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.widget.ArrayAdapter;
@@ -34,34 +33,38 @@ import android.widget.Spinner;
import android.widget.Switch;
import android.widget.TextView;
import androidx.annotation.Nullable;
import com.android.systemui.R;
import com.android.systemui.settings.UserContextProvider;
import com.android.systemui.statusbar.phone.SystemUIDialog;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.inject.Inject;
/**
* Activity to select screen recording options
* Dialog to select screen recording options
*/
public class ScreenRecordDialog extends Activity {
public class ScreenRecordDialog extends SystemUIDialog {
private static final List<ScreenRecordingAudioSource> MODES = Arrays.asList(INTERNAL, MIC,
MIC_AND_INTERNAL);
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 final UserContextProvider mUserContextProvider;
@Nullable
private final Runnable mOnStartRecordingClicked;
private Switch mTapsSwitch;
private Switch mAudioSwitch;
private Spinner mOptions;
private List<ScreenRecordingAudioSource> mModes;
@Inject
public ScreenRecordDialog(RecordingController controller,
UserContextProvider userContextProvider) {
public ScreenRecordDialog(Context context, RecordingController controller,
UserContextProvider userContextProvider, @Nullable Runnable onStartRecordingClicked) {
super(context);
mController = controller;
mUserContextProvider = userContextProvider;
mOnStartRecordingClicked = onStartRecordingClicked;
}
@Override
@@ -69,37 +72,35 @@ public class ScreenRecordDialog extends Activity {
super.onCreate(savedInstanceState);
Window window = getWindow();
// Inflate the decor view, so the attributes below are not overwritten by the theme.
window.getDecorView();
window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
window.addPrivateFlags(WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS);
window.setGravity(Gravity.TOP);
window.setGravity(Gravity.CENTER);
setTitle(R.string.screenrecord_name);
setContentView(R.layout.screen_record_dialog);
TextView cancelBtn = findViewById(R.id.button_cancel);
cancelBtn.setOnClickListener(v -> {
finish();
});
cancelBtn.setOnClickListener(v -> dismiss());
TextView startBtn = findViewById(R.id.button_start);
startBtn.setOnClickListener(v -> {
requestScreenCapture();
finish();
});
if (mOnStartRecordingClicked != null) {
// Note that it is important to run this callback before dismissing, so that the
// callback can disable the dialog exit animation if it wants to.
mOnStartRecordingClicked.run();
}
mModes = new ArrayList<>();
mModes.add(INTERNAL);
mModes.add(MIC);
mModes.add(MIC_AND_INTERNAL);
requestScreenCapture();
dismiss();
});
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(),
ArrayAdapter a = new ScreenRecordingAdapter(getContext().getApplicationContext(),
android.R.layout.simple_spinner_dropdown_item,
mModes);
MODES);
a.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
mOptions.setAdapter(a);
mOptions.setOnItemClickListenerInt((parent, view, position, id) -> {
@@ -116,7 +117,7 @@ public class ScreenRecordDialog extends Activity {
PendingIntent startIntent = PendingIntent.getForegroundService(userContext,
RecordingService.REQUEST_CODE,
RecordingService.getStartIntent(
userContext, RESULT_OK,
userContext, Activity.RESULT_OK,
audioMode.ordinal(), showTaps),
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
PendingIntent stopIntent = PendingIntent.getService(userContext,

View File

@@ -60,9 +60,20 @@ class DialogLaunchAnimatorTest : SysuiTestCase() {
assertEquals(0, dialog.findViewById<ViewGroup>(android.R.id.content).childCount)
assertEquals(1, hostDialogContent.childCount)
// The original dialog content is added to another view that is the same size as the
// original dialog window.
val hostDialogRoot = hostDialogContent.getChildAt(0) as ViewGroup
assertEquals(1, hostDialogRoot.childCount)
assertEquals(dialog.contentView, hostDialogRoot.getChildAt(0))
val dialogContentParent = hostDialogRoot.getChildAt(0) as ViewGroup
assertEquals(1, dialogContentParent.childCount)
assertEquals(TestDialog.DIALOG_WIDTH, dialogContentParent.layoutParams.width)
assertEquals(TestDialog.DIALOG_HEIGHT, dialogContentParent.layoutParams.height)
val dialogContent = dialogContentParent.getChildAt(0)
assertEquals(dialog.contentView, dialogContent)
assertEquals(ViewGroup.LayoutParams.MATCH_PARENT, dialogContent.layoutParams.width)
assertEquals(ViewGroup.LayoutParams.MATCH_PARENT, dialogContent.layoutParams.height)
// Hiding/showing/dismissing the dialog should hide/show/dismiss the host dialog given that
// it's a ListenableDialog.
@@ -126,6 +137,11 @@ class DialogLaunchAnimatorTest : SysuiTestCase() {
}
private class TestDialog(context: Context) : Dialog(context), ListenableDialog {
companion object {
const val DIALOG_WIDTH = 100
const val DIALOG_HEIGHT = 200
}
private val listeners = hashSetOf<DialogListener>()
val contentView = View(context)
var onStartCalled = false
@@ -138,6 +154,7 @@ class DialogLaunchAnimatorTest : SysuiTestCase() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.setLayout(DIALOG_WIDTH, DIALOG_HEIGHT)
setContentView(contentView)
}

View File

@@ -17,9 +17,11 @@
package com.android.systemui.qs.tiles;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertTrue;
import static org.junit.Assert.assertFalse;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -34,6 +36,7 @@ import androidx.test.filters.SmallTest;
import com.android.internal.logging.MetricsLogger;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.animation.DialogLaunchAnimator;
import com.android.systemui.classifier.FalsingManagerFake;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -41,10 +44,12 @@ import com.android.systemui.qs.QSTileHost;
import com.android.systemui.qs.logging.QSLogger;
import com.android.systemui.screenrecord.RecordingController;
import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@@ -67,6 +72,10 @@ public class ScreenRecordTileTest extends SysuiTestCase {
private ActivityStarter mActivityStarter;
@Mock
private QSLogger mQSLogger;
@Mock
private KeyguardStateController mKeyguardStateController;
@Mock
private DialogLaunchAnimator mDialogLaunchAnimator;
private TestableLooper mTestableLooper;
private ScreenRecordTile mTile;
@@ -89,7 +98,9 @@ public class ScreenRecordTileTest extends SysuiTestCase {
mActivityStarter,
mQSLogger,
mController,
mKeyguardDismissUtil
mKeyguardDismissUtil,
mKeyguardStateController,
mDialogLaunchAnimator
);
mTile.initialize();
@@ -112,7 +123,15 @@ public class ScreenRecordTileTest extends SysuiTestCase {
mTile.handleClick(null /* view */);
mTestableLooper.processAllMessages();
verify(mController, times(1)).getPromptIntent();
ArgumentCaptor<Runnable> onStartRecordingClicked = ArgumentCaptor.forClass(Runnable.class);
verify(mController).createScreenRecordDialog(any(), onStartRecordingClicked.capture());
// When starting the recording, we collapse the shade and disable the dialog animation.
assertNotNull(onStartRecordingClicked.getValue());
onStartRecordingClicked.getValue().run();
verify(mDialogLaunchAnimator).disableAllCurrentDialogsExitAnimations();
verify(mHost).collapsePanels();
}
// Test that the tile is active and labeled correctly when the controller is starting

View File

@@ -32,6 +32,7 @@ import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.settings.UserContextProvider;
import org.junit.Before;
import org.junit.Test;
@@ -52,6 +53,8 @@ public class RecordingControllerTest extends SysuiTestCase {
private RecordingController.RecordingStateChangeCallback mCallback;
@Mock
private BroadcastDispatcher mBroadcastDispatcher;
@Mock
private UserContextProvider mUserContextProvider;
private RecordingController mController;
@@ -60,7 +63,7 @@ public class RecordingControllerTest extends SysuiTestCase {
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mController = new RecordingController(mBroadcastDispatcher);
mController = new RecordingController(mBroadcastDispatcher, mUserContextProvider);
mController.addCallback(mCallback);
}