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

Commit 1c18828d authored by Jim Miller's avatar Jim Miller
Browse files

Fix 5044158: Integrate music transport control into LockScreen

This integrates a new version of TransportControlView into LockScreen
and adds plumbing to handle new AudioService events to show/hide the view
and updates the required assets for all devices.

Updated to use new AudioManager API. Since the current API only supports
one RCD, the handler now lives in TransportControlView.

Change-Id: I220d4dd760bef35bd84209adc3c5829bf5bc9a2c
parent f0eb7655
Loading
Loading
Loading
Loading
+324 −43
Original line number Diff line number Diff line
@@ -16,88 +16,369 @@

package com.android.internal.widget;

import com.android.internal.R;
import java.lang.ref.WeakReference;

import com.android.internal.widget.LockScreenWidgetCallback;
import com.android.internal.widget.LockScreenWidgetInterface;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.media.AudioManager;
import android.media.MediaMetadataRetriever;
import android.media.RemoteControlClient;
import android.media.IRemoteControlDisplay;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.RemoteException;
import android.text.Spannable;
import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.LinearLayout;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;


import com.android.internal.R;

public class TransportControlView extends FrameLayout implements OnClickListener,
        LockScreenWidgetInterface {

    private static final int MSG_UPDATE_STATE = 100;
    private static final int MSG_SET_METADATA = 101;
    private static final int MSG_SET_TRANSPORT_CONTROLS = 102;
    private static final int MSG_SET_ARTWORK = 103;
    private static final int MSG_SET_GENERATION_ID = 104;
    private static final int MAXDIM = 512;
    protected static final boolean DEBUG = true;
    protected static final String TAG = "TransportControlView";

    private ImageView mAlbumArt;
    private TextView mTrackTitle;
    private ImageView mBtnPrev;
    private ImageView mBtnPlay;
    private ImageView mBtnNext;
    private int mClientGeneration;
    private Metadata mMetadata = new Metadata();
    private boolean mAttached;
    private ComponentName mClientName;
    private int mTransportControlFlags;
    private int mPlayState;
    private AudioManager mAudioManager;
    private LockScreenWidgetCallback mWidgetCallbacks;
    private IRemoteControlDisplayWeak mIRCD;

    /**
 * A special widget for displaying audio playback ("transport controls") in LockScreen.
 *
     * The metadata which should be populated into the view once we've been attached
     */
public class TransportControlView extends LinearLayout implements LockScreenWidgetInterface,
        OnClickListener {
    private static final String TAG = "TransportControlView";
    static final int sViewIds[] = { R.id.control_prev, R.id.control_pauseplay, R.id.control_next };
    protected static final int AUDIO_FOCUS_CHANGED = 100;
    private LockScreenWidgetCallback mCallback;
    private Bundle mPopulateMetadataWhenAttached = null;

    // This handler is required to ensure messages from IRCD are handled in sequence and on
    // the UI thread.
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
            case AUDIO_FOCUS_CHANGED:
                handleAudioFocusChange(msg.arg1);
            case MSG_UPDATE_STATE:
                if (mClientGeneration == msg.arg1) updatePlayPauseState(msg.arg2);
                break;

            case MSG_SET_METADATA:
                if (mClientGeneration == msg.arg1) updateMetadata((Bundle) msg.obj);
                break;

            case MSG_SET_TRANSPORT_CONTROLS:
                if (mClientGeneration == msg.arg1) updateTransportControls(msg.arg2);
                break;

            case MSG_SET_ARTWORK:
                if (mClientGeneration == msg.arg1) {
                    mMetadata.bitmap = (Bitmap) msg.obj;
                    mAlbumArt.setImageBitmap(mMetadata.bitmap);
                }
                break;

            case MSG_SET_GENERATION_ID:
                if (mWidgetCallbacks != null) {
                    boolean clearing = msg.arg2 != 0;
                    if (DEBUG) Log.v(TAG, "New genId = " + msg.arg1 + ", clearing = " + clearing);
                    if (!clearing) {
                        mWidgetCallbacks.requestShow(TransportControlView.this);
                    } else {
                        mWidgetCallbacks.requestHide(TransportControlView.this);
                    }
    };
                }
                mClientGeneration = msg.arg1;
                mClientName = (ComponentName) msg.obj;
                break;

    AudioManager.OnAudioFocusChangeListener mAudioFocusChangeListener =
        new AudioManager.OnAudioFocusChangeListener() {
            public void onAudioFocusChange(final int focusChange) {
                mHandler.obtainMessage(AUDIO_FOCUS_CHANGED, focusChange, 0).sendToTarget();
            }
        }
    };

    public TransportControlView(Context context) {
        this(context, null);
    /**
     * This class is required to have weak linkage to the current TransportControlView
     * because the remote process can hold a strong reference to this binder object and
     * we can't predict when it will be GC'd in the remote process. Without this code, it
     * would allow a heavyweight object to be held on this side of the binder when there's
     * no requirement to run a GC on the other side.
     */
    private static class IRemoteControlDisplayWeak extends IRemoteControlDisplay.Stub {
        private WeakReference<Handler> mLocalHandler;

        IRemoteControlDisplayWeak(Handler handler) {
            mLocalHandler = new WeakReference<Handler>(handler);
        }

    public TransportControlView(Context context, AttributeSet attrs) {
        super(context, attrs);
        public void setPlaybackState(int generationId, int state) {
            Handler handler = mLocalHandler.get();
            if (handler != null) {
                handler.obtainMessage(MSG_UPDATE_STATE, generationId, state).sendToTarget();
            }
        }

    protected void handleAudioFocusChange(int focusChange) {
        // TODO
        public void setMetadata(int generationId, Bundle metadata) {
            Handler handler = mLocalHandler.get();
            if (handler != null) {
                handler.obtainMessage(MSG_SET_METADATA, generationId, 0, metadata).sendToTarget();
            }
        }

    public void setCallback(LockScreenWidgetCallback callback) {
        mCallback = callback;
        public void setTransportControlFlags(int generationId, int flags) {
            Handler handler = mLocalHandler.get();
            if (handler != null) {
                handler.obtainMessage(MSG_SET_TRANSPORT_CONTROLS, generationId, flags)
                        .sendToTarget();
            }
        }

        public void setArtwork(int generationId, Bitmap bitmap) {
            Handler handler = mLocalHandler.get();
            if (handler != null) {
                handler.obtainMessage(MSG_SET_ARTWORK, generationId, 0, bitmap).sendToTarget();
            }
        }

        public void setAllMetadata(int generationId, Bundle metadata, Bitmap bitmap) {
            Handler handler = mLocalHandler.get();
            if (handler != null) {
                handler.obtainMessage(MSG_SET_METADATA, generationId, 0, metadata).sendToTarget();
                handler.obtainMessage(MSG_SET_ARTWORK, generationId, 0, bitmap).sendToTarget();
            }
        }

        public void setCurrentClientId(int clientGeneration, ComponentName clientEventReceiver,
                boolean clearing) throws RemoteException {
            Handler handler = mLocalHandler.get();
            if (handler != null) {
                handler.obtainMessage(MSG_SET_GENERATION_ID,
                    clientGeneration, (clearing ? 1 : 0), clientEventReceiver).sendToTarget();
            }
        }
    };

    public TransportControlView(Context context, AttributeSet attrs) {
        super(context, attrs);
        Log.v(TAG, "Create TCV " + this);
        mAudioManager = new AudioManager(mContext);
        mIRCD = new IRemoteControlDisplayWeak(mHandler);
    }

    private void updateTransportControls(int transportControlFlags) {
        mTransportControlFlags = transportControlFlags;
    }

    @Override
    public void onFinishInflate() {
        for (int i = 0; i < sViewIds.length; i++) {
            View view = findViewById(sViewIds[i]);
            if (view != null) {
        super.onFinishInflate();
        mTrackTitle = (TextView) findViewById(R.id.title);
        mTrackTitle.setSelected(true); // enable marquee
        mAlbumArt = (ImageView) findViewById(R.id.albumart);
        mBtnPrev = (ImageView) findViewById(R.id.btn_prev);
        mBtnPlay = (ImageView) findViewById(R.id.btn_play);
        mBtnNext = (ImageView) findViewById(R.id.btn_next);
        final View buttons[] = { mBtnPrev, mBtnPlay, mBtnNext };
        for (View view : buttons) {
            view.setOnClickListener(this);
        }
    }

    @Override
    public void onAttachedToWindow() {
        super.onAttachedToWindow();
        if (mPopulateMetadataWhenAttached != null) {
            updateMetadata(mPopulateMetadataWhenAttached);
            mPopulateMetadataWhenAttached = null;
        }
        if (!mAttached) {
            if (DEBUG) Log.v(TAG, "Registering TCV " + this);
            mAudioManager.registerRemoteControlDisplay(mIRCD);
        }
        mAttached = true;
    }

    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.control_prev:
                // TODO
    @Override
    public void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        if (mAttached) {
            if (DEBUG) Log.v(TAG, "Unregistering TCV " + this);
            mAudioManager.unregisterRemoteControlDisplay(mIRCD);
        }
        mAttached = false;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int dim = Math.min(MAXDIM, Math.max(getWidth(), getHeight()));
//        Log.v(TAG, "setting max bitmap size: " + dim + "x" + dim);
//        mAudioManager.remoteControlDisplayUsesBitmapSize(mIRCD, dim, dim);
    }

    class Metadata {
        private String artist;
        private String trackTitle;
        private String albumTitle;
        private Bitmap bitmap;

        public String toString() {
            return "Metadata[artist=" + artist + " trackTitle=" + trackTitle + " albumTitle=" + albumTitle + "]";
        }
    }

    private String getMdString(Bundle data, int id) {
        return data.getString(Integer.toString(id));
    }

    private void updateMetadata(Bundle data) {
        if (mAttached) {
            mMetadata.artist = getMdString(data, MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST);
            mMetadata.trackTitle = getMdString(data, MediaMetadataRetriever.METADATA_KEY_TITLE);
            mMetadata.albumTitle = getMdString(data, MediaMetadataRetriever.METADATA_KEY_ALBUM);
            populateMetadata();
        } else {
            mPopulateMetadataWhenAttached = data;
        }
    }

    /**
     * Populates the given metadata into the view
     */
    private void populateMetadata() {
        StringBuilder sb = new StringBuilder();
        int trackTitleLength = 0;
        if (!TextUtils.isEmpty(mMetadata.trackTitle)) {
            sb.append(mMetadata.trackTitle);
            trackTitleLength = mMetadata.trackTitle.length();
        }
        if (!TextUtils.isEmpty(mMetadata.artist)) {
            if (sb.length() != 0) {
                sb.append(" - ");
            }
            sb.append(mMetadata.artist);
        }
        if (!TextUtils.isEmpty(mMetadata.albumTitle)) {
            if (sb.length() != 0) {
                sb.append(" - ");
            }
            sb.append(mMetadata.albumTitle);
        }
        mTrackTitle.setText(sb.toString(), TextView.BufferType.SPANNABLE);
        Spannable str = (Spannable) mTrackTitle.getText();
        if (trackTitleLength != 0) {
            str.setSpan(new ForegroundColorSpan(0xffffffff), 0, trackTitleLength,
                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            trackTitleLength++;
        }
        if (sb.length() > trackTitleLength) {
            str.setSpan(new ForegroundColorSpan(0x7fffffff), trackTitleLength, sb.length(),
                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        }

        mAlbumArt.setImageBitmap(mMetadata.bitmap);
        final int flags = mTransportControlFlags;
        setVisibilityBasedOnFlag(mBtnPrev, flags, RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS);
        setVisibilityBasedOnFlag(mBtnNext, flags, RemoteControlClient.FLAG_KEY_MEDIA_NEXT);
        setVisibilityBasedOnFlag(mBtnPrev, flags,
                RemoteControlClient.FLAG_KEY_MEDIA_PLAY
                | RemoteControlClient.FLAG_KEY_MEDIA_PAUSE
                | RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE
                | RemoteControlClient.FLAG_KEY_MEDIA_STOP);

        updatePlayPauseState(mPlayState);
    }

    private static void setVisibilityBasedOnFlag(View view, int flags, int flag) {
        if ((flags & flag) != 0) {
            view.setVisibility(View.VISIBLE);
        } else {
            view.setVisibility(View.GONE);
        }
    }

    private void updatePlayPauseState(int state) {
        if (DEBUG) Log.v(TAG,
                "updatePlayPauseState(), old=" + mPlayState + ", state=" + state);
        if (state == mPlayState) {
            return;
        }
        switch (state) {
            case RemoteControlClient.PLAYSTATE_PLAYING:
                mBtnPlay.setImageResource(com.android.internal.R.drawable.ic_media_pause);
                break;

            case R.id.control_pauseplay:
                // TODO
            case RemoteControlClient.PLAYSTATE_BUFFERING:
                mBtnPlay.setImageResource(com.android.internal.R.drawable.ic_media_stop);
                break;

            case R.id.control_next:
                // TODO
            case RemoteControlClient.PLAYSTATE_PAUSED:
            default:
                mBtnPlay.setImageResource(com.android.internal.R.drawable.ic_media_play);
                break;
        }
        // Have any button click extend lockscreen's timeout.
        if (mCallback != null) {
            mCallback.userActivity(this);
        mPlayState = state;
    }

    public void onClick(View v) {
        int keyCode = -1;
        if (v == mBtnPrev) {
            keyCode = KeyEvent.KEYCODE_MEDIA_PREVIOUS;
        } else if (v == mBtnNext) {
            keyCode = KeyEvent.KEYCODE_MEDIA_NEXT;
        } else if (v == mBtnPlay) {
            keyCode = KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE;

        }
        if (keyCode != -1) {
            sendMediaButtonClick(keyCode);
            if (mWidgetCallbacks != null) {
                mWidgetCallbacks.userActivity(this);
            }
        }
    }

    private void sendMediaButtonClick(int keyCode) {
        // TODO: target to specific player based on mClientName
        KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode);
        Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
        intent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent);
        getContext().sendOrderedBroadcast(intent, null);

        keyEvent = new KeyEvent(KeyEvent.ACTION_UP, keyCode);
        intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
        intent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent);
        getContext().sendOrderedBroadcast(intent, null);
    }

    public void setCallback(LockScreenWidgetCallback callback) {
        mWidgetCallbacks = callback;
    }

}
+243 B
Loading image diff...
+4.11 KiB (5.2 KiB)
Loading image diff...
+230 B
Loading image diff...
+2.17 KiB (3.08 KiB)
Loading image diff...
Loading