diff --git a/res/values/strings.xml b/res/values/strings.xml
index ebcc236c..a33db2aa 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -76,6 +76,15 @@
%s by %s
+
+ - 1 minute
+ - %d minutes
+
+
+ - 1 hour
+ - %d hours
+
+
Audio Output
Volume
Music volume
@@ -101,6 +110,10 @@
What to do when an item is tapped
Miscellaneous Features
+ Enable Idle Timeout
+ When active, playback will be stopped after the given period of inactivity
+ Idle Timeout
+ The amount of time that must pass before becoming idle
Use ScrobbleDroid API
Scrobble to Last.FM through ScrobbleDroid or Simple Last.FM Scrobbler
\ No newline at end of file
diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml
index 294381bc..ca28549b 100644
--- a/res/xml/preferences.xml
+++ b/res/xml/preferences.xml
@@ -74,6 +74,16 @@
android:defaultValue="0" />
+
+
+ *
+ * This file is part of Vanilla Music Player.
+ *
+ * Vanilla Music Player is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Library General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your
+ * option) any later version.
+ *
+ * Vanilla Music Player is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.kreed.vanilla;
+
+import android.app.AlertDialog.Builder;
+import android.content.Context;
+import android.content.res.Resources;
+import android.preference.DialogPreference;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.SeekBar;
+import android.widget.TextView;
+
+/**
+ * A preference that provides a dialog with a slider for idle time.
+ *
+ * The slider produces a value in seconds from 60 (1 minute) to 21600
+ * (6 hours). The values range on an approximately exponential scale.
+ */
+public class IdlePreference extends DialogPreference implements SeekBar.OnSeekBarChangeListener {
+ private static int MIN = 60;
+ private static int MAX = 21600;
+
+ /**
+ * The current idle timeout displayed on the slider. Will not be persisted
+ * until the dialog is closed.
+ */
+ private int mValue;
+ /**
+ * The view in which the value is displayed.
+ */
+ private TextView mValueText;
+
+ public IdlePreference(Context context, AttributeSet attrs)
+ {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onPrepareDialogBuilder(Builder builder)
+ {
+ Context context = getContext();
+ ViewGroup.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+
+ mValue = getPersistedInt(3600);
+
+ LinearLayout layout = new LinearLayout(context);
+ layout.setOrientation(LinearLayout.VERTICAL);
+ layout.setLayoutParams(params);
+
+ mValueText = new TextView(context);
+ mValueText.setGravity(Gravity.RIGHT);
+ mValueText.setPadding(20, 0, 20, 0);
+ layout.addView(mValueText);
+
+ SeekBar seekBar = new SeekBar(context);
+ seekBar.setPadding(20, 0, 20, 20);
+ seekBar.setLayoutParams(params);
+ seekBar.setMax(1000);
+ seekBar.setProgress((int)(Math.pow((float)(mValue - MIN) / (MAX - MIN), 0.25f) * 1000));
+ seekBar.setOnSeekBarChangeListener(this);
+ layout.addView(seekBar);
+
+ updateText();
+
+ builder.setView(layout);
+ }
+
+ /**
+ * Update the text view with the current value.
+ */
+ private void updateText()
+ {
+ Resources res = getContext().getResources();
+ int value = mValue;
+ String text;
+ if (value >= 3570) {
+ int hours = (int)Math.round(value / 3600f);
+ text = res.getQuantityString(R.plurals.hours, hours, hours);
+ } else {
+ int minutes = (int)Math.round(value / 60f);
+ text = res.getQuantityString(R.plurals.minutes, minutes, minutes);
+ }
+ mValueText.setText(text);
+ }
+
+ @Override
+ protected void onDialogClosed(boolean positiveResult)
+ {
+ if (positiveResult && shouldPersist())
+ persistInt(mValue);
+ }
+
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser)
+ {
+ // Approximate an exponential curve with x^4. Produces a value from 60-10860.
+ if (fromUser) {
+ float value = seekBar.getProgress() / 1000.0f;
+ value *= value;
+ value *= value;
+ mValue = (int)(value * (MAX - MIN)) + MIN;
+ updateText();
+ }
+ }
+
+ public void onStartTrackingTouch(SeekBar seekBar)
+ {
+ }
+
+ public void onStopTrackingTouch(SeekBar seekBar)
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/org/kreed/vanilla/PlaybackActivity.java b/src/org/kreed/vanilla/PlaybackActivity.java
index 60b98044..e87f3260 100644
--- a/src/org/kreed/vanilla/PlaybackActivity.java
+++ b/src/org/kreed/vanilla/PlaybackActivity.java
@@ -71,6 +71,14 @@ public class PlaybackActivity extends Activity implements Handler.Callback, View
startService(new Intent(this, PlaybackService.class));
}
+ @Override
+ public void onResume()
+ {
+ super.onResume();
+ if (ContextApplication.hasService())
+ ContextApplication.getService().userActionTriggered();
+ }
+
public static boolean handleKeyLongPress(int keyCode)
{
switch (keyCode) {
diff --git a/src/org/kreed/vanilla/PlaybackService.java b/src/org/kreed/vanilla/PlaybackService.java
index b92f37c8..fe7422f4 100644
--- a/src/org/kreed/vanilla/PlaybackService.java
+++ b/src/org/kreed/vanilla/PlaybackService.java
@@ -104,6 +104,10 @@ public final class PlaybackService extends Service implements Handler.Callback,
public static final int FLAG_SHUFFLE = 0x4;
public static final int FLAG_REPEAT = 0x8;
public static final int ALL_FLAGS = FLAG_NO_MEDIA + FLAG_PLAYING + FLAG_SHUFFLE + FLAG_REPEAT;
+ /**
+ * The flags that are (usually) only toggled by user action.
+ */
+ public static final int USER_MASK = FLAG_PLAYING + FLAG_SHUFFLE + FLAG_REPEAT;
public static final int NEVER = 0;
public static final int WHEN_PLAYING = 1;
@@ -114,6 +118,10 @@ public final class PlaybackService extends Service implements Handler.Callback,
private boolean mScrobble;
private int mNotificationMode;
private byte mHeadsetControls = -1;
+ /**
+ * The time to wait before considering the player idle.
+ */
+ private int mIdleTimeout;
private Looper mLooper;
private Handler mHandler;
@@ -138,6 +146,15 @@ public final class PlaybackService extends Service implements Handler.Callback,
private boolean mIgnoreNextUp;
private boolean mLoaded;
boolean mInCall;
+ /**
+ * The volume set by the user in the preferences.
+ */
+ private float mUserVolume = 1.0f;
+ /**
+ * The actual volume of the media player. Will differ from the user volume
+ * when fading the volume.
+ */
+ private float mCurrentVolume = 1.0f;
private Method mIsWiredHeadsetOn;
private Method mStartForeground;
@@ -325,8 +342,11 @@ public final class PlaybackService extends Service implements Handler.Callback,
mNotificationMode = Integer.parseInt(mSettings.getString("notification_mode", "1"));
mScrobble = mSettings.getBoolean("scrobble", false);
float volume = mSettings.getFloat("volume", 1.0f);
- if (volume != 1.0f)
+ if (volume != 1.0f) {
+ mCurrentVolume = mUserVolume = volume;
mMediaPlayer.setVolume(volume, volume);
+ }
+ mIdleTimeout = mSettings.getBoolean("use_idle_timeout", false) ? mSettings.getInt("idle_timeout", 3600) : 0;
PowerManager powerManager = (PowerManager)getSystemService(POWER_SERVICE);
mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "VanillaMusicSongChangeLock");
@@ -359,6 +379,7 @@ public final class PlaybackService extends Service implements Handler.Callback,
mScrobble = mSettings.getBoolean("scrobble", false);
} else if ("volume".equals(key)) {
float volume = mSettings.getFloat("volume", 1.0f);
+ mCurrentVolume = mUserVolume = volume;
if (mMediaPlayer != null) {
synchronized (mMediaPlayer) {
mMediaPlayer.setVolume(volume, volume);
@@ -367,6 +388,9 @@ public final class PlaybackService extends Service implements Handler.Callback,
} else if ("media_button".equals(key)) {
mHeadsetControls = (byte)(mSettings.getBoolean("media_button", true) ? 1 : 0);
setupReceiver();
+ } else if ("use_idle_timeout".equals(key) || "idle_timeout".equals(key)) {
+ mIdleTimeout = mSettings.getBoolean("use_idle_timeout", false) ? mSettings.getInt("idle_timeout", 3600) : 0;
+ userActionTriggered();
}
}
@@ -378,21 +402,21 @@ public final class PlaybackService extends Service implements Handler.Callback,
ContextApplication.broadcast(intent);
}
- boolean setFlag(int flag)
+ void setFlag(int flag)
{
synchronized (mStateLock) {
- return updateState(mState | flag);
+ updateState(mState | flag);
}
}
- boolean unsetFlag(int flag)
+ void unsetFlag(int flag)
{
synchronized (mStateLock) {
- return updateState(mState & ~flag);
+ updateState(mState & ~flag);
}
}
- private boolean updateState(int state)
+ private void updateState(int state)
{
state &= ALL_FLAGS;
@@ -401,7 +425,7 @@ public final class PlaybackService extends Service implements Handler.Callback,
Song song = getSong(0);
if (song == null && (state & FLAG_PLAYING) != 0)
- return false;
+ return;
int oldState = mState;
mState = state;
@@ -454,11 +478,10 @@ public final class PlaybackService extends Service implements Handler.Callback,
mMediaPlayer.pause();
}
}
- } else {
- return false;
}
- return true;
+ if ((oldState & USER_MASK) != (state & USER_MASK))
+ userActionTriggered();
}
private void updateNotification(Song song)
@@ -544,6 +567,9 @@ public final class PlaybackService extends Service implements Handler.Callback,
Log.e("VanillaMusic", "IOException", e);
}
+ if (delta != 0)
+ userActionTriggered();
+
mHandler.sendEmptyMessage(PROCESS_SONG);
}
@@ -686,8 +712,12 @@ public final class PlaybackService extends Service implements Handler.Callback,
case TelephonyManager.CALL_STATE_RINGING:
case TelephonyManager.CALL_STATE_OFFHOOK:
mInCall = true;
- if (!mPlayingBeforeCall)
- mPlayingBeforeCall = unsetFlag(FLAG_PLAYING);
+ if (!mPlayingBeforeCall) {
+ synchronized (mStateLock) {
+ if (mPlayingBeforeCall = (mState & FLAG_PLAYING) != 0)
+ unsetFlag(FLAG_PLAYING);
+ }
+ }
break;
case TelephonyManager.CALL_STATE_IDLE:
mInCall = false;
@@ -741,8 +771,21 @@ public final class PlaybackService extends Service implements Handler.Callback,
private static final int POST_CREATE = 1;
private static final int MEDIA_BUTTON = 2;
private static final int CREATE = 3;
+ /**
+ * This message is sent with a delay specified by a user preference. After
+ * this delay, assuming no new IDLE_TIMEOUT messages cancel it, playback
+ * will be stopped.
+ */
+ private static final int IDLE_TIMEOUT = 4;
private static final int TRACK_CHANGED = 5;
private static final int RELEASE_WAKE_LOCK = 6;
+ /**
+ * Decrease the volume gradually over five seconds, pausing when 0 is
+ * reached.
+ *
+ * arg1 should be the progress in the fade as a percentage, 1-100.
+ */
+ private static final int FADE_OUT = 7;
private static final int SAVE_STATE = 12;
private static final int PROCESS_SONG = 13;
@@ -793,6 +836,31 @@ public final class PlaybackService extends Service implements Handler.Callback,
TelephonyManager telephonyManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
telephonyManager.listen(mCallListener, PhoneStateListener.LISTEN_CALL_STATE);
break;
+ case IDLE_TIMEOUT:
+ if ((mState & FLAG_PLAYING) != 0)
+ mHandler.sendMessage(mHandler.obtainMessage(FADE_OUT, 100, 0));
+ break;
+ case FADE_OUT:
+ int progress = message.arg1 - 1;
+ if (progress == 0) {
+ unsetFlag(FLAG_PLAYING);
+ mCurrentVolume = mUserVolume;
+ } else {
+ // Fade out on a x^4 curve. This produces a smoother
+ // transition, since we are using raw sound intensities which
+ // are heard by humans with a logarithmic scale. Don't fall
+ // below .01 though: past this, hearing this music becomes
+ // difficult or impossible.
+ mCurrentVolume = Math.max((float)(Math.pow(progress / 100f, 4) * mUserVolume), .01f);
+
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(FADE_OUT, progress, 0), 50);
+ }
+ if (mMediaPlayer != null) {
+ synchronized (mMediaPlayer) {
+ mMediaPlayer.setVolume(mCurrentVolume, mCurrentVolume);
+ }
+ }
+ break;
default:
return false;
}
@@ -887,4 +955,24 @@ public final class PlaybackService extends Service implements Handler.Callback,
if (shouldAdvance)
setCurrentSong(0);
}
+
+ /**
+ * Resets the idle timeout countdown. Should be called by a user action
+ * has been trigger (new song chosen or playback toggled).
+ *
+ * If an idle fade out is actually in progress, aborts it and resets the
+ * volume.
+ */
+ public void userActionTriggered()
+ {
+ mHandler.removeMessages(FADE_OUT);
+ mHandler.removeMessages(IDLE_TIMEOUT);
+ if (mIdleTimeout != 0)
+ mHandler.sendEmptyMessageDelayed(IDLE_TIMEOUT, mIdleTimeout * 1000);
+
+ if (mCurrentVolume != mUserVolume) {
+ mCurrentVolume = mUserVolume;
+ mMediaPlayer.setVolume(mCurrentVolume, mCurrentVolume);
+ }
+ }
}