summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Robert Snoeberger <snoeberger@google.com> 2020-03-20 11:56:22 -0400
committer Robert Snoeberger <snoeberger@google.com> 2020-03-24 00:16:43 -0400
commitb6464ea36c09b60e00dd1366b633bb8682a856d9 (patch)
treef0fb718fe8eac3f346321466646f5875ae8259bb
parent25eee749f91459bf57081026bf9011120548a8a8 (diff)
Lock screen media controls
Media controls are added as a new bucket/section to the NSSL. This bucket is only shown on the lock screen. Bug: 150454272 Test: manual - play music and check that controls appear on lock screen Test: manual - play music and check that controls don't appear in shade Change-Id: Ibbeebf05db9400048dae48dbbd8d3865664513a1
-rw-r--r--packages/SystemUI/res/layout/keyguard_media_header.xml153
-rw-r--r--packages/SystemUI/src/com/android/keyguard/KeyguardMediaPlayer.java266
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java18
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java3
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/MediaNotificationProcessor.java21
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt13
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaHeaderView.java55
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.java66
-rw-r--r--packages/SystemUI/tests/src/com/android/keyguard/KeyguardMediaPlayerTest.kt120
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt11
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java77
11 files changed, 790 insertions, 13 deletions
diff --git a/packages/SystemUI/res/layout/keyguard_media_header.xml b/packages/SystemUI/res/layout/keyguard_media_header.xml
new file mode 100644
index 000000000000..9c2d244cb8ec
--- /dev/null
+++ b/packages/SystemUI/res/layout/keyguard_media_header.xml
@@ -0,0 +1,153 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2020 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<!-- Layout for media controls on the lockscreen -->
+<com.android.systemui.statusbar.notification.stack.MediaHeaderView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingStart="0dp"
+ android:paddingEnd="0dp"
+ android:focusable="true"
+ android:clickable="true"
+>
+
+ <!-- Background views required by ActivatableNotificationView. -->
+ <com.android.systemui.statusbar.notification.row.NotificationBackgroundView
+ android:id="@+id/backgroundNormal"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ />
+
+ <com.android.systemui.statusbar.notification.row.NotificationBackgroundView
+ android:id="@+id/backgroundDimmed"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ />
+
+ <com.android.systemui.statusbar.notification.FakeShadowView
+ android:id="@+id/fake_shadow"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ />
+
+ <!-- Layout for media controls. -->
+ <LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/keyguard_media_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center_horizontal|fill_vertical"
+ android:padding="16dp"
+ >
+ <ImageView
+ android:id="@+id/album_art"
+ android:layout_width="@dimen/qs_media_album_size"
+ android:layout_height="@dimen/qs_media_album_size"
+ android:layout_marginRight="16dp"
+ android:layout_weight="0"
+ />
+
+ <!-- Media information -->
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="0dp"
+ android:layout_height="@dimen/qs_media_album_size"
+ android:layout_weight="1"
+ >
+ <LinearLayout
+ android:orientation="horizontal"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ >
+ <com.android.internal.widget.CachingIconView
+ android:id="@+id/icon"
+ android:layout_width="16dp"
+ android:layout_height="16dp"
+ android:layout_marginEnd="5dp"
+ />
+ <TextView
+ android:id="@+id/app_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="14sp"
+ android:singleLine="true"
+ />
+ </LinearLayout>
+
+ <!-- Song name -->
+ <TextView
+ android:id="@+id/header_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:fontFamily="@*android:string/config_headlineFontFamilyMedium"
+ android:textSize="18sp"
+ android:paddingBottom="6dp"
+ android:gravity="center"/>
+
+ <!-- Artist name -->
+ <TextView
+ android:id="@+id/header_artist"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:fontFamily="@*android:string/config_bodyFontFamily"
+ android:textSize="14sp"
+ android:singleLine="true"
+ />
+ </LinearLayout>
+
+ <!-- Controls -->
+ <LinearLayout
+ android:id="@+id/media_actions"
+ android:orientation="horizontal"
+ android:layoutDirection="ltr"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="center"
+ android:layout_gravity="center"
+ >
+ <ImageButton
+ style="@android:style/Widget.Material.Button.Borderless.Small"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:gravity="center"
+ android:visibility="gone"
+ android:id="@+id/action0"
+ />
+ <ImageButton
+ style="@android:style/Widget.Material.Button.Borderless.Small"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:gravity="center"
+ android:visibility="gone"
+ android:id="@+id/action1"
+ />
+ <ImageButton
+ style="@android:style/Widget.Material.Button.Borderless.Small"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:gravity="center"
+ android:visibility="gone"
+ android:id="@+id/action2"
+ />
+ </LinearLayout>
+ </LinearLayout>
+
+</com.android.systemui.statusbar.notification.stack.MediaHeaderView>
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardMediaPlayer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardMediaPlayer.java
new file mode 100644
index 000000000000..b0017269ff7e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardMediaPlayer.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.keyguard;
+
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.media.MediaMetadata;
+import android.util.Log;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.core.graphics.drawable.RoundedBitmapDrawable;
+import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
+import androidx.palette.graphics.Palette;
+
+import com.android.internal.util.ContrastColorUtil;
+import com.android.systemui.R;
+import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.statusbar.notification.MediaNotificationProcessor;
+import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.statusbar.notification.stack.MediaHeaderView;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+/**
+ * Media controls to display on the lockscreen
+ *
+ * TODO: Should extend MediaControlPanel to avoid code duplication.
+ * Unfortunately, it isn't currently possible because the ActivatableNotificationView background is
+ * different.
+ */
+@Singleton
+public class KeyguardMediaPlayer {
+
+ private static final String TAG = "KeyguardMediaPlayer";
+ // Buttons that can be displayed on lock screen media controls.
+ private static final int[] ACTION_IDS = {R.id.action0, R.id.action1, R.id.action2};
+
+ private final Context mContext;
+ private final Executor mBackgroundExecutor;
+ private float mAlbumArtRadius;
+ private int mAlbumArtSize;
+ private View mMediaNotifView;
+
+ @Inject
+ public KeyguardMediaPlayer(Context context, @Background Executor backgroundExecutor) {
+ mContext = context;
+ mBackgroundExecutor = backgroundExecutor;
+ loadDimens();
+ }
+
+ /** Binds media controls to a view hierarchy. */
+ public void bindView(View v) {
+ if (mMediaNotifView != null) {
+ throw new IllegalStateException("cannot bind views, already bound");
+ }
+ mMediaNotifView = v;
+ loadDimens();
+ }
+
+ /** Unbinds media controls. */
+ public void unbindView() {
+ if (mMediaNotifView == null) {
+ throw new IllegalStateException("cannot unbind views, nothing bound");
+ }
+ mMediaNotifView = null;
+ }
+
+ /** Clear the media controls because there isn't an active session. */
+ public void clearControls() {
+ if (mMediaNotifView != null) {
+ mMediaNotifView.setVisibility(View.GONE);
+ }
+ }
+
+ /**
+ * Update the media player
+ *
+ * TODO: consider registering a MediaLister instead of exposing this update method.
+ *
+ * @param entry Media notification that will be used to update the player
+ * @param appIcon Icon for the app playing the media
+ * @param mediaMetadata Media metadata that will be used to update the player
+ */
+ public void updateControls(NotificationEntry entry, Icon appIcon,
+ MediaMetadata mediaMetadata) {
+ if (mMediaNotifView == null) {
+ throw new IllegalStateException("cannot update controls, views not bound");
+ }
+ if (mediaMetadata == null) {
+ throw new IllegalArgumentException("media metadata was null");
+ }
+ mMediaNotifView.setVisibility(View.VISIBLE);
+
+ Notification notif = entry.getSbn().getNotification();
+
+ // Computed foreground and background color based on album art.
+ int fgColor = notif.color;
+ int bgColor = entry.getRow() == null ? -1 : entry.getRow().getCurrentBackgroundTint();
+ Bitmap artworkBitmap = mediaMetadata.getBitmap(MediaMetadata.METADATA_KEY_ART);
+ if (artworkBitmap == null) {
+ artworkBitmap = mediaMetadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART);
+ }
+ if (artworkBitmap != null) {
+ // If we have art, get colors from that
+ Palette p = MediaNotificationProcessor.generateArtworkPaletteBuilder(artworkBitmap)
+ .generate();
+ Palette.Swatch swatch = MediaNotificationProcessor.findBackgroundSwatch(p);
+ bgColor = swatch.getRgb();
+ fgColor = MediaNotificationProcessor.selectForegroundColor(bgColor, p);
+ }
+ // Make sure colors will be legible
+ boolean isDark = !ContrastColorUtil.isColorLight(bgColor);
+ fgColor = ContrastColorUtil.resolveContrastColor(mContext, fgColor, bgColor,
+ isDark);
+ fgColor = ContrastColorUtil.ensureTextContrast(fgColor, bgColor, isDark);
+
+ // Album art
+ ImageView albumView = mMediaNotifView.findViewById(R.id.album_art);
+ if (albumView != null) {
+ // Resize art in a background thread
+ final Bitmap bm = artworkBitmap;
+ mBackgroundExecutor.execute(() -> processAlbumArt(bm, albumView));
+ }
+
+ // App icon
+ ImageView appIconView = mMediaNotifView.findViewById(R.id.icon);
+ if (appIconView != null) {
+ Drawable iconDrawable = appIcon.loadDrawable(mContext);
+ iconDrawable.setTint(fgColor);
+ appIconView.setImageDrawable(iconDrawable);
+ }
+
+ // App name
+ TextView appName = mMediaNotifView.findViewById(R.id.app_name);
+ if (appName != null) {
+ Notification.Builder builder = Notification.Builder.recoverBuilder(mContext, notif);
+ String appNameString = builder.loadHeaderAppName();
+ appName.setText(appNameString);
+ appName.setTextColor(fgColor);
+ }
+
+ // Song name
+ TextView titleText = mMediaNotifView.findViewById(R.id.header_title);
+ if (titleText != null) {
+ String songName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE);
+ titleText.setText(songName);
+ titleText.setTextColor(fgColor);
+ }
+
+ // Artist name
+ TextView artistText = mMediaNotifView.findViewById(R.id.header_artist);
+ if (artistText != null) {
+ String artistName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST);
+ artistText.setText(artistName);
+ artistText.setTextColor(fgColor);
+ }
+
+ // Background color
+ if (mMediaNotifView instanceof MediaHeaderView) {
+ MediaHeaderView head = (MediaHeaderView) mMediaNotifView;
+ head.setBackgroundColor(bgColor);
+ }
+
+ // Control buttons
+ final List<Icon> icons = new ArrayList<>();
+ final List<PendingIntent> intents = new ArrayList<>();
+ Notification.Action[] actions = notif.actions;
+ final int[] actionsToShow = notif.extras.getIntArray(Notification.EXTRA_COMPACT_ACTIONS);
+
+ for (int i = 0; i < ACTION_IDS.length; i++) {
+ if (actionsToShow != null && actions != null && i < actionsToShow.length
+ && actionsToShow[i] < actions.length) {
+ final int idx = actionsToShow[i];
+ icons.add(actions[idx].getIcon());
+ intents.add(actions[idx].actionIntent);
+ } else {
+ icons.add(null);
+ intents.add(null);
+ }
+ }
+
+ Context packageContext = entry.getSbn().getPackageContext(mContext);
+ for (int i = 0; i < ACTION_IDS.length; i++) {
+ ImageButton button = mMediaNotifView.findViewById(ACTION_IDS[i]);
+ if (button == null) {
+ continue;
+ }
+ Icon icon = icons.get(i);
+ if (icon == null) {
+ button.setVisibility(View.GONE);
+ } else {
+ button.setVisibility(View.VISIBLE);
+ button.setImageDrawable(icon.loadDrawable(packageContext));
+ button.setImageTintList(ColorStateList.valueOf(fgColor));
+ final PendingIntent intent = intents.get(i);
+ if (intent != null) {
+ button.setOnClickListener(v -> {
+ try {
+ intent.send();
+ } catch (PendingIntent.CanceledException e) {
+ Log.d(TAG, "failed to send action intent", e);
+ }
+ });
+ }
+ }
+ }
+ }
+
+ /**
+ * Process album art for layout
+ * @param albumArt bitmap to use for album art
+ * @param albumView view to hold the album art
+ */
+ private void processAlbumArt(Bitmap albumArt, ImageView albumView) {
+ RoundedBitmapDrawable roundedDrawable = null;
+ if (albumArt != null) {
+ Bitmap original = albumArt.copy(Bitmap.Config.ARGB_8888, true);
+ Bitmap scaled = Bitmap.createScaledBitmap(original, mAlbumArtSize, mAlbumArtSize,
+ false);
+ roundedDrawable = RoundedBitmapDrawableFactory.create(mContext.getResources(), scaled);
+ roundedDrawable.setCornerRadius(mAlbumArtRadius);
+ } else {
+ Log.e(TAG, "No album art available");
+ }
+
+ // Now that it's resized, update the UI
+ final RoundedBitmapDrawable result = roundedDrawable;
+ albumView.post(() -> {
+ albumView.setImageDrawable(result);
+ albumView.setVisibility(result == null ? View.GONE : View.VISIBLE);
+ });
+ }
+
+ private void loadDimens() {
+ mAlbumArtRadius = mContext.getResources().getDimension(R.dimen.qs_media_corner_radius);
+ mAlbumArtSize = (int) mContext.getResources().getDimension(
+ R.dimen.qs_media_album_size);
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
index 87be73998fcc..53928ab166e4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
@@ -46,6 +46,7 @@ import android.widget.ImageView;
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import com.android.internal.statusbar.NotificationVisibility;
+import com.android.keyguard.KeyguardMediaPlayer;
import com.android.systemui.Dependency;
import com.android.systemui.Dumpable;
import com.android.systemui.Interpolators;
@@ -65,6 +66,7 @@ import com.android.systemui.statusbar.phone.ScrimState;
import com.android.systemui.statusbar.phone.StatusBar;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.util.DeviceConfigProxy;
+import com.android.systemui.util.Utils;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -110,6 +112,7 @@ public class NotificationMediaManager implements Dumpable {
private ScrimController mScrimController;
@Nullable
private LockscreenWallpaper mLockscreenWallpaper;
+ private final KeyguardMediaPlayer mMediaPlayer;
private final Executor mMainExecutor;
@@ -183,11 +186,13 @@ public class NotificationMediaManager implements Dumpable {
NotificationEntryManager notificationEntryManager,
MediaArtworkProcessor mediaArtworkProcessor,
KeyguardBypassController keyguardBypassController,
+ KeyguardMediaPlayer keyguardMediaPlayer,
@Main Executor mainExecutor,
DeviceConfigProxy deviceConfig) {
mContext = context;
mMediaArtworkProcessor = mediaArtworkProcessor;
mKeyguardBypassController = keyguardBypassController;
+ mMediaPlayer = keyguardMediaPlayer;
mMediaListeners = new ArrayList<>();
// TODO: use MediaSessionManager.SessionListener to hook us up to future updates
// in session state
@@ -468,6 +473,7 @@ public class NotificationMediaManager implements Dumpable {
&& mBiometricUnlockController.isWakeAndUnlock();
if (mKeyguardStateController.isLaunchTransitionFadingAway() || wakeAndUnlock) {
mBackdrop.setVisibility(View.INVISIBLE);
+ mMediaPlayer.clearControls();
Trace.endSection();
return;
}
@@ -490,6 +496,14 @@ public class NotificationMediaManager implements Dumpable {
}
}
+ NotificationEntry entry = mEntryManager
+ .getActiveNotificationUnfiltered(mMediaNotificationKey);
+ if (entry != null) {
+ mMediaPlayer.updateControls(entry, getMediaIcon(), mediaMetadata);
+ } else {
+ mMediaPlayer.clearControls();
+ }
+
// Process artwork on a background thread and send the resulting bitmap to
// finishUpdateMediaMetaData.
if (metaDataChanged) {
@@ -498,7 +512,7 @@ public class NotificationMediaManager implements Dumpable {
}
mProcessArtworkTasks.clear();
}
- if (artworkBitmap != null) {
+ if (artworkBitmap != null && !Utils.useQsMediaPlayer(mContext)) {
mProcessArtworkTasks.add(new ProcessArtworkTask(this, metaDataChanged,
allowEnterAnimation).execute(artworkBitmap));
} else {
@@ -612,6 +626,7 @@ public class NotificationMediaManager implements Dumpable {
// We are unlocking directly - no animation!
mBackdrop.setVisibility(View.GONE);
mBackdropBack.setImageDrawable(null);
+ mMediaPlayer.clearControls();
if (windowController != null) {
windowController.setBackdropShowing(false);
}
@@ -628,6 +643,7 @@ public class NotificationMediaManager implements Dumpable {
mBackdrop.setVisibility(View.GONE);
mBackdropFront.animate().cancel();
mBackdropBack.setImageDrawable(null);
+ mMediaPlayer.clearControls();
mMainExecutor.execute(mHideBackdropFront);
});
if (mKeyguardStateController.isKeyguardFadingAway()) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java
index 4c99a90e7da0..e64b423aab60 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java
@@ -21,6 +21,7 @@ import android.content.Context;
import android.os.Handler;
import com.android.internal.statusbar.IStatusBarService;
+import com.android.keyguard.KeyguardMediaPlayer;
import com.android.systemui.bubbles.BubbleController;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -93,6 +94,7 @@ public interface StatusBarDependenciesModule {
NotificationEntryManager notificationEntryManager,
MediaArtworkProcessor mediaArtworkProcessor,
KeyguardBypassController keyguardBypassController,
+ KeyguardMediaPlayer keyguardMediaPlayer,
@Main Executor mainExecutor,
DeviceConfigProxy deviceConfigProxy) {
return new NotificationMediaManager(
@@ -102,6 +104,7 @@ public interface StatusBarDependenciesModule {
notificationEntryManager,
mediaArtworkProcessor,
keyguardBypassController,
+ keyguardMediaPlayer,
mainExecutor,
deviceConfigProxy);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/MediaNotificationProcessor.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/MediaNotificationProcessor.java
index 5d1ab4fd9ad2..db5458664023 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/MediaNotificationProcessor.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/MediaNotificationProcessor.java
@@ -152,7 +152,13 @@ public class MediaNotificationProcessor {
}
}
- private int selectForegroundColor(int backgroundColor, Palette palette) {
+ /**
+ * Select a foreground color depending on whether the background color is dark or light
+ * @param backgroundColor Background color to coordinate with
+ * @param palette Artwork palette, should be obtained from {@link generateArtworkPaletteBuilder}
+ * @return foreground color
+ */
+ public static int selectForegroundColor(int backgroundColor, Palette palette) {
if (ContrastColorUtil.isColorLight(backgroundColor)) {
return selectForegroundColorForSwatches(palette.getDarkVibrantSwatch(),
palette.getVibrantSwatch(),
@@ -170,7 +176,7 @@ public class MediaNotificationProcessor {
}
}
- private int selectForegroundColorForSwatches(Palette.Swatch moreVibrant,
+ private static int selectForegroundColorForSwatches(Palette.Swatch moreVibrant,
Palette.Swatch vibrant, Palette.Swatch moreMutedSwatch, Palette.Swatch mutedSwatch,
Palette.Swatch dominantSwatch, int fallbackColor) {
Palette.Swatch coloredCandidate = selectVibrantCandidate(moreVibrant, vibrant);
@@ -194,7 +200,7 @@ public class MediaNotificationProcessor {
}
}
- private Palette.Swatch selectMutedCandidate(Palette.Swatch first,
+ private static Palette.Swatch selectMutedCandidate(Palette.Swatch first,
Palette.Swatch second) {
boolean firstValid = hasEnoughPopulation(first);
boolean secondValid = hasEnoughPopulation(second);
@@ -215,7 +221,8 @@ public class MediaNotificationProcessor {
return null;
}
- private Palette.Swatch selectVibrantCandidate(Palette.Swatch first, Palette.Swatch second) {
+ private static Palette.Swatch selectVibrantCandidate(Palette.Swatch first,
+ Palette.Swatch second) {
boolean firstValid = hasEnoughPopulation(first);
boolean secondValid = hasEnoughPopulation(second);
if (firstValid && secondValid) {
@@ -235,7 +242,7 @@ public class MediaNotificationProcessor {
return null;
}
- private boolean hasEnoughPopulation(Palette.Swatch swatch) {
+ private static boolean hasEnoughPopulation(Palette.Swatch swatch) {
// We want a fraction that is at least 1% of the image
return swatch != null
&& (swatch.getPopulation() / (float) RESIZE_BITMAP_AREA > MINIMUM_IMAGE_FRACTION);
@@ -257,7 +264,7 @@ public class MediaNotificationProcessor {
* @param palette Artwork palette, should be obtained from {@link generateArtworkPaletteBuilder}
* @return Swatch that should be used as the background of the media notification.
*/
- private static Palette.Swatch findBackgroundSwatch(Palette palette) {
+ public static Palette.Swatch findBackgroundSwatch(Palette palette) {
// by default we use the dominant palette
Palette.Swatch dominantSwatch = palette.getDominantSwatch();
if (dominantSwatch == null) {
@@ -301,7 +308,7 @@ public class MediaNotificationProcessor {
* @param artwork Media artwork
* @return Builder that generates the {@link Palette} for the media artwork.
*/
- private static Palette.Builder generateArtworkPaletteBuilder(Bitmap artwork) {
+ public static Palette.Builder generateArtworkPaletteBuilder(Bitmap artwork) {
// for the background we only take the left side of the image to ensure
// a smooth transition
return Palette.from(artwork)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt
index 48386dce5d3f..e2b01ffe59b8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt
@@ -23,9 +23,11 @@ import com.android.internal.annotations.VisibleForTesting
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.NOTIFICATIONS_USE_PEOPLE_FILTERING
import com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_ALERTING
import com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_HEADS_UP
+import com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_MEDIA_CONTROLS
import com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_PEOPLE
import com.android.systemui.statusbar.notification.stack.NotificationSectionsManager.BUCKET_SILENT
import com.android.systemui.util.DeviceConfigProxy
+import com.android.systemui.util.Utils
import javax.inject.Inject
@@ -43,9 +45,18 @@ class NotificationSectionsFeatureManager @Inject constructor(
return usePeopleFiltering(proxy)
}
+ fun isMediaControlsEnabled(): Boolean {
+ return Utils.useQsMediaPlayer(context)
+ }
+
fun getNotificationBuckets(): IntArray {
return when {
- isFilteringEnabled() ->
+ isFilteringEnabled() && isMediaControlsEnabled() ->
+ intArrayOf(BUCKET_HEADS_UP, BUCKET_MEDIA_CONTROLS, BUCKET_PEOPLE, BUCKET_ALERTING,
+ BUCKET_SILENT)
+ !isFilteringEnabled() && isMediaControlsEnabled() ->
+ intArrayOf(BUCKET_HEADS_UP, BUCKET_MEDIA_CONTROLS, BUCKET_ALERTING, BUCKET_SILENT)
+ isFilteringEnabled() && !isMediaControlsEnabled() ->
intArrayOf(BUCKET_HEADS_UP, BUCKET_PEOPLE, BUCKET_ALERTING, BUCKET_SILENT)
NotificationUtils.useNewInterruptionModel(context) ->
intArrayOf(BUCKET_ALERTING, BUCKET_SILENT)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaHeaderView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaHeaderView.java
new file mode 100644
index 000000000000..ab055e1bdc36
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaHeaderView.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.stack;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+
+import com.android.systemui.R;
+import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
+
+/**
+ * Root view to insert Lock screen media controls into the notification stack.
+ */
+public class MediaHeaderView extends ActivatableNotificationView {
+
+ private View mContentView;
+
+ public MediaHeaderView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mContentView = findViewById(R.id.keyguard_media_view);
+ }
+
+ @Override
+ protected View getContentView() {
+ return mContentView;
+ }
+
+ /**
+ * Sets the background color, to be used when album art changes.
+ * @param color background
+ */
+ public void setBackgroundColor(int color) {
+ setTintColor(color);
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.java
index 42a7c6a07e0f..7633aa589dde 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.java
@@ -30,6 +30,7 @@ import android.view.LayoutInflater;
import android.view.View;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.keyguard.KeyguardMediaPlayer;
import com.android.systemui.R;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -71,6 +72,7 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section
private final StatusBarStateController mStatusBarStateController;
private final ConfigurationController mConfigurationController;
private final PeopleHubViewAdapter mPeopleHubViewAdapter;
+ private final KeyguardMediaPlayer mKeyguardMediaPlayer;
private final NotificationSectionsFeatureManager mSectionsFeatureManager;
private final int mNumberOfSections;
@@ -110,17 +112,21 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section
private boolean mPeopleHubVisible = false;
@Nullable private Subscription mPeopleHubSubscription;
+ private MediaHeaderView mMediaControlsView;
+
@Inject
NotificationSectionsManager(
ActivityStarter activityStarter,
StatusBarStateController statusBarStateController,
ConfigurationController configurationController,
PeopleHubViewAdapter peopleHubViewAdapter,
+ KeyguardMediaPlayer keyguardMediaPlayer,
NotificationSectionsFeatureManager sectionsFeatureManager) {
mActivityStarter = activityStarter;
mStatusBarStateController = statusBarStateController;
mConfigurationController = configurationController;
mPeopleHubViewAdapter = peopleHubViewAdapter;
+ mKeyguardMediaPlayer = keyguardMediaPlayer;
mSectionsFeatureManager = sectionsFeatureManager;
mNumberOfSections = mSectionsFeatureManager.getNumberOfBuckets();
}
@@ -188,6 +194,13 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section
}
mPeopleHubView = reinflateView(mPeopleHubView, layoutInflater, R.layout.people_strip);
mPeopleHubSubscription = mPeopleHubViewAdapter.bindView(mPeopleHubViewBoundary);
+
+ if (mMediaControlsView != null) {
+ mKeyguardMediaPlayer.unbindView();
+ }
+ mMediaControlsView = reinflateView(mMediaControlsView, layoutInflater,
+ R.layout.keyguard_media_header);
+ mKeyguardMediaPlayer.bindView(mMediaControlsView);
}
/** Listener for when the "clear all" button is clicked on the gentle notification header. */
@@ -198,6 +211,7 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section
@Override
public boolean beginsSection(@NonNull View view, @Nullable View previous) {
return view == mGentleHeader
+ || view == mMediaControlsView
|| view == mPeopleHubView
|| view == mAlertingHeader
|| !Objects.equals(getBucket(view), getBucket(previous));
@@ -211,6 +225,8 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section
private Integer getBucket(View view) {
if (view == mGentleHeader) {
return BUCKET_SILENT;
+ } else if (view == mMediaControlsView) {
+ return BUCKET_MEDIA_CONTROLS;
} else if (view == mPeopleHubView) {
return BUCKET_PEOPLE;
} else if (view == mAlertingHeader) {
@@ -238,9 +254,15 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section
final boolean showHeaders = mStatusBarStateController.getState() != StatusBarState.KEYGUARD;
final boolean usingPeopleFiltering = mSectionsFeatureManager.isFilteringEnabled();
+ final boolean isKeyguard = mStatusBarStateController.getState() == StatusBarState.KEYGUARD;
+ final boolean usingMediaControls = mSectionsFeatureManager.isMediaControlsEnabled();
boolean peopleNotifsPresent = false;
+ int currentMediaControlsIdx = -1;
+ // Currently, just putting media controls in the front and incrementing the position based
+ // on the number of heads-up notifs.
+ int mediaControlsTarget = isKeyguard && usingMediaControls ? 0 : -1;
int currentPeopleHeaderIdx = -1;
int peopleHeaderTarget = -1;
int currentAlertingHeaderIdx = -1;
@@ -255,6 +277,10 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section
View child = mParent.getChildAt(i);
// Track the existing positions of the headers
+ if (child == mMediaControlsView) {
+ currentMediaControlsIdx = i;
+ continue;
+ }
if (child == mPeopleHubView) {
currentPeopleHeaderIdx = i;
continue;
@@ -276,6 +302,9 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section
// Once we enter a new section, calculate the target position for the header.
switch (row.getEntry().getBucket()) {
case BUCKET_HEADS_UP:
+ if (mediaControlsTarget != -1) {
+ mediaControlsTarget++;
+ }
break;
case BUCKET_PEOPLE:
peopleNotifsPresent = true;
@@ -345,6 +374,8 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section
alertingHeaderTarget, mAlertingHeader, currentAlertingHeaderIdx);
adjustHeaderVisibilityAndPosition(
peopleHeaderTarget, mPeopleHubView, currentPeopleHeaderIdx);
+ adjustViewPosition(
+ mediaControlsTarget, mMediaControlsView, currentMediaControlsIdx);
// Update headers to reflect state of section contents
mGentleHeader.setAreThereDismissableGentleNotifs(
@@ -378,6 +409,28 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section
}
}
+ private void adjustViewPosition(int targetPosition, ExpandableView header,
+ int currentPosition) {
+ if (targetPosition == -1) {
+ if (currentPosition != -1) {
+ mParent.removeView(header);
+ }
+ } else {
+ if (currentPosition == -1) {
+ // If the header is animating away, it will still have a parent, so detach it first
+ // TODO: We should really cancel the active animations here. This will happen
+ // automatically when the view's intro animation starts, but it's a fragile link.
+ if (header.getTransientContainer() != null) {
+ header.getTransientContainer().removeTransientView(header);
+ header.setTransientContainer(null);
+ }
+ mParent.addView(header, targetPosition);
+ } else {
+ mParent.changeViewPosition(header, targetPosition);
+ }
+ }
+ }
+
/**
* Updates the boundaries (as tracked by their first and last views) of the priority sections.
*
@@ -464,6 +517,11 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section
}
@VisibleForTesting
+ ExpandableView getMediaControlsView() {
+ return mMediaControlsView;
+ }
+
+ @VisibleForTesting
void setPeopleHubVisible(boolean visible) {
mPeopleHubVisible = visible;
}
@@ -502,13 +560,15 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section
@Retention(SOURCE)
@IntDef(prefix = { "BUCKET_" }, value = {
BUCKET_HEADS_UP,
+ BUCKET_MEDIA_CONTROLS,
BUCKET_PEOPLE,
BUCKET_ALERTING,
BUCKET_SILENT
})
public @interface PriorityBucket {}
public static final int BUCKET_HEADS_UP = 0;
- public static final int BUCKET_PEOPLE = 1;
- public static final int BUCKET_ALERTING = 2;
- public static final int BUCKET_SILENT = 3;
+ public static final int BUCKET_MEDIA_CONTROLS = 1;
+ public static final int BUCKET_PEOPLE = 2;
+ public static final int BUCKET_ALERTING = 3;
+ public static final int BUCKET_SILENT = 4;
}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMediaPlayerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMediaPlayerTest.kt
new file mode 100644
index 000000000000..464a740c931c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMediaPlayerTest.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.keyguard
+
+import android.graphics.drawable.Icon
+import android.media.MediaMetadata
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.View
+import android.widget.TextView
+import androidx.test.filters.SmallTest
+
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.time.FakeSystemClock
+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.Mockito.mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+public class KeyguardMediaPlayerTest : SysuiTestCase() {
+
+ private lateinit var keyguardMediaPlayer: KeyguardMediaPlayer
+ private lateinit var fakeExecutor: FakeExecutor
+ private lateinit var mediaMetadata: MediaMetadata.Builder
+ private lateinit var entry: NotificationEntryBuilder
+ @Mock private lateinit var mockView: View
+ private lateinit var textView: TextView
+ @Mock private lateinit var mockIcon: Icon
+
+ @Before
+ public fun setup() {
+ fakeExecutor = FakeExecutor(FakeSystemClock())
+ keyguardMediaPlayer = KeyguardMediaPlayer(context, fakeExecutor)
+ mockView = mock(View::class.java)
+ textView = TextView(context)
+ mockIcon = mock(Icon::class.java)
+ mediaMetadata = MediaMetadata.Builder()
+ entry = NotificationEntryBuilder()
+
+ keyguardMediaPlayer.bindView(mockView)
+ }
+
+ @After
+ public fun tearDown() {
+ keyguardMediaPlayer.unbindView()
+ }
+
+ @Test
+ public fun testBind() {
+ keyguardMediaPlayer.unbindView()
+ keyguardMediaPlayer.bindView(mockView)
+ }
+
+ @Test
+ public fun testUnboundClearControls() {
+ keyguardMediaPlayer.unbindView()
+ keyguardMediaPlayer.clearControls()
+ keyguardMediaPlayer.bindView(mockView)
+ }
+
+ @Test
+ public fun testUpdateControls() {
+ keyguardMediaPlayer.updateControls(entry.build(), mockIcon, mediaMetadata.build())
+ verify(mockView).setVisibility(View.VISIBLE)
+ }
+
+ @Test
+ public fun testClearControls() {
+ keyguardMediaPlayer.clearControls()
+ verify(mockView).setVisibility(View.GONE)
+ }
+
+ @Test
+ public fun testSongName() {
+ whenever<TextView>(mockView.findViewById(R.id.header_title)).thenReturn(textView)
+ val song: String = "Song"
+ mediaMetadata.putText(MediaMetadata.METADATA_KEY_TITLE, song)
+
+ keyguardMediaPlayer.updateControls(entry.build(), mockIcon, mediaMetadata.build())
+
+ assertThat(textView.getText()).isEqualTo(song)
+ }
+
+ @Test
+ public fun testArtistName() {
+ whenever<TextView>(mockView.findViewById(R.id.header_artist)).thenReturn(textView)
+ val artist: String = "Artist"
+ mediaMetadata.putText(MediaMetadata.METADATA_KEY_ARTIST, artist)
+
+ keyguardMediaPlayer.updateControls(entry.build(), mockIcon, mediaMetadata.build())
+
+ assertThat(textView.getText()).isEqualTo(artist)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt
index 6388fe1a69c1..b501a2ebeb64 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt
@@ -27,6 +27,7 @@ import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.NOTIFICATIONS
import com.android.systemui.SysuiTestCase
import com.android.systemui.util.DeviceConfigProxyFake
+import org.junit.After
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
@@ -38,6 +39,7 @@ import org.junit.runner.RunWith
class NotificationSectionsFeatureManagerTest : SysuiTestCase() {
var manager: NotificationSectionsFeatureManager? = null
val proxyFake = DeviceConfigProxyFake()
+ var originalQsMediaPlayer: Int = 0
@Before
public fun setup() {
@@ -45,6 +47,15 @@ class NotificationSectionsFeatureManagerTest : SysuiTestCase() {
NOTIFICATION_NEW_INTERRUPTION_MODEL, 1)
manager = NotificationSectionsFeatureManager(proxyFake, mContext)
manager!!.clearCache()
+ originalQsMediaPlayer = Settings.System.getInt(context.getContentResolver(),
+ "qs_media_player", 1)
+ Settings.System.putInt(context.getContentResolver(), "qs_media_player", 0)
+ }
+
+ @After
+ public fun teardown() {
+ Settings.System.putInt(context.getContentResolver(), "qs_media_player",
+ originalQsMediaPlayer)
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java
index a263a7232352..646bc9699ff8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java
@@ -42,6 +42,7 @@ import android.view.ViewGroup;
import androidx.test.filters.SmallTest;
+import com.android.keyguard.KeyguardMediaPlayer;
import com.android.systemui.ActivityStarterDelegate;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -73,6 +74,7 @@ public class NotificationSectionsManagerTest extends SysuiTestCase {
@Mock private StatusBarStateController mStatusBarStateController;
@Mock private ConfigurationController mConfigurationController;
@Mock private PeopleHubViewAdapter mPeopleHubAdapter;
+ @Mock private KeyguardMediaPlayer mKeyguardMediaPlayer;
@Mock private NotificationSectionsFeatureManager mSectionsFeatureManager;
@Mock private NotificationRowComponent mNotificationRowComponent;
@Mock private ActivatableNotificationViewController mActivatableNotificationViewController;
@@ -91,6 +93,7 @@ public class NotificationSectionsManagerTest extends SysuiTestCase {
mStatusBarStateController,
mConfigurationController,
mPeopleHubAdapter,
+ mKeyguardMediaPlayer,
mSectionsFeatureManager
);
// Required in order for the header inflation to work properly
@@ -333,13 +336,82 @@ public class NotificationSectionsManagerTest extends SysuiTestCase {
verify(mNssl).changeViewPosition(mSectionsManager.getPeopleHeaderView(), 0);
}
+ @Test
+ public void testMediaControls_AddWhenEnterKeyguard() {
+ enableMediaControls();
+
+ // GIVEN a stack that doesn't include media controls
+ setStackState(ChildType.ALERTING, ChildType.GENTLE_HEADER, ChildType.GENTLE);
+
+ // WHEN we go back to the keyguard
+ when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD);
+ mSectionsManager.updateSectionBoundaries();
+
+ // Then the media controls are added
+ verify(mNssl).addView(mSectionsManager.getMediaControlsView(), 0);
+ }
+
+ @Test
+ public void testMediaControls_AddWhenEnterKeyguardWithHeadsUp() {
+ enableMediaControls();
+
+ // GIVEN a stack that doesn't include media controls but includes HEADS_UP
+ setStackState(ChildType.HEADS_UP, ChildType.ALERTING, ChildType.GENTLE_HEADER,
+ ChildType.GENTLE);
+
+ // WHEN we go back to the keyguard
+ when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD);
+ mSectionsManager.updateSectionBoundaries();
+
+ // Then the media controls are added after HEADS_UP
+ verify(mNssl).addView(mSectionsManager.getMediaControlsView(), 1);
+ }
+
+ @Test
+ public void testMediaControls_RemoveWhenExitKeyguard() {
+ enableMediaControls();
+
+ // GIVEN a stack with media controls
+ setStackState(ChildType.MEDIA_CONTROLS, ChildType.ALERTING, ChildType.GENTLE_HEADER,
+ ChildType.GENTLE);
+
+ // WHEN we leave the keyguard
+ when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE);
+ mSectionsManager.updateSectionBoundaries();
+
+ // Then the media controls is removed
+ verify(mNssl).removeView(mSectionsManager.getMediaControlsView());
+ }
+
+ @Test
+ public void testMediaControls_RemoveWhenPullDownShade() {
+ enableMediaControls();
+
+ // GIVEN a stack with media controls
+ setStackState(ChildType.MEDIA_CONTROLS, ChildType.ALERTING, ChildType.GENTLE_HEADER,
+ ChildType.GENTLE);
+
+ // WHEN we pull down the shade on the keyguard
+ when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE_LOCKED);
+ mSectionsManager.updateSectionBoundaries();
+
+ // Then the media controls is removed
+ verify(mNssl).removeView(mSectionsManager.getMediaControlsView());
+ }
+
private void enablePeopleFiltering() {
when(mSectionsFeatureManager.isFilteringEnabled()).thenReturn(true);
when(mSectionsFeatureManager.getNumberOfBuckets()).thenReturn(4);
}
+ private void enableMediaControls() {
+ when(mSectionsFeatureManager.isMediaControlsEnabled()).thenReturn(true);
+ when(mSectionsFeatureManager.getNumberOfBuckets()).thenReturn(4);
+ }
+
private enum ChildType {
- PEOPLE_HEADER, ALERTING_HEADER, GENTLE_HEADER, HEADS_UP, PERSON, ALERTING, GENTLE, OTHER
+ MEDIA_CONTROLS, PEOPLE_HEADER, ALERTING_HEADER, GENTLE_HEADER, HEADS_UP, PERSON, ALERTING,
+ GENTLE, OTHER
}
private void setStackState(ChildType... children) {
@@ -347,6 +419,9 @@ public class NotificationSectionsManagerTest extends SysuiTestCase {
for (int i = 0; i < children.length; i++) {
View child;
switch (children[i]) {
+ case MEDIA_CONTROLS:
+ child = mSectionsManager.getMediaControlsView();
+ break;
case PEOPLE_HEADER:
child = mSectionsManager.getPeopleHeaderView();
break;