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

Commit 69b1dfd9 authored by Beth Thibodeau's avatar Beth Thibodeau
Browse files

Dynamic output switcher chip and bug fixes

- Update the output switcher view to show the current device name and icon
- Show the requested buttons in mini player view
- Handle missing art or different number of buttons when changing tracks
- Catch NPEs for missing media metadata

Fixes: 143235162
Test: manual

Change-Id: I6fec4955a965a2011b6a02cb22f14a291afd1a18
parent 379f81b2
Loading
Loading
Loading
Loading
+64 −24
Original line number Diff line number Diff line
@@ -47,7 +47,9 @@ 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;
@@ -61,10 +63,13 @@ public class QSMediaPlayer {

    private Context mContext;
    private LinearLayout mMediaNotifView;
    private View mSeamless;
    private MediaSession.Token mToken;
    private MediaController mController;
    private int mWidth;
    private int mHeight;
    private int mForegroundColor;
    private int mBackgroundColor;

    /**
     *
@@ -93,15 +98,17 @@ public class QSMediaPlayer {
     * @param iconColor foreground color (for text, icons)
     * @param bgColor background color
     * @param actionsContainer a LinearLayout containing the media action buttons
     * @param notif
     * @param notif reference to original notification
     * @param device current playback device
     */
    public void setMediaSession(MediaSession.Token token, Icon icon, int iconColor, int bgColor,
            View actionsContainer, Notification notif) {
            View actionsContainer, Notification notif, MediaDevice device) {
        Log.d(TAG, "got media session: " + token);
        mToken = token;
        mForegroundColor = iconColor;
        mBackgroundColor = bgColor;
        mController = new MediaController(mContext, token);
        MediaMetadata mMediaMetadata = mController.getMetadata();

        if (mMediaMetadata == null) {
            Log.e(TAG, "Media metadata was null");
            return;
@@ -123,9 +130,6 @@ public class QSMediaPlayer {
        headerView.removeAllViews();
        headerView.addView(result);

        View seamless = headerView.findViewById(com.android.internal.R.id.media_seamless);
        seamless.setVisibility(View.VISIBLE);

        // App icon
        ImageView appIcon = headerView.findViewById(com.android.internal.R.id.icon);
        Drawable iconDrawable = icon.loadDrawable(mContext);
@@ -168,23 +172,11 @@ public class QSMediaPlayer {
        }

        // Transfer chip
        View transferBackgroundView = headerView.findViewById(
                com.android.internal.R.id.media_seamless);
        LinearLayout viewLayout = (LinearLayout) transferBackgroundView;
        RippleDrawable bkgDrawable = (RippleDrawable) viewLayout.getBackground();
        GradientDrawable rect = (GradientDrawable) bkgDrawable.getDrawable(0);
        rect.setStroke(2, iconColor);
        rect.setColor(bgColor);
        ImageView transferIcon = headerView.findViewById(
                com.android.internal.R.id.media_seamless_image);
        transferIcon.setBackgroundColor(bgColor);
        transferIcon.setImageTintList(ColorStateList.valueOf(iconColor));
        TextView transferText = headerView.findViewById(
                com.android.internal.R.id.media_seamless_text);
        transferText.setTextColor(iconColor);

        mSeamless = headerView.findViewById(com.android.internal.R.id.media_seamless);
        mSeamless.setVisibility(View.VISIBLE);
        updateChip(device);
        ActivityStarter mActivityStarter = Dependency.get(ActivityStarter.class);
        transferBackgroundView.setOnClickListener(v -> {
        mSeamless.setOnClickListener(v -> {
            final Intent intent = new Intent()
                    .setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT);
            mActivityStarter.startActivity(intent, false, true /* dismissShade */,
@@ -219,10 +211,13 @@ public class QSMediaPlayer {
                com.android.internal.R.id.action3,
                com.android.internal.R.id.action4
        };
        for (int i = 0; i < parentActionsLayout.getChildCount() && i < actionIds.length; i++) {

        int i = 0;
        for (; i < parentActionsLayout.getChildCount() && i < actionIds.length; i++) {
            ImageButton thisBtn = mMediaNotifView.findViewById(actionIds[i]);
            ImageButton thatBtn = parentActionsLayout.findViewById(notifActionIds[i]);
            if (thatBtn == null || thatBtn.getDrawable() == null) {
            if (thatBtn == null || thatBtn.getDrawable() == null
                    || thatBtn.getVisibility() != View.VISIBLE) {
                thisBtn.setVisibility(View.GONE);
                continue;
            }
@@ -235,6 +230,13 @@ public class QSMediaPlayer {
                thatBtn.performClick();
            });
        }

        // Hide any unused buttons
        for (; i < actionIds.length; i++) {
            ImageButton thisBtn = mMediaNotifView.findViewById(actionIds[i]);
            thisBtn.setVisibility(View.GONE);
            Log.d(TAG, "hid a button");
        }
    }

    public MediaSession.Token getMediaSessionToken() {
@@ -284,6 +286,7 @@ public class QSMediaPlayer {
            mMediaNotifView.setBackground(roundedDrawable);
        } else {
            Log.e(TAG, "No album art available");
            mMediaNotifView.setBackground(null);
        }
    }

@@ -303,4 +306,41 @@ public class QSMediaPlayer {

        return cropped;
    }

    protected void updateChip(MediaDevice device) {
        if (mSeamless == null) {
            return;
        }
        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(com.android.internal.R.id.media_seamless_image);
        TextView deviceName = mSeamless.findViewById(com.android.internal.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);
        }
    }
}
+47 −1
Original line number Diff line number Diff line
@@ -46,6 +46,8 @@ import android.widget.LinearLayout;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.settingslib.Utils;
import com.android.settingslib.media.LocalMediaManager;
import com.android.settingslib.media.MediaDevice;
import com.android.systemui.Dependency;
import com.android.systemui.DumpController;
import com.android.systemui.Dumpable;
@@ -70,6 +72,7 @@ import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

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

    private final LinearLayout mMediaCarousel;
    private final ArrayList<QSMediaPlayer> mMediaPlayers = new ArrayList<>();
    private LocalMediaManager mLocalMediaManager;
    private MediaDevice mDevice;

    protected boolean mExpanded;
    protected boolean mListening;
@@ -117,6 +122,31 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne
    private final PluginManager mPluginManager;
    private NPVPluginManager mNPVPluginManager;

    private final LocalMediaManager.DeviceCallback mDeviceCallback =
            new LocalMediaManager.DeviceCallback() {
        @Override
        public void onDeviceListUpdate(List<MediaDevice> devices) {
            MediaDevice currentDevice = mLocalMediaManager.getCurrentConnectedDevice();
            // Check because this can be called several times while changing devices
            if (mDevice == null || !mDevice.equals(currentDevice)) {
                mDevice = currentDevice;
                for (QSMediaPlayer p : mMediaPlayers) {
                    p.updateChip(mDevice);
                }
            }
        }

        @Override
        public void onSelectedDeviceStateChanged(MediaDevice device, int state) {
            if (mDevice == null || !mDevice.equals(device)) {
                mDevice = device;
                for (QSMediaPlayer p : mMediaPlayers) {
                    p.updateChip(mDevice);
                }
            }
        }
    };

    public QSPanel(Context context) {
        this(context, null);
    }
@@ -208,6 +238,11 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne
            Log.e(TAG, "Tried to add media session without player!");
            return;
        }
        if (token == null) {
            Log.e(TAG, "Media session token was null!");
            return;
        }

        QSMediaPlayer player = null;
        String packageName = notif.getPackageName();
        for (QSMediaPlayer p : mMediaPlayers) {
@@ -250,10 +285,17 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne

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

        if (mMediaPlayers.size() > 0) {
            ((View) mMediaCarousel.getParent()).setVisibility(View.VISIBLE);

            // Set up listener for device changes
            // TODO: integrate with MediaTransferManager?
            mLocalMediaManager = new LocalMediaManager(mContext, null, null);
            mLocalMediaManager.startScan();
            mDevice = mLocalMediaManager.getCurrentConnectedDevice();
            mLocalMediaManager.registerCallback(mDeviceCallback);
        }
    }

@@ -326,6 +368,10 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne
            mBrightnessMirrorController.removeCallback(this);
        }
        if (mDumpController != null) mDumpController.unregisterDumpable(this);
        if (mLocalMediaManager != null) {
            mLocalMediaManager.stopScan();
            mLocalMediaManager.unregisterCallback(mDeviceCallback);
        }
        super.onDetachedFromWindow();
    }

+36 −19
Original line number Diff line number Diff line
@@ -76,9 +76,11 @@ public class QuickQSMediaPlayer {
     * @param iconColor foreground color (for text, icons)
     * @param bgColor background color
     * @param actionsContainer a LinearLayout containing the media action buttons
     * @param actionsToShow indices of which actions to display in the mini player
     *                      (max 3: Notification.MediaStyle.MAX_MEDIA_BUTTONS_IN_COMPACT)
     */
    public void setMediaSession(MediaSession.Token token, Icon icon, int iconColor, int bgColor,
            View actionsContainer) {
            View actionsContainer, int[] actionsToShow) {
        Log.d(TAG, "Setting media session: " + token);
        mToken = token;
        mController = new MediaController(mContext, token);
@@ -110,20 +112,29 @@ public class QuickQSMediaPlayer {
        titleText.setText(songName);
        titleText.setTextColor(iconColor);

        // Action buttons
        LinearLayout parentActionsLayout = (LinearLayout) actionsContainer;
        // Buttons we can display
        final int[] actionIds = {R.id.action0, R.id.action1, R.id.action2};

        // TODO some apps choose different buttons to show in compact mode
        // Existing buttons in the notification
        LinearLayout parentActionsLayout = (LinearLayout) actionsContainer;
        final int[] notifActionIds = {
                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.action3,
                com.android.internal.R.id.action4
        };
        for (int i = 0; i < parentActionsLayout.getChildCount() && i < actionIds.length; i++) {

        int i = 0;
        if (actionsToShow != null) {
            int maxButtons = Math.min(actionsToShow.length, parentActionsLayout.getChildCount());
            maxButtons = Math.min(maxButtons, actionIds.length);
            for (; i < maxButtons; i++) {
                ImageButton thisBtn = mMediaNotifView.findViewById(actionIds[i]);
            ImageButton thatBtn = parentActionsLayout.findViewById(notifActionIds[i]);
            if (thatBtn == null || thatBtn.getDrawable() == null) {
                int thatId = notifActionIds[actionsToShow[i]];
                ImageButton thatBtn = parentActionsLayout.findViewById(thatId);
                if (thatBtn == null || thatBtn.getDrawable() == null
                        || thatBtn.getVisibility() != View.VISIBLE) {
                    thisBtn.setVisibility(View.GONE);
                    continue;
                }
@@ -131,14 +142,19 @@ public class QuickQSMediaPlayer {
                Drawable thatIcon = thatBtn.getDrawable();
                thisBtn.setImageDrawable(thatIcon.mutate());
                thisBtn.setVisibility(View.VISIBLE);

                thisBtn.setOnClickListener(v -> {
                Log.d(TAG, "clicking on other button");
                    thatBtn.performClick();
                });
            }
        }

        // Hide any unused buttons
        for (; i < actionIds.length; i++) {
            ImageButton thisBtn = mMediaNotifView.findViewById(actionIds[i]);
            thisBtn.setVisibility(View.GONE);
        }
    }

    public MediaSession.Token getMediaSessionToken() {
        return mToken;
    }
@@ -186,6 +202,7 @@ public class QuickQSMediaPlayer {
            mMediaNotifView.setBackground(roundedDrawable);
        } else {
            Log.e(TAG, "No album art available");
            mMediaNotifView.setBackground(null);
        }
    }

+104 −14
Original line number Diff line number Diff line
@@ -19,10 +19,12 @@ package com.android.systemui.statusbar;
import android.content.Context;
import android.content.Intent;
import android.content.res.ColorStateList;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.RippleDrawable;
import android.service.notification.StatusBarNotification;
import android.util.FeatureFlagUtils;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
@@ -30,19 +32,29 @@ import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;

import com.android.internal.R;
import com.android.settingslib.media.LocalMediaManager;
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.plugins.ActivityStarter;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;

import java.util.ArrayList;
import java.util.List;

/**
 * Class for handling MediaTransfer state over a set of notifications.
 */
public class MediaTransferManager {
    private final Context mContext;
    private final ActivityStarter mActivityStarter;
    private MediaDevice mDevice;
    private List<View> mViews = new ArrayList<>();
    private LocalMediaManager mLocalMediaManager;

    private static final String TAG = "MediaTransferManager";

    private final View.OnClickListener mOnClickHandler = new View.OnClickListener() {
        @Override
@@ -70,9 +82,50 @@ public class MediaTransferManager {
        }
    };

    private final LocalMediaManager.DeviceCallback mMediaDeviceCallback =
            new LocalMediaManager.DeviceCallback() {
        @Override
        public void onDeviceListUpdate(List<MediaDevice> devices) {
            MediaDevice currentDevice = mLocalMediaManager.getCurrentConnectedDevice();
            // Check because this can be called several times while changing devices
            if (mDevice == null || !mDevice.equals(currentDevice)) {
                mDevice = currentDevice;
                updateAllChips();
            }
        }

        @Override
        public void onSelectedDeviceStateChanged(MediaDevice device, int state) {
            if (mDevice == null || !mDevice.equals(device)) {
                mDevice = device;
                updateAllChips();
            }
        }
    };

    public MediaTransferManager(Context context) {
        mContext = context;
        mActivityStarter = Dependency.get(ActivityStarter.class);
        mLocalMediaManager = new LocalMediaManager(mContext, null, null);
    }

    /**
     * Mark a view as removed. If no views remain the media device listener will be unregistered.
     * @param root
     */
    public void setRemoved(View root) {
        if (!FeatureFlagUtils.isEnabled(mContext, FeatureFlagUtils.SEAMLESS_TRANSFER)
                || mLocalMediaManager == null || root == null) {
            return;
        }
        View view = root.findViewById(com.android.internal.R.id.media_seamless);
        if (mViews.remove(view)) {
            if (mViews.size() == 0) {
                mLocalMediaManager.unregisterCallback(mMediaDeviceCallback);
            }
        } else {
            Log.e(TAG, "Tried to remove unknown view " + view);
        }
    }

    private ExpandableNotificationRow getRowForParent(ViewParent parent) {
@@ -92,7 +145,8 @@ public class MediaTransferManager {
     * @param entry The entry of MediaTransfer action button.
     */
    public void applyMediaTransferView(ViewGroup root, NotificationEntry entry) {
        if (!FeatureFlagUtils.isEnabled(mContext, FeatureFlagUtils.SEAMLESS_TRANSFER)) {
        if (!FeatureFlagUtils.isEnabled(mContext, FeatureFlagUtils.SEAMLESS_TRANSFER)
                || mLocalMediaManager == null || root == null) {
            return;
        }

@@ -103,23 +157,59 @@ public class MediaTransferManager {

        view.setVisibility(View.VISIBLE);
        view.setOnClickListener(mOnClickHandler);
        if (!mViews.contains(view)) {
            mViews.add(view);
            if (mViews.size() == 1) {
                mLocalMediaManager.registerCallback(mMediaDeviceCallback);
            }
        }

        // Initial update
        mLocalMediaManager.startScan();
        mDevice = mLocalMediaManager.getCurrentConnectedDevice();
        updateChip(view);
    }

    private void updateAllChips() {
        for (View view : mViews) {
            updateChip(view);
        }
    }

    private void updateChip(View view) {
        ExpandableNotificationRow enr = getRowForParent(view.getParent());
        int color = enr.getNotificationHeader().getOriginalIconColor();
        ColorStateList tintList = ColorStateList.valueOf(color);
        int fgColor = enr.getNotificationHeader().getOriginalIconColor();
        ColorStateList fgTintList = ColorStateList.valueOf(fgColor);
        int bgColor = enr.getCurrentBackgroundTint();

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

        // Update the image color
        ImageView image = view.findViewById(R.id.media_seamless_image);
        image.setImageTintList(tintList);

        // Update the text color
        TextView text = view.findViewById(R.id.media_seamless_text);
        text.setTextColor(tintList);
        rect.setStroke(2, fgColor);
        rect.setColor(bgColor);

        ImageView iconView = view.findViewById(com.android.internal.R.id.media_seamless_image);
        TextView deviceName = view.findViewById(com.android.internal.R.id.media_seamless_text);
        deviceName.setTextColor(fgTintList);

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

            if (icon instanceof AdaptiveIcon) {
                AdaptiveIcon aIcon = (AdaptiveIcon) icon;
                aIcon.setBackgroundColor(bgColor);
                iconView.setImageDrawable(aIcon);
            } else {
                iconView.setImageDrawable(icon);
            }
            deviceName.setText(mDevice.getName());
        } else {
            // Reset to default
            iconView.setVisibility(View.GONE);
            deviceName.setText(com.android.internal.R.string.ext_media_seamless_action);
        }
    }
}
+2 −0
Original line number Diff line number Diff line
@@ -1546,9 +1546,11 @@ public class NotificationContentView extends FrameLayout {
        }
        if (mExpandedWrapper != null) {
            mExpandedWrapper.setRemoved();
            mMediaTransferManager.setRemoved(mExpandedChild);
        }
        if (mContractedWrapper != null) {
            mContractedWrapper.setRemoved();
            mMediaTransferManager.setRemoved(mContractedChild);
        }
        if (mHeadsUpWrapper != null) {
            mHeadsUpWrapper.setRemoved();
Loading