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

Commit 5dbef2d9 authored by Selim Cinek's avatar Selim Cinek
Browse files

Refactored the Media Player management

Previously all media instances had their own management
and it could easily happen that certain players
would get out of sync as a result. We now have
a unified architecture to listen to media notifications
and inflating a singular UI

Test: atest SystemUiTests
Bug: 154137987
Change-Id: I9f807e6431dd7cb54ca9b6d983379d770a281f31
parent d835792b
Loading
Loading
Loading
Loading
+0 −105
Original line number Diff line number Diff line
@@ -45,109 +45,4 @@
        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"
        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="wrap_content"
            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="@style/MediaPlayer.Button"
                android:layout_width="48dp"
                android:layout_height="48dp"
                android:gravity="center"
                android:visibility="gone"
                android:id="@+id/action0"
            />
            <ImageButton
                style="@style/MediaPlayer.Button"
                android:layout_width="48dp"
                android:layout_height="48dp"
                android:gravity="center"
                android:visibility="gone"
                android:id="@+id/action1"
            />
            <ImageButton
                style="@style/MediaPlayer.Button"
                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>
+1 −2
Original line number Diff line number Diff line
@@ -22,11 +22,10 @@
    android:layout_height="wrap_content"
    android:padding="@dimen/qs_media_padding"
    android:scrollbars="none"
    android:visibility="gone"
    >
    <LinearLayout
        android:id="@+id/media_carousel"
        android:layout_width="match_parent"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        >
+0 −381
Original line number 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.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.media.session.MediaController;
import android.media.session.MediaSession;
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.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer;
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.media.MediaControllerFactory;
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 final KeyguardMediaViewModel mViewModel;
    private KeyguardMediaObserver mObserver;

    @Inject
    public KeyguardMediaPlayer(Context context, MediaControllerFactory factory,
            @Background Executor backgroundExecutor) {
        mContext = context;
        mBackgroundExecutor = backgroundExecutor;
        mViewModel = new KeyguardMediaViewModel(context, factory);
    }

    /** Binds media controls to a view hierarchy. */
    public void bindView(View v) {
        if (mObserver != null) {
            throw new IllegalStateException("cannot bind views, already bound");
        }
        mViewModel.loadDimens();
        mObserver = new KeyguardMediaObserver(v);
        // Control buttons
        for (int i = 0; i < ACTION_IDS.length; i++) {
            ImageButton button = v.findViewById(ACTION_IDS[i]);
            if (button == null) {
                continue;
            }
            final int index = i;
            button.setOnClickListener(unused -> mViewModel.onActionClick(index));
        }
        mViewModel.getKeyguardMedia().observeForever(mObserver);
    }

    /** Unbinds media controls. */
    public void unbindView() {
        if (mObserver == null) {
            throw new IllegalStateException("cannot unbind views, nothing bound");
        }
        mViewModel.getKeyguardMedia().removeObserver(mObserver);
        mObserver = null;
    }

    /** Clear the media controls because there isn't an active session. */
    public void clearControls() {
        mBackgroundExecutor.execute(mViewModel::clearControls);
    }

    /**
     * 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 (mObserver == null) {
            throw new IllegalStateException("cannot update controls, views not bound");
        }
        if (mediaMetadata == null) {
            Log.d(TAG, "media metadata was null, closing media controls");
            // Note that clearControls() executes on the same background executor, so there
            // shouldn't be an issue with an outdated update running after clear. However, if stale
            // controls are observed then consider removing any enqueued updates.
            clearControls();
            return;
        }
        mBackgroundExecutor.execute(() -> mViewModel.updateControls(entry, appIcon, mediaMetadata));
    }

    /** ViewModel for KeyguardMediaControls. */
    private static final class KeyguardMediaViewModel {

        private final Context mContext;
        private final MediaControllerFactory mMediaControllerFactory;
        private final MutableLiveData<KeyguardMedia> mMedia = new MutableLiveData<>();
        private final Object mActionsLock = new Object();
        private List<PendingIntent> mActions;
        private float mAlbumArtRadius;
        private int mAlbumArtSize;

        KeyguardMediaViewModel(Context context, MediaControllerFactory factory) {
            mContext = context;
            mMediaControllerFactory = factory;
            loadDimens();
        }

        /** Close the media player because there isn't an active session. */
        public void clearControls() {
            synchronized (mActionsLock) {
                mActions = null;
            }
            mMedia.postValue(null);
        }

        /** Update the media player with information about the active session. */
        public void updateControls(NotificationEntry entry, Icon appIcon,
                MediaMetadata mediaMetadata) {

            // Check the playback state of the media controller. If it is null, then the session was
            // probably destroyed. Don't update in this case.
            final MediaSession.Token token = entry.getSbn().getNotification().extras
                    .getParcelable(Notification.EXTRA_MEDIA_SESSION);
            final MediaController controller = token != null
                    ? mMediaControllerFactory.create(token) : null;
            if (controller != null && controller.getPlaybackState() == null) {
                clearControls();
                return;
            }

            // Foreground and Background colors computed from album art
            Notification notif = entry.getSbn().getNotification();
            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
            RoundedBitmapDrawable artwork = null;
            if (artworkBitmap != null) {
                Bitmap original = artworkBitmap.copy(Bitmap.Config.ARGB_8888, true);
                Bitmap scaled = Bitmap.createScaledBitmap(original, mAlbumArtSize, mAlbumArtSize,
                        false);
                artwork = RoundedBitmapDrawableFactory.create(mContext.getResources(), scaled);
                artwork.setCornerRadius(mAlbumArtRadius);
            }

            // App name
            Notification.Builder builder = Notification.Builder.recoverBuilder(mContext, notif);
            String app = builder.loadHeaderAppName();

            // App Icon
            Drawable appIconDrawable = appIcon.loadDrawable(mContext);

            // Song name
            String song = mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE);

            // Artist name
            String artist = mediaMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST);

            // Control buttons
            List<Drawable> actionIcons = new ArrayList<>();
            final List<PendingIntent> intents = new ArrayList<>();
            Notification.Action[] actions = notif.actions;
            final int[] actionsToShow = notif.extras.getIntArray(
                    Notification.EXTRA_COMPACT_ACTIONS);

            Context packageContext = entry.getSbn().getPackageContext(mContext);
            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];
                    actionIcons.add(actions[idx].getIcon().loadDrawable(packageContext));
                    intents.add(actions[idx].actionIntent);
                } else {
                    actionIcons.add(null);
                    intents.add(null);
                }
            }
            synchronized (mActionsLock) {
                mActions = intents;
            }

            KeyguardMedia data = new KeyguardMedia(fgColor, bgColor, app, appIconDrawable, artist,
                    song, artwork, actionIcons);
            mMedia.postValue(data);
        }

        /** Gets state for the lock screen media controls. */
        public LiveData<KeyguardMedia> getKeyguardMedia() {
            return mMedia;
        }

        /**
         * Handle user clicks on media control buttons (actions).
         *
         * @param index position of the button that was clicked.
         */
        public void onActionClick(int index) {
            PendingIntent intent = null;
            // This might block the ui thread to wait for the lock. Currently, however, the
            // lock is held by the bg thread to assign a member, which should be fast. An
            // alternative could be to add the intents to the state and let the observer set
            // the onClick listeners.
            synchronized (mActionsLock) {
                if (mActions != null && index < mActions.size()) {
                    intent = mActions.get(index);
                }
            }
            if (intent != null) {
                try {
                    intent.send();
                } catch (PendingIntent.CanceledException e) {
                    Log.d(TAG, "failed to send action intent", e);
                }
            }
        }

        void loadDimens() {
            mAlbumArtRadius = mContext.getResources().getDimension(R.dimen.qs_media_corner_radius);
            mAlbumArtSize = (int) mContext.getResources().getDimension(
                    R.dimen.qs_media_album_size);
        }
    }

    /** Observer for state changes of lock screen media controls. */
    private static final class KeyguardMediaObserver implements Observer<KeyguardMedia> {

        private final View mRootView;
        private final MediaHeaderView mMediaHeaderView;
        private final ImageView mAlbumView;
        private final ImageView mAppIconView;
        private final TextView mAppNameView;
        private final TextView mTitleView;
        private final TextView mArtistView;
        private final List<ImageButton> mButtonViews = new ArrayList<>();

        KeyguardMediaObserver(View v) {
            mRootView = v;
            mMediaHeaderView = v instanceof MediaHeaderView ? (MediaHeaderView) v : null;
            mAlbumView = v.findViewById(R.id.album_art);
            mAppIconView = v.findViewById(R.id.icon);
            mAppNameView = v.findViewById(R.id.app_name);
            mTitleView = v.findViewById(R.id.header_title);
            mArtistView = v.findViewById(R.id.header_artist);
            for (int i = 0; i < ACTION_IDS.length; i++) {
                mButtonViews.add(v.findViewById(ACTION_IDS[i]));
            }
        }

        /** Updates lock screen media player views when state changes. */
        @Override
        public void onChanged(KeyguardMedia data) {
            if (data == null) {
                mRootView.setVisibility(View.GONE);
                return;
            }
            mRootView.setVisibility(View.VISIBLE);

            // Background color
            if (mMediaHeaderView != null) {
                mMediaHeaderView.setBackgroundColor(data.getBackgroundColor());
            }

            // Album art
            if (mAlbumView != null) {
                mAlbumView.setImageDrawable(data.getArtwork());
                mAlbumView.setVisibility(data.getArtwork() == null ? View.GONE : View.VISIBLE);
            }

            // App icon
            if (mAppIconView != null) {
                Drawable iconDrawable = data.getAppIcon();
                iconDrawable.setTint(data.getForegroundColor());
                mAppIconView.setImageDrawable(iconDrawable);
            }

            // App name
            if (mAppNameView != null) {
                String appNameString = data.getApp();
                mAppNameView.setText(appNameString);
                mAppNameView.setTextColor(data.getForegroundColor());
            }

            // Song name
            if (mTitleView != null) {
                mTitleView.setText(data.getSong());
                mTitleView.setTextColor(data.getForegroundColor());
            }

            // Artist name
            if (mArtistView != null) {
                mArtistView.setText(data.getArtist());
                mArtistView.setTextColor(data.getForegroundColor());
            }

            // Control buttons
            for (int i = 0; i < ACTION_IDS.length; i++) {
                ImageButton button = mButtonViews.get(i);
                if (button == null) {
                    continue;
                }
                Drawable icon = data.getActionIcons().get(i);
                if (icon == null) {
                    button.setVisibility(View.GONE);
                    button.setImageDrawable(null);
                } else {
                    button.setVisibility(View.VISIBLE);
                    button.setImageDrawable(icon);
                    button.setImageTintList(ColorStateList.valueOf(data.getForegroundColor()));
                }
            }
        }
    }
}
+2 −1
Original line number Diff line number Diff line
@@ -451,7 +451,8 @@ public class KeyguardSliceProvider extends SliceProvider implements
     * @param metadata New metadata.
     */
    @Override
    public void onMetadataOrStateChanged(MediaMetadata metadata, @PlaybackState.State int state) {
    public void onPrimaryMetadataOrStateChanged(MediaMetadata metadata,
            @PlaybackState.State int state) {
        synchronized (this) {
            boolean nextVisible = NotificationMediaManager.isPlayingState(state);
            mMediaHandler.removeCallbacksAndMessages(null);
+67 −0
Original line number 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.view.View
import com.android.internal.util.ContrastColorUtil
import com.android.systemui.statusbar.NotificationMediaManager
import com.android.systemui.statusbar.notification.stack.MediaHeaderView
import com.android.systemui.statusbar.phone.KeyguardBypassController
import javax.inject.Inject
import javax.inject.Singleton

/**
 * A class that controls the media notifications on the lock screen, handles its visibility and
 * is responsible for the embedding of he media experience.
 */
@Singleton
class KeyguardMediaController @Inject constructor(
    private val mediaHierarchyManager: MediaHierarchyManager,
    private val notifMediaManager: NotificationMediaManager,
    private val bypassController: KeyguardBypassController
) {
    private var view: MediaHeaderView? = null

    init {
        notifMediaManager.addCallback(object : NotificationMediaManager.MediaListener {
            override fun onMediaDataLoaded(key: String, data: MediaData) {
                updateVisibility()
            }

            override fun onMediaDataRemoved(key: String) {
                updateVisibility()
            }
        })
    }

    /**
     * Attach this controller to a media view, initializing its state
     */
    fun attach(mediaControlsView: MediaHeaderView) {
        view = mediaControlsView
        val hostView = mediaHierarchyManager.createMediaHost(
                MediaHierarchyManager.LOCATION_LOCKSCREEN)
        mediaControlsView.setMediaHost(hostView)
        updateVisibility()
    }

    private fun updateVisibility() {
        val shouldBeVisible = notifMediaManager.hasActiveMedia() && !bypassController.bypassEnabled
        view?.visibility = if (shouldBeVisible) View.VISIBLE else View.GONE
    }
}
Loading