diff options
author | Mikalacki Sava <mikalackis@gmail.com> | 2015-01-13 17:37:46 +0100 |
---|---|---|
committer | Mikalacki Sava <mikalackis@gmail.com> | 2015-02-23 17:35:42 +0100 |
commit | 758ec43f27e07c329578e606db079dd56e9d4c2e (patch) | |
tree | 957456570b434b81ad99ae17704d684cc472fafe | |
parent | fd06a775bb1668dc0df4de96f3c84ad1e9b5a459 (diff) | |
download | android_packages_apps_Eleven-stable/cm-12.0-YNG1I.tar.gz android_packages_apps_Eleven-stable/cm-12.0-YNG1I.tar.bz2 android_packages_apps_Eleven-stable/cm-12.0-YNG1I.zip |
Eleven: shake to play next song, only available while music is playing.stable/cm-12.0-YNG1I
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
-rw-r--r-- | AndroidManifest.xml | 3 | ||||
-rw-r--r-- | res/values/strings.xml | 2 | ||||
-rw-r--r-- | res/xml/settings.xml | 7 | ||||
-rw-r--r-- | src/com/cyanogenmod/eleven/IElevenService.aidl | 1 | ||||
-rw-r--r-- | src/com/cyanogenmod/eleven/MusicPlaybackService.java | 82 | ||||
-rw-r--r-- | src/com/cyanogenmod/eleven/ui/activities/SettingsActivity.java | 17 | ||||
-rw-r--r-- | src/com/cyanogenmod/eleven/utils/MusicUtils.java | 16 | ||||
-rw-r--r-- | src/com/cyanogenmod/eleven/utils/PreferenceUtils.java | 18 | ||||
-rw-r--r-- | src/com/cyanogenmod/eleven/utils/ShakeDetector.java | 276 |
9 files changed, 420 insertions, 2 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 25381a3..ce19488 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -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" diff --git a/res/values/strings.xml b/res/values/strings.xml index 5bf25b1..1c0c540 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -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> diff --git a/res/xml/settings.xml b/res/xml/settings.xml index 05860ea..33151a4 100644 --- a/res/xml/settings.xml +++ b/res/xml/settings.xml @@ -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" > diff --git a/src/com/cyanogenmod/eleven/IElevenService.aidl b/src/com/cyanogenmod/eleven/IElevenService.aidl index 82a7a97..21ac3ae 100644 --- a/src/com/cyanogenmod/eleven/IElevenService.aidl +++ b/src/com/cyanogenmod/eleven/IElevenService.aidl @@ -48,5 +48,6 @@ interface IElevenService int getRepeatMode(); int getMediaMountedCount(); int getAudioSessionId(); + void setShakeToPlayEnabled(boolean enabled); } diff --git a/src/com/cyanogenmod/eleven/MusicPlaybackService.java b/src/com/cyanogenmod/eleven/MusicPlaybackService.java index 73c3694..6703dbf 100644 --- a/src/com/cyanogenmod/eleven/MusicPlaybackService.java +++ b/src/com/cyanogenmod/eleven/MusicPlaybackService.java @@ -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; @@ -517,6 +520,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} */ @Override @@ -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); + } + } } diff --git a/src/com/cyanogenmod/eleven/ui/activities/SettingsActivity.java b/src/com/cyanogenmod/eleven/ui/activities/SettingsActivity.java index c4f8139..14209a3 100644 --- a/src/com/cyanogenmod/eleven/ui/activities/SettingsActivity.java +++ b/src/com/cyanogenmod/eleven/ui/activities/SettingsActivity.java @@ -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)); + } + } } diff --git a/src/com/cyanogenmod/eleven/utils/MusicUtils.java b/src/com/cyanogenmod/eleven/utils/MusicUtils.java index 441413a..252f869 100644 --- a/src/com/cyanogenmod/eleven/utils/MusicUtils.java +++ b/src/com/cyanogenmod/eleven/utils/MusicUtils.java @@ -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 @@ -271,6 +275,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 */ public static void asyncNext(final Context context) { diff --git a/src/com/cyanogenmod/eleven/utils/PreferenceUtils.java b/src/com/cyanogenmod/eleven/utils/PreferenceUtils.java index 27823c3..e03cf2d 100644 --- a/src/com/cyanogenmod/eleven/utils/PreferenceUtils.java +++ b/src/com/cyanogenmod/eleven/utils/PreferenceUtils.java @@ -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 diff --git a/src/com/cyanogenmod/eleven/utils/ShakeDetector.java b/src/com/cyanogenmod/eleven/utils/ShakeDetector.java new file mode 100644 index 0000000..d0be70d --- /dev/null +++ b/src/com/cyanogenmod/eleven/utils/ShakeDetector.java @@ -0,0 +1,276 @@ + +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) { + } +} |