Shake actions

Also moves Action into its own file since it is now also used in
PlaybackService
This commit is contained in:
Christopher Eby 2011-10-25 02:16:01 -05:00
parent 7a34f5db51
commit 44897e85d2
8 changed files with 454 additions and 92 deletions

37
res/layout/shake_pref.xml Normal file
View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2012 Christopher Eby <kreed@kreed.org>
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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/value"
android:padding="5dip"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="right" />
<SeekBar
android:id="@+id/seek_bar"
android:layout_width="fill_parent"
android:layout_height="wrap_content" />
</LinearLayout>

View File

@ -159,6 +159,12 @@ THE SOFTWARE.
<string name="default_action_title">Default Action</string>
<string name="default_playlist_action_title">Default Playlist Action</string>
<string name="pref_shake">Accelerometer Shake</string>
<string name="enable_shake_title">Enable Accelerometer Shake</string>
<string name="enable_shake_summary">Only active when music is playing. Does not work when screen is off on some devices.</string>
<string name="shake_action_title">Accelerometer Shake Action</string>
<string name="shake_threshold_title">Shake Force Threshold</string>
<string name="pref_misc">Miscellaneous Features</string>
<string name="disable_lockscreen_title">Disable Lockscreen</string>
<string name="disable_lockscreen_summary">Do not show the lockscreen with the library screen or playback screen open.</string>

View File

@ -123,6 +123,27 @@ THE SOFTWARE.
android:entryValues="@array/entry_values"
android:defaultValue="3" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/pref_shake">
<CheckBoxPreference
android:key="enable_shake"
android:title="@string/enable_shake_title"
android:summary="@string/enable_shake_summary"
android:defaultValue="false" />
<org.kreed.vanilla.ListPreferenceSummary
android:key="shake_action"
android:title="@string/shake_action_title"
android:entries="@array/swipe_action_entries"
android:entryValues="@array/swipe_action_values"
android:defaultValue="NextSong"
android:dependency="enable_shake" />
<!-- Might be nice to show something indicating the current shake force for this preference. -->
<org.kreed.vanilla.ShakeThresholdPreference
android:key="shake_threshold"
android:negativeButtonText="@null"
android:dialogLayout="@layout/shake_pref"
android:title="@string/shake_threshold_title"
android:dependency="enable_shake" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/pref_misc">
<CheckBoxPreference
android:key="disable_lockscreen"

View File

@ -0,0 +1,107 @@
/*
* Copyright (C) 2012 Christopher Eby <kreed@kreed.org>
*
* 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;
}
}
}

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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);
}
}
}

View File

@ -0,0 +1,103 @@
/*
* Copyright (C) 2012 Christopher Eby <kreed@kreed.org>
*
* 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)
{
}
}