Move bitmap generation and service interaction into a background thread

This commit is contained in:
Christopher Eby 2010-04-24 13:23:48 -05:00
parent 69ea872f75
commit 88784789ca
5 changed files with 137 additions and 124 deletions

View File

@ -29,7 +29,6 @@ import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.TypedValue;
@ -39,7 +38,7 @@ import android.view.View;
import android.view.ViewConfiguration;
import android.widget.Scroller;
public final class CoverView extends View implements Handler.Callback {
public final class CoverView extends View {
private static final int STORE_SIZE = 3;
private static int SNAP_VELOCITY = -1;
@ -52,20 +51,27 @@ public final class CoverView extends View implements Handler.Callback {
private static Bitmap ALBUM_ICON;
private static Bitmap ARTIST_ICON;
private Scroller mScroller;
private Handler mHandler = new Handler(this);
/**
* The Handler with which to do background work. Must be initialized by
* the containing Activity.
*/
Handler mHandler;
/**
* Whether or not to display song info on top of the cover art. Can be
* initialized by the containing Activity.
*/
boolean mSeparateInfo;
private Song[] mSongs = new Song[3];
private Bitmap[] mBitmaps = new Bitmap[3];
private int mTimelinePos;
private Scroller mScroller;
private VelocityTracker mVelocityTracker;
private float mLastMotionX;
private float mStartX;
private float mStartY;
private int mTentativeCover = -1;
private boolean mSeparateInfo;
public CoverView(Context context, AttributeSet attributes)
{
super(context, attributes);
@ -105,21 +111,6 @@ public final class CoverView extends View implements Handler.Callback {
bitmap.recycle();
}
public Song getCurrentSong()
{
return mSongs[STORE_SIZE / 2];
}
public boolean hasSeparateInfo()
{
return mSeparateInfo;
}
public void setSeparateInfo(boolean separate)
{
mSeparateInfo = separate;
}
/**
* Query the service for initial song info.
*/
@ -377,18 +368,19 @@ public final class CoverView extends View implements Handler.Callback {
mBitmaps[i] = createSeparatedBitmap(mSongs[i], getWidth(), getHeight());
else
mBitmaps[i] = createOverlappingBitmap(mSongs[i], getWidth(), getHeight());
postInvalidate();
if (oldBitmap != null)
oldBitmap.recycle();
}
private void refreshSongs()
{
mHandler.sendEmptyMessage(1);
mHandler.sendEmptyMessage(2);
mHandler.sendEmptyMessage(0);
postLoadSong(1, null);
postLoadSong(2, null);
postLoadSong(0, null);
}
private void shiftCover(int delta)
public void go(int delta)
{
int i = delta > 0 ? STORE_SIZE - 1 : 0;
@ -397,21 +389,16 @@ public final class CoverView extends View implements Handler.Callback {
int from = delta > 0 ? 1 : 0;
int to = delta > 0 ? 0 : 1;
System.arraycopy(mSongs, from, mSongs, to, STORE_SIZE - 1);
System.arraycopy(mBitmaps, from, mBitmaps, to, STORE_SIZE - 1);
mSongs[i] = null;
mBitmaps[i] = null;
postLoadSong(i, null);
mTimelinePos += delta;
reset();
invalidate();
mHandler.sendEmptyMessage(i);
}
public void go(int delta)
{
mHandler.sendMessage(mHandler.obtainMessage(GO, delta, 0));
}
public void reset()
@ -419,7 +406,7 @@ public final class CoverView extends View implements Handler.Callback {
if (!mScroller.isFinished())
mScroller.abortAnimation();
scrollTo(getWidth(), 0);
invalidate();
postInvalidate();
}
private void regenerateBitmaps()
@ -545,32 +532,34 @@ public final class CoverView extends View implements Handler.Callback {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
} else if (mTentativeCover != -1) {
shiftCover(mTentativeCover - 1);
int delta = mTentativeCover - 1;
mTentativeCover = -1;
mHandler.sendMessage(mHandler.obtainMessage(PlaybackActivity.MSG_SET_SONG, delta, 0));
go(delta);
}
}
private static final int GO = 10;
public boolean handleMessage(Message message)
/**
* Load a song in the given position. If the song is null, queries the
* PlaybackService for the song at that position.
*/
void loadSong(int i, Song song)
{
switch (message.what) {
case GO:
shiftCover(message.arg1);
break;
default:
int i = message.what;
if (message.obj == null)
mSongs[i] = ContextApplication.service.getSong(i - STORE_SIZE / 2);
else
mSongs[i] = (Song)message.obj;
createBitmap(i);
if (i == STORE_SIZE / 2)
reset();
break;
}
mSongs[i] = song == null ? ContextApplication.service.getSong(i - STORE_SIZE / 2) : song;
createBitmap(i);
}
return true;
/**
* Post a Handler message to call loadSong.
*/
private void postLoadSong(final int i, final Song song)
{
mHandler.post(new Runnable() {
public void run()
{
loadSong(i, song);
}
});
}
/**
@ -585,7 +574,7 @@ public final class CoverView extends View implements Handler.Callback {
if (PlaybackService.EVENT_REPLACE_SONG.equals(action)) {
int i = STORE_SIZE / 2 + intent.getIntExtra("pos", 0);
Song song = intent.getParcelableExtra("song");
mHandler.sendMessage(mHandler.obtainMessage(i, song));
postLoadSong(i, song);
} else if (PlaybackService.EVENT_CHANGED.equals(action)) {
Song currentSong = mSongs[STORE_SIZE / 2];
Song playingSong = intent.getParcelableExtra("song");

View File

@ -24,6 +24,7 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.preference.PreferenceManager;
import android.view.KeyEvent;
@ -31,7 +32,6 @@ import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.SeekBar;
@ -42,11 +42,9 @@ public class FullPlaybackActivity extends PlaybackActivity implements SeekBar.On
private View mControlsTop;
private View mControlsBottom;
private ImageView mPlayPauseButton;
private SeekBar mSeekBar;
private TextView mSeekText;
int mState;
private int mDuration;
private boolean mSeekBarTracking;
private boolean mPaused;
@ -63,15 +61,16 @@ public class FullPlaybackActivity extends PlaybackActivity implements SeekBar.On
setContentView(R.layout.full_playback);
mCoverView = (CoverView)findViewById(R.id.cover_view);
mCoverView.mHandler = mHandler;
mCoverView.setOnClickListener(this);
mCoverView.setSeparateInfo(settings.getBoolean("separate_info", false));
mCoverView.mSeparateInfo = settings.getBoolean("separate_info", false);
mControlsTop = findViewById(R.id.controls_top);
mControlsBottom = findViewById(R.id.controls_bottom);
View previousButton = findViewById(R.id.previous);
previousButton.setOnClickListener(this);
mPlayPauseButton = (ImageView)findViewById(R.id.play_pause);
mPlayPauseButton = (ControlButton)findViewById(R.id.play_pause);
mPlayPauseButton.setOnClickListener(this);
View nextButton = findViewById(R.id.next);
nextButton.setOnClickListener(this);
@ -103,7 +102,7 @@ public class FullPlaybackActivity extends PlaybackActivity implements SeekBar.On
if (state == mState)
return;
mState = state;
super.setState(state);
if (mMessageOverlay != null)
mMessageOverlay.setVisibility(View.GONE);
@ -130,11 +129,6 @@ public class FullPlaybackActivity extends PlaybackActivity implements SeekBar.On
text.setLayoutParams(layoutParams);
mMessageOverlay.addView(text);
}
if ((mState & PlaybackService.FLAG_PLAYING) != 0)
mPlayPauseButton.setImageResource(R.drawable.pause);
else
mPlayPauseButton.setImageResource(R.drawable.play);
}
@Override
@ -152,7 +146,7 @@ public class FullPlaybackActivity extends PlaybackActivity implements SeekBar.On
if (PlaybackService.EVENT_CHANGED.equals(intent.getAction())) {
mDuration = ContextApplication.service.getDuration();
mHandler.sendEmptyMessage(MSG_UPDATE_PROGRESS);
mUiHandler.sendEmptyMessage(MSG_UPDATE_PROGRESS);
}
}
@ -234,10 +228,10 @@ public class FullPlaybackActivity extends PlaybackActivity implements SeekBar.On
// Try to update right when the duration increases by one second
long next = 1000 - position % 1000;
mHandler.removeMessages(MSG_UPDATE_PROGRESS);
mHandler.sendEmptyMessageDelayed(MSG_UPDATE_PROGRESS, next);
mUiHandler.removeMessages(MSG_UPDATE_PROGRESS);
mUiHandler.sendEmptyMessageDelayed(MSG_UPDATE_PROGRESS, next);
}
@Override
public void onClick(View view)
{
@ -265,13 +259,10 @@ public class FullPlaybackActivity extends PlaybackActivity implements SeekBar.On
public boolean handleMessage(Message message)
{
switch (message.what) {
case MSG_UPDATE_PROGRESS:
updateProgress();
return true;
case MSG_SAVE_DISPLAY_MODE:
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(FullPlaybackActivity.this);
SharedPreferences.Editor editor = settings.edit();
editor.putBoolean("separate_info", mCoverView.hasSeparateInfo());
editor.putBoolean("separate_info", mCoverView.mSeparateInfo);
editor.commit();
return true;
default:
@ -279,6 +270,18 @@ public class FullPlaybackActivity extends PlaybackActivity implements SeekBar.On
}
}
private Handler mUiHandler = new Handler() {
@Override
public void handleMessage(Message message)
{
switch (message.what) {
case MSG_UPDATE_PROGRESS:
updateProgress();
break;
}
}
};
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser)
{
if (fromUser)
@ -294,4 +297,4 @@ public class FullPlaybackActivity extends PlaybackActivity implements SeekBar.On
{
mSeekBarTracking = false;
}
}
}

View File

@ -27,15 +27,12 @@ import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.widget.ImageView;
/**
* Playback activity that displays itself like a dialog, i.e. as a small window
* with a border. Includes a CoverView and control buttons.
*/
public class MiniPlaybackActivity extends PlaybackActivity implements View.OnClickListener {
private ImageView mPlayPauseButton;
@Override
public void onCreate(Bundle state)
{
@ -45,6 +42,7 @@ public class MiniPlaybackActivity extends PlaybackActivity implements View.OnCli
setContentView(R.layout.mini_playback);
mCoverView = (CoverView)findViewById(R.id.cover_view);
mCoverView.mHandler = mHandler;
View openButton = findViewById(R.id.open_button);
openButton.setOnClickListener(this);
@ -52,21 +50,12 @@ public class MiniPlaybackActivity extends PlaybackActivity implements View.OnCli
killButton.setOnClickListener(this);
View previousButton = findViewById(R.id.previous);
previousButton.setOnClickListener(this);
mPlayPauseButton = (ImageView)findViewById(R.id.play_pause);
mPlayPauseButton = (ControlButton)findViewById(R.id.play_pause);
mPlayPauseButton.setOnClickListener(this);
View nextButton = findViewById(R.id.next);
nextButton.setOnClickListener(this);
}
@Override
protected void setState(int state)
{
if ((state & PlaybackService.FLAG_NO_MEDIA) != 0)
finish();
mPlayPauseButton.setImageResource((state & PlaybackService.FLAG_PLAYING) == 0 ? R.drawable.play : R.drawable.pause);
}
@Override
public void onClick(View view)
{

View File

@ -22,6 +22,8 @@ import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.view.KeyEvent;
import android.view.Menu;
@ -30,9 +32,11 @@ import android.view.View;
import android.widget.Toast;
public class PlaybackActivity extends Activity implements Handler.Callback, View.OnClickListener {
Handler mHandler = new Handler(this);
Handler mHandler;
Looper mLooper;
CoverView mCoverView;
ControlButton mPlayPauseButton;
int mState;
@Override
@ -40,6 +44,12 @@ public class PlaybackActivity extends Activity implements Handler.Callback, View
{
super.onCreate(state);
ContextApplication.addActivity(this);
HandlerThread thread = new HandlerThread(getClass().getName());
thread.start();
mLooper = thread.getLooper();
mHandler = new Handler(mLooper, this);
}
@Override
@ -47,6 +57,7 @@ public class PlaybackActivity extends Activity implements Handler.Callback, View
{
super.onDestroy();
ContextApplication.removeActivity(this);
mLooper.quit();
}
@Override
@ -55,6 +66,8 @@ public class PlaybackActivity extends Activity implements Handler.Callback, View
super.onStart();
startService(new Intent(this, PlaybackService.class));
if (ContextApplication.service != null)
onServiceReady();
}
public static boolean handleKeyLongPress(int keyCode)
@ -78,30 +91,43 @@ public class PlaybackActivity extends Activity implements Handler.Callback, View
{
switch (view.getId()) {
case R.id.next:
mHandler.sendMessage(mHandler.obtainMessage(MSG_SET_SONG, 1, 0));
if (mCoverView != null)
mCoverView.go(1);
mHandler.sendMessage(mHandler.obtainMessage(MSG_SET_SONG, 1, 0));
break;
case R.id.play_pause:
mHandler.sendMessage(mHandler.obtainMessage(MSG_TOGGLE_FLAG, PlaybackService.FLAG_PLAYING, 0));
break;
case R.id.previous:
mHandler.sendMessage(mHandler.obtainMessage(MSG_SET_SONG, -1, 0));
if (mCoverView != null)
mCoverView.go(-1);
mHandler.sendMessage(mHandler.obtainMessage(MSG_SET_SONG, -1, 0));
break;
}
}
/**
* Sets <code>mState</code> to <code>state</code>. Override to implement
* further behavior in subclasses.
* Updates <code>mState</code> and the play/pause button. Override to
* implement further behavior in subclasses.
*
* @param state PlaybackService state
*/
protected void setState(int state)
{
if (mState == state)
return;
mState = state;
if (mPlayPauseButton != null) {
final int res = (mState & PlaybackService.FLAG_PLAYING) == 0 ? R.drawable.play : R.drawable.pause;
runOnUiThread(new Runnable() {
public void run()
{
mPlayPauseButton.setImageResource(res);
}
});
}
}
/**
@ -112,7 +138,6 @@ public class PlaybackActivity extends Activity implements Handler.Callback, View
{
if (mCoverView != null)
mCoverView.initialize();
setState(ContextApplication.service.getState());
}

View File

@ -27,7 +27,6 @@ import android.content.res.Resources;
import android.database.ContentObserver;
import android.graphics.Color;
import android.graphics.drawable.PaintDrawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Message;
import android.preference.PreferenceManager;
@ -60,8 +59,7 @@ public class SongSelector extends PlaybackActivity implements AdapterView.OnItem
private View mClearButton;
private View mStatus;
private TextView mStatusText;
private ControlButton mPlayPauseButton;
TextView mStatusText;
private ViewGroup mLimiterViews;
@ -76,14 +74,6 @@ public class SongSelector extends PlaybackActivity implements AdapterView.OnItem
return (MediaAdapter)list.getAdapter();
}
private void initializeList(int id, Uri store, String[] fields, String[] fieldKeys)
{
ListView view = (ListView)findViewById(id);
view.setOnItemClickListener(this);
view.setOnCreateContextMenuListener(this);
view.setAdapter(new MediaAdapter(this, store, fields, fieldKeys, true));
}
@Override
public void onCreate(Bundle state)
{
@ -133,6 +123,11 @@ public class SongSelector extends PlaybackActivity implements AdapterView.OnItem
mPlayPauseButton.setOnClickListener(this);
next.setOnClickListener(this);
if (ContextApplication.service != null) {
setState(ContextApplication.service.getState());
onSongChange(ContextApplication.service.getSong(0));
}
} else if (!status && mStatus != null) {
mStatus.setVisibility(View.GONE);
mStatus = null;
@ -376,6 +371,25 @@ public class SongSelector extends PlaybackActivity implements AdapterView.OnItem
getAdapter(i).filter(text, null);
}
/**
* Hook up a ListView to this Activity and the supplied adapter
*
* @param id The id of the ListView
* @param adapter The adapter to be used
*/
private void setupView(int id, final MediaAdapter adapter)
{
final ListView view = (ListView)findViewById(id);
view.setOnItemClickListener(this);
view.setOnCreateContextMenuListener(this);
runOnUiThread(new Runnable() {
public void run()
{
view.setAdapter(adapter);
}
});
}
private static final int MSG_INIT = 10;
@Override
@ -383,13 +397,9 @@ public class SongSelector extends PlaybackActivity implements AdapterView.OnItem
{
switch (message.what) {
case MSG_INIT:
initializeList(R.id.artist_list, MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI, MediaAdapter.ARTIST_FIELDS, MediaAdapter.ARTIST_FIELD_KEYS);
initializeList(R.id.album_list, MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, MediaAdapter.ALBUM_FIELDS, MediaAdapter.ALBUM_FIELD_KEYS);
ListView view = (ListView)findViewById(R.id.song_list);
view.setOnItemClickListener(SongSelector.this);
view.setOnCreateContextMenuListener(SongSelector.this);
view.setAdapter(new SongMediaAdapter(this));
setupView(R.id.artist_list, new MediaAdapter(this, MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI, MediaAdapter.ARTIST_FIELDS, MediaAdapter.ARTIST_FIELD_KEYS, true));
setupView(R.id.album_list, new MediaAdapter(this, MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, MediaAdapter.ALBUM_FIELDS, MediaAdapter.ALBUM_FIELD_KEYS, true));
setupView(R.id.song_list, new SongMediaAdapter(this));
ContentResolver resolver = getContentResolver();
resolver.registerContentObserver(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, true, mObserver);
@ -421,10 +431,16 @@ public class SongSelector extends PlaybackActivity implements AdapterView.OnItem
/**
* Call to update the status text for a newly-playing song.
*/
private void onSongChange(Song song)
private void onSongChange(final Song song)
{
if (mStatusText != null)
mStatusText.setText(song == null ? getResources().getText(R.string.none) : song.title);
if (mStatusText != null) {
runOnUiThread(new Runnable() {
public void run()
{
mStatusText.setText(song == null ? getResources().getText(R.string.none) : song.title);
}
});
}
}
@Override
@ -435,13 +451,4 @@ public class SongSelector extends PlaybackActivity implements AdapterView.OnItem
onSongChange(song);
}
}
@Override
protected void setState(int state)
{
if (mPlayPauseButton != null) {
boolean playing = (state & PlaybackService.FLAG_PLAYING) != 0;
mPlayPauseButton.setImageResource(playing ? R.drawable.pause : R.drawable.play);
}
}
}