/* == This file is part of Tomahawk Player - <http://tomahawk-player.org> ===
*
* Copyright 2014, Enno Gottschalk <mrmaffen@googlemail.com>
*
* Tomahawk 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 3 of the License, or
* (at your option) any later version.
*
* Tomahawk 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 Tomahawk. If not, see <http://www.gnu.org/licenses/>.
*/
package org.tomahawk.tomahawk_android.services;
import org.tomahawk.tomahawk_android.TomahawkApp;
import android.annotation.TargetApi;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.media.AudioManager;
import android.media.MediaMetadata;
import android.media.MediaMetadataRetriever;
import android.media.RemoteController;
import android.media.session.MediaController;
import android.media.session.MediaSessionManager;
import android.os.Build;
import android.os.IBinder;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.util.Log;
import android.view.KeyEvent;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
/**
* Service to fetch all metadata from other media player apps. Does nothing if not run on Kitkat.
* Compat code is included in MicroService instead.
*/
@TargetApi(Build.VERSION_CODES.KITKAT)
public class RemoteControllerService extends NotificationListenerService
implements RemoteController.OnClientUpdateListener {
private static final String TAG = RemoteControllerService.class.getSimpleName();
//dimensions in pixels for artwork
private static final int BITMAP_HEIGHT = 1024;
private static final int BITMAP_WIDTH = 1024;
private RemoteController mRemoteController;
private List<MediaController> mActiveSessions;
private MediaController.Callback mSessionCallback;
private MediaSessionManager.OnActiveSessionsChangedListener mSessionsChangedListener;
@Override
public IBinder onBind(Intent intent) {
if ("android.service.notification.NotificationListenerService".equals(intent.getAction())) {
setRemoteControllerEnabled();
}
return super.onBind(intent);
}
/**
* Enables the RemoteController thus allowing us to receive metadata updates.
*/
public void setRemoteControllerEnabled() {
Log.d(TAG, "setRemoteControllerEnabled");
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
mRemoteController = new RemoteController(TomahawkApp.getContext(), this);
Object service = TomahawkApp.getContext().getSystemService(Context.AUDIO_SERVICE);
if (service instanceof AudioManager
&& ((AudioManager) service).registerRemoteController(mRemoteController)) {
mRemoteController.setArtworkConfiguration(BITMAP_WIDTH, BITMAP_HEIGHT);
setSynchronizationMode(mRemoteController,
RemoteController.POSITION_SYNCHRONIZATION_CHECK);
}
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Object service =
TomahawkApp.getContext().getSystemService(Context.MEDIA_SESSION_SERVICE);
if (service instanceof MediaSessionManager) {
MediaSessionManager manager = (MediaSessionManager) service;
ComponentName componentName =
new ComponentName(this, RemoteControllerService.class);
mSessionsChangedListener
= new MediaSessionManager.OnActiveSessionsChangedListener() {
@Override
public void onActiveSessionsChanged(List<MediaController> controllers) {
synchronized (this) {
mActiveSessions = controllers;
registerSessionCallbacks();
}
}
};
manager.addOnActiveSessionsChangedListener(mSessionsChangedListener, componentName);
synchronized (this) {
mActiveSessions = manager.getActiveSessions(componentName);
registerSessionCallbacks();
}
}
}
}
@Override
public void onDestroy() {
Log.d(TAG, "onDestroy");
setRemoteControllerDisabled();
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void registerSessionCallbacks() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
for (MediaController controller : mActiveSessions) {
if (mSessionCallback == null) {
mSessionCallback = new MediaController.Callback() {
@Override
public void onMetadataChanged(MediaMetadata metadata) {
if (metadata != null) {
String trackName =
metadata.getString(MediaMetadata.METADATA_KEY_TITLE);
String artistName =
metadata.getString(MediaMetadata.METADATA_KEY_ARTIST);
String albumArtistName =
metadata.getString(MediaMetadata.METADATA_KEY_ALBUM_ARTIST);
String albumName =
metadata.getString(MediaMetadata.METADATA_KEY_ALBUM);
MicroService.scrobbleTrack(trackName, artistName, albumName,
albumArtistName);
}
}
};
}
controller.registerCallback(mSessionCallback);
}
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void unregisterSessionCallbacks() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && mSessionCallback != null) {
for (MediaController controller : mActiveSessions) {
controller.unregisterCallback(mSessionCallback);
}
}
}
/**
* Disables RemoteController.
*/
public void setRemoteControllerDisabled() {
Log.d(TAG, "setRemoteControllerDisabled");
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
Object service = TomahawkApp.getContext().getSystemService(Context.AUDIO_SERVICE);
if (service instanceof AudioManager
&& ((AudioManager) service).registerRemoteController(mRemoteController)) {
((AudioManager) service).unregisterRemoteController(mRemoteController);
}
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Object service =
TomahawkApp.getContext().getSystemService(Context.MEDIA_SESSION_SERVICE);
if (service instanceof MediaSessionManager) {
MediaSessionManager manager = (MediaSessionManager) service;
if (mSessionsChangedListener != null) {
manager.removeOnActiveSessionsChangedListener(mSessionsChangedListener);
}
synchronized (this) {
unregisterSessionCallbacks();
mActiveSessions = new ArrayList<>();
}
}
}
}
@Override
public void onNotificationPosted(StatusBarNotification notification) {
}
@Override
public void onNotificationRemoved(StatusBarNotification notification) {
}
@Override
public void onClientChange(boolean arg0) {
}
@Override
public void onClientMetadataUpdate(RemoteController.MetadataEditor arg0) {
Log.d(TAG, "onClientMetadataUpdate");
String trackName = arg0.getString(MediaMetadataRetriever.METADATA_KEY_TITLE, null);
String artistName = arg0.getString(MediaMetadataRetriever.METADATA_KEY_ARTIST, null);
String albumArtistName = arg0
.getString(MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST, null);
String albumName = arg0.getString(MediaMetadataRetriever.METADATA_KEY_ALBUM, null);
MicroService.scrobbleTrack(trackName, artistName, albumName, albumArtistName);
}
@Override
public void onClientPlaybackStateUpdate(int arg0) {
}
@Override
public void onClientPlaybackStateUpdate(int arg0, long arg1, long arg2, float arg3) {
}
@Override
public void onClientTransportControlUpdate(int arg0) {
}
/**
* This method lets us avoid a bug in RemoteController which results in an exception when
* calling RemoteController#setSynchronizationMode(int) (doesn't seem to work though)
*/
private void setSynchronizationMode(RemoteController controller, int sync) {
if ((sync != RemoteController.POSITION_SYNCHRONIZATION_NONE) && (sync
!= RemoteController.POSITION_SYNCHRONIZATION_CHECK)) {
throw new IllegalArgumentException("Unknown synchronization mode " + sync);
}
Class<?> iRemoteControlDisplayClass;
try {
iRemoteControlDisplayClass = Class.forName("android.media.IRemoteControlDisplay");
} catch (ClassNotFoundException e1) {
throw new RuntimeException(
"Class IRemoteControlDisplay doesn't exist, can't access it with reflection");
}
Method remoteControlDisplayWantsPlaybackPositionSyncMethod;
try {
remoteControlDisplayWantsPlaybackPositionSyncMethod = AudioManager.class
.getDeclaredMethod("remoteControlDisplayWantsPlaybackPositionSync",
iRemoteControlDisplayClass, boolean.class);
remoteControlDisplayWantsPlaybackPositionSyncMethod.setAccessible(true);
} catch (NoSuchMethodException e) {
throw new RuntimeException(
"Method remoteControlDisplayWantsPlaybackPositionSync() doesn't exist, can't access it with reflection");
}
Object rcDisplay;
Field rcDisplayField;
try {
rcDisplayField = RemoteController.class.getDeclaredField("mRcd");
rcDisplayField.setAccessible(true);
rcDisplay = rcDisplayField.get(mRemoteController);
} catch (NoSuchFieldException e) {
throw new RuntimeException("Field mRcd doesn't exist, can't access it with reflection");
} catch (IllegalAccessException e) {
throw new RuntimeException("Field mRcd can't be accessed - access denied");
} catch (IllegalArgumentException e) {
throw new RuntimeException("Field mRcd can't be accessed - invalid argument");
}
AudioManager am =
(AudioManager) TomahawkApp.getContext().getSystemService(Context.AUDIO_SERVICE);
try {
remoteControlDisplayWantsPlaybackPositionSyncMethod
.invoke(am, iRemoteControlDisplayClass.cast(rcDisplay), true);
} catch (IllegalAccessException e) {
throw new RuntimeException(
"Method remoteControlDisplayWantsPlaybackPositionSync() invocation failure - access denied");
} catch (IllegalArgumentException e) {
throw new RuntimeException(
"Method remoteControlDisplayWantsPlaybackPositionSync() invocation failure - invalid arguments");
} catch (InvocationTargetException e) {
throw new RuntimeException(
"Method remoteControlDisplayWantsPlaybackPositionSync() invocation failure - invalid invocation target");
}
}
/**
* Send a keyEvent with the given keyCode to the RemoteController
*
* @param keyCode the keyCode that should be send to the RemoteController
* @return return true if both clicks (up and down) were delivered successfully
*/
private boolean sendKeyEvent(int keyCode) {
//send "down" and "up" keyevents.
KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode);
boolean first = mRemoteController.sendMediaKeyEvent(keyEvent);
keyEvent = new KeyEvent(KeyEvent.ACTION_UP, keyCode);
boolean second = mRemoteController.sendMediaKeyEvent(keyEvent);
return first && second;
}
}