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

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

Merge "Lock screen media controls" into rvc-dev am: d6a8e512 am: 63f7a048

Change-Id: I0a487562a0bcf3d54d468fd156d8d5b16c5be100
parents 483cef51 63f7a048
Loading
Loading
Loading
Loading
+153 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ 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
  -->

<!-- Layout for media controls on the lockscreen -->
<com.android.systemui.statusbar.notification.stack.MediaHeaderView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingStart="0dp"
    android:paddingEnd="0dp"
    android:focusable="true"
    android:clickable="true"
>

    <!-- Background views required by ActivatableNotificationView. -->
    <com.android.systemui.statusbar.notification.row.NotificationBackgroundView
        android:id="@+id/backgroundNormal"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
    />

    <com.android.systemui.statusbar.notification.row.NotificationBackgroundView
        android:id="@+id/backgroundDimmed"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
    />

    <com.android.systemui.statusbar.notification.FakeShadowView
        android:id="@+id/fake_shadow"
        android:layout_width="match_parent"
        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_horizontal|fill_vertical"
        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="@dimen/qs_media_album_size"
            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="@android:style/Widget.Material.Button.Borderless.Small"
                android:layout_width="48dp"
                android:layout_height="48dp"
                android:gravity="center"
                android:visibility="gone"
                android:id="@+id/action0"
            />
            <ImageButton
                style="@android:style/Widget.Material.Button.Borderless.Small"
                android:layout_width="48dp"
                android:layout_height="48dp"
                android:gravity="center"
                android:visibility="gone"
                android:id="@+id/action1"
            />
            <ImageButton
                style="@android:style/Widget.Material.Button.Borderless.Small"
                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>
+266 −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.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.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.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.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 float mAlbumArtRadius;
    private int mAlbumArtSize;
    private View mMediaNotifView;

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

    /** Binds media controls to a view hierarchy. */
    public void bindView(View v) {
        if (mMediaNotifView != null) {
            throw new IllegalStateException("cannot bind views, already bound");
        }
        mMediaNotifView = v;
        loadDimens();
    }

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

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

    /**
     * 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 (mMediaNotifView == null) {
            throw new IllegalStateException("cannot update controls, views not bound");
        }
        if (mediaMetadata == null) {
            throw new IllegalArgumentException("media metadata was null");
        }
        mMediaNotifView.setVisibility(View.VISIBLE);

        Notification notif = entry.getSbn().getNotification();

        // Computed foreground and background color based on album art.
        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
        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);
        }

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

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

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

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

        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());
                intents.add(actions[idx].actionIntent);
            } else {
                icons.add(null);
                intents.add(null);
            }
        }

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

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

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

    private void loadDimens() {
        mAlbumArtRadius = mContext.getResources().getDimension(R.dimen.qs_media_corner_radius);
        mAlbumArtSize = (int) mContext.getResources().getDimension(
                    R.dimen.qs_media_album_size);
    }
}
+17 −1
Original line number Diff line number Diff line
@@ -46,6 +46,7 @@ import android.widget.ImageView;

import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import com.android.internal.statusbar.NotificationVisibility;
import com.android.keyguard.KeyguardMediaPlayer;
import com.android.systemui.Dependency;
import com.android.systemui.Dumpable;
import com.android.systemui.Interpolators;
@@ -65,6 +66,7 @@ import com.android.systemui.statusbar.phone.ScrimState;
import com.android.systemui.statusbar.phone.StatusBar;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.util.DeviceConfigProxy;
import com.android.systemui.util.Utils;

import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -111,6 +113,7 @@ public class NotificationMediaManager implements Dumpable {
    private ScrimController mScrimController;
    @Nullable
    private LockscreenWallpaper mLockscreenWallpaper;
    private final KeyguardMediaPlayer mMediaPlayer;

    private final Executor mMainExecutor;

@@ -184,11 +187,13 @@ public class NotificationMediaManager implements Dumpable {
            NotificationEntryManager notificationEntryManager,
            MediaArtworkProcessor mediaArtworkProcessor,
            KeyguardBypassController keyguardBypassController,
            KeyguardMediaPlayer keyguardMediaPlayer,
            @Main Executor mainExecutor,
            DeviceConfigProxy deviceConfig) {
        mContext = context;
        mMediaArtworkProcessor = mediaArtworkProcessor;
        mKeyguardBypassController = keyguardBypassController;
        mMediaPlayer = keyguardMediaPlayer;
        mMediaListeners = new ArrayList<>();
        // TODO: use MediaSessionManager.SessionListener to hook us up to future updates
        // in session state
@@ -468,6 +473,7 @@ public class NotificationMediaManager implements Dumpable {
            && mBiometricUnlockController.isWakeAndUnlock();
        if (mKeyguardStateController.isLaunchTransitionFadingAway() || wakeAndUnlock) {
            mBackdrop.setVisibility(View.INVISIBLE);
            mMediaPlayer.clearControls();
            Trace.endSection();
            return;
        }
@@ -490,6 +496,14 @@ public class NotificationMediaManager implements Dumpable {
            }
        }

        NotificationEntry entry = mEntryManager
                .getActiveNotificationUnfiltered(mMediaNotificationKey);
        if (entry != null) {
            mMediaPlayer.updateControls(entry, getMediaIcon(), mediaMetadata);
        } else {
            mMediaPlayer.clearControls();
        }

        // Process artwork on a background thread and send the resulting bitmap to
        // finishUpdateMediaMetaData.
        if (metaDataChanged) {
@@ -498,7 +512,7 @@ public class NotificationMediaManager implements Dumpable {
            }
            mProcessArtworkTasks.clear();
        }
        if (artworkBitmap != null) {
        if (artworkBitmap != null && !Utils.useQsMediaPlayer(mContext)) {
            mProcessArtworkTasks.add(new ProcessArtworkTask(this, metaDataChanged,
                    allowEnterAnimation).execute(artworkBitmap));
        } else {
@@ -612,6 +626,7 @@ public class NotificationMediaManager implements Dumpable {
                    // We are unlocking directly - no animation!
                    mBackdrop.setVisibility(View.GONE);
                    mBackdropBack.setImageDrawable(null);
                    mMediaPlayer.clearControls();
                    if (windowController != null) {
                        windowController.setBackdropShowing(false);
                    }
@@ -628,6 +643,7 @@ public class NotificationMediaManager implements Dumpable {
                                mBackdrop.setVisibility(View.GONE);
                                mBackdropFront.animate().cancel();
                                mBackdropBack.setImageDrawable(null);
                                mMediaPlayer.clearControls();
                                mMainExecutor.execute(mHideBackdropFront);
                            });
                    if (mKeyguardStateController.isKeyguardFadingAway()) {
+3 −0
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import android.content.Context;
import android.os.Handler;

import com.android.internal.statusbar.IStatusBarService;
import com.android.keyguard.KeyguardMediaPlayer;
import com.android.systemui.bubbles.BubbleController;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -93,6 +94,7 @@ public interface StatusBarDependenciesModule {
            NotificationEntryManager notificationEntryManager,
            MediaArtworkProcessor mediaArtworkProcessor,
            KeyguardBypassController keyguardBypassController,
            KeyguardMediaPlayer keyguardMediaPlayer,
            @Main Executor mainExecutor,
            DeviceConfigProxy deviceConfigProxy) {
        return new NotificationMediaManager(
@@ -102,6 +104,7 @@ public interface StatusBarDependenciesModule {
                notificationEntryManager,
                mediaArtworkProcessor,
                keyguardBypassController,
                keyguardMediaPlayer,
                mainExecutor,
                deviceConfigProxy);
    }
+14 −7
Original line number Diff line number Diff line
@@ -152,7 +152,13 @@ public class MediaNotificationProcessor {
        }
    }

    private int selectForegroundColor(int backgroundColor, Palette palette) {
    /**
     * Select a foreground color depending on whether the background color is dark or light
     * @param backgroundColor Background color to coordinate with
     * @param palette Artwork palette, should be obtained from {@link generateArtworkPaletteBuilder}
     * @return foreground color
     */
    public static int selectForegroundColor(int backgroundColor, Palette palette) {
        if (ContrastColorUtil.isColorLight(backgroundColor)) {
            return selectForegroundColorForSwatches(palette.getDarkVibrantSwatch(),
                    palette.getVibrantSwatch(),
@@ -170,7 +176,7 @@ public class MediaNotificationProcessor {
        }
    }

    private int selectForegroundColorForSwatches(Palette.Swatch moreVibrant,
    private static int selectForegroundColorForSwatches(Palette.Swatch moreVibrant,
            Palette.Swatch vibrant, Palette.Swatch moreMutedSwatch, Palette.Swatch mutedSwatch,
            Palette.Swatch dominantSwatch, int fallbackColor) {
        Palette.Swatch coloredCandidate = selectVibrantCandidate(moreVibrant, vibrant);
@@ -194,7 +200,7 @@ public class MediaNotificationProcessor {
        }
    }

    private Palette.Swatch selectMutedCandidate(Palette.Swatch first,
    private static Palette.Swatch selectMutedCandidate(Palette.Swatch first,
            Palette.Swatch second) {
        boolean firstValid = hasEnoughPopulation(first);
        boolean secondValid = hasEnoughPopulation(second);
@@ -215,7 +221,8 @@ public class MediaNotificationProcessor {
        return null;
    }

    private Palette.Swatch selectVibrantCandidate(Palette.Swatch first, Palette.Swatch second) {
    private static Palette.Swatch selectVibrantCandidate(Palette.Swatch first,
            Palette.Swatch second) {
        boolean firstValid = hasEnoughPopulation(first);
        boolean secondValid = hasEnoughPopulation(second);
        if (firstValid && secondValid) {
@@ -235,7 +242,7 @@ public class MediaNotificationProcessor {
        return null;
    }

    private boolean hasEnoughPopulation(Palette.Swatch swatch) {
    private static boolean hasEnoughPopulation(Palette.Swatch swatch) {
        // We want a fraction that is at least 1% of the image
        return swatch != null
                && (swatch.getPopulation() / (float) RESIZE_BITMAP_AREA > MINIMUM_IMAGE_FRACTION);
@@ -257,7 +264,7 @@ public class MediaNotificationProcessor {
     * @param palette Artwork palette, should be obtained from {@link generateArtworkPaletteBuilder}
     * @return Swatch that should be used as the background of the media notification.
     */
    private static Palette.Swatch findBackgroundSwatch(Palette palette) {
    public static Palette.Swatch findBackgroundSwatch(Palette palette) {
        // by default we use the dominant palette
        Palette.Swatch dominantSwatch = palette.getDominantSwatch();
        if (dominantSwatch == null) {
@@ -301,7 +308,7 @@ public class MediaNotificationProcessor {
     * @param artwork Media artwork
     * @return Builder that generates the {@link Palette} for the media artwork.
     */
    private static Palette.Builder generateArtworkPaletteBuilder(Bitmap artwork) {
    public static Palette.Builder generateArtworkPaletteBuilder(Bitmap artwork) {
        // for the background we only take the left side of the image to ensure
        // a smooth transition
        return Palette.from(artwork)
Loading