aboutsummaryrefslogtreecommitdiff
path: root/subsonic-android/src/net/sourceforge/subsonic/androidapp/activity
diff options
context:
space:
mode:
Diffstat (limited to 'subsonic-android/src/net/sourceforge/subsonic/androidapp/activity')
-rw-r--r--subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/DownloadActivity.java874
-rw-r--r--subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/EqualizerActivity.java181
-rw-r--r--subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/HelpActivity.java117
-rw-r--r--subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/LyricsActivity.java72
-rw-r--r--subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/MainActivity.java258
-rw-r--r--subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/PlayVideoActivity.java147
-rw-r--r--subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/QueryReceiverActivity.java56
-rw-r--r--subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/SearchActivity.java368
-rw-r--r--subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/SelectAlbumActivity.java568
-rw-r--r--subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/SelectArtistActivity.java228
-rw-r--r--subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/SelectPlaylistActivity.java141
-rw-r--r--subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/SettingsActivity.java297
-rw-r--r--subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/SubsonicTabActivity.java383
-rw-r--r--subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/VoiceQueryReceiverActivity.java59
14 files changed, 3749 insertions, 0 deletions
diff --git a/subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/DownloadActivity.java b/subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/DownloadActivity.java
new file mode 100644
index 00000000..68144481
--- /dev/null
+++ b/subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/DownloadActivity.java
@@ -0,0 +1,874 @@
+/*
+ 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 <http://www.gnu.org/licenses/>.
+
+ 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.graphics.Color;
+import android.graphics.Typeface;
+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.Button;
+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 static final int COLOR_BUTTON_ENABLED = Color.rgb(129, 201, 54);
+ private static final int COLOR_BUTTON_DISABLED = Color.rgb(164, 166, 158);
+
+ 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 Button equalizerButton;
+ private Button visualizerButton;
+ private Button jukeboxButton;
+ 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;
+
+ /**
+ * 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);
+ equalizerButton = (Button) findViewById(R.id.download_equalizer);
+ visualizerButton = (Button) findViewById(R.id.download_visualizer);
+ jukeboxButton = (Button) findViewById(R.id.download_jukebox);
+ 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);
+ equalizerButton.setOnTouchListener(touchListener);
+ visualizerButton.setOnTouchListener(touchListener);
+ jukeboxButton.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;
+ }
+ }
+ });
+
+ equalizerButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ startActivity(new Intent(DownloadActivity.this, EqualizerActivity.class));
+ }
+ });
+
+ visualizerButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ boolean active = !visualizerView.isActive();
+ visualizerView.setActive(active);
+ getDownloadService().setShowVisualization(visualizerView.isActive());
+ updateButtons();
+ Util.toast(DownloadActivity.this, active ? R.string.download_visualizer_on : R.string.download_visualizer_off);
+ }
+ });
+
+ jukeboxButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ boolean jukeboxEnabled = !getDownloadService().isJukeboxEnabled();
+ getDownloadService().setJukeboxEnabled(jukeboxEnabled);
+ updateButtons();
+ Util.toast(DownloadActivity.this, jukeboxEnabled ? R.string.download_jukebox_on : R.string.download_jukebox_off, false);
+ }
+ });
+
+ 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();
+ }
+ });
+
+ registerForContextMenu(playlistView);
+
+ DownloadService downloadService = getDownloadService();
+ if (downloadService != null && getIntent().getBooleanExtra(Constants.INTENT_EXTRA_NAME_SHUFFLE, false)) {
+ warnIfNetworkOrStorageUnavailable();
+ downloadService.setShufflePlayEnabled(true);
+ }
+
+ boolean visualizerAvailable = downloadService != null && downloadService.getVisualizerController() != null;
+ boolean equalizerAvailable = downloadService != null && downloadService.getEqualizerController() != null;
+
+ if (!equalizerAvailable) {
+ equalizerButton.setVisibility(View.GONE);
+ }
+ if (!visualizerAvailable) {
+ visualizerButton.setVisibility(View.GONE);
+ } else {
+ visualizerView = new VisualizerView(this);
+ visualizerViewLayout.addView(visualizerView, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.FILL_PARENT, LinearLayout.LayoutParams.FILL_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;
+ }
+ });
+ }
+
+ // TODO: Extract to utility method and cache.
+ Typeface typeface = Typeface.createFromAsset(getAssets(), "fonts/Storopia.ttf");
+ equalizerButton.setTypeface(typeface);
+ visualizerButton.setTypeface(typeface);
+ jukeboxButton.setTypeface(typeface);
+ }
+
+ @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());
+ }
+
+ updateButtons();
+ }
+
+ private void updateButtons() {
+ boolean eqEnabled = getDownloadService() != null && getDownloadService().getEqualizerController() != null &&
+ getDownloadService().getEqualizerController().isEnabled();
+ equalizerButton.setTextColor(eqEnabled ? COLOR_BUTTON_ENABLED : COLOR_BUTTON_DISABLED);
+
+ if (visualizerView != null) {
+ visualizerButton.setTextColor(visualizerView.isActive() ? COLOR_BUTTON_ENABLED : COLOR_BUTTON_DISABLED);
+ }
+
+ boolean jukeboxEnabled = getDownloadService() != null && getDownloadService().isJukeboxEnabled();
+ jukeboxButton.setTextColor(jukeboxEnabled ? COLOR_BUTTON_ENABLED : COLOR_BUTTON_DISABLED);
+ }
+
+ // 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);
+ 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);
+ 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;
+ 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<Void>(this) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ List<MusicDirectory.Entry> entries = new LinkedList<MusicDirectory.Entry>();
+ 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<DownloadFile> 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;
+ }
+
+ jukeboxButton.setTextColor(getDownloadService().isJukeboxEnabled() ? COLOR_BUTTON_ENABLED : COLOR_BUTTON_DISABLED);
+ }
+
+ private class SongListAdapter extends ArrayAdapter<DownloadFile> {
+ public SongListAdapter(List<DownloadFile> 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;
+ }
+}
diff --git a/subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/EqualizerActivity.java b/subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/EqualizerActivity.java
new file mode 100644
index 00000000..daf6193e
--- /dev/null
+++ b/subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/EqualizerActivity.java
@@ -0,0 +1,181 @@
+/*
+ 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 <http://www.gnu.org/licenses/>.
+
+ Copyright 2011 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.androidapp.activity;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import android.app.Activity;
+import android.media.audiofx.Equalizer;
+import android.os.Bundle;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.LinearLayout;
+import android.widget.SeekBar;
+import android.widget.TextView;
+import net.sourceforge.subsonic.androidapp.R;
+import net.sourceforge.subsonic.androidapp.audiofx.EqualizerController;
+import net.sourceforge.subsonic.androidapp.service.DownloadServiceImpl;
+
+/**
+ * Equalizer controls.
+ *
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class EqualizerActivity extends Activity {
+
+ private static final int MENU_GROUP_PRESET = 100;
+
+ private final Map<Short, SeekBar> bars = new HashMap<Short, SeekBar>();
+ private EqualizerController equalizerController;
+ private Equalizer equalizer;
+
+ @Override
+ public void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ setContentView(R.layout.equalizer);
+ equalizerController = DownloadServiceImpl.getInstance().getEqualizerController();
+ equalizer = equalizerController.getEqualizer();
+
+ initEqualizer();
+
+ final View presetButton = findViewById(R.id.equalizer_preset);
+ registerForContextMenu(presetButton);
+ presetButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ presetButton.showContextMenu();
+ }
+ });
+
+ CheckBox enabledCheckBox = (CheckBox) findViewById(R.id.equalizer_enabled);
+ enabledCheckBox.setChecked(equalizer.getEnabled());
+ enabledCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
+ setEqualizerEnabled(b);
+ }
+ });
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ equalizerController.saveSettings();
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, view, menuInfo);
+
+ short currentPreset;
+ try {
+ currentPreset = equalizer.getCurrentPreset();
+ } catch (Exception x) {
+ currentPreset = -1;
+ }
+
+ for (short preset = 0; preset < equalizer.getNumberOfPresets(); preset++) {
+ MenuItem menuItem = menu.add(MENU_GROUP_PRESET, preset, preset, equalizer.getPresetName(preset));
+ if (preset == currentPreset) {
+ menuItem.setChecked(true);
+ }
+ }
+ menu.setGroupCheckable(MENU_GROUP_PRESET, true, true);
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem menuItem) {
+ short preset = (short) menuItem.getItemId();
+ equalizer.usePreset(preset);
+ updateBars();
+ return true;
+ }
+
+ private void setEqualizerEnabled(boolean enabled) {
+ equalizer.setEnabled(enabled);
+ updateBars();
+ }
+
+ private void updateBars() {
+
+ for (Map.Entry<Short, SeekBar> entry : bars.entrySet()) {
+ short band = entry.getKey();
+ SeekBar bar = entry.getValue();
+ bar.setEnabled(equalizer.getEnabled());
+ short minEQLevel = equalizer.getBandLevelRange()[0];
+ bar.setProgress(equalizer.getBandLevel(band) - minEQLevel);
+ }
+ }
+
+ private void initEqualizer() {
+ LinearLayout layout = (LinearLayout) findViewById(R.id.equalizer_layout);
+
+ final short minEQLevel = equalizer.getBandLevelRange()[0];
+ final short maxEQLevel = equalizer.getBandLevelRange()[1];
+
+ for (short i = 0; i < equalizer.getNumberOfBands(); i++) {
+ final short band = i;
+
+ View bandBar = LayoutInflater.from(this).inflate(R.layout.equalizer_bar, null);
+ TextView freqTextView = (TextView) bandBar.findViewById(R.id.equalizer_frequency);
+ final TextView levelTextView = (TextView) bandBar.findViewById(R.id.equalizer_level);
+ SeekBar bar = (SeekBar) bandBar.findViewById(R.id.equalizer_bar);
+
+ freqTextView.setText((equalizer.getCenterFreq(band) / 1000) + " Hz");
+
+ bars.put(band, bar);
+ bar.setMax(maxEQLevel - minEQLevel);
+ short level = equalizer.getBandLevel(band);
+ bar.setProgress(level - minEQLevel);
+ bar.setEnabled(equalizer.getEnabled());
+ updateLevelText(levelTextView, level);
+
+ bar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ short level = (short) (progress + minEQLevel);
+ if (fromUser) {
+ equalizer.setBandLevel(band, level);
+ }
+ updateLevelText(levelTextView, level);
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ }
+ });
+ layout.addView(bandBar);
+ }
+ }
+
+ private void updateLevelText(TextView levelTextView, short level) {
+ levelTextView.setText((level > 0 ? "+" : "") + level / 100 + " dB");
+ }
+
+}
diff --git a/subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/HelpActivity.java b/subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/HelpActivity.java
new file mode 100644
index 00000000..4b2eb63b
--- /dev/null
+++ b/subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/HelpActivity.java
@@ -0,0 +1,117 @@
+/*
+ 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 <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+
+package net.sourceforge.subsonic.androidapp.activity;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.Window;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import android.widget.Button;
+import net.sourceforge.subsonic.androidapp.R;
+import net.sourceforge.subsonic.androidapp.util.Util;
+
+/**
+ * An HTML-based help screen with Back and Done buttons at the bottom.
+ *
+ * @author Sindre Mehus
+ */
+public final class HelpActivity extends Activity {
+
+ private WebView webView;
+ private Button backButton;
+
+ @Override
+ protected void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ getWindow().requestFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
+
+ setContentView(R.layout.help);
+
+ webView = (WebView) findViewById(R.id.help_contents);
+ webView.getSettings().setJavaScriptEnabled(true);
+ webView.setWebViewClient(new HelpClient());
+ if (bundle != null) {
+ webView.restoreState(bundle);
+ } else {
+ webView.loadUrl(getResources().getString(R.string.help_url));
+ }
+
+ backButton = (Button) findViewById(R.id.help_back);
+ backButton.setOnClickListener(new Button.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ webView.goBack();
+ }
+ });
+
+ Button doneButton = (Button) findViewById(R.id.help_close);
+ doneButton.setOnClickListener(new Button.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ finish();
+ }
+ });
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle state) {
+ webView.saveState(state);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ if (webView.canGoBack()) {
+ webView.goBack();
+ return true;
+ }
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+ private final class HelpClient extends WebViewClient {
+ @Override
+ public void onLoadResource(WebView webView, String url) {
+ setProgressBarIndeterminateVisibility(true);
+ setTitle(getResources().getString(R.string.help_loading));
+ super.onLoadResource(webView, url);
+ }
+
+ @Override
+ public void onPageFinished(WebView view, String url) {
+ setProgressBarIndeterminateVisibility(false);
+ setTitle(view.getTitle());
+ backButton.setEnabled(view.canGoBack());
+ }
+
+ @Override
+ public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
+ Util.toast(HelpActivity.this, description);
+ }
+ }
+}
diff --git a/subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/LyricsActivity.java b/subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/LyricsActivity.java
new file mode 100644
index 00000000..0ec75e2c
--- /dev/null
+++ b/subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/LyricsActivity.java
@@ -0,0 +1,72 @@
+/*
+ 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 <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+
+package net.sourceforge.subsonic.androidapp.activity;
+
+import android.os.Bundle;
+import android.widget.TextView;
+import net.sourceforge.subsonic.androidapp.R;
+import net.sourceforge.subsonic.androidapp.domain.Lyrics;
+import net.sourceforge.subsonic.androidapp.service.MusicService;
+import net.sourceforge.subsonic.androidapp.service.MusicServiceFactory;
+import net.sourceforge.subsonic.androidapp.util.BackgroundTask;
+import net.sourceforge.subsonic.androidapp.util.Constants;
+import net.sourceforge.subsonic.androidapp.util.TabActivityBackgroundTask;
+
+/**
+ * Displays song lyrics.
+ *
+ * @author Sindre Mehus
+ */
+public final class LyricsActivity extends SubsonicTabActivity {
+
+ @Override
+ protected void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ setContentView(R.layout.lyrics);
+ load();
+ }
+
+ private void load() {
+ BackgroundTask<Lyrics> task = new TabActivityBackgroundTask<Lyrics>(this) {
+ @Override
+ protected Lyrics doInBackground() throws Throwable {
+ String artist = getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_ARTIST);
+ String title = getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_TITLE);
+ MusicService musicService = MusicServiceFactory.getMusicService(LyricsActivity.this);
+ return musicService.getLyrics(artist, title, LyricsActivity.this, this);
+ }
+
+ @Override
+ protected void done(Lyrics result) {
+ TextView artistView = (TextView) findViewById(R.id.lyrics_artist);
+ TextView titleView = (TextView) findViewById(R.id.lyrics_title);
+ TextView textView = (TextView) findViewById(R.id.lyrics_text);
+ if (result != null && result.getArtist() != null) {
+ artistView.setText(result.getArtist());
+ titleView.setText(result.getTitle());
+ textView.setText(result.getText());
+ } else {
+ artistView.setText(R.string.lyrics_nomatch);
+ }
+ }
+ };
+ task.execute();
+ }
+} \ No newline at end of file
diff --git a/subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/MainActivity.java b/subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/MainActivity.java
new file mode 100644
index 00000000..c63a391b
--- /dev/null
+++ b/subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/MainActivity.java
@@ -0,0 +1,258 @@
+/*
+ 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 <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+
+package net.sourceforge.subsonic.androidapp.activity;
+
+import java.util.Arrays;
+
+import net.sourceforge.subsonic.androidapp.R;
+import net.sourceforge.subsonic.androidapp.service.DownloadService;
+import net.sourceforge.subsonic.androidapp.service.DownloadServiceImpl;
+import net.sourceforge.subsonic.androidapp.util.Constants;
+import net.sourceforge.subsonic.androidapp.util.MergeAdapter;
+import net.sourceforge.subsonic.androidapp.util.Util;
+import net.sourceforge.subsonic.androidapp.util.FileUtil;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ImageButton;
+import android.widget.ListView;
+import android.widget.TextView;
+
+public class MainActivity extends SubsonicTabActivity {
+
+ private static final int MENU_GROUP_SERVER = 10;
+ private static final int MENU_ITEM_SERVER_1 = 101;
+ private static final int MENU_ITEM_SERVER_2 = 102;
+ private static final int MENU_ITEM_SERVER_3 = 103;
+ private static final int MENU_ITEM_OFFLINE = 104;
+
+ private String theme;
+
+ private static boolean infoDialogDisplayed;
+
+ /**
+ * Called when the activity is first created.
+ */
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (getIntent().hasExtra(Constants.INTENT_EXTRA_NAME_EXIT)) {
+ exit();
+ }
+ setContentView(R.layout.main);
+
+ loadSettings();
+
+ View buttons = LayoutInflater.from(this).inflate(R.layout.main_buttons, null);
+
+ final View serverButton = buttons.findViewById(R.id.main_select_server);
+ final TextView serverTextView = (TextView) serverButton.findViewById(R.id.main_select_server_2);
+
+ final View albumsTitle = buttons.findViewById(R.id.main_albums);
+ final View albumsNewestButton = buttons.findViewById(R.id.main_albums_newest);
+ final View albumsRandomButton = buttons.findViewById(R.id.main_albums_random);
+ final View albumsHighestButton = buttons.findViewById(R.id.main_albums_highest);
+ final View albumsRecentButton = buttons.findViewById(R.id.main_albums_recent);
+ final View albumsFrequentButton = buttons.findViewById(R.id.main_albums_frequent);
+
+ final View dummyView = findViewById(R.id.main_dummy);
+
+ int instance = Util.getActiveServer(this);
+ String name = Util.getServerName(this, instance);
+ serverTextView.setText(name);
+
+ ListView list = (ListView) findViewById(R.id.main_list);
+
+ MergeAdapter adapter = new MergeAdapter();
+ adapter.addViews(Arrays.asList(serverButton), true);
+ if (!Util.isOffline(this)) {
+ adapter.addView(albumsTitle, false);
+ adapter.addViews(Arrays.asList(albumsNewestButton, albumsRandomButton, albumsHighestButton, albumsRecentButton, albumsFrequentButton), true);
+ }
+ list.setAdapter(adapter);
+ registerForContextMenu(dummyView);
+
+ list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ if (view == serverButton) {
+ dummyView.showContextMenu();
+ } else if (view == albumsNewestButton) {
+ showAlbumList("newest");
+ } else if (view == albumsRandomButton) {
+ showAlbumList("random");
+ } else if (view == albumsHighestButton) {
+ showAlbumList("highest");
+ } else if (view == albumsRecentButton) {
+ showAlbumList("recent");
+ } else if (view == albumsFrequentButton) {
+ showAlbumList("frequent");
+ }
+ }
+ });
+
+ // Title: Subsonic
+ setTitle(R.string.common_appname);
+
+ // Button 1: shuffle
+ ImageButton actionShuffleButton = (ImageButton)findViewById(R.id.action_button_1);
+ actionShuffleButton.setImageResource(R.drawable.action_shuffle);
+ actionShuffleButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Intent intent = new Intent(MainActivity.this, DownloadActivity.class);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_SHUFFLE, true);
+ Util.startActivityWithoutTransition(MainActivity.this, intent);
+ }
+ });
+
+ // Button 2: search
+ ImageButton actionSearchButton = (ImageButton)findViewById(R.id.action_button_2);
+ actionSearchButton.setImageResource(R.drawable.action_search);
+ actionSearchButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Intent intent = new Intent(MainActivity.this, SearchActivity.class);
+ intent.putExtra(Constants.INTENT_EXTRA_REQUEST_SEARCH, true);
+ Util.startActivityWithoutTransition(MainActivity.this, intent);
+ }
+ });
+
+ // Remember the current theme.
+ theme = Util.getTheme(this);
+
+ showInfoDialog();
+ }
+
+ private void loadSettings() {
+ PreferenceManager.setDefaultValues(this, R.xml.settings, false);
+ SharedPreferences prefs = Util.getPreferences(this);
+ if (!prefs.contains(Constants.PREFERENCES_KEY_CACHE_LOCATION)) {
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putString(Constants.PREFERENCES_KEY_CACHE_LOCATION, FileUtil.getDefaultMusicDirectory().getPath());
+ editor.commit();
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ // Restart activity if theme has changed.
+ if (theme != null && !theme.equals(Util.getTheme(this))) {
+ restart();
+ }
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, view, menuInfo);
+
+ MenuItem menuItem1 = menu.add(MENU_GROUP_SERVER, MENU_ITEM_SERVER_1, MENU_ITEM_SERVER_1, Util.getServerName(this, 1));
+ MenuItem menuItem2 = menu.add(MENU_GROUP_SERVER, MENU_ITEM_SERVER_2, MENU_ITEM_SERVER_2, Util.getServerName(this, 2));
+ MenuItem menuItem3 = menu.add(MENU_GROUP_SERVER, MENU_ITEM_SERVER_3, MENU_ITEM_SERVER_3, Util.getServerName(this, 3));
+ MenuItem menuItem4 = menu.add(MENU_GROUP_SERVER, MENU_ITEM_OFFLINE, MENU_ITEM_OFFLINE, Util.getServerName(this, 0));
+ menu.setGroupCheckable(MENU_GROUP_SERVER, true, true);
+ menu.setHeaderTitle(R.string.main_select_server);
+
+ switch (Util.getActiveServer(this)) {
+ case 0:
+ menuItem4.setChecked(true);
+ break;
+ case 1:
+ menuItem1.setChecked(true);
+ break;
+ case 2:
+ menuItem2.setChecked(true);
+ break;
+ case 3:
+ menuItem3.setChecked(true);
+ break;
+ }
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem menuItem) {
+ switch (menuItem.getItemId()) {
+ case MENU_ITEM_OFFLINE:
+ setActiveServer(0);
+ break;
+ case MENU_ITEM_SERVER_1:
+ setActiveServer(1);
+ break;
+ case MENU_ITEM_SERVER_2:
+ setActiveServer(2);
+ break;
+ case MENU_ITEM_SERVER_3:
+ setActiveServer(3);
+ break;
+ default:
+ return super.onContextItemSelected(menuItem);
+ }
+
+ // Restart activity
+ restart();
+ return true;
+ }
+
+ private void setActiveServer(int instance) {
+ if (Util.getActiveServer(this) != instance) {
+ DownloadService service = getDownloadService();
+ if (service != null) {
+ service.clearIncomplete();
+ }
+ Util.setActiveServer(this, instance);
+ }
+ }
+
+ private void restart() {
+ Intent intent = new Intent(this, MainActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ Util.startActivityWithoutTransition(this, intent);
+ }
+
+ private void exit() {
+ stopService(new Intent(this, DownloadServiceImpl.class));
+ finish();
+ }
+
+ private void showInfoDialog() {
+ if (!infoDialogDisplayed) {
+ infoDialogDisplayed = true;
+ if (Util.getRestUrl(this, null).contains("demo.subsonic.org")) {
+ Util.info(this, R.string.main_welcome_title, R.string.main_welcome_text);
+ }
+ }
+ }
+
+ private void showAlbumList(String type) {
+ Intent intent = new Intent(this, SelectAlbumActivity.class);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, type);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 20);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0);
+ Util.startActivityWithoutTransition(this, intent);
+ }
+} \ No newline at end of file
diff --git a/subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/PlayVideoActivity.java b/subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/PlayVideoActivity.java
new file mode 100644
index 00000000..ea332ca0
--- /dev/null
+++ b/subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/PlayVideoActivity.java
@@ -0,0 +1,147 @@
+/*
+ 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 <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+
+package net.sourceforge.subsonic.androidapp.activity;
+
+import java.lang.reflect.Method;
+
+import android.app.Activity;
+import android.graphics.Bitmap;
+import android.media.AudioManager;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.Window;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import net.sourceforge.subsonic.androidapp.R;
+import net.sourceforge.subsonic.androidapp.service.MusicServiceFactory;
+import net.sourceforge.subsonic.androidapp.util.Constants;
+import net.sourceforge.subsonic.androidapp.util.Util;
+
+/**
+ * Plays videos in a web page.
+ *
+ * @author Sindre Mehus
+ */
+public final class PlayVideoActivity extends Activity {
+
+ private static final String TAG = PlayVideoActivity.class.getSimpleName();
+ private WebView webView;
+
+ @Override
+ protected void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ getWindow().requestFeature(Window.FEATURE_NO_TITLE);
+ setVolumeControlStream(AudioManager.STREAM_MUSIC);
+
+ setContentView(R.layout.play_video);
+
+ webView = (WebView) findViewById(R.id.play_video_contents);
+ webView.getSettings().setJavaScriptEnabled(true);
+ webView.getSettings().setPluginsEnabled(true);
+ webView.getSettings().setAllowFileAccess(true);
+ webView.getSettings().setSupportZoom(true);
+ webView.getSettings().setBuiltInZoomControls(true);
+
+ webView.setWebViewClient(new Client());
+ if (bundle != null) {
+ webView.restoreState(bundle);
+ } else {
+ webView.loadUrl(getVideoUrl());
+ }
+
+ // Show warning if Flash plugin is not installed.
+ if (isFlashPluginInstalled()) {
+ Util.toast(this, R.string.play_video_loading, false);
+ } else {
+ Util.toast(this, R.string.play_video_noplugin, false);
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ callHiddenWebViewMethod("onPause");
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ callHiddenWebViewMethod("onResume");
+ }
+
+ private String getVideoUrl() {
+ String id = getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_ID);
+ return MusicServiceFactory.getMusicService(this).getVideoUrl(this, id);
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle state) {
+ webView.saveState(state);
+ }
+
+ private void callHiddenWebViewMethod(String name){
+ if( webView != null ){
+ try {
+ Method method = WebView.class.getMethod(name);
+ method.invoke(webView);
+ } catch (Throwable x) {
+ Log.e(TAG, "Failed to invoke " + name, x);
+ }
+ }
+ }
+
+ private boolean isFlashPluginInstalled() {
+ try {
+ PackageInfo packageInfo = getPackageManager().getPackageInfo("com.adobe.flashplayer", 0);
+ return packageInfo != null;
+ } catch (PackageManager.NameNotFoundException x) {
+ return false;
+ }
+ }
+
+ private final class Client extends WebViewClient {
+
+ @Override
+ public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
+ Util.toast(PlayVideoActivity.this, description);
+ Log.e(TAG, "Error: " + description);
+ }
+
+ @Override
+ public void onLoadResource(WebView view, String url) {
+ super.onLoadResource(view, url);
+ Log.d(TAG, "onLoadResource: " + url);
+ }
+
+ @Override
+ public void onPageStarted(WebView view, String url, Bitmap favicon) {
+ super.onPageStarted(view, url, favicon);
+ Log.d(TAG, "onPageStarted: " + url);
+ }
+
+ @Override
+ public void onPageFinished(WebView view, String url) {
+ super.onPageFinished(view, url);
+ Log.d(TAG, "onPageFinished: " + url);
+ }
+ }
+}
diff --git a/subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/QueryReceiverActivity.java b/subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/QueryReceiverActivity.java
new file mode 100644
index 00000000..35b5ccaf
--- /dev/null
+++ b/subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/QueryReceiverActivity.java
@@ -0,0 +1,56 @@
+/*
+ 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 <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+
+package net.sourceforge.subsonic.androidapp.activity;
+
+import android.app.Activity;
+import android.app.SearchManager;
+import android.content.Intent;
+import android.os.Bundle;
+import android.provider.SearchRecentSuggestions;
+import net.sourceforge.subsonic.androidapp.util.Constants;
+import net.sourceforge.subsonic.androidapp.util.Util;
+import net.sourceforge.subsonic.androidapp.provider.SearchSuggestionProvider;
+
+/**
+ * Receives search queries and forwards to the SelectAlbumActivity.
+ *
+ * @author Sindre Mehus
+ */
+public class QueryReceiverActivity extends Activity {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ String query = getIntent().getStringExtra(SearchManager.QUERY);
+
+ if (query != null) {
+ SearchRecentSuggestions suggestions = new SearchRecentSuggestions(this, SearchSuggestionProvider.AUTHORITY,
+ SearchSuggestionProvider.MODE);
+ suggestions.saveRecentQuery(query, null);
+
+ Intent intent = new Intent(QueryReceiverActivity.this, SearchActivity.class);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_QUERY, query);
+ Util.startActivityWithoutTransition(QueryReceiverActivity.this, intent);
+ }
+ finish();
+ Util.disablePendingTransition(this);
+ }
+} \ No newline at end of file
diff --git a/subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/SearchActivity.java b/subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/SearchActivity.java
new file mode 100644
index 00000000..73b787a0
--- /dev/null
+++ b/subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/SearchActivity.java
@@ -0,0 +1,368 @@
+/*
+ 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 <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+
+package net.sourceforge.subsonic.androidapp.activity;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Arrays;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.MenuInflater;
+import android.view.View;
+import android.view.MenuItem;
+import android.widget.AdapterView;
+import android.widget.ImageButton;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.net.Uri;
+import net.sourceforge.subsonic.androidapp.R;
+import net.sourceforge.subsonic.androidapp.domain.Artist;
+import net.sourceforge.subsonic.androidapp.domain.MusicDirectory;
+import net.sourceforge.subsonic.androidapp.domain.SearchCritera;
+import net.sourceforge.subsonic.androidapp.domain.SearchResult;
+import net.sourceforge.subsonic.androidapp.service.MusicService;
+import net.sourceforge.subsonic.androidapp.service.MusicServiceFactory;
+import net.sourceforge.subsonic.androidapp.service.DownloadService;
+import net.sourceforge.subsonic.androidapp.util.ArtistAdapter;
+import net.sourceforge.subsonic.androidapp.util.BackgroundTask;
+import net.sourceforge.subsonic.androidapp.util.Constants;
+import net.sourceforge.subsonic.androidapp.util.EntryAdapter;
+import net.sourceforge.subsonic.androidapp.util.MergeAdapter;
+import net.sourceforge.subsonic.androidapp.util.TabActivityBackgroundTask;
+import net.sourceforge.subsonic.androidapp.util.Util;
+
+/**
+ * Performs searches and displays the matching artists, albums and songs.
+ *
+ * @author Sindre Mehus
+ */
+public class SearchActivity extends SubsonicTabActivity {
+
+ private static final int DEFAULT_ARTISTS = 3;
+ private static final int DEFAULT_ALBUMS = 5;
+ private static final int DEFAULT_SONGS = 10;
+
+ private static final int MAX_ARTISTS = 10;
+ private static final int MAX_ALBUMS = 20;
+ private static final int MAX_SONGS = 25;
+ private ListView list;
+
+ private View artistsHeading;
+ private View albumsHeading;
+ private View songsHeading;
+ private TextView searchButton;
+ private View moreArtistsButton;
+ private View moreAlbumsButton;
+ private View moreSongsButton;
+ private SearchResult searchResult;
+ private MergeAdapter mergeAdapter;
+ private ArtistAdapter artistAdapter;
+ private ListAdapter moreArtistsAdapter;
+ private EntryAdapter albumAdapter;
+ private ListAdapter moreAlbumsAdapter;
+ private ListAdapter moreSongsAdapter;
+ private EntryAdapter songAdapter;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.search);
+
+ setTitle(R.string.search_title);
+
+ View buttons = LayoutInflater.from(this).inflate(R.layout.search_buttons, null);
+
+ artistsHeading = buttons.findViewById(R.id.search_artists);
+ albumsHeading = buttons.findViewById(R.id.search_albums);
+ songsHeading = buttons.findViewById(R.id.search_songs);
+
+ searchButton = (TextView) buttons.findViewById(R.id.search_search);
+ moreArtistsButton = buttons.findViewById(R.id.search_more_artists);
+ moreAlbumsButton = buttons.findViewById(R.id.search_more_albums);
+ moreSongsButton = buttons.findViewById(R.id.search_more_songs);
+
+ list = (ListView) findViewById(R.id.search_list);
+
+ list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ if (view == searchButton) {
+ onSearchRequested();
+ } else if (view == moreArtistsButton) {
+ expandArtists();
+ } else if (view == moreAlbumsButton) {
+ expandAlbums();
+ } else if (view == moreSongsButton) {
+ expandSongs();
+ } else {
+ Object item = parent.getItemAtPosition(position);
+ if (item instanceof Artist) {
+ onArtistSelected((Artist) item);
+ } else if (item instanceof MusicDirectory.Entry) {
+ MusicDirectory.Entry entry = (MusicDirectory.Entry) item;
+ if (entry.isDirectory()) {
+ onAlbumSelected(entry, false);
+ } else if (entry.isVideo()) {
+ onVideoSelected(entry);
+ } else {
+ onSongSelected(entry, false, true, true, false);
+ }
+
+ }
+ }
+ }
+ });
+ registerForContextMenu(list);
+
+ // Button 1: gone
+ findViewById(R.id.action_button_1).setVisibility(View.GONE);
+
+ // Button 2: search
+ final ImageButton actionSearchButton = (ImageButton)findViewById(R.id.action_button_2);
+ actionSearchButton.setImageResource(R.drawable.action_search);
+ actionSearchButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ onSearchRequested();
+ }
+ });
+
+ onNewIntent(getIntent());
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ String query = intent.getStringExtra(Constants.INTENT_EXTRA_NAME_QUERY);
+ boolean autoplay = intent.getBooleanExtra(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false);
+ boolean requestsearch = intent.getBooleanExtra(Constants.INTENT_EXTRA_REQUEST_SEARCH, false);
+
+ if (query != null) {
+ mergeAdapter = new MergeAdapter();
+ list.setAdapter(mergeAdapter);
+ search(query, autoplay);
+ } else {
+ populateList();
+ if (requestsearch)
+ onSearchRequested();
+ }
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, view, menuInfo);
+
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
+ Object selectedItem = list.getItemAtPosition(info.position);
+
+ boolean isArtist = selectedItem instanceof Artist;
+ boolean isAlbum = selectedItem instanceof MusicDirectory.Entry && ((MusicDirectory.Entry) selectedItem).isDirectory();
+ boolean isSong = selectedItem instanceof MusicDirectory.Entry && (!((MusicDirectory.Entry) selectedItem).isDirectory())
+ && (!((MusicDirectory.Entry) selectedItem).isVideo());
+
+ if (isArtist || isAlbum) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.select_album_context, menu);
+ } else if (isSong) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.select_song_context, menu);
+ }
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem menuItem) {
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
+ Object selectedItem = list.getItemAtPosition(info.position);
+
+ Artist artist = selectedItem instanceof Artist ? (Artist) selectedItem : null;
+ MusicDirectory.Entry entry = selectedItem instanceof MusicDirectory.Entry ? (MusicDirectory.Entry) selectedItem : null;
+ String id = artist != null ? artist.getId() : entry.getId();
+
+ switch (menuItem.getItemId()) {
+ case R.id.album_menu_play_now:
+ downloadRecursively(id, false, false, true);
+ break;
+ case R.id.album_menu_play_last:
+ downloadRecursively(id, false, true, false);
+ break;
+ case R.id.album_menu_pin:
+ downloadRecursively(id, true, true, false);
+ break;
+ case R.id.song_menu_play_now:
+ onSongSelected(entry, false, false, true, false);
+ break;
+ case R.id.song_menu_play_next:
+ onSongSelected(entry, false, true, false, true);
+ break;
+ case R.id.song_menu_play_last:
+ onSongSelected(entry, false, true, false, false);
+ break;
+ default:
+ return super.onContextItemSelected(menuItem);
+ }
+
+ return true;
+ }
+
+ private void search(final String query, final boolean autoplay) {
+ BackgroundTask<SearchResult> task = new TabActivityBackgroundTask<SearchResult>(this) {
+ @Override
+ protected SearchResult doInBackground() throws Throwable {
+ SearchCritera criteria = new SearchCritera(query, MAX_ARTISTS, MAX_ALBUMS, MAX_SONGS);
+ MusicService service = MusicServiceFactory.getMusicService(SearchActivity.this);
+ return service.search(criteria, SearchActivity.this, this);
+ }
+
+ @Override
+ protected void done(SearchResult result) {
+ searchResult = result;
+ populateList();
+ if (autoplay) {
+ autoplay();
+ }
+
+ }
+ };
+ task.execute();
+ }
+
+ private void populateList() {
+ mergeAdapter = new MergeAdapter();
+ mergeAdapter.addView(searchButton, true);
+
+ if (searchResult != null) {
+ List<Artist> artists = searchResult.getArtists();
+ if (!artists.isEmpty()) {
+ mergeAdapter.addView(artistsHeading);
+ List<Artist> displayedArtists = new ArrayList<Artist>(artists.subList(0, Math.min(DEFAULT_ARTISTS, artists.size())));
+ artistAdapter = new ArtistAdapter(this, displayedArtists);
+ mergeAdapter.addAdapter(artistAdapter);
+ if (artists.size() > DEFAULT_ARTISTS) {
+ moreArtistsAdapter = mergeAdapter.addView(moreArtistsButton, true);
+ }
+ }
+
+ List<MusicDirectory.Entry> albums = searchResult.getAlbums();
+ if (!albums.isEmpty()) {
+ mergeAdapter.addView(albumsHeading);
+ List<MusicDirectory.Entry> displayedAlbums = new ArrayList<MusicDirectory.Entry>(albums.subList(0, Math.min(DEFAULT_ALBUMS, albums.size())));
+ albumAdapter = new EntryAdapter(this, getImageLoader(), displayedAlbums, false);
+ mergeAdapter.addAdapter(albumAdapter);
+ if (albums.size() > DEFAULT_ALBUMS) {
+ moreAlbumsAdapter = mergeAdapter.addView(moreAlbumsButton, true);
+ }
+ }
+
+ List<MusicDirectory.Entry> songs = searchResult.getSongs();
+ if (!songs.isEmpty()) {
+ mergeAdapter.addView(songsHeading);
+ List<MusicDirectory.Entry> displayedSongs = new ArrayList<MusicDirectory.Entry>(songs.subList(0, Math.min(DEFAULT_SONGS, songs.size())));
+ songAdapter = new EntryAdapter(this, getImageLoader(), displayedSongs, false);
+ mergeAdapter.addAdapter(songAdapter);
+ if (songs.size() > DEFAULT_SONGS) {
+ moreSongsAdapter = mergeAdapter.addView(moreSongsButton, true);
+ }
+ }
+
+ boolean empty = searchResult.getArtists().isEmpty() && searchResult.getAlbums().isEmpty() && searchResult.getSongs().isEmpty();
+ searchButton.setText(empty ? R.string.search_no_match : R.string.search_search);
+ }
+
+ list.setAdapter(mergeAdapter);
+ }
+
+ private void expandArtists() {
+ artistAdapter.clear();
+ for (Artist artist : searchResult.getArtists()) {
+ artistAdapter.add(artist);
+ }
+ artistAdapter.notifyDataSetChanged();
+ mergeAdapter.removeAdapter(moreArtistsAdapter);
+ mergeAdapter.notifyDataSetChanged();
+ }
+
+ private void expandAlbums() {
+ albumAdapter.clear();
+ for (MusicDirectory.Entry album : searchResult.getAlbums()) {
+ albumAdapter.add(album);
+ }
+ albumAdapter.notifyDataSetChanged();
+ mergeAdapter.removeAdapter(moreAlbumsAdapter);
+ mergeAdapter.notifyDataSetChanged();
+ }
+
+ private void expandSongs() {
+ songAdapter.clear();
+ for (MusicDirectory.Entry song : searchResult.getSongs()) {
+ songAdapter.add(song);
+ }
+ songAdapter.notifyDataSetChanged();
+ mergeAdapter.removeAdapter(moreSongsAdapter);
+ mergeAdapter.notifyDataSetChanged();
+ }
+
+ private void onArtistSelected(Artist artist) {
+ Intent intent = new Intent(this, SelectAlbumActivity.class);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_ID, artist.getId());
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_NAME, artist.getName());
+ Util.startActivityWithoutTransition(this, intent);
+ }
+
+ private void onAlbumSelected(MusicDirectory.Entry album, boolean autoplay) {
+ Intent intent = new Intent(SearchActivity.this, SelectAlbumActivity.class);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_ID, album.getId());
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_NAME, album.getTitle());
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_AUTOPLAY, autoplay);
+ Util.startActivityWithoutTransition(SearchActivity.this, intent);
+ }
+
+ private void onSongSelected(MusicDirectory.Entry song, boolean save, boolean append, boolean autoplay, boolean playNext) {
+ DownloadService downloadService = getDownloadService();
+ if (downloadService != null) {
+ if (!append) {
+ downloadService.clear();
+ }
+ downloadService.download(Arrays.asList(song), save, false, playNext);
+ if (autoplay) {
+ downloadService.play(downloadService.size() - 1);
+ }
+
+ Util.toast(SearchActivity.this, getResources().getQuantityString(R.plurals.select_album_n_songs_added, 1, 1));
+ }
+ }
+
+ private void onVideoSelected(MusicDirectory.Entry entry) {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(Uri.parse(MusicServiceFactory.getMusicService(this).getVideoUrl(this, entry.getId())));
+ startActivity(intent);
+ }
+
+ private void autoplay() {
+ if (!searchResult.getSongs().isEmpty()) {
+ onSongSelected(searchResult.getSongs().get(0), false, false, true, false);
+ } else if (!searchResult.getAlbums().isEmpty()) {
+ onAlbumSelected(searchResult.getAlbums().get(0), true);
+ }
+ }
+} \ No newline at end of file
diff --git a/subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/SelectAlbumActivity.java b/subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/SelectAlbumActivity.java
new file mode 100644
index 00000000..b354599f
--- /dev/null
+++ b/subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/SelectAlbumActivity.java
@@ -0,0 +1,568 @@
+/*
+ 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 <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.androidapp.activity;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.Button;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.ListView;
+import net.sourceforge.subsonic.androidapp.R;
+import net.sourceforge.subsonic.androidapp.domain.MusicDirectory;
+import net.sourceforge.subsonic.androidapp.service.DownloadFile;
+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.EntryAdapter;
+import net.sourceforge.subsonic.androidapp.util.Pair;
+import net.sourceforge.subsonic.androidapp.util.TabActivityBackgroundTask;
+import net.sourceforge.subsonic.androidapp.util.Util;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class SelectAlbumActivity extends SubsonicTabActivity {
+
+ private static final String TAG = SelectAlbumActivity.class.getSimpleName();
+
+ private ListView entryList;
+ private View footer;
+ private View emptyView;
+ private Button selectButton;
+ private Button playNowButton;
+ private Button playLastButton;
+ private Button pinButton;
+ private Button unpinButton;
+ private Button deleteButton;
+ private Button moreButton;
+ private ImageView coverArtView;
+ private boolean licenseValid;
+ private ImageButton playAllButton;
+
+ /**
+ * Called when the activity is first created.
+ */
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.select_album);
+
+ entryList = (ListView) findViewById(R.id.select_album_entries);
+
+ footer = LayoutInflater.from(this).inflate(R.layout.select_album_footer, entryList, false);
+ entryList.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
+ entryList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ if (position >= 0) {
+ MusicDirectory.Entry entry = (MusicDirectory.Entry) parent.getItemAtPosition(position);
+ if (entry.isDirectory()) {
+ Intent intent = new Intent(SelectAlbumActivity.this, SelectAlbumActivity.class);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_ID, entry.getId());
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_NAME, entry.getTitle());
+ Util.startActivityWithoutTransition(SelectAlbumActivity.this, intent);
+ } else if (entry.isVideo()) {
+ playVideo(entry);
+ } else {
+ enableButtons();
+ }
+ }
+ }
+ });
+
+ coverArtView = (ImageView) findViewById(R.id.actionbar_home_icon);
+ selectButton = (Button) findViewById(R.id.select_album_select);
+ playNowButton = (Button) findViewById(R.id.select_album_play_now);
+ playLastButton = (Button) findViewById(R.id.select_album_play_last);
+ pinButton = (Button) footer.findViewById(R.id.select_album_pin);
+ unpinButton = (Button) footer.findViewById(R.id.select_album_unpin);
+ unpinButton = (Button) footer.findViewById(R.id.select_album_unpin);
+ deleteButton = (Button) footer.findViewById(R.id.select_album_delete);
+ moreButton = (Button) footer.findViewById(R.id.select_album_more);
+ emptyView = findViewById(R.id.select_album_empty);
+
+ selectButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ selectAllOrNone();
+ }
+ });
+ playNowButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ download(false, false, true, false);
+ selectAll(false, false);
+ }
+ });
+ playLastButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ download(true, false, false, false);
+ selectAll(false, false);
+ }
+ });
+ pinButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ download(true, true, false, false);
+ selectAll(false, false);
+ }
+ });
+ unpinButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ unpin();
+ selectAll(false, false);
+ }
+ });
+ deleteButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ delete();
+ selectAll(false, false);
+ }
+ });
+
+ registerForContextMenu(entryList);
+
+ enableButtons();
+
+ String id = getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_ID);
+ String name = getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_NAME);
+ String playlistId = getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID);
+ String playlistName = getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME);
+ String albumListType = getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE);
+ int albumListSize = getIntent().getIntExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0);
+ int albumListOffset = getIntent().getIntExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0);
+
+ if (playlistId != null) {
+ getPlaylist(playlistId, playlistName);
+ } else if (albumListType != null) {
+ getAlbumList(albumListType, albumListSize, albumListOffset);
+ } else {
+ getMusicDirectory(id, name);
+ }
+
+ // Button 1: play all
+ playAllButton = (ImageButton) findViewById(R.id.action_button_1);
+ playAllButton.setImageResource(R.drawable.action_play_all);
+ playAllButton.setVisibility(View.GONE);
+ playAllButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ playAll();
+ }
+ });
+
+ // Button 2: refresh
+ ImageButton refreshButton = (ImageButton) findViewById(R.id.action_button_2);
+ refreshButton.setImageResource(R.drawable.action_refresh);
+ refreshButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ refresh();
+ }
+ });
+ }
+
+ private void playAll() {
+ boolean hasSubFolders = false;
+ for (int i = 0; i < entryList.getCount(); i++) {
+ MusicDirectory.Entry entry = (MusicDirectory.Entry) entryList.getItemAtPosition(i);
+ if (entry != null && entry.isDirectory()) {
+ hasSubFolders = true;
+ break;
+ }
+ }
+
+ String id = getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_ID);
+ if (hasSubFolders && id != null) {
+ downloadRecursively(id, false, false, true);
+ } else {
+ selectAll(true, false);
+ download(false, false, true, false);
+ selectAll(false, false);
+ }
+ }
+
+ private void refresh() {
+ finish();
+ Intent intent = getIntent();
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_REFRESH, true);
+ Util.startActivityWithoutTransition(this, intent);
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, view, menuInfo);
+ AdapterView.AdapterContextMenuInfo info =
+ (AdapterView.AdapterContextMenuInfo) menuInfo;
+
+ MusicDirectory.Entry entry = (MusicDirectory.Entry) entryList.getItemAtPosition(info.position);
+
+ if (entry.isDirectory()) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.select_album_context, menu);
+ } else {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.select_song_context, menu);
+ }
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem menuItem) {
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
+ MusicDirectory.Entry entry = (MusicDirectory.Entry) entryList.getItemAtPosition(info.position);
+ List<MusicDirectory.Entry> songs = new ArrayList<MusicDirectory.Entry>(10);
+ songs.add((MusicDirectory.Entry) entryList.getItemAtPosition(info.position));
+ switch (menuItem.getItemId()) {
+ case R.id.album_menu_play_now:
+ downloadRecursively(entry.getId(), false, false, true);
+ break;
+ case R.id.album_menu_play_last:
+ downloadRecursively(entry.getId(), false, true, false);
+ break;
+ case R.id.album_menu_pin:
+ downloadRecursively(entry.getId(), true, true, false);
+ break;
+ case R.id.song_menu_play_now:
+ getDownloadService().download(songs, false, true, true);
+ break;
+ case R.id.song_menu_play_next:
+ getDownloadService().download(songs, false, false, true);
+ break;
+ case R.id.song_menu_play_last:
+ getDownloadService().download(songs, false, false, false);
+ break;
+ default:
+ return super.onContextItemSelected(menuItem);
+ }
+ return true;
+ }
+
+ private void getMusicDirectory(final String id, String name) {
+ setTitle(name);
+
+ new LoadTask() {
+ @Override
+ protected MusicDirectory load(MusicService service) throws Exception {
+ boolean refresh = getIntent().getBooleanExtra(Constants.INTENT_EXTRA_NAME_REFRESH, false);
+ return service.getMusicDirectory(id, refresh, SelectAlbumActivity.this, this);
+ }
+ }.execute();
+ }
+
+ private void getPlaylist(final String playlistId, String playlistName) {
+ setTitle(playlistName);
+
+ new LoadTask() {
+ @Override
+ protected MusicDirectory load(MusicService service) throws Exception {
+ return service.getPlaylist(playlistId, SelectAlbumActivity.this, this);
+ }
+ }.execute();
+ }
+
+ private void getAlbumList(final String albumListType, final int size, final int offset) {
+
+ if ("newest".equals(albumListType)) {
+ setTitle(R.string.main_albums_newest);
+ } else if ("random".equals(albumListType)) {
+ setTitle(R.string.main_albums_random);
+ } else if ("highest".equals(albumListType)) {
+ setTitle(R.string.main_albums_highest);
+ } else if ("recent".equals(albumListType)) {
+ setTitle(R.string.main_albums_recent);
+ } else if ("frequent".equals(albumListType)) {
+ setTitle(R.string.main_albums_frequent);
+ }
+
+ new LoadTask() {
+ @Override
+ protected MusicDirectory load(MusicService service) throws Exception {
+ return service.getAlbumList(albumListType, size, offset, SelectAlbumActivity.this, this);
+ }
+
+ @Override
+ protected void done(Pair<MusicDirectory, Boolean> result) {
+ if (!result.getFirst().getChildren().isEmpty()) {
+ pinButton.setVisibility(View.GONE);
+ unpinButton.setVisibility(View.GONE);
+ deleteButton.setVisibility(View.GONE);
+ moreButton.setVisibility(View.VISIBLE);
+ entryList.addFooterView(footer);
+
+ moreButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Intent intent = new Intent(SelectAlbumActivity.this, SelectAlbumActivity.class);
+ String type = getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE);
+ int size = getIntent().getIntExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0);
+ int offset = getIntent().getIntExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0) + size;
+
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, type);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, size);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, offset);
+ Util.startActivityWithoutTransition(SelectAlbumActivity.this, intent);
+ }
+ });
+ }
+ super.done(result);
+ }
+ }.execute();
+ }
+
+ private void selectAllOrNone() {
+ boolean someUnselected = false;
+ int count = entryList.getCount();
+ for (int i = 0; i < count; i++) {
+ if (!entryList.isItemChecked(i) && entryList.getItemAtPosition(i) instanceof MusicDirectory.Entry) {
+ someUnselected = true;
+ break;
+ }
+ }
+ selectAll(someUnselected, true);
+ }
+
+ private void selectAll(boolean selected, boolean toast) {
+ int count = entryList.getCount();
+ int selectedCount = 0;
+ for (int i = 0; i < count; i++) {
+ MusicDirectory.Entry entry = (MusicDirectory.Entry) entryList.getItemAtPosition(i);
+ if (entry != null && !entry.isDirectory() && !entry.isVideo()) {
+ entryList.setItemChecked(i, selected);
+ selectedCount++;
+ }
+ }
+
+ // Display toast: N tracks selected / N tracks unselected
+ if (toast) {
+ int toastResId = selected ? R.string.select_album_n_selected
+ : R.string.select_album_n_unselected;
+ Util.toast(this, getString(toastResId, selectedCount));
+ }
+
+ enableButtons();
+ }
+
+ private void enableButtons() {
+ if (getDownloadService() == null) {
+ return;
+ }
+
+ List<MusicDirectory.Entry> selection = getSelectedSongs();
+ boolean enabled = !selection.isEmpty();
+ boolean unpinEnabled = false;
+ boolean deleteEnabled = false;
+
+ for (MusicDirectory.Entry song : selection) {
+ DownloadFile downloadFile = getDownloadService().forSong(song);
+ if (downloadFile.isCompleteFileAvailable()) {
+ deleteEnabled = true;
+ }
+ if (downloadFile.isSaved()) {
+ unpinEnabled = true;
+ }
+ }
+
+ playNowButton.setEnabled(enabled);
+ playLastButton.setEnabled(enabled);
+ pinButton.setEnabled(enabled && !Util.isOffline(this));
+ unpinButton.setEnabled(unpinEnabled);
+ deleteButton.setEnabled(deleteEnabled);
+ }
+
+ private List<MusicDirectory.Entry> getSelectedSongs() {
+ List<MusicDirectory.Entry> songs = new ArrayList<MusicDirectory.Entry>(10);
+ int count = entryList.getCount();
+ for (int i = 0; i < count; i++) {
+ if (entryList.isItemChecked(i)) {
+ songs.add((MusicDirectory.Entry) entryList.getItemAtPosition(i));
+ }
+ }
+ return songs;
+ }
+
+ private void download(final boolean append, final boolean save, final boolean autoplay, final boolean playNext) {
+ if (getDownloadService() == null) {
+ return;
+ }
+
+ final List<MusicDirectory.Entry> songs = getSelectedSongs();
+ Runnable onValid = new Runnable() {
+ @Override
+ public void run() {
+ if (!append) {
+ getDownloadService().clear();
+ }
+
+ warnIfNetworkOrStorageUnavailable();
+ getDownloadService().download(songs, save, autoplay, playNext);
+ String playlistName = getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME);
+ if (playlistName != null) {
+ getDownloadService().setSuggestedPlaylistName(playlistName);
+ }
+ if (autoplay) {
+ Util.startActivityWithoutTransition(SelectAlbumActivity.this, DownloadActivity.class);
+ } else if (save) {
+ Util.toast(SelectAlbumActivity.this,
+ getResources().getQuantityString(R.plurals.select_album_n_songs_downloading, songs.size(), songs.size()));
+ } else if (append) {
+ Util.toast(SelectAlbumActivity.this,
+ getResources().getQuantityString(R.plurals.select_album_n_songs_added, songs.size(), songs.size()));
+ }
+ }
+ };
+
+ checkLicenseAndTrialPeriod(onValid);
+ }
+
+ private void delete() {
+ if (getDownloadService() != null) {
+ getDownloadService().delete(getSelectedSongs());
+ }
+ }
+
+ private void unpin() {
+ if (getDownloadService() != null) {
+ getDownloadService().unpin(getSelectedSongs());
+ }
+ }
+
+ private void playVideo(MusicDirectory.Entry entry) {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(Uri.parse(MusicServiceFactory.getMusicService(this).getVideoUrl(this, entry.getId())));
+
+ startActivity(intent);
+ }
+
+ private void checkLicenseAndTrialPeriod(Runnable onValid) {
+ if (licenseValid) {
+ onValid.run();
+ return;
+ }
+
+ int trialDaysLeft = Util.getRemainingTrialDays(this);
+ Log.i(TAG, trialDaysLeft + " trial days left.");
+
+ if (trialDaysLeft == 0) {
+ showDonationDialog(trialDaysLeft, null);
+ } else if (trialDaysLeft < Constants.FREE_TRIAL_DAYS / 2) {
+ showDonationDialog(trialDaysLeft, onValid);
+ } else {
+ Util.toast(this, getResources().getString(R.string.select_album_not_licensed, trialDaysLeft));
+ onValid.run();
+ }
+ }
+
+ private void showDonationDialog(int trialDaysLeft, final Runnable onValid) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setIcon(android.R.drawable.ic_dialog_info);
+
+ if (trialDaysLeft == 0) {
+ builder.setTitle(R.string.select_album_donate_dialog_0_trial_days_left);
+ } else {
+ builder.setTitle(getResources().getQuantityString(R.plurals.select_album_donate_dialog_n_trial_days_left,
+ trialDaysLeft, trialDaysLeft));
+ }
+
+ builder.setMessage(R.string.select_album_donate_dialog_message);
+
+ builder.setPositiveButton(R.string.select_album_donate_dialog_now,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(Constants.DONATION_URL)));
+ }
+ });
+
+ builder.setNegativeButton(R.string.select_album_donate_dialog_later,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ dialogInterface.dismiss();
+ if (onValid != null) {
+ onValid.run();
+ }
+ }
+ });
+
+ builder.create().show();
+ }
+
+ private abstract class LoadTask extends TabActivityBackgroundTask<Pair<MusicDirectory, Boolean>> {
+
+ public LoadTask() {
+ super(SelectAlbumActivity.this);
+ }
+
+ protected abstract MusicDirectory load(MusicService service) throws Exception;
+
+ @Override
+ protected Pair<MusicDirectory, Boolean> doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(SelectAlbumActivity.this);
+ MusicDirectory dir = load(musicService);
+ boolean valid = musicService.isLicenseValid(SelectAlbumActivity.this, this);
+ return new Pair<MusicDirectory, Boolean>(dir, valid);
+ }
+
+ @Override
+ protected void done(Pair<MusicDirectory, Boolean> result) {
+ List<MusicDirectory.Entry> entries = result.getFirst().getChildren();
+
+ int songCount = 0;
+ for (MusicDirectory.Entry entry : entries) {
+ if (!entry.isDirectory()) {
+ songCount++;
+ }
+ }
+
+ if (songCount > 0) {
+ getImageLoader().loadImage(coverArtView, entries.get(0), false, true);
+ entryList.addFooterView(footer);
+ selectButton.setVisibility(View.VISIBLE);
+ playNowButton.setVisibility(View.VISIBLE);
+ playLastButton.setVisibility(View.VISIBLE);
+ }
+
+ boolean isAlbumList = getIntent().hasExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE);
+
+ emptyView.setVisibility(entries.isEmpty() ? View.VISIBLE : View.GONE);
+ playAllButton.setVisibility(isAlbumList || entries.isEmpty() ? View.GONE : View.VISIBLE);
+ entryList.setAdapter(new EntryAdapter(SelectAlbumActivity.this, getImageLoader(), entries, true));
+ licenseValid = result.getSecond();
+
+ boolean playAll = getIntent().getBooleanExtra(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false);
+ if (playAll && songCount > 0) {
+ playAll();
+ }
+ }
+ }
+}
diff --git a/subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/SelectArtistActivity.java b/subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/SelectArtistActivity.java
new file mode 100644
index 00000000..959066ab
--- /dev/null
+++ b/subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/SelectArtistActivity.java
@@ -0,0 +1,228 @@
+/*
+ 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 <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+
+package net.sourceforge.subsonic.androidapp.activity;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ImageButton;
+import android.widget.ListView;
+import android.widget.TextView;
+import net.sourceforge.subsonic.androidapp.R;
+import net.sourceforge.subsonic.androidapp.domain.Artist;
+import net.sourceforge.subsonic.androidapp.domain.Indexes;
+import net.sourceforge.subsonic.androidapp.domain.MusicFolder;
+import net.sourceforge.subsonic.androidapp.service.MusicService;
+import net.sourceforge.subsonic.androidapp.service.MusicServiceFactory;
+import net.sourceforge.subsonic.androidapp.util.ArtistAdapter;
+import net.sourceforge.subsonic.androidapp.util.BackgroundTask;
+import net.sourceforge.subsonic.androidapp.util.Constants;
+import net.sourceforge.subsonic.androidapp.util.TabActivityBackgroundTask;
+import net.sourceforge.subsonic.androidapp.util.Util;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class SelectArtistActivity extends SubsonicTabActivity implements AdapterView.OnItemClickListener {
+
+ private static final int MENU_GROUP_MUSIC_FOLDER = 10;
+
+ private ListView artistList;
+ private View folderButton;
+ private TextView folderName;
+ private List<MusicFolder> musicFolders;
+
+ /**
+ * Called when the activity is first created.
+ */
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.select_artist);
+
+ artistList = (ListView) findViewById(R.id.select_artist_list);
+ artistList.setOnItemClickListener(this);
+
+ folderButton = LayoutInflater.from(this).inflate(R.layout.select_artist_header, artistList, false);
+ folderName = (TextView) folderButton.findViewById(R.id.select_artist_folder_2);
+
+ if (!Util.isOffline(this)) {
+ artistList.addHeaderView(folderButton);
+ }
+
+ registerForContextMenu(artistList);
+
+ setTitle(Util.isOffline(this) ? R.string.music_library_label_offline : R.string.music_library_label);
+
+ // Button 1: shuffle
+ ImageButton shuffleButton = (ImageButton) findViewById(R.id.action_button_1);
+ shuffleButton.setImageResource(R.drawable.action_shuffle);
+ shuffleButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Intent intent = new Intent(SelectArtistActivity.this, DownloadActivity.class);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_SHUFFLE, true);
+ Util.startActivityWithoutTransition(SelectArtistActivity.this, intent);
+ }
+ });
+
+ // Button 2: refresh
+ ImageButton refreshButton = (ImageButton) findViewById(R.id.action_button_2);
+ refreshButton.setImageResource(R.drawable.action_refresh);
+ refreshButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ refresh();
+ }
+ });
+
+ musicFolders = null;
+ load();
+ }
+
+ private void refresh() {
+ finish();
+ Intent intent = getIntent();
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_REFRESH, true);
+ Util.startActivityWithoutTransition(this, intent);
+ }
+
+ private void selectFolder() {
+ folderButton.showContextMenu();
+ }
+
+ private void load() {
+ BackgroundTask<Indexes> task = new TabActivityBackgroundTask<Indexes>(this) {
+ @Override
+ protected Indexes doInBackground() throws Throwable {
+ boolean refresh = getIntent().getBooleanExtra(Constants.INTENT_EXTRA_NAME_REFRESH, false);
+ MusicService musicService = MusicServiceFactory.getMusicService(SelectArtistActivity.this);
+ if (!Util.isOffline(SelectArtistActivity.this)) {
+ musicFolders = musicService.getMusicFolders(refresh, SelectArtistActivity.this, this);
+ }
+ String musicFolderId = Util.getSelectedMusicFolderId(SelectArtistActivity.this);
+ return musicService.getIndexes(musicFolderId, refresh, SelectArtistActivity.this, this);
+ }
+
+ @Override
+ protected void done(Indexes result) {
+ List<Artist> artists = new ArrayList<Artist>(result.getShortcuts().size() + result.getArtists().size());
+ artists.addAll(result.getShortcuts());
+ artists.addAll(result.getArtists());
+ artistList.setAdapter(new ArtistAdapter(SelectArtistActivity.this, artists));
+
+ // Display selected music folder
+ if (musicFolders != null) {
+ String musicFolderId = Util.getSelectedMusicFolderId(SelectArtistActivity.this);
+ if (musicFolderId == null) {
+ folderName.setText(R.string.select_artist_all_folders);
+ } else {
+ for (MusicFolder musicFolder : musicFolders) {
+ if (musicFolder.getId().equals(musicFolderId)) {
+ folderName.setText(musicFolder.getName());
+ break;
+ }
+ }
+ }
+ }
+ }
+ };
+ task.execute();
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ if (view == folderButton) {
+ selectFolder();
+ } else {
+ Artist artist = (Artist) parent.getItemAtPosition(position);
+ Intent intent = new Intent(this, SelectAlbumActivity.class);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_ID, artist.getId());
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_NAME, artist.getName());
+ Util.startActivityWithoutTransition(this, intent);
+ }
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, view, menuInfo);
+
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
+
+ if (artistList.getItemAtPosition(info.position) instanceof Artist) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.select_artist_context, menu);
+ } else if (info.position == 0) {
+ String musicFolderId = Util.getSelectedMusicFolderId(this);
+ MenuItem menuItem = menu.add(MENU_GROUP_MUSIC_FOLDER, -1, 0, R.string.select_artist_all_folders);
+ if (musicFolderId == null) {
+ menuItem.setChecked(true);
+ }
+ if (musicFolders != null) {
+ for (int i = 0; i < musicFolders.size(); i++) {
+ MusicFolder musicFolder = musicFolders.get(i);
+ menuItem = menu.add(MENU_GROUP_MUSIC_FOLDER, i, i + 1, musicFolder.getName());
+ if (musicFolder.getId().equals(musicFolderId)) {
+ menuItem.setChecked(true);
+ }
+ }
+ }
+ menu.setGroupCheckable(MENU_GROUP_MUSIC_FOLDER, true, true);
+ }
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem menuItem) {
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
+
+ Artist artist = (Artist) artistList.getItemAtPosition(info.position);
+
+ if (artist != null) {
+ switch (menuItem.getItemId()) {
+ case R.id.artist_menu_play_now:
+ downloadRecursively(artist.getId(), false, false, true);
+ break;
+ case R.id.artist_menu_play_last:
+ downloadRecursively(artist.getId(), false, true, false);
+ break;
+ case R.id.artist_menu_pin:
+ downloadRecursively(artist.getId(), true, true, false);
+ break;
+ default:
+ return super.onContextItemSelected(menuItem);
+ }
+ } else if (info.position == 0) {
+ MusicFolder selectedFolder = menuItem.getItemId() == -1 ? null : musicFolders.get(menuItem.getItemId());
+ String musicFolderId = selectedFolder == null ? null : selectedFolder.getId();
+ String musicFolderName = selectedFolder == null ? getString(R.string.select_artist_all_folders)
+ : selectedFolder.getName();
+ Util.setSelectedMusicFolderId(this, musicFolderId);
+ folderName.setText(musicFolderName);
+ refresh();
+ }
+
+ return true;
+ }
+} \ No newline at end of file
diff --git a/subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/SelectPlaylistActivity.java b/subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/SelectPlaylistActivity.java
new file mode 100644
index 00000000..253124b5
--- /dev/null
+++ b/subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/SelectPlaylistActivity.java
@@ -0,0 +1,141 @@
+/*
+ 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 <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+
+package net.sourceforge.subsonic.androidapp.activity;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.ContextMenu;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ImageButton;
+import android.widget.ListView;
+import net.sourceforge.subsonic.androidapp.R;
+import net.sourceforge.subsonic.androidapp.domain.Playlist;
+import net.sourceforge.subsonic.androidapp.service.MusicServiceFactory;
+import net.sourceforge.subsonic.androidapp.service.MusicService;
+import net.sourceforge.subsonic.androidapp.util.BackgroundTask;
+import net.sourceforge.subsonic.androidapp.util.Constants;
+import net.sourceforge.subsonic.androidapp.util.PlaylistAdapter;
+import net.sourceforge.subsonic.androidapp.util.TabActivityBackgroundTask;
+import net.sourceforge.subsonic.androidapp.util.Util;
+
+import java.util.List;
+
+public class SelectPlaylistActivity extends SubsonicTabActivity implements AdapterView.OnItemClickListener {
+
+ private static final int MENU_ITEM_PLAY_ALL = 1;
+
+ private ListView list;
+ private View emptyTextView;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.select_playlist);
+
+ list = (ListView) findViewById(R.id.select_playlist_list);
+ emptyTextView = findViewById(R.id.select_playlist_empty);
+ list.setOnItemClickListener(this);
+ registerForContextMenu(list);
+
+ // Title: Playlists
+ setTitle(R.string.playlist_label);
+
+ // Button 1: gone
+ ImageButton searchButton = (ImageButton)findViewById(R.id.action_button_1);
+ searchButton.setVisibility(View.GONE);
+
+ // Button 2: refresh
+ ImageButton refreshButton = (ImageButton) findViewById(R.id.action_button_2);
+ refreshButton.setImageResource(R.drawable.action_refresh);
+ refreshButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ refresh();
+ }
+ });
+
+ load();
+ }
+
+ private void refresh() {
+ finish();
+ Intent intent = new Intent(this, SelectPlaylistActivity.class);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_REFRESH, true);
+ Util.startActivityWithoutTransition(this, intent);
+ }
+
+ private void load() {
+ BackgroundTask<List<Playlist>> task = new TabActivityBackgroundTask<List<Playlist>>(this) {
+ @Override
+ protected List<Playlist> doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(SelectPlaylistActivity.this);
+ boolean refresh = getIntent().getBooleanExtra(Constants.INTENT_EXTRA_NAME_REFRESH, false);
+ return musicService.getPlaylists(refresh, SelectPlaylistActivity.this, this);
+ }
+
+ @Override
+ protected void done(List<Playlist> result) {
+ list.setAdapter(new PlaylistAdapter(SelectPlaylistActivity.this, PlaylistAdapter.PlaylistComparator.sort(result)));
+ emptyTextView.setVisibility(result.isEmpty() ? View.VISIBLE : View.GONE);
+ }
+ };
+ task.execute();
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, view, menuInfo);
+ menu.add(Menu.NONE, MENU_ITEM_PLAY_ALL, MENU_ITEM_PLAY_ALL, R.string.common_play_now);
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem menuItem) {
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
+ Playlist playlist = (Playlist) list.getItemAtPosition(info.position);
+
+ switch (menuItem.getItemId()) {
+ case MENU_ITEM_PLAY_ALL:
+ Intent intent = new Intent(SelectPlaylistActivity.this, SelectAlbumActivity.class);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, playlist.getId());
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME, playlist.getName());
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_AUTOPLAY, true);
+ Util.startActivityWithoutTransition(SelectPlaylistActivity.this, intent);
+ break;
+ default:
+ return super.onContextItemSelected(menuItem);
+ }
+ return true;
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+
+ Playlist playlist = (Playlist) parent.getItemAtPosition(position);
+
+ Intent intent = new Intent(SelectPlaylistActivity.this, SelectAlbumActivity.class);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, playlist.getId());
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME, playlist.getName());
+ Util.startActivityWithoutTransition(SelectPlaylistActivity.this, intent);
+ }
+
+} \ No newline at end of file
diff --git a/subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/SettingsActivity.java b/subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/SettingsActivity.java
new file mode 100644
index 00000000..f726a2af
--- /dev/null
+++ b/subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/SettingsActivity.java
@@ -0,0 +1,297 @@
+/*
+ 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 <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.androidapp.activity;
+
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.preference.EditTextPreference;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.PreferenceActivity;
+import android.preference.PreferenceScreen;
+import android.provider.SearchRecentSuggestions;
+import android.util.Log;
+import net.sourceforge.subsonic.androidapp.R;
+import net.sourceforge.subsonic.androidapp.provider.SearchSuggestionProvider;
+import net.sourceforge.subsonic.androidapp.service.DownloadService;
+import net.sourceforge.subsonic.androidapp.service.DownloadServiceImpl;
+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.ErrorDialog;
+import net.sourceforge.subsonic.androidapp.util.FileUtil;
+import net.sourceforge.subsonic.androidapp.util.ModalBackgroundTask;
+import net.sourceforge.subsonic.androidapp.util.Util;
+
+import java.io.File;
+import java.net.URL;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public class SettingsActivity extends PreferenceActivity implements SharedPreferences.OnSharedPreferenceChangeListener {
+
+ private static final String TAG = SettingsActivity.class.getSimpleName();
+ private final Map<String, ServerSettings> serverSettings = new LinkedHashMap<String, ServerSettings>();
+ private boolean testingConnection;
+ private ListPreference theme;
+ private ListPreference maxBitrateWifi;
+ private ListPreference maxBitrateMobile;
+ private ListPreference cacheSize;
+ private EditTextPreference cacheLocation;
+ private ListPreference preloadCount;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ addPreferencesFromResource(R.xml.settings);
+
+ theme = (ListPreference) findPreference(Constants.PREFERENCES_KEY_THEME);
+ maxBitrateWifi = (ListPreference) findPreference(Constants.PREFERENCES_KEY_MAX_BITRATE_WIFI);
+ maxBitrateMobile = (ListPreference) findPreference(Constants.PREFERENCES_KEY_MAX_BITRATE_MOBILE);
+ cacheSize = (ListPreference) findPreference(Constants.PREFERENCES_KEY_CACHE_SIZE);
+ cacheLocation = (EditTextPreference) findPreference(Constants.PREFERENCES_KEY_CACHE_LOCATION);
+ preloadCount = (ListPreference) findPreference(Constants.PREFERENCES_KEY_PRELOAD_COUNT);
+
+ findPreference("testConnection1").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ testConnection(1);
+ return false;
+ }
+ });
+
+ findPreference("testConnection2").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ testConnection(2);
+ return false;
+ }
+ });
+
+ findPreference("testConnection3").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ testConnection(3);
+ return false;
+ }
+ });
+
+ findPreference("clearSearchHistory").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ SearchRecentSuggestions suggestions = new SearchRecentSuggestions(SettingsActivity.this, SearchSuggestionProvider.AUTHORITY, SearchSuggestionProvider.MODE);
+ suggestions.clearHistory();
+ Util.toast(SettingsActivity.this, R.string.settings_search_history_cleared);
+ return false;
+ }
+ });
+
+ for (int i = 1; i <= 3; i++) {
+ String instance = String.valueOf(i);
+ serverSettings.put(instance, new ServerSettings(instance));
+ }
+
+ SharedPreferences prefs = Util.getPreferences(this);
+ prefs.registerOnSharedPreferenceChangeListener(this);
+
+ update();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+
+ SharedPreferences prefs = Util.getPreferences(this);
+ prefs.unregisterOnSharedPreferenceChangeListener(this);
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+ Log.d(TAG, "Preference changed: " + key);
+ update();
+
+ if (Constants.PREFERENCES_KEY_HIDE_MEDIA.equals(key)) {
+ setHideMedia(sharedPreferences.getBoolean(key, false));
+ }
+ else if (Constants.PREFERENCES_KEY_MEDIA_BUTTONS.equals(key)) {
+ setMediaButtonsEnabled(sharedPreferences.getBoolean(key, true));
+ }
+ else if (Constants.PREFERENCES_KEY_CACHE_LOCATION.equals(key)) {
+ setCacheLocation(sharedPreferences.getString(key, ""));
+ }
+ }
+
+ private void update() {
+ if (testingConnection) {
+ return;
+ }
+
+ theme.setSummary(theme.getEntry());
+ maxBitrateWifi.setSummary(maxBitrateWifi.getEntry());
+ maxBitrateMobile.setSummary(maxBitrateMobile.getEntry());
+ cacheSize.setSummary(cacheSize.getEntry());
+ cacheLocation.setSummary(cacheLocation.getText());
+ preloadCount.setSummary(preloadCount.getEntry());
+ for (ServerSettings ss : serverSettings.values()) {
+ ss.update();
+ }
+ }
+
+ private void setHideMedia(boolean hide) {
+ File nomediaDir = new File(FileUtil.getSubsonicDirectory(), ".nomedia");
+ if (hide && !nomediaDir.exists()) {
+ if (!nomediaDir.mkdir()) {
+ Log.w(TAG, "Failed to create " + nomediaDir);
+ }
+ } else if (nomediaDir.exists()) {
+ if (!nomediaDir.delete()) {
+ Log.w(TAG, "Failed to delete " + nomediaDir);
+ }
+ }
+ Util.toast(this, R.string.settings_hide_media_toast, false);
+ }
+
+ private void setMediaButtonsEnabled(boolean enabled) {
+ if (enabled) {
+ Util.registerMediaButtonEventReceiver(this);
+ } else {
+ Util.unregisterMediaButtonEventReceiver(this);
+ }
+ }
+
+ private void setCacheLocation(String path) {
+ File dir = new File(path);
+ if (!FileUtil.ensureDirectoryExistsAndIsReadWritable(dir)) {
+ Util.toast(this, R.string.settings_cache_location_error, false);
+
+ // Reset it to the default.
+ String defaultPath = FileUtil.getDefaultMusicDirectory().getPath();
+ if (!defaultPath.equals(path)) {
+ SharedPreferences prefs = Util.getPreferences(this);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putString(Constants.PREFERENCES_KEY_CACHE_LOCATION, defaultPath);
+ editor.commit();
+ cacheLocation.setSummary(defaultPath);
+ cacheLocation.setText(defaultPath);
+ }
+
+ // Clear download queue.
+ DownloadService downloadService = DownloadServiceImpl.getInstance();
+ downloadService.clear();
+ }
+ }
+
+ private void testConnection(final int instance) {
+ ModalBackgroundTask<Boolean> task = new ModalBackgroundTask<Boolean>(this, false) {
+ private int previousInstance;
+
+ @Override
+ protected Boolean doInBackground() throws Throwable {
+ updateProgress(R.string.settings_testing_connection);
+
+ previousInstance = Util.getActiveServer(SettingsActivity.this);
+ testingConnection = true;
+ Util.setActiveServer(SettingsActivity.this, instance);
+ try {
+ MusicService musicService = MusicServiceFactory.getMusicService(SettingsActivity.this);
+ musicService.ping(SettingsActivity.this, this);
+ return musicService.isLicenseValid(SettingsActivity.this, null);
+ } finally {
+ Util.setActiveServer(SettingsActivity.this, previousInstance);
+ testingConnection = false;
+ }
+ }
+
+ @Override
+ protected void done(Boolean licenseValid) {
+ if (licenseValid) {
+ Util.toast(SettingsActivity.this, R.string.settings_testing_ok);
+ } else {
+ Util.toast(SettingsActivity.this, R.string.settings_testing_unlicensed);
+ }
+ }
+
+ @Override
+ protected void cancel() {
+ super.cancel();
+ Util.setActiveServer(SettingsActivity.this, previousInstance);
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ Log.w(TAG, error.toString(), error);
+ new ErrorDialog(SettingsActivity.this, getResources().getString(R.string.settings_connection_failure) +
+ " " + getErrorMessage(error), false);
+ }
+ };
+ task.execute();
+ }
+
+ private class ServerSettings {
+ private EditTextPreference serverName;
+ private EditTextPreference serverUrl;
+ private EditTextPreference username;
+ private PreferenceScreen screen;
+
+ private ServerSettings(String instance) {
+
+ screen = (PreferenceScreen) findPreference("server" + instance);
+ serverName = (EditTextPreference) findPreference(Constants.PREFERENCES_KEY_SERVER_NAME + instance);
+ serverUrl = (EditTextPreference) findPreference(Constants.PREFERENCES_KEY_SERVER_URL + instance);
+ username = (EditTextPreference) findPreference(Constants.PREFERENCES_KEY_USERNAME + instance);
+
+ serverUrl.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object value) {
+ try {
+ String url = (String) value;
+ new URL(url);
+ if (!url.equals(url.trim()) || url.contains("@")) {
+ throw new Exception();
+ }
+ } catch (Exception x) {
+ new ErrorDialog(SettingsActivity.this, R.string.settings_invalid_url, false);
+ return false;
+ }
+ return true;
+ }
+ });
+
+ username.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object value) {
+ String username = (String) value;
+ if (username == null || !username.equals(username.trim())) {
+ new ErrorDialog(SettingsActivity.this, R.string.settings_invalid_username, false);
+ return false;
+ }
+ return true;
+ }
+ });
+ }
+
+ public void update() {
+ serverName.setSummary(serverName.getText());
+ serverUrl.setSummary(serverUrl.getText());
+ username.setSummary(username.getText());
+ screen.setSummary(serverUrl.getText());
+ screen.setTitle(serverName.getText());
+ }
+ }
+} \ No newline at end of file
diff --git a/subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/SubsonicTabActivity.java b/subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/SubsonicTabActivity.java
new file mode 100644
index 00000000..8c9c0687
--- /dev/null
+++ b/subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/SubsonicTabActivity.java
@@ -0,0 +1,383 @@
+/*
+ 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 <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.androidapp.activity;
+
+import java.io.File;
+import java.io.PrintWriter;
+import java.util.LinkedList;
+import java.util.List;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.graphics.Typeface;
+import android.media.AudioManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Environment;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.Window;
+import android.widget.TextView;
+import net.sourceforge.subsonic.androidapp.R;
+import net.sourceforge.subsonic.androidapp.domain.MusicDirectory;
+import net.sourceforge.subsonic.androidapp.service.DownloadService;
+import net.sourceforge.subsonic.androidapp.service.DownloadServiceImpl;
+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.ImageLoader;
+import net.sourceforge.subsonic.androidapp.util.ModalBackgroundTask;
+import net.sourceforge.subsonic.androidapp.util.Util;
+
+/**
+ * @author Sindre Mehus
+ */
+public class SubsonicTabActivity extends Activity {
+
+ private static final String TAG = SubsonicTabActivity.class.getSimpleName();
+ private static ImageLoader IMAGE_LOADER;
+
+ private boolean destroyed;
+ private View homeButton;
+ private View musicButton;
+ private View searchButton;
+ private View playlistButton;
+ private View nowPlayingButton;
+
+ @Override
+ protected void onCreate(Bundle bundle) {
+ setUncaughtExceptionHandler();
+ applyTheme();
+ super.onCreate(bundle);
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+ startService(new Intent(this, DownloadServiceImpl.class));
+ setVolumeControlStream(AudioManager.STREAM_MUSIC);
+ }
+
+ @Override
+ protected void onPostCreate(Bundle bundle) {
+ super.onPostCreate(bundle);
+
+ homeButton = findViewById(R.id.button_bar_home);
+ homeButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Intent intent = new Intent(SubsonicTabActivity.this, MainActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ Util.startActivityWithoutTransition(SubsonicTabActivity.this, intent);
+ }
+ });
+
+ musicButton = findViewById(R.id.button_bar_music);
+ musicButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Intent intent = new Intent(SubsonicTabActivity.this, SelectArtistActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ Util.startActivityWithoutTransition(SubsonicTabActivity.this, intent);
+ }
+ });
+
+ searchButton = findViewById(R.id.button_bar_search);
+ searchButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Intent intent = new Intent(SubsonicTabActivity.this, SearchActivity.class);
+ intent.putExtra(Constants.INTENT_EXTRA_REQUEST_SEARCH, true);
+ Util.startActivityWithoutTransition(SubsonicTabActivity.this, intent);
+ }
+ });
+
+ playlistButton = findViewById(R.id.button_bar_playlists);
+ playlistButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Intent intent = new Intent(SubsonicTabActivity.this, SelectPlaylistActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ Util.startActivityWithoutTransition(SubsonicTabActivity.this, intent);
+ }
+ });
+
+ nowPlayingButton = findViewById(R.id.button_bar_now_playing);
+ nowPlayingButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Util.startActivityWithoutTransition(SubsonicTabActivity.this, DownloadActivity.class);
+ }
+ });
+
+ if (this instanceof MainActivity) {
+ homeButton.setEnabled(false);
+ } else if (this instanceof SelectAlbumActivity || this instanceof SelectArtistActivity) {
+ musicButton.setEnabled(false);
+ } else if (this instanceof SearchActivity) {
+ searchButton.setEnabled(false);
+ } else if (this instanceof SelectPlaylistActivity) {
+ playlistButton.setEnabled(false);
+ } else if (this instanceof DownloadActivity || this instanceof LyricsActivity) {
+ nowPlayingButton.setEnabled(false);
+ }
+
+ updateButtonVisibility();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ Util.registerMediaButtonEventReceiver(this);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.main, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+
+ case R.id.menu_exit:
+ Intent intent = new Intent(this, MainActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_EXIT, true);
+ Util.startActivityWithoutTransition(this, intent);
+ return true;
+
+ case R.id.menu_settings:
+ startActivity(new Intent(this, SettingsActivity.class));
+ return true;
+
+ case R.id.menu_help:
+ startActivity(new Intent(this, HelpActivity.class));
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ destroyed = true;
+ getImageLoader().clear();
+ }
+
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ boolean isVolumeDown = keyCode == KeyEvent.KEYCODE_VOLUME_DOWN;
+ boolean isVolumeUp = keyCode == KeyEvent.KEYCODE_VOLUME_UP;
+ boolean isVolumeAdjust = isVolumeDown || isVolumeUp;
+ boolean isJukebox = getDownloadService() != null && getDownloadService().isJukeboxEnabled();
+
+ if (isVolumeAdjust && isJukebox) {
+ getDownloadService().adjustJukeboxVolume(isVolumeUp);
+ return true;
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public void finish() {
+ super.finish();
+ Util.disablePendingTransition(this);
+ }
+
+ @Override
+ public void setTitle(CharSequence title) {
+ super.setTitle(title);
+
+ // Set the font of title in the action bar.
+ TextView text = (TextView) findViewById(R.id.actionbar_title_text);
+ Typeface typeface = Typeface.createFromAsset(getAssets(), "fonts/Storopia.ttf");
+ text.setTypeface(typeface);
+
+ text.setText(title);
+ }
+
+ @Override
+ public void setTitle(int titleId) {
+ setTitle(getString(titleId));
+ }
+
+ private void applyTheme() {
+ String theme = Util.getTheme(this);
+ if ("dark".equals(theme)) {
+ setTheme(android.R.style.Theme);
+ } else if ("light".equals(theme)) {
+ setTheme(android.R.style.Theme_Light);
+ }
+ }
+
+ public boolean isDestroyed() {
+ return destroyed;
+ }
+
+ private void updateButtonVisibility() {
+ int visibility = Util.isOffline(this) ? View.GONE : View.VISIBLE;
+ searchButton.setVisibility(visibility);
+ playlistButton.setVisibility(visibility);
+ }
+
+ public void setProgressVisible(boolean visible) {
+ View view = findViewById(R.id.tab_progress);
+ if (view != null) {
+ view.setVisibility(visible ? View.VISIBLE : View.GONE);
+ }
+ }
+
+ public void updateProgress(String message) {
+ TextView view = (TextView) findViewById(R.id.tab_progress_message);
+ if (view != null) {
+ view.setText(message);
+ }
+ }
+
+ public DownloadService getDownloadService() {
+ // If service is not available, request it to start and wait for it.
+ for (int i = 0; i < 5; i++) {
+ DownloadService downloadService = DownloadServiceImpl.getInstance();
+ if (downloadService != null) {
+ return downloadService;
+ }
+ Log.w(TAG, "DownloadService not running. Attempting to start it.");
+ startService(new Intent(this, DownloadServiceImpl.class));
+ Util.sleepQuietly(50L);
+ }
+ return DownloadServiceImpl.getInstance();
+ }
+
+ protected void warnIfNetworkOrStorageUnavailable() {
+ if (!Util.isExternalStoragePresent()) {
+ Util.toast(this, R.string.select_album_no_sdcard);
+ } else if (!Util.isOffline(this) && !Util.isNetworkConnected(this)) {
+ Util.toast(this, R.string.select_album_no_network);
+ }
+ }
+
+ protected synchronized ImageLoader getImageLoader() {
+ if (IMAGE_LOADER == null) {
+ IMAGE_LOADER = new ImageLoader(this);
+ }
+ return IMAGE_LOADER;
+ }
+
+ protected void downloadRecursively(final String id, final boolean save, final boolean append, final boolean autoplay) {
+ ModalBackgroundTask<List<MusicDirectory.Entry>> task = new ModalBackgroundTask<List<MusicDirectory.Entry>>(this, false) {
+
+ private static final int MAX_SONGS = 500;
+
+ @Override
+ protected List<MusicDirectory.Entry> doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(SubsonicTabActivity.this);
+ MusicDirectory root = musicService.getMusicDirectory(id, false, SubsonicTabActivity.this, this);
+ List<MusicDirectory.Entry> songs = new LinkedList<MusicDirectory.Entry>();
+ getSongsRecursively(root, songs);
+ return songs;
+ }
+
+ private void getSongsRecursively(MusicDirectory parent, List<MusicDirectory.Entry> songs) throws Exception {
+ if (songs.size() > MAX_SONGS) {
+ return;
+ }
+
+ for (MusicDirectory.Entry song : parent.getChildren(false, true)) {
+ if (!song.isVideo()) {
+ songs.add(song);
+ }
+ }
+ for (MusicDirectory.Entry dir : parent.getChildren(true, false)) {
+ MusicService musicService = MusicServiceFactory.getMusicService(SubsonicTabActivity.this);
+ getSongsRecursively(musicService.getMusicDirectory(dir.getId(), false, SubsonicTabActivity.this, this), songs);
+ }
+ }
+
+ @Override
+ protected void done(List<MusicDirectory.Entry> songs) {
+ DownloadService downloadService = getDownloadService();
+ if (!songs.isEmpty() && downloadService != null) {
+ if (!append) {
+ downloadService.clear();
+ }
+ warnIfNetworkOrStorageUnavailable();
+ downloadService.download(songs, save, autoplay, false);
+ Util.startActivityWithoutTransition(SubsonicTabActivity.this, DownloadActivity.class);
+ }
+ }
+ };
+
+ task.execute();
+ }
+
+ private void setUncaughtExceptionHandler() {
+ Thread.UncaughtExceptionHandler handler = Thread.getDefaultUncaughtExceptionHandler();
+ if (!(handler instanceof SubsonicUncaughtExceptionHandler)) {
+ Thread.setDefaultUncaughtExceptionHandler(new SubsonicUncaughtExceptionHandler(this));
+ }
+ }
+
+ /**
+ * Logs the stack trace of uncaught exceptions to a file on the SD card.
+ */
+ private static class SubsonicUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
+
+ private final Thread.UncaughtExceptionHandler defaultHandler;
+ private final Context context;
+
+ private SubsonicUncaughtExceptionHandler(Context context) {
+ this.context = context;
+ defaultHandler = Thread.getDefaultUncaughtExceptionHandler();
+ }
+
+ @Override
+ public void uncaughtException(Thread thread, Throwable throwable) {
+ File file = null;
+ PrintWriter printWriter = null;
+ try {
+
+ PackageInfo packageInfo = context.getPackageManager().getPackageInfo("net.sourceforge.subsonic.androidapp", 0);
+ file = new File(Environment.getExternalStorageDirectory(), "subsonic-stacktrace.txt");
+ printWriter = new PrintWriter(file);
+ printWriter.println("Android API level: " + Build.VERSION.SDK);
+ printWriter.println("Subsonic version name: " + packageInfo.versionName);
+ printWriter.println("Subsonic version code: " + packageInfo.versionCode);
+ printWriter.println();
+ throwable.printStackTrace(printWriter);
+ Log.i(TAG, "Stack trace written to " + file);
+ } catch (Throwable x) {
+ Log.e(TAG, "Failed to write stack trace to " + file, x);
+ } finally {
+ Util.close(printWriter);
+ if (defaultHandler != null) {
+ defaultHandler.uncaughtException(thread, throwable);
+ }
+
+ }
+ }
+ }
+}
+
diff --git a/subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/VoiceQueryReceiverActivity.java b/subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/VoiceQueryReceiverActivity.java
new file mode 100644
index 00000000..205c2fe7
--- /dev/null
+++ b/subsonic-android/src/net/sourceforge/subsonic/androidapp/activity/VoiceQueryReceiverActivity.java
@@ -0,0 +1,59 @@
+/*
+ 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 <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+
+package net.sourceforge.subsonic.androidapp.activity;
+
+import android.app.Activity;
+import android.app.SearchManager;
+import android.content.Intent;
+import android.os.Bundle;
+import android.provider.SearchRecentSuggestions;
+import net.sourceforge.subsonic.androidapp.util.Constants;
+import net.sourceforge.subsonic.androidapp.util.Util;
+import net.sourceforge.subsonic.androidapp.provider.SearchSuggestionProvider;
+
+/**
+ * Receives voice search queries and forwards to the SearchActivity.
+ *
+ * http://android-developers.blogspot.com/2010/09/supporting-new-music-voice-action.html
+ *
+ * @author Sindre Mehus
+ */
+public class VoiceQueryReceiverActivity extends Activity {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ String query = getIntent().getStringExtra(SearchManager.QUERY);
+
+ if (query != null) {
+ SearchRecentSuggestions suggestions = new SearchRecentSuggestions(this, SearchSuggestionProvider.AUTHORITY,
+ SearchSuggestionProvider.MODE);
+ suggestions.saveRecentQuery(query, null);
+
+ Intent intent = new Intent(VoiceQueryReceiverActivity.this, SearchActivity.class);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_QUERY, query);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_AUTOPLAY, true);
+ Util.startActivityWithoutTransition(VoiceQueryReceiverActivity.this, intent);
+ }
+ finish();
+ Util.disablePendingTransition(this);
+ }
+} \ No newline at end of file