/* * 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.app.Activity; import android.content.Context; import android.content.Intent; import android.support.annotation.NonNull; import android.support.v4.media.session.PlaybackStateCompat; import android.util.Log; import android.view.KeyEvent; import com.achep.acdisplay.Atomic; import com.achep.base.Device; import com.achep.base.interfaces.ISubscriptable; import com.achep.base.tests.Check; import java.util.ArrayList; import timber.log.Timber; import static com.achep.base.Build.DEBUG; /** * Allows an app to interact with an ongoing media session. Media buttons and * other commands can be sent to the session. A callback may be registered to * receive updates from the session, such as metadata and play state changes. * * @author Artem Chepurnoy */ public abstract class MediaController2 implements Atomic.Callback, ISubscriptable<MediaController2.MediaListener> { protected static final String TAG = "MediaController"; public static final int ACTION_SKIP_TO_NEXT = 2; public static final int ACTION_SKIP_TO_PREVIOUS = 3; public static final int ACTION_PLAY_PAUSE = 0; public static final int ACTION_STOP = 1; /** * Creates new instance, created for working on this device's * Android version. * * @return new instance. */ @NonNull public static MediaController2 newInstance(@NonNull Activity activity) { if (Device.hasLollipopApi()) { return new MediaController2Lollipop(activity); } else if (Device.hasKitKatApi()) { return new MediaController2KitKat(activity); } return new MediaController2Empty(activity); } /** * Emulates hardware buttons' click via broadcast system. * * @see android.view.KeyEvent */ public static void broadcastMediaAction(@NonNull Context context, int action) { int keyCode; switch (action) { case ACTION_PLAY_PAUSE: keyCode = KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE; break; case ACTION_STOP: keyCode = KeyEvent.KEYCODE_MEDIA_STOP; break; case ACTION_SKIP_TO_NEXT: keyCode = KeyEvent.KEYCODE_MEDIA_NEXT; break; case ACTION_SKIP_TO_PREVIOUS: keyCode = KeyEvent.KEYCODE_MEDIA_PREVIOUS; break; default: throw new IllegalArgumentException(); } Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON); KeyEvent keyDown = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode); KeyEvent keyUp = new KeyEvent(KeyEvent.ACTION_UP, keyCode); context.sendOrderedBroadcast(intent.putExtra(Intent.EXTRA_KEY_EVENT, keyDown), null); context.sendOrderedBroadcast(intent.putExtra(Intent.EXTRA_KEY_EVENT, keyUp), null); } /** * Callback for receiving updates on from the session. A Callback can be * registered using {@link #registerListener(MediaListener)} */ public interface MediaListener { /** * Override to handle changes to the current metadata. <br/><br/><b>Warning:</b> You must NOT call * {@link #registerListener(MediaListener)} nor {@link #unregisterListener(MediaListener)} * from here, otherwise it will crash! * * @param metadata The current metadata for the session. * @see com.achep.acdisplay.services.media.Metadata * @see #getMetadata() */ void onMetadataChanged(@NonNull Metadata metadata); /** * Override to handle changes in playback state. <br/><br/><b>Warning:</b> You must NOT call * {@link #registerListener(MediaListener)} nor {@link #unregisterListener(MediaListener)} * from here, otherwise it will crash! * * @param state The new playback state of the session * @see #getPlaybackState() */ void onPlaybackStateChanged(int state); } @NonNull private final Atomic mAtomic; @NonNull protected final Context mContext; @NonNull protected final ArrayList<MediaListener> mListeners; @NonNull protected final Metadata mMetadata; protected int mPlaybackState; protected MediaController2(@NonNull Context context) { mContext = context; mListeners = new ArrayList<>(); mMetadata = new Metadata(); mAtomic = new Atomic(this); } public void start() { Timber.d("Starting the media controller: was_running=" + mAtomic.isRunning()); mAtomic.start(); } public void stop() { Timber.d("Stopping the media controller: was_running=" + mAtomic.isRunning()); mAtomic.stop(); } /** * {@inheritDoc} */ @Override public void onStart(Object... objects) { /* empty */ } /** * {@inheritDoc} */ @Override public void onStop(Object... objects) { mPlaybackState = PlaybackStateCompat.STATE_NONE; } /** * {@inheritDoc} */ @Override public void registerListener(@NonNull MediaListener listener) { synchronized (this) { mListeners.add(listener); } } /** * {@inheritDoc} */ @Override public void unregisterListener(@NonNull MediaListener listener) { synchronized (this) { mListeners.remove(listener); } } @NonNull public MediaController2 asyncWrap() { return this instanceof MediaControllerAsyncWrapper ? this : new MediaControllerAsyncWrapper(this); } /** * Sends media action. One of the following: * <ul> * <li> {@link #ACTION_PLAY_PAUSE}</li> * <li> {@link #ACTION_STOP}</li> * <li> {@link #ACTION_SKIP_TO_NEXT}</li> * <li> {@link #ACTION_SKIP_TO_PREVIOUS}</li> * </ul> */ public abstract void sendMediaAction(int action); /** * Move to a new location in the media stream. * * @param position Position to move to, in milliseconds. */ public abstract void seekTo(long position); /** * Get the current buffered position in ms. This is the farthest playback point * that can be reached from the current position using only buffered content. * * @return the current buffered position in ms. or {@code -1} if something went wrong. */ public abstract long getPlaybackBufferedPosition(); /** * Get the current playback position in ms. * * @return the current playback position in ms. or {@code -1} if something went wrong. */ public abstract long getPlaybackPosition(); protected void notifyOnMetadataChanged() { Check.getInstance().isInMainThread(); if (DEBUG) Log.d(TAG, "Notifying on metadata state changed."); synchronized (this) { for (MediaListener listener : mListeners) { listener.onMetadataChanged(mMetadata); } } } protected void updatePlaybackState(int playbackState) { if (mPlaybackState == (mPlaybackState = playbackState)) return; notifyOnPlaybackStateChanged(); } protected void notifyOnPlaybackStateChanged() { Check.getInstance().isInMainThread(); if (DEBUG) Log.d(TAG, "Notifying on playback state changed."); synchronized (this) { for (MediaListener listener : mListeners) { listener.onPlaybackStateChanged(mPlaybackState); } } } /** * Get the current metadata for this session. * * @return {@link Metadata the metadata} of playing track. */ @NonNull public Metadata getMetadata() { return mMetadata; } /** * Get the current state of playback. One of the following: * <ul> * <li> {@link android.media.session.PlaybackState#STATE_NONE}</li> * <li> {@link android.media.session.PlaybackState#STATE_STOPPED}</li> * <li> {@link android.media.session.PlaybackState#STATE_PLAYING}</li> * <li> {@link android.media.session.PlaybackState#STATE_PAUSED}</li> * <li> {@link android.media.session.PlaybackState#STATE_FAST_FORWARDING}</li> * <li> {@link android.media.session.PlaybackState#STATE_REWINDING}</li> * <li> {@link android.media.session.PlaybackState#STATE_BUFFERING}</li> * <li> {@link android.media.session.PlaybackState#STATE_ERROR}</li> * </ul> * You also may use {@link android.support.v4.media.session.PlaybackStateCompat} to * access those values. */ public int getPlaybackState() { return mPlaybackState; } }