From 44897e85d27bba5e4d3f23e4ff8b314f245d2be9 Mon Sep 17 00:00:00 2001 From: Christopher Eby Date: Tue, 25 Oct 2011 02:16:01 -0500 Subject: [PATCH] Shake actions Also moves Action into its own file since it is now also used in PlaybackService --- res/layout/shake_pref.xml | 37 ++++ res/values/translatable.xml | 6 + res/xml/preferences.xml | 21 +++ src/org/kreed/vanilla/Action.java | 107 +++++++++++ .../kreed/vanilla/FullPlaybackActivity.java | 7 +- src/org/kreed/vanilla/PlaybackActivity.java | 92 +--------- src/org/kreed/vanilla/PlaybackService.java | 173 ++++++++++++++++++ .../vanilla/ShakeThresholdPreference.java | 103 +++++++++++ 8 files changed, 454 insertions(+), 92 deletions(-) create mode 100644 res/layout/shake_pref.xml create mode 100644 src/org/kreed/vanilla/Action.java create mode 100644 src/org/kreed/vanilla/ShakeThresholdPreference.java 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) + { + } +}