/* * Copyright (C) 2013 by Alexander Leontev * * 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.woodblockwithoutco.remotemetadataprovider.media; import android.app.PendingIntent; import android.app.PendingIntent.CanceledException; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager.NameNotFoundException; import android.media.AudioManager; import android.os.Handler; import android.os.Looper; import android.os.SystemClock; import android.util.Log; import android.view.KeyEvent; import com.woodblockwithoutco.remotemetadataprovider.internal.MetadataUpdaterCallback; import com.woodblockwithoutco.remotemetadataprovider.internal.RemoteControlDisplay; import com.woodblockwithoutco.remotemetadataprovider.media.enums.MediaCommand; import com.woodblockwithoutco.remotemetadataprovider.media.listeners.OnArtworkChangeListener; import com.woodblockwithoutco.remotemetadataprovider.media.listeners.OnMetadataChangeListener; import com.woodblockwithoutco.remotemetadataprovider.media.listeners.OnPlaybackStateChangeListener; import com.woodblockwithoutco.remotemetadataprovider.media.listeners.OnRemoteControlFeaturesChangeListener; public final class RemoteMetadataProvider { private static RemoteMetadataProvider INSTANCE; private static final String TAG = "RemoteMetadataProvider"; private OnArtworkChangeListener mArtworkListener; private AudioManager mAudioManager; private PendingIntent mClientIntent; private Context mContext; private OnRemoteControlFeaturesChangeListener mFeaturesListener; private Handler mHandler; private boolean mIsLooperUsed = false; private Looper mLooper; private OnMetadataChangeListener mMetadataListener; private MetadataUpdaterCallback mMetadataUpdaterCallback; private OnPlaybackStateChangeListener mPlaystateListener; private RemoteControlDisplay mRemoteControlDisplay; private boolean mShouldUpdateHandler; /* * Constructor should be private as we don't want multiple instances. */ private RemoteMetadataProvider(Context context) { mContext = context; if (mContext != null) { mAudioManager = (AudioManager) mContext .getSystemService(Context.AUDIO_SERVICE); } } /** * Returns instance of RemoteMetadataProvider * * @param context * Current application context. This is required to get instance * of AudioManager. * @return Active instance of RemoteMetadataProvider. */ public static synchronized RemoteMetadataProvider getInstance( Context context) { if (INSTANCE == null) { INSTANCE = new RemoteMetadataProvider(context); } return INSTANCE; } /** * Acquires remote media controls. This method MUST be called whenever your * View displaying metadata is shown or else you will not receive metadata * updates and probably you won't be able to send media commands. */ public void acquireRemoteControls() { if (mAudioManager != null) { // if we don't have any RemoteControlDisplay or we have to update // our Handler if (mRemoteControlDisplay == null || mShouldUpdateHandler) { if (mMetadataUpdaterCallback == null) { mMetadataUpdaterCallback = new MetadataUpdaterCallback( INSTANCE); } // if we don't have any Handler or we should update it. if (mHandler == null || mShouldUpdateHandler) { if (mIsLooperUsed) { mHandler = new Handler(mLooper, mMetadataUpdaterCallback); } else { mHandler = new Handler(mMetadataUpdaterCallback); mLooper = null; } } mRemoteControlDisplay = new RemoteControlDisplay(mHandler); mShouldUpdateHandler = false; } // registering our RemoteControlDisplay mAudioManager.registerRemoteControlDisplay(mRemoteControlDisplay); } else { Log.w(TAG, "Failed to get instance of AudioManager while acquiring remote media controls"); } } /** * Drops remote media controls. This method MUST be called whenever your * View displaying metadata is hidden or else you may block other * applications working with remote media controls. * * @param destroyRemoteControls * Set this to true if you have problems with displaying artwork. * Otherwise use false. */ public void dropRemoteControls(boolean destroyRemoteControls) { if (mAudioManager != null) { mAudioManager.unregisterRemoteControlDisplay(mRemoteControlDisplay); if (destroyRemoteControls) { mRemoteControlDisplay = null; } // remove all the messages from Handler mHandler.removeMessages(RemoteControlDisplay.MSG_SET_ARTWORK); mHandler.removeMessages(RemoteControlDisplay.MSG_SET_GENERATION_ID); mHandler.removeMessages(RemoteControlDisplay.MSG_SET_METADATA); mHandler.removeMessages(RemoteControlDisplay.MSG_SET_TRANSPORT_CONTROLS); mHandler.removeMessages(RemoteControlDisplay.MSG_UPDATE_STATE); } else { Log.w(TAG, "Failed to get instance of AudioManager while adropping remote media controls"); } } /** * @return Intent for launching current player or null if there is no * current client. * @throws NameNotFoundException * This exception will be thrown in case the launch intent for * current player can't be found. */ public Intent getCurrentClientIntent() throws NameNotFoundException { if (mClientIntent == null) { return null; } return mContext.getPackageManager().getLaunchIntentForPackage( mClientIntent.getTargetPackage()); } /** * Returns current client media events receiver in form of PendingIntent. * * @return Current client media events receiver or null if there is no * client. */ public PendingIntent getCurrentClientPendingIntent() { return mClientIntent; } /** * Returns user-defined Looper. * * @return user-defined Looper or null if default is used. */ public Looper getLooper() { return mLooper; } /** * Returns the registered callback for artwork change event. * * @return The callback or null if there is nothing registered. */ public OnArtworkChangeListener getOnArtworkChangeListener() { return mArtworkListener; } /** * Returns the registered callback for metadata change event. * * @return The callback or null if there is nothing registered. */ public OnMetadataChangeListener getOnMetadataChangeListener() { return mMetadataListener; } /** * Returns the registered callback for playback state change event. * * @return The callback or null if there is nothing registered. */ public OnPlaybackStateChangeListener getOnPlaybackStateChangeListener() { return mPlaystateListener; } /** * Returns the registered callback for remote control features change event. * * @return The callback or null if there is nothing registered. */ public OnRemoteControlFeaturesChangeListener getOnRemoteControlFlagsChangeListener() { return mFeaturesListener; } /** * Check if remote media client is active(e.g. can receive media events and * provide metadata). * * @return true if there is remote media client to send events to and false * otherwise. */ public boolean isClientActive() { return !(mClientIntent == null); } /** * Tells the RemoteMetadataProvider to stop using the looper. Note that * there will be no effect until you call * {@link RemoteMetadataProvider#acquireRemoteControls()}. */ public void removeLooper() { mIsLooperUsed = false; mShouldUpdateHandler = true; } private void sendBroadcastButton(int keyCode) { if (mContext != null) { long eventtime = SystemClock.uptimeMillis(); Intent keyIntent = new Intent(Intent.ACTION_MEDIA_BUTTON, null); KeyEvent keyEvent = new KeyEvent(eventtime, eventtime, KeyEvent.ACTION_DOWN, keyCode, 0); keyIntent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent); mContext.sendOrderedBroadcast(keyIntent, null); keyEvent = KeyEvent.changeAction(keyEvent, KeyEvent.ACTION_UP); keyIntent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent); mContext.sendOrderedBroadcast(keyIntent, null); } } /** * Sends input event with the specified keycode system-wide in form of * Broadcast. Use this method only in case there is no active clients and * you want to try to initialize one. If there is active client, use * {@link RemoteMetadataProvider#sendMediaCommand(int)} instead. Please note * that not all players support this. * * @param keyCode * Button keycode to send. */ public void sendBroadcastMediaCommand(int keyCode) { sendBroadcastButton(keyCode); } /** * Sends input event with the specified MediaCommand system-wide in form of * Broadcast. Use this method only in case there is no active clients and * you want to try to initialize one. If there is active client, use * {@link RemoteMetadataProvider#sendMediaCommand(MediaCommand)} instead. * Please note that not all players support this. * * @param command * MediaCommand to send. */ public void sendBroadcastMediaCommand(MediaCommand command) { switch (command) { case REWIND: sendBroadcastButton(KeyEvent.KEYCODE_MEDIA_REWIND); break; case PREVIOUS: sendBroadcastButton(KeyEvent.KEYCODE_MEDIA_PREVIOUS); break; case PLAY: sendBroadcastButton(KeyEvent.KEYCODE_MEDIA_PLAY); break; case PAUSE: sendBroadcastButton(KeyEvent.KEYCODE_MEDIA_PAUSE); break; case PLAY_PAUSE: sendBroadcastButton(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE); break; case STOP: sendBroadcastButton(KeyEvent.KEYCODE_MEDIA_STOP); break; case NEXT: sendBroadcastButton(KeyEvent.KEYCODE_MEDIA_NEXT); break; case FAST_FORWARD: sendBroadcastButton(KeyEvent.KEYCODE_MEDIA_FAST_FORWARD); break; } } /* * Should not be used by user. Sends media button click event. */ private boolean sendButton(int keyCode) { KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode); Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON); intent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent); try { if (mClientIntent == null) { return false; } mClientIntent.send(mContext, 0, intent); } catch (CanceledException e) { // will silently fail with false return return false; } keyEvent = new KeyEvent(KeyEvent.ACTION_UP, keyCode); intent = new Intent(Intent.ACTION_MEDIA_BUTTON); intent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent); try { if (mClientIntent == null) { return false; } mClientIntent.send(mContext, 0, intent); } catch (CanceledException e) { // will silently fail with false return return false; } return true; } /** * Sends input event with the specified keycode to current player. * * @param keyCode * Button keycode to send. * @return True if event was delivered to player, false otherwise. */ public boolean sendMediaCommand(int keyCode) { if (mClientIntent != null) { return sendButton(keyCode); } // will silently fail with false return if client is missing return false; } /** * Sends input event with the specified MediaCommand to current player. * * @param command * MediaCommand enum with necessary action. * @return True if event was delivered to player, false otherwise. */ public boolean sendMediaCommand(MediaCommand command) { if (mClientIntent != null) { switch (command) { case REWIND: return sendButton(KeyEvent.KEYCODE_MEDIA_REWIND); case PREVIOUS: return sendButton(KeyEvent.KEYCODE_MEDIA_PREVIOUS); case PLAY: return sendButton(KeyEvent.KEYCODE_MEDIA_PLAY); case PAUSE: return sendButton(KeyEvent.KEYCODE_MEDIA_PAUSE); case PLAY_PAUSE: return sendButton(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE); case STOP: return sendButton(KeyEvent.KEYCODE_MEDIA_STOP); case NEXT: return sendButton(KeyEvent.KEYCODE_MEDIA_NEXT); case FAST_FORWARD: return sendButton(KeyEvent.KEYCODE_MEDIA_FAST_FORWARD); } } // will silently fail with false return if client is missing return false; } /** * Sets current client PendingIntent. Should not be used manually unless you * sure you are holding the right PendingIntent to send events to. */ public void setCurrentClientPendingIntent(PendingIntent pintent) { mClientIntent = pintent; } /** * Sets looper to be used to process messages. Please note that effect * * @param looper * Looper to be used */ public void setLooper(Looper looper) { mIsLooperUsed = true; mLooper = looper; mShouldUpdateHandler = true; } /** * Register a callback to be invoked when artwork should be updated. * * @param l * The callback that will run. */ public void setOnArtworkChangeListener(OnArtworkChangeListener l) { mArtworkListener = l; } /** * Register a callback to be invoked when metadata should be updated. * * @param l * The callback that will run. */ public void setOnMetadataChangeListener(OnMetadataChangeListener l) { mMetadataListener = l; } /** * Register a callback to be invoked when playback state should be updated. * * @param l * The callback that will run. */ public void setOnPlaybackStateChangeListener(OnPlaybackStateChangeListener l) { mPlaystateListener = l; } /** * Register a callback to be invoked when remote control features should be * updated. * * @param l * The callback that will run. */ public void setOnRemoteControlFeaturesChangeListener( OnRemoteControlFeaturesChangeListener l) { mFeaturesListener = l; } }