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:exported="false"
android:finishOnTaskLaunch="true" /> android:finishOnTaskLaunch="true" />
<activity android:name=".screenrecord.ScreenRecordDialog"
android:theme="@style/ScreenRecord"
android:showForAllUsers="true"
android:excludeFromRecents="true" />
<service android:name=".screenrecord.RecordingService" /> <service android:name=".screenrecord.RecordingService" />
<receiver android:name=".SysuiRestartReceiver" <receiver android:name=".SysuiRestartReceiver"

View File

@@ -16,11 +16,16 @@
package com.android.systemui.animation package com.android.systemui.animation
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
import android.app.Dialog import android.app.Dialog
import android.content.Context import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.graphics.Rect
import android.os.Looper import android.os.Looper
import android.util.Log import android.util.Log
import android.util.MathUtils
import android.view.GhostView import android.view.GhostView
import android.view.Gravity import android.view.Gravity
import android.view.View 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.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
import android.view.WindowManagerPolicyConstants import android.view.WindowManagerPolicyConstants
import android.widget.FrameLayout import android.widget.FrameLayout
import kotlin.math.roundToInt
private const val TAG = "DialogLaunchAnimator" private const val TAG = "DialogLaunchAnimator"
@@ -52,7 +58,8 @@ class DialogLaunchAnimator(
private val currentAnimations = hashSetOf<DialogLaunchAnimation>() 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 * 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 * 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 * must call dismiss(), hide() and show() on the [Dialog] returned by this function to actually
* dismiss, hide or show the dialog. * 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()) { if (Looper.myLooper() != Looper.getMainLooper()) {
throw IllegalStateException( throw IllegalStateException(
"showFromView must be called from the main thread and dialog must be created in " + "showFromView must be called from the main thread and dialog must be created in " +
@@ -78,7 +90,8 @@ class DialogLaunchAnimator(
val launchAnimation = DialogLaunchAnimation( val launchAnimation = DialogLaunchAnimation(
context, launchAnimator, hostDialogProvider, view, context, launchAnimator, hostDialogProvider, view,
onDialogDismissed = { currentAnimations.remove(it) }, originalDialog = dialog) onDialogDismissed = { currentAnimations.remove(it) }, originalDialog = dialog,
animateBackgroundBoundsChange)
val hostDialog = launchAnimation.hostDialog val hostDialog = launchAnimation.hostDialog
currentAnimations.add(launchAnimation) currentAnimations.add(launchAnimation)
@@ -208,7 +221,10 @@ private class DialogLaunchAnimation(
private val onDialogDismissed: (DialogLaunchAnimation) -> Unit, private val onDialogDismissed: (DialogLaunchAnimation) -> Unit,
/** The original dialog whose content will be shown and animate in/out in [hostDialog]. */ /** 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 * 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) private val hostDialogRoot = FrameLayout(context)
/** /**
* The content view of [originalDialog], which will be stolen from that dialog and added to * The parent of the original dialog content view, that serves as a fake window that will have
* [hostDialogRoot]. * 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] * The background color of [originalDialogView], taking into consideration the [originalDialog]
@@ -246,6 +263,11 @@ private class DialogLaunchAnimation(
private var isTouchSurfaceGhostDrawn = false private var isTouchSurfaceGhostDrawn = false
private var isOriginalDialogViewLaidOut = false private var isOriginalDialogViewLaidOut = false
private var backgroundLayoutListener = if (animateBackgroundBoundsChange) {
AnimatedBoundsLayoutListener()
} else {
null
}
fun start() { fun start() {
// Show the host (fullscreen) dialog, to which we will add the stolen dialog view. // 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) { 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. // Close the dialog when clicking outside of it.
hostDialogRoot.setOnClickListener { hostDialog.dismiss() } hostDialogRoot.setOnClickListener { hostDialog.dismiss() }
dialogView.isClickable = true dialogView.isClickable = true
@@ -394,17 +413,13 @@ private class DialogLaunchAnimation(
throw IllegalStateException("Dialogs with no backgrounds on window are not supported") throw IllegalStateException("Dialogs with no backgrounds on window are not supported")
} }
dialogView.setBackgroundResource(backgroundRes) // Add a parent view to the original dialog view to which we will set the original dialog
originalDialogBackgroundColor = // window background. This View serves as a fake window with background, so that we are sure
GhostedViewLaunchAnimatorController.findGradientDrawable(dialogView.background!!) // that we don't override the dialog view paddings with the window background that usually
?.color // has insets.
?.defaultColor ?: Color.BLACK dialogContentParent.setBackgroundResource(backgroundRes)
// 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)
hostDialogRoot.addView( hostDialogRoot.addView(
dialogView, dialogContentParent,
// We give it the size of its original dialog window. // We give it the size of its original dialog window.
FrameLayout.LayoutParams( FrameLayout.LayoutParams(
@@ -413,10 +428,31 @@ private class DialogLaunchAnimation(
Gravity.CENTER 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. // 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( override fun onLayoutChange(
view: View, view: View,
left: Int, left: Int,
@@ -428,7 +464,7 @@ private class DialogLaunchAnimation(
oldRight: Int, oldRight: Int,
oldBottom: Int oldBottom: Int
) { ) {
dialogView.removeOnLayoutChangeListener(this) dialogContentParent.removeOnLayoutChangeListener(this)
isOriginalDialogViewLaidOut = true isOriginalDialogViewLaidOut = true
maybeStartLaunchAnimation() maybeStartLaunchAnimation()
@@ -479,6 +515,13 @@ private class DialogLaunchAnimation(
if (dismissRequested) { if (dismissRequested) {
hostDialog.dismiss() 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 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 // 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 // removed only once we draw the touch surface, to avoid flickering that would
@@ -578,12 +625,10 @@ private class DialogLaunchAnimation(
onLaunchAnimationStart: () -> Unit = {}, onLaunchAnimationStart: () -> Unit = {},
onLaunchAnimationEnd: () -> Unit = {} onLaunchAnimationEnd: () -> Unit = {}
) { ) {
val dialogView = this.originalDialogView!!
// Create 2 ghost controllers to animate both the dialog and the touch surface in the host // Create 2 ghost controllers to animate both the dialog and the touch surface in the host
// dialog. // dialog.
val startView = if (isLaunching) touchSurface else dialogView val startView = if (isLaunching) touchSurface else dialogContentParent
val endView = if (isLaunching) dialogView else touchSurface val endView = if (isLaunching) dialogContentParent else touchSurface
val startViewController = GhostedViewLaunchAnimatorController(startView) val startViewController = GhostedViewLaunchAnimatorController(startView)
val endViewController = GhostedViewLaunchAnimatorController(endView) val endViewController = GhostedViewLaunchAnimatorController(endView)
startViewController.launchContainer = hostDialogRoot startViewController.launchContainer = hostDialogRoot
@@ -662,4 +707,81 @@ private class DialogLaunchAnimation(
return (touchSurface.parent as? View)?.isShown ?: true 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. ~ limitations under the License.
--> -->
<FrameLayout <androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:sysui="http://schemas.android.com/apk/res-auto" xmlns:sysui="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
<androidx.constraintlayout.widget.ConstraintLayout android:padding="24dp"
android:layout_width="match_parent" android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
>
<TextView
android:id="@+id/title"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="24dp" android:layout_width="0dp"
android:layout_marginStart="16dp" android:textAlignment="center"
android:layout_marginEnd="16dp" android:text="@string/qs_user_switch_dialog_title"
> android:textAppearance="@style/TextAppearance.QSDialog.Title"
<TextView android:layout_marginBottom="32dp"
android:id="@+id/title" sysui:layout_constraintTop_toTopOf="parent"
android:layout_height="wrap_content" sysui:layout_constraintStart_toStartOf="parent"
android:layout_width="0dp" sysui:layout_constraintEnd_toEndOf="parent"
android:textAlignment="center" sysui:layout_constraintBottom_toTopOf="@id/grid"
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"
/> />
<androidx.constraintlayout.widget.Barrier <com.android.systemui.qs.PseudoGridView
android:id="@+id/barrier" android:id="@+id/grid"
android:layout_height="wrap_content" android:layout_width="wrap_content"
android:layout_width="wrap_content" android:layout_height="wrap_content"
sysui:barrierDirection="top" android:layout_marginBottom="28dp"
sysui:constraint_referenced_ids="settings,done" 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 <androidx.constraintlayout.widget.Barrier
android:id="@+id/settings" android:id="@+id/barrier"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_height="48dp" android:layout_width="wrap_content"
android:text="@string/quick_settings_more_user_settings" sysui:barrierDirection="top"
sysui:layout_constraintTop_toBottomOf="@id/barrier" sysui:constraint_referenced_ids="settings,done"
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"
/>
<Button <Button
android:id="@+id/done" android:id="@+id/settings"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="48dp" android:layout_height="48dp"
android:text="@string/quick_settings_done" android:text="@string/quick_settings_more_user_settings"
sysui:layout_constraintTop_toBottomOf="@id/barrier" sysui:layout_constraintTop_toBottomOf="@id/barrier"
sysui:layout_constraintBottom_toBottomOf="parent" sysui:layout_constraintBottom_toBottomOf="parent"
sysui:layout_constraintStart_toEndOf="@id/settings" sysui:layout_constraintStart_toStartOf="parent"
sysui:layout_constraintEnd_toEndOf="parent" sysui:layout_constraintEnd_toStartOf="@id/done"
style="@style/Widget.QSDialog.Button" sysui:layout_constraintHorizontal_chainStyle="spread_inside"
/> style="@style/Widget.QSDialog.Button.BorderButton"
/>
</androidx.constraintlayout.widget.ConstraintLayout> <Button
</FrameLayout> 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" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical">
android:background="@drawable/rounded_bg_full">
<!-- Scrollview is necessary to fit everything in landscape layout --> <!-- Scrollview is necessary to fit everything in landscape layout -->
<ScrollView <ScrollView

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -26,7 +26,6 @@ import android.app.PendingIntent;
import android.content.Context; import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import android.view.Gravity; import android.view.Gravity;
import android.view.ViewGroup;
import android.view.Window; import android.view.Window;
import android.view.WindowManager; import android.view.WindowManager;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
@@ -34,34 +33,38 @@ import android.widget.Spinner;
import android.widget.Switch; import android.widget.Switch;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.Nullable;
import com.android.systemui.R; import com.android.systemui.R;
import com.android.systemui.settings.UserContextProvider; 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 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 DELAY_MS = 3000;
private static final long INTERVAL_MS = 1000; private static final long INTERVAL_MS = 1000;
private static final String TAG = "ScreenRecordDialog";
private final RecordingController mController; private final RecordingController mController;
private final UserContextProvider mUserContextProvider; private final UserContextProvider mUserContextProvider;
@Nullable
private final Runnable mOnStartRecordingClicked;
private Switch mTapsSwitch; private Switch mTapsSwitch;
private Switch mAudioSwitch; private Switch mAudioSwitch;
private Spinner mOptions; private Spinner mOptions;
private List<ScreenRecordingAudioSource> mModes;
@Inject public ScreenRecordDialog(Context context, RecordingController controller,
public ScreenRecordDialog(RecordingController controller, UserContextProvider userContextProvider, @Nullable Runnable onStartRecordingClicked) {
UserContextProvider userContextProvider) { super(context);
mController = controller; mController = controller;
mUserContextProvider = userContextProvider; mUserContextProvider = userContextProvider;
mOnStartRecordingClicked = onStartRecordingClicked;
} }
@Override @Override
@@ -69,37 +72,35 @@ public class ScreenRecordDialog extends Activity {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
Window window = getWindow(); 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.addPrivateFlags(WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS);
window.setGravity(Gravity.TOP);
window.setGravity(Gravity.CENTER);
setTitle(R.string.screenrecord_name); setTitle(R.string.screenrecord_name);
setContentView(R.layout.screen_record_dialog); setContentView(R.layout.screen_record_dialog);
TextView cancelBtn = findViewById(R.id.button_cancel); TextView cancelBtn = findViewById(R.id.button_cancel);
cancelBtn.setOnClickListener(v -> { cancelBtn.setOnClickListener(v -> dismiss());
finish();
});
TextView startBtn = findViewById(R.id.button_start); TextView startBtn = findViewById(R.id.button_start);
startBtn.setOnClickListener(v -> { startBtn.setOnClickListener(v -> {
requestScreenCapture(); if (mOnStartRecordingClicked != null) {
finish(); // 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<>(); requestScreenCapture();
mModes.add(INTERNAL); dismiss();
mModes.add(MIC); });
mModes.add(MIC_AND_INTERNAL);
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);
ArrayAdapter a = new ScreenRecordingAdapter(getApplicationContext(), ArrayAdapter a = new ScreenRecordingAdapter(getContext().getApplicationContext(),
android.R.layout.simple_spinner_dropdown_item, android.R.layout.simple_spinner_dropdown_item,
mModes); MODES);
a.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); a.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
mOptions.setAdapter(a); mOptions.setAdapter(a);
mOptions.setOnItemClickListenerInt((parent, view, position, id) -> { mOptions.setOnItemClickListenerInt((parent, view, position, id) -> {
@@ -116,7 +117,7 @@ public class ScreenRecordDialog extends Activity {
PendingIntent startIntent = PendingIntent.getForegroundService(userContext, PendingIntent startIntent = PendingIntent.getForegroundService(userContext,
RecordingService.REQUEST_CODE, RecordingService.REQUEST_CODE,
RecordingService.getStartIntent( RecordingService.getStartIntent(
userContext, RESULT_OK, userContext, Activity.RESULT_OK,
audioMode.ordinal(), showTaps), audioMode.ordinal(), showTaps),
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
PendingIntent stopIntent = PendingIntent.getService(userContext, 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(0, dialog.findViewById<ViewGroup>(android.R.id.content).childCount)
assertEquals(1, hostDialogContent.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 val hostDialogRoot = hostDialogContent.getChildAt(0) as ViewGroup
assertEquals(1, hostDialogRoot.childCount) 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 // Hiding/showing/dismissing the dialog should hide/show/dismiss the host dialog given that
// it's a ListenableDialog. // it's a ListenableDialog.
@@ -126,6 +137,11 @@ class DialogLaunchAnimatorTest : SysuiTestCase() {
} }
private class TestDialog(context: Context) : Dialog(context), ListenableDialog { 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>() private val listeners = hashSetOf<DialogListener>()
val contentView = View(context) val contentView = View(context)
var onStartCalled = false var onStartCalled = false
@@ -138,6 +154,7 @@ class DialogLaunchAnimatorTest : SysuiTestCase() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
window.setLayout(DIALOG_WIDTH, DIALOG_HEIGHT)
setContentView(contentView) setContentView(contentView)
} }

View File

@@ -17,9 +17,11 @@
package com.android.systemui.qs.tiles; package com.android.systemui.qs.tiles;
import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertTrue; 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.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;
@@ -34,6 +36,7 @@ import androidx.test.filters.SmallTest;
import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.MetricsLogger;
import com.android.systemui.R; 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.classifier.FalsingManagerFake; import com.android.systemui.classifier.FalsingManagerFake;
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;
@@ -41,10 +44,12 @@ import com.android.systemui.qs.QSTileHost;
import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.logging.QSLogger;
import com.android.systemui.screenrecord.RecordingController; import com.android.systemui.screenrecord.RecordingController;
import com.android.systemui.statusbar.phone.KeyguardDismissUtil; import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
@@ -67,6 +72,10 @@ public class ScreenRecordTileTest extends SysuiTestCase {
private ActivityStarter mActivityStarter; private ActivityStarter mActivityStarter;
@Mock @Mock
private QSLogger mQSLogger; private QSLogger mQSLogger;
@Mock
private KeyguardStateController mKeyguardStateController;
@Mock
private DialogLaunchAnimator mDialogLaunchAnimator;
private TestableLooper mTestableLooper; private TestableLooper mTestableLooper;
private ScreenRecordTile mTile; private ScreenRecordTile mTile;
@@ -89,7 +98,9 @@ public class ScreenRecordTileTest extends SysuiTestCase {
mActivityStarter, mActivityStarter,
mQSLogger, mQSLogger,
mController, mController,
mKeyguardDismissUtil mKeyguardDismissUtil,
mKeyguardStateController,
mDialogLaunchAnimator
); );
mTile.initialize(); mTile.initialize();
@@ -112,7 +123,15 @@ public class ScreenRecordTileTest extends SysuiTestCase {
mTile.handleClick(null /* view */); mTile.handleClick(null /* view */);
mTestableLooper.processAllMessages(); 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 // 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.SysuiTestCase;
import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.settings.UserContextProvider;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@@ -52,6 +53,8 @@ public class RecordingControllerTest extends SysuiTestCase {
private RecordingController.RecordingStateChangeCallback mCallback; private RecordingController.RecordingStateChangeCallback mCallback;
@Mock @Mock
private BroadcastDispatcher mBroadcastDispatcher; private BroadcastDispatcher mBroadcastDispatcher;
@Mock
private UserContextProvider mUserContextProvider;
private RecordingController mController; private RecordingController mController;
@@ -60,7 +63,7 @@ public class RecordingControllerTest extends SysuiTestCase {
@Before @Before
public void setUp() { public void setUp() {
MockitoAnnotations.initMocks(this); MockitoAnnotations.initMocks(this);
mController = new RecordingController(mBroadcastDispatcher); mController = new RecordingController(mBroadcastDispatcher, mUserContextProvider);
mController.addCallback(mCallback); mController.addCallback(mCallback);
} }