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

Commit e0109bb9 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Automerger Merge Worker
Browse files

Merge "Separate updating views from deriving state" into rvc-dev am: e174ee83

Change-Id: I030f4c7ec5e4e3c92f03e6b7129fee84c6538201
parents eb910be6 e174ee83
Loading
Loading
Loading
Loading
+33 −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.keyguard

import android.graphics.drawable.Drawable

import java.util.List

/** State for lock screen media controls. */
data class KeyguardMedia(
    val foregroundColor: Int,
    val backgroundColor: Int,
    val app: String?,
    val appIcon: Drawable?,
    val artist: String?,
    val song: String?,
    val artwork: Drawable?,
    val actionIcons: List<Drawable>
)
+233 −137
Original line number Diff line number Diff line
@@ -32,6 +32,9 @@ 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;
@@ -64,39 +67,47 @@ public class KeyguardMediaPlayer {

    private final Context mContext;
    private final Executor mBackgroundExecutor;
    private float mAlbumArtRadius;
    private int mAlbumArtSize;
    private View mMediaNotifView;
    private final KeyguardMediaViewModel mViewModel;
    private KeyguardMediaObserver mObserver;

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

    /** Binds media controls to a view hierarchy. */
    public void bindView(View v) {
        if (mMediaNotifView != null) {
        if (mObserver != null) {
            throw new IllegalStateException("cannot bind views, already bound");
        }
        mMediaNotifView = v;
        loadDimens();
        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 (mMediaNotifView == null) {
        if (mObserver == null) {
            throw new IllegalStateException("cannot unbind views, nothing bound");
        }
        mMediaNotifView = null;
        mViewModel.getKeyguardMedia().removeObserver(mObserver);
        mObserver = null;
    }

    /** Clear the media controls because there isn't an active session. */
    public void clearControls() {
        if (mMediaNotifView != null) {
            mMediaNotifView.setVisibility(View.GONE);
        }
        mBackgroundExecutor.execute(mViewModel::clearControls);
    }

    /**
@@ -110,19 +121,49 @@ public class KeyguardMediaPlayer {
     */
    public void updateControls(NotificationEntry entry, Icon appIcon,
            MediaMetadata mediaMetadata) {
        if (mMediaNotifView == null) {
        if (mObserver == null) {
            throw new IllegalStateException("cannot update controls, views not bound");
        }
        if (mediaMetadata == null) {
            mMediaNotifView.setVisibility(View.GONE);
            Log.d(TAG, "media metadata was 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;
        }
        mMediaNotifView.setVisibility(View.VISIBLE);
        mBackgroundExecutor.execute(() -> mViewModel.updateControls(entry, appIcon, mediaMetadata));
    }

        Notification notif = entry.getSbn().getNotification();
    /** ViewModel for KeyguardMediaControls. */
    private static final class KeyguardMediaViewModel {

        private final Context mContext;
        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) {
            mContext = context;
            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) {

        // Computed foreground and background color based on album art.
            // 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);
@@ -144,125 +185,180 @@ public class KeyguardMediaPlayer {
            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);
            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
        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);
        }
            String app = builder.loadHeaderAppName();

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

            // 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);
        }
            String song = mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE);

            // 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);
        }
            String artist = mediaMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST);

            // Control buttons
        final List<Icon> icons = new ArrayList<>();
            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);
            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];
                icons.add(actions[idx].getIcon());
                    actionIcons.add(actions[idx].getIcon().loadDrawable(packageContext));
                    intents.add(actions[idx].actionIntent);
                } else {
                icons.add(null);
                    actionIcons.add(null);
                    intents.add(null);
                }
            }
            synchronized (mActionsLock) {
                mActions = intents;
            }

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

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

    /**
     * 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");
    /** 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]));
            }
        }

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

    private void loadDimens() {
        mAlbumArtRadius = mContext.getResources().getDimension(R.dimen.qs_media_corner_radius);
        mAlbumArtSize = (int) mContext.getResources().getDimension(
                    R.dimen.qs_media_album_size);
            // 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()));
                }
            }
        }
    }
}
+32 −7
Original line number Diff line number Diff line
@@ -22,6 +22,8 @@ import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.view.View
import android.widget.TextView
import androidx.arch.core.executor.ArchTaskExecutor
import androidx.arch.core.executor.TaskExecutor
import androidx.test.filters.SmallTest

import com.android.systemui.R
@@ -50,25 +52,46 @@ public class KeyguardMediaPlayerTest : SysuiTestCase() {
    private lateinit var mediaMetadata: MediaMetadata.Builder
    private lateinit var entry: NotificationEntryBuilder
    @Mock private lateinit var mockView: View
    private lateinit var textView: TextView
    private lateinit var songView: TextView
    private lateinit var artistView: TextView
    @Mock private lateinit var mockIcon: Icon

    private val taskExecutor: TaskExecutor = object : TaskExecutor() {
        public override fun executeOnDiskIO(runnable: Runnable) {
            runnable.run()
        }
        public override fun postToMainThread(runnable: Runnable) {
            runnable.run()
        }
        public override fun isMainThread(): Boolean {
            return true
        }
    }

    @Before
    public fun setup() {
        fakeExecutor = FakeExecutor(FakeSystemClock())
        keyguardMediaPlayer = KeyguardMediaPlayer(context, fakeExecutor)
        mockView = mock(View::class.java)
        textView = TextView(context)
        mockIcon = mock(Icon::class.java)

        mockView = mock(View::class.java)
        songView = TextView(context)
        artistView = TextView(context)
        whenever<TextView>(mockView.findViewById(R.id.header_title)).thenReturn(songView)
        whenever<TextView>(mockView.findViewById(R.id.header_artist)).thenReturn(artistView)

        mediaMetadata = MediaMetadata.Builder()
        entry = NotificationEntryBuilder()

        ArchTaskExecutor.getInstance().setDelegate(taskExecutor)

        keyguardMediaPlayer.bindView(mockView)
    }

    @After
    public fun tearDown() {
        keyguardMediaPlayer.unbindView()
        ArchTaskExecutor.getInstance().setDelegate(null)
    }

    @Test
@@ -87,34 +110,36 @@ public class KeyguardMediaPlayerTest : SysuiTestCase() {
    @Test
    public fun testUpdateControls() {
        keyguardMediaPlayer.updateControls(entry.build(), mockIcon, mediaMetadata.build())
        FakeExecutor.exhaustExecutors(fakeExecutor)
        verify(mockView).setVisibility(View.VISIBLE)
    }

    @Test
    public fun testClearControls() {
        keyguardMediaPlayer.clearControls()
        FakeExecutor.exhaustExecutors(fakeExecutor)
        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)
        assertThat(fakeExecutor.runAllReady()).isEqualTo(1)
        assertThat(songView.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)
        assertThat(fakeExecutor.runAllReady()).isEqualTo(1)
        assertThat(artistView.getText()).isEqualTo(artist)
    }
}