OSDN Git Service

Eleven: shake to play next song, only available while music is playing.
authorMikalacki Sava <mikalackis@gmail.com>
Tue, 13 Jan 2015 16:37:46 +0000 (17:37 +0100)
committerMikalacki Sava <mikalackis@gmail.com>
Mon, 23 Feb 2015 16:35:42 +0000 (17:35 +0100)
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

AndroidManifest.xml
res/values/strings.xml
res/xml/settings.xml
src/com/cyanogenmod/eleven/IElevenService.aidl
src/com/cyanogenmod/eleven/MusicPlaybackService.java
src/com/cyanogenmod/eleven/ui/activities/SettingsActivity.java
src/com/cyanogenmod/eleven/utils/MusicUtils.java
src/com/cyanogenmod/eleven/utils/PreferenceUtils.java
src/com/cyanogenmod/eleven/utils/ShakeDetector.java [new file with mode: 0644]

index 25381a3..ce19488 100644 (file)
@@ -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"
index 5bf25b1..1c0c540 100644 (file)
     <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>
index 05860ea..33151a4 100644 (file)
             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" >
index 82a7a97..21ac3ae 100644 (file)
@@ -48,5 +48,6 @@ interface IElevenService
     int getRepeatMode();
     int getMediaMountedCount();
     int getAudioSessionId();
+    void setShakeToPlayEnabled(boolean enabled);
 }
 
index 73c3694..6703dbf 100644 (file)
@@ -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);
+        }
+
     }
 
 }
index c4f8139..14209a3 100644 (file)
@@ -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));
+        }
+    }
 }
index 441413a..252f869 100644 (file)
@@ -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) {
index 27823c3..e03cf2d 100644 (file)
@@ -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 (file)
index 0000000..d0be70d
--- /dev/null
@@ -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) {
+    }
+}