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