/*
TunesRemote+ - http://code.google.com/p/tunesremote-plus/
Copyright (C) 2008 Jeffrey Sharkey, http://jsharkey.org/
Copyright (C) 2010 TunesRemote+, http://code.google.com/p/tunesremote-plus/
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
The Initial Developer of the Original Code is Jeffrey Sharkey.
Portions created by Jeffrey Sharkey are
Copyright (C) 2008. Jeffrey Sharkey, http://jsharkey.org/
All Rights Reserved.
*/
package org.tunesremote;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.tunesremote.daap.Session;
import org.tunesremote.daap.Status;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.graphics.Color;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Vibrator;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MenuItem.OnMenuItemClickListener;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup.LayoutParams;
import android.view.animation.AnimationUtils;
import android.widget.ImageButton;
import android.widget.ImageSwitcher;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.RatingBar;
import android.widget.RatingBar.OnRatingBarChangeListener;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.ViewSwitcher.ViewFactory;
/**
* Main activity of TunesRemote. This controls the player and drives all the
* other activities.
* <p>
*/
public class ControlActivity extends Activity implements ViewFactory {
public final static String TAG = ControlActivity.class.toString();
protected static BackendService backend;
protected static Session session;
protected static Status status;
protected boolean dragging = false;
protected String showingAlbumId = null;
public ServiceConnection connection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder service) {
backend = ((BackendService.BackendBinder) service).getService();
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(ControlActivity.this);
backend.setPrefs(settings);
if (!agreed)
return;
Log.w(TAG, "onServiceConnected");
session = backend.getSession();
if (session == null) {
// we couldnt connect with library, so launch picker
ControlActivity.this.startActivityForResult(new Intent(ControlActivity.this, LibraryActivity.class), 1);
} else {
// for some reason we arent correctly disposing of the session
// threads we create so we purge any existing ones before creating a
// new one
status = session.singletonStatus(statusUpdate);
status.updateHandler(statusUpdate);
// push update through to make sure we get updated
statusUpdate.sendEmptyMessage(Status.UPDATE_TRACK);
}
}
public void onServiceDisconnected(ComponentName className) {
// make sure we clean up our handler-specific status
Log.w(TAG, "onServiceDisconnected");
status.updateHandler(null);
backend = null;
status = null;
}
};
protected Handler statusUpdate = new Handler() {
@Override
public void handleMessage(Message msg) {
// update gui based on severity
switch (msg.what) {
case Status.UPDATE_TRACK:
trackName.setText(status.getTrackName());
trackArtist.setText(status.getTrackArtist());
trackAlbum.setText(status.getTrackAlbum());
// Set to invisible until rating is known
ratingBar.setVisibility(RatingBar.INVISIBLE);
// fade new details up if requested
if (fadeUpNew)
fadeview.keepAwake();
case Status.UPDATE_COVER:
boolean forced = (msg.what == Status.UPDATE_COVER);
boolean shouldUpdate = (status.albumId != showingAlbumId) && !status.coverEmpty;
if (forced)
shouldUpdate = true;
// only update coverart if different than already shown
Log.d(TAG, String.format("Artwork for albumid=%s, value=%s, what=%d", status.albumId, status.coverCache,
msg.what));
if (shouldUpdate) {
if (status.coverEmpty) {
// fade down if no coverart
cover.setImageDrawable(new ColorDrawable(Color.BLACK));
} else if (status.coverCache != null) {
// fade over to new coverart
cover.setImageDrawable(new BitmapDrawable(status.coverCache));
}
showingAlbumId = status.albumId;
}
case Status.UPDATE_STATE:
controlPause
.setImageResource((status.getPlayStatus() == Status.STATE_PLAYING) ? android.R.drawable.ic_media_pause
: android.R.drawable.ic_media_play);
seekBar.setMax(status.getProgressTotal());
// TODO: update menu items for shuffle, etc?
case Status.UPDATE_PROGRESS:
if (ignoreNextTick) {
ignoreNextTick = false;
return;
}
seekPosition.setText(formatTime(status.getProgress()));
seekRemain.setText("-" + formatTime(status.getRemaining()));
if (!dragging) {
seekBar.setProgress(status.getProgress());
}
break;
// This one is triggered by a thread, so should not be used to update
// progress, etc...
case Status.UPDATE_RATING:
long rating = status.getRating();
if (rating >= 0) {
ratingBar.setRating(((float) status.getRating() / 100) * 5);
ratingBar.setVisibility(RatingBar.VISIBLE);
// fade new details up if requested
if (fadeUpNew)
fadeview.keepAwake();
}
break;
}
}
};
public android.telephony.PhoneStateListener psListener = new android.telephony.PhoneStateListener() {
private boolean wasPlaying = false;
@Override
public void onCallStateChanged(int state, java.lang.String incomingNumber) {
if (ControlActivity.this.autoPause) {
switch (state) {
case android.telephony.TelephonyManager.CALL_STATE_IDLE:
if (wasPlaying && session != null && ControlActivity.status.getPlayStatus() == Status.STATE_PAUSED) {
session.controlPlayPause();
wasPlaying = false;
}
break;
case android.telephony.TelephonyManager.CALL_STATE_OFFHOOK:
if (session != null && ControlActivity.status.getPlayStatus() == Status.STATE_PLAYING) {
session.controlPlayPause();
wasPlaying = true;
}
break;
case android.telephony.TelephonyManager.CALL_STATE_RINGING:
if (session != null && ControlActivity.status.getPlayStatus() == Status.STATE_PLAYING) {
session.controlPlayPause();
wasPlaying = true;
}
break;
default:
break;
}
}
}
};
protected void StartNowPlaying() {
if (status == null)
return;
// launch tracks view for current album
Intent intent = new Intent(ControlActivity.this, NowPlayingActivity.class);
intent.putExtra(Intent.EXTRA_TITLE, status.getAlbumId());
ControlActivity.this.startActivity(intent);
}
protected Handler doubleTapHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
StartNowPlaying();
}
};
protected RatingBar ratingBar;
protected TextView trackName, trackArtist, trackAlbum, seekPosition, seekRemain;
protected SeekBar seekBar;
protected ImageSwitcher cover;
protected ImageButton controlPrev, controlPause, controlNext;
protected SimpleDateFormat format = new SimpleDateFormat("m:ss");
protected Date date = new Date(0);
protected synchronized String formatTime(int seconds) {
date.setTime(seconds * 1000);
return format.format(date);
}
public View makeView() {
ImageView view = new ImageView(this);
view.setScaleType(ImageView.ScaleType.CENTER_CROP);
view.setLayoutParams(new ImageSwitcher.LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
return view;
}
protected View volume;
protected ProgressBar volumeBar;
protected Toast volumeToast;
protected FadeView fadeview;
protected Toast shuffleToast;
protected Toast repeatToast;
protected boolean stayConnected = false, fadeDetails = true, fadeUpNew = true, vibrate = true;
protected boolean autoPause = false;
@Override
public void onStart() {
super.onStart();
this.stayConnected = this.prefs.getBoolean(this.getString(R.string.pref_background), this.stayConnected);
this.fadeDetails = this.prefs.getBoolean(this.getString(R.string.pref_fade), this.fadeDetails);
this.fadeUpNew = this.prefs.getBoolean(this.getString(R.string.pref_fadeupnew), this.fadeUpNew);
this.vibrate = this.prefs.getBoolean(this.getString(R.string.pref_vibrate), this.vibrate);
this.autoPause = this.prefs.getBoolean(this.getString(R.string.pref_autopause), this.autoPause);
this.fadeview.allowFade = this.fadeDetails;
this.fadeview.keepAwake();
Intent service = new Intent(this, BackendService.class);
if (this.stayConnected) {
// if were running background service, start now
this.startService(service);
} else {
// otherwise make sure we kill the static background service
this.stopService(service);
}
// regardless of stayConnected, were always going to need a bound backend
// for this activity
this.bindService(service, connection, Context.BIND_AUTO_CREATE);
android.telephony.TelephonyManager tm = (android.telephony.TelephonyManager) ControlActivity.this
.getSystemService(android.content.Context.TELEPHONY_SERVICE);
tm.listen(psListener, android.telephony.PhoneStateListener.LISTEN_CALL_STATE);
}
@Override
public void onStop() {
super.onStop();
Log.w(TAG, "Stopping TunesRemote...");
try {
if (!this.stayConnected && session != null) {
session.purgeAllStatus();
}
this.unbindService(connection);
} catch (Exception ex) {
Log.e(TAG, ex.getMessage(), ex);
}
}
@Override
protected void onDestroy() {
super.onDestroy();
Log.w(TAG, "Destroying TunesRemote...");
try {
if (session != null) {
session.purgeAllStatus();
session.logout();
session = null;
}
backend = null;
} catch (Exception ex) {
Log.e(TAG, ex.getMessage(), ex);
}
Log.w(TAG, "Destroyed TunesRemote!");
}
protected Vibrator vibrator;
protected SharedPreferences prefs;
protected boolean agreed = false;
public final static String EULA = "eula";
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == Activity.RESULT_OK) {
// yay they agreed, so store that info
Editor edit = prefs.edit();
edit.putBoolean(EULA, true);
edit.commit();
this.agreed = true;
} else {
// user didnt agree, so close
this.finish();
}
}
public final static int VIBRATE_LEN = 150;
protected boolean ignoreNextTick = false;
// public boolean shouldPause = false;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// before we go any further, make sure theyve agreed to EULA
this.prefs = PreferenceManager.getDefaultSharedPreferences(this);
this.agreed = prefs.getBoolean(EULA, false);
if (!this.agreed) {
// show eula wizard
this.startActivityForResult(new Intent(this, WizardActivity.class), 1);
}
setContentView(R.layout.act_control);
this.vibrator = (Vibrator) this.getSystemService(Context.VIBRATOR_SERVICE);
// prepare volume toast view
LayoutInflater inflater = (LayoutInflater) this.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
this.volume = inflater.inflate(R.layout.toa_volume, null, false);
this.volumeBar = (ProgressBar) this.volume.findViewById(R.id.volume);
this.volumeToast = new Toast(this);
this.volumeToast.setDuration(Toast.LENGTH_SHORT);
this.volumeToast.setGravity(Gravity.CENTER, 0, 0);
this.volumeToast.setView(this.volume);
this.shuffleToast = Toast.makeText(this, R.string.control_menu_shuffle_off, Toast.LENGTH_SHORT);
this.shuffleToast.setGravity(Gravity.CENTER, 0, 0);
this.repeatToast = Toast.makeText(this, R.string.control_menu_repeat_none, Toast.LENGTH_SHORT);
this.repeatToast.setGravity(Gravity.CENTER, 0, 0);
// pull out interesting controls
this.trackName = (TextView) findViewById(R.id.info_title);
this.trackArtist = (TextView) findViewById(R.id.info_artist);
this.trackAlbum = (TextView) findViewById(R.id.info_album);
this.ratingBar = (RatingBar) findViewById(R.id.rating_bar);
this.cover = (ImageSwitcher) findViewById(R.id.cover);
this.cover.setFactory(this);
this.cover.setInAnimation(AnimationUtils.loadAnimation(this, android.R.anim.fade_in));
this.cover.setOutAnimation(AnimationUtils.loadAnimation(this, android.R.anim.fade_out));
this.seekBar = (SeekBar) findViewById(R.id.seek);
this.seekPosition = (TextView) findViewById(R.id.seek_position);
this.seekRemain = (TextView) findViewById(R.id.seek_remain);
this.controlPrev = (ImageButton) findViewById(R.id.control_prev);
this.controlPause = (ImageButton) findViewById(R.id.control_pause);
this.controlNext = (ImageButton) findViewById(R.id.control_next);
this.seekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromTouch) {
}
public void onStartTrackingTouch(SeekBar seekBar) {
dragging = true;
}
public void onStopTrackingTouch(SeekBar seekBar) {
// scan to location in song
dragging = false;
session.controlProgress(seekBar.getProgress());
ignoreNextTick = true;
if (vibrate)
vibrator.vibrate(VIBRATE_LEN);
}
});
this.controlPrev.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
session.controlPrev();
if (vibrate)
vibrator.vibrate(VIBRATE_LEN);
}
});
this.controlNext.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
session.controlNext();
if (vibrate)
vibrator.vibrate(VIBRATE_LEN);
}
});
this.controlPause.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
session.controlPlayPause();
if (vibrate)
vibrator.vibrate(VIBRATE_LEN);
}
});
this.fadeview = (FadeView) findViewById(R.id.fadeview);
this.fadeview.startFade();
this.fadeview.doubleTapHandler = this.doubleTapHandler;
// cover.setImageDrawable(this.getResources().getDrawable(R.drawable.folder));
this.ratingBar.setOnRatingBarChangeListener(new OnRatingBarChangeListener() {
public void onRatingChanged(RatingBar ratingBar, float rating, boolean fromUser) {
// TODO Auto-generated method stub
if (fromUser && rating <= 5) {
session.controlRating((long) ((rating / 5) * 100));
}
}
});
}
protected long cachedTime = -1;
protected long cachedVolume = -1;
// keep volume cache for 10 seconds
public final static long CACHE_TIME = 10000;
protected void incrementVolume(long increment) {
// try assuming a cached volume instead of requesting it each time
if (System.currentTimeMillis() - cachedTime > CACHE_TIME) {
this.cachedVolume = status.getVolume();
this.cachedTime = System.currentTimeMillis();
}
// increment the volume and send control signal off
this.cachedVolume += increment;
session.controlVolume(this.cachedVolume);
// update our volume gui and show
this.volumeBar.setProgress((int) this.cachedVolume);
this.volume.invalidate();
this.volumeToast.show();
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
final int increment = Integer.parseInt(backend.getPrefs().getString(
getResources().getString(R.string.pref_volumeincrement), "5"));
// check for volume keys
if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
this.incrementVolume(+increment);
return true;
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
this.incrementVolume(-increment);
return true;
}
// regardless of key, make sure we keep view alive
this.fadeview.keepAwake();
return super.onKeyDown(keyCode, event);
}
// get rid of volume rocker default sound effect
@Override
public boolean onKeyUp(int keycode, KeyEvent event) {
switch (keycode) {
case KeyEvent.KEYCODE_VOLUME_DOWN:
case KeyEvent.KEYCODE_VOLUME_UP:
break;
default:
return super.onKeyUp(keycode, event);
}
return true;
}
protected MenuItem repeat, shuffle;
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
MenuItem search = menu.add(R.string.control_menu_search);
search.setIcon(android.R.drawable.ic_menu_search);
search.setOnMenuItemClickListener(new OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
ControlActivity.this.onSearchRequested();
return true;
}
});
MenuItem artists = menu.add(R.string.control_menu_artists);
artists.setIcon(R.drawable.ic_search_category_music_artist);
Intent artistIntent = new Intent(ControlActivity.this, BrowseActivity.class);
artistIntent.putExtra("windowType", BaseBrowseActivity.RESULT_SWITCH_TO_ARTISTS);
artists.setIntent(artistIntent);
MenuItem playlists = menu.add("Playlists");
playlists.setIcon(R.drawable.ic_search_category_music_song);
Intent playlistIntent = new Intent(ControlActivity.this, BrowseActivity.class);
playlistIntent.putExtra("windowType", BaseBrowseActivity.RESULT_SWITCH_TO_PLAYLISTS);
playlists.setIntent(playlistIntent);
this.repeat = menu.add("Repeat");
this.repeat.setIcon(android.R.drawable.ic_menu_rotate);
this.repeat.setOnMenuItemClickListener(new OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
if (session == null || status == null)
return true;
// correctly rotate through states
switch (status.getRepeat()) {
case Status.REPEAT_ALL:
session.controlRepeat(Status.REPEAT_OFF);
repeatToast.setText(R.string.control_menu_repeat_none);
break;
case Status.REPEAT_OFF:
session.controlRepeat(Status.REPEAT_SINGLE);
repeatToast.setText(R.string.control_menu_repeat_one);
break;
case Status.REPEAT_SINGLE:
session.controlRepeat(Status.REPEAT_ALL);
repeatToast.setText(R.string.control_menu_repeat_all);
break;
}
repeatToast.show();
return true;
}
});
this.shuffle = menu.add("Shuffle");
this.shuffle.setIcon(R.drawable.ic_menu_shuffle);
this.shuffle.setOnMenuItemClickListener(new OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
if (session == null || status == null)
return true;
// correctly rotate through states
switch (status.getShuffle()) {
case Status.SHUFFLE_OFF:
session.controlShuffle(Status.SHUFFLE_ON);
shuffleToast.setText(R.string.control_menu_shuffle_on);
break;
case Status.SHUFFLE_ON:
session.controlShuffle(Status.SHUFFLE_OFF);
shuffleToast.setText(R.string.control_menu_shuffle_off);
break;
}
shuffleToast.show();
return true;
}
});
MenuItem pickLibrary = menu.add(R.string.control_menu_addlibrary);
pickLibrary.setIcon(android.R.drawable.ic_menu_add);
pickLibrary.setOnMenuItemClickListener(new OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
// launch off library picking
ControlActivity.this.startActivity(new Intent(ControlActivity.this, LibraryActivity.class));
return true;
}
});
MenuItem addLibrary = menu.add(R.string.control_menu_picklibrary);
addLibrary.setIcon(android.R.drawable.ic_menu_share);
addLibrary.setOnMenuItemClickListener(new OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
// launch off library picking
ControlActivity.this.startActivity(new Intent(ControlActivity.this, ServerActivity.class));
return true;
}
});
MenuItem nowPlaying = menu.add(R.string.control_menu_nowplaying);
nowPlaying.setOnMenuItemClickListener(new OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
// launch now playing
StartNowPlaying();
return true;
}
});
MenuItem settings = menu.add("Settings");
settings.setIcon(android.R.drawable.ic_menu_preferences);
settings.setIntent(new Intent(ControlActivity.this, PrefsActivity.class));
MenuItem about = menu.add("About");
settings.setIcon(android.R.drawable.ic_menu_help);
about.setIntent(new Intent(ControlActivity.this, WizardActivity.class));
return true;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
if (session == null || status == null)
return true;
switch (status.getRepeat()) {
case Status.REPEAT_ALL:
this.repeat.setTitle(R.string.control_menu_repeat_none);
break;
case Status.REPEAT_OFF:
this.repeat.setTitle(R.string.control_menu_repeat_one);
break;
case Status.REPEAT_SINGLE:
this.repeat.setTitle(R.string.control_menu_repeat_all);
break;
}
switch (status.getShuffle()) {
case Status.SHUFFLE_OFF:
this.shuffle.setTitle(R.string.control_menu_shuffle_on);
break;
case Status.SHUFFLE_ON:
this.shuffle.setTitle(R.string.control_menu_shuffle_off);
break;
}
return true;
}
}
|