diff --git a/res/layout/shake_pref.xml b/res/layout/shake_pref.xml
new file mode 100644
index 00000000..3e1d0cda
--- /dev/null
+++ b/res/layout/shake_pref.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
diff --git a/res/values/translatable.xml b/res/values/translatable.xml
index 22932eab..94085490 100644
--- a/res/values/translatable.xml
+++ b/res/values/translatable.xml
@@ -159,6 +159,12 @@ THE SOFTWARE.
Default Action
Default Playlist Action
+ Accelerometer Shake
+ Enable Accelerometer Shake
+ Only active when music is playing. Does not work when screen is off on some devices.
+ Accelerometer Shake Action
+ Shake Force Threshold
+
Miscellaneous Features
Disable Lockscreen
Do not show the lockscreen with the library screen or playback screen open.
diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml
index 68edfc22..dbda9e36 100644
--- a/res/xml/preferences.xml
+++ b/res/xml/preferences.xml
@@ -123,6 +123,27 @@ THE SOFTWARE.
android:entryValues="@array/entry_values"
android:defaultValue="3" />
+
+
+
+
+
+
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package org.kreed.vanilla;
+
+import android.content.SharedPreferences;
+
+/**
+ * Various actions that can be passed to {@link PlaybackService#performAction(Action, PlaybackActivity)}.
+ */
+enum Action {
+ /**
+ * Dummy action: do nothing.
+ */
+ Nothing,
+ /**
+ * Open the library activity.
+ */
+ Library,
+ /**
+ * If playing music, pause. Otherwise, start playing.
+ */
+ PlayPause,
+ /**
+ * Skip to the next song.
+ */
+ NextSong,
+ /**
+ * Go back to the previous song.
+ */
+ PreviousSong,
+ /**
+ * Skip to the first song from the next album.
+ */
+ NextAlbum,
+ /**
+ * Skip to the last song from the previous album.
+ */
+ PreviousAlbum,
+ /**
+ * Cycle the repeat mode.
+ */
+ Repeat,
+ /**
+ * Cycle the shuffle mode.
+ */
+ Shuffle,
+ /**
+ * Enqueue the rest of the current album.
+ */
+ EnqueueAlbum,
+ /**
+ * Enqueue the rest of the songs by the current artist.
+ */
+ EnqueueArtist,
+ /**
+ * Enqueue the rest of the songs in the current genre.
+ */
+ EnqueueGenre,
+ /**
+ * Clear the queue of all remaining songs.
+ */
+ ClearQueue,
+ /**
+ * Toggle the controls in the playback activity.
+ */
+ ToggleControls;
+
+ /**
+ * Retrieve an action from the given SharedPreferences.
+ *
+ * @param prefs The SharedPreferences instance to load from.
+ * @param key The preference key to load.
+ * @param def The value to return if the key is not found or cannot be loaded.
+ * @return The loaded action or def if no action could be loaded.
+ */
+ public static Action getAction(SharedPreferences prefs, String key, Action def)
+ {
+ try {
+ String pref = prefs.getString(key, null);
+ if (pref == null)
+ return def;
+ return Enum.valueOf(Action.class, pref);
+ } catch (Exception e) {
+ return def;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/org/kreed/vanilla/FullPlaybackActivity.java b/src/org/kreed/vanilla/FullPlaybackActivity.java
index 7b335bb5..f1ab9af9 100644
--- a/src/org/kreed/vanilla/FullPlaybackActivity.java
+++ b/src/org/kreed/vanilla/FullPlaybackActivity.java
@@ -177,8 +177,8 @@ public class FullPlaybackActivity extends PlaybackActivity
startActivity(new Intent(this, FullPlaybackActivity.class));
}
- mCoverPressAction = getAction(settings, "cover_press_action", Action.ToggleControls);
- mCoverLongPressAction = getAction(settings, "cover_longpress_action", Action.PlayPause);
+ mCoverPressAction = Action.getAction(settings, "cover_press_action", Action.ToggleControls);
+ mCoverLongPressAction = Action.getAction(settings, "cover_longpress_action", Action.PlayPause);
}
@Override
@@ -435,14 +435,13 @@ public class FullPlaybackActivity extends PlaybackActivity
mSeekBarTracking = false;
}
- @Override
public void performAction(Action action)
{
if (action == Action.ToggleControls) {
setControlsVisible(!mControlsVisible);
mHandler.sendEmptyMessage(MSG_SAVE_CONTROLS);
} else {
- super.performAction(action);
+ PlaybackService.get(this).performAction(action, this);
}
}
diff --git a/src/org/kreed/vanilla/PlaybackActivity.java b/src/org/kreed/vanilla/PlaybackActivity.java
index 4d4c72ee..26f87591 100644
--- a/src/org/kreed/vanilla/PlaybackActivity.java
+++ b/src/org/kreed/vanilla/PlaybackActivity.java
@@ -52,23 +52,6 @@ public class PlaybackActivity extends Activity
View.OnClickListener,
CoverView.Callback
{
- enum Action {
- Nothing,
- Library,
- PlayPause,
- NextSong,
- PreviousSong,
- NextAlbum,
- PreviousAlbum,
- Repeat,
- Shuffle,
- EnqueueAlbum,
- EnqueueArtist,
- EnqueueGenre,
- ClearQueue,
- ToggleControls,
- }
-
private Action mUpAction;
private Action mDownAction;
@@ -119,26 +102,6 @@ public class PlaybackActivity extends Activity
super.onDestroy();
}
- /**
- * Retrieve an action from the given SharedPreferences.
- *
- * @param prefs The SharedPreferences instance to load from.
- * @param key The preference key.
- * @param def The value to return if the key is not found or cannot be loaded.
- * @return An action
- */
- public static Action getAction(SharedPreferences prefs, String key, Action def)
- {
- try {
- String pref = prefs.getString(key, null);
- if (pref == null)
- return def;
- return Enum.valueOf(Action.class, pref);
- } catch (Exception e) {
- return def;
- }
- }
-
@Override
public void onStart()
{
@@ -150,8 +113,8 @@ public class PlaybackActivity extends Activity
startService(new Intent(this, PlaybackService.class));
SharedPreferences prefs = PlaybackService.getSettings(this);
- mUpAction = getAction(prefs, "swipe_up_action", Action.Nothing);
- mDownAction = getAction(prefs, "swipe_down_action", Action.Nothing);
+ mUpAction = Action.getAction(prefs, "swipe_up_action", Action.Nothing);
+ mDownAction = Action.getAction(prefs, "swipe_down_action", Action.Nothing);
Window window = getWindow();
if (prefs.getBoolean("disable_lockscreen", false))
@@ -411,63 +374,16 @@ public class PlaybackActivity extends Activity
PlaybackService.get(this).enqueueFromCurrent(type);
}
- public void performAction(Action action)
- {
- switch (action) {
- case Nothing:
- break;
- case Library:
- openLibrary(null);
- break;
- case PlayPause:
- playPause();
- break;
- case NextSong:
- shiftCurrentSong(SongTimeline.SHIFT_NEXT_SONG);
- break;
- case PreviousSong:
- shiftCurrentSong(SongTimeline.SHIFT_PREVIOUS_SONG);
- break;
- case NextAlbum:
- shiftCurrentSong(SongTimeline.SHIFT_NEXT_ALBUM);
- break;
- case PreviousAlbum:
- shiftCurrentSong(SongTimeline.SHIFT_PREVIOUS_ALBUM);
- break;
- case Repeat:
- cycleFinishAction();
- break;
- case Shuffle:
- cycleShuffle();
- break;
- case EnqueueAlbum:
- enqueue(MediaUtils.TYPE_ALBUM);
- break;
- case EnqueueArtist:
- enqueue(MediaUtils.TYPE_ARTIST);
- break;
- case EnqueueGenre:
- enqueue(MediaUtils.TYPE_GENRE);
- break;
- case ClearQueue:
- PlaybackService.get(this).clearQueue();
- Toast.makeText(this, R.string.queue_cleared, Toast.LENGTH_SHORT).show();
- break;
- default:
- throw new IllegalArgumentException("Invalid action: " + action);
- }
- }
-
@Override
public void upSwipe()
{
- performAction(mUpAction);
+ PlaybackService.get(this).performAction(mUpAction, this);
}
@Override
public void downSwipe()
{
- performAction(mDownAction);
+ PlaybackService.get(this).performAction(mDownAction, this);
}
private static final int GROUP_SHUFFLE = 100;
diff --git a/src/org/kreed/vanilla/PlaybackService.java b/src/org/kreed/vanilla/PlaybackService.java
index 9480c084..0011eff1 100644
--- a/src/org/kreed/vanilla/PlaybackService.java
+++ b/src/org/kreed/vanilla/PlaybackService.java
@@ -37,6 +37,10 @@ import android.content.res.TypedArray;
import android.database.ContentObserver;
import android.database.Cursor;
import android.graphics.Bitmap;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.os.Build;
@@ -70,6 +74,7 @@ public final class PlaybackService extends Service
, MediaPlayer.OnErrorListener
, SharedPreferences.OnSharedPreferenceChangeListener
, SongTimeline.Callback
+ , SensorEventListener
{
/**
* Name of the state file.
@@ -269,6 +274,10 @@ public final class PlaybackService extends Service
private PowerManager.WakeLock mWakeLock;
private NotificationManager mNotificationManager;
private AudioManager mAudioManager;
+ /**
+ * The SensorManager service.
+ */
+ private SensorManager mSensorManager;
SongTimeline mTimeline;
private Song mCurrentSong;
@@ -308,6 +317,32 @@ public final class PlaybackService extends Service
*/
private boolean mDuckedLoss;
+ /**
+ * Minimum time in milliseconds between shake actions.
+ */
+ private static final int MIN_SHAKE_PERIOD = 500;
+
+ /**
+ * Magnitude of last sensed acceleration.
+ */
+ private float mAccelLast;
+ /**
+ * Filtered acceleration used for shake detection.
+ */
+ private float mAccelFiltered;
+ /**
+ * Elapsed realtime of last shake action.
+ */
+ private long mLastShakeTime;
+ /**
+ * Minimum jerk required for shake.
+ */
+ private float mShakeThreshold;
+ /**
+ * What to do when an accelerometer shake is detected.
+ */
+ private Action mShakeAction;
+
@Override
public void onCreate()
{
@@ -325,6 +360,7 @@ public final class PlaybackService extends Service
mNotificationManager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
mAudioManager = (AudioManager)getSystemService(AUDIO_SERVICE);
+ mSensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) {
CompatFroyo.createAudioFocus();
@@ -347,6 +383,8 @@ public final class PlaybackService extends Service
mInvertNotification = settings.getBoolean("notification_inverted_color", false);
mNotificationAction = createNotificationAction(settings);
mHeadsetPause = getSettings(this).getBoolean("headset_pause", true);
+ mShakeAction = settings.getBoolean("enable_shake", false) ? Action.getAction(settings, "shake_action", Action.NextSong) : Action.Nothing;
+ mShakeThreshold = settings.getInt("shake_threshold", 80) / 10.0f;
PowerManager powerManager = (PowerManager)getSystemService(POWER_SERVICE);
mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "VanillaMusicLock");
@@ -380,6 +418,10 @@ public final class PlaybackService extends Service
synchronized (sWait) {
sWait.notifyAll();
}
+
+ mAccelFiltered = 0.0f;
+ mAccelLast = SensorManager.GRAVITY_EARTH;
+ setupSensor();
}
@Override
@@ -456,6 +498,9 @@ public final class PlaybackService extends Service
// we haven't registered the receiver yet
}
+ if (mShakeAction != Action.Nothing)
+ mSensorManager.unregisterListener(this);
+
if (mWakeLock != null && mWakeLock.isHeld())
mWakeLock.release();
@@ -473,6 +518,17 @@ public final class PlaybackService extends Service
return sSettings;
}
+ /**
+ * Setup the accelerometer.
+ */
+ private void setupSensor()
+ {
+ if (mShakeAction == Action.Nothing || (mState & FLAG_PLAYING) == 0)
+ mSensorManager.unregisterListener(this);
+ else
+ mSensorManager.registerListener(this, mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER), SensorManager.SENSOR_DELAY_UI);
+ }
+
private void loadPreference(String key)
{
SharedPreferences settings = getSettings(this);
@@ -515,6 +571,11 @@ public final class PlaybackService extends Service
mStockBroadcast = settings.getBoolean(key, false);
} else if ("headset_play".equals(key)) {
mHeadsetPlay = settings.getBoolean(key, false);
+ } else if ("enable_shake".equals(key) || "shake_action".equals(key)) {
+ mShakeAction = settings.getBoolean("enable_shake", false) ? Action.getAction(settings, "shake_action", Action.NextSong) : Action.Nothing;
+ setupSensor();
+ } else if ("shake_threshold".equals(key)) {
+ mShakeThreshold = settings.getInt("shake_threshold", 80) / 10.0f;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) {
@@ -603,6 +664,8 @@ public final class PlaybackService extends Service
if (mWakeLock != null && mWakeLock.isHeld())
mWakeLock.release();
}
+
+ setupSensor();
}
if ((toggled & FLAG_NO_MEDIA) != 0 && (state & FLAG_NO_MEDIA) != 0) {
@@ -1547,4 +1610,114 @@ public final class PlaybackService extends Service
break;
}
}
+
+ @Override
+ public void onSensorChanged(SensorEvent se)
+ {
+ double x = se.values[0];
+ double y = se.values[1];
+ double z = se.values[2];
+
+ float accel = (float)Math.sqrt(x*x + y*y + z*z);
+ float delta = accel - mAccelLast;
+ mAccelLast = accel;
+
+ float filtered = mAccelFiltered * 0.9f + delta;
+ mAccelFiltered = filtered;
+
+ if (filtered > mShakeThreshold) {
+ long now = SystemClock.elapsedRealtime();
+ if (now - mLastShakeTime > MIN_SHAKE_PERIOD) {
+ mLastShakeTime = now;
+ performAction(mShakeAction, null);
+ }
+ }
+ }
+
+ @Override
+ public void onAccuracyChanged(Sensor sensor, int accuracy)
+ {
+ }
+
+ /**
+ * Execute the given action.
+ *
+ * @param action The action to execute.
+ * @param receiver Optional. If non-null, update the PlaybackActivity with
+ * new song or state from the executed action. The activity will still be
+ * updated by the broadcast if not passed here; passing it just makes the
+ * update immediate.
+ */
+ public void performAction(Action action, PlaybackActivity receiver)
+ {
+ switch (action) {
+ case Nothing:
+ break;
+ case Library:
+ Intent intent = new Intent(this, LibraryActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ break;
+ case PlayPause: {
+ int state = playPause();
+ if (receiver != null)
+ receiver.setState(state);
+ break;
+ }
+ case NextSong: {
+ Song song = shiftCurrentSong(SongTimeline.SHIFT_NEXT_SONG);
+ if (receiver != null)
+ receiver.setSong(song);
+ break;
+ }
+ case PreviousSong: {
+ Song song = shiftCurrentSong(SongTimeline.SHIFT_PREVIOUS_SONG);
+ if (receiver != null)
+ receiver.setSong(song);
+ break;
+ }
+ case NextAlbum: {
+ Song song = shiftCurrentSong(SongTimeline.SHIFT_NEXT_ALBUM);
+ if (receiver != null)
+ receiver.setSong(song);
+ break;
+ }
+ case PreviousAlbum: {
+ Song song = shiftCurrentSong(SongTimeline.SHIFT_PREVIOUS_ALBUM);
+ if (receiver != null)
+ receiver.setSong(song);
+ break;
+ }
+ case Repeat: {
+ int state = cycleFinishAction();
+ if (receiver != null)
+ receiver.setState(state);
+ break;
+ }
+ case Shuffle: {
+ int state = cycleShuffle();
+ if (receiver != null)
+ receiver.setState(state);
+ break;
+ }
+ case EnqueueAlbum:
+ enqueueFromCurrent(MediaUtils.TYPE_ALBUM);
+ break;
+ case EnqueueArtist:
+ enqueueFromCurrent(MediaUtils.TYPE_ARTIST);
+ break;
+ case EnqueueGenre:
+ enqueueFromCurrent(MediaUtils.TYPE_GENRE);
+ break;
+ case ClearQueue:
+ clearQueue();
+ Toast.makeText(this, R.string.queue_cleared, Toast.LENGTH_SHORT).show();
+ break;
+ case ToggleControls:
+ // Handled in FullPlaybackActivity.performAction
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid action: " + action);
+ }
+ }
}
diff --git a/src/org/kreed/vanilla/ShakeThresholdPreference.java b/src/org/kreed/vanilla/ShakeThresholdPreference.java
new file mode 100644
index 00000000..4773c1e0
--- /dev/null
+++ b/src/org/kreed/vanilla/ShakeThresholdPreference.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2012 Christopher Eby
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package org.kreed.vanilla;
+
+import android.content.Context;
+import android.preference.DialogPreference;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.SeekBar;
+import android.widget.TextView;
+
+/**
+ * SeekBar preference to set the shake force threshold.
+ */
+public class ShakeThresholdPreference extends DialogPreference implements SeekBar.OnSeekBarChangeListener {
+ /**
+ * TextView to display current threshold.
+ */
+ private TextView mValueText;
+
+ public ShakeThresholdPreference(Context context, AttributeSet attrs)
+ {
+ super(context, attrs);
+ }
+
+ @Override
+ public CharSequence getSummary()
+ {
+ return getSummary(getPersistedInt(80));
+ }
+
+ /**
+ * Create the summary for the given value.
+ *
+ * @param value The force threshold.
+ * @return A string representation of the threshold.
+ */
+ private static String getSummary(int value)
+ {
+ return String.valueOf(value / 10.0f);
+ }
+
+ @Override
+ protected View onCreateDialogView()
+ {
+ View view = super.onCreateDialogView();
+ int value = getPersistedInt(80);
+
+ mValueText = (TextView)view.findViewById(R.id.value);
+ mValueText.setText(getSummary(value));
+
+ SeekBar seekBar = (SeekBar)view.findViewById(R.id.seek_bar);
+ seekBar.setMax(150);
+ seekBar.setProgress(value);
+ seekBar.setOnSeekBarChangeListener(this);
+ return view;
+ }
+
+ @Override
+ protected void onDialogClosed(boolean positiveResult)
+ {
+ notifyChanged();
+ }
+
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser)
+ {
+ if (fromUser) {
+ mValueText.setText(getSummary(progress));
+ persistInt(progress);
+ }
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar)
+ {
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar)
+ {
+ }
+}