Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 7b6c1789 authored by Beth Thibodeau's avatar Beth Thibodeau
Browse files

Reset when playback state is NONE, and refactoring

If a notification is cleared it won't always send an update first and
call onSessionDestroyed. NotificationMediaManager already checks if the
current media notification is being removed and sends an event, so we
can listen for that.

This fixes the bug with cast notifications. It can also be tested by
playing media, pausing, and then clearing all notifications - the
controls will now switch to the resumption state.

Also refactored to put common code for QQS and QS players in a single
class, and removed the long-press menu in QSMediaPlayer (which was just for
testing)

Fixes: 150437753
Fixes: 150742919
Bug: 150854549
Test: manual; atest com.android.systemui.qs com.android.systemui.statusbar.notification
Change-Id: I0ecada16967a6d21b82dc86d05544cf78027bcd9
parent 74640d59
Loading
Loading
Loading
Loading
+1 −2
Original line number Original line Diff line number Diff line
@@ -32,7 +32,6 @@
        android:orientation="horizontal"
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/header"
        android:layout_marginBottom="16dp"
        android:layout_marginBottom="16dp"
    >
    >


@@ -73,7 +72,7 @@


            <!-- Song name -->
            <!-- Song name -->
            <TextView
            <TextView
                android:id="@+id/header_text"
                android:id="@+id/header_title"
                android:layout_width="wrap_content"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_height="wrap_content"
                android:singleLine="true"
                android:singleLine="true"
+425 −0
Original line number Original line Diff line number Diff line
/*
 * 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.media;

import android.annotation.LayoutRes;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.ColorStateList;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.Icon;
import android.graphics.drawable.RippleDrawable;
import android.media.MediaMetadata;
import android.media.session.MediaController;
import android.media.session.MediaSession;
import android.media.session.PlaybackState;
import android.os.Handler;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;

import androidx.core.graphics.drawable.RoundedBitmapDrawable;
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;

import com.android.settingslib.media.MediaDevice;
import com.android.settingslib.media.MediaOutputSliceConstants;
import com.android.settingslib.widget.AdaptiveIcon;
import com.android.systemui.Dependency;
import com.android.systemui.R;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.statusbar.NotificationMediaManager;

import java.util.List;
import java.util.concurrent.Executor;

/**
 * Base media control panel for System UI
 */
public class MediaControlPanel implements NotificationMediaManager.MediaListener {
    private static final String TAG = "MediaControlPanel";
    private final NotificationMediaManager mMediaManager;
    private final Executor mBackgroundExecutor;

    private Context mContext;
    protected LinearLayout mMediaNotifView;
    private View mSeamless;
    private MediaSession.Token mToken;
    private MediaController mController;
    private int mForegroundColor;
    private int mBackgroundColor;
    protected ComponentName mRecvComponent;

    private final int[] mActionIds;

    // Button IDs used in notifications
    protected static final int[] NOTIF_ACTION_IDS = {
            com.android.internal.R.id.action0,
            com.android.internal.R.id.action1,
            com.android.internal.R.id.action2,
            com.android.internal.R.id.action3,
            com.android.internal.R.id.action4
    };

    private MediaController.Callback mSessionCallback = new MediaController.Callback() {
        @Override
        public void onSessionDestroyed() {
            Log.d(TAG, "session destroyed");
            mController.unregisterCallback(mSessionCallback);
            clearControls();
        }
    };

    /**
     * Initialize a new control panel
     * @param context
     * @param parent
     * @param manager
     * @param layoutId layout resource to use for this control panel
     * @param actionIds resource IDs for action buttons in the layout
     * @param backgroundExecutor background executor, used for processing artwork
     */
    public MediaControlPanel(Context context, ViewGroup parent, NotificationMediaManager manager,
            @LayoutRes int layoutId, int[] actionIds, Executor backgroundExecutor) {
        mContext = context;
        LayoutInflater inflater = LayoutInflater.from(mContext);
        mMediaNotifView = (LinearLayout) inflater.inflate(layoutId, parent, false);
        mMediaManager = manager;
        mActionIds = actionIds;
        mBackgroundExecutor = backgroundExecutor;
    }

    /**
     * Get the view used to display media controls
     * @return the view
     */
    public View getView() {
        return mMediaNotifView;
    }

    /**
     * Get the context
     * @return context
     */
    public Context getContext() {
        return mContext;
    }

    /**
     * Update the media panel view for the given media session
     * @param token
     * @param icon
     * @param iconColor
     * @param bgColor
     * @param contentIntent
     * @param appNameString
     * @param device
     */
    public void setMediaSession(MediaSession.Token token, Icon icon, int iconColor,
            int bgColor, PendingIntent contentIntent, String appNameString, MediaDevice device) {
        mToken = token;
        mForegroundColor = iconColor;
        mBackgroundColor = bgColor;
        mController = new MediaController(mContext, mToken);

        MediaMetadata mediaMetadata = mController.getMetadata();

        // Try to find a receiver for the media button that matches this app
        PackageManager pm = mContext.getPackageManager();
        Intent it = new Intent(Intent.ACTION_MEDIA_BUTTON);
        List<ResolveInfo> info = pm.queryBroadcastReceiversAsUser(it, 0, mContext.getUser());
        if (info != null) {
            for (ResolveInfo inf : info) {
                if (inf.activityInfo.packageName.equals(mController.getPackageName())) {
                    mRecvComponent = inf.getComponentInfo().getComponentName();
                }
            }
        }

        mController.registerCallback(mSessionCallback);

        if (mediaMetadata == null) {
            Log.e(TAG, "Media metadata was null");
            return;
        }

        ImageView albumView = mMediaNotifView.findViewById(R.id.album_art);
        if (albumView != null) {
            // Resize art in a background thread
            mBackgroundExecutor.execute(() -> processAlbumArt(mediaMetadata, albumView));
        }
        mMediaNotifView.setBackgroundTintList(ColorStateList.valueOf(mBackgroundColor));

        // Click action
        mMediaNotifView.setOnClickListener(v -> {
            try {
                contentIntent.send();
                // Also close shade
                mContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
            } catch (PendingIntent.CanceledException e) {
                Log.e(TAG, "Pending intent was canceled", e);
            }
        });

        // App icon
        ImageView appIcon = mMediaNotifView.findViewById(R.id.icon);
        Drawable iconDrawable = icon.loadDrawable(mContext);
        iconDrawable.setTint(mForegroundColor);
        appIcon.setImageDrawable(iconDrawable);

        // Song name
        TextView titleText = mMediaNotifView.findViewById(R.id.header_title);
        String songName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE);
        titleText.setText(songName);
        titleText.setTextColor(mForegroundColor);

        // Not in mini player:
        // App title
        TextView appName = mMediaNotifView.findViewById(R.id.app_name);
        if (appName != null) {
            appName.setText(appNameString);
            appName.setTextColor(mForegroundColor);
        }

        // 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(mForegroundColor);
        }

        // Transfer chip
        mSeamless = mMediaNotifView.findViewById(R.id.media_seamless);
        if (mSeamless != null) {
            mSeamless.setVisibility(View.VISIBLE);
            updateDevice(device);
            ActivityStarter mActivityStarter = Dependency.get(ActivityStarter.class);
            mSeamless.setOnClickListener(v -> {
                final Intent intent = new Intent()
                        .setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT)
                        .putExtra(MediaOutputSliceConstants.EXTRA_PACKAGE_NAME,
                                mController.getPackageName())
                        .putExtra(MediaOutputSliceConstants.KEY_MEDIA_SESSION_TOKEN, mToken);
                mActivityStarter.startActivity(intent, false, true /* dismissShade */,
                        Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
            });
        }

        // Ensure is only added once
        mMediaManager.removeCallback(this);
        mMediaManager.addCallback(this);
    }

    /**
     * Return the token for the current media session
     * @return the token
     */
    public MediaSession.Token getMediaSessionToken() {
        return mToken;
    }

    /**
     * Get the current media controller
     * @return the controller
     */
    public MediaController getController() {
        return mController;
    }

    /**
     * Get the name of the package associated with the current media controller
     * @return the package name
     */
    public String getMediaPlayerPackage() {
        return mController.getPackageName();
    }

    /**
     * Check whether this player has an attached media session.
     * @return whether there is a controller with a current media session.
     */
    public boolean hasMediaSession() {
        return mController != null && mController.getPlaybackState() != null;
    }

    /**
     * Check whether the media controlled by this player is currently playing
     * @return whether it is playing, or false if no controller information
     */
    public boolean isPlaying() {
        return isPlaying(mController);
    }

    /**
     * Check whether the given controller is currently playing
     * @param controller media controller to check
     * @return whether it is playing, or false if no controller information
     */
    protected boolean isPlaying(MediaController controller) {
        if (controller == null) {
            return false;
        }

        PlaybackState state = controller.getPlaybackState();
        if (state == null) {
            return false;
        }

        return (state.getState() == PlaybackState.STATE_PLAYING);
    }

    /**
     * Process album art for layout
     * @param metadata media metadata
     * @param albumView view to hold the album art
     */
    private void processAlbumArt(MediaMetadata metadata, ImageView albumView) {
        Bitmap albumArt = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART);
        float radius = mContext.getResources().getDimension(R.dimen.qs_media_corner_radius);
        RoundedBitmapDrawable roundedDrawable = null;
        if (albumArt != null) {
            Bitmap original = albumArt.copy(Bitmap.Config.ARGB_8888, true);
            int albumSize = (int) mContext.getResources().getDimension(
                    R.dimen.qs_media_album_size);
            Bitmap scaled = Bitmap.createScaledBitmap(original, albumSize, albumSize, false);
            roundedDrawable = RoundedBitmapDrawableFactory.create(mContext.getResources(), scaled);
            roundedDrawable.setCornerRadius(radius);
        } else {
            Log.e(TAG, "No album art available");
        }

        // Now that it's resized, update the UI
        final RoundedBitmapDrawable result = roundedDrawable;
        albumView.getHandler().post(() -> {
            if (result != null) {
                albumView.setImageDrawable(result);
                albumView.setVisibility(View.VISIBLE);
            } else {
                albumView.setImageDrawable(null);
                albumView.setVisibility(View.GONE);
            }
        });
    }

    /**
     * Update the current device information
     * @param device device information to display
     */
    public void updateDevice(MediaDevice device) {
        if (mSeamless == null) {
            return;
        }
        Handler handler = mSeamless.getHandler();
        handler.post(() -> {
            updateChipInternal(device);
        });
    }

    private void updateChipInternal(MediaDevice device) {
        ColorStateList fgTintList = ColorStateList.valueOf(mForegroundColor);

        // Update the outline color
        LinearLayout viewLayout = (LinearLayout) mSeamless;
        RippleDrawable bkgDrawable = (RippleDrawable) viewLayout.getBackground();
        GradientDrawable rect = (GradientDrawable) bkgDrawable.getDrawable(0);
        rect.setStroke(2, mForegroundColor);
        rect.setColor(mBackgroundColor);

        ImageView iconView = mSeamless.findViewById(R.id.media_seamless_image);
        TextView deviceName = mSeamless.findViewById(R.id.media_seamless_text);
        deviceName.setTextColor(fgTintList);

        if (device != null) {
            Drawable icon = device.getIcon();
            iconView.setVisibility(View.VISIBLE);
            iconView.setImageTintList(fgTintList);

            if (icon instanceof AdaptiveIcon) {
                AdaptiveIcon aIcon = (AdaptiveIcon) icon;
                aIcon.setBackgroundColor(mBackgroundColor);
                iconView.setImageDrawable(aIcon);
            } else {
                iconView.setImageDrawable(icon);
            }
            deviceName.setText(device.getName());
        } else {
            // Reset to default
            iconView.setVisibility(View.GONE);
            deviceName.setText(com.android.internal.R.string.ext_media_seamless_action);
        }
    }

    /**
     * Put controls into a resumption state
     */
    public void clearControls() {
        // Hide all the old buttons
        for (int i = 0; i < mActionIds.length; i++) {
            ImageButton thisBtn = mMediaNotifView.findViewById(mActionIds[i]);
            if (thisBtn != null) {
                thisBtn.setVisibility(View.GONE);
            }
        }

        // Add a restart button
        ImageButton btn = mMediaNotifView.findViewById(mActionIds[0]);
        btn.setOnClickListener(v -> {
            Log.d(TAG, "Attempting to restart session");
            // Send a media button event to previously found receiver
            if (mRecvComponent != null) {
                Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
                intent.setComponent(mRecvComponent);
                int keyCode = KeyEvent.KEYCODE_MEDIA_PLAY;
                intent.putExtra(
                        Intent.EXTRA_KEY_EVENT,
                        new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
                mContext.sendBroadcast(intent);
            } else {
                Log.d(TAG, "No receiver to restart");
                // If we don't have a receiver, try relaunching the activity instead
                try {
                    mController.getSessionActivity().send();
                } catch (PendingIntent.CanceledException e) {
                    Log.e(TAG, "Pending intent was canceled", e);
                }
            }
        });
        btn.setImageDrawable(mContext.getResources().getDrawable(R.drawable.lb_ic_play));
        btn.setImageTintList(ColorStateList.valueOf(mForegroundColor));
        btn.setVisibility(View.VISIBLE);
    }

    @Override
    public void onMetadataOrStateChanged(MediaMetadata metadata, int state) {
        if (state == PlaybackState.STATE_NONE) {
            clearControls();
            mMediaManager.removeCallback(this);
        }
    }
}
+28 −334

File changed.

Preview size limit exceeded, changes collapsed.

+15 −5
Original line number Original line Diff line number Diff line
@@ -50,6 +50,7 @@ import com.android.systemui.Dependency;
import com.android.systemui.Dumpable;
import com.android.systemui.Dumpable;
import com.android.systemui.R;
import com.android.systemui.R;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.plugins.qs.DetailAdapter;
import com.android.systemui.plugins.qs.DetailAdapter;
import com.android.systemui.plugins.qs.QSTile;
import com.android.systemui.plugins.qs.QSTile;
@@ -60,6 +61,7 @@ import com.android.systemui.qs.external.CustomTile;
import com.android.systemui.qs.logging.QSLogger;
import com.android.systemui.qs.logging.QSLogger;
import com.android.systemui.settings.BrightnessController;
import com.android.systemui.settings.BrightnessController;
import com.android.systemui.settings.ToggleSliderView;
import com.android.systemui.settings.ToggleSliderView;
import com.android.systemui.statusbar.NotificationMediaManager;
import com.android.systemui.statusbar.policy.BrightnessMirrorController;
import com.android.systemui.statusbar.policy.BrightnessMirrorController;
import com.android.systemui.statusbar.policy.BrightnessMirrorController.BrightnessMirrorListener;
import com.android.systemui.statusbar.policy.BrightnessMirrorController.BrightnessMirrorListener;
import com.android.systemui.tuner.TunerService;
import com.android.systemui.tuner.TunerService;
@@ -70,6 +72,7 @@ import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collection;
import java.util.List;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.stream.Collectors;
import java.util.stream.Collectors;


import javax.inject.Inject;
import javax.inject.Inject;
@@ -94,6 +97,8 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne


    private final LinearLayout mMediaCarousel;
    private final LinearLayout mMediaCarousel;
    private final ArrayList<QSMediaPlayer> mMediaPlayers = new ArrayList<>();
    private final ArrayList<QSMediaPlayer> mMediaPlayers = new ArrayList<>();
    private final NotificationMediaManager mNotificationMediaManager;
    private final Executor mBackgroundExecutor;
    private LocalMediaManager mLocalMediaManager;
    private LocalMediaManager mLocalMediaManager;
    private MediaDevice mDevice;
    private MediaDevice mDevice;
    private boolean mUpdateCarousel = false;
    private boolean mUpdateCarousel = false;
@@ -128,7 +133,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne
            if (mDevice == null || !mDevice.equals(currentDevice)) {
            if (mDevice == null || !mDevice.equals(currentDevice)) {
                mDevice = currentDevice;
                mDevice = currentDevice;
                for (QSMediaPlayer p : mMediaPlayers) {
                for (QSMediaPlayer p : mMediaPlayers) {
                    p.updateChip(mDevice);
                    p.updateDevice(mDevice);
                }
                }
            }
            }
        }
        }
@@ -138,7 +143,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne
            if (mDevice == null || !mDevice.equals(device)) {
            if (mDevice == null || !mDevice.equals(device)) {
                mDevice = device;
                mDevice = device;
                for (QSMediaPlayer p : mMediaPlayers) {
                for (QSMediaPlayer p : mMediaPlayers) {
                    p.updateChip(mDevice);
                    p.updateDevice(mDevice);
                }
                }
            }
            }
        }
        }
@@ -150,12 +155,16 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne
            AttributeSet attrs,
            AttributeSet attrs,
            DumpManager dumpManager,
            DumpManager dumpManager,
            BroadcastDispatcher broadcastDispatcher,
            BroadcastDispatcher broadcastDispatcher,
            QSLogger qsLogger
            QSLogger qsLogger,
            NotificationMediaManager notificationMediaManager,
            @Background Executor backgroundExecutor
    ) {
    ) {
        super(context, attrs);
        super(context, attrs);
        mContext = context;
        mContext = context;
        mQSLogger = qsLogger;
        mQSLogger = qsLogger;
        mDumpManager = dumpManager;
        mDumpManager = dumpManager;
        mNotificationMediaManager = notificationMediaManager;
        mBackgroundExecutor = backgroundExecutor;


        setOrientation(VERTICAL);
        setOrientation(VERTICAL);


@@ -255,7 +264,8 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne


        if (player == null) {
        if (player == null) {
            Log.d(TAG, "creating new player");
            Log.d(TAG, "creating new player");
            player = new QSMediaPlayer(mContext, this);
            player = new QSMediaPlayer(mContext, this, mNotificationMediaManager,
                    mBackgroundExecutor);


            if (player.isPlaying()) {
            if (player.isPlaying()) {
                mMediaCarousel.addView(player.getView(), 0, lp); // add in front
                mMediaCarousel.addView(player.getView(), 0, lp); // add in front
@@ -268,7 +278,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne
        }
        }


        Log.d(TAG, "setting player session");
        Log.d(TAG, "setting player session");
        player.setMediaSession(this, token, icon, iconColor, bgColor, actionsContainer,
        player.setMediaSession(token, icon, iconColor, bgColor, actionsContainer,
                notif.getNotification(), mDevice);
                notif.getNotification(), mDevice);


        if (mMediaPlayers.size() > 0) {
        if (mMediaPlayers.size() > 0) {
+29 −173

File changed.

Preview size limit exceeded, changes collapsed.

Loading