Import version 1.0

This commit is contained in:
Christopher Eby 2010-02-15 09:51:10 -06:00
commit 5e4244a95d
24 changed files with 1647 additions and 0 deletions

7
.classpath Normal file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" path="src"/>
<classpathentry kind="src" path="gen"/>
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
<classpathentry kind="output" path="bin"/>
</classpath>

33
.project Normal file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>Tumult</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.ApkBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>com.android.ide.eclipse.adt.AndroidNature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
</projectDescription>

@ -0,0 +1,5 @@
#Thu Dec 24 11:30:20 CST 2009
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.5
org.eclipse.jdt.core.compiler.compliance=1.5
org.eclipse.jdt.core.compiler.source=1.5

24
AndroidManifest.xml Normal file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.kreed.tumult"
android:versionCode="1" android:versionName="1.0">
<application
android:icon="@drawable/icon"
android:label="@string/app_name"
android:name="Tumult">
<activity
android:name="NowPlayingActivity"
android:label="@string/app_name" android:theme="@style/Theme.NoBackground">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service android:name="PlaybackService" />
<activity android:name="PreferencesActivity" />
<activity android:name="SongSelector"/>
</application>
<uses-sdk android:minSdkVersion="3"/>
</manifest>

11
default.properties Normal file

@ -0,0 +1,11 @@
# This file is automatically generated by Android Tools.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must be checked in Version Control Systems.
#
# To customize properties used by the Ant build system use,
# "build.properties", and override values to adapt the script to your
# project structure.
# Project target.
target=android-3

BIN
res/drawable/icon.png Normal file

Binary file not shown.

After

(image error) Size: 3.4 KiB

Binary file not shown.

After

(image error) Size: 1.5 KiB

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
android:id="@+id/selector_layout"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical">
<ListView
android:id="@+id/song_list"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1" />
<LinearLayout
android:id="@+id/filter_layout"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:orientation="horizontal"
android:visibility="gone">
<TextView
android:id="@+id/filter_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="5px"
android:layout_weight="1" />
<Button
android:id="@+id/backspace"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="&lt;" />
<Button
android:id="@+id/close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="x" />
</LinearLayout>
<TableLayout
android:id="@+id/numpad"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:stretchColumns="0,1,2"
android:visibility="gone">
<TableRow>
<Button android:id="@+id/Button1" android:text="0 1" />
<Button android:id="@+id/Button2" android:text="2 abc" />
<Button android:id="@+id/Button3" android:text="3 def" />
</TableRow>
<TableRow>
<Button android:id="@+id/Button4" android:text="4 ghi" />
<Button android:id="@+id/Button5" android:text="5 jkl" />
<Button android:id="@+id/Button6" android:text="6 mno" />
</TableRow>
<TableRow>
<Button android:id="@+id/Button7" android:text="7 pqrs" />
<Button android:id="@+id/Button8" android:text="8 tuv" />
<Button android:id="@+id/Button9" android:text="9 wxyz" />
</TableRow>
</TableLayout>
</LinearLayout>

32
res/layout/statusbar.xml Normal file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:id="@+id/icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:padding="4dip" />
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/title"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMediumInverse"
android:singleLine="true"
android:ellipsize="marquee" />
<TextView
android:id="@+id/artist"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmallInverse"
android:singleLine="true"
android:ellipsize="marquee" />
</LinearLayout>
</LinearLayout>

7
res/values/strings.xml Normal file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Tumult</string>
<string name="headset_only_summary_on">Audio only plays when a headset is plugged in</string>
<string name="headset_only_title">Headset only</string>
<string name="headset_only_summary_off">Audio may play without a headset plugged in</string>
</resources>

5
res/values/theme.xml Normal file

@ -0,0 +1,5 @@
<resources>
<style name="Theme.NoBackground" parent="android:Theme.NoTitleBar">
<item name="android:windowBackground">@null</item>
</style>
</resources>

11
res/xml/preferences.xml Normal file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8" ?>
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
android:persistent="true">
<CheckBoxPreference
android:key="headset_only"
android:title="@string/headset_only_title"
android:defaultValue="false"
android:summaryOn="@string/headset_only_summary_on"
android:summaryOff="@string/headset_only_summary_off" />
</PreferenceScreen>

@ -0,0 +1,306 @@
package org.kreed.tumult;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.widget.Scroller;
public class CoverView extends View {
private static final int SNAP_VELOCITY = 1000;
private Scroller mScroller;
private VelocityTracker mVelocityTracker;
private float mLastMotionX;
private float mStartX;
private float mStartY;
private CoverViewWatcher mListener;
Song[] mSongs = new Song[3];
private Bitmap[] mBitmaps = new Bitmap[3];
private int mTentativeCover = -1;
public interface CoverViewWatcher {
public void next();
public void previous();
public void togglePlayback();
}
public CoverView(Context context)
{
super(context);
mScroller = new Scroller(context);
}
public void setCoverSwapListener(CoverViewWatcher listener)
{
mListener = listener;
}
private RectF scale(Bitmap bitmap, int maxWidth, int maxHeight)
{
float bitmapWidth = bitmap.getWidth();
float bitmapHeight = bitmap.getHeight();
float drawableAspectRatio = bitmapHeight / bitmapWidth;
float viewAspectRatio = (float) maxHeight / maxWidth;
float scale = drawableAspectRatio > viewAspectRatio ? maxHeight / bitmapWidth
: maxWidth / bitmapHeight;
bitmapWidth *= scale;
bitmapHeight *= scale;
float left = (maxWidth - bitmapWidth) / 2;
float top = (maxHeight - bitmapHeight) / 2;
return new RectF(left, top, maxWidth - left, maxHeight - top);
}
private void drawText(Canvas canvas, String text, float left, float top, float width, float maxWidth, Paint paint)
{
float offset = Math.max(0, maxWidth - width) / 2;
canvas.clipRect(left, top, left + maxWidth, top + paint.getTextSize() * 2, Region.Op.REPLACE);
canvas.drawText(text, left + offset, top - paint.ascent(), paint);
}
private void createBitmap(int i)
{
Song song = mSongs[i];
Bitmap bitmap = null;
if (song != null) {
int width = getWidth();
int height = getHeight();
bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
Canvas canvas = new Canvas(bitmap);
Paint paint = new Paint();
Bitmap cover = song.coverPath == null ? null : BitmapFactory.decodeFile(song.coverPath);
if (cover != null) {
RectF dest = scale(cover, width, height);
canvas.drawBitmap(cover, new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()), dest, paint);
cover.recycle();
cover = null;
}
paint.setAntiAlias(true);
String title = song.title == null ? "" : song.title;
String album = song.album == null ? "" : song.album;
String artist = song.artist == null ? "" : song.artist;
float titleSize = 20;
float subSize = 14;
float padding = 10;
paint.setTextSize(titleSize);
float titleWidth = paint.measureText(title);
paint.setTextSize(subSize);
float albumWidth = paint.measureText(album);
float artistWidth = paint.measureText(artist);
float boxWidth = Math.max(titleWidth, Math.max(artistWidth, albumWidth)) + padding * 2;
float boxHeight = titleSize + subSize * 2 + padding * 4;
boxWidth = Math.min(boxWidth, width);
boxHeight = Math.min(boxHeight, height);
paint.setARGB(150, 0, 0, 0);
float left = (width - boxWidth) / 2;
float top = (height - boxHeight) / 2;
float right = (width + boxWidth) / 2;
float bottom = (height + boxHeight) / 2;
canvas.drawRect(left, top, right, bottom, paint);
float maxWidth = boxWidth - padding * 2;
paint.setARGB(255, 255, 255, 255);
top += padding;
left += padding;
paint.setTextSize(titleSize);
drawText(canvas, title, left, top, titleWidth, maxWidth, paint);
top += titleSize + padding;
paint.setTextSize(subSize);
drawText(canvas, album, left, top, albumWidth, maxWidth, paint);
top += subSize + padding;
drawText(canvas, artist, left, top, artistWidth, maxWidth, paint);
}
Bitmap oldBitmap = mBitmaps[i];
mBitmaps[i] = bitmap;
if (oldBitmap != null)
oldBitmap.recycle();
}
public void setSongs(Song[] songs)
{
mSongs = songs;
regenerateBitmaps();
}
public void regenerateBitmaps()
{
if (getWidth() == 0 || getHeight() == 0)
return;
for (int i = mSongs.length; --i != -1; )
createBitmap(i);
reset();
}
public void setForwardSong(Song song)
{
if (mSongs[mSongs.length - 1] != null) {
System.arraycopy(mSongs, 1, mSongs, 0, mSongs.length - 1);
System.arraycopy(mBitmaps, 1, mBitmaps, 0, mBitmaps.length - 1);
mBitmaps[mBitmaps.length - 1] = null;
reset();
}
mSongs[mSongs.length - 1] = song;
createBitmap(mSongs.length - 1);
}
public void setBackwardSong(Song song)
{
if (mSongs[0] != null) {
System.arraycopy(mSongs, 0, mSongs, 1, mSongs.length - 1);
System.arraycopy(mBitmaps, 0, mBitmaps, 1, mBitmaps.length - 1);
mBitmaps[0] = null;
reset();
}
mSongs[0] = song;
createBitmap(0);
}
public void reset()
{
if (!mScroller.isFinished())
mScroller.abortAnimation();
scrollTo(getWidth(), 0);
postInvalidate();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh)
{
regenerateBitmaps();
}
@Override
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
Rect clip = canvas.getClipBounds();
Paint paint = new Paint();
int width = getWidth();
int height = getHeight();
for (int x = 0, i = 0; i != mBitmaps.length; ++i, x += width)
if (mBitmaps[i] != null && clip.intersects(x, 0, x + width, height))
canvas.drawBitmap(mBitmaps[i], x, 0, paint);
paint = null;
}
@Override
public boolean onTouchEvent(MotionEvent ev)
{
// based on code from com.android.launcher.Workspace
if (mVelocityTracker == null)
mVelocityTracker = VelocityTracker.obtain();
mVelocityTracker.addMovement(ev);
float x = ev.getX();
int scrollX = getScrollX();
int width = getWidth();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
if (!mScroller.isFinished())
mScroller.abortAnimation();
mStartX = x;
mStartY = ev.getY();
mLastMotionX = x;
break;
case MotionEvent.ACTION_MOVE:
int deltaX = (int) (mLastMotionX - x);
mLastMotionX = x;
if (deltaX < 0) {
int availableToScroll = scrollX - (mBitmaps[0] == null ? width : 0);
if (availableToScroll > 0)
scrollBy(Math.max(-availableToScroll, deltaX), 0);
} else if (deltaX > 0) {
int availableToScroll = getWidth() * 2 - scrollX;
if (availableToScroll > 0)
scrollBy(Math.min(availableToScroll, deltaX), 0);
}
break;
case MotionEvent.ACTION_UP:
if (Math.abs(mStartX - x) + Math.abs(mStartY - ev.getY()) < 10) {
mListener.togglePlayback();
} else {
VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000);
int velocity = (int) velocityTracker.getXVelocity();
int min = mBitmaps[0] == null ? 1 : 0;
int max = 2;
int nearestCover = (scrollX + width / 2) / width;
int whichCover = Math.max(min, Math.min(nearestCover, max));
if (velocity > SNAP_VELOCITY && whichCover != min)
--whichCover;
else if (velocity < -SNAP_VELOCITY && whichCover != max)
++whichCover;
int newX = whichCover * width;
int delta = newX - scrollX;
mScroller.startScroll(scrollX, 0, delta, 0, Math.abs(delta) * 2);
mTentativeCover = whichCover;
postInvalidate();
}
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
break;
}
return true;
}
public void computeScroll()
{
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
} else if (mTentativeCover != -1) {
if (mListener != null) {
if (mTentativeCover == 2)
mListener.next();
else if (mTentativeCover == 0)
mListener.previous();
}
mTentativeCover = -1;
}
}
}

@ -0,0 +1,9 @@
package org.kreed.tumult;
import org.kreed.tumult.Song;
oneway interface IMusicPlayerWatcher {
void previousSong(in Song playingSong, in Song nextForwardSong);
void nextSong(in Song playingSong, in Song nextBackwardSong);
void stateChanged(in int oldState, in int newState);
}

@ -0,0 +1,14 @@
package org.kreed.tumult;
import org.kreed.tumult.Song;
import org.kreed.tumult.IMusicPlayerWatcher;
interface IPlaybackService {
void registerWatcher(IMusicPlayerWatcher watcher);
Song[] getCurrentSongs();
void previousSong();
void togglePlayback();
void nextSong();
}

@ -0,0 +1,349 @@
package org.kreed.tumult;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.util.Log;
import android.widget.RemoteViews;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Random;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.preference.PreferenceManager;
public class MusicPlayer implements Runnable, MediaPlayer.OnCompletionListener, SharedPreferences.OnSharedPreferenceChangeListener {
private static final int NOTIFICATION_ID = 2;
public static final int STATE_NORMAL = 0;
public static final int STATE_NO_MEDIA = 1;
public IPlaybackService.Stub mBinder = new IPlaybackService.Stub() {
public Song[] getCurrentSongs()
{
return new Song[] { getSong(-1), getSong(0), getSong(1) };
}
public void nextSong()
{
if (mHandler == null)
return;
mHandler.sendMessage(mHandler.obtainMessage(SET_SONG, 1, 0));
}
public void previousSong()
{
if (mHandler == null)
return;
mHandler.sendMessage(mHandler.obtainMessage(SET_SONG, -1, 0));
}
public void togglePlayback()
{
if (mHandler == null)
return;
mHandler.sendMessage(mHandler.obtainMessage(PLAY_PAUSE, 0, 0));
}
public void registerWatcher(IMusicPlayerWatcher watcher)
{
if (watcher != null)
mWatchers.register(watcher);
}
};
public void queueSong(int songId)
{
if (mHandler == null)
return;
mHandler.sendMessage(mHandler.obtainMessage(QUEUE_ITEM, ITEM_SONG, songId));
}
public void stopQueueing()
{
if (mHandler == null)
return;
mHandler.sendMessage(mHandler.obtainMessage(QUEUE_ITEM, ITEM_RESET, 0));
}
private PlaybackService mService;
private RemoteCallbackList<IMusicPlayerWatcher> mWatchers;
private boolean mHeadsetOnly = true;
private Handler mHandler;
private MediaPlayer mMediaPlayer;
private Random mRandom;
private int[] mSongs;
private ArrayList<Song> mSongTimeline;
private int mCurrentSong = -1;
private int mQueuePos = 0;
private boolean mPlugged = true;
private int mState = STATE_NORMAL;
public MusicPlayer(PlaybackService service)
{
mService = service;
mWatchers = new RemoteCallbackList<IMusicPlayerWatcher>();
mSongTimeline = new ArrayList<Song>();
new Thread(this).start();
}
private static final int SET_SONG = 0;
private static final int PLAY_PAUSE = 1;
private static final int HEADSET_PLUGGED = 2;
private static final int HEADSET_PREF_CHANGED = 3;
private static final int QUEUE_ITEM = 4;
private static final int ITEM_SONG = 0;
private static final int ITEM_RESET = 1;
public void run()
{
Looper.prepare();
mMediaPlayer = new MediaPlayer();
mRandom = new Random();
mHandler = new Handler() {
public void handleMessage(Message message)
{
switch (message.what) {
case SET_SONG:
setCurrentSong(message.arg1);
break;
case PLAY_PAUSE:
if (mCurrentSong == -1) {
setCurrentSong(+1);
return;
}
setPlaying(!mMediaPlayer.isPlaying());
break;
case HEADSET_PLUGGED:
boolean plugged = message.arg1 == 1;
if (plugged != mPlugged) {
mPlugged = plugged;
if (mCurrentSong == -1 || mPlugged == mMediaPlayer.isPlaying())
return;
setPlaying(mPlugged);
}
break;
case HEADSET_PREF_CHANGED:
mHeadsetOnly = message.arg1 == 1;
if (mHeadsetOnly && !mPlugged && mMediaPlayer.isPlaying())
pause();
break;
case QUEUE_ITEM:
switch (message.arg1) {
case ITEM_SONG:
int i = mCurrentSong + 1 + mQueuePos++;
Song song = new Song(message.arg2);
if (i < mSongTimeline.size())
mSongTimeline.set(i, song);
else
mSongTimeline.add(song);
break;
case ITEM_RESET:
mQueuePos = 0;
break;
}
break;
}
}
};
mService.registerReceiver(mReceiver, new IntentFilter(Intent.ACTION_HEADSET_PLUG));
mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mMediaPlayer.setOnCompletionListener(this);
retrieveSongs();
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mService);
mHeadsetOnly = settings.getBoolean("headset_only", false);
settings.registerOnSharedPreferenceChangeListener(this);
mHandler.post(new Runnable() {
public void run()
{
setCurrentSong(1);
}
});
Looper.loop();
}
public void release()
{
pause();
if (mMediaPlayer != null) {
mMediaPlayer.release();
mMediaPlayer = null;
}
}
public void setState(int state)
{
int oldState = mState;
mState = state;
int i = mWatchers.beginBroadcast();
while (--i != -1) {
try {
mWatchers.getBroadcastItem(i).stateChanged(oldState, mState);
} catch (RemoteException e) {
// Null elements will be removed automatically
}
}
mWatchers.finishBroadcast();
}
private void retrieveSongs()
{
mSongs = Song.getAllSongs();
if (mSongs == null && mState == STATE_NORMAL)
setState(STATE_NO_MEDIA);
}
private void play()
{
if (mHeadsetOnly && !mPlugged)
return;
mMediaPlayer.start();
Song song = getSong(0);
RemoteViews views = new RemoteViews(mService.getPackageName(), R.layout.statusbar);
views.setImageViewResource(R.id.icon, R.drawable.status_icon);
views.setTextViewText(R.id.title, song.title);
views.setTextViewText(R.id.artist, song.artist);
Notification notification = new Notification();
notification.contentView = views;
notification.icon = R.drawable.status_icon;
notification.flags |= Notification.FLAG_ONGOING_EVENT;
Intent intent = new Intent(mService, NowPlayingActivity.class);
notification.contentIntent = PendingIntent.getActivity(mService, 0, intent, 0);
mService.startForegroundCompat(NOTIFICATION_ID, notification);
}
private void pause()
{
mMediaPlayer.pause();
Log.i("Tumult", "background");
mService.stopForegroundCompat(NOTIFICATION_ID);
}
private void setPlaying(boolean play)
{
if (play)
play();
else
pause();
}
private void setCurrentSong(int delta)
{
Song song = getSong(delta);
if (song == null)
return;
mCurrentSong += delta;
Song newSong = getSong(delta);
int i = mWatchers.beginBroadcast();
while (--i != -1) {
try {
if (delta < 0)
mWatchers.getBroadcastItem(i).previousSong(song, newSong);
else
mWatchers.getBroadcastItem(i).nextSong(song, newSong);
} catch (RemoteException e) {
// Null elements will be removed automatically
}
}
mWatchers.finishBroadcast();
try {
mMediaPlayer.reset();
mMediaPlayer.setDataSource(song.path);
mMediaPlayer.prepare();
play();
} catch (IOException e) {
Log.e("Tumult", "IOException", e);
}
getSong(+2);
while (mCurrentSong > 15) {
mSongTimeline.remove(0);
--mCurrentSong;
}
}
public void onCompletion(MediaPlayer player)
{
setCurrentSong(+1);
}
private Song randomSong()
{
return new Song(mSongs[mRandom.nextInt(mSongs.length)]);
}
private synchronized Song getSong(int delta)
{
int pos = mCurrentSong + delta;
if (pos < 0)
return null;
int size = mSongTimeline.size();
if (pos > size)
return null;
if (pos == size) {
if (mSongs == null)
return null;
mSongTimeline.add(randomSong());
}
return mSongTimeline.get(pos);
}
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context content, Intent intent)
{
if (intent.getAction().equals(Intent.ACTION_HEADSET_PLUG) && mHandler != null) {
int plugged = intent.getIntExtra("state", 0) == 130 ? 1 : 0;
mHandler.sendMessage(mHandler.obtainMessage(HEADSET_PLUGGED, plugged, 0));
}
}
};
public void onSharedPreferenceChanged(SharedPreferences settings, String key)
{
if ("headset_only".equals(key) && mHandler != null) {
int arg = settings.getBoolean(key, false) ? 1 : 0;
mHandler.sendMessage(mHandler.obtainMessage(HEADSET_PREF_CHANGED, arg, 0));
}
}
}

@ -0,0 +1,220 @@
package org.kreed.tumult;
import org.kreed.tumult.CoverView.CoverViewWatcher;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import android.view.Gravity;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.LinearLayout;
import android.widget.TextView;
public class NowPlayingActivity extends Activity implements CoverViewWatcher, ServiceConnection {
private IPlaybackService mService;
private CoverView mCoverView;
private LinearLayout mMessageBox;
private static final int MENU_PREVIOUS = 0;
private static final int MENU_NEXT = 1;
private static final int MENU_PREFS = 2;
private static final int MENU_QUEUE = 3;
@Override
public void onCreate(Bundle icicle)
{
super.onCreate(icicle);
mCoverView = new CoverView(this);
mCoverView.setCoverSwapListener(this);;
setContentView(mCoverView);
// Bundle extras = getIntent().getExtras();
}
public void setState(int state)
{
switch (state) {
case MusicPlayer.STATE_NORMAL:
setContentView(mCoverView);
mMessageBox = null;
break;
case MusicPlayer.STATE_NO_MEDIA:
mMessageBox = new LinearLayout(this);
mMessageBox.setGravity(Gravity.CENTER);
TextView text = new TextView(this);
text.setText("No songs found on your device.");
text.setGravity(Gravity.CENTER);
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
layoutParams.gravity = Gravity.CENTER;
text.setLayoutParams(layoutParams);
mMessageBox.addView(text);
setContentView(mMessageBox);
break;
}
}
@Override
public void onResume()
{
super.onResume();
reconnect();
}
private void reconnect()
{
Intent intent = new Intent(this, PlaybackService.class);
startService(intent);
bindService(intent, this, Context.BIND_AUTO_CREATE);
}
@Override
public void onPause()
{
super.onPause();
unbindService(this);
}
private void refreshSongs()
{
try {
mCoverView.setSongs(mService.getCurrentSongs());
} catch (RemoteException e) {
Log.e("Tumult", "RemoteException", e);
}
}
public void onServiceConnected(ComponentName name, IBinder service)
{
mService = IPlaybackService.Stub.asInterface(service);
try {
mService.registerWatcher(mWatcher);
refreshSongs();
} catch (RemoteException e) {
Log.i("Tumult", "Failed to initialize connection to playback service", e);
}
}
public void onServiceDisconnected(ComponentName name)
{
mService = null;
reconnect();
}
private IMusicPlayerWatcher mWatcher = new IMusicPlayerWatcher.Stub() {
public void nextSong(final Song playingSong, final Song forwardSong)
{
if (mCoverView.mSongs[1] != null && mCoverView.mSongs[1].path.equals(playingSong.path)) {
mCoverView.setForwardSong(forwardSong);
} else {
runOnUiThread(new Runnable() {
public void run()
{
refreshSongs();
}
});
}
}
public void previousSong(final Song playingSong, final Song backwardSong)
{
if (mCoverView.mSongs[1] != null && mCoverView.mSongs[1].path.equals(playingSong.path)) {
mCoverView.setBackwardSong(backwardSong);
} else {
runOnUiThread(new Runnable() {
public void run()
{
refreshSongs();
}
});
}
}
public void stateChanged(final int oldState, final int newState)
{
runOnUiThread(new Runnable() {
public void run()
{
setState(newState);
}
});
}
};
public void next()
{
mCoverView.setForwardSong(null);
try {
mService.nextSong();
} catch (RemoteException e) {
}
}
public void previous()
{
mCoverView.setBackwardSong(null);
try {
mService.previousSong();
} catch (RemoteException e) {
}
}
public void togglePlayback()
{
try {
mService.togglePlayback();
} catch (RemoteException e) {
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu)
{
menu.add(0, MENU_PREVIOUS, 0, "Previous");
menu.add(0, MENU_NEXT, 0, "Next");
menu.add(0, MENU_PREFS, 0, "Preferences");
menu.add(0, MENU_QUEUE, 0, "Add to Queue");
return true;
}
@Override
public boolean onOptionsItemSelected(final MenuItem item)
{
new Thread(new Runnable() {
public void run()
{
switch (item.getItemId()) {
case MENU_PREVIOUS:
previous();
break;
case MENU_NEXT:
next();
break;
case MENU_PREFS:
startActivity(new Intent(NowPlayingActivity.this, PreferencesActivity.class));
break;
case MENU_QUEUE:
onSearchRequested();
break;
}
}
}).start();
return true;
}
@Override
public boolean onSearchRequested()
{
startActivity(new Intent(this, SongSelector.class));
return false;
}
}

@ -0,0 +1,96 @@
package org.kreed.tumult;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;
public class PlaybackService extends Service {
private MusicPlayer mPlayer;
private NotificationManager mNotificationManager;
private Method mStartForeground;
private Method mStopForeground;
@Override
public IBinder onBind(Intent intent)
{
if (mPlayer == null)
return null;
return mPlayer.mBinder;
}
@Override
public void onCreate()
{
mNotificationManager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
try {
mStartForeground = getClass().getMethod("startForeground", new Class[] { int.class, Notification.class });
mStopForeground = getClass().getMethod("stopForeground", new Class[] { boolean.class });
} catch (NoSuchMethodException e) {
// Running on an older platform.
mStartForeground = mStopForeground = null;
}
mPlayer = new MusicPlayer(this);
}
@Override
public void onStart(Intent intent, int flags)
{
int id;
if ((id = intent.getIntExtra("songId", -1)) != -1)
mPlayer.queueSong(id);
else
mPlayer.stopQueueing();
}
@Override
public void onDestroy()
{
super.onDestroy();
if (mPlayer != null) {
mPlayer.release();
mPlayer = null;
}
}
public void startForegroundCompat(int id, Notification notification)
{
if (mStartForeground == null) {
setForeground(true);
mNotificationManager.notify(id, notification);
} else {
Object[] startForegroundArgs = { Integer.valueOf(id), notification };
try {
mStartForeground.invoke(this, startForegroundArgs);
} catch (InvocationTargetException e) {
Log.w("Tumult", "Unable to invoke startForeground", e);
} catch (IllegalAccessException e) {
Log.w("Tumult", "Unable to invoke startForeground", e);
}
}
}
public void stopForegroundCompat(int id)
{
if (mStopForeground == null) {
mNotificationManager.cancel(id);
setForeground(false);
} else {
Object[] topForegroundArgs = { Boolean.TRUE };
try {
mStopForeground.invoke(this, topForegroundArgs);
} catch (InvocationTargetException e) {
Log.w("Tumult", "Unable to invoke stopForeground", e);
} catch (IllegalAccessException e) {
Log.w("Tumult", "Unable to invoke stopForeground", e);
}
}
}
}

@ -0,0 +1,13 @@
package org.kreed.tumult;
import android.os.Bundle;
import android.preference.PreferenceActivity;
public class PreferencesActivity extends PreferenceActivity {
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.preferences);
}
}

@ -0,0 +1,3 @@
package org.kreed.tumult;
parcelable Song;

@ -0,0 +1,113 @@
package org.kreed.tumult;
import android.content.ContentResolver;
import android.database.Cursor;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import android.provider.MediaStore;
public class Song implements Parcelable {
public String path;
public String coverPath;
public String title;
public String album;
public String artist;
public Song(int id)
{
Uri media = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
String[] projection = {
MediaStore.Audio.Media.DATA,
MediaStore.Audio.Media.TITLE,
MediaStore.Audio.Media.ALBUM,
MediaStore.Audio.Media.ARTIST,
MediaStore.Audio.Media.ALBUM_ID
};
String selection = MediaStore.Audio.Media._ID + "==" + id;;
ContentResolver resolver = Tumult.getContext().getContentResolver();
Cursor cursor = resolver.query(media, projection, selection, null, null);
if (cursor != null && cursor.moveToNext()) {
path = cursor.getString(0);
title = cursor.getString(1);
album = cursor.getString(2);
artist = cursor.getString(3);
String albumId = cursor.getString(4);
cursor.close();
media = MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI;
String[] albumProjection = { MediaStore.Audio.Albums.ALBUM_ART };
String albumSelection = MediaStore.Audio.Albums._ID + "==" + albumId;
cursor = resolver.query(media, albumProjection, albumSelection, null, null);
if (cursor != null && cursor.moveToNext()) {
coverPath = cursor.getString(0);
cursor.close();
}
}
}
public static int[] getAllSongs()
{
Uri media = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
String[] projection = { MediaStore.Audio.Media._ID };
String selection = MediaStore.Audio.Media.IS_MUSIC + "!=0";
ContentResolver resolver = Tumult.getContext().getContentResolver();
Cursor cursor = resolver.query(media, projection, selection, null, null);
if (cursor == null)
return null;
int count = cursor.getCount();
if (count == 0)
return null;
int[] songs = new int[count];
while (--count != -1 && cursor.moveToNext())
songs[count] = cursor.getInt(0);
cursor.close();
cursor = null;
return songs;
}
public static Parcelable.Creator<Song> CREATOR = new Parcelable.Creator<Song>() {
public Song createFromParcel(Parcel in)
{
return new Song(in);
}
public Song[] newArray(int size)
{
return new Song[size];
}
};
public Song(Parcel in)
{
path = in.readString();
coverPath = in.readString();
title = in.readString();
album = in.readString();
artist = in.readString();
}
public void writeToParcel(Parcel out, int flags)
{
out.writeString(path);
out.writeString(coverPath);
out.writeString(title);
out.writeString(album);
out.writeString(artist);
}
public int describeContents()
{
return 0;
}
}

@ -0,0 +1,188 @@
package org.kreed.tumult;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.MediaStore;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Filter;
import android.widget.Filterable;
import android.widget.TextView;
public class SongAdapter extends BaseAdapter implements Filterable {
private Context mContext;
private final Object mLock = new Object();
private List<StringPair> mObjects;
private List<StringPair> mOriginalValues;
private ArrayFilter mFilter;
private class StringPair implements Comparable<StringPair> {
public int id;
public String value;
public int compareTo(StringPair another)
{
return value.compareTo(another.value);
}
}
public SongAdapter(Context context)
{
mContext = context;
querySongs();
}
private void querySongs()
{
Uri media = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
String[] projection = { MediaStore.Audio.Media._ID,
MediaStore.Audio.Media.TITLE,
MediaStore.Audio.Media.ALBUM,
MediaStore.Audio.Media.ARTIST };
String selection = MediaStore.Audio.Media.IS_MUSIC + "!=0";
ContentResolver resolver = mContext.getContentResolver();
Cursor cursor = resolver.query(media, projection, selection, null, null);
if (cursor == null)
return;
mObjects = new ArrayList<StringPair>(cursor.getCount());
while (cursor.moveToNext()) {
StringPair pair = new StringPair();
pair.id = cursor.getInt(0);
pair.value = cursor.getString(3) + " / " + cursor.getString(2) + " / " + cursor.getString(1);
mObjects.add(pair);
}
cursor.close();
Collections.sort(mObjects);
}
@Override
public boolean hasStableIds()
{
return true;
}
public View getView(int position, View convertView, ViewGroup parent)
{
TextView view = null;
try {
view = (TextView)convertView;
} catch (ClassCastException e) {
}
if (view == null)
view = new TextView(mContext);
view.setText(mObjects.get(position).value);
return view;
}
public Filter getFilter() {
if (mFilter == null)
mFilter = new ArrayFilter();
return mFilter;
}
private static final String[] mRanges = { "[01 ]", "[2abc]", "[3def]", "[4ghi]", "[5jkl]", "[6mno]", "[7pqrs]", "[8tuv]", "[9wxyz]"};
private class ArrayFilter extends Filter {
@Override
protected FilterResults performFiltering(CharSequence filter)
{
FilterResults results = new FilterResults();
if (mOriginalValues == null) {
synchronized (mLock) {
mOriginalValues = new ArrayList<StringPair>(mObjects);
}
}
if (filter == null || filter.length() == 0) {
synchronized (mLock) {
ArrayList<StringPair> list = new ArrayList<StringPair>(mOriginalValues);
results.values = list;
results.count = list.size();
}
} else {
String patternString = "";
for (int i = 0, end = filter.length(); i != end; ++i) {
char c = filter.charAt(i);
int value = c - '1';
if (value >= 0 && value < 9)
patternString += mRanges[value];
else
patternString += c;
}
Pattern pattern = Pattern.compile(patternString);
Matcher matcher = pattern.matcher("");
List<StringPair> values = mOriginalValues;
int count = values.size();
ArrayList<StringPair> newValues = new ArrayList<StringPair>();
newValues.ensureCapacity(count);
int i;
for (i = 0; i != count; ++i) {
StringPair value = values.get(i);
matcher.reset(value.value.toLowerCase());
if (matcher.find())
newValues.add(value);
}
newValues.trimToSize();
results.values = newValues;
results.count = newValues.size();
}
return results;
}
@SuppressWarnings("unchecked")
@Override
protected void publishResults(CharSequence constraint, FilterResults results)
{
mObjects = (List<StringPair>) results.values;
if (results.count == 0)
notifyDataSetInvalidated();
else
notifyDataSetChanged();
}
}
public int getCount()
{
if (mObjects == null)
return 0;
return mObjects.size();
}
public Object getItem(int position)
{
if (mObjects == null)
return null;
return mObjects.get(position);
}
public long getItemId(int position)
{
if (mObjects == null || mObjects.isEmpty())
return 0;
return mObjects.get(position).id;
}
}

@ -0,0 +1,125 @@
package org.kreed.tumult;
import android.app.Activity;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.View;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.AdapterView.OnItemClickListener;
public class SongSelector extends Activity implements View.OnClickListener, OnItemClickListener {
private ListView mListView;
private SongAdapter mAdapter;
private LinearLayout mFilterLayout;
private TextView mFilterText;
private Button mBackspaceButton;
private Button mCloseButton;
private View mNumpad;
private Button[] mButtons;
@Override
public void onCreate(Bundle icicle)
{
super.onCreate(icicle);
setContentView(R.layout.songselector);
mListView = (ListView)findViewById(R.id.song_list);
mAdapter = new SongAdapter(this);
mListView.setAdapter(mAdapter);
mListView.setTextFilterEnabled(true);
mListView.setOnItemClickListener(this);
mFilterLayout = (LinearLayout)findViewById(R.id.filter_layout);
mFilterText = (TextView)findViewById(R.id.filter_text);
mBackspaceButton = (Button)findViewById(R.id.backspace);
mBackspaceButton.setOnClickListener(this);
mCloseButton = (Button)findViewById(R.id.close);
mCloseButton.setOnClickListener(this);
mNumpad = findViewById(R.id.numpad);
Configuration config = getResources().getConfiguration();
boolean hasKeyboard = config.keyboardHidden == Configuration.KEYBOARDHIDDEN_NO && config.keyboard == Configuration.KEYBOARD_QWERTY;
mNumpad.setVisibility(hasKeyboard ? View.GONE : View.VISIBLE);
mButtons = new Button[] {
(Button)findViewById(R.id.Button1),
(Button)findViewById(R.id.Button2),
(Button)findViewById(R.id.Button3),
(Button)findViewById(R.id.Button4),
(Button)findViewById(R.id.Button5),
(Button)findViewById(R.id.Button6),
(Button)findViewById(R.id.Button7),
(Button)findViewById(R.id.Button8),
(Button)findViewById(R.id.Button9)
};
for (Button button : mButtons)
button.setOnClickListener(this);
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event)
{
switch (keyCode) {
case KeyEvent.KEYCODE_BACK:
if (mFilterLayout.getVisibility() != View.GONE)
onClick(mCloseButton);
else
finish();
return true;
}
return false;
}
@Override
public boolean onSearchRequested()
{
mNumpad.setVisibility(mNumpad.getVisibility() == View.GONE ? View.VISIBLE : View.GONE);
return false;
}
public void onClick(View view)
{
int visible = View.VISIBLE;
String text = mFilterText.getText().toString();
if (text.length() == 0)
text = "Filter: ";
if (view == mCloseButton) {
visible = View.GONE;
text = null;
} else if (view == mBackspaceButton) {
if (text.length() > 8)
text = text.substring(0, text.length() - 1);
} else {
int i = -1;
while (++i != mButtons.length)
if (mButtons[i] == view)
break;
text += i + 1;
}
mFilterText.setText(text);
mFilterLayout.setVisibility(visible);
mListView.setTextFilterEnabled(visible == View.VISIBLE);
String filterText = text == null || text.length() <= 8 ? null : text.substring(8);
mAdapter.getFilter().filter(filterText);
}
public void onItemClick(AdapterView<?> list, View view, int pos, long id)
{
Intent intent = new Intent(this, PlaybackService.class);
intent.putExtra("songId", (int)id);
startService(intent);
}
}

@ -0,0 +1,18 @@
package org.kreed.tumult;
import android.app.Application;
import android.content.Context;
public class Tumult extends Application {
private static Tumult instance;
public Tumult()
{
instance = this;
}
public static Context getContext()
{
return instance;
}
}