Merge "Lock screen media controls" into rvc-dev

This commit is contained in:
TreeHugger Robot
2020-03-24 15:54:11 +00:00
committed by Android (Google) Code Review
11 changed files with 790 additions and 13 deletions

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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;
@@ -111,6 +113,7 @@ public class NotificationMediaManager implements Dumpable {
private ScrimController mScrimController;
@Nullable
private LockscreenWallpaper mLockscreenWallpaper;
private final KeyguardMediaPlayer mMediaPlayer;
private final Executor mMainExecutor;
@@ -184,11 +187,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()) {

View File

@@ -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);
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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);
}
}

View File

@@ -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.
*
@@ -462,6 +515,11 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section
return mPeopleHubView;
}
@VisibleForTesting
ExpandableView getMediaControlsView() {
return mMediaControlsView;
}
@VisibleForTesting
void setPeopleHubVisible(boolean visible) {
mPeopleHubVisible = visible;
@@ -501,13 +559,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;
}

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -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;