From 7de65f6b928c3ad9f88c069a2a715b54c8f8c733 Mon Sep 17 00:00:00 2001 From: Christopher Eby Date: Sun, 23 May 2010 00:45:05 -0500 Subject: [PATCH] Split out media button code into MediaButtonHandler singleton This allows us to avoid starting up the service simply to check if a media button event should be handled. --- src/org/kreed/vanilla/MediaButtonHandler.java | 202 ++++++++++++++++++ .../kreed/vanilla/MediaButtonReceiver.java | 5 +- src/org/kreed/vanilla/PlaybackService.java | 108 +--------- 3 files changed, 214 insertions(+), 101 deletions(-) create mode 100644 src/org/kreed/vanilla/MediaButtonHandler.java diff --git a/src/org/kreed/vanilla/MediaButtonHandler.java b/src/org/kreed/vanilla/MediaButtonHandler.java new file mode 100644 index 00000000..3046093f --- /dev/null +++ b/src/org/kreed/vanilla/MediaButtonHandler.java @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2010 Christopher Eby + * + * This file is part of Vanilla Music Player. + * + * Vanilla Music Player is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Vanilla Music Player 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.kreed.vanilla; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Handler; +import android.os.Message; +import android.preference.PreferenceManager; +import android.telephony.TelephonyManager; +import android.view.KeyEvent; + +/** + * Handle a provided MediaButton event and take the appropriate action in + * PlaybackService. + */ +public class MediaButtonHandler implements Handler.Callback { + /** + * If another button event is received before this time in milliseconds + * expires, the event with be considered a double click. + */ + private static final int DOUBLE_CLICK_DELAY = 400; + + /** + * The current global instance of this class. + */ + private static MediaButtonHandler mInstance; + /** + * The Handler for delayed processing. + */ + private Handler mHandler; + /** + * Whether the headset controls should be used. 1 for yes, 0 for no, -1 for + * uninitialized. + */ + private byte mUseControls = -1; + /** + * Whether the phone is currently in a call. 1 for yes, 0 for no, -1 for + * uninitialized. + */ + private byte mInCall = -1; + + /** + * Retrieve the MediaButtonHandler singleton, creating it if necessary. + */ + public static MediaButtonHandler getInstance() + { + if (mInstance == null) + mInstance = new MediaButtonHandler(); + return mInstance; + } + + /** + * Construct a MediaButtonHandler. + */ + private MediaButtonHandler() + { + mHandler = new Handler(this); + } + + /** + * Return whether headset controls should be used, loading the preference + * if necessary. + */ + public boolean useHeadsetControls() + { + if (mUseControls == -1) { + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(ContextApplication.getContext()); + mUseControls = (byte)(settings.getBoolean("media_button", true) ? 1 : 0); + } + return mUseControls == 1; + } + + /** + * Set the cached value for the headset controls preference. + * + * @param value True if controls should be used, false otherwise. + */ + public void setUseHeadsetControls(boolean value) + { + mUseControls = (byte)(value ? 1 : 0); + } + + /** + * Return whether the phone is currently in a call. + */ + private boolean isInCall() + { + if (mInCall == -1) { + Context context = ContextApplication.getContext(); + TelephonyManager manager = (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE); + mInCall = (byte)(manager.getCallState() == TelephonyManager.CALL_STATE_IDLE ? 0 : 1); + } + return mInCall == 1; + } + + /** + * Set the cached value for whether the phone is in a call. + * + * @param value True if in a call, false otherwise. + */ + public void setInCall(boolean value) + { + mInCall = (byte)(value ? 1 : 0); + } + + /** + * Send the given action to the playback service. + * + * @param action One of the PlaybackService.ACTION_* actions. + */ + private static void act(String action) + { + Context context = ContextApplication.getContext(); + Intent intent = new Intent(context, PlaybackService.class); + intent.setAction(action); + intent.putExtra("autoplay", true); + context.startService(intent); + } + + /** + * Process a MediaButton broadcast. + * + * @param intent The intent that was broadcast + * @return True if the intent was handled and the broadcast should be + * aborted. + */ + public boolean process(Intent intent) + { + KeyEvent event = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT); + if (event == null || isInCall() || !useHeadsetControls()) + return false; + + int action = event.getAction(); + + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_HEADSETHOOK: + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + // single click: pause/resume. + // double click: next track + + if (action == KeyEvent.ACTION_DOWN) { + if (mHandler.hasMessages(MSG_SINGLE_PRESS_TIMEOUT)) { + // double click + mHandler.removeMessages(MSG_SINGLE_PRESS_TIMEOUT); + act(PlaybackService.ACTION_NEXT_SONG); + } else { + mHandler.sendEmptyMessageDelayed(MSG_SINGLE_PRESS_TIMEOUT, DOUBLE_CLICK_DELAY); + } + } + break; + case KeyEvent.KEYCODE_MEDIA_NEXT: + if (action == KeyEvent.ACTION_DOWN) + act(PlaybackService.ACTION_NEXT_SONG); + break; + case KeyEvent.KEYCODE_MEDIA_PREVIOUS: + if (action == KeyEvent.ACTION_DOWN) + act(PlaybackService.ACTION_PREVIOUS_SONG); + break; + default: + return false; + } + + return true; + } + + /** + * A delayed message that performs the single press action after the double + * click period has expired. + */ + private static final int MSG_SINGLE_PRESS_TIMEOUT = 0; + + public boolean handleMessage(Message message) + { + switch (message.what) { + case MSG_SINGLE_PRESS_TIMEOUT: + act(PlaybackService.ACTION_TOGGLE_PLAYBACK); + break; + default: + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/src/org/kreed/vanilla/MediaButtonReceiver.java b/src/org/kreed/vanilla/MediaButtonReceiver.java index d420f0f3..7fa69fa2 100644 --- a/src/org/kreed/vanilla/MediaButtonReceiver.java +++ b/src/org/kreed/vanilla/MediaButtonReceiver.java @@ -27,11 +27,8 @@ public class MediaButtonReceiver extends BroadcastReceiver { public void onReceive(Context context, Intent intent) { if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())) { - if (!intent.getBooleanExtra("org.kreed.vanilla.resent", false)) { - intent.setClass(context, PlaybackService.class); - context.startService(intent); + if (MediaButtonHandler.getInstance().process(intent)) abortBroadcast(); - } } } } diff --git a/src/org/kreed/vanilla/PlaybackService.java b/src/org/kreed/vanilla/PlaybackService.java index fe7422f4..6e4a2ddf 100644 --- a/src/org/kreed/vanilla/PlaybackService.java +++ b/src/org/kreed/vanilla/PlaybackService.java @@ -48,12 +48,10 @@ import android.provider.MediaStore; import android.telephony.PhoneStateListener; import android.telephony.TelephonyManager; import android.util.Log; -import android.view.KeyEvent; import android.widget.Toast; public final class PlaybackService extends Service implements Handler.Callback, MediaPlayer.OnCompletionListener, MediaPlayer.OnErrorListener, SharedPreferences.OnSharedPreferenceChangeListener, SongTimeline.Callback { private static final int NOTIFICATION_ID = 2; - private static final int DOUBLE_CLICK_DELAY = 400; public static final String ACTION_TOGGLE_PLAYBACK = "org.kreed.vanilla.action.TOGGLE_PLAYBACK"; public static final String ACTION_NEXT_SONG = "org.kreed.vanilla.action.NEXT_SONG"; @@ -117,7 +115,6 @@ public final class PlaybackService extends Service implements Handler.Callback, boolean mHeadsetOnly; private boolean mScrobble; private int mNotificationMode; - private byte mHeadsetControls = -1; /** * The time to wait before considering the player idle. */ @@ -143,9 +140,7 @@ public final class PlaybackService extends Service implements Handler.Callback, private ContentObserver mMediaObserver; public Receiver mReceiver; public InCallListener mCallListener; - private boolean mIgnoreNextUp; private boolean mLoaded; - boolean mInCall; /** * The volume set by the user in the preferences. */ @@ -196,32 +191,16 @@ public final class PlaybackService extends Service implements Handler.Callback, { if (intent != null) { String action = intent.getAction(); - int delta = -10; if (ACTION_TOGGLE_PLAYBACK.equals(action)) { - delta = 0; + go(0, false); } else if (ACTION_NEXT_SONG.equals(action)) { - delta = 1; // Preemptively broadcast an update in attempt to hasten UI // feedback. broadcastReplaceSong(0, getSong(+1)); + go(1, false); } else if (ACTION_PREVIOUS_SONG.equals(action)) { - delta = -1; - } else if (Intent.ACTION_MEDIA_BUTTON.equals(action)) { - KeyEvent event = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT); - boolean handled = handleMediaKey(event); - if (handled) { - if (!mLoaded) - showStartupToast(); - } else { - // We aborted this broadcast in MediaButtonReceiver. - // Since we did not handle it, we should pass it on - // to others. - intent.setComponent(null); - // Make sure we don't try to handle this again. - intent.putExtra("org.kreed.vanilla.resent", true); - sendOrderedBroadcast(intent, null); - } + go(-1, false); } else if (ACTION_PLAY_ITEMS.equals(action)) { mTimeline.chooseSongs(false, intent.getIntExtra("type", 3), intent.getLongExtra("id", -1)); mHandler.sendEmptyMessage(TRACK_CHANGED); @@ -232,13 +211,6 @@ public final class PlaybackService extends Service implements Handler.Callback, } else if (ACTION_FINISH_ENQUEUEING.equals(action)) { mTimeline.finishEnqueueing(); } - - if (delta != -10) { - if (!mLoaded) - showStartupToast(); - - go(delta, false); - } } } @@ -386,7 +358,7 @@ public final class PlaybackService extends Service implements Handler.Callback, } } } else if ("media_button".equals(key)) { - mHeadsetControls = (byte)(mSettings.getBoolean("media_button", true) ? 1 : 0); + MediaButtonHandler.getInstance().setUseHeadsetControls(mSettings.getBoolean("media_button", true)); setupReceiver(); } else if ("use_idle_timeout".equals(key) || "idle_timeout".equals(key)) { mIdleTimeout = mSettings.getBoolean("use_idle_timeout", false) ? mSettings.getInt("idle_timeout", 3600) : 0; @@ -609,6 +581,9 @@ public final class PlaybackService extends Service implements Handler.Callback, private void go(int delta, boolean autoPlay) { + if (!mLoaded) + showStartupToast(); + if (autoPlay) { synchronized (mStateLock) { mState |= FLAG_PLAYING; @@ -625,66 +600,6 @@ public final class PlaybackService extends Service implements Handler.Callback, } } - /** - * Return whether headset controls should be used, loading the preference - * if necessary. - */ - private boolean useHeadsetControls() - { - if (mSettings == null) - mSettings = PreferenceManager.getDefaultSharedPreferences(this); - if (mHeadsetControls == -1) - mHeadsetControls = (byte)(mSettings.getBoolean("media_button", true) ? 1 : 0); - return mHeadsetControls == 1; - } - - boolean handleMediaKey(KeyEvent event) - { - if (mInCall || !useHeadsetControls()) - return false; - - int action = event.getAction(); - - switch (event.getKeyCode()) { - case KeyEvent.KEYCODE_HEADSETHOOK: - case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: - // single quick press: pause/resume. - // double press: next track - // long press: unused (could also do next track? open player?) - - if (action == KeyEvent.ACTION_UP && mIgnoreNextUp) { - mIgnoreNextUp = false; - break; - } - - if (mHandler.hasMessages(MEDIA_BUTTON)) { - // double press - if (action == KeyEvent.ACTION_DOWN) { - mHandler.removeMessages(MEDIA_BUTTON); - mIgnoreNextUp = true; - go(1, true); - } - } else { - // single press - if (action == KeyEvent.ACTION_UP) - mHandler.sendEmptyMessageDelayed(MEDIA_BUTTON, DOUBLE_CLICK_DELAY); - } - break; - case KeyEvent.KEYCODE_MEDIA_NEXT: - if (action == KeyEvent.ACTION_DOWN) - go(1, true); - break; - case KeyEvent.KEYCODE_MEDIA_PREVIOUS: - if (action == KeyEvent.ACTION_DOWN) - go(-1, true); - break; - default: - return false; - } - - return true; - } - private class Receiver extends BroadcastReceiver { @Override public void onReceive(Context content, Intent intent) @@ -697,8 +612,7 @@ public final class PlaybackService extends Service implements Handler.Callback, if (mPlugged != oldPlugged && mHeadsetPause && !mPlugged || mHeadsetOnly && isSpeakerOn()) unsetFlag(FLAG_PLAYING); } else if (Intent.ACTION_MEDIA_BUTTON.equals(action)) { - KeyEvent event = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT); - if (event != null && handleMediaKey(event)) + if (MediaButtonHandler.getInstance().process(intent)) abortBroadcast(); } } @@ -711,7 +625,7 @@ public final class PlaybackService extends Service implements Handler.Callback, switch (state) { case TelephonyManager.CALL_STATE_RINGING: case TelephonyManager.CALL_STATE_OFFHOOK: - mInCall = true; + MediaButtonHandler.getInstance().setInCall(true); if (!mPlayingBeforeCall) { synchronized (mStateLock) { if (mPlayingBeforeCall = (mState & FLAG_PLAYING) != 0) @@ -720,7 +634,7 @@ public final class PlaybackService extends Service implements Handler.Callback, } break; case TelephonyManager.CALL_STATE_IDLE: - mInCall = false; + MediaButtonHandler.getInstance().setInCall(false); if (mPlayingBeforeCall) { setFlag(FLAG_PLAYING); mPlayingBeforeCall = false; @@ -761,7 +675,7 @@ public final class PlaybackService extends Service implements Handler.Callback, IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_HEADSET_PLUG); - if (useHeadsetControls()) + if (MediaButtonHandler.getInstance().useHeadsetControls()) filter.addAction(Intent.ACTION_MEDIA_BUTTON); filter.setPriority(2000); registerReceiver(mReceiver, filter);