Commit 758ec43f authored by Mikalacki Sava's avatar Mikalacki Sava
Browse files

Eleven: shake to play next song, only available while music is playing.

Allows user to shake his device to switch to next song.
This feature is available through settings and is invoked
only while music is playing.

Change-Id: Ifb0866565d49443af7f3ac679e80601660506515
parent fd06a775
......@@ -48,6 +48,9 @@
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<!-- Accelerometer feature for shake to play -->
<uses-feature android:name="android.hardware.sensor.accelerometer" />
<application
android:name="com.cyanogenmod.eleven.ElevenApplication"
android:allowBackup="true"
......
......@@ -140,6 +140,8 @@
<string name="settings_show_music_visualization_title">Show music visualization</string>
<string name="settings_show_lyrics_title">Show song lyrics</string>
<string name="settings_show_lyrics_summary">For songs that have an srt file</string>
<string name="settings_shake_to_play">Shake To Play</string>
<string name="settings_shake_to_play_summary">Shake your device to play next song</string>
<!-- App widget -->
<string name="app_widget_small">Music: 4 \u00d7 1</string>
......
......@@ -51,6 +51,13 @@
android:key="show_lyrics"
android:title="@string/settings_show_lyrics_title"
android:summary="@string/settings_show_lyrics_summary"/>
<!-- Shake to switch songs -->
<CheckBoxPreference
android:defaultValue="false"
android:key="shake_to_play"
android:title="@string/settings_shake_to_play"
android:summary="@string/settings_shake_to_play_summary"/>
</PreferenceCategory>
<!-- Storage catetory -->
<PreferenceCategory android:title="@string/settings_storage_category" >
......
......@@ -48,5 +48,6 @@ interface IElevenService
int getRepeatMode();
int getMediaMountedCount();
int getAudioSessionId();
void setShakeToPlayEnabled(boolean enabled);
}
......@@ -31,6 +31,7 @@ import android.database.ContentObserver;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.graphics.Bitmap;
import android.hardware.SensorManager;
import android.media.AudioManager;
import android.media.AudioManager.OnAudioFocusChangeListener;
import android.media.MediaMetadata;
......@@ -66,6 +67,8 @@ import com.cyanogenmod.eleven.provider.SongPlayCount;
import com.cyanogenmod.eleven.service.MusicPlaybackTrack;
import com.cyanogenmod.eleven.utils.BitmapWithColors;
import com.cyanogenmod.eleven.utils.Lists;
import com.cyanogenmod.eleven.utils.PreferenceUtils;
import com.cyanogenmod.eleven.utils.ShakeDetector;
import com.cyanogenmod.eleven.utils.SrtManager;
import java.io.File;
......@@ -516,6 +519,25 @@ public class MusicPlaybackService extends Service {
*/
private MusicPlaybackState mPlaybackStateStore;
/**
* Shake detector class used for shake to switch song feature
*/
private ShakeDetector mShakeDetector;
private ShakeDetector.Listener mShakeDetectorListener=new ShakeDetector.Listener() {
@Override
public void hearShake() {
/*
* on shake detect, play next song
*/
if (D) {
Log.d(TAG,"Shake detected!!!");
}
gotoNext(true);
}
};
/**
* {@inheritDoc}
*/
......@@ -551,6 +573,7 @@ public class MusicPlaybackService extends Service {
return true;
}
stopSelf(mServiceStartId);
return true;
}
......@@ -738,6 +761,9 @@ public class MusicPlaybackService extends Service {
mUnmountReceiver = null;
}
// deinitialize shake detector
stopShakeDetector(true);
// Release the wake lock
mWakeLock.release();
}
......@@ -2337,6 +2363,7 @@ public class MusicPlaybackService extends Service {
* Stops playback.
*/
public void stop() {
stopShakeDetector(false);
stop(true);
}
......@@ -2344,6 +2371,7 @@ public class MusicPlaybackService extends Service {
* Resumes or starts playback.
*/
public void play() {
startShakeDetector();
play(true);
}
......@@ -2402,6 +2430,7 @@ public class MusicPlaybackService extends Service {
if (mIsSupposedToBePlaying) {
mPlayer.pause();
setIsSupposedToBePlaying(false, true);
stopShakeDetector(false);
}
}
}
......@@ -2718,6 +2747,51 @@ public class MusicPlaybackService extends Service {
notifyChange(PLAYLIST_CHANGED);
}
/**
* Called to set the status of shake to play feature
*/
public void setShakeToPlayEnabled(boolean enabled) {
if (D) {
Log.d(TAG, "ShakeToPlay status: " + enabled);
}
if (enabled) {
if (mShakeDetector == null) {
mShakeDetector = new ShakeDetector(mShakeDetectorListener);
}
// if song is already playing, start listening immediately
if (isPlaying()) {
startShakeDetector();
}
}
else {
stopShakeDetector(true);
}
}
/**
* Called to start listening to shakes
*/
private void startShakeDetector() {
if (mShakeDetector != null) {
mShakeDetector.start((SensorManager)getSystemService(SENSOR_SERVICE));
}
}
/**
* Called to stop listening to shakes
*/
private void stopShakeDetector(final boolean destroyShakeDetector) {
if (mShakeDetector != null) {
mShakeDetector.stop();
}
if(destroyShakeDetector){
mShakeDetector = null;
if (D) {
Log.d(TAG, "ShakeToPlay destroyed!!!");
}
}
}
private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
/**
* {@inheritDoc}
......@@ -3625,6 +3699,14 @@ public class MusicPlaybackService extends Service {
return mService.get().getAudioSessionId();
}
/**
* {@inheritDoc}
*/
@Override
public void setShakeToPlayEnabled(boolean enabled) {
mService.get().setShakeToPlayEnabled(enabled);
}
}
}
......@@ -16,14 +16,19 @@ package com.cyanogenmod.eleven.ui.activities;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.os.Bundle;
import android.preference.Preference;
import android.preference.Preference.OnPreferenceClickListener;
import android.preference.PreferenceActivity;
import android.preference.PreferenceManager;
import android.view.MenuItem;
import com.cyanogenmod.eleven.R;
import com.cyanogenmod.eleven.cache.ImageFetcher;
import com.cyanogenmod.eleven.utils.MusicUtils;
import com.cyanogenmod.eleven.utils.PreferenceUtils;
/**
* Settings.
......@@ -31,7 +36,7 @@ import com.cyanogenmod.eleven.cache.ImageFetcher;
* @author Andrew Neal (andrewdneal@gmail.com)
*/
@SuppressWarnings("deprecation")
public class SettingsActivity extends PreferenceActivity {
public class SettingsActivity extends PreferenceActivity implements OnSharedPreferenceChangeListener{
/**
* {@inheritDoc}
......@@ -50,6 +55,8 @@ public class SettingsActivity extends PreferenceActivity {
// Removes the cache entries
deleteCache();
PreferenceUtils.getInstance(this).setOnSharedPreferenceChangeListener(this);
}
/**
......@@ -94,4 +101,12 @@ public class SettingsActivity extends PreferenceActivity {
}
});
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
String key) {
if(key.equals(PreferenceUtils.SHAKE_TO_PLAY)){
MusicUtils.setShakeToPlayEnabled(sharedPreferences.getBoolean(key, false));
}
}
}
......@@ -87,6 +87,8 @@ public final class MusicUtils {
public static final String MUSIC_ONLY_SELECTION = MediaStore.Audio.AudioColumns.IS_MUSIC + "=1"
+ " AND " + MediaStore.Audio.AudioColumns.TITLE + " != ''"; //$NON-NLS-2$
private static boolean sShakeToPlayEnabled;
static {
mConnectionMap = new WeakHashMap<Context, ServiceBinder>();
sEmptyList = new long[0];
......@@ -107,6 +109,7 @@ public final class MusicUtils {
if (realActivity == null) {
realActivity = (Activity)context;
}
sShakeToPlayEnabled = PreferenceUtils.getInstance(context).getShakeToPlay();
final ContextWrapper contextWrapper = new ContextWrapper(realActivity);
contextWrapper.startService(new Intent(contextWrapper, MusicPlaybackService.class));
final ServiceBinder binder = new ServiceBinder(callback);
......@@ -154,6 +157,7 @@ public final class MusicUtils {
if (mCallback != null) {
mCallback.onServiceConnected(className, service);
}
MusicUtils.setShakeToPlayEnabled(sShakeToPlayEnabled);
}
@Override
......@@ -270,6 +274,18 @@ public final class MusicUtils {
}
}
/**
* Set shake to play status
*/
public static void setShakeToPlayEnabled(boolean enabled) {
try {
if (mService != null) {
mService.setShakeToPlayEnabled(enabled);
}
} catch (final RemoteException ignored) {
}
}
/**
* Changes to the next track asynchronously
*/
......
......@@ -16,6 +16,7 @@ package com.cyanogenmod.eleven.utils;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.preference.PreferenceManager;
import com.cyanogenmod.eleven.R;
......@@ -77,6 +78,9 @@ public final class PreferenceUtils {
// show visualizer flag
public static final String SHOW_VISUALIZER = "music_visualization";
// shake to play flag
public static final String SHAKE_TO_PLAY = "shake_to_play";
private static PreferenceUtils sInstance;
private final SharedPreferences mPreferences;
......@@ -119,6 +123,14 @@ public final class PreferenceUtils {
}
}, (Void[])null);
}
/**
* Set the listener for preference change
* @param listener
*/
public void setOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener){
mPreferences.registerOnSharedPreferenceChangeListener(listener);
}
/**
* Returns the last page the user was on when the app was exited.
......@@ -322,4 +334,8 @@ public final class PreferenceUtils {
public boolean getShowVisualizer() {
return mPreferences.getBoolean(SHOW_VISUALIZER, true);
}
}
public boolean getShakeToPlay() {
return mPreferences.getBoolean(SHAKE_TO_PLAY, false);
}
}
\ No newline at end of file
package com.cyanogenmod.eleven.utils;
/*
* Copyright 2012 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import java.util.ArrayList;
import java.util.List;
/**
* Detects phone shaking. If > 75% of the samples taken in the past 0.5s are accelerating, the
* device is a) shaking, or b) free falling 1.84m (h = 1/2*g*t^2*3/4).
*
* @author Bob Lee (bob@squareup.com)
* @author Eric Burke (eric@squareup.com)
*/
public class ShakeDetector implements SensorEventListener {
/**
* When the magnitude of total acceleration exceeds this value, the phone is accelerating.
*/
private static final int ACCELERATION_THRESHOLD = 13;
/**
* Minimum time between two consecutive shakes in milliseconds to invoke listener
*/
private static final int MIN_TIME_BETWEEN_TWO_SHAKES = 1000;
private long mDetectedShakeStartTime = 0;
/** Listens for shakes. */
public interface Listener {
/** Called on the main thread when the device is shaken. */
void hearShake();
}
private final SampleQueue queue = new SampleQueue();
private final Listener listener;
private SensorManager sensorManager;
private Sensor accelerometer;
public ShakeDetector(Listener listener) {
this.listener = listener;
}
/**
* Starts listening for shakes on devices with appropriate hardware.
*
* @returns true if the device supports shake detection.
*/
public boolean start(SensorManager sensorManager) {
// Already started?
if (accelerometer != null) {
return true;
}
accelerometer = sensorManager
.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
// If this phone has an accelerometer, listen to it.
if (accelerometer != null) {
this.sensorManager = sensorManager;
sensorManager.registerListener(this, accelerometer,
SensorManager.SENSOR_DELAY_FASTEST);
}
return accelerometer != null;
}
/**
* Stops listening. Safe to call when already stopped. Ignored on devices without appropriate
* hardware.
*/
public void stop() {
if (accelerometer != null) {
sensorManager.unregisterListener(this, accelerometer);
sensorManager = null;
accelerometer = null;
}
}
@Override
public void onSensorChanged(SensorEvent event) {
boolean accelerating = isAccelerating(event);
long timestamp = event.timestamp;
queue.add(timestamp, accelerating);
if (queue.isShaking()) {
/*
* detect time between two concecutive shakes and limit it to
* MIN_TIME_BETWEEN_TWO_SHAKES
*/
long currentTime = System.currentTimeMillis();
if (currentTime - mDetectedShakeStartTime > MIN_TIME_BETWEEN_TWO_SHAKES) {
queue.clear();
listener.hearShake();
mDetectedShakeStartTime = System.currentTimeMillis();
}
}
}
/** Returns true if the device is currently accelerating. */
private boolean isAccelerating(SensorEvent event) {
float ax = event.values[0];
float ay = event.values[1];
float az = event.values[2];
// Instead of comparing magnitude to ACCELERATION_THRESHOLD,
// compare their squares. This is equivalent and doesn't need the
// actual magnitude, which would be computed using (expesive)
// Math.sqrt().
final double magnitudeSquared = ax * ax + ay * ay + az * az;
return magnitudeSquared > ACCELERATION_THRESHOLD
* ACCELERATION_THRESHOLD;
}
/** Queue of samples. Keeps a running average. */
static class SampleQueue {
/** Window size in ns. Used to compute the average. */
private static final long MAX_WINDOW_SIZE = 500000000; // 0.5s
private static final long MIN_WINDOW_SIZE = MAX_WINDOW_SIZE >> 1; // 0.25s
/**
* Ensure the queue size never falls below this size, even if the device fails to deliver
* this many events during the time window. The LG Ally is one such device.
*/
private static final int MIN_QUEUE_SIZE = 4;
private final SamplePool pool = new SamplePool();
private Sample oldest;
private Sample newest;
private int sampleCount;
private int acceleratingCount;
/**
* Adds a sample.
*
* @param timestamp in nanoseconds of sample
* @param accelerating true if > {@link #ACCELERATION_THRESHOLD}.
*/
void add(long timestamp, boolean accelerating) {
// Purge samples that proceed window.
purge(timestamp - MAX_WINDOW_SIZE);
// Add the sample to the queue.
Sample added = pool.acquire();
added.timestamp = timestamp;
added.accelerating = accelerating;
added.next = null;
if (newest != null) {
newest.next = added;
}
newest = added;
if (oldest == null) {
oldest = added;
}
// Update running average.
sampleCount++;
if (accelerating) {
acceleratingCount++;
}
}
/** Removes all samples from this queue. */
void clear() {
while (oldest != null) {
Sample removed = oldest;
oldest = removed.next;
pool.release(removed);
}
newest = null;
sampleCount = 0;
acceleratingCount = 0;
}
/** Purges samples with timestamps older than cutoff. */
void purge(long cutoff) {
while (sampleCount >= MIN_QUEUE_SIZE && oldest != null
&& cutoff - oldest.timestamp > 0) {
// Remove sample.
Sample removed = oldest;
if (removed.accelerating) {
acceleratingCount--;
}
sampleCount--;
oldest = removed.next;
if (oldest == null) {
newest = null;
}
pool.release(removed);
}
}
/** Copies the samples into a list, with the oldest entry at index 0. */
List<Sample> asList() {
List<Sample> list = new ArrayList<Sample>();
Sample s = oldest;
while (s != null) {
list.add(s);
s = s.next;
}
return list;
}
/**
* Returns true if we have enough samples and more than 3/4 of those samples are
* accelerating.
*/
boolean isShaking() {
return newest != null
&& oldest != null
&& newest.timestamp - oldest.timestamp >= MIN_WINDOW_SIZE
&& acceleratingCount >= (sampleCount >> 1)
+ (sampleCount >> 2);
}
}
/** An accelerometer sample. */
static class Sample {
/** Time sample was taken. */
long timestamp;
/** If acceleration > {@link #ACCELERATION_THRESHOLD}. */
boolean accelerating;
/** Next sample in the queue or pool. */
Sample next;
}
/** Pools samples. Avoids garbage collection. */
static class SamplePool {
private Sample head;
/** Acquires a sample from the pool. */
Sample acquire() {
Sample acquired = head;
if (acquired == null) {
acquired = new Sample();
} else {
// Remove instance from pool.
head = acquired.next;
}
return acquired;
}
/** Returns a sample to the pool. */
void release(Sample sample) {
sample.next = head;
head = sample;
}
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment