diff options
Diffstat (limited to 'src/github/daneren2005/dsub/view')
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); + } +} |