aboutsummaryrefslogtreecommitdiff
path: root/src/github/daneren2005/dsub/view
diff options
context:
space:
mode:
Diffstat (limited to 'src/github/daneren2005/dsub/view')
-rw-r--r--src/github/daneren2005/dsub/view/AlbumListAdapter.java83
-rw-r--r--src/github/daneren2005/dsub/view/AlbumView.java93
-rw-r--r--src/github/daneren2005/dsub/view/ArtistAdapter.java95
-rw-r--r--src/github/daneren2005/dsub/view/ArtistEntryView.java83
-rw-r--r--src/github/daneren2005/dsub/view/ArtistView.java84
-rw-r--r--src/github/daneren2005/dsub/view/AutoRepeatButton.java86
-rw-r--r--src/github/daneren2005/dsub/view/ChangeLog.java552
-rw-r--r--src/github/daneren2005/dsub/view/ChatAdapter.java100
-rw-r--r--src/github/daneren2005/dsub/view/EntryAdapter.java79
-rw-r--r--src/github/daneren2005/dsub/view/ErrorDialog.java70
-rw-r--r--src/github/daneren2005/dsub/view/FadeOutAnimation.java77
-rw-r--r--src/github/daneren2005/dsub/view/GenreAdapter.java59
-rw-r--r--src/github/daneren2005/dsub/view/GenreView.java53
-rw-r--r--src/github/daneren2005/dsub/view/MergeAdapter.java292
-rw-r--r--src/github/daneren2005/dsub/view/MyViewFlipper.java53
-rw-r--r--src/github/daneren2005/dsub/view/PlaylistAdapter.java68
-rw-r--r--src/github/daneren2005/dsub/view/PlaylistView.java76
-rw-r--r--src/github/daneren2005/dsub/view/PodcastChannelAdapter.java59
-rw-r--r--src/github/daneren2005/dsub/view/PodcastChannelView.java76
-rw-r--r--src/github/daneren2005/dsub/view/SackOfViewsAdapter.java181
-rw-r--r--src/github/daneren2005/dsub/view/SongView.java241
-rw-r--r--src/github/daneren2005/dsub/view/UpdateView.java133
-rw-r--r--src/github/daneren2005/dsub/view/VisualizerView.java137
23 files changed, 2830 insertions, 0 deletions
diff --git a/src/github/daneren2005/dsub/view/AlbumListAdapter.java b/src/github/daneren2005/dsub/view/AlbumListAdapter.java
new file mode 100644
index 00000000..3ff8350b
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/AlbumListAdapter.java
@@ -0,0 +1,83 @@
+/*
+ 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 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.view;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import com.commonsware.cwac.endless.EndlessAdapter;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import java.util.List;
+
+public class AlbumListAdapter extends EndlessAdapter {
+ Context context;
+ ArrayAdapter<MusicDirectory.Entry> adapter;
+ String type;
+ String extra;
+ int size;
+ int offset;
+ List<MusicDirectory.Entry> entries;
+
+ public AlbumListAdapter(Context context, ArrayAdapter<MusicDirectory.Entry> adapter, String type, String extra, int size) {
+ super(adapter);
+ this.context = context;
+ this.adapter = adapter;
+ this.type = type;
+ this.extra = extra;
+ this.size = size;
+ this.offset = size;
+ }
+
+ @Override
+ protected boolean cacheInBackground() throws Exception {
+ MusicService service = MusicServiceFactory.getMusicService(context);
+ MusicDirectory result;
+ if("genres".equals(type)) {
+ result = service.getSongsByGenre(extra, size, offset, context, null);
+ } else {
+ result = service.getAlbumList(type, size, offset, context, null);
+ }
+ entries = result.getChildren();
+ if(entries.size() > 0) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ protected void appendCachedData() {
+ for(MusicDirectory.Entry entry: entries) {
+ adapter.add(entry);
+ }
+ offset += entries.size();
+ }
+
+ @Override
+ protected View getPendingView(ViewGroup parent) {
+ View progress = LayoutInflater.from(context).inflate(R.layout.tab_progress, null);
+ progress.setVisibility(View.VISIBLE);
+ return progress;
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/AlbumView.java b/src/github/daneren2005/dsub/view/AlbumView.java
new file mode 100644
index 00000000..3a7b895e
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/AlbumView.java
@@ -0,0 +1,93 @@
+/*
+ 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 github.daneren2005.dsub.view;
+
+import android.content.Context;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.TextView;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.ImageLoader;
+import github.daneren2005.dsub.util.Util;
+import java.io.File;
+/**
+ * Used to display albums in a {@code ListView}.
+ *
+ * @author Sindre Mehus
+ */
+public class AlbumView extends UpdateView {
+ private static final String TAG = AlbumView.class.getSimpleName();
+
+ private Context context;
+ private MusicDirectory.Entry album;
+
+ private TextView titleView;
+ private TextView artistView;
+ private View coverArtView;
+ private ImageButton starButton;
+ private ImageView moreButton;
+
+ public AlbumView(Context context) {
+ super(context);
+ this.context = context;
+ LayoutInflater.from(context).inflate(R.layout.album_list_item, this, true);
+
+ titleView = (TextView) findViewById(R.id.album_title);
+ artistView = (TextView) findViewById(R.id.album_artist);
+ coverArtView = findViewById(R.id.album_coverart);
+ starButton = (ImageButton) findViewById(R.id.album_star);
+
+ moreButton = (ImageView) findViewById(R.id.album_more);
+ moreButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ v.showContextMenu();
+ }
+ });
+ }
+
+ public void setAlbum(MusicDirectory.Entry album, ImageLoader imageLoader) {
+ this.album = album;
+
+ titleView.setText(album.getTitle());
+ artistView.setText(album.getArtist());
+ artistView.setVisibility(album.getArtist() == null ? View.GONE : View.VISIBLE);
+ imageLoader.loadImage(coverArtView, album, false, true);
+
+ starButton.setVisibility(!album.isStarred() ? View.GONE : View.VISIBLE);
+ starButton.setFocusable(false);
+
+ update();
+ }
+
+ @Override
+ protected void update() {
+ starButton.setVisibility(!album.isStarred() ? View.GONE : View.VISIBLE);
+ File file = FileUtil.getAlbumDirectory(context, album);
+ if(file.exists()) {
+ moreButton.setImageResource(R.drawable.list_item_more_shaded);
+ } else {
+ moreButton.setImageResource(R.drawable.list_item_more);
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/ArtistAdapter.java b/src/github/daneren2005/dsub/view/ArtistAdapter.java
new file mode 100644
index 00000000..7e9bf218
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/ArtistAdapter.java
@@ -0,0 +1,95 @@
+/*
+ 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 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.view;
+
+import android.content.Context;
+import github.daneren2005.dsub.R;
+import java.util.List;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.SectionIndexer;
+import github.daneren2005.dsub.domain.Artist;
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+/**
+ * @author Sindre Mehus
+ */
+public class ArtistAdapter extends ArrayAdapter<Artist> implements SectionIndexer {
+
+ private final Context activity;
+
+ // Both arrays are indexed by section ID.
+ private final Object[] sections;
+ private final Integer[] positions;
+
+ public ArtistAdapter(Context activity, List<Artist> artists) {
+ super(activity, R.layout.artist_list_item, artists);
+ this.activity = activity;
+
+ Set<String> sectionSet = new LinkedHashSet<String>(30);
+ List<Integer> positionList = new ArrayList<Integer>(30);
+ for (int i = 0; i < artists.size(); i++) {
+ Artist artist = artists.get(i);
+ String index = artist.getIndex();
+ if (!sectionSet.contains(index)) {
+ sectionSet.add(index);
+ positionList.add(i);
+ }
+ }
+ sections = sectionSet.toArray(new Object[sectionSet.size()]);
+ positions = positionList.toArray(new Integer[positionList.size()]);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ Artist entry = getItem(position);
+ ArtistView view;
+ if (convertView != null && convertView instanceof ArtistView) {
+ view = (ArtistView) convertView;
+ } else {
+ view = new ArtistView(activity);
+ }
+ view.setArtist(entry);
+ return view;
+ }
+
+ @Override
+ public Object[] getSections() {
+ return sections;
+ }
+
+ @Override
+ public int getPositionForSection(int section) {
+ section = Math.min(section, positions.length - 1);
+ return positions[section];
+ }
+
+ @Override
+ public int getSectionForPosition(int pos) {
+ for (int i = 0; i < sections.length - 1; i++) {
+ if (pos < positions[i + 1]) {
+ return i;
+ }
+ }
+ return sections.length - 1;
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/ArtistEntryView.java b/src/github/daneren2005/dsub/view/ArtistEntryView.java
new file mode 100644
index 00000000..3b6a50e4
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/ArtistEntryView.java
@@ -0,0 +1,83 @@
+/*
+ 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 github.daneren2005.dsub.view;
+
+import android.content.Context;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.TextView;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.ImageLoader;
+import github.daneren2005.dsub.util.Util;
+import java.io.File;
+/**
+ * Used to display albums in a {@code ListView}.
+ *
+ * @author Sindre Mehus
+ */
+public class ArtistEntryView extends UpdateView {
+ private static final String TAG = AlbumView.class.getSimpleName();
+
+ private Context context;
+ private MusicDirectory.Entry artist;
+
+ private TextView titleView;
+ private ImageButton starButton;
+ private ImageView moreButton;
+
+ public ArtistEntryView(Context context) {
+ super(context);
+ this.context = context;
+ LayoutInflater.from(context).inflate(R.layout.artist_list_item, this, true);
+
+ titleView = (TextView) findViewById(R.id.artist_name);
+ starButton = (ImageButton) findViewById(R.id.artist_star);
+ moreButton = (ImageView) findViewById(R.id.artist_more);
+ moreButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ v.showContextMenu();
+ }
+ });
+ }
+
+ public void setArtist(MusicDirectory.Entry artist) {
+ this.artist = artist;
+
+ titleView.setText(artist.getTitle());
+ starButton.setVisibility((Util.isOffline(getContext()) || !artist.isStarred()) ? View.GONE : View.VISIBLE);
+ starButton.setFocusable(false);
+ update();
+ }
+
+ @Override
+ protected void update() {
+ starButton.setVisibility((Util.isOffline(getContext()) || !artist.isStarred()) ? View.GONE : View.VISIBLE);
+ File file = FileUtil.getArtistDirectory(context, artist);
+ if(file.exists()) {
+ moreButton.setImageResource(R.drawable.list_item_more_shaded);
+ } else {
+ moreButton.setImageResource(R.drawable.list_item_more);
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/ArtistView.java b/src/github/daneren2005/dsub/view/ArtistView.java
new file mode 100644
index 00000000..353be618
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/ArtistView.java
@@ -0,0 +1,84 @@
+/*
+ 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 github.daneren2005.dsub.view;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.TextView;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.Artist;
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.Util;
+import java.io.File;
+
+/**
+ * Used to display albums in a {@code ListView}.
+ *
+ * @author Sindre Mehus
+ */
+public class ArtistView extends UpdateView {
+ private static final String TAG = ArtistView.class.getSimpleName();
+
+ private Context context;
+ private Artist artist;
+
+ private TextView titleView;
+ private ImageButton starButton;
+ private ImageView moreButton;
+
+ public ArtistView(Context context) {
+ super(context);
+ this.context = context;
+ LayoutInflater.from(context).inflate(R.layout.artist_list_item, this, true);
+
+ titleView = (TextView) findViewById(R.id.artist_name);
+ starButton = (ImageButton) findViewById(R.id.artist_star);
+ moreButton = (ImageView) findViewById(R.id.artist_more);
+ moreButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ v.showContextMenu();
+ }
+ });
+ }
+
+ public void setArtist(Artist artist) {
+ this.artist = artist;
+
+ titleView.setText(artist.getName());
+
+ starButton.setVisibility((Util.isOffline(getContext()) || !artist.isStarred()) ? View.GONE : View.VISIBLE);
+ starButton.setFocusable(false);
+
+ update();
+ }
+
+ @Override
+ protected void update() {
+ starButton.setVisibility((Util.isOffline(getContext()) || !artist.isStarred()) ? View.GONE : View.VISIBLE);
+ File file = FileUtil.getArtistDirectory(context, artist);
+ if(file.exists()) {
+ moreButton.setImageResource(R.drawable.list_item_more_shaded);
+ } else {
+ moreButton.setImageResource(R.drawable.list_item_more);
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/AutoRepeatButton.java b/src/github/daneren2005/dsub/view/AutoRepeatButton.java
new file mode 100644
index 00000000..798c1649
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/AutoRepeatButton.java
@@ -0,0 +1,86 @@
+package github.daneren2005.dsub.view;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.ImageButton;
+
+public class AutoRepeatButton extends ImageButton {
+
+ private long initialRepeatDelay = 1000;
+ private long repeatIntervalInMilliseconds = 300;
+ private boolean doClick = true;
+ private Runnable repeatEvent = null;
+
+ private Runnable repeatClickWhileButtonHeldRunnable = new Runnable() {
+ @Override
+ public void run() {
+ doClick = false;
+ //Perform the present repetition of the click action provided by the user
+ // in setOnClickListener().
+ if(repeatEvent != null)
+ repeatEvent.run();
+
+ //Schedule the next repetitions of the click action, using a faster repeat
+ // interval than the initial repeat delay interval.
+ postDelayed(repeatClickWhileButtonHeldRunnable, repeatIntervalInMilliseconds);
+ }
+ };
+
+ private void commonConstructorCode() {
+ this.setOnTouchListener(new OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ int action = event.getAction();
+ if(action == MotionEvent.ACTION_DOWN)
+ {
+ doClick = true;
+ //Just to be sure that we removed all callbacks,
+ // which should have occurred in the ACTION_UP
+ removeCallbacks(repeatClickWhileButtonHeldRunnable);
+
+ //Schedule the start of repetitions after a one half second delay.
+ postDelayed(repeatClickWhileButtonHeldRunnable, initialRepeatDelay);
+
+ setPressed(true);
+ }
+ else if(action == MotionEvent.ACTION_UP) {
+ //Cancel any repetition in progress.
+ removeCallbacks(repeatClickWhileButtonHeldRunnable);
+
+ if(doClick || repeatEvent == null) {
+ performClick();
+ }
+
+ setPressed(false);
+ }
+
+ //Returning true here prevents performClick() from getting called
+ // in the usual manner, which would be redundant, given that we are
+ // already calling it above.
+ return true;
+ }
+ });
+ }
+
+ public void setOnRepeatListener(Runnable runnable) {
+ repeatEvent = runnable;
+ }
+
+ public AutoRepeatButton(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ commonConstructorCode();
+ }
+
+
+ public AutoRepeatButton(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ commonConstructorCode();
+ }
+
+ public AutoRepeatButton(Context context) {
+ super(context);
+ commonConstructorCode();
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/ChangeLog.java b/src/github/daneren2005/dsub/view/ChangeLog.java
new file mode 100644
index 00000000..b847733e
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/ChangeLog.java
@@ -0,0 +1,552 @@
+/*
+ * Copyright (C) 2012 Christian Ketterer (cketti)
+ *
+ * Portions Copyright (C) 2012 Martin van Zuilekom (http://martin.cubeactive.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ *
+ * Based on android-change-log:
+ *
+ * Copyright (C) 2011, Karsten Priegnitz
+ *
+ * Permission to use, copy, modify, and distribute this piece of software
+ * for any purpose with or without fee is hereby granted, provided that
+ * the above copyright notice and this permission notice appear in the
+ * source code of all copies.
+ *
+ * It would be appreciated if you mention the author in your change log,
+ * contributors list or the like.
+ *
+ * http://code.google.com/p/android-change-log/
+ */
+package github.daneren2005.dsub.view;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.SharedPreferences;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources;
+import android.content.res.XmlResourceParser;
+import android.preference.PreferenceManager;
+import android.util.Log;
+import android.util.SparseArray;
+import android.webkit.WebView;
+import github.daneren2005.dsub.R;
+
+
+/**
+ * Display a dialog showing a full or partial (What's New) change log.
+ */
+public class ChangeLog {
+ /**
+ * Tag that is used when sending error/debug messages to the log.
+ */
+ protected static final String LOG_TAG = "ckChangeLog";
+
+ /**
+ * This is the key used when storing the version code in SharedPreferences.
+ */
+ protected static final String VERSION_KEY = "ckChangeLog_last_version_code";
+
+ /**
+ * Constant that used when no version code is available.
+ */
+ protected static final int NO_VERSION = -1;
+
+ /**
+ * Default CSS styles used to format the change log.
+ */
+ private static final String DEFAULT_CSS =
+ "div.title { margin-left: 0px; font-size: 1.2em; text-align: center;}" +
+ "div.subtitle {margin-left: 0px; font-size: .8em; text-align: center;}" +
+ "li { margin-left: 0px;}" +
+ "ul { padding-left: 2em;}";
+
+
+ /**
+ * Context that is used to access the resources and to create the ChangeLog dialogs.
+ */
+ protected final Context mContext;
+
+ /**
+ * Contains the CSS rules used to format the change log.
+ */
+ protected final String mCss;
+
+ /**
+ * Last version code read from {@code SharedPreferences} or {@link #NO_VERSION}.
+ */
+ private int mLastVersionCode;
+
+ /**
+ * Version code of the current installation.
+ */
+ private int mCurrentVersionCode;
+
+ /**
+ * Version name of the current installation.
+ */
+ private String mCurrentVersionName;
+
+
+ /**
+ * Contains constants for the root element of {@code changelog.xml}.
+ */
+ protected interface ChangeLogTag {
+ static final String NAME = "changelog";
+ }
+
+ /**
+ * Contains constants for the release element of {@code changelog.xml}.
+ */
+ protected interface ReleaseTag {
+ static final String NAME = "release";
+ static final String ATTRIBUTE_VERSION = "version";
+ static final String ATTRIBUTE_VERSION_CODE = "versioncode";
+ static final String ATTRIBUTE_RELEASE_DATE = "releasedate";
+ }
+
+ /**
+ * Contains constants for the change element of {@code changelog.xml}.
+ */
+ protected interface ChangeTag {
+ static final String NAME = "change";
+ }
+
+ /**
+ * Create a {@code ChangeLog} instance using the default {@link SharedPreferences} file.
+ *
+ * @param context
+ * Context that is used to access the resources and to create the ChangeLog dialogs.
+ */
+ public ChangeLog(Context context) {
+ this(context, PreferenceManager.getDefaultSharedPreferences(context), DEFAULT_CSS);
+ }
+
+ /**
+ * Create a {@code ChangeLog} instance using the default {@link SharedPreferences} file.
+ *
+ * @param context
+ * Context that is used to access the resources and to create the ChangeLog dialogs.
+ * @param css
+ * CSS styles that will be used to format the change log.
+ */
+ public ChangeLog(Context context, String css) {
+ this(context, PreferenceManager.getDefaultSharedPreferences(context), css);
+ }
+
+ public ChangeLog(Context context, SharedPreferences preferences) {
+ this(context, preferences, DEFAULT_CSS);
+ }
+
+ /**
+ * Create a {@code ChangeLog} instance using the supplied {@code SharedPreferences} instance.
+ *
+ * @param context
+ * Context that is used to access the resources and to create the ChangeLog dialogs.
+ * @param preferences
+ * {@code SharedPreferences} instance that is used to persist the last version code.
+ * @param css
+ * CSS styles used to format the change log (excluding {@code <style>} and
+ * {@code </style>}).
+ *
+ */
+ public ChangeLog(Context context, SharedPreferences preferences, String css) {
+ mContext = context;
+ mCss = css;
+
+ // Get last version code
+ mLastVersionCode = preferences.getInt(VERSION_KEY, NO_VERSION);
+
+ // Get current version code and version name
+ try {
+ PackageInfo packageInfo = context.getPackageManager().getPackageInfo(
+ context.getPackageName(), 0);
+
+ mCurrentVersionCode = packageInfo.versionCode;
+ mCurrentVersionName = packageInfo.versionName;
+ } catch (NameNotFoundException e) {
+ mCurrentVersionCode = NO_VERSION;
+ Log.e(LOG_TAG, "Could not get version information from manifest!", e);
+ }
+ }
+
+ /**
+ * Get version code of last installation.
+ *
+ * @return The version code of the last installation of this app (as described in the former
+ * manifest). This will be the same as returned by {@link #getCurrentVersionCode()} the
+ * second time this version of the app is launched (more precisely: the second time
+ * {@code ChangeLog} is instantiated).
+ *
+ * @see AndroidManifest.xml#android:versionCode
+ */
+ public int getLastVersionCode() {
+ return mLastVersionCode;
+ }
+
+ /**
+ * Get version code of current installation.
+ *
+ * @return The version code of this app as described in the manifest.
+ *
+ * @see AndroidManifest.xml#android:versionCode
+ */
+ public int getCurrentVersionCode() {
+ return mCurrentVersionCode;
+ }
+
+ /**
+ * Get version name of current installation.
+ *
+ * @return The version name of this app as described in the manifest.
+ *
+ * @see AndroidManifest.xml#android:versionName
+ */
+ public String getCurrentVersionName() {
+ return mCurrentVersionName;
+ }
+
+ /**
+ * Check if this is the first execution of this app version.
+ *
+ * @return {@code true} if this version of your app is started the first time.
+ */
+ public boolean isFirstRun() {
+ return mLastVersionCode < mCurrentVersionCode;
+ }
+
+ /**
+ * Check if this is a new installation.
+ *
+ * @return {@code true} if your app including {@code ChangeLog} is started the first time ever.
+ * Also {@code true} if your app was uninstalled and installed again.
+ */
+ public boolean isFirstRunEver() {
+ return mLastVersionCode == NO_VERSION;
+ }
+
+ /**
+ * Get the "What's New" dialog.
+ *
+ * @return An AlertDialog displaying the changes since the previous installed version of your
+ * app (What's New). But when this is the first run of your app including
+ * {@code ChangeLog} then the full log dialog is show.
+ */
+ public AlertDialog getLogDialog() {
+ return getDialog(isFirstRunEver());
+ }
+
+ /**
+ * Get a dialog with the full change log.
+ *
+ * @return An AlertDialog with a full change log displayed.
+ */
+ public AlertDialog getFullLogDialog() {
+ return getDialog(true);
+ }
+
+ /**
+ * Create a dialog containing (parts of the) change log.
+ *
+ * @param full
+ * If this is {@code true} the full change log is displayed. Otherwise only changes for
+ * versions newer than the last version are displayed.
+ *
+ * @return A dialog containing the (partial) change log.
+ */
+ protected AlertDialog getDialog(boolean full) {
+ WebView wv = new WebView(mContext);
+ //wv.setBackgroundColor(0); // transparent
+ wv.loadDataWithBaseURL(null, getLog(full), "text/html", "UTF-8", null);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
+ builder.setTitle(
+ mContext.getResources().getString(
+ full ? R.string.changelog_full_title : R.string.changelog_title))
+ .setView(wv)
+ .setCancelable(false)
+ // OK button
+ .setPositiveButton(
+ mContext.getResources().getString(R.string.changelog_ok_button),
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ // The user clicked "OK" so save the current version code as
+ // "last version code".
+ updateVersionInPreferences();
+ }
+ });
+
+ if (!full) {
+ // Show "Moreā€¦" button if we're only displaying a partial change log.
+ builder.setNegativeButton(R.string.changelog_show_full,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ getFullLogDialog().show();
+ }
+ });
+ }
+
+ return builder.create();
+ }
+
+ /**
+ * Write current version code to the preferences.
+ */
+ protected void updateVersionInPreferences() {
+ SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext);
+ SharedPreferences.Editor editor = sp.edit();
+ editor.putInt(VERSION_KEY, mCurrentVersionCode);
+
+ // TODO: Update preferences from a background thread
+ editor.commit();
+ }
+
+ /**
+ * Get changes since last version as HTML string.
+ *
+ * @return HTML string containing the changes since the previous installed version of your app
+ * (What's New).
+ */
+ public String getLog() {
+ return getLog(false);
+ }
+
+ /**
+ * Get full change log as HTML string.
+ *
+ * @return HTML string containing the full change log.
+ */
+ public String getFullLog() {
+ return getLog(true);
+ }
+
+ /**
+ * Get (partial) change log as HTML string.
+ *
+ * @param full
+ * If this is {@code true} the full change log is returned. Otherwise only changes for
+ * versions newer than the last version are returned.
+ *
+ * @return The (partial) change log.
+ */
+ private String getLog(boolean full) {
+ StringBuilder sb = new StringBuilder();
+
+ sb.append("<html><head><style type=\"text/css\">");
+ sb.append(mCss);
+ sb.append("</style></head><body>");
+
+ Resources resources = mContext.getResources();
+
+ // Read master change log from raw/changelog.xml
+ SparseArray<ReleaseItem> defaultChangelog;
+ try {
+ XmlPullParser xml = XmlPullParserFactory.newInstance().newPullParser();
+ InputStreamReader reader = new InputStreamReader(resources.openRawResource(R.raw.changelog));
+ xml.setInput(reader);
+ try {
+ defaultChangelog = readChangeLog(xml, full);
+ } finally {
+ try { reader.close(); } catch (Exception e) { /* do nothing */ }
+ }
+ } catch (XmlPullParserException e) {
+ Log.e(LOG_TAG, "Error reading raw/changelog.xml", e);
+ return null;
+ }
+
+ // Read localized change log from xml[-lang]/changelog.xml
+ XmlResourceParser resXml = mContext.getResources().getXml(R.xml.changelog);
+ SparseArray<ReleaseItem> changelog;
+ try {
+ changelog = readChangeLog(resXml, full);
+ } finally {
+ resXml.close();
+ }
+
+ String versionFormat = resources.getString(R.string.changelog_version_format);
+
+ // Get all version codes from the master change log...
+ List<Integer> versions = new ArrayList<Integer>(defaultChangelog.size());
+ for (int i = 0, len = defaultChangelog.size(); i < len; i++) {
+ int key = defaultChangelog.keyAt(i);
+ versions.add(key);
+ }
+
+ // ... and sort them (newest version first).
+ Collections.sort(versions, Collections.reverseOrder());
+
+ for (Integer version : versions) {
+ int key = version.intValue();
+
+ // Use release information from localized change log and fall back to the master file
+ // if necessary.
+ ReleaseItem release = changelog.get(key, defaultChangelog.get(key));
+
+ sb.append("<div class='title'>");
+ sb.append(String.format(versionFormat, release.versionName));
+ sb.append("</div>");
+ if(release.releaseDate != null) {
+ sb.append("<div class='subtitle'>");
+ sb.append(release.releaseDate);
+ sb.append("</div>");
+ }
+ sb.append("<ul>");
+ for (String change : release.changes) {
+ sb.append("<li>");
+ sb.append(change);
+ sb.append("</li>");
+ }
+ sb.append("</ul>");
+ }
+
+ sb.append("</body></html>");
+
+ return sb.toString();
+ }
+
+ /**
+ * Read the change log from an XML file.
+ *
+ * @param xml
+ * The {@code XmlPullParser} instance used to read the change log.
+ * @param full
+ * If {@code true} the full change log is read. Otherwise only the changes since the
+ * last (saved) version are read.
+ *
+ * @return A {@code SparseArray} mapping the version codes to release information.
+ */
+ protected SparseArray<ReleaseItem> readChangeLog(XmlPullParser xml, boolean full) {
+ SparseArray<ReleaseItem> result = new SparseArray<ReleaseItem>();
+
+ try {
+ int eventType = xml.getEventType();
+ while (eventType != XmlPullParser.END_DOCUMENT) {
+ if (eventType == XmlPullParser.START_TAG && xml.getName().equals(ReleaseTag.NAME)) {
+ if (parseReleaseTag(xml, full, result)) {
+ // Stop reading more elements if this entry is not newer than the last
+ // version.
+ break;
+ }
+ }
+ eventType = xml.next();
+ }
+ } catch (XmlPullParserException e) {
+ Log.e(LOG_TAG, e.getMessage(), e);
+ } catch (IOException e) {
+ Log.e(LOG_TAG, e.getMessage(), e);
+ }
+
+ return result;
+ }
+
+ /**
+ * Parse the {@code release} tag of a change log XML file.
+ *
+ * @param xml
+ * The {@code XmlPullParser} instance used to read the change log.
+ * @param full
+ * If {@code true} the contents of the {@code release} tag are always added to
+ * {@code changelog}. Otherwise only if the item's {@code versioncode} attribute is
+ * higher than the last version code.
+ * @param changelog
+ * The {@code SparseArray} to add a new {@link ReleaseItem} instance to.
+ *
+ * @return {@code true} if the {@code release} element is describing changes of a version older
+ * or equal to the last version. In that case {@code changelog} won't be modified and
+ * {@link #readChangeLog(XmlPullParser, boolean)} will stop reading more elements from
+ * the change log file.
+ *
+ * @throws XmlPullParserException
+ * @throws IOException
+ */
+ private boolean parseReleaseTag(XmlPullParser xml, boolean full,
+ SparseArray<ReleaseItem> changelog) throws XmlPullParserException, IOException {
+
+ String version = xml.getAttributeValue(null, ReleaseTag.ATTRIBUTE_VERSION);
+
+ int versionCode;
+ try {
+ String versionCodeStr = xml.getAttributeValue(null, ReleaseTag.ATTRIBUTE_VERSION_CODE);
+ versionCode = Integer.parseInt(versionCodeStr);
+ } catch (NumberFormatException e) {
+ versionCode = NO_VERSION;
+ }
+
+ String releaseDate = xml.getAttributeValue(null, ReleaseTag.ATTRIBUTE_RELEASE_DATE);
+
+ if (!full && versionCode <= mLastVersionCode) {
+ return true;
+ }
+
+ int eventType = xml.getEventType();
+ List<String> changes = new ArrayList<String>();
+ while (eventType != XmlPullParser.END_TAG || xml.getName().equals(ChangeTag.NAME)) {
+ if (eventType == XmlPullParser.START_TAG && xml.getName().equals(ChangeTag.NAME)) {
+ eventType = xml.next();
+
+ changes.add(xml.getText());
+ }
+ eventType = xml.next();
+ }
+
+ ReleaseItem release = new ReleaseItem(versionCode, version, releaseDate, changes);
+ changelog.put(versionCode, release);
+
+ return false;
+ }
+
+ /**
+ * Container used to store information about a release/version.
+ */
+ protected static class ReleaseItem {
+ /**
+ * Version code of the release.
+ */
+ public final int versionCode;
+
+ /**
+ * Version name of the release.
+ */
+ public final String versionName;
+
+ public final String releaseDate;
+
+ /**
+ * List of changes introduced with that release.
+ */
+ public final List<String> changes;
+
+ ReleaseItem(int versionCode, String versionName, String releaseDate, List<String> changes) {
+ this.versionCode = versionCode;
+ this.versionName = versionName;
+ this.releaseDate = releaseDate;
+ this.changes = changes;
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/ChatAdapter.java b/src/github/daneren2005/dsub/view/ChatAdapter.java
new file mode 100644
index 00000000..518f81ef
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/ChatAdapter.java
@@ -0,0 +1,100 @@
+package github.daneren2005.dsub.view;
+
+import android.text.method.LinkMovementMethod;
+import android.text.util.Linkify;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.TextView;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.activity.SubsonicActivity;
+import github.daneren2005.dsub.domain.ChatMessage;
+import github.daneren2005.dsub.util.Util;
+
+import java.text.DateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.regex.Pattern;
+
+public class ChatAdapter extends ArrayAdapter<ChatMessage> {
+
+ private final SubsonicActivity activity;
+ private ArrayList<ChatMessage> messages;
+
+ private static final String phoneRegex = "1?\\W*([2-9][0-8][0-9])\\W*([2-9][0-9]{2})\\W*([0-9]{4})"; //you can just place your support phone here
+ private static final Pattern phoneMatcher = Pattern.compile(phoneRegex);
+
+ public ChatAdapter(SubsonicActivity activity, ArrayList<ChatMessage> messages) {
+ super(activity, R.layout.chat_item, messages);
+ this.activity = activity;
+ this.messages = messages;
+ }
+
+ @Override
+ public int getCount() {
+ return messages.size();
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ ChatMessage message = this.getItem(position);
+
+ ViewHolder holder;
+ int layout;
+
+ String messageUser = message.getUsername();
+ Date messageTime = new java.util.Date(message.getTime());
+ String messageText = message.getMessage();
+
+ String me = Util.getUserName(activity, Util.getActiveServer(activity));
+
+ if (messageUser.equals(me)) {
+ layout = R.layout.chat_item_reverse;
+ } else {
+ layout = R.layout.chat_item;
+ }
+
+ if (convertView == null)
+ {
+ holder = new ViewHolder();
+
+ convertView = LayoutInflater.from(activity).inflate(layout, parent, false);
+
+ TextView usernameView = (TextView) convertView.findViewById(R.id.chat_username);
+ TextView timeView = (TextView) convertView.findViewById(R.id.chat_time);
+ TextView messageView = (TextView) convertView.findViewById(R.id.chat_message);
+
+ messageView.setMovementMethod(LinkMovementMethod.getInstance());
+ Linkify.addLinks(messageView, Linkify.EMAIL_ADDRESSES);
+ Linkify.addLinks(messageView, Linkify.WEB_URLS);
+ Linkify.addLinks(messageView, phoneMatcher, "tel:");
+
+ holder.message = messageView;
+ holder.username = usernameView;
+ holder.time = timeView;
+
+ convertView.setTag(holder);
+ }
+ else
+ {
+ holder = (ViewHolder) convertView.getTag();
+ }
+
+ DateFormat timeFormat = android.text.format.DateFormat.getTimeFormat(activity);
+ String messageTimeFormatted = String.format("[%s]", timeFormat.format(messageTime));
+
+ holder.username.setText(messageUser);
+ holder.message.setText(messageText);
+ holder.time.setText(messageTimeFormatted);
+
+ return convertView;
+ }
+
+ private static class ViewHolder
+ {
+ TextView message;
+ TextView username;
+ TextView time;
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/EntryAdapter.java b/src/github/daneren2005/dsub/view/EntryAdapter.java
new file mode 100644
index 00000000..ff7393c6
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/EntryAdapter.java
@@ -0,0 +1,79 @@
+/*
+ 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 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.view;
+
+import android.content.Context;
+import android.util.Log;
+import java.util.List;
+
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.util.ImageLoader;
+
+/**
+ * @author Sindre Mehus
+ */
+public class EntryAdapter extends ArrayAdapter<MusicDirectory.Entry> {
+ private final static String TAG = EntryAdapter.class.getSimpleName();
+ private final Context activity;
+ private final ImageLoader imageLoader;
+ private final boolean checkable;
+ private List<MusicDirectory.Entry> entries;
+
+ public EntryAdapter(Context activity, ImageLoader imageLoader, List<MusicDirectory.Entry> entries, boolean checkable) {
+ super(activity, android.R.layout.simple_list_item_1, entries);
+ this.entries = entries;
+ this.activity = activity;
+ this.imageLoader = imageLoader;
+ this.checkable = checkable;
+ }
+
+ public void removeAt(int position) {
+ entries.remove(position);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ MusicDirectory.Entry entry = getItem(position);
+
+ if (entry.isDirectory()) {
+ if(entry.getArtist() != null || entry.getParent() != null) {
+ AlbumView view;
+ view = new AlbumView(activity);
+ view.setAlbum(entry, imageLoader);
+ return view;
+ } else {
+ ArtistEntryView view = new ArtistEntryView(activity);
+ view.setArtist(entry);
+ return view;
+ }
+ } else {
+ SongView view;
+ if (convertView != null && convertView instanceof SongView) {
+ view = (SongView) convertView;
+ } else {
+ view = new SongView(activity);
+ }
+ view.setSong(entry, checkable);
+ return view;
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/ErrorDialog.java b/src/github/daneren2005/dsub/view/ErrorDialog.java
new file mode 100644
index 00000000..e9f25a2d
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/ErrorDialog.java
@@ -0,0 +1,70 @@
+/*
+ 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 github.daneren2005.dsub.view;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import github.daneren2005.dsub.activity.MainActivity;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.util.Util;
+
+/**
+ * @author Sindre Mehus
+ */
+public class ErrorDialog {
+
+ public ErrorDialog(Activity activity, int messageId, boolean finishActivityOnCancel) {
+ this(activity, activity.getResources().getString(messageId), finishActivityOnCancel);
+ }
+
+ public ErrorDialog(final Activity activity, String message, final boolean finishActivityOnClose) {
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+ builder.setIcon(android.R.drawable.ic_dialog_alert);
+ builder.setTitle(R.string.error_label);
+ builder.setMessage(message);
+ builder.setCancelable(true);
+ builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialogInterface) {
+ if (finishActivityOnClose) {
+ restart(activity);
+ }
+ }
+ });
+ builder.setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ if (finishActivityOnClose) {
+ restart(activity);
+ }
+ }
+ });
+
+ builder.create().show();
+ }
+
+ private void restart(Activity context) {
+ Intent intent = new Intent(context, MainActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ Util.startActivityWithoutTransition(context, intent);
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/FadeOutAnimation.java b/src/github/daneren2005/dsub/view/FadeOutAnimation.java
new file mode 100644
index 00000000..292529e6
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/FadeOutAnimation.java
@@ -0,0 +1,77 @@
+/*
+ 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 github.daneren2005.dsub.view;
+
+import android.view.View;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+
+/**
+ * Fades a view out by changing its alpha value.
+ *
+ * @author Sindre Mehus
+ * @version $Id: Util.java 3203 2012-10-04 09:12:08Z sindre_mehus $
+ */
+public class FadeOutAnimation extends AlphaAnimation {
+
+ private boolean cancelled;
+
+ /**
+ * Creates and starts the fade out animation.
+ *
+ * @param view The view to fade out (or display).
+ * @param fadeOut If true, the view is faded out. Otherwise it is immediately made visible.
+ * @param durationMillis Fade duration.
+ */
+ public static void createAndStart(View view, boolean fadeOut, long durationMillis) {
+ if (fadeOut) {
+ view.clearAnimation();
+ view.startAnimation(new FadeOutAnimation(view, durationMillis));
+ } else {
+ Animation animation = view.getAnimation();
+ if (animation instanceof FadeOutAnimation) {
+ ((FadeOutAnimation) animation).cancelFadeOut();
+ }
+ view.clearAnimation();
+ view.setVisibility(View.VISIBLE);
+ }
+ }
+
+ FadeOutAnimation(final View view, long durationMillis) {
+ super(1.0F, 0.0F);
+ setDuration(durationMillis);
+ setAnimationListener(new AnimationListener() {
+ public void onAnimationStart(Animation animation) {
+ }
+
+ public void onAnimationRepeat(Animation animation) {
+ }
+
+ public void onAnimationEnd(Animation animation) {
+ if (!cancelled) {
+ view.setVisibility(View.INVISIBLE);
+ }
+ }
+ });
+ }
+
+ private void cancelFadeOut() {
+ cancelled = true;
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/GenreAdapter.java b/src/github/daneren2005/dsub/view/GenreAdapter.java
new file mode 100644
index 00000000..b98efd20
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/GenreAdapter.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 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.view;
+
+import android.widget.ArrayAdapter;
+import android.widget.SectionIndexer;
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.Genre;
+
+import java.util.List;
+import java.util.Set;
+import java.util.LinkedHashSet;
+import java.util.ArrayList;
+
+/**
+ * @author Sindre Mehus
+*/
+public class GenreAdapter extends ArrayAdapter<Genre>{
+ private Context activity;
+ private List<Genre> genres;
+
+ public GenreAdapter(Context context, List<Genre> genres) {
+ super(context, android.R.layout.simple_list_item_1, genres);
+ this.activity = context;
+ this.genres = genres;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ Genre genre = genres.get(position);
+ GenreView view;
+ if (convertView != null && convertView instanceof GenreView) {
+ view = (GenreView) convertView;
+ } else {
+ view = new GenreView(activity);
+ }
+ view.setGenre(genre);
+ return view;
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/GenreView.java b/src/github/daneren2005/dsub/view/GenreView.java
new file mode 100644
index 00000000..dbb0248b
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/GenreView.java
@@ -0,0 +1,53 @@
+/*
+ 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 github.daneren2005.dsub.view;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.TextView;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.Genre;
+
+public class GenreView extends UpdateView {
+ private static final String TAG = GenreView.class.getSimpleName();
+
+ private TextView titleView;
+ private ImageButton starButton;
+ private ImageView moreButton;
+
+ public GenreView(Context context) {
+ super(context);
+ LayoutInflater.from(context).inflate(R.layout.artist_list_item, this, true);
+
+ titleView = (TextView) findViewById(R.id.artist_name);
+ starButton = (ImageButton) findViewById(R.id.artist_star);
+ moreButton = (ImageView) findViewById(R.id.artist_more);
+ moreButton.setClickable(false);
+ }
+
+ public void setGenre(Genre genre) {
+ titleView.setText(genre.getName());
+
+ starButton.setVisibility(View.GONE);
+ starButton.setFocusable(false);
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/MergeAdapter.java b/src/github/daneren2005/dsub/view/MergeAdapter.java
new file mode 100644
index 00000000..bfe777ea
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/MergeAdapter.java
@@ -0,0 +1,292 @@
+/***
+ Copyright (c) 2008-2009 CommonsWare, LLC
+ Portions (c) 2009 Google, Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License"); you may
+ not use this file except in compliance with the License. You may obtain
+ a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+package github.daneren2005.dsub.view;
+
+import android.database.DataSetObserver;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ListAdapter;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Arrays;
+
+import github.daneren2005.dsub.view.SackOfViewsAdapter;
+
+/**
+ * Adapter that merges multiple child adapters and views
+ * into a single contiguous whole.
+ * <p/>
+ * Adapters used as pieces within MergeAdapter must
+ * have view type IDs monotonically increasing from 0. Ideally,
+ * adapters also have distinct ranges for their row ids, as
+ * returned by getItemId().
+ */
+public class MergeAdapter extends BaseAdapter {
+
+ private final CascadeDataSetObserver observer = new CascadeDataSetObserver();
+ private final ArrayList<ListAdapter> pieces = new ArrayList<ListAdapter>();
+
+ /**
+ * Stock constructor, simply chaining to the superclass.
+ */
+ public MergeAdapter() {
+ super();
+ }
+
+ /**
+ * Adds a new adapter to the roster of things to appear
+ * in the aggregate list.
+ *
+ * @param adapter Source for row views for this section
+ */
+ public void addAdapter(ListAdapter adapter) {
+ pieces.add(adapter);
+ adapter.registerDataSetObserver(observer);
+ }
+
+ public void removeAdapter(ListAdapter adapter) {
+ adapter.unregisterDataSetObserver(observer);
+ pieces.remove(adapter);
+ }
+
+ /**
+ * Adds a new View to the roster of things to appear
+ * in the aggregate list.
+ *
+ * @param view Single view to add
+ */
+ public ListAdapter addView(View view) {
+ return addView(view, false);
+ }
+
+ /**
+ * Adds a new View to the roster of things to appear
+ * in the aggregate list.
+ *
+ * @param view Single view to add
+ * @param enabled false if views are disabled, true if enabled
+ */
+ public ListAdapter addView(View view, boolean enabled) {
+ return addViews(Arrays.asList(view), enabled);
+ }
+
+ /**
+ * Adds a list of views to the roster of things to appear
+ * in the aggregate list.
+ *
+ * @param views List of views to add
+ */
+ public ListAdapter addViews(List<View> views) {
+ return addViews(views, false);
+ }
+
+ /**
+ * Adds a list of views to the roster of things to appear
+ * in the aggregate list.
+ *
+ * @param views List of views to add
+ * @param enabled false if views are disabled, true if enabled
+ */
+ public ListAdapter addViews(List<View> views, boolean enabled) {
+ ListAdapter adapter = enabled ? new EnabledSackAdapter(views) : new SackOfViewsAdapter(views);
+ addAdapter(adapter);
+ return adapter;
+ }
+
+ /**
+ * Get the data item associated with the specified
+ * position in the data set.
+ *
+ * @param position Position of the item whose data we want
+ */
+ @Override
+ public Object getItem(int position) {
+ for (ListAdapter piece : pieces) {
+ int size = piece.getCount();
+
+ if (position < size) {
+ return (piece.getItem(position));
+ }
+
+ position -= size;
+ }
+
+ return (null);
+ }
+
+ /**
+ * How many items are in the data set represented by this
+ * Adapter.
+ */
+ @Override
+ public int getCount() {
+ int total = 0;
+
+ for (ListAdapter piece : pieces) {
+ total += piece.getCount();
+ }
+
+ return (total);
+ }
+
+ /**
+ * Returns the number of types of Views that will be
+ * created by getView().
+ */
+ @Override
+ public int getViewTypeCount() {
+ int total = 0;
+
+ for (ListAdapter piece : pieces) {
+ total += piece.getViewTypeCount();
+ }
+
+ return (Math.max(total, 1)); // needed for setListAdapter() before content add'
+ }
+
+ /**
+ * Get the type of View that will be created by getView()
+ * for the specified item.
+ *
+ * @param position Position of the item whose data we want
+ */
+ @Override
+ public int getItemViewType(int position) {
+ int typeOffset = 0;
+ int result = -1;
+
+ for (ListAdapter piece : pieces) {
+ int size = piece.getCount();
+
+ if (position < size) {
+ result = typeOffset + piece.getItemViewType(position);
+ break;
+ }
+
+ position -= size;
+ typeOffset += piece.getViewTypeCount();
+ }
+
+ return (result);
+ }
+
+ /**
+ * Are all items in this ListAdapter enabled? If yes it
+ * means all items are selectable and clickable.
+ */
+ @Override
+ public boolean areAllItemsEnabled() {
+ return (false);
+ }
+
+ /**
+ * Returns true if the item at the specified position is
+ * not a separator.
+ *
+ * @param position Position of the item whose data we want
+ */
+ @Override
+ public boolean isEnabled(int position) {
+ for (ListAdapter piece : pieces) {
+ int size = piece.getCount();
+
+ if (position < size) {
+ return (piece.isEnabled(position));
+ }
+
+ position -= size;
+ }
+
+ return (false);
+ }
+
+ /**
+ * Get a View that displays the data at the specified
+ * position in the data set.
+ *
+ * @param position Position of the item whose data we want
+ * @param convertView View to recycle, if not null
+ * @param parent ViewGroup containing the returned View
+ */
+ @Override
+ public View getView(int position, View convertView,
+ ViewGroup parent) {
+ for (ListAdapter piece : pieces) {
+ int size = piece.getCount();
+
+ if (position < size) {
+
+ return (piece.getView(position, convertView, parent));
+ }
+
+ position -= size;
+ }
+
+ return (null);
+ }
+
+ /**
+ * Get the row id associated with the specified position
+ * in the list.
+ *
+ * @param position Position of the item whose data we want
+ */
+ @Override
+ public long getItemId(int position) {
+ for (ListAdapter piece : pieces) {
+ int size = piece.getCount();
+
+ if (position < size) {
+ return (piece.getItemId(position));
+ }
+
+ position -= size;
+ }
+
+ return (-1);
+ }
+
+ private static class EnabledSackAdapter extends SackOfViewsAdapter {
+ public EnabledSackAdapter(List<View> views) {
+ super(views);
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ return (true);
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ return (true);
+ }
+ }
+
+ private class CascadeDataSetObserver extends DataSetObserver {
+ @Override
+ public void onChanged() {
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public void onInvalidated() {
+ notifyDataSetInvalidated();
+ }
+ }
+}
+
diff --git a/src/github/daneren2005/dsub/view/MyViewFlipper.java b/src/github/daneren2005/dsub/view/MyViewFlipper.java
new file mode 100644
index 00000000..26a3de08
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/MyViewFlipper.java
@@ -0,0 +1,53 @@
+/*
+ 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 github.daneren2005.dsub.view;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.ViewFlipper;
+
+/**
+ * Work-around for Android Issue 6191 (http://code.google.com/p/android/issues/detail?id=6191)
+ *
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class MyViewFlipper extends ViewFlipper {
+
+ public MyViewFlipper(Context context) {
+ super(context);
+ }
+
+ public MyViewFlipper(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+
+ @Override
+ protected void onDetachedFromWindow() {
+ try {
+ super.onDetachedFromWindow();
+ }
+ catch (IllegalArgumentException e) {
+ // Call stopFlipping() in order to kick off updateRunning()
+ stopFlipping();
+ }
+ }
+}
+
diff --git a/src/github/daneren2005/dsub/view/PlaylistAdapter.java b/src/github/daneren2005/dsub/view/PlaylistAdapter.java
new file mode 100644
index 00000000..71727c04
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/PlaylistAdapter.java
@@ -0,0 +1,68 @@
+/*
+ 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 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.view;
+
+import android.content.Context;
+import github.daneren2005.dsub.R;
+import java.util.List;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import github.daneren2005.dsub.domain.Playlist;
+import java.util.Collections;
+import java.util.Comparator;
+
+/**
+ * @author Sindre Mehus
+ */
+public class PlaylistAdapter extends ArrayAdapter<Playlist> {
+
+ private final Context activity;
+
+ public PlaylistAdapter(Context activity, List<Playlist> Playlists) {
+ super(activity, R.layout.playlist_list_item, Playlists);
+ this.activity = activity;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ Playlist entry = getItem(position);
+ PlaylistView view;
+ if (convertView != null && convertView instanceof PlaylistView) {
+ view = (PlaylistView) convertView;
+ } else {
+ view = new PlaylistView(activity);
+ }
+ view.setPlaylist(entry);
+ return view;
+ }
+
+ public static class PlaylistComparator implements Comparator<Playlist> {
+ @Override
+ public int compare(Playlist playlist1, Playlist playlist2) {
+ return playlist1.getName().compareToIgnoreCase(playlist2.getName());
+ }
+
+ public static List<Playlist> sort(List<Playlist> playlists) {
+ Collections.sort(playlists, new PlaylistComparator());
+ return playlists;
+ }
+
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/PlaylistView.java b/src/github/daneren2005/dsub/view/PlaylistView.java
new file mode 100644
index 00000000..876e0691
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/PlaylistView.java
@@ -0,0 +1,76 @@
+/*
+ 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 github.daneren2005.dsub.view;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.Playlist;
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.Util;
+import java.io.File;
+
+/**
+ * Used to display albums in a {@code ListView}.
+ *
+ * @author Sindre Mehus
+ */
+public class PlaylistView extends UpdateView {
+ private static final String TAG = PlaylistView.class.getSimpleName();
+
+ private Context context;
+ private Playlist playlist;
+
+ private TextView titleView;
+ private ImageView moreButton;
+
+ public PlaylistView(Context context) {
+ super(context);
+ this.context = context;
+ LayoutInflater.from(context).inflate(R.layout.playlist_list_item, this, true);
+
+ titleView = (TextView) findViewById(R.id.playlist_name);
+ moreButton = (ImageView) findViewById(R.id.playlist_more);
+ moreButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ v.showContextMenu();
+ }
+ });
+ }
+
+ public void setPlaylist(Playlist playlist) {
+ this.playlist = playlist;
+
+ titleView.setText(playlist.getName());
+ update();
+ }
+
+ @Override
+ protected void update() {
+ File file = FileUtil.getPlaylistFile(Util.getServerName(context), playlist.getName());
+ if(file.exists() || Util.isOffline(context)) {
+ moreButton.setImageResource(R.drawable.list_item_more_shaded);
+ } else {
+ moreButton.setImageResource(R.drawable.list_item_more);
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/PodcastChannelAdapter.java b/src/github/daneren2005/dsub/view/PodcastChannelAdapter.java
new file mode 100644
index 00000000..6b7af991
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/PodcastChannelAdapter.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 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.view;
+
+import android.widget.ArrayAdapter;
+import android.widget.SectionIndexer;
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.PodcastChannel;
+
+import java.util.List;
+import java.util.Set;
+import java.util.LinkedHashSet;
+import java.util.ArrayList;
+
+/**
+ * @author Sindre Mehus
+*/
+public class PodcastChannelAdapter extends ArrayAdapter<PodcastChannel>{
+ private Context activity;
+ private List<PodcastChannel> podcasts;
+
+ public PodcastChannelAdapter(Context context, List<PodcastChannel> podcasts) {
+ super(context, android.R.layout.simple_list_item_1, podcasts);
+ this.activity = context;
+ this.podcasts = podcasts;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ PodcastChannel podcast = podcasts.get(position);
+ PodcastChannelView view;
+ if (convertView != null && convertView instanceof PodcastChannelView) {
+ view = (PodcastChannelView) convertView;
+ } else {
+ view = new PodcastChannelView(activity);
+ }
+ view.setPodcastChannel(podcast);
+ return view;
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/PodcastChannelView.java b/src/github/daneren2005/dsub/view/PodcastChannelView.java
new file mode 100644
index 00000000..94eedf2c
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/PodcastChannelView.java
@@ -0,0 +1,76 @@
+/*
+ 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 github.daneren2005.dsub.view;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.TextView;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.PodcastChannel;
+import github.daneren2005.dsub.util.FileUtil;
+import java.io.File;
+
+public class PodcastChannelView extends UpdateView {
+ private static final String TAG = PodcastChannelView.class.getSimpleName();
+
+ private Context context;
+ private PodcastChannel channel;
+
+ private TextView titleView;
+ private ImageView moreButton;
+
+ public PodcastChannelView(Context context) {
+ super(context);
+ this.context = context;
+ LayoutInflater.from(context).inflate(R.layout.artist_list_item, this, true);
+
+ titleView = (TextView) findViewById(R.id.artist_name);
+ ImageButton starButton = (ImageButton) findViewById(R.id.artist_star);
+ starButton.setVisibility(View.GONE);
+ starButton.setFocusable(false);
+ moreButton = (ImageView) findViewById(R.id.artist_more);
+ moreButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ v.showContextMenu();
+ }
+ });
+ }
+
+ public void setPodcastChannel(PodcastChannel podcastChannel) {
+ channel = podcastChannel;
+ if(podcastChannel.getName() != null) {
+ titleView.setText(podcastChannel.getName());
+ } else {
+ titleView.setText(podcastChannel.getUrl());
+ }
+ }
+
+ @Override
+ protected void update() {
+ File file = FileUtil.getPodcastDirectory(context, channel);
+ if(file.exists()) {
+ moreButton.setImageResource(R.drawable.list_item_more_shaded);
+ } else {
+ moreButton.setImageResource(R.drawable.list_item_more);
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/SackOfViewsAdapter.java b/src/github/daneren2005/dsub/view/SackOfViewsAdapter.java
new file mode 100644
index 00000000..ff2280c1
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/SackOfViewsAdapter.java
@@ -0,0 +1,181 @@
+/***
+ Copyright (c) 2008-2009 CommonsWare, LLC
+ Portions (c) 2009 Google, Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License"); you may
+ not use this file except in compliance with the License. You may obtain
+ a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+package github.daneren2005.dsub.view;
+
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ListView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Adapter that simply returns row views from a list.
+ * <p/>
+ * If you supply a size, you must implement newView(), to
+ * create a required view. The adapter will then cache these
+ * views.
+ * <p/>
+ * If you supply a list of views in the constructor, that
+ * list will be used directly. If any elements in the list
+ * are null, then newView() will be called just for those
+ * slots.
+ * <p/>
+ * Subclasses may also wish to override areAllItemsEnabled()
+ * (default: false) and isEnabled() (default: false), if some
+ * of their rows should be selectable.
+ * <p/>
+ * It is assumed each view is unique, and therefore will not
+ * get recycled.
+ * <p/>
+ * Note that this adapter is not designed for long lists. It
+ * is more for screens that should behave like a list. This
+ * is particularly useful if you combine this with other
+ * adapters (e.g., SectionedAdapter) that might have an
+ * arbitrary number of rows, so it all appears seamless.
+ */
+public class SackOfViewsAdapter extends BaseAdapter {
+ private List<View> views = null;
+
+ /**
+ * Constructor creating an empty list of views, but with
+ * a specified count. Subclasses must override newView().
+ */
+ public SackOfViewsAdapter(int count) {
+ super();
+
+ views = new ArrayList<View>(count);
+
+ for (int i = 0; i < count; i++) {
+ views.add(null);
+ }
+ }
+
+ /**
+ * Constructor wrapping a supplied list of views.
+ * Subclasses must override newView() if any of the elements
+ * in the list are null.
+ */
+ public SackOfViewsAdapter(List<View> views) {
+ for (View view : views) {
+ view.setLayoutParams(new ListView.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
+ }
+ this.views = views;
+ }
+
+ /**
+ * Get the data item associated with the specified
+ * position in the data set.
+ *
+ * @param position Position of the item whose data we want
+ */
+ @Override
+ public Object getItem(int position) {
+ return (views.get(position));
+ }
+
+ /**
+ * How many items are in the data set represented by this
+ * Adapter.
+ */
+ @Override
+ public int getCount() {
+ return (views.size());
+ }
+
+ /**
+ * Returns the number of types of Views that will be
+ * created by getView().
+ */
+ @Override
+ public int getViewTypeCount() {
+ return (getCount());
+ }
+
+ /**
+ * Get the type of View that will be created by getView()
+ * for the specified item.
+ *
+ * @param position Position of the item whose data we want
+ */
+ @Override
+ public int getItemViewType(int position) {
+ return (position);
+ }
+
+ /**
+ * Are all items in this ListAdapter enabled? If yes it
+ * means all items are selectable and clickable.
+ */
+ @Override
+ public boolean areAllItemsEnabled() {
+ return (false);
+ }
+
+ /**
+ * Returns true if the item at the specified position is
+ * not a separator.
+ *
+ * @param position Position of the item whose data we want
+ */
+ @Override
+ public boolean isEnabled(int position) {
+ return (false);
+ }
+
+ /**
+ * Get a View that displays the data at the specified
+ * position in the data set.
+ *
+ * @param position Position of the item whose data we want
+ * @param convertView View to recycle, if not null
+ * @param parent ViewGroup containing the returned View
+ */
+ @Override
+ public View getView(int position, View convertView,
+ ViewGroup parent) {
+ View result = views.get(position);
+
+ if (result == null) {
+ result = newView(position, parent);
+ views.set(position, result);
+ }
+
+ return (result);
+ }
+
+ /**
+ * Get the row id associated with the specified position
+ * in the list.
+ *
+ * @param position Position of the item whose data we want
+ */
+ @Override
+ public long getItemId(int position) {
+ return (position);
+ }
+
+ /**
+ * Create a new View to go into the list at the specified
+ * position.
+ *
+ * @param position Position of the item whose data we want
+ * @param parent ViewGroup containing the returned View
+ */
+ protected View newView(int position, ViewGroup parent) {
+ throw new RuntimeException("You must override newView()!");
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/SongView.java b/src/github/daneren2005/dsub/view/SongView.java
new file mode 100644
index 00000000..042d8031
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/SongView.java
@@ -0,0 +1,241 @@
+/*
+ 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 github.daneren2005.dsub.view;
+
+import android.content.Context;
+import android.media.MediaMetadataRetriever;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.*;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.PodcastEpisode;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.service.DownloadServiceImpl;
+import github.daneren2005.dsub.service.DownloadFile;
+import github.daneren2005.dsub.util.Util;
+
+import java.io.File;
+import java.text.DateFormat;
+
+/**
+ * Used to display songs in a {@code ListView}.
+ *
+ * @author Sindre Mehus
+ */
+public class SongView extends UpdateView implements Checkable {
+ private static final String TAG = SongView.class.getSimpleName();
+
+ private Context context;
+ private MusicDirectory.Entry song;
+
+ private CheckedTextView checkedTextView;
+ private TextView titleTextView;
+ private TextView artistTextView;
+ private TextView durationTextView;
+ private TextView statusTextView;
+ private ImageButton starButton;
+ private ImageView moreButton;
+
+ private DownloadService downloadService;
+ private long revision = -1;
+ private DownloadFile downloadFile;
+
+ private boolean playing = false;
+ private int rightImage = 0;
+ private int moreImage = 0;
+ private boolean starred = false;
+ private boolean isWorkDone = false;
+ private boolean isSaved = false;
+ private File partialFile;
+ private boolean partialFileExists = false;
+
+ public SongView(Context context) {
+ super(context);
+ this.context = context;
+ LayoutInflater.from(context).inflate(R.layout.song_list_item, this, true);
+
+ checkedTextView = (CheckedTextView) findViewById(R.id.song_check);
+ titleTextView = (TextView) findViewById(R.id.song_title);
+ artistTextView = (TextView) findViewById(R.id.song_artist);
+ durationTextView = (TextView) findViewById(R.id.song_duration);
+ statusTextView = (TextView) findViewById(R.id.song_status);
+ starButton = (ImageButton) findViewById(R.id.song_star);
+ starButton.setFocusable(false);
+ moreButton = (ImageView) findViewById(R.id.artist_more);
+ moreButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ v.showContextMenu();
+ }
+ });
+ }
+
+ public void setSong(MusicDirectory.Entry song, boolean checkable) {
+ this.song = song;
+
+ StringBuilder artist = new StringBuilder(40);
+
+ String bitRate = null;
+ if (song.getBitRate() != null) {
+ bitRate = String.format(getContext().getString(R.string.song_details_kbps), song.getBitRate());
+ }
+
+ String fileFormat = null;
+ if (song.getTranscodedSuffix() != null && !song.getTranscodedSuffix().equals(song.getSuffix())) {
+ fileFormat = String.format("%s > %s", song.getSuffix(), song.getTranscodedSuffix());
+ } else {
+ fileFormat = song.getSuffix();
+ }
+
+ if(!song.isVideo()) {
+ if(song instanceof PodcastEpisode) {
+ String date = ((PodcastEpisode)song).getDate();
+ if(date != null) {
+ int index = date.indexOf(" ");
+ artist.append(date.substring(0, index != -1 ? index : date.length()));
+ }
+ }
+ else if(song.getArtist() != null) {
+ artist.append(song.getArtist());
+ }
+
+ String status = (song instanceof PodcastEpisode) ? ((PodcastEpisode)song).getStatus() : "";
+ artist.append(" (");
+ if("error".equals(status)) {
+ artist.append(getContext().getString(R.string.song_details_error));
+ } else if("skipped".equals(status)) {
+ artist.append(getContext().getString(R.string.song_details_skipped));
+ } else if("downloading".equals(status)) {
+ artist.append(getContext().getString(R.string.song_details_downloading));
+ } else {
+ artist.append(String.format(getContext().getString(R.string.song_details_all), bitRate == null ? "" : bitRate, fileFormat));
+ }
+ artist.append(")");
+ } else {
+ artist.append(String.format(getContext().getString(R.string.song_details_all), bitRate == null ? "" : bitRate, fileFormat));
+ }
+
+ String title = song.getTitle();
+ Integer track = song.getTrack();
+ if(track != null && Util.getDisplayTrack(context)) {
+ title = String.format("%02d", track) + " " + title;
+ }
+
+ titleTextView.setText(title);
+ artistTextView.setText(artist);
+ durationTextView.setText(Util.formatDuration(song.getDuration()));
+ checkedTextView.setVisibility(checkable && !song.isVideo() ? View.VISIBLE : View.GONE);
+
+ revision = -1;
+ updateBackground();
+ update();
+ }
+
+ @Override
+ protected void updateBackground() {
+ if (downloadService == null) {
+ downloadService = DownloadServiceImpl.getInstance();
+ if(downloadService == null) {
+ return;
+ }
+ }
+
+ long newRevision = downloadService.getDownloadListUpdateRevision();
+ if(revision != newRevision || downloadFile == null) {
+ downloadFile = downloadService.forSong(song);
+ revision = newRevision;
+ }
+
+ isWorkDone = downloadFile.isWorkDone();
+ isSaved = downloadFile.isSaved();
+ partialFile = downloadFile.getPartialFile();
+ partialFileExists = partialFile.exists();
+ }
+
+ @Override
+ protected void update() {
+ if (downloadService == null) {
+ return;
+ }
+
+ if(song.isStarred()) {
+ if(!starred) {
+ starButton.setVisibility(View.VISIBLE);
+ starred = true;
+ }
+ } else {
+ if(starred) {
+ starButton.setVisibility(View.GONE);
+ starred = false;
+ }
+ }
+
+ int rightImage = 0;
+ if (isWorkDone) {
+ int moreImage = isSaved ? R.drawable.list_item_more_saved : R.drawable.list_item_more_shaded;
+ if(moreImage != this.moreImage) {
+ moreButton.setImageResource(moreImage);
+ this.moreImage = moreImage;
+ }
+ } else if(this.moreImage != R.drawable.list_item_more) {
+ moreButton.setImageResource(R.drawable.list_item_more);
+ this.moreImage = R.drawable.list_item_more;
+ }
+
+ if (downloadFile.isDownloading() && !downloadFile.isDownloadCancelled() && partialFileExists) {
+ statusTextView.setText(Util.formatLocalizedBytes(partialFile.length(), getContext()));
+ rightImage = R.drawable.downloading;
+ } else if(this.rightImage != 0) {
+ statusTextView.setText(null);
+ }
+ if(this.rightImage != rightImage) {
+ statusTextView.setCompoundDrawablesWithIntrinsicBounds(0, 0, rightImage, 0);
+ this.rightImage = rightImage;
+ }
+
+ boolean playing = downloadService.getCurrentPlaying() == downloadFile;
+ if (playing) {
+ if(!this.playing) {
+ this.playing = playing;
+ titleTextView.setCompoundDrawablesWithIntrinsicBounds(R.drawable.stat_notify_playing, 0, 0, 0);
+ }
+ } else {
+ if(this.playing) {
+ this.playing = playing;
+ titleTextView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
+ }
+ }
+ }
+
+ @Override
+ public void setChecked(boolean b) {
+ checkedTextView.setChecked(b);
+ }
+
+ @Override
+ public boolean isChecked() {
+ return checkedTextView.isChecked();
+ }
+
+ @Override
+ public void toggle() {
+ checkedTextView.toggle();
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/UpdateView.java b/src/github/daneren2005/dsub/view/UpdateView.java
new file mode 100644
index 00000000..7ce27f06
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/UpdateView.java
@@ -0,0 +1,133 @@
+/*
+ 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 github.daneren2005.dsub.view;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.LinearLayout;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.WeakHashMap;
+
+public class UpdateView extends LinearLayout {
+ private static final String TAG = UpdateView.class.getSimpleName();
+ private static final WeakHashMap<UpdateView, ?> INSTANCES = new WeakHashMap<UpdateView, Object>();
+
+ private static Handler backgroundHandler;
+ private static Handler uiHandler;
+ private static Runnable updateRunnable;
+
+ public UpdateView(Context context) {
+ super(context);
+
+ setLayoutParams(new AbsListView.LayoutParams(
+ ViewGroup.LayoutParams.FILL_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT));
+
+ INSTANCES.put(this, null);
+ int instanceCount = INSTANCES.size();
+ if (instanceCount > 50) {
+ Log.w(TAG, instanceCount + " live UpdateView instances");
+ }
+
+ startUpdater();
+ }
+
+ @Override
+ public void setPressed(boolean pressed) {
+
+ }
+
+ private static synchronized void startUpdater() {
+ if(uiHandler != null) {
+ return;
+ }
+
+ uiHandler = new Handler();
+ updateRunnable = new Runnable() {
+ @Override
+ public void run() {
+ updateAll();
+ }
+ };
+
+ new Thread(new Runnable() {
+ public void run() {
+ Looper.prepare();
+ backgroundHandler = new Handler(Looper.myLooper());
+ uiHandler.post(updateRunnable);
+ Looper.loop();
+ }
+ }).start();
+ }
+
+ private static void updateAll() {
+ try {
+ List<UpdateView> views = new ArrayList<UpdateView>();;
+ for (UpdateView view : INSTANCES.keySet()) {
+ if (view.isShown()) {
+ views.add(view);
+ }
+ }
+ updateAllLive(views);
+ } catch (Throwable x) {
+ Log.w(TAG, "Error when updating song views.", x);
+ }
+ }
+ private static void updateAllLive(final List<UpdateView> views) {
+ final Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ try {
+ for(UpdateView view: views) {
+ view.update();
+ }
+ } catch (Throwable x) {
+ Log.w(TAG, "Error when updating song views.", x);
+ }
+ uiHandler.postDelayed(updateRunnable, 1000L);
+ }
+ };
+
+ backgroundHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ for(UpdateView view: views) {
+ view.updateBackground();
+ }
+ uiHandler.post(runnable);
+ } catch (Throwable x) {
+ Log.w(TAG, "Error when updating song views.", x);
+ }
+ }
+ });
+ }
+
+ protected void updateBackground() {
+
+ }
+ protected void update() {
+
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/VisualizerView.java b/src/github/daneren2005/dsub/view/VisualizerView.java
new file mode 100644
index 00000000..53ebc2ec
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/VisualizerView.java
@@ -0,0 +1,137 @@
+/*
+ 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 github.daneren2005.dsub.view;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.media.audiofx.Visualizer;
+import android.util.AttributeSet;
+import android.view.View;
+import github.daneren2005.dsub.audiofx.VisualizerController;
+import github.daneren2005.dsub.domain.PlayerState;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.service.DownloadServiceImpl;
+
+/**
+ * A simple class that draws waveform data received from a
+ * {@link Visualizer.OnDataCaptureListener#onWaveFormDataCapture}
+ *
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class VisualizerView extends View {
+
+ private static final int PREFERRED_CAPTURE_RATE_MILLIHERTZ = 20000;
+
+ private final Paint paint = new Paint();
+
+ private byte[] data;
+ private float[] points;
+ private boolean active = false;
+
+ public VisualizerView(Context context) {
+ super(context);
+
+ paint.setStrokeWidth(2f);
+ paint.setAntiAlias(true);
+ paint.setColor(Color.rgb(51, 181, 229));
+ }
+
+ public boolean isActive() {
+ return active;
+ }
+
+ public void setActive(boolean active) {
+ this.active = active;
+ VisualizerController visualizerController = getVizualiser();
+ Visualizer visualizer = visualizerController == null ? null : visualizerController.getVisualizer();
+ if (visualizer == null) {
+ this.active = false;
+ return;
+ }
+
+ int captureRate = Math.min(PREFERRED_CAPTURE_RATE_MILLIHERTZ, Visualizer.getMaxCaptureRate());
+ if (active) {
+ visualizer.setDataCaptureListener(new Visualizer.OnDataCaptureListener() {
+ @Override
+ public void onWaveFormDataCapture(Visualizer visualizer, byte[] waveform, int samplingRate) {
+ updateVisualizer(waveform);
+ }
+
+ @Override
+ public void onFftDataCapture(Visualizer visualizer, byte[] fft, int samplingRate) {
+ }
+ }, captureRate, true, false);
+ } else {
+ visualizer.setDataCaptureListener(null, captureRate, false, false);
+ }
+
+ visualizer.setEnabled(active);
+ if(!active) {
+ visualizerController.release();
+ }
+ invalidate();
+ }
+
+ private VisualizerController getVizualiser() {
+ DownloadService downloadService = DownloadServiceImpl.getInstance();
+ VisualizerController visualizerController = downloadService == null ? null : downloadService.getVisualizerController();
+ return visualizerController;
+ }
+
+ private void updateVisualizer(byte[] waveform) {
+ this.data = waveform;
+ invalidate();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ if (!active) {
+ return;
+ }
+ DownloadService downloadService = DownloadServiceImpl.getInstance();
+ if (downloadService != null && downloadService.getPlayerState() != PlayerState.STARTED) {
+ return;
+ }
+
+ if (data == null) {
+ return;
+ }
+
+ if (points == null || points.length < data.length * 4) {
+ points = new float[data.length * 4];
+ }
+
+ int w = getWidth();
+ int h = getHeight();
+
+ for (int i = 0; i < data.length - 1; i++) {
+ points[i * 4] = w * i / (data.length - 1);
+ points[i * 4 + 1] = h / 2 + ((byte) (data[i] + 128)) * (h / 2) / 128;
+ points[i * 4 + 2] = w * (i + 1) / (data.length - 1);
+ points[i * 4 + 3] = h / 2 + ((byte) (data[i + 1] + 128)) * (h / 2) / 128;
+ }
+
+ canvas.drawLines(points, paint);
+ }
+}