From 1404616b359bb9b54ba84f871a7a62ed63c45470 Mon Sep 17 00:00:00 2001 From: Joshua Bahnsen Date: Sun, 10 Feb 2013 01:13:20 -0700 Subject: [PATCH] Use StreamProxy for local playback (no stutter!) Use SeekBar instead of custom HorizonalSlider for seeking --- AndroidManifest.xml | 4 +- res/layout/download_slider.xml | 3 +- .../androidapp/activity/DownloadActivity.java | 1685 +++++++++-------- .../service/DownloadServiceImpl.java | 33 +- .../androidapp/util/HorizontalSlider.java | 141 -- .../subsonic/androidapp/util/StreamProxy.java | 243 +++ 6 files changed, 1123 insertions(+), 986 deletions(-) delete mode 100644 src/net/sourceforge/subsonic/androidapp/util/HorizontalSlider.java create mode 100644 src/net/sourceforge/subsonic/androidapp/util/StreamProxy.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index db2c3e28..28e0afa5 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1,8 +1,8 @@ + a:versionCode="58" + a:versionName="3.9.9.17" a:installLocation="auto"> diff --git a/res/layout/download_slider.xml b/res/layout/download_slider.xml index 66d6fc51..0af08195 100644 --- a/res/layout/download_slider.xml +++ b/res/layout/download_slider.xml @@ -4,10 +4,9 @@ android:layout_height="wrap_content" android:orientation="vertical" > - . - - Copyright 2009 (C) Sindre Mehus - */ -package net.sourceforge.subsonic.androidapp.activity; - -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.LinkedList; -import java.util.List; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; - -import android.app.AlertDialog; -import android.app.Dialog; -import android.content.DialogInterface; -import android.content.Intent; -import android.os.Bundle; -import android.os.Handler; -import android.view.ContextMenu; -import android.view.Display; -import android.view.GestureDetector; -import android.view.GestureDetector.OnGestureListener; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.view.WindowManager; -import android.view.animation.AnimationUtils; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.EditText; -import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.ListView; -import android.widget.TextView; -import android.widget.ViewFlipper; -import net.sourceforge.subsonic.androidapp.R; -import net.sourceforge.subsonic.androidapp.domain.MusicDirectory; -import net.sourceforge.subsonic.androidapp.domain.PlayerState; -import net.sourceforge.subsonic.androidapp.domain.RepeatMode; -import net.sourceforge.subsonic.androidapp.service.DownloadFile; -import net.sourceforge.subsonic.androidapp.service.DownloadService; -import net.sourceforge.subsonic.androidapp.service.MusicService; -import net.sourceforge.subsonic.androidapp.service.MusicServiceFactory; -import net.sourceforge.subsonic.androidapp.util.Constants; -import net.sourceforge.subsonic.androidapp.util.HorizontalSlider; -import net.sourceforge.subsonic.androidapp.util.SilentBackgroundTask; -import net.sourceforge.subsonic.androidapp.util.SongView; -import net.sourceforge.subsonic.androidapp.util.Util; -import net.sourceforge.subsonic.androidapp.view.VisualizerView; - -import static net.sourceforge.subsonic.androidapp.domain.PlayerState.*; - -public class DownloadActivity extends SubsonicTabActivity implements OnGestureListener { - - private static final int DIALOG_SAVE_PLAYLIST = 100; - private static final int PERCENTAGE_OF_SCREEN_FOR_SWIPE = 5; - - private ViewFlipper playlistFlipper; - private ViewFlipper buttonBarFlipper; - private TextView emptyTextView; - private TextView songTitleTextView; - private TextView albumTextView; - private TextView artistTextView; - private ImageView albumArtImageView; - private ListView playlistView; - private TextView positionTextView; - private TextView durationTextView; - private TextView statusTextView; - private HorizontalSlider progressBar; - private View previousButton; - private View nextButton; - private View pauseButton; - private View stopButton; - private View startButton; - private View shuffleButton; - private ImageButton repeatButton; - private MenuItem equalizerMenuItem; - private MenuItem visualizerMenuItem; - private View toggleListButton; - private ScheduledExecutorService executorService; - private DownloadFile currentPlaying; - private long currentRevision; - private EditText playlistNameView; - private GestureDetector gestureScanner; - private int swipeDistance; - private int swipeVelocity; - private VisualizerView visualizerView; - private boolean visualizerAvailable; - private boolean equalizerAvailable; - - /** - * Called when the activity is first created. - */ - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.download); - - WindowManager w = getWindowManager(); - Display d = w.getDefaultDisplay(); - swipeDistance = (d.getWidth() + d.getHeight()) * PERCENTAGE_OF_SCREEN_FOR_SWIPE / 100; - swipeVelocity = (d.getWidth() + d.getHeight()) * PERCENTAGE_OF_SCREEN_FOR_SWIPE / 100; - gestureScanner = new GestureDetector(this); - - playlistFlipper = (ViewFlipper) findViewById(R.id.download_playlist_flipper); - buttonBarFlipper = (ViewFlipper) findViewById(R.id.download_button_bar_flipper); - emptyTextView = (TextView) findViewById(R.id.download_empty); - songTitleTextView = (TextView) findViewById(R.id.download_song_title); - albumTextView = (TextView) findViewById(R.id.download_album); - artistTextView = (TextView) findViewById(R.id.download_artist); - albumArtImageView = (ImageView) findViewById(R.id.download_album_art_image); - positionTextView = (TextView) findViewById(R.id.download_position); - durationTextView = (TextView) findViewById(R.id.download_duration); - statusTextView = (TextView) findViewById(R.id.download_status); - progressBar = (HorizontalSlider) findViewById(R.id.download_progress_bar); - playlistView = (ListView) findViewById(R.id.download_list); - previousButton = findViewById(R.id.download_previous); - nextButton = findViewById(R.id.download_next); - pauseButton = findViewById(R.id.download_pause); - stopButton = findViewById(R.id.download_stop); - startButton = findViewById(R.id.download_start); - shuffleButton = findViewById(R.id.download_shuffle); - repeatButton = (ImageButton) findViewById(R.id.download_repeat); - LinearLayout visualizerViewLayout = (LinearLayout) findViewById(R.id.download_visualizer_view_layout); - - toggleListButton = findViewById(R.id.download_toggle_list); - - View.OnTouchListener touchListener = new View.OnTouchListener() { - @Override - public boolean onTouch(View v, MotionEvent me) { - return gestureScanner.onTouchEvent(me); - } - }; - previousButton.setOnTouchListener(touchListener); - nextButton.setOnTouchListener(touchListener); - pauseButton.setOnTouchListener(touchListener); - stopButton.setOnTouchListener(touchListener); - startButton.setOnTouchListener(touchListener); - buttonBarFlipper.setOnTouchListener(touchListener); - emptyTextView.setOnTouchListener(touchListener); - albumArtImageView.setOnTouchListener(touchListener); - - albumArtImageView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - toggleFullscreenAlbumArt(); - } - }); - - previousButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - warnIfNetworkOrStorageUnavailable(); - getDownloadService().previous(); - onCurrentChanged(); - onProgressChanged(); - } - }); - - nextButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - warnIfNetworkOrStorageUnavailable(); - if (getDownloadService().getCurrentPlayingIndex() < getDownloadService().size() - 1) { - getDownloadService().next(); - onCurrentChanged(); - onProgressChanged(); - } - } - }); - - pauseButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - getDownloadService().pause(); - onCurrentChanged(); - onProgressChanged(); - } - }); - - stopButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - getDownloadService().reset(); - onCurrentChanged(); - onProgressChanged(); - } - }); - - startButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - warnIfNetworkOrStorageUnavailable(); - start(); - onCurrentChanged(); - onProgressChanged(); - } - }); - - shuffleButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - getDownloadService().shuffle(); - Util.toast(DownloadActivity.this, R.string.download_menu_shuffle_notification); - } - }); - - repeatButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - RepeatMode repeatMode = getDownloadService().getRepeatMode().next(); - getDownloadService().setRepeatMode(repeatMode); - onDownloadListChanged(); - switch (repeatMode) { - case OFF: - Util.toast(DownloadActivity.this, R.string.download_repeat_off); - break; - case ALL: - Util.toast(DownloadActivity.this, R.string.download_repeat_all); - break; - case SINGLE: - Util.toast(DownloadActivity.this, R.string.download_repeat_single); - break; - default: - break; - } - } - }); - - toggleListButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - toggleFullscreenAlbumArt(); - } - }); - - progressBar.setOnSliderChangeListener(new HorizontalSlider.OnSliderChangeListener() { - @Override - public void onSliderChanged(View view, int position, boolean inProgress) { - Util.toast(DownloadActivity.this, Util.formatDuration(position / 1000), true); - if (!inProgress) { - getDownloadService().seekTo(position); - onProgressChanged(); - } - } - }); - playlistView.setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - warnIfNetworkOrStorageUnavailable(); - getDownloadService().play(position); - onCurrentChanged(); - onProgressChanged(); - } - }); - - playlistView.setOnTouchListener(gestureListener); - - registerForContextMenu(playlistView); - - DownloadService downloadService = getDownloadService(); - if (downloadService != null && getIntent().getBooleanExtra(Constants.INTENT_EXTRA_NAME_SHUFFLE, false)) { - warnIfNetworkOrStorageUnavailable(); - downloadService.setShufflePlayEnabled(true); - } - - visualizerAvailable = downloadService != null && downloadService.getVisualizerController() != null; - equalizerAvailable = downloadService != null && downloadService.getEqualizerController() != null; - - if (visualizerAvailable) { - visualizerView = new VisualizerView(this); - visualizerViewLayout.addView(visualizerView, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT)); - - visualizerView.setOnTouchListener(new View.OnTouchListener() { - @Override - public boolean onTouch(View view, MotionEvent motionEvent) { - visualizerView.setActive(!visualizerView.isActive()); - getDownloadService().setShowVisualization(visualizerView.isActive()); - //updateButtons(); - return true; - } - }); - } - } - - @Override - protected void onResume() { - super.onResume(); - - final Handler handler = new Handler(); - Runnable runnable = new Runnable() { - @Override - public void run() { - handler.post(new Runnable() { - @Override - public void run() { - update(); - } - }); - } - }; - - executorService = Executors.newSingleThreadScheduledExecutor(); - executorService.scheduleWithFixedDelay(runnable, 0L, 1000L, TimeUnit.MILLISECONDS); - - DownloadService downloadService = getDownloadService(); - if (downloadService == null || downloadService.getCurrentPlaying() == null) { - playlistFlipper.setDisplayedChild(1); - buttonBarFlipper.setDisplayedChild(1); - } - - onDownloadListChanged(); - onCurrentChanged(); - onProgressChanged(); - scrollToCurrent(); - if (downloadService != null && downloadService.getKeepScreenOn()) { - getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } else { - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } - - if (visualizerView != null) { - visualizerView.setActive(downloadService != null && downloadService.getShowVisualization()); - } - } - - // Scroll to current playing/downloading. - private void scrollToCurrent() { - if (getDownloadService() == null) { - return; - } - - for (int i = 0; i < playlistView.getAdapter().getCount(); i++) { - if (currentPlaying == playlistView.getItemAtPosition(i)) { - playlistView.setSelectionFromTop(i, 40); - return; - } - } - DownloadFile currentDownloading = getDownloadService().getCurrentDownloading(); - for (int i = 0; i < playlistView.getAdapter().getCount(); i++) { - if (currentDownloading == playlistView.getItemAtPosition(i)) { - playlistView.setSelectionFromTop(i, 40); - return; - } - } - } - - @Override - protected void onPause() { - super.onPause(); - executorService.shutdown(); - if (visualizerView != null) { - visualizerView.setActive(false); - } - } - - @Override - protected Dialog onCreateDialog(int id) { - if (id == DIALOG_SAVE_PLAYLIST) { - AlertDialog.Builder builder; - - LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE); - final View layout = inflater.inflate(R.layout.save_playlist, (ViewGroup) findViewById(R.id.save_playlist_root)); - playlistNameView = (EditText) layout.findViewById(R.id.save_playlist_name); - - builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.download_playlist_title); - builder.setMessage(R.string.download_playlist_name); - builder.setPositiveButton(R.string.common_save, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int id) { - savePlaylistInBackground(String.valueOf(playlistNameView.getText())); - } - }); - builder.setNegativeButton(R.string.common_cancel, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int id) { - dialog.cancel(); - } - }); - builder.setView(layout); - builder.setCancelable(true); - - return builder.create(); - } else { - return super.onCreateDialog(id); - } - } - - @Override - protected void onPrepareDialog(int id, Dialog dialog) { - if (id == DIALOG_SAVE_PLAYLIST) { - String playlistName = getDownloadService().getSuggestedPlaylistName(); - if (playlistName != null) { - playlistNameView.setText(playlistName); - } else { - DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); - playlistNameView.setText(dateFormat.format(new Date())); - } - } - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.nowplaying, menu); - super.onCreateOptionsMenu(menu); - return true; - } - - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - MenuItem savePlaylist = menu.findItem(R.id.menu_save_playlist); - boolean savePlaylistEnabled = !Util.isOffline(this); - savePlaylist.setEnabled(savePlaylistEnabled); - savePlaylist.setVisible(savePlaylistEnabled); - MenuItem screenOption = menu.findItem(R.id.menu_screen_on_off); - equalizerMenuItem = menu.findItem(R.id.download_equalizer); - visualizerMenuItem = menu.findItem(R.id.download_visualizer); - - equalizerMenuItem.setEnabled(equalizerAvailable); - equalizerMenuItem.setVisible(equalizerAvailable); - visualizerMenuItem.setEnabled(visualizerAvailable); - visualizerMenuItem.setVisible(visualizerAvailable); - - DownloadService downloadService = getDownloadService(); - - if (downloadService != null) { - if (getDownloadService().getKeepScreenOn()) { - screenOption.setTitle(R.string.download_menu_screen_off); - } else { - screenOption.setTitle(R.string.download_menu_screen_on); - } - } - return super.onPrepareOptionsMenu(menu); - } - - @Override - public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) { - super.onCreateContextMenu(menu, view, menuInfo); - if (view == playlistView) { - AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo; - DownloadFile downloadFile = (DownloadFile) playlistView.getItemAtPosition(info.position); - - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.nowplaying_context, menu); - - if (downloadFile.getSong().getParent() == null) { - menu.findItem(R.id.menu_show_album).setVisible(false); - } - if (Util.isOffline(this)) { - menu.findItem(R.id.menu_lyrics).setVisible(false); - menu.findItem(R.id.menu_save_playlist).setVisible(false); - } - } - } - - @Override - public boolean onContextItemSelected(MenuItem menuItem) { - AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo(); - DownloadFile downloadFile = (DownloadFile) playlistView.getItemAtPosition(info.position); - return menuItemSelected(menuItem.getItemId(), downloadFile) || super.onContextItemSelected(menuItem); - } - - @Override - public boolean onOptionsItemSelected(MenuItem menuItem) { - return menuItemSelected(menuItem.getItemId(), null) || super.onOptionsItemSelected(menuItem); - } - - private boolean menuItemSelected(int menuItemId, DownloadFile song) { - switch (menuItemId) { - case R.id.menu_show_album: - Intent intent = new Intent(this, SelectAlbumActivity.class); - intent.putExtra(Constants.INTENT_EXTRA_NAME_ID, song.getSong().getParent()); - intent.putExtra(Constants.INTENT_EXTRA_NAME_NAME, song.getSong().getAlbum()); - Util.startActivityWithoutTransition(this, intent); - return true; - case R.id.menu_lyrics: - intent = new Intent(this, LyricsActivity.class); - intent.putExtra(Constants.INTENT_EXTRA_NAME_ARTIST, song.getSong().getArtist()); - intent.putExtra(Constants.INTENT_EXTRA_NAME_TITLE, song.getSong().getTitle()); - Util.startActivityWithoutTransition(this, intent); - return true; - case R.id.menu_remove: - getDownloadService().remove(song); - onDownloadListChanged(); - return true; - case R.id.menu_remove_all: - getDownloadService().setShufflePlayEnabled(false); - getDownloadService().clear(); - onDownloadListChanged(); - return true; - case R.id.menu_screen_on_off: - if (getDownloadService().getKeepScreenOn()) { - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - getDownloadService().setKeepScreenOn(false); - } else { - getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - getDownloadService().setKeepScreenOn(true); - } - return true; - case R.id.menu_shuffle: - getDownloadService().shuffle(); - Util.toast(this, R.string.download_menu_shuffle_notification); - return true; - case R.id.menu_save_playlist: - showDialog(DIALOG_SAVE_PLAYLIST); - return true; - case R.id.download_equalizer: - startActivity(new Intent(DownloadActivity.this, EqualizerActivity.class)); - return true; - case R.id.download_visualizer: - boolean active = !visualizerView.isActive(); - visualizerView.setActive(active); - getDownloadService().setShowVisualization(visualizerView.isActive()); - Util.toast(DownloadActivity.this, active ? R.string.download_visualizer_on : R.string.download_visualizer_off); - return true; - case R.id.download_jukebox: - boolean jukeboxEnabled = !getDownloadService().isJukeboxEnabled(); - getDownloadService().setJukeboxEnabled(jukeboxEnabled); - Util.toast(DownloadActivity.this, jukeboxEnabled ? R.string.download_jukebox_on : R.string.download_jukebox_off, false); - return true; - default: - return false; - } - } - - private void update() { - if (getDownloadService() == null) { - return; - } - - if (currentRevision != getDownloadService().getDownloadListUpdateRevision()) { - onDownloadListChanged(); - } - - if (currentPlaying != getDownloadService().getCurrentPlaying()) { - onCurrentChanged(); - } - - onProgressChanged(); - } - - private void savePlaylistInBackground(final String playlistName) { - Util.toast(DownloadActivity.this, getResources().getString(R.string.download_playlist_saving, playlistName)); - getDownloadService().setSuggestedPlaylistName(playlistName); - new SilentBackgroundTask(this) { - @Override - protected Void doInBackground() throws Throwable { - List entries = new LinkedList(); - for (DownloadFile downloadFile : getDownloadService().getDownloads()) { - entries.add(downloadFile.getSong()); - } - MusicService musicService = MusicServiceFactory.getMusicService(DownloadActivity.this); - musicService.createPlaylist(null, playlistName, entries, DownloadActivity.this, null); - return null; - } - - @Override - protected void done(Void result) { - Util.toast(DownloadActivity.this, R.string.download_playlist_done); - } - - @Override - protected void error(Throwable error) { - String msg = getResources().getString(R.string.download_playlist_error) + " " + getErrorMessage(error); - Util.toast(DownloadActivity.this, msg); - } - }.execute(); - } - - private void toggleFullscreenAlbumArt() { - scrollToCurrent(); - if (playlistFlipper.getDisplayedChild() == 1) { - playlistFlipper.setInAnimation(AnimationUtils.loadAnimation(this, R.anim.push_down_in)); - playlistFlipper.setOutAnimation(AnimationUtils.loadAnimation(this, R.anim.push_down_out)); - playlistFlipper.setDisplayedChild(0); - buttonBarFlipper.setInAnimation(AnimationUtils.loadAnimation(this, R.anim.push_down_in)); - buttonBarFlipper.setOutAnimation(AnimationUtils.loadAnimation(this, R.anim.push_down_out)); - buttonBarFlipper.setDisplayedChild(0); - - - } else { - playlistFlipper.setInAnimation(AnimationUtils.loadAnimation(this, R.anim.push_up_in)); - playlistFlipper.setOutAnimation(AnimationUtils.loadAnimation(this, R.anim.push_up_out)); - playlistFlipper.setDisplayedChild(1); - buttonBarFlipper.setInAnimation(AnimationUtils.loadAnimation(this, R.anim.push_up_in)); - buttonBarFlipper.setOutAnimation(AnimationUtils.loadAnimation(this, R.anim.push_up_out)); - buttonBarFlipper.setDisplayedChild(1); - } - } - - private void start() { - DownloadService service = getDownloadService(); - PlayerState state = service.getPlayerState(); - if (state == PAUSED || state == COMPLETED) { - service.start(); - } else if (state == STOPPED || state == IDLE) { - warnIfNetworkOrStorageUnavailable(); - int current = service.getCurrentPlayingIndex(); - // TODO: Use play() method. - if (current == -1) { - service.play(0); - } else { - service.play(current); - } - } - } - - private void onDownloadListChanged() { - DownloadService downloadService = getDownloadService(); - if (downloadService == null) { - return; - } - - List list = downloadService.getDownloads(); - - playlistView.setAdapter(new SongListAdapter(list)); - emptyTextView.setVisibility(list.isEmpty() ? View.VISIBLE : View.GONE); - currentRevision = downloadService.getDownloadListUpdateRevision(); - - switch (downloadService.getRepeatMode()) { - case OFF: - repeatButton.setImageResource(R.drawable.media_repeat_off); - break; - case ALL: - repeatButton.setImageResource(R.drawable.media_repeat_all); - break; - case SINGLE: - repeatButton.setImageResource(R.drawable.media_repeat_single); - break; - default: - break; - } - } - - private void onCurrentChanged() { - if (getDownloadService() == null) { - return; - } - - currentPlaying = getDownloadService().getCurrentPlaying(); - if (currentPlaying != null) { - MusicDirectory.Entry song = currentPlaying.getSong(); - songTitleTextView.setText(song.getTitle()); - albumTextView.setText(song.getAlbum()); - artistTextView.setText(song.getArtist()); - getImageLoader().loadImage(albumArtImageView, song, true, true); - } else { - songTitleTextView.setText(null); - albumTextView.setText(null); - artistTextView.setText(null); - getImageLoader().loadImage(albumArtImageView, null, true, false); - } - } - - private void onProgressChanged() { - if (getDownloadService() == null) { - return; - } - - if (currentPlaying != null) { - - int millisPlayed = Math.max(0, getDownloadService().getPlayerPosition()); - Integer duration = getDownloadService().getPlayerDuration(); - int millisTotal = duration == null ? 0 : duration; - - positionTextView.setText(Util.formatDuration(millisPlayed / 1000)); - durationTextView.setText(Util.formatDuration(millisTotal / 1000)); - progressBar.setMax(millisTotal == 0 ? 100 : millisTotal); // Work-around for apparent bug. - progressBar.setProgress(millisPlayed); - progressBar.setSlidingEnabled(currentPlaying.isCompleteFileAvailable() || getDownloadService().isJukeboxEnabled()); - } else { - positionTextView.setText("0:00"); - durationTextView.setText("-:--"); - progressBar.setProgress(0); - progressBar.setSlidingEnabled(false); - } - - PlayerState playerState = getDownloadService().getPlayerState(); - - switch (playerState) { - case DOWNLOADING: - long bytes = currentPlaying.getPartialFile().length(); - statusTextView.setText(getResources().getString(R.string.download_playerstate_downloading, Util.formatLocalizedBytes(bytes, this))); - break; - case PREPARING: - statusTextView.setText(R.string.download_playerstate_buffering); - break; - case STARTED: - if (getDownloadService().isShufflePlayEnabled()) { - statusTextView.setText(R.string.download_playerstate_playing_shuffle); - } else { - statusTextView.setText(null); - } - break; - default: - statusTextView.setText(null); - break; - } - - switch (playerState) { - case STARTED: - pauseButton.setVisibility(View.VISIBLE); - stopButton.setVisibility(View.GONE); - startButton.setVisibility(View.GONE); - break; - case DOWNLOADING: - case PREPARING: - pauseButton.setVisibility(View.GONE); - stopButton.setVisibility(View.VISIBLE); - startButton.setVisibility(View.GONE); - break; - default: - pauseButton.setVisibility(View.GONE); - stopButton.setVisibility(View.GONE); - startButton.setVisibility(View.VISIBLE); - break; - } - } - - private class SongListAdapter extends ArrayAdapter { - public SongListAdapter(List entries) { - super(DownloadActivity.this, android.R.layout.simple_list_item_1, entries); - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - SongView view; - if (convertView != null && convertView instanceof SongView) { - view = (SongView) convertView; - } else { - view = new SongView(DownloadActivity.this); - } - DownloadFile downloadFile = getItem(position); - view.setSong(downloadFile.getSong(), false); - return view; - } - } - - @Override - public boolean onTouchEvent(MotionEvent me) { - return gestureScanner.onTouchEvent(me); - } - - @Override - public boolean onDown(MotionEvent me) { - return false; - } - - @Override - public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { - - DownloadService downloadService = getDownloadService(); - if (downloadService == null) { - return false; - } - - // Right to Left swipe - if (e1.getX() - e2.getX() > swipeDistance && Math.abs(velocityX) > swipeVelocity) { - warnIfNetworkOrStorageUnavailable(); - if (downloadService.getCurrentPlayingIndex() < downloadService.size() - 1) { - downloadService.next(); - onCurrentChanged(); - onProgressChanged(); - } - return true; - } - - // Left to Right swipe - if (e2.getX() - e1.getX() > swipeDistance && Math.abs(velocityX) > swipeVelocity) { - warnIfNetworkOrStorageUnavailable(); - downloadService.previous(); - onCurrentChanged(); - onProgressChanged(); - return true; - } - - // Top to Bottom swipe - if (e2.getY() - e1.getY() > swipeDistance && Math.abs(velocityY) > swipeVelocity) { - warnIfNetworkOrStorageUnavailable(); - downloadService.seekTo(downloadService.getPlayerPosition() + 30000); - onProgressChanged(); - return true; - } - - // Bottom to Top swipe - if (e1.getY() - e2.getY() > swipeDistance && Math.abs(velocityY) > swipeVelocity) { - warnIfNetworkOrStorageUnavailable(); - downloadService.seekTo(downloadService.getPlayerPosition() - 8000); - onProgressChanged(); - return true; - } - - return false; - } - - @Override - public void onLongPress(MotionEvent e) { - } - - @Override - public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { - return false; - } - - @Override - public void onShowPress(MotionEvent e) { - } - - @Override - public boolean onSingleTapUp(MotionEvent e) { - return false; - } -} +/* + This file is part of Subsonic. + + Subsonic 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. + + Subsonic 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 Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.androidapp.activity; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.view.ContextMenu; +import android.view.Display; +import android.view.GestureDetector; +import android.view.GestureDetector.OnGestureListener; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.view.animation.AnimationUtils; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ListView; +import android.widget.SeekBar; +import android.widget.TextView; +import android.widget.ViewFlipper; +import net.sourceforge.subsonic.androidapp.R; +import net.sourceforge.subsonic.androidapp.domain.MusicDirectory; +import net.sourceforge.subsonic.androidapp.domain.PlayerState; +import net.sourceforge.subsonic.androidapp.domain.RepeatMode; +import net.sourceforge.subsonic.androidapp.service.DownloadFile; +import net.sourceforge.subsonic.androidapp.service.DownloadService; +import net.sourceforge.subsonic.androidapp.service.MusicService; +import net.sourceforge.subsonic.androidapp.service.MusicServiceFactory; +import net.sourceforge.subsonic.androidapp.util.Constants; +import net.sourceforge.subsonic.androidapp.util.SilentBackgroundTask; +import net.sourceforge.subsonic.androidapp.util.SongView; +import net.sourceforge.subsonic.androidapp.util.Util; +import net.sourceforge.subsonic.androidapp.view.VisualizerView; + +import static net.sourceforge.subsonic.androidapp.domain.PlayerState.*; + +public class DownloadActivity extends SubsonicTabActivity implements OnGestureListener { + + private static final int DIALOG_SAVE_PLAYLIST = 100; + private static final int PERCENTAGE_OF_SCREEN_FOR_SWIPE = 5; + + private ViewFlipper playlistFlipper; + private ViewFlipper buttonBarFlipper; + private TextView emptyTextView; + private TextView songTitleTextView; + private TextView albumTextView; + private TextView artistTextView; + private ImageView albumArtImageView; + private ListView playlistView; + private TextView positionTextView; + private TextView durationTextView; + private TextView statusTextView; + private static SeekBar progressBar; + private View previousButton; + private View nextButton; + private View pauseButton; + private View stopButton; + private View startButton; + private View shuffleButton; + private ImageButton repeatButton; + private MenuItem equalizerMenuItem; + private MenuItem visualizerMenuItem; + private View toggleListButton; + private ScheduledExecutorService executorService; + private DownloadFile currentPlaying; + private long currentRevision; + private EditText playlistNameView; + private GestureDetector gestureScanner; + private int swipeDistance; + private int swipeVelocity; + private VisualizerView visualizerView; + private boolean visualizerAvailable; + private boolean equalizerAvailable; + + /** + * Called when the activity is first created. + */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.download); + + WindowManager w = getWindowManager(); + Display d = w.getDefaultDisplay(); + swipeDistance = (d.getWidth() + d.getHeight()) * PERCENTAGE_OF_SCREEN_FOR_SWIPE / 100; + swipeVelocity = (d.getWidth() + d.getHeight()) * PERCENTAGE_OF_SCREEN_FOR_SWIPE / 100; + gestureScanner = new GestureDetector(this); + + playlistFlipper = (ViewFlipper) findViewById(R.id.download_playlist_flipper); + buttonBarFlipper = (ViewFlipper) findViewById(R.id.download_button_bar_flipper); + emptyTextView = (TextView) findViewById(R.id.download_empty); + songTitleTextView = (TextView) findViewById(R.id.download_song_title); + albumTextView = (TextView) findViewById(R.id.download_album); + artistTextView = (TextView) findViewById(R.id.download_artist); + albumArtImageView = (ImageView) findViewById(R.id.download_album_art_image); + positionTextView = (TextView) findViewById(R.id.download_position); + durationTextView = (TextView) findViewById(R.id.download_duration); + statusTextView = (TextView) findViewById(R.id.download_status); + progressBar = (SeekBar) findViewById(R.id.download_progress_bar); + playlistView = (ListView) findViewById(R.id.download_list); + previousButton = findViewById(R.id.download_previous); + nextButton = findViewById(R.id.download_next); + pauseButton = findViewById(R.id.download_pause); + stopButton = findViewById(R.id.download_stop); + startButton = findViewById(R.id.download_start); + shuffleButton = findViewById(R.id.download_shuffle); + repeatButton = (ImageButton) findViewById(R.id.download_repeat); + LinearLayout visualizerViewLayout = (LinearLayout) findViewById(R.id.download_visualizer_view_layout); + + toggleListButton = findViewById(R.id.download_toggle_list); + + View.OnTouchListener touchListener = new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent me) { + return gestureScanner.onTouchEvent(me); + } + }; + previousButton.setOnTouchListener(touchListener); + nextButton.setOnTouchListener(touchListener); + pauseButton.setOnTouchListener(touchListener); + stopButton.setOnTouchListener(touchListener); + startButton.setOnTouchListener(touchListener); + buttonBarFlipper.setOnTouchListener(touchListener); + emptyTextView.setOnTouchListener(touchListener); + albumArtImageView.setOnTouchListener(touchListener); + + albumArtImageView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + toggleFullscreenAlbumArt(); + } + }); + + previousButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + warnIfNetworkOrStorageUnavailable(); + getDownloadService().previous(); + onCurrentChanged(); + onSliderProgressChanged(); + } + }); + + nextButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + warnIfNetworkOrStorageUnavailable(); + if (getDownloadService().getCurrentPlayingIndex() < getDownloadService().size() - 1) { + getDownloadService().next(); + onCurrentChanged(); + onSliderProgressChanged(); + } + } + }); + + pauseButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + getDownloadService().pause(); + onCurrentChanged(); + onSliderProgressChanged(); + } + }); + + stopButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + getDownloadService().reset(); + onCurrentChanged(); + onSliderProgressChanged(); + } + }); + + startButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + warnIfNetworkOrStorageUnavailable(); + start(); + onCurrentChanged(); + onSliderProgressChanged(); + } + }); + + shuffleButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + getDownloadService().shuffle(); + Util.toast(DownloadActivity.this, R.string.download_menu_shuffle_notification); + } + }); + + repeatButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + RepeatMode repeatMode = getDownloadService().getRepeatMode().next(); + getDownloadService().setRepeatMode(repeatMode); + onDownloadListChanged(); + switch (repeatMode) { + case OFF: + Util.toast(DownloadActivity.this, R.string.download_repeat_off); + break; + case ALL: + Util.toast(DownloadActivity.this, R.string.download_repeat_all); + break; + case SINGLE: + Util.toast(DownloadActivity.this, R.string.download_repeat_single); + break; + default: + break; + } + } + }); + + toggleListButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + toggleFullscreenAlbumArt(); + } + }); + + progressBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (fromUser) { + getDownloadService().seekTo(progress); + onSliderProgressChanged(); + } + } + }); + + playlistView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + warnIfNetworkOrStorageUnavailable(); + getDownloadService().play(position); + onCurrentChanged(); + onSliderProgressChanged(); + } + }); + + playlistView.setOnTouchListener(gestureListener); + + registerForContextMenu(playlistView); + + DownloadService downloadService = getDownloadService(); + if (downloadService != null && getIntent().getBooleanExtra(Constants.INTENT_EXTRA_NAME_SHUFFLE, false)) { + warnIfNetworkOrStorageUnavailable(); + downloadService.setShufflePlayEnabled(true); + } + + visualizerAvailable = downloadService != null && downloadService.getVisualizerController() != null; + equalizerAvailable = downloadService != null && downloadService.getEqualizerController() != null; + + if (visualizerAvailable) { + visualizerView = new VisualizerView(this); + visualizerViewLayout.addView(visualizerView, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT)); + + visualizerView.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View view, MotionEvent motionEvent) { + visualizerView.setActive(!visualizerView.isActive()); + getDownloadService().setShowVisualization(visualizerView.isActive()); + //updateButtons(); + return true; + } + }); + } + } + + @Override + protected void onResume() { + super.onResume(); + + final Handler handler = new Handler(); + Runnable runnable = new Runnable() { + @Override + public void run() { + handler.post(new Runnable() { + @Override + public void run() { + update(); + } + }); + } + }; + + executorService = Executors.newSingleThreadScheduledExecutor(); + executorService.scheduleWithFixedDelay(runnable, 0L, 1000L, TimeUnit.MILLISECONDS); + + DownloadService downloadService = getDownloadService(); + if (downloadService == null || downloadService.getCurrentPlaying() == null) { + playlistFlipper.setDisplayedChild(1); + buttonBarFlipper.setDisplayedChild(1); + } + + onDownloadListChanged(); + onCurrentChanged(); + onSliderProgressChanged(); + scrollToCurrent(); + if (downloadService != null && downloadService.getKeepScreenOn()) { + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } else { + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + if (visualizerView != null) { + visualizerView.setActive(downloadService != null && downloadService.getShowVisualization()); + } + } + + // Scroll to current playing/downloading. + private void scrollToCurrent() { + if (getDownloadService() == null) { + return; + } + + for (int i = 0; i < playlistView.getAdapter().getCount(); i++) { + if (currentPlaying == playlistView.getItemAtPosition(i)) { + playlistView.setSelectionFromTop(i, 40); + return; + } + } + DownloadFile currentDownloading = getDownloadService().getCurrentDownloading(); + for (int i = 0; i < playlistView.getAdapter().getCount(); i++) { + if (currentDownloading == playlistView.getItemAtPosition(i)) { + playlistView.setSelectionFromTop(i, 40); + return; + } + } + } + + @Override + protected void onPause() { + super.onPause(); + executorService.shutdown(); + if (visualizerView != null) { + visualizerView.setActive(false); + } + } + + @Override + protected Dialog onCreateDialog(int id) { + if (id == DIALOG_SAVE_PLAYLIST) { + AlertDialog.Builder builder; + + LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE); + final View layout = inflater.inflate(R.layout.save_playlist, (ViewGroup) findViewById(R.id.save_playlist_root)); + playlistNameView = (EditText) layout.findViewById(R.id.save_playlist_name); + + builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.download_playlist_title); + builder.setMessage(R.string.download_playlist_name); + builder.setPositiveButton(R.string.common_save, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int id) { + savePlaylistInBackground(String.valueOf(playlistNameView.getText())); + } + }); + builder.setNegativeButton(R.string.common_cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int id) { + dialog.cancel(); + } + }); + builder.setView(layout); + builder.setCancelable(true); + + return builder.create(); + } else { + return super.onCreateDialog(id); + } + } + + @Override + protected void onPrepareDialog(int id, Dialog dialog) { + if (id == DIALOG_SAVE_PLAYLIST) { + String playlistName = getDownloadService().getSuggestedPlaylistName(); + if (playlistName != null) { + playlistNameView.setText(playlistName); + } else { + DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); + playlistNameView.setText(dateFormat.format(new Date())); + } + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.nowplaying, menu); + super.onCreateOptionsMenu(menu); + return true; + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + MenuItem savePlaylist = menu.findItem(R.id.menu_save_playlist); + boolean savePlaylistEnabled = !Util.isOffline(this); + savePlaylist.setEnabled(savePlaylistEnabled); + savePlaylist.setVisible(savePlaylistEnabled); + MenuItem screenOption = menu.findItem(R.id.menu_screen_on_off); + equalizerMenuItem = menu.findItem(R.id.download_equalizer); + visualizerMenuItem = menu.findItem(R.id.download_visualizer); + + equalizerMenuItem.setEnabled(equalizerAvailable); + equalizerMenuItem.setVisible(equalizerAvailable); + visualizerMenuItem.setEnabled(visualizerAvailable); + visualizerMenuItem.setVisible(visualizerAvailable); + + DownloadService downloadService = getDownloadService(); + + if (downloadService != null) { + if (getDownloadService().getKeepScreenOn()) { + screenOption.setTitle(R.string.download_menu_screen_off); + } else { + screenOption.setTitle(R.string.download_menu_screen_on); + } + } + return super.onPrepareOptionsMenu(menu); + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, view, menuInfo); + if (view == playlistView) { + AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo; + DownloadFile downloadFile = (DownloadFile) playlistView.getItemAtPosition(info.position); + + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.nowplaying_context, menu); + + if (downloadFile.getSong().getParent() == null) { + menu.findItem(R.id.menu_show_album).setVisible(false); + } + if (Util.isOffline(this)) { + menu.findItem(R.id.menu_lyrics).setVisible(false); + menu.findItem(R.id.menu_save_playlist).setVisible(false); + } + } + } + + @Override + public boolean onContextItemSelected(MenuItem menuItem) { + AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo(); + DownloadFile downloadFile = (DownloadFile) playlistView.getItemAtPosition(info.position); + return menuItemSelected(menuItem.getItemId(), downloadFile) || super.onContextItemSelected(menuItem); + } + + @Override + public boolean onOptionsItemSelected(MenuItem menuItem) { + return menuItemSelected(menuItem.getItemId(), null) || super.onOptionsItemSelected(menuItem); + } + + private boolean menuItemSelected(int menuItemId, DownloadFile song) { + switch (menuItemId) { + case R.id.menu_show_album: + Intent intent = new Intent(this, SelectAlbumActivity.class); + intent.putExtra(Constants.INTENT_EXTRA_NAME_ID, song.getSong().getParent()); + intent.putExtra(Constants.INTENT_EXTRA_NAME_NAME, song.getSong().getAlbum()); + Util.startActivityWithoutTransition(this, intent); + return true; + case R.id.menu_lyrics: + intent = new Intent(this, LyricsActivity.class); + intent.putExtra(Constants.INTENT_EXTRA_NAME_ARTIST, song.getSong().getArtist()); + intent.putExtra(Constants.INTENT_EXTRA_NAME_TITLE, song.getSong().getTitle()); + Util.startActivityWithoutTransition(this, intent); + return true; + case R.id.menu_remove: + getDownloadService().remove(song); + onDownloadListChanged(); + return true; + case R.id.menu_remove_all: + getDownloadService().setShufflePlayEnabled(false); + getDownloadService().clear(); + onDownloadListChanged(); + return true; + case R.id.menu_screen_on_off: + if (getDownloadService().getKeepScreenOn()) { + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + getDownloadService().setKeepScreenOn(false); + } else { + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + getDownloadService().setKeepScreenOn(true); + } + return true; + case R.id.menu_shuffle: + getDownloadService().shuffle(); + Util.toast(this, R.string.download_menu_shuffle_notification); + return true; + case R.id.menu_save_playlist: + showDialog(DIALOG_SAVE_PLAYLIST); + return true; + case R.id.download_equalizer: + startActivity(new Intent(DownloadActivity.this, EqualizerActivity.class)); + return true; + case R.id.download_visualizer: + boolean active = !visualizerView.isActive(); + visualizerView.setActive(active); + getDownloadService().setShowVisualization(visualizerView.isActive()); + Util.toast(DownloadActivity.this, active ? R.string.download_visualizer_on : R.string.download_visualizer_off); + return true; + case R.id.download_jukebox: + boolean jukeboxEnabled = !getDownloadService().isJukeboxEnabled(); + getDownloadService().setJukeboxEnabled(jukeboxEnabled); + Util.toast(DownloadActivity.this, jukeboxEnabled ? R.string.download_jukebox_on : R.string.download_jukebox_off, false); + return true; + default: + return false; + } + } + + private void update() { + if (getDownloadService() == null) { + return; + } + + if (currentRevision != getDownloadService().getDownloadListUpdateRevision()) { + onDownloadListChanged(); + } + + if (currentPlaying != getDownloadService().getCurrentPlaying()) { + onCurrentChanged(); + } + + onSliderProgressChanged(); + } + + private void savePlaylistInBackground(final String playlistName) { + Util.toast(DownloadActivity.this, getResources().getString(R.string.download_playlist_saving, playlistName)); + getDownloadService().setSuggestedPlaylistName(playlistName); + new SilentBackgroundTask(this) { + @Override + protected Void doInBackground() throws Throwable { + List entries = new LinkedList(); + for (DownloadFile downloadFile : getDownloadService().getDownloads()) { + entries.add(downloadFile.getSong()); + } + MusicService musicService = MusicServiceFactory.getMusicService(DownloadActivity.this); + musicService.createPlaylist(null, playlistName, entries, DownloadActivity.this, null); + return null; + } + + @Override + protected void done(Void result) { + Util.toast(DownloadActivity.this, R.string.download_playlist_done); + } + + @Override + protected void error(Throwable error) { + String msg = getResources().getString(R.string.download_playlist_error) + " " + getErrorMessage(error); + Util.toast(DownloadActivity.this, msg); + } + }.execute(); + } + + private void toggleFullscreenAlbumArt() { + scrollToCurrent(); + if (playlistFlipper.getDisplayedChild() == 1) { + playlistFlipper.setInAnimation(AnimationUtils.loadAnimation(this, R.anim.push_down_in)); + playlistFlipper.setOutAnimation(AnimationUtils.loadAnimation(this, R.anim.push_down_out)); + playlistFlipper.setDisplayedChild(0); + buttonBarFlipper.setInAnimation(AnimationUtils.loadAnimation(this, R.anim.push_down_in)); + buttonBarFlipper.setOutAnimation(AnimationUtils.loadAnimation(this, R.anim.push_down_out)); + buttonBarFlipper.setDisplayedChild(0); + + + } else { + playlistFlipper.setInAnimation(AnimationUtils.loadAnimation(this, R.anim.push_up_in)); + playlistFlipper.setOutAnimation(AnimationUtils.loadAnimation(this, R.anim.push_up_out)); + playlistFlipper.setDisplayedChild(1); + buttonBarFlipper.setInAnimation(AnimationUtils.loadAnimation(this, R.anim.push_up_in)); + buttonBarFlipper.setOutAnimation(AnimationUtils.loadAnimation(this, R.anim.push_up_out)); + buttonBarFlipper.setDisplayedChild(1); + } + } + + private void start() { + DownloadService service = getDownloadService(); + PlayerState state = service.getPlayerState(); + if (state == PAUSED || state == COMPLETED) { + service.start(); + } else if (state == STOPPED || state == IDLE) { + warnIfNetworkOrStorageUnavailable(); + int current = service.getCurrentPlayingIndex(); + // TODO: Use play() method. + if (current == -1) { + service.play(0); + } else { + service.play(current); + } + } + } + + private void onDownloadListChanged() { + DownloadService downloadService = getDownloadService(); + if (downloadService == null) { + return; + } + + List list = downloadService.getDownloads(); + + playlistView.setAdapter(new SongListAdapter(list)); + emptyTextView.setVisibility(list.isEmpty() ? View.VISIBLE : View.GONE); + currentRevision = downloadService.getDownloadListUpdateRevision(); + + switch (downloadService.getRepeatMode()) { + case OFF: + repeatButton.setImageResource(R.drawable.media_repeat_off); + break; + case ALL: + repeatButton.setImageResource(R.drawable.media_repeat_all); + break; + case SINGLE: + repeatButton.setImageResource(R.drawable.media_repeat_single); + break; + default: + break; + } + } + + private void onCurrentChanged() { + if (getDownloadService() == null) { + return; + } + + currentPlaying = getDownloadService().getCurrentPlaying(); + if (currentPlaying != null) { + MusicDirectory.Entry song = currentPlaying.getSong(); + songTitleTextView.setText(song.getTitle()); + albumTextView.setText(song.getAlbum()); + artistTextView.setText(song.getArtist()); + getImageLoader().loadImage(albumArtImageView, song, true, true); + } else { + songTitleTextView.setText(null); + albumTextView.setText(null); + artistTextView.setText(null); + getImageLoader().loadImage(albumArtImageView, null, true, false); + } + } + + private void onSliderProgressChanged() { + if (getDownloadService() == null) { + return; + } + + if (currentPlaying != null) { + int millisPlayed = Math.max(0, getDownloadService().getPlayerPosition()); + Integer duration = getDownloadService().getPlayerDuration(); + int millisTotal = duration == null ? 0 : duration; + + positionTextView.setText(Util.formatDuration(millisPlayed / 1000)); + durationTextView.setText(Util.formatDuration(millisTotal / 1000)); + progressBar.setMax(millisTotal == 0 ? 100 : millisTotal); // Work-around for apparent bug. + progressBar.setProgress(millisPlayed); + } else { + positionTextView.setText("0:00"); + durationTextView.setText("-:--"); + progressBar.setProgress(0); + progressBar.setMax(0); + } + + PlayerState playerState = getDownloadService().getPlayerState(); + + switch (playerState) { + case DOWNLOADING: + long bytes = currentPlaying.getPartialFile().length(); + statusTextView.setText(getResources().getString(R.string.download_playerstate_downloading, Util.formatLocalizedBytes(bytes, this))); + break; + case PREPARING: + statusTextView.setText(R.string.download_playerstate_buffering); + break; + case STARTED: + if (getDownloadService().isShufflePlayEnabled()) { + statusTextView.setText(R.string.download_playerstate_playing_shuffle); + } else { + statusTextView.setText(null); + } + break; + default: + statusTextView.setText(null); + break; + } + + switch (playerState) { + case STARTED: + pauseButton.setVisibility(View.VISIBLE); + stopButton.setVisibility(View.GONE); + startButton.setVisibility(View.GONE); + break; + case DOWNLOADING: + case PREPARING: + pauseButton.setVisibility(View.GONE); + stopButton.setVisibility(View.VISIBLE); + startButton.setVisibility(View.GONE); + break; + default: + pauseButton.setVisibility(View.GONE); + stopButton.setVisibility(View.GONE); + startButton.setVisibility(View.VISIBLE); + break; + } + } + + private class SongListAdapter extends ArrayAdapter { + public SongListAdapter(List entries) { + super(DownloadActivity.this, android.R.layout.simple_list_item_1, entries); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + SongView view; + if (convertView != null && convertView instanceof SongView) { + view = (SongView) convertView; + } else { + view = new SongView(DownloadActivity.this); + } + DownloadFile downloadFile = getItem(position); + view.setSong(downloadFile.getSong(), false); + return view; + } + } + + @Override + public boolean onTouchEvent(MotionEvent me) { + return gestureScanner.onTouchEvent(me); + } + + @Override + public boolean onDown(MotionEvent me) { + return false; + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + + DownloadService downloadService = getDownloadService(); + if (downloadService == null) { + return false; + } + + // Right to Left swipe + if (e1.getX() - e2.getX() > swipeDistance && Math.abs(velocityX) > swipeVelocity) { + warnIfNetworkOrStorageUnavailable(); + if (downloadService.getCurrentPlayingIndex() < downloadService.size() - 1) { + downloadService.next(); + onCurrentChanged(); + onSliderProgressChanged(); + } + return true; + } + + // Left to Right swipe + if (e2.getX() - e1.getX() > swipeDistance && Math.abs(velocityX) > swipeVelocity) { + warnIfNetworkOrStorageUnavailable(); + downloadService.previous(); + onCurrentChanged(); + onSliderProgressChanged(); + return true; + } + + // Top to Bottom swipe + if (e2.getY() - e1.getY() > swipeDistance && Math.abs(velocityY) > swipeVelocity) { + warnIfNetworkOrStorageUnavailable(); + downloadService.seekTo(downloadService.getPlayerPosition() + 30000); + onSliderProgressChanged(); + return true; + } + + // Bottom to Top swipe + if (e1.getY() - e2.getY() > swipeDistance && Math.abs(velocityY) > swipeVelocity) { + warnIfNetworkOrStorageUnavailable(); + downloadService.seekTo(downloadService.getPlayerPosition() - 8000); + onSliderProgressChanged(); + return true; + } + + return false; + } + + @Override + public void onLongPress(MotionEvent e) { + } + + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + return false; + } + + @Override + public void onShowPress(MotionEvent e) { + } + + @Override + public boolean onSingleTapUp(MotionEvent e) { + return false; + } + + public static SeekBar getProgressBar() { + return progressBar; + } +} diff --git a/src/net/sourceforge/subsonic/androidapp/service/DownloadServiceImpl.java b/src/net/sourceforge/subsonic/androidapp/service/DownloadServiceImpl.java index 5c236256..fd9f66a9 100644 --- a/src/net/sourceforge/subsonic/androidapp/service/DownloadServiceImpl.java +++ b/src/net/sourceforge/subsonic/androidapp/service/DownloadServiceImpl.java @@ -36,6 +36,7 @@ import android.os.PowerManager; import android.util.DisplayMetrics; import android.util.Log; import android.widget.RemoteViews; +import android.widget.SeekBar; import net.sourceforge.subsonic.androidapp.R; import net.sourceforge.subsonic.androidapp.activity.DownloadActivity; import net.sourceforge.subsonic.androidapp.audiofx.EqualizerController; @@ -48,6 +49,7 @@ import net.sourceforge.subsonic.androidapp.util.CancellableTask; import net.sourceforge.subsonic.androidapp.util.LRUCache; import net.sourceforge.subsonic.androidapp.util.ShufflePlayBuffer; import net.sourceforge.subsonic.androidapp.util.SimpleServiceBinder; +import net.sourceforge.subsonic.androidapp.util.StreamProxy; import net.sourceforge.subsonic.androidapp.util.Util; import net.sourceforge.subsonic.androidapp.util.RemoteControlHelper; import net.sourceforge.subsonic.androidapp.util.RemoteControlClientCompat; @@ -57,7 +59,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; - import static net.sourceforge.subsonic.androidapp.domain.PlayerState.*; /** @@ -105,9 +106,10 @@ public class DownloadServiceImpl extends Service implements DownloadService { private VisualizerController visualizerController; private boolean showVisualization; private boolean jukeboxEnabled; + private StreamProxy proxy; private static MusicDirectory.Entry currentSong; - + RemoteControlClientCompat remoteControlClientCompat; static { @@ -161,7 +163,7 @@ public class DownloadServiceImpl extends Service implements DownloadService { Intent notificationIntent = new Intent(this, DownloadActivity.class); notification.contentIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0); Util.linkButtons(this, notification.contentView, false); - + if (equalizerAvailable) { equalizerController = new EqualizerController(this, mediaPlayer); if (!equalizerController.isAvailable()) { @@ -827,10 +829,33 @@ public class DownloadServiceImpl extends Service implements DownloadService { mediaPlayer.reset(); setPlayerState(IDLE); mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); - mediaPlayer.setDataSource(file.getPath()); + + String url = file.getPath(); + String playUrl = url; + + if (proxy == null) { + proxy = new StreamProxy(this); + proxy.start(); + } + + playUrl = String.format("http://127.0.0.1:%d/%s", proxy.getPort(), url); + + mediaPlayer.setDataSource(playUrl); setPlayerState(PREPARING); mediaPlayer.prepare(); setPlayerState(PREPARED); + + mediaPlayer.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() { + @Override + public void onBufferingUpdate(MediaPlayer mp, int percent) { + SeekBar progressBar = DownloadActivity.getProgressBar(); + if (progressBar != null) { + int max = progressBar.getMax(); + int secondaryProgress = (int) (((double)percent / (double)100) * max); + DownloadActivity.getProgressBar().setSecondaryProgress(secondaryProgress); + } + } + }); mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { @Override diff --git a/src/net/sourceforge/subsonic/androidapp/util/HorizontalSlider.java b/src/net/sourceforge/subsonic/androidapp/util/HorizontalSlider.java deleted file mode 100644 index 4e6ff64c..00000000 --- a/src/net/sourceforge/subsonic/androidapp/util/HorizontalSlider.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - This file is part of Subsonic. - - Subsonic 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. - - Subsonic 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 Subsonic. If not, see . - - Copyright 2009 (C) Sindre Mehus - */ -package net.sourceforge.subsonic.androidapp.util; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.Canvas; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.view.View; -import android.widget.ProgressBar; -import net.sourceforge.subsonic.androidapp.R; - -/** - * @author Sindre Mehus - * @version $Id$ - */ -public class HorizontalSlider extends ProgressBar { - - private final Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.slider_knob); - private boolean slidingEnabled; - private OnSliderChangeListener listener; - private static final int PADDING = 2; - private boolean sliding; - private int sliderPosition; - private int startPosition; - - public interface OnSliderChangeListener { - void onSliderChanged(View view, int position, boolean inProgress); - } - - public HorizontalSlider(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - public HorizontalSlider(Context context, AttributeSet attrs) { - super(context, attrs, android.R.attr.progressBarStyleHorizontal); - } - - public HorizontalSlider(Context context) { - super(context); - } - - public void setSlidingEnabled(boolean slidingEnabled) { - if (this.slidingEnabled != slidingEnabled) { - this.slidingEnabled = slidingEnabled; - invalidate(); - } - } - - public boolean isSlidingEnabled() { - return slidingEnabled; - } - - public void setOnSliderChangeListener(OnSliderChangeListener listener) { - this.listener = listener; - } - - @Override - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - - int max = getMax(); - if (!slidingEnabled || max == 0) { - return; - } - - int paddingLeft = getPaddingLeft(); - int paddingRight = getPaddingRight(); - int paddingTop = getPaddingTop(); - int paddingBottom = getPaddingBottom(); - - int w = getWidth() - paddingLeft - paddingRight; - int h = getHeight() - paddingTop - paddingBottom; - int position = sliding ? sliderPosition : getProgress(); - - int bitmapWidth = bitmap.getWidth(); - int bitmapHeight = bitmap.getWidth(); - float x = paddingLeft + w * ((float) position / max) - bitmapWidth / 2.0F; - x = Math.max(x, paddingLeft); - x = Math.min(x, paddingLeft + w - bitmapWidth); - float y = paddingTop + h / 2.0F - bitmapHeight / 2.0F; - - canvas.drawBitmap(bitmap, x, y, null); - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - if (!slidingEnabled) { - return false; - } - - int action = event.getAction(); - - if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_MOVE) { - - if (action == MotionEvent.ACTION_DOWN) { - sliding = true; - startPosition = getProgress(); - } - - float x = event.getX() - PADDING; - float width = getWidth() - 2 * PADDING; - sliderPosition = Math.round((float) getMax() * (x / width)); - sliderPosition = Math.max(sliderPosition, 0); - - setProgress(Math.min(startPosition, sliderPosition)); - setSecondaryProgress(Math.max(startPosition, sliderPosition)); - if (listener != null) { - listener.onSliderChanged(this, sliderPosition, true); - } - - } else if (action == MotionEvent.ACTION_UP) { - sliding = false; - setProgress(sliderPosition); - setSecondaryProgress(0); - if (listener != null) { - listener.onSliderChanged(this, sliderPosition, false); - } - } - - return true; - } -} \ No newline at end of file diff --git a/src/net/sourceforge/subsonic/androidapp/util/StreamProxy.java b/src/net/sourceforge/subsonic/androidapp/util/StreamProxy.java new file mode 100644 index 00000000..f94da21b --- /dev/null +++ b/src/net/sourceforge/subsonic/androidapp/util/StreamProxy.java @@ -0,0 +1,243 @@ +package net.sourceforge.subsonic.androidapp.util; + +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; +import java.net.SocketTimeoutException; +import java.net.URLDecoder; +import java.net.UnknownHostException; +import java.util.StringTokenizer; + +import org.apache.http.Header; +import org.apache.http.HttpRequest; +import org.apache.http.message.BasicHttpRequest; + +import net.sourceforge.subsonic.androidapp.service.DownloadService; +import android.os.AsyncTask; +import android.os.Looper; +import android.util.Log; + +public class StreamProxy implements Runnable { + private static final String TAG = StreamProxy.class.getSimpleName(); + + private Thread thread; + private boolean isRunning; + private ServerSocket socket; + private int port; + private DownloadService downloadService; + + public StreamProxy(DownloadService downloadService) { + + // Create listening socket + try { + socket = new ServerSocket(0, 0, InetAddress.getByAddress(new byte[] { 127, 0, 0, 1 })); + socket.setSoTimeout(5000); + port = socket.getLocalPort(); + this.downloadService = downloadService; + } catch (UnknownHostException e) { // impossible + } catch (IOException e) { + Log.e(TAG, "IOException initializing server", e); + } + } + + public int getPort() { + return port; + } + + public void start() { + thread = new Thread(this); + thread.start(); + } + + public void stop() { + isRunning = false; + thread.interrupt(); + try { + thread.join(5000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + @Override + public void run() { + Looper.prepare(); + isRunning = true; + while (isRunning) { + try { + Socket client = socket.accept(); + if (client == null) { + continue; + } + Log.d(TAG, "client connected"); + + StreamToMediaPlayerTask task = new StreamToMediaPlayerTask(client); + if (task.processRequest()) { + task.execute(); + } + + } catch (SocketTimeoutException e) { + // Do nothing + } catch (IOException e) { + Log.e(TAG, "Error connecting to client", e); + } + } + Log.d(TAG, "Proxy interrupted. Shutting down."); + } + + private class StreamToMediaPlayerTask extends AsyncTask { + + String localPath; + Socket client; + int cbSkip; + + public StreamToMediaPlayerTask(Socket client) { + this.client = client; + } + + private HttpRequest readRequest() { + HttpRequest request = null; + InputStream is; + String firstLine; + try { + is = client.getInputStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(is), 8192); + firstLine = reader.readLine(); + } catch (IOException e) { + Log.e(TAG, "Error parsing request", e); + return request; + } + + if (firstLine == null) { + Log.i(TAG, "Proxy client closed connection without a request."); + return request; + } + + StringTokenizer st = new StringTokenizer(firstLine); + String method = st.nextToken(); + String uri = st.nextToken(); + Log.d(TAG, uri); + String realUri = uri.substring(1); + Log.d(TAG, realUri); + request = new BasicHttpRequest(method, realUri); + return request; + } + + public boolean processRequest() { + HttpRequest request = readRequest(); + if (request == null) { + return false; + } + + // Read HTTP headers + Log.d(TAG, "Processing request"); + + try { + localPath = URLDecoder.decode(request.getRequestLine().getUri(), "UTF-8"); + } catch (UnsupportedEncodingException e) { + Log.e(TAG, "Unsupported encoding", e); + return false; + } + + File file = new File(localPath); + if (!file.exists()) { + Log.e(TAG, "File " + localPath + " does not exist"); + return false; + } + + Header rangeHeader = request.getLastHeader("Range"); + + if (rangeHeader != null) { + cbSkip = Integer.parseInt(rangeHeader.getValue()); + } + + return true; + } + + @Override + protected Integer doInBackground(String... params) { + long fileSize = downloadService.getCurrentPlaying().getSong().getSize(); + + // Create HTTP header + String headers = "HTTP/1.0 200 OK\r\n"; + headers += "Content-Type: " + "application/octet-stream" + "\r\n"; + headers += "Content-Length: " + fileSize + "\r\n"; + headers += "Connection: close\r\n"; + headers += "\r\n"; + + long cbToSend = fileSize - cbSkip; + OutputStream output = null; + byte[] buff = new byte[64 * 1024]; + try { + output = new BufferedOutputStream(client.getOutputStream(), 32*1024); + output.write(headers.getBytes()); + + // Loop as long as there's stuff to send + while (isRunning && cbToSend>0 && !client.isClosed()) { + + // See if there's more to send + File file = new File(localPath); + int cbSentThisBatch = 0; + if (file.exists()) { + FileInputStream input = new FileInputStream(file); + input.skip(cbSkip); + int cbToSendThisBatch = input.available(); + while (cbToSendThisBatch > 0) { + int cbToRead = Math.min(cbToSendThisBatch, buff.length); + int cbRead = input.read(buff, 0, cbToRead); + if (cbRead == -1) { + break; + } + cbToSendThisBatch -= cbRead; + cbToSend -= cbRead; + output.write(buff, 0, cbRead); + output.flush(); + cbSkip += cbRead; + cbSentThisBatch += cbRead; + } + input.close(); + } + + // If we did nothing this batch, block for a second + if (cbSentThisBatch == 0) { + Log.d(TAG, "Blocking until more data appears"); + Thread.sleep(1000); + } + } + } + catch (SocketException socketException) { + Log.e(TAG, "SocketException() thrown, proxy client has probably closed. This can exit harmlessly"); + } + catch (Exception e) { + Log.e(TAG, "Exception thrown from streaming task:"); + Log.e(TAG, e.getClass().getName() + " : " + e.getLocalizedMessage()); + e.printStackTrace(); + } + + // Cleanup + try { + if (output != null) { + output.close(); + } + client.close(); + } + catch (IOException e) { + Log.e(TAG, "IOException while cleaning up streaming task:"); + Log.e(TAG, e.getClass().getName() + " : " + e.getLocalizedMessage()); + e.printStackTrace(); + } + + return 1; + } + } +} \ No newline at end of file