blob: 9c4625e91110d2bdb9120d5d98ccd4cf7a71e642 [file] [log] [blame]
/*
* Copyright (C) 2017 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;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Notification;
import android.content.Context;
import android.graphics.drawable.Icon;
import android.media.MediaMetadata;
import android.media.session.MediaController;
import android.media.session.MediaSession;
import android.media.session.PlaybackState;
import android.service.notification.NotificationStats;
import android.service.notification.StatusBarNotification;
import android.util.Log;
import com.android.systemui.Dumpable;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.media.controls.models.player.MediaData;
import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData;
import com.android.systemui.media.controls.pipeline.MediaDataManager;
import com.android.systemui.statusbar.dagger.CentralSurfacesModule;
import com.android.systemui.statusbar.notification.collection.NotifCollection;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.notifcollection.DismissedByUserStats;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Objects;
import java.util.Optional;
/**
* Handles tasks and state related to media notifications. For example, there is a 'current' media
* notification, which this class keeps track of.
*/
public class NotificationMediaManager implements Dumpable {
private static final String TAG = "NotificationMediaManager";
public static final boolean DEBUG_MEDIA = false;
private static final HashSet<Integer> PAUSED_MEDIA_STATES = new HashSet<>();
private static final HashSet<Integer> CONNECTING_MEDIA_STATES = new HashSet<>();
static {
PAUSED_MEDIA_STATES.add(PlaybackState.STATE_NONE);
PAUSED_MEDIA_STATES.add(PlaybackState.STATE_STOPPED);
PAUSED_MEDIA_STATES.add(PlaybackState.STATE_PAUSED);
PAUSED_MEDIA_STATES.add(PlaybackState.STATE_ERROR);
CONNECTING_MEDIA_STATES.add(PlaybackState.STATE_CONNECTING);
CONNECTING_MEDIA_STATES.add(PlaybackState.STATE_BUFFERING);
}
private final NotificationVisibilityProvider mVisibilityProvider;
private final MediaDataManager mMediaDataManager;
private final NotifPipeline mNotifPipeline;
private final NotifCollection mNotifCollection;
private final Context mContext;
private final ArrayList<MediaListener> mMediaListeners;
protected NotificationPresenter mPresenter;
private MediaController mMediaController;
private String mMediaNotificationKey;
private MediaMetadata mMediaMetadata;
private final MediaController.Callback mMediaListener = new MediaController.Callback() {
@Override
public void onPlaybackStateChanged(PlaybackState state) {
super.onPlaybackStateChanged(state);
if (DEBUG_MEDIA) {
Log.v(TAG, "DEBUG_MEDIA: onPlaybackStateChanged: " + state);
}
if (state != null) {
if (!isPlaybackActive(state.getState())) {
clearCurrentMediaNotification();
}
findAndUpdateMediaNotifications();
}
}
@Override
public void onMetadataChanged(MediaMetadata metadata) {
super.onMetadataChanged(metadata);
if (DEBUG_MEDIA) {
Log.v(TAG, "DEBUG_MEDIA: onMetadataChanged: " + metadata);
}
mMediaMetadata = metadata;
dispatchUpdateMediaMetaData();
}
};
/**
* Injected constructor. See {@link CentralSurfacesModule}.
*/
public NotificationMediaManager(
Context context,
NotificationVisibilityProvider visibilityProvider,
NotifPipeline notifPipeline,
NotifCollection notifCollection,
MediaDataManager mediaDataManager,
DumpManager dumpManager) {
mContext = context;
mMediaListeners = new ArrayList<>();
mVisibilityProvider = visibilityProvider;
mMediaDataManager = mediaDataManager;
mNotifPipeline = notifPipeline;
mNotifCollection = notifCollection;
setupNotifPipeline();
dumpManager.registerDumpable(this);
}
private void setupNotifPipeline() {
mNotifPipeline.addCollectionListener(new NotifCollectionListener() {
@Override
public void onEntryAdded(@NonNull NotificationEntry entry) {
mMediaDataManager.onNotificationAdded(entry.getKey(), entry.getSbn());
}
@Override
public void onEntryUpdated(NotificationEntry entry) {
mMediaDataManager.onNotificationAdded(entry.getKey(), entry.getSbn());
}
@Override
public void onEntryBind(NotificationEntry entry, StatusBarNotification sbn) {
findAndUpdateMediaNotifications();
}
@Override
public void onEntryRemoved(@NonNull NotificationEntry entry, int reason) {
removeEntry(entry);
}
@Override
public void onEntryCleanUp(@NonNull NotificationEntry entry) {
removeEntry(entry);
}
});
mMediaDataManager.addListener(new MediaDataManager.Listener() {
@Override
public void onMediaDataLoaded(@NonNull String key,
@Nullable String oldKey, @NonNull MediaData data, boolean immediately,
int receivedSmartspaceCardLatency, boolean isSsReactivated) {
}
@Override
public void onSmartspaceMediaDataLoaded(@NonNull String key,
@NonNull SmartspaceMediaData data, boolean shouldPrioritize) {
}
@Override
public void onMediaDataRemoved(@NonNull String key) {
mNotifPipeline.getAllNotifs()
.stream()
.filter(entry -> Objects.equals(entry.getKey(), key))
.findAny()
.ifPresent(entry -> {
// TODO(b/160713608): "removing" this notification won't happen and
// won't send the 'deleteIntent' if the notification is ongoing.
mNotifCollection.dismissNotification(entry,
getDismissedByUserStats(entry));
});
}
@Override
public void onSmartspaceMediaDataRemoved(@NonNull String key, boolean immediately) {}
});
}
private DismissedByUserStats getDismissedByUserStats(NotificationEntry entry) {
return new DismissedByUserStats(
NotificationStats.DISMISSAL_SHADE, // Add DISMISSAL_MEDIA?
NotificationStats.DISMISS_SENTIMENT_NEUTRAL,
mVisibilityProvider.obtain(entry, /* visible= */ true));
}
private void removeEntry(NotificationEntry entry) {
onNotificationRemoved(entry.getKey());
mMediaDataManager.onNotificationRemoved(entry.getKey());
}
/**
* Check if a state should be considered actively playing
* @param state a PlaybackState
* @return true if playing
*/
public static boolean isPlayingState(int state) {
return !PAUSED_MEDIA_STATES.contains(state)
&& !CONNECTING_MEDIA_STATES.contains(state);
}
/**
* Check if a state should be considered as connecting
* @param state a PlaybackState
* @return true if connecting or buffering
*/
public static boolean isConnectingState(int state) {
return CONNECTING_MEDIA_STATES.contains(state);
}
public void setUpWithPresenter(NotificationPresenter presenter) {
mPresenter = presenter;
}
public void onNotificationRemoved(String key) {
if (key.equals(mMediaNotificationKey)) {
clearCurrentMediaNotification();
dispatchUpdateMediaMetaData();
}
}
@Nullable
public String getMediaNotificationKey() {
return mMediaNotificationKey;
}
public MediaMetadata getMediaMetadata() {
return mMediaMetadata;
}
public Icon getMediaIcon() {
if (mMediaNotificationKey == null) {
return null;
}
return Optional.ofNullable(mNotifPipeline.getEntry(mMediaNotificationKey))
.map(entry -> entry.getIcons().getShelfIcon())
.map(StatusBarIconView::getSourceIcon)
.orElse(null);
}
public void addCallback(MediaListener callback) {
mMediaListeners.add(callback);
callback.onPrimaryMetadataOrStateChanged(mMediaMetadata,
getMediaControllerPlaybackState(mMediaController));
}
public void removeCallback(MediaListener callback) {
mMediaListeners.remove(callback);
}
public void findAndUpdateMediaNotifications() {
// TODO(b/169655907): get the semi-filtered notifications for current user
Collection<NotificationEntry> allNotifications = mNotifPipeline.getAllNotifs();
findPlayingMediaNotification(allNotifications);
dispatchUpdateMediaMetaData();
}
/**
* Find a notification and media controller associated with the playing media session, and
* update this manager's internal state.
* TODO(b/273443374) check this method
*/
void findPlayingMediaNotification(@NonNull Collection<NotificationEntry> allNotifications) {
// Promote the media notification with a controller in 'playing' state, if any.
NotificationEntry mediaNotification = null;
MediaController controller = null;
for (NotificationEntry entry : allNotifications) {
Notification notif = entry.getSbn().getNotification();
if (notif.isMediaNotification()) {
final MediaSession.Token token =
entry.getSbn().getNotification().extras.getParcelable(
Notification.EXTRA_MEDIA_SESSION, MediaSession.Token.class);
if (token != null) {
MediaController aController = new MediaController(mContext, token);
if (PlaybackState.STATE_PLAYING
== getMediaControllerPlaybackState(aController)) {
if (DEBUG_MEDIA) {
Log.v(TAG, "DEBUG_MEDIA: found mediastyle controller matching "
+ entry.getSbn().getKey());
}
mediaNotification = entry;
controller = aController;
break;
}
}
}
}
if (controller != null && !sameSessions(mMediaController, controller)) {
// We have a new media session
clearCurrentMediaNotificationSession();
mMediaController = controller;
mMediaController.registerCallback(mMediaListener);
mMediaMetadata = mMediaController.getMetadata();
if (DEBUG_MEDIA) {
Log.v(TAG, "DEBUG_MEDIA: insert listener, found new controller: "
+ mMediaController + ", receive metadata: " + mMediaMetadata);
}
}
if (mediaNotification != null
&& !mediaNotification.getSbn().getKey().equals(mMediaNotificationKey)) {
mMediaNotificationKey = mediaNotification.getSbn().getKey();
if (DEBUG_MEDIA) {
Log.v(TAG, "DEBUG_MEDIA: Found new media notification: key="
+ mMediaNotificationKey);
}
}
}
public void clearCurrentMediaNotification() {
mMediaNotificationKey = null;
clearCurrentMediaNotificationSession();
}
private void dispatchUpdateMediaMetaData() {
@PlaybackState.State int state = getMediaControllerPlaybackState(mMediaController);
ArrayList<MediaListener> callbacks = new ArrayList<>(mMediaListeners);
for (int i = 0; i < callbacks.size(); i++) {
callbacks.get(i).onPrimaryMetadataOrStateChanged(mMediaMetadata, state);
}
}
@Override
public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
pw.print(" mMediaNotificationKey=");
pw.println(mMediaNotificationKey);
pw.print(" mMediaController=");
pw.print(mMediaController);
if (mMediaController != null) {
pw.print(" state=" + mMediaController.getPlaybackState());
}
pw.println();
pw.print(" mMediaMetadata=");
pw.print(mMediaMetadata);
if (mMediaMetadata != null) {
pw.print(" title=" + mMediaMetadata.getText(MediaMetadata.METADATA_KEY_TITLE));
}
pw.println();
}
private boolean isPlaybackActive(int state) {
return state != PlaybackState.STATE_STOPPED && state != PlaybackState.STATE_ERROR
&& state != PlaybackState.STATE_NONE;
}
private boolean sameSessions(MediaController a, MediaController b) {
if (a == b) {
return true;
}
if (a == null) {
return false;
}
return a.controlsSameSession(b);
}
private int getMediaControllerPlaybackState(MediaController controller) {
if (controller != null) {
final PlaybackState playbackState = controller.getPlaybackState();
if (playbackState != null) {
return playbackState.getState();
}
}
return PlaybackState.STATE_NONE;
}
private void clearCurrentMediaNotificationSession() {
mMediaMetadata = null;
if (mMediaController != null) {
if (DEBUG_MEDIA) {
Log.v(TAG, "DEBUG_MEDIA: Disconnecting from old controller: "
+ mMediaController.getPackageName());
}
mMediaController.unregisterCallback(mMediaListener);
}
mMediaController = null;
}
public interface MediaListener {
/**
* Called whenever there's new metadata or playback state.
* @param metadata Current metadata.
* @param state Current playback state
* @see PlaybackState.State
*/
default void onPrimaryMetadataOrStateChanged(MediaMetadata metadata,
@PlaybackState.State int state) {}
}
}