package me.barrasso.android.volume; import android.annotation.TargetApi; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.media.RemoteController; import android.os.*; import android.os.Process; import android.service.notification.NotificationListenerService; import android.service.notification.StatusBarNotification; import android.support.v4.media.MediaMetadataCompat; import android.support.v4.media.session.PlaybackStateCompat; import android.text.TextUtils; import android.util.Pair; import com.squareup.otto.Produce; import com.squareup.otto.Subscribe; import com.squareup.otto.MainThreadBus; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import me.barrasso.android.volume.media.compat.RemoteControlCompat; import me.barrasso.android.volume.utils.Utils; import static me.barrasso.android.volume.LogUtils.LOGD; import static me.barrasso.android.volume.LogUtils.LOGI; // NOTE: DO NOT CHANGE THIS CLASS NAME ONCE PUBLISHED! // Aside from being part of the system-app API interaction, we've had to // hardcode its class name to avoid API compatibility issues (because it // descends from NotificationListenerService, trying to access anything // on it will result in a NoClassDefFoundError). This only applies to // JellyBean MR2 (4.3) /** * Simple {@link android.service.notification.NotificationListenerService} meant to be used * merely as a proxy for the media-related events we care about. Broadcasts an {@link android.content.Intent} * locally for our app to consume and monitor media events (sent locally to avoid issues relating * to performance and security). */ @TargetApi(Build.VERSION_CODES.LOLLIPOP) public final class MediaControllerService extends NotificationListenerService implements RemoteControlCompat.MediaControlListener, RemoteController.OnClientUpdateListener { public static final String ACTION_REQUEST_INTERRUPTION_FILTER = MediaControllerService.class.getPackage().getName() + '.' + "ACTION_REQUEST_INTERRUPTION_FILTER"; public static final String EXTRA_FILTER = "filter"; // Must remain as such to monitor the SharedPreferences. public static final String TAG = MediaControllerService.class.getSimpleName(); /** @return True if {@link VolumeAccessibilityService} is enabled. */ public static boolean isEnabled(Context mContext) { return Utils.isNotificationListenerServiceRunning(mContext, MediaControllerService.class); } /** @return True if {@link VolumeAccessibilityService} is running. */ public static boolean isRunning(Context context) { return Utils.isMyServiceRunning(context, MediaControllerService.class); } protected final Map<String, Bitmap> mLargeIconMap = new ConcurrentHashMap<>(); @Override public void onNotificationPosted(StatusBarNotification sbn) { LOGI(TAG, "onNotificationPosted(" + sbn.getPackageName() + ')'); super.onNotificationPosted(sbn); mLargeIconMap.put(sbn.getPackageName(), sbn.getNotification().largeIcon); } @Override public void onNotificationRemoved(StatusBarNotification sbn) { LOGI(TAG, "onNotificationRemoved(" + sbn.getPackageName() + ')'); super.onNotificationRemoved(sbn); mLargeIconMap.remove(sbn.getPackageName()); } protected RemoteControlCompat mController; public MediaControllerService() { super(); } public static Intent getInterruptionFilterRequestIntent(Context context, final int filter) { Intent request = new Intent(ACTION_REQUEST_INTERRUPTION_FILTER); request.setComponent(new ComponentName(context, MediaControllerService.class)); request.setPackage(context.getPackageName()); request.putExtra(EXTRA_FILTER, filter); return request; } /** Convenience method for sending an {@link android.content.Intent} with {@link #ACTION_REQUEST_INTERRUPTION_FILTER}. */ public static void requestInterruptionFilter(Context context, final int filter) { Intent request = getInterruptionFilterRequestIntent(context, filter); context.sendBroadcast(request); } @Override public int onStartCommand(Intent intent, int flags, int startId) { LOGI(TAG, "onStartCommand(" + intent.getAction() + ", " + flags + ", " + startId + ')'); // Handle being told to change the interruption filter (zen mode). if (!TextUtils.isEmpty(intent.getAction())) { if (ACTION_REQUEST_INTERRUPTION_FILTER.equals(intent.getAction())) { if (intent.hasExtra(EXTRA_FILTER)) { final int filter = intent.getIntExtra(EXTRA_FILTER, INTERRUPTION_FILTER_NONE); switch (filter) { case INTERRUPTION_FILTER_NONE: case INTERRUPTION_FILTER_ALL: case INTERRUPTION_FILTER_PRIORITY: LOGI(TAG, "requestInterruptionFilter(" + filter + ')'); requestInterruptionFilter(filter); break; } } } } return super.onStartCommand(intent, flags, startId); } @Override public void onCreate() { LOGI(TAG, "onCreate(pid=" + Process.myPid() + ", uid=" + Process.myUid() + ", tid=" + Process.myTid() + ")"); registerController(); Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO); MainThreadBus.get().register(this); super.onCreate(); } @Override public void onDestroy() { LOGD(TAG, "onDestroy()"); unregisterController(); MainThreadBus.get().unregister(this); super.onDestroy(); } protected void registerController() { mController = RemoteControlCompat.get(this, getClass()); mController.setMediaControlListener(this); } protected void unregisterController() { if (null != mController) { mController.release(); mController.setMediaControlListener(null); } mController = null; } @Override public void onMetadataChanged(MediaMetadataCompat metadata) { LOGI(TAG, "onMetadataChanged()"); broadcast(); } @Override public void onPlaybackStateChanged(PlaybackStateCompat state) { LOGI(TAG, "onPlaybackStateChanged()"); broadcast(); } protected void broadcast() { MainThreadBus.get().post(produceEvent()); } // Stupid RemoteController Proxy Shit @Override public void onClientChange(boolean clearing) { ((RemoteController.OnClientUpdateListener) mController).onClientChange(clearing); } @Override public void onClientMetadataUpdate(RemoteController.MetadataEditor metadataEditor) { ((RemoteController.OnClientUpdateListener) mController).onClientMetadataUpdate(metadataEditor); } @Override public void onClientPlaybackStateUpdate(int state, long stateChangeTimeMs, long currentPosMs, float speed) { ((RemoteController.OnClientUpdateListener) mController).onClientPlaybackStateUpdate(state, stateChangeTimeMs, currentPosMs, speed); } @Override public void onClientPlaybackStateUpdate(int state) { ((RemoteController.OnClientUpdateListener) mController).onClientPlaybackStateUpdate(state); } @Override public void onClientTransportControlUpdate(int transportControlFlags) { LOGI("RemoteControlJellyBeanMR2", "onClientTransportControlUpdate(" + transportControlFlags + ")"); ((RemoteController.OnClientUpdateListener) mController).onClientTransportControlUpdate(transportControlFlags); } // Android 5.0 Lollipop @Override public void onListenerConnected() { LOGI(TAG, "onListenerConnected()"); } @Override public void onListenerHintsChanged(int hints) { LOGI(TAG, "onListenerHintsChanged(" + hints + ')'); } @Override public void onInterruptionFilterChanged(int interruptionFilter) { LOGI(TAG, "onInterruptionFilterChanged(" + interruptionFilter + ')'); } // ========== EventBus ========== @Produce public Pair<MediaMetadataCompat, PlaybackStateCompat> produceEvent() { final MediaMetadataCompat metadata; if (null == mController.getMetadata()) { metadata = (new MediaMetadataCompat.Builder()).build(); } else { // If the notification didn't provide an icon, add one! MediaMetadataCompat metadata1 = mController.getMetadata(); String packageName = metadata1.getString(RemoteControlCompat.METADATA_KEY_PACKAGE); if (!metadata1.containsKey(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON) && mLargeIconMap.containsKey(packageName)) { MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder(metadata1); builder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, mLargeIconMap.get(packageName)); metadata1 = builder.build(); } metadata = metadata1; } return Pair.create(metadata, mController.getPlaybackState()); } }