Import version 1.0
This commit is contained in:
commit
5e4244a95d
.classpath.project
.settings
AndroidManifest.xmldefault.propertiesres
drawable
layout
values
xml
src/org/kreed/tumult
7
.classpath
Normal file
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
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>
|
5
.settings/org.eclipse.jdt.core.prefs
Normal file
5
.settings/org.eclipse.jdt.core.prefs
Normal file
@ -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
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
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
BIN
res/drawable/icon.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 3.4 KiB |
BIN
res/drawable/status_icon.png
Normal file
BIN
res/drawable/status_icon.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 1.5 KiB |
58
res/layout/songselector.xml
Normal file
58
res/layout/songselector.xml
Normal file
@ -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="<" />
|
||||
<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
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
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
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
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>
|
306
src/org/kreed/tumult/CoverView.java
Executable file
306
src/org/kreed/tumult/CoverView.java
Executable file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
9
src/org/kreed/tumult/IMusicPlayerWatcher.aidl
Normal file
9
src/org/kreed/tumult/IMusicPlayerWatcher.aidl
Normal file
@ -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);
|
||||
}
|
14
src/org/kreed/tumult/IPlaybackService.aidl
Normal file
14
src/org/kreed/tumult/IPlaybackService.aidl
Normal file
@ -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();
|
||||
}
|
349
src/org/kreed/tumult/MusicPlayer.java
Normal file
349
src/org/kreed/tumult/MusicPlayer.java
Normal file
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
220
src/org/kreed/tumult/NowPlayingActivity.java
Normal file
220
src/org/kreed/tumult/NowPlayingActivity.java
Normal file
@ -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;
|
||||
}
|
||||
}
|
96
src/org/kreed/tumult/PlaybackService.java
Normal file
96
src/org/kreed/tumult/PlaybackService.java
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
13
src/org/kreed/tumult/PreferencesActivity.java
Normal file
13
src/org/kreed/tumult/PreferencesActivity.java
Normal file
@ -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);
|
||||
}
|
||||
}
|
3
src/org/kreed/tumult/Song.aidl
Normal file
3
src/org/kreed/tumult/Song.aidl
Normal file
@ -0,0 +1,3 @@
|
||||
package org.kreed.tumult;
|
||||
|
||||
parcelable Song;
|
113
src/org/kreed/tumult/Song.java
Normal file
113
src/org/kreed/tumult/Song.java
Normal file
@ -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;
|
||||
}
|
||||
}
|
188
src/org/kreed/tumult/SongAdapter.java
Normal file
188
src/org/kreed/tumult/SongAdapter.java
Normal file
@ -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;
|
||||
}
|
||||
}
|
125
src/org/kreed/tumult/SongSelector.java
Normal file
125
src/org/kreed/tumult/SongSelector.java
Normal file
@ -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);
|
||||
}
|
||||
}
|
18
src/org/kreed/tumult/Tumult.java
Normal file
18
src/org/kreed/tumult/Tumult.java
Normal file
@ -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;
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user