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

Commit f55bc6af authored by Beth Thibodeau's avatar Beth Thibodeau
Browse files

Adding back media resumption

Behind a flag for now, while we work on animation issues and settings
Enable with: adb shell settings put system qs_media_resumption 1

Bug: 156104922
Fixes: 154823185
Fixes: 156256312
Test: manual; atest com.android.systemui.media
Change-Id: I77e3706031da820ea0d49c5dfd17f89ae7fb7db4
parent cf3274b3
Loading
Loading
Loading
Loading
+24 −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.
  -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
        android:width="24dp"
        android:height="24dp"
        android:viewportWidth="24"
        android:viewportHeight="24">
    <path
        android:fillColor="#FF000000"
        android:pathData="M12,3v10.55c-0.59,-0.34 -1.27,-0.55 -2,-0.55 -2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4V7h4V3h-6z"/>
</vector>
+2 −0
Original line number Diff line number Diff line
@@ -2781,6 +2781,8 @@

    <!-- Close the controls associated with a specific media session [CHAR_LIMIT=NONE] -->
    <string name="controls_media_close_session">Close this media session</string>
    <!-- Label for button to resume media playback [CHAR_LIMIT=NONE] -->
    <string name="controls_media_resume">Resume</string>

    <!-- Error message indicating that a control timed out while waiting for an update [CHAR_LIMIT=30] -->
    <string name="controls_error_timeout">Inactive, check app</string>
+24 −232
Original line number Diff line number Diff line
@@ -17,12 +17,8 @@
package com.android.systemui.media;

import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.ColorStateList;
import android.graphics.Bitmap;
import android.graphics.Canvas;
@@ -35,7 +31,6 @@ import android.graphics.drawable.RippleDrawable;
import android.media.session.MediaController;
import android.media.session.MediaSession;
import android.media.session.PlaybackState;
import android.service.media.MediaBrowserService;
import android.util.Log;
import android.view.View;
import android.widget.ImageButton;
@@ -55,7 +50,6 @@ import com.android.settingslib.media.MediaOutputSliceConstants;
import com.android.settingslib.widget.AdaptiveIcon;
import com.android.systemui.R;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.qs.QSMediaBrowser;
import com.android.systemui.util.animation.TransitionLayout;
import com.android.systemui.util.concurrency.DelayableExecutor;

@@ -81,7 +75,6 @@ public class MediaControlPanel {

    private final SeekBarViewModel mSeekBarViewModel;
    private SeekBarObserver mSeekBarObserver;
    private final Executor mForegroundExecutor;
    protected final Executor mBackgroundExecutor;
    private final ActivityStarter mActivityStarter;

@@ -91,48 +84,18 @@ public class MediaControlPanel {
    private MediaSession.Token mToken;
    private MediaController mController;
    private int mBackgroundColor;
    protected ComponentName mServiceComponent;
    private boolean mIsRegistered = false;
    private String mKey;
    private int mAlbumArtSize;
    private int mAlbumArtRadius;
    private int mViewWidth;

    public static final String MEDIA_PREFERENCES = "media_control_prefs";
    public static final String MEDIA_PREFERENCE_KEY = "browser_components";
    private SharedPreferences mSharedPrefs;
    private boolean mCheckedForResumption = false;
    private QSMediaBrowser mQSMediaBrowser;

    private final MediaController.Callback mSessionCallback = new MediaController.Callback() {
        @Override
        public void onSessionDestroyed() {
            Log.d(TAG, "session destroyed");
            mController.unregisterCallback(mSessionCallback);
            clearControls();
        }
        @Override
        public void onPlaybackStateChanged(PlaybackState state) {
            final int s = state != null ? state.getState() : PlaybackState.STATE_NONE;
            if (s == PlaybackState.STATE_NONE) {
                Log.d(TAG, "playback state change will trigger resumption, state=" + state);
                clearControls();
            }
        }
    };

    /**
     * Initialize a new control panel
     * @param context
     * @param foregroundExecutor foreground executor
     * @param backgroundExecutor background executor, used for processing artwork
     * @param activityStarter activity starter
     */
    public MediaControlPanel(Context context, Executor foregroundExecutor,
            DelayableExecutor backgroundExecutor, ActivityStarter activityStarter,
            MediaHostStatesManager mediaHostStatesManager) {
    public MediaControlPanel(Context context, DelayableExecutor backgroundExecutor,
            ActivityStarter activityStarter, MediaHostStatesManager mediaHostStatesManager) {
        mContext = context;
        mForegroundExecutor = foregroundExecutor;
        mBackgroundExecutor = backgroundExecutor;
        mActivityStarter = activityStarter;
        mSeekBarViewModel = new SeekBarViewModel(backgroundExecutor);
@@ -214,45 +177,18 @@ public class MediaControlPanel {
        MediaSession.Token token = data.getToken();
        mBackgroundColor = data.getBackgroundColor();
        if (mToken == null || !mToken.equals(token)) {
            if (mQSMediaBrowser != null) {
                Log.d(TAG, "Disconnecting old media browser");
                mQSMediaBrowser.disconnect();
                mQSMediaBrowser = null;
            }
            mToken = token;
            mServiceComponent = null;
            mCheckedForResumption = false;
        }

        if (mToken != null) {
            mController = new MediaController(mContext, mToken);
        } else {
            mController = null;
        }

        ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
        ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();

        // Try to find a browser service component for this app
        // TODO also check for a media button receiver intended for restarting (b/154127084)
        // Only check if we haven't tried yet or the session token changed
        final String pkgName = data.getPackageName();
        if (mServiceComponent == null && !mCheckedForResumption) {
            Log.d(TAG, "Checking for service component");
            PackageManager pm = mContext.getPackageManager();
            Intent resumeIntent = new Intent(MediaBrowserService.SERVICE_INTERFACE);
            List<ResolveInfo> resumeInfo = pm.queryIntentServices(resumeIntent, 0);
            // TODO: look into this resumption
            if (resumeInfo != null) {
                for (ResolveInfo inf : resumeInfo) {
                    if (inf.serviceInfo.packageName.equals(mController.getPackageName())) {
                        mBackgroundExecutor.execute(() ->
                                tryUpdateResumptionList(inf.getComponentInfo().getComponentName()));
                        break;
                    }
                }
            }
            mCheckedForResumption = true;
        }

        mController.registerCallback(mSessionCallback);

        mViewHolder.getPlayer().setBackgroundTintList(
                ColorStateList.valueOf(mBackgroundColor));

@@ -267,12 +203,22 @@ public class MediaControlPanel {
        ImageView albumView = mViewHolder.getAlbumView();
        // TODO: migrate this to a view with rounded corners instead of baking the rounding
        // into the bitmap
        boolean hasArtwork = data.getArtwork() != null;
        if (hasArtwork) {
            Drawable artwork = createRoundedBitmap(data.getArtwork());
            albumView.setImageDrawable(artwork);
        }
        setVisibleAndAlpha(collapsedSet, R.id.album_art, hasArtwork);
        setVisibleAndAlpha(expandedSet, R.id.album_art, hasArtwork);

        // App icon
        ImageView appIcon = mViewHolder.getAppIcon();
        if (data.getAppIcon() != null) {
            appIcon.setImageDrawable(data.getAppIcon());
        } else {
            Drawable iconDrawable = mContext.getDrawable(R.drawable.ic_music_note);
            appIcon.setImageDrawable(iconDrawable);
        }

        // Song name
        TextView titleText = mViewHolder.getTitleText();
@@ -294,7 +240,7 @@ public class MediaControlPanel {
            final Intent intent = new Intent()
                    .setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT)
                    .putExtra(MediaOutputSliceConstants.EXTRA_PACKAGE_NAME,
                            mController.getPackageName())
                            data.getPackageName())
                    .putExtra(MediaOutputSliceConstants.KEY_MEDIA_SESSION_TOKEN, mToken);
            mActivityStarter.startActivity(intent, false, true /* dismissShade */,
                    Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
@@ -350,15 +296,11 @@ public class MediaControlPanel {
            MediaAction mediaAction = actionIcons.get(i);
            button.setImageDrawable(mediaAction.getDrawable());
            button.setContentDescription(mediaAction.getContentDescription());
            PendingIntent actionIntent = mediaAction.getIntent();
            Runnable action = mediaAction.getAction();

            button.setOnClickListener(v -> {
                if (actionIntent != null) {
                    try {
                        actionIntent.send();
                    } catch (PendingIntent.CanceledException e) {
                        e.printStackTrace();
                    }
                if (action != null) {
                    action.run();
                }
            });
            boolean visibleInCompat = actionsWhenCollapsed.contains(i);
@@ -443,14 +385,6 @@ public class MediaControlPanel {
        return mController.getPackageName();
    }

    /**
     * Return the original notification's key
     * @return The notification key
     */
    public String getKey()  {
        return mKey;
    }

    /**
     * Check whether this player has an attached media session.
     * @return whether there is a controller with a current media session.
@@ -485,150 +419,8 @@ public class MediaControlPanel {
        return (state.getState() == PlaybackState.STATE_PLAYING);
    }

    /**
     * Puts controls into a resumption state if possible, or calls removePlayer if no component was
     * found that could resume playback
     */
    public void clearControls() {
        Log.d(TAG, "clearControls to resumption state package=" + getMediaPlayerPackage());
        if (mServiceComponent == null) {
            // If we don't have a way to resume, just remove the player altogether
            Log.d(TAG, "Removing unresumable controls");
            removePlayer();
            return;
        }
        resetButtons();
    }

    /**
     * Hide the media buttons and show only a restart button
     */
    protected void resetButtons() {
        if (mViewHolder == null) {
            return;
        }
        // Hide all the old buttons

        ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
        ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
        for (int i = 1; i < ACTION_IDS.length; i++) {
            setVisibleAndAlpha(expandedSet, ACTION_IDS[i], false /*visible */);
            setVisibleAndAlpha(collapsedSet, ACTION_IDS[i], false /*visible */);
        }

        // Add a restart button
        ImageButton btn = mViewHolder.getAction0();
        btn.setOnClickListener(v -> {
            Log.d(TAG, "Attempting to restart session");
            if (mQSMediaBrowser != null) {
                mQSMediaBrowser.disconnect();
            }
            mQSMediaBrowser = new QSMediaBrowser(mContext, new QSMediaBrowser.Callback(){
                @Override
                public void onConnected() {
                    Log.d(TAG, "Successfully restarted");
                }
                @Override
                public void onError() {
                    Log.e(TAG, "Error restarting");
                    mQSMediaBrowser.disconnect();
                    mQSMediaBrowser = null;
                }
            }, mServiceComponent);
            mQSMediaBrowser.restart();
        });
        btn.setImageDrawable(mContext.getResources().getDrawable(R.drawable.lb_ic_play));
        setVisibleAndAlpha(expandedSet, ACTION_IDS[0], true /*visible */);
        setVisibleAndAlpha(collapsedSet, ACTION_IDS[0], true /*visible */);

        mSeekBarViewModel.clearController();
        // TODO: fix guts
        //        View guts = mMediaNotifView.findViewById(R.id.media_guts);
        View options = mViewHolder.getOptions();

        mViewHolder.getPlayer().setOnLongClickListener(v -> {
            // Replace player view with close/cancel view
//            guts.setVisibility(View.GONE);
            options.setVisibility(View.VISIBLE);
            return true; // consumed click
        });
        mMediaViewController.refreshState();
    }

    private void setVisibleAndAlpha(ConstraintSet set, int actionId, boolean visible) {
        set.setVisibility(actionId, visible? ConstraintSet.VISIBLE : ConstraintSet.GONE);
        set.setAlpha(actionId, visible ? 1.0f : 0.0f);
    }

    /**
     * Verify that we can connect to the given component with a MediaBrowser, and if so, add that
     * component to the list of resumption components
     */
    private void tryUpdateResumptionList(ComponentName componentName) {
        Log.d(TAG, "Testing if we can connect to " + componentName);
        if (mQSMediaBrowser != null) {
            mQSMediaBrowser.disconnect();
        }
        mQSMediaBrowser = new QSMediaBrowser(mContext,
                new QSMediaBrowser.Callback() {
                    @Override
                    public void onConnected() {
                        Log.d(TAG, "yes we can resume with " + componentName);
                        mServiceComponent = componentName;
                        updateResumptionList(componentName);
                        mQSMediaBrowser.disconnect();
                        mQSMediaBrowser = null;
                    }

                    @Override
                    public void onError() {
                        Log.d(TAG, "Cannot resume with " + componentName);
                        mServiceComponent = null;
                        if (!hasMediaSession()) {
                            // If it's not active and we can't resume, remove
                            removePlayer();
                        }
                        mQSMediaBrowser.disconnect();
                        mQSMediaBrowser = null;
                    }
                },
                componentName);
        mQSMediaBrowser.testConnection();
    }

    /**
     * Add the component to the saved list of media browser services, checking for duplicates and
     * removing older components that exceed the maximum limit
     * @param componentName
     */
    private synchronized void updateResumptionList(ComponentName componentName) {
        // Add to front of saved list
        if (mSharedPrefs == null) {
            mSharedPrefs = mContext.getSharedPreferences(MEDIA_PREFERENCES, 0);
        }
        String componentString = componentName.flattenToString();
        String listString = mSharedPrefs.getString(MEDIA_PREFERENCE_KEY, null);
        if (listString == null) {
            listString = componentString;
        } else {
            String[] components = listString.split(QSMediaBrowser.DELIMITER);
            StringBuilder updated = new StringBuilder(componentString);
            int nBrowsers = 1;
            for (int i = 0; i < components.length
                    && nBrowsers < QSMediaBrowser.MAX_RESUMPTION_CONTROLS; i++) {
                if (componentString.equals(components[i])) {
                    continue;
                }
                updated.append(QSMediaBrowser.DELIMITER).append(components[i]);
                nBrowsers++;
            }
            listString = updated.toString();
        }
        mSharedPrefs.edit().putString(MEDIA_PREFERENCE_KEY, listString).apply();
    }

    /**
     * Called when a player can't be resumed to give it an opportunity to hide or remove itself
     */
    protected void removePlayer() { }
}
+5 −3
Original line number Diff line number Diff line
@@ -32,17 +32,19 @@ data class MediaData(
    val artwork: Icon?,
    val actions: List<MediaAction>,
    val actionsToShowInCompact: List<Int>,
    val packageName: String?,
    val packageName: String,
    val token: MediaSession.Token?,
    val clickIntent: PendingIntent?,
    val device: MediaDeviceData?,
    val notificationKey: String = "INVALID"
    var resumeAction: Runnable?,
    val notificationKey: String = "INVALID",
    var hasCheckedForResume: Boolean = false
)

/** State of a media action. */
data class MediaAction(
    val drawable: Drawable?,
    val intent: PendingIntent?,
    val action: Runnable?,
    val contentDescription: CharSequence?
)

+12 −6
Original line number Diff line number Diff line
@@ -32,9 +32,15 @@ class MediaDataCombineLatest @Inject constructor(

    init {
        dataSource.addListener(object : MediaDataManager.Listener {
            override fun onMediaDataLoaded(key: String, data: MediaData) {
            override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
                if (oldKey != null && !oldKey.equals(key)) {
                    val s = entries[oldKey]?.second
                    entries[key] = data to entries[oldKey]?.second
                    entries.remove(oldKey)
                } else {
                    entries[key] = data to entries[key]?.second
                update(key)
                }
                update(key, oldKey)
            }
            override fun onMediaDataRemoved(key: String) {
                remove(key)
@@ -43,7 +49,7 @@ class MediaDataCombineLatest @Inject constructor(
        deviceSource.addListener(object : MediaDeviceManager.Listener {
            override fun onMediaDeviceChanged(key: String, data: MediaDeviceData?) {
                entries[key] = entries[key]?.first to data
                update(key)
                update(key, key)
            }
            override fun onKeyRemoved(key: String) {
                remove(key)
@@ -61,13 +67,13 @@ class MediaDataCombineLatest @Inject constructor(
     */
    fun removeListener(listener: MediaDataManager.Listener) = listeners.remove(listener)

    private fun update(key: String) {
    private fun update(key: String, oldKey: String?) {
        val (entry, device) = entries[key] ?: null to null
        if (entry != null && device != null) {
            val data = entry.copy(device = device)
            val listenersCopy = listeners.toSet()
            listenersCopy.forEach {
                it.onMediaDataLoaded(key, data)
                it.onMediaDataLoaded(key, oldKey, data)
            }
        }
    }
Loading