/* * Copyright (C) 2014 AChep@xda <artemchep@gmail.com> * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. */ package com.achep.acdisplay.services.media; import android.annotation.TargetApi; import android.content.ComponentName; import android.content.Context; import android.graphics.Bitmap; import android.media.MediaMetadata; import android.media.session.MediaController; import android.media.session.MediaSession; import android.media.session.MediaSessionManager; import android.media.session.PlaybackState; import android.os.Build; import android.os.Handler; import android.os.Looper; import android.os.SystemClock; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; import com.achep.acdisplay.R; import com.achep.acdisplay.services.MediaService; import com.achep.base.async.TaskQueueThread; import com.achep.base.tests.Check; import java.lang.ref.Reference; import java.lang.ref.WeakReference; import java.util.List; import static com.achep.base.Build.DEBUG; /** * {@inheritDoc} */ @TargetApi(Build.VERSION_CODES.LOLLIPOP) class MediaController2Lollipop extends MediaController2 { @NonNull private final ComponentName mComponent; @NonNull private final OnActiveSessionsChangedListener mSessionListener = new OnActiveSessionsChangedListener(); @NonNull private final MediaController.Callback mCallback = new MediaController.Callback() { @Override public void onMetadataChanged(MediaMetadata metadata) { super.onMetadataChanged(metadata); Check.getInstance().isInMainThread(); updateMetadata(metadata); } @Override public void onPlaybackStateChanged(@NonNull PlaybackState state) { super.onPlaybackStateChanged(state); Check.getInstance().isInMainThread(); updatePlaybackState(state.getState()); } }; /** * @author Artem Chepurnoy */ private static class OnActiveSessionsChangedListener implements MediaSessionManager.OnActiveSessionsChangedListener { @NonNull private Reference<MediaController2Lollipop> mMediaControllerRef = new WeakReference<>(null); public void setMediaController(@Nullable MediaController2Lollipop mc) { if (mc == null) { mMediaControllerRef.clear(); return; } mMediaControllerRef = new WeakReference<>(mc); } @Override public void onActiveSessionsChanged(List<MediaController> controllers) { MediaController2Lollipop p = mMediaControllerRef.get(); if (p == null) return; if (p.mMediaController != null) { for (MediaController controller : controllers) { if (p.mMediaController == controller) { // Current media controller is still alive. return; } } } MediaController mc = pickBestMediaController(controllers); if (mc != null) { p.setMediaController(mc); } else { p.clearMediaController(true); } } @Nullable private MediaController pickBestMediaController( @NonNull List<MediaController> list) { int mediaControllerScore = -1; MediaController mediaController = null; for (MediaController mc : list) { if (mc == null) continue; int mcScore = 0; // Check for the current state PlaybackState state = mc.getPlaybackState(); if (state != null) { switch (state.getState()) { case PlaybackState.STATE_STOPPED: case PlaybackState.STATE_ERROR: break; default: mcScore++; break; } } if (mcScore > mediaControllerScore) { mediaControllerScore = mcScore; mediaController = mc; } } return mediaController; } } @Nullable private MediaSessionManager mMediaSessionManager; @Nullable private MediaController mMediaController; private boolean mSessionListening; private T mThread; /** * {@inheritDoc} */ protected MediaController2Lollipop(@NonNull Context context) { super(context); mComponent = new ComponentName(context, MediaService.class); } /** * {@inheritDoc} */ @Override public void onStart(Object... objects) { super.onStart(); // Init a new thread. mThread = new T(this); mThread.setPriority(Thread.MIN_PRIORITY); mThread.start(); // Media session manager leaks/holds the context for too long. // Don't let it to leak the activity, better lak the whole app. final Context context = mContext.getApplicationContext(); mMediaSessionManager = (MediaSessionManager) context .getSystemService(Context.MEDIA_SESSION_SERVICE); try { mMediaSessionManager.addOnActiveSessionsChangedListener(mSessionListener, mComponent); mSessionListener.setMediaController(this); mSessionListening = true; } catch (SecurityException exception) { Log.w(TAG, "Failed to start Lollipop media controller: " + exception.getMessage()); // Try to unregister it, just it case. try { mMediaSessionManager.removeOnActiveSessionsChangedListener(mSessionListener); } catch (Exception e) { /* unused */ } finally { mMediaSessionManager = null; mSessionListening = false; } // Media controller needs notification listener service // permissions to be granted. return; } List<MediaController> controllers = mMediaSessionManager.getActiveSessions(mComponent); mSessionListener.onActiveSessionsChanged(controllers); } /** * {@inheritDoc} */ @Override public void onStop(Object... objects) { // Force stop the thread. mThread.finish(true); if (mSessionListening) { mSessionListening = false; mSessionListener.setMediaController(null); assert mMediaSessionManager != null; mMediaSessionManager.removeOnActiveSessionsChangedListener(mSessionListener); clearMediaController(true); } super.onStop(); mMediaSessionManager = null; } private void clearMediaController(boolean clear) { if (mMediaController != null) { mMediaController.unregisterCallback(mCallback); mMediaController = null; if (clear) { clearMetadata(); updatePlaybackState(null); } } } private void setMediaController(@NonNull MediaController controller) { if (DEBUG) Log.d(TAG, "Switching to \'" + controller.getPackageName() + "\' controller."); clearMediaController(true); mMediaController = controller; mMediaController.registerCallback(mCallback); // Get the new metadata and new playback state async-ly // to prevent possible ANRs. mThread.sendTask(new EventUpdateMetadata(mMediaController.getSessionToken())); } /** * {@inheritDoc} */ public void sendMediaAction(int action) { if (mMediaController == null) { // Maybe somebody is waiting to start his player by // this lovely event. // TODO: Check if it works as expected. MediaController2.broadcastMediaAction(mContext, action); return; } MediaController.TransportControls controls = mMediaController.getTransportControls(); switch (action) { case ACTION_PLAY_PAUSE: if (mPlaybackState == PlaybackState.STATE_PLAYING) { controls.pause(); } else { controls.play(); } break; case ACTION_STOP: controls.stop(); break; case ACTION_SKIP_TO_NEXT: controls.skipToNext(); break; case ACTION_SKIP_TO_PREVIOUS: controls.skipToPrevious(); break; default: throw new IllegalArgumentException(); } } /** * {@inheritDoc} */ @Override public void seekTo(long position) { if (mMediaController == null) { // Do nothing or crash? return; } mMediaController.getTransportControls().seekTo(position); } /** * {@inheritDoc} */ @Override public long getPlaybackBufferedPosition() { if (mMediaController == null || mMediaController.getPlaybackState() == null) { // Do nothing or crash? return -1; } return mMediaController.getPlaybackState().getBufferedPosition(); } /** * {@inheritDoc} */ @Override public long getPlaybackPosition() { if (mMediaController == null || mMediaController.getPlaybackState() == null) { // Do nothing or crash? return -1; } return mMediaController.getPlaybackState().getPosition(); } /** * Clears {@link #mMetadata metadata}. Same as calling * {@link #updateMetadata(MediaMetadata)} * with {@code null} parameter. * * @see #updateMetadata(MediaMetadata) */ private void clearMetadata() { updateMetadata(null); } /** * Updates {@link #mMetadata metadata} from given media metadata class. * This also updates play state. * * @param data Object of metadata to update from, or {@code null} to clear local metadata. * @see #clearMetadata() */ private void updateMetadata(@Nullable MediaMetadata data) { if (data == null) { if (mMetadata.isEmpty()) { // No need to clear it again nor // notify subscribers about it. return; } mMetadata.clear(); } else { String id; try { id = data.getString(MediaMetadata.METADATA_KEY_MEDIA_ID); } catch (RuntimeException e) { // This is weird, but happens on some devices // periodically. try { // Try again. id = data.getString(MediaMetadata.METADATA_KEY_MEDIA_ID); } catch (RuntimeException e2) { mMetadata.clear(); notifyOnMetadataChanged(); return; } } if (id != null && id.equals(mMetadata.id)) return; mMetadata.id = id; mMetadata.title = data.getDescription().getTitle(); mMetadata.artist = data.getText(MediaMetadata.METADATA_KEY_ARTIST); mMetadata.album = data.getText(MediaMetadata.METADATA_KEY_ALBUM); mMetadata.duration = data.getLong(MediaMetadata.METADATA_KEY_DURATION); mMetadata.generateSubtitle(); // Load the artwork Bitmap artwork = data.getBitmap(MediaMetadata.METADATA_KEY_ART); if (artwork == null) { artwork = data.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART); // Might still be null } if (artwork != null) { final int size = mContext.getResources().getDimensionPixelSize(R.dimen.media_artwork_size); try { mMetadata.bitmap = Bitmap.createScaledBitmap(artwork, size, size, true); } catch (OutOfMemoryError e) { mMetadata.bitmap = null; } } else { mMetadata.bitmap = null; // Clear previous artwork } } notifyOnMetadataChanged(); } private void updatePlaybackState(@Nullable PlaybackState state) { updatePlaybackState(state == null ? PlaybackState.STATE_NONE : state.getState()); } //-- THREADING ------------------------------------------------------------ static class T extends TaskQueueThread<E> { @NonNull private final Reference<MediaController2> mMediaControllerRef; public T(@NonNull MediaController2 mc) { mMediaControllerRef = new WeakReference<>(mc); } @Override protected void onHandleTask(E object) { MediaController2 mc = mMediaControllerRef.get(); if (mc == null) { mRunning = false; return; } object.run(mc); } @Override public void sendTask(@NonNull E object) { onHandleTask(object); } @Override protected boolean isLost() { return false; } } /** * Represents one single event. */ static abstract class E { public abstract void run(@NonNull MediaController2 mc); } /** * An event to seek to song's specific position. * * @author Artem Chepurnoy */ private static class EventUpdateMetadata extends E { @NonNull private final MediaSession.Token mToken; @NonNull private final Handler mHandler; public EventUpdateMetadata(@NonNull MediaSession.Token token) { super(); mHandler = new Handler(Looper.getMainLooper()); mToken = token; } @Override public void run(@NonNull MediaController2 mc) { final MediaController2Lollipop mcl = (MediaController2Lollipop) mc; final MediaController source = mcl.mMediaController; if (source != null && mToken.equals(source.getSessionToken())) { long now = SystemClock.elapsedRealtime(); final MediaMetadata metadata = source.getMetadata(); final PlaybackState playbackState = source.getPlaybackState(); long delta = SystemClock.elapsedRealtime() - now; Log.i(TAG, "Got the new metadata & playback state in " + delta + " millis. " + "The media controller is " + source.getPackageName()); mHandler.post(new Runnable() { @Override public void run() { mcl.updateMetadata(metadata); mcl.updatePlaybackState(playbackState); } }); } } } }