Allow playback to be halted after a set idle timeout

This commit is contained in:
Christopher Eby 2010-05-22 23:43:41 -05:00
parent 6ea8b257c5
commit 1527a56b31
5 changed files with 262 additions and 12 deletions

View File

@ -76,6 +76,15 @@
<string name="title_by_artist">%s by %s</string>
<!-- Preferences -->
<plurals name="minutes">
<item quantity="one">1 minute</item>
<item quantity="other">%d minutes</item>
</plurals>
<plurals name="hours">
<item quantity="one">1 hour</item>
<item quantity="other">%d hours</item>
</plurals>
<string name="pref_output">Audio Output</string>
<string name="volume_title">Volume</string>
<string name="volume_summary">Music volume</string>
@ -101,6 +110,10 @@
<string name="default_action_summary">What to do when an item is tapped</string>
<string name="pref_misc">Miscellaneous Features</string>
<string name="use_idle_timeout_title">Enable Idle Timeout</string>
<string name="use_idle_timeout_summary">When active, playback will be stopped after the given period of inactivity</string>
<string name="idle_timeout_title">Idle Timeout</string>
<string name="idle_timeout_summary">The amount of time that must pass before becoming idle</string>
<string name="scrobble_title">Use ScrobbleDroid API</string>
<string name="scrobble_summary">Scrobble to Last.FM through ScrobbleDroid or Simple Last.FM Scrobbler</string>
</resources>

View File

@ -74,6 +74,16 @@
android:defaultValue="0" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/pref_misc">
<CheckBoxPreference
android:key="use_idle_timeout"
android:title="@string/use_idle_timeout_title"
android:summary="@string/use_idle_timeout_summary"
android:defaultValue="false" />
<org.kreed.vanilla.IdlePreference
android:key="idle_timeout"
android:title="@string/idle_timeout_title"
android:summary="@string/idle_timeout_summary"
android:dependency="use_idle_timeout" />
<CheckBoxPreference
android:key="scrobble"
android:title="@string/scrobble_title"

View File

@ -0,0 +1,131 @@
/*
* Copyright (C) 2010 Christopher Eby <kreed@kreed.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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)
{
}
}

View File

@ -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) {

View File

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