diff options
author | Scott Jackson <daneren2005@gmail.com> | 2015-04-25 17:03:02 -0700 |
---|---|---|
committer | Scott Jackson <daneren2005@gmail.com> | 2015-04-25 17:03:05 -0700 |
commit | cfd014d38cba03ba05f571597b361ab253bff578 (patch) | |
tree | 4256723561dec7ef3ed3507382eb7020724ec570 /app/src/main/java/github/daneren2005/dsub/view | |
parent | 8a332a20ec272d59fe74520825b18017a8f0cac3 (diff) | |
download | dsub-cfd014d38cba03ba05f571597b361ab253bff578.tar.gz dsub-cfd014d38cba03ba05f571597b361ab253bff578.tar.bz2 dsub-cfd014d38cba03ba05f571597b361ab253bff578.zip |
Update to gradle
Diffstat (limited to 'app/src/main/java/github/daneren2005/dsub/view')
24 files changed, 3627 insertions, 0 deletions
diff --git a/app/src/main/java/github/daneren2005/dsub/view/AlbumCell.java b/app/src/main/java/github/daneren2005/dsub/view/AlbumCell.java new file mode 100644 index 00000000..8707ece7 --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/view/AlbumCell.java @@ -0,0 +1,108 @@ +/* + 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 2014 (C) Scott Jackson +*/ + +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.RatingBar; +import android.widget.TextView; + +import java.io.File; + +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.util.FileUtil; +import github.daneren2005.dsub.util.ImageLoader; + +public class AlbumCell extends UpdateView { + private static final String TAG = AlbumCell.class.getSimpleName(); + + private Context context; + private MusicDirectory.Entry album; + private File file; + + private View coverArtView; + private TextView titleView; + private TextView artistView; + private boolean showArtist = true; + + public AlbumCell(Context context) { + super(context); + this.context = context; + LayoutInflater.from(context).inflate(R.layout.album_cell_item, this, true); + + coverArtView = findViewById(R.id.album_coverart); + titleView = (TextView) findViewById(R.id.album_title); + artistView = (TextView) findViewById(R.id.album_artist); + + ratingBar = (RatingBar) findViewById(R.id.album_rating); + ratingBar.setFocusable(false); + starButton = (ImageButton) findViewById(R.id.album_star); + starButton.setFocusable(false); + moreButton = (ImageView) findViewById(R.id.album_more); + moreButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + v.showContextMenu(); + } + }); + } + + public void setShowArtist(boolean showArtist) { + this.showArtist = showArtist; + } + + protected void setObjectImpl(Object obj1, Object obj2) { + this.album = (MusicDirectory.Entry) obj1; + titleView.setText(album.getAlbumDisplay()); + String artist = ""; + if(showArtist) { + artist = album.getArtist(); + if (artist == null) { + artist = ""; + } + if (album.getYear() != null) { + artist += " - " + album.getYear(); + } + } else if(album.getYear() != null) { + artist += album.getYear(); + } + artistView.setText(album.getArtist() == null ? "" : artist); + imageTask = ((ImageLoader)obj2).loadImage(coverArtView, album, false, true); + file = null; + } + + @Override + protected void updateBackground() { + if(file == null) { + file = FileUtil.getAlbumDirectory(context, album); + } + + exists = file.exists(); + isStarred = album.isStarred(); + isRated = album.getRating(); + } + + public MusicDirectory.Entry getEntry() { + return album; + } + + public File getFile() { + return file; + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/view/AlbumView.java b/app/src/main/java/github/daneren2005/dsub/view/AlbumView.java new file mode 100644 index 00000000..bd54ea1e --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/view/AlbumView.java @@ -0,0 +1,107 @@ +/* + 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.RatingBar; +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; +import java.util.List; + +/** + * 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 File file; + + private TextView titleView; + private TextView artistView; + private View coverArtView; + + 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); + ratingBar = (RatingBar) findViewById(R.id.album_rating); + starButton = (ImageButton) findViewById(R.id.album_star); + starButton.setFocusable(false); + + moreButton = (ImageView) findViewById(R.id.album_more); + moreButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + v.showContextMenu(); + } + }); + } + + protected void setObjectImpl(Object obj1, Object obj2) { + this.album = (MusicDirectory.Entry) obj1; + titleView.setText(album.getAlbumDisplay()); + String artist = album.getArtist(); + if(artist == null) { + artist = ""; + } + if(album.getYear() != null) { + artist += " - " + album.getYear(); + } + artistView.setText(artist); + artistView.setVisibility(album.getArtist() == null ? View.GONE : View.VISIBLE); + imageTask = ((ImageLoader)obj2).loadImage(coverArtView, album, false, true); + file = null; + } + + @Override + protected void updateBackground() { + if(file == null) { + file = FileUtil.getAlbumDirectory(context, album); + } + + exists = file.exists(); + isStarred = album.isStarred(); + isRated = album.getRating(); + } + + public MusicDirectory.Entry getEntry() { + return album; + } + + public File getFile() { + return file; + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/view/ArtistEntryView.java b/app/src/main/java/github/daneren2005/dsub/view/ArtistEntryView.java new file mode 100644 index 00000000..71bdeb78 --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/view/ArtistEntryView.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 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 = ArtistEntryView.class.getSimpleName(); + + private Context context; + private MusicDirectory.Entry artist; + private File file; + + private TextView titleView; + + public ArtistEntryView(Context context) { + super(context); + this.context = context; + LayoutInflater.from(context).inflate(R.layout.basic_list_item, this, true); + + titleView = (TextView) findViewById(R.id.item_name); + starButton = (ImageButton) findViewById(R.id.item_star); + starButton.setFocusable(false); + moreButton = (ImageView) findViewById(R.id.item_more); + moreButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + v.showContextMenu(); + } + }); + } + + protected void setObjectImpl(Object obj) { + this.artist = (MusicDirectory.Entry) obj; + titleView.setText(artist.getTitle()); + file = FileUtil.getArtistDirectory(context, artist); + } + + @Override + protected void updateBackground() { + exists = file.exists(); + isStarred = artist.isStarred(); + } + + public File getFile() { + return file; + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/view/ArtistView.java b/app/src/main/java/github/daneren2005/dsub/view/ArtistView.java new file mode 100644 index 00000000..c255be69 --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/view/ArtistView.java @@ -0,0 +1,78 @@ +/* + 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 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 File file; + + private TextView titleView; + + public ArtistView(Context context) { + super(context); + this.context = context; + LayoutInflater.from(context).inflate(R.layout.basic_list_item, this, true); + + titleView = (TextView) findViewById(R.id.item_name); + starButton = (ImageButton) findViewById(R.id.item_star); + starButton.setFocusable(false); + moreButton = (ImageView) findViewById(R.id.item_more); + moreButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + v.showContextMenu(); + } + }); + } + + protected void setObjectImpl(Object obj) { + this.artist = (Artist) obj; + titleView.setText(artist.getName()); + file = FileUtil.getArtistDirectory(context, artist); + } + + @Override + protected void updateBackground() { + exists = file.exists(); + isStarred = artist.isStarred(); + } + + public File getFile() { + return file; + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/view/AutoRepeatButton.java b/app/src/main/java/github/daneren2005/dsub/view/AutoRepeatButton.java new file mode 100644 index 00000000..3c59dd37 --- /dev/null +++ b/app/src/main/java/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 static final long initialRepeatDelay = 1000; + private static final 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/app/src/main/java/github/daneren2005/dsub/view/ChangeLog.java b/app/src/main/java/github/daneren2005/dsub/view/ChangeLog.java new file mode 100644 index 00000000..096583c7 --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/view/ChangeLog.java @@ -0,0 +1,546 @@ +/* + * 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 + String log = getLog(full); + // No changes to show + if(log == null) { + return null; + } + + wv.loadDataWithBaseURL(null, log, "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. + */ + public 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 xml/changelog.xml + SparseArray<ReleaseItem> changelog; + XmlResourceParser resXml = mContext.getResources().getXml(R.xml.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>(changelog.size()); + for (int i = 0, len = changelog.size(); i < len; i++) { + int key = changelog.keyAt(i); + versions.add(key); + } + + // ... and sort them (newest version first). + Collections.sort(versions, Collections.reverseOrder()); + + if(versions.size() == 0) { + return null; + } + + 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); + + 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/app/src/main/java/github/daneren2005/dsub/view/ErrorDialog.java b/app/src/main/java/github/daneren2005/dsub/view/ErrorDialog.java new file mode 100644 index 00000000..0b9d05a0 --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/view/ErrorDialog.java @@ -0,0 +1,75 @@ +/* + 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.R; +import github.daneren2005.dsub.activity.SubsonicFragmentActivity; +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); + } + } + }); + + try { + builder.create().show(); + } catch(Exception e) { + // Don't care, just means no activity to attach to + } + } + + private void restart(Activity activity) { + Intent intent = new Intent(activity, SubsonicFragmentActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + Util.startActivityWithoutTransition(activity, intent); + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/view/FadeOutAnimation.java b/app/src/main/java/github/daneren2005/dsub/view/FadeOutAnimation.java new file mode 100644 index 00000000..817839ef --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/github/daneren2005/dsub/view/GenreView.java b/app/src/main/java/github/daneren2005/dsub/view/GenreView.java new file mode 100644 index 00000000..8dbcf89d --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/view/GenreView.java @@ -0,0 +1,58 @@ +/* + 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.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 TextView songsView; + private TextView albumsView; + + public GenreView(Context context) { + super(context, false); + LayoutInflater.from(context).inflate(R.layout.genre_list_item, this, true); + + titleView = (TextView) findViewById(R.id.genre_name); + songsView = (TextView) findViewById(R.id.genre_songs); + albumsView = (TextView) findViewById(R.id.genre_albums); + } + + public void setObjectImpl(Object obj) { + Genre genre = (Genre) obj; + titleView.setText(genre.getName()); + + if(genre.getAlbumCount() != null) { + songsView.setVisibility(View.VISIBLE); + albumsView.setVisibility(View.VISIBLE); + songsView.setText(context.getResources().getString(R.string.select_genre_songs, genre.getSongCount())); + albumsView.setText(context.getResources().getString(R.string.select_genre_albums, genre.getAlbumCount())); + } else { + songsView.setVisibility(View.GONE); + albumsView.setVisibility(View.GONE); + } + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/view/HeaderGridView.java b/app/src/main/java/github/daneren2005/dsub/view/HeaderGridView.java new file mode 100644 index 00000000..8a82f353 --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/view/HeaderGridView.java @@ -0,0 +1,836 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * 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.annotation.TargetApi; +import android.content.Context; +import android.database.DataSetObservable; +import android.database.DataSetObserver; +import android.os.Build; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.*; + +import java.lang.reflect.Field; +import java.util.ArrayList; + +/** + * A {@link GridView} that supports adding header rows in a + * very similar way to {@link android.widget.ListView}. + * See {@link HeaderGridView#addHeaderView(View, Object, boolean)} + * See {@link HeaderGridView#addFooterView(View, Object, boolean)} + */ +public class HeaderGridView extends GridView { + private static final String TAG = HeaderGridView.class.getSimpleName(); + public static boolean DEBUG = false; + + /** + * A class that represents a fixed view in a list, for example a header at the top + * or a footer at the bottom. + */ + private static class FixedViewInfo { + /** + * The view to add to the grid + */ + public View view; + public ViewGroup viewContainer; + /** + * The data backing the view. This is returned from {@link ListAdapter#getItem(int)}. + */ + public Object data; + /** + * <code>true</code> if the fixed view should be selectable in the grid + */ + public boolean isSelectable; + } + + private int mNumColumns = AUTO_FIT; + private View mViewForMeasureRowHeight = null; + private int mRowHeight = -1; + private static final String LOG_TAG = HeaderGridView.class.getSimpleName(); + + private ArrayList<FixedViewInfo> mHeaderViewInfos = new ArrayList<FixedViewInfo>(); + private ArrayList<FixedViewInfo> mFooterViewInfos = new ArrayList<FixedViewInfo>(); + + private void initHeaderGridView() { + } + + public HeaderGridView(Context context) { + super(context); + initHeaderGridView(); + } + + public HeaderGridView(Context context, AttributeSet attrs) { + super(context, attrs); + initHeaderGridView(); + } + + public HeaderGridView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + initHeaderGridView(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + ListAdapter adapter = getAdapter(); + if (adapter != null && adapter instanceof HeaderViewGridAdapter) { + ((HeaderViewGridAdapter) adapter).setNumColumns(getNumColumnsCompatible()); + ((HeaderViewGridAdapter) adapter).setRowHeight(getRowHeight()); + } + } + + @Override + public void setClipChildren(boolean clipChildren) { + // Ignore, since the header rows depend on not being clipped + } + + /** + * Do not call this method unless you know how it works. + * + * @param clipChildren + */ + public void setClipChildrenSupper(boolean clipChildren) { + super.setClipChildren(false); + } + + /** + * Add a fixed view to appear at the top of the grid. If addHeaderView is + * called more than once, the views will appear in the order they were + * added. Views added using this call can take focus if they want. + * <p/> + * NOTE: Call this before calling setAdapter. This is so HeaderGridView can wrap + * the supplied cursor with one that will also account for header views. + * + * @param v The view to add. + */ + public void addHeaderView(View v) { + addHeaderView(v, null, true); + } + + /** + * Add a fixed view to appear at the top of the grid. If addHeaderView is + * called more than once, the views will appear in the order they were + * added. Views added using this call can take focus if they want. + * <p/> + * NOTE: Call this before calling setAdapter. This is so HeaderGridView can wrap + * the supplied cursor with one that will also account for header views. + * + * @param v The view to add. + * @param data Data to associate with this view + * @param isSelectable whether the item is selectable + */ + public void addHeaderView(View v, Object data, boolean isSelectable) { + ListAdapter adapter = getAdapter(); + if (adapter != null && !(adapter instanceof HeaderViewGridAdapter)) { + throw new IllegalStateException( + "Cannot add header view to grid -- setAdapter has already been called."); + } + + ViewGroup.LayoutParams lyp = v.getLayoutParams(); + + FixedViewInfo info = new FixedViewInfo(); + FrameLayout fl = new FullWidthFixedViewLayout(getContext()); + + if (lyp != null) { + v.setLayoutParams(new FrameLayout.LayoutParams(lyp.width, lyp.height)); + fl.setLayoutParams(new AbsListView.LayoutParams(lyp.width, lyp.height)); + } + fl.addView(v); + info.view = v; + info.viewContainer = fl; + info.data = data; + info.isSelectable = isSelectable; + mHeaderViewInfos.add(info); + // in the case of re-adding a header view, or adding one later on, + // we need to notify the observer + if (adapter != null) { + ((HeaderViewGridAdapter) adapter).notifyDataSetChanged(); + } + } + + public void addFooterView(View v) { + addFooterView(v, null, true); + } + + public void addFooterView(View v, Object data, boolean isSelectable) { + ListAdapter mAdapter = getAdapter(); + if (mAdapter != null && !(mAdapter instanceof HeaderViewGridAdapter)) { + throw new IllegalStateException( + "Cannot add header view to grid -- setAdapter has already been called."); + } + + ViewGroup.LayoutParams lyp = v.getLayoutParams(); + + FixedViewInfo info = new FixedViewInfo(); + FrameLayout fl = new FullWidthFixedViewLayout(getContext()); + + if (lyp != null) { + v.setLayoutParams(new FrameLayout.LayoutParams(lyp.width, lyp.height)); + fl.setLayoutParams(new AbsListView.LayoutParams(lyp.width, lyp.height)); + } + fl.addView(v); + info.view = v; + info.viewContainer = fl; + info.data = data; + info.isSelectable = isSelectable; + mFooterViewInfos.add(info); + + if (mAdapter != null) { + ((HeaderViewGridAdapter) mAdapter).notifyDataSetChanged(); + } + } + + public int getHeaderViewCount() { + return mHeaderViewInfos.size(); + } + + public int getFooterViewCount() { + return mFooterViewInfos.size(); + } + + /** + * Removes a previously-added header view. + * + * @param v The view to remove + * @return true if the view was removed, false if the view was not a header + * view + */ + public boolean removeHeaderView(View v) { + if (mHeaderViewInfos.size() > 0) { + boolean result = false; + ListAdapter adapter = getAdapter(); + if (adapter != null && ((HeaderViewGridAdapter) adapter).removeHeader(v)) { + result = true; + } + removeFixedViewInfo(v, mHeaderViewInfos); + return result; + } + return false; + } + + /** + * Removes a previously-added footer view. + * + * @param v The view to remove + * @return true if the view was removed, false if the view was not a header + * view + */ + public boolean removeFooterView(View v) { + if (mFooterViewInfos.size() > 0) { + boolean result = false; + ListAdapter adapter = getAdapter(); + if (adapter != null && ((HeaderViewGridAdapter) adapter).removeFooter(v)) { + result = true; + } + removeFixedViewInfo(v, mFooterViewInfos); + return result; + } + return false; + } + + private void removeFixedViewInfo(View v, ArrayList<FixedViewInfo> where) { + int len = where.size(); + for (int i = 0; i < len; ++i) { + FixedViewInfo info = where.get(i); + if (info.view == v) { + where.remove(i); + break; + } + } + } + + @TargetApi(11) + private int getNumColumnsCompatible() { + if (Build.VERSION.SDK_INT >= 11) { + return super.getNumColumns(); + } else { + try { + Field numColumns = GridView.class.getSuperclass().getDeclaredField("mNumColumns"); + numColumns.setAccessible(true); + return numColumns.getInt(this); + } catch (Exception e) { + if (mNumColumns != -1) { + return mNumColumns; + } else { + return 2; + } + } + } + } + + @TargetApi(16) + private int getColumnWidthCompatible() { + if (Build.VERSION.SDK_INT >= 16) { + return super.getColumnWidth(); + } else { + try { + Field numColumns = getClass().getSuperclass().getDeclaredField("mColumnWidth"); + numColumns.setAccessible(true); + return numColumns.getInt(this); + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mViewForMeasureRowHeight = null; + } + + public void invalidateRowHeight() { + mRowHeight = -1; + } + + public int getHeaderHeight(int row) { + if (row >= 0) { + return mHeaderViewInfos.get(row).view.getMeasuredHeight(); + } + + return 0; + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + public int getVerticalSpacing(){ + int value = 0; + + try { + int currentapiVersion = android.os.Build.VERSION.SDK_INT; + if (currentapiVersion < Build.VERSION_CODES.JELLY_BEAN){ + Field field = this.getClass().getSuperclass().getDeclaredField("mVerticalSpacing"); + field.setAccessible(true); + value = field.getInt(this); + } else{ + value = super.getVerticalSpacing(); + } + + }catch (Exception ex){ + + } + + return value; + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + public int getHorizontalSpacing(){ + int value = 0; + + try { + int currentapiVersion = android.os.Build.VERSION.SDK_INT; + if (currentapiVersion < Build.VERSION_CODES.JELLY_BEAN){ + Field field = this.getClass().getSuperclass().getDeclaredField("mHorizontalSpacing"); + field.setAccessible(true); + value = field.getInt(this); + } else{ + value = super.getHorizontalSpacing(); + } + + }catch (Exception ex){ + + } + + return value; + } + + public int getRowHeight() { + if (mRowHeight > 0) { + // return mRowHeight; + } + ListAdapter adapter = getAdapter(); + int numColumns = getNumColumnsCompatible(); + + // adapter has not been set or has no views in it; + if (adapter == null || adapter.getCount() <= numColumns * (mHeaderViewInfos.size() + mFooterViewInfos.size()) || numColumns == -1) { + return -1; + } + int mColumnWidth = getColumnWidthCompatible(); + View view = getAdapter().getView(numColumns * mHeaderViewInfos.size(), mViewForMeasureRowHeight, this); + AbsListView.LayoutParams p = (AbsListView.LayoutParams) view.getLayoutParams(); + if (p == null) { + p = new AbsListView.LayoutParams(-1, -2, 0); + view.setLayoutParams(p); + } + int childHeightSpec = getChildMeasureSpec( + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 0, p.height); + int childWidthSpec = getChildMeasureSpec( + MeasureSpec.makeMeasureSpec(mColumnWidth, MeasureSpec.EXACTLY), 0, p.width); + view.measure(childWidthSpec, childHeightSpec); + mViewForMeasureRowHeight = view; + mRowHeight = view.getMeasuredHeight(); + return mRowHeight; + } + + @TargetApi(11) + public void tryToScrollToBottomSmoothly() { + int lastPos = getAdapter().getCount() - 1; + if (Build.VERSION.SDK_INT >= 11) { + smoothScrollToPositionFromTop(lastPos, 0); + } else { + setSelection(lastPos); + } + } + + @TargetApi(11) + public void tryToScrollToBottomSmoothly(int duration) { + int lastPos = getAdapter().getCount() - 1; + if (Build.VERSION.SDK_INT >= 11) { + smoothScrollToPositionFromTop(lastPos, 0, duration); + } else { + setSelection(lastPos); + } + } + + @Override + public void setAdapter(ListAdapter adapter) { + if (mHeaderViewInfos.size() > 0 || mFooterViewInfos.size() > 0) { + HeaderViewGridAdapter headerViewGridAdapter = new HeaderViewGridAdapter(mHeaderViewInfos, mFooterViewInfos, adapter); + int numColumns = getNumColumnsCompatible(); + if (numColumns > 1) { + headerViewGridAdapter.setNumColumns(numColumns); + } + headerViewGridAdapter.setRowHeight(getRowHeight()); + super.setAdapter(headerViewGridAdapter); + } else { + super.setAdapter(adapter); + } + } + + /** + * full width + */ + private class FullWidthFixedViewLayout extends FrameLayout { + + public FullWidthFixedViewLayout(Context context) { + super(context); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + int realLeft = HeaderGridView.this.getPaddingLeft() + getPaddingLeft(); + // Try to make where it should be, from left, full width + if (realLeft != left) { + offsetLeftAndRight(realLeft - left); + } + super.onLayout(changed, left, top, right, bottom); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int targetWidth = HeaderGridView.this.getMeasuredWidth() + - HeaderGridView.this.getPaddingLeft() + - HeaderGridView.this.getPaddingRight(); + widthMeasureSpec = MeasureSpec.makeMeasureSpec(targetWidth, + MeasureSpec.getMode(widthMeasureSpec)); + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + } + + @Override + public void setNumColumns(int numColumns) { + super.setNumColumns(numColumns); + mNumColumns = numColumns; + ListAdapter adapter = getAdapter(); + if (adapter != null && adapter instanceof HeaderViewGridAdapter) { + ((HeaderViewGridAdapter) adapter).setNumColumns(numColumns); + } + } + + /** + * ListAdapter used when a HeaderGridView has header views. This ListAdapter + * wraps another one and also keeps track of the header views and their + * associated data objects. + * <p>This is intended as a base class; you will probably not need to + * use this class directly in your own code. + */ + private static class HeaderViewGridAdapter extends BaseAdapter implements WrapperListAdapter, Filterable { + // This is used to notify the container of updates relating to number of columns + // or headers changing, which changes the number of placeholders needed + private final DataSetObservable mDataSetObservable = new DataSetObservable(); + private final ListAdapter mAdapter; + static final ArrayList<FixedViewInfo> EMPTY_INFO_LIST = + new ArrayList<FixedViewInfo>(); + + // This ArrayList is assumed to NOT be null. + ArrayList<FixedViewInfo> mHeaderViewInfos; + ArrayList<FixedViewInfo> mFooterViewInfos; + private int mNumColumns = 1; + private int mRowHeight = -1; + boolean mAreAllFixedViewsSelectable; + private final boolean mIsFilterable; + private boolean mCachePlaceHoldView = true; + // From Recycle Bin or calling getView, this a question... + private boolean mCacheFirstHeaderView = false; + + public HeaderViewGridAdapter(ArrayList<FixedViewInfo> headerViewInfos, ArrayList<FixedViewInfo> footViewInfos, ListAdapter adapter) { + mAdapter = adapter; + mIsFilterable = adapter instanceof Filterable; + if (headerViewInfos == null) { + mHeaderViewInfos = EMPTY_INFO_LIST; + } else { + mHeaderViewInfos = headerViewInfos; + } + + if (footViewInfos == null) { + mFooterViewInfos = EMPTY_INFO_LIST; + } else { + mFooterViewInfos = footViewInfos; + } + mAreAllFixedViewsSelectable = areAllListInfosSelectable(mHeaderViewInfos) + && areAllListInfosSelectable(mFooterViewInfos); + } + + public void setNumColumns(int numColumns) { + if (numColumns < 1) { + return; + } + if (mNumColumns != numColumns) { + mNumColumns = numColumns; + notifyDataSetChanged(); + } + } + + public void setRowHeight(int height) { + mRowHeight = height; + } + + public int getHeadersCount() { + return mHeaderViewInfos.size(); + } + + public int getFootersCount() { + return mFooterViewInfos.size(); + } + + @Override + public boolean isEmpty() { + return (mAdapter == null || mAdapter.isEmpty()) && getHeadersCount() == 0 && getFootersCount() == 0; + } + + private boolean areAllListInfosSelectable(ArrayList<FixedViewInfo> infos) { + if (infos != null) { + for (FixedViewInfo info : infos) { + if (!info.isSelectable) { + return false; + } + } + } + return true; + } + + public boolean removeHeader(View v) { + for (int i = 0; i < mHeaderViewInfos.size(); i++) { + FixedViewInfo info = mHeaderViewInfos.get(i); + if (info.view == v) { + mHeaderViewInfos.remove(i); + mAreAllFixedViewsSelectable = + areAllListInfosSelectable(mHeaderViewInfos) && areAllListInfosSelectable(mFooterViewInfos); + mDataSetObservable.notifyChanged(); + return true; + } + } + return false; + } + + public boolean removeFooter(View v) { + for (int i = 0; i < mFooterViewInfos.size(); i++) { + FixedViewInfo info = mFooterViewInfos.get(i); + if (info.view == v) { + mFooterViewInfos.remove(i); + mAreAllFixedViewsSelectable = + areAllListInfosSelectable(mHeaderViewInfos) && areAllListInfosSelectable(mFooterViewInfos); + mDataSetObservable.notifyChanged(); + return true; + } + } + return false; + } + + @Override + public int getCount() { + if (mAdapter != null) { + return (getFootersCount() + getHeadersCount()) * mNumColumns + getAdapterAndPlaceHolderCount(); + } else { + return (getFootersCount() + getHeadersCount()) * mNumColumns; + } + } + + @Override + public boolean areAllItemsEnabled() { + if (mAdapter != null) { + return mAreAllFixedViewsSelectable && mAdapter.areAllItemsEnabled(); + } else { + return true; + } + } + + private int getAdapterAndPlaceHolderCount() { + final int adapterCount = (int) (Math.ceil(1f * mAdapter.getCount() / mNumColumns) * mNumColumns); + return adapterCount; + } + + @Override + public boolean isEnabled(int position) { + // Header (negative positions will throw an IndexOutOfBoundsException) + int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns; + if (position < numHeadersAndPlaceholders) { + return position % mNumColumns == 0 + && mHeaderViewInfos.get(position / mNumColumns).isSelectable; + } + + // Adapter + final int adjPosition = position - numHeadersAndPlaceholders; + int adapterCount = 0; + if (mAdapter != null) { + adapterCount = getAdapterAndPlaceHolderCount(); + if (adjPosition < adapterCount) { + return adjPosition < mAdapter.getCount() && mAdapter.isEnabled(adjPosition); + } + } + + // Footer (off-limits positions will throw an IndexOutOfBoundsException) + final int footerPosition = adjPosition - adapterCount; + return footerPosition % mNumColumns == 0 + && mFooterViewInfos.get(footerPosition / mNumColumns).isSelectable; + } + + @Override + public Object getItem(int position) { + // Header (negative positions will throw an ArrayIndexOutOfBoundsException) + int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns; + if (position < numHeadersAndPlaceholders) { + if (position % mNumColumns == 0) { + return mHeaderViewInfos.get(position / mNumColumns).data; + } + return null; + } + + // Adapter + final int adjPosition = position - numHeadersAndPlaceholders; + int adapterCount = 0; + if (mAdapter != null) { + adapterCount = getAdapterAndPlaceHolderCount(); + if (adjPosition < adapterCount) { + if (adjPosition < mAdapter.getCount()) { + return mAdapter.getItem(adjPosition); + } else { + return null; + } + } + } + + // Footer (off-limits positions will throw an IndexOutOfBoundsException) + final int footerPosition = adjPosition - adapterCount; + if (footerPosition % mNumColumns == 0) { + return mFooterViewInfos.get(footerPosition).data; + } else { + return null; + } + } + + @Override + public long getItemId(int position) { + int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns; + if (mAdapter != null && position >= numHeadersAndPlaceholders) { + int adjPosition = position - numHeadersAndPlaceholders; + int adapterCount = mAdapter.getCount(); + if (adjPosition < adapterCount) { + return mAdapter.getItemId(adjPosition); + } + } + return -1; + } + + @Override + public boolean hasStableIds() { + if (mAdapter != null) { + return mAdapter.hasStableIds(); + } + return false; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (DEBUG) { + Log.d(LOG_TAG, String.format("getView: %s, reused: %s", position, convertView == null)); + } + // Header (negative positions will throw an ArrayIndexOutOfBoundsException) + int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns; + if (position < numHeadersAndPlaceholders) { + View headerViewContainer = mHeaderViewInfos + .get(position / mNumColumns).viewContainer; + if (position % mNumColumns == 0) { + return headerViewContainer; + } else { + if (convertView == null) { + convertView = new View(parent.getContext()); + } + // We need to do this because GridView uses the height of the last item + // in a row to determine the height for the entire row. + convertView.setVisibility(View.INVISIBLE); + convertView.setMinimumHeight(headerViewContainer.getHeight()); + return convertView; + } + } + // Adapter + final int adjPosition = position - numHeadersAndPlaceholders; + int adapterCount = 0; + if (mAdapter != null) { + adapterCount = getAdapterAndPlaceHolderCount(); + if (adjPosition < adapterCount) { + if (adjPosition < mAdapter.getCount()) { + View view = mAdapter.getView(adjPosition, convertView, parent); + return view; + } else { + if (convertView == null) { + convertView = new View(parent.getContext()); + } + convertView.setVisibility(View.INVISIBLE); + convertView.setMinimumHeight(mRowHeight); + return convertView; + } + } + } + // Footer + final int footerPosition = adjPosition - adapterCount; + if (footerPosition < getCount()) { + View footViewContainer = mFooterViewInfos + .get(footerPosition / mNumColumns).viewContainer; + if (position % mNumColumns == 0) { + return footViewContainer; + } else { + if (convertView == null) { + convertView = new View(parent.getContext()); + } + // We need to do this because GridView uses the height of the last item + // in a row to determine the height for the entire row. + convertView.setVisibility(View.INVISIBLE); + convertView.setMinimumHeight(footViewContainer.getHeight()); + return convertView; + } + } + throw new ArrayIndexOutOfBoundsException(position); + } + + @Override + public int getItemViewType(int position) { + + final int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns; + final int adapterViewTypeStart = mAdapter == null ? 0 : mAdapter.getViewTypeCount() - 1; + int type = AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER; + if (mCachePlaceHoldView) { + // Header + if (position < numHeadersAndPlaceholders) { + if (position == 0) { + if (mCacheFirstHeaderView) { + type = adapterViewTypeStart + mHeaderViewInfos.size() + mFooterViewInfos.size() + 1 + 1; + } + } + if (position % mNumColumns != 0) { + type = adapterViewTypeStart + (position / mNumColumns + 1); + } + } + } + + // Adapter + final int adjPosition = position - numHeadersAndPlaceholders; + int adapterCount = 0; + if (mAdapter != null) { + adapterCount = getAdapterAndPlaceHolderCount(); + if (adjPosition >= 0 && adjPosition < adapterCount) { + if (adjPosition < mAdapter.getCount()) { + type = mAdapter.getItemViewType(adjPosition); + } else { + if (mCachePlaceHoldView) { + type = adapterViewTypeStart + mHeaderViewInfos.size() + 1; + } + } + } + } + + if (mCachePlaceHoldView) { + // Footer + final int footerPosition = adjPosition - adapterCount; + if (footerPosition >= 0 && footerPosition < getCount() && (footerPosition % mNumColumns) != 0) { + type = adapterViewTypeStart + mHeaderViewInfos.size() + 1 + (footerPosition / mNumColumns + 1); + } + } + if (DEBUG) { + Log.d(LOG_TAG, String.format("getItemViewType: pos: %s, result: %s", position, type, mCachePlaceHoldView, mCacheFirstHeaderView)); + } + return type; + } + + /** + * content view, content view holder, header[0], header and footer placeholder(s) + * + * @return + */ + @Override + public int getViewTypeCount() { + int count = mAdapter == null ? 1 : mAdapter.getViewTypeCount(); + if (mCachePlaceHoldView) { + int offset = mHeaderViewInfos.size() + 1 + mFooterViewInfos.size(); + if (mCacheFirstHeaderView) { + offset += 1; + } + count += offset; + } + if (DEBUG) { + Log.d(LOG_TAG, String.format("getViewTypeCount: %s", count)); + } + return count; + } + + @Override + public void registerDataSetObserver(DataSetObserver observer) { + mDataSetObservable.registerObserver(observer); + if (mAdapter != null) { + mAdapter.registerDataSetObserver(observer); + } + } + + @Override + public void unregisterDataSetObserver(DataSetObserver observer) { + mDataSetObservable.unregisterObserver(observer); + if (mAdapter != null) { + mAdapter.unregisterDataSetObserver(observer); + } + } + + @Override + public Filter getFilter() { + if (mIsFilterable) { + return ((Filterable) mAdapter).getFilter(); + } + return null; + } + + @Override + public ListAdapter getWrappedAdapter() { + return mAdapter; + } + + public void notifyDataSetChanged() { + mDataSetObservable.notifyChanged(); + } + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/view/MyLeadingMarginSpan2.java b/app/src/main/java/github/daneren2005/dsub/view/MyLeadingMarginSpan2.java new file mode 100644 index 00000000..20281a28 --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/view/MyLeadingMarginSpan2.java @@ -0,0 +1,34 @@ +package github.daneren2005.dsub.view; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.text.Layout; +import android.text.style.LeadingMarginSpan; + +/** + * Created by Scott on 1/13/2015. + */ +public class MyLeadingMarginSpan2 implements LeadingMarginSpan.LeadingMarginSpan2 { + private int margin; + private int lines; + + public MyLeadingMarginSpan2(int lines, int margin) { + this.margin = margin; + this.lines = lines; + } + + @Override + public int getLeadingMargin(boolean first) { + return first ? margin : 0; + } + + @Override + public int getLeadingMarginLineCount() { + return lines; + } + + @Override + public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, + int top, int baseline, int bottom, CharSequence text, + int start, int end, boolean first, Layout layout) {} +} diff --git a/app/src/main/java/github/daneren2005/dsub/view/MyViewFlipper.java b/app/src/main/java/github/daneren2005/dsub/view/MyViewFlipper.java new file mode 100644 index 00000000..26a3de08 --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/github/daneren2005/dsub/view/PlaylistSongView.java b/app/src/main/java/github/daneren2005/dsub/view/PlaylistSongView.java new file mode 100644 index 00000000..0264a785 --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/view/PlaylistSongView.java @@ -0,0 +1,102 @@ +/* + 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 java.util.List; + +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.domain.Playlist; +import github.daneren2005.dsub.util.FileUtil; +import github.daneren2005.dsub.util.SyncUtil; +import github.daneren2005.dsub.util.Util; + +public class PlaylistSongView extends UpdateView { + private static final String TAG = PlaylistSongView.class.getSimpleName(); + + private Context context; + private Playlist playlist; + + private TextView titleView; + private TextView countView; + private int count = 0; + private List<MusicDirectory.Entry> songs; + + public PlaylistSongView(Context context) { + super(context, false); + this.context = context; + LayoutInflater.from(context).inflate(R.layout.basic_count_item, this, true); + + titleView = (TextView) findViewById(R.id.basic_count_name); + countView = (TextView) findViewById(R.id.basic_count_count); + } + + protected void setObjectImpl(Object obj1, Object obj2) { + this.playlist = (Playlist) obj1; + this.songs = (List<MusicDirectory.Entry>) obj2; + count = 0; + titleView.setText(playlist.getName()); + // Make sure to hide initially so it's not present briefly before update + countView.setVisibility(View.GONE); + } + + @Override + protected void updateBackground() { + // Make sure to reset when starting count + count = 0; + + // Don't try to lookup playlist for Create New + if(!"-1".equals(playlist.getId())) { + MusicDirectory cache = FileUtil.deserialize(context, Util.getCacheName(context, "playlist", playlist.getId()), MusicDirectory.class); + if(cache != null) { + // Try to find song instances in the given playlists + for(MusicDirectory.Entry song: songs) { + if(cache.getChildren().contains(song)) { + count++; + } + } + } + } + } + + @Override + protected void update() { + // Update count display with appropriate information + if(count <= 0) { + countView.setVisibility(View.GONE); + } else { + String displayName; + if(count < 10) { + displayName = "0" + count; + } else { + displayName = "" + count; + } + + countView.setText(displayName); + countView.setVisibility(View.VISIBLE); + } + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/view/PlaylistView.java b/app/src/main/java/github/daneren2005/dsub/view/PlaylistView.java new file mode 100644 index 00000000..25613984 --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/view/PlaylistView.java @@ -0,0 +1,69 @@ +/* + 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.Playlist; +import github.daneren2005.dsub.util.SyncUtil; + +/** + * 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; + + public PlaylistView(Context context) { + super(context); + this.context = context; + LayoutInflater.from(context).inflate(R.layout.basic_list_item, this, true); + + titleView = (TextView) findViewById(R.id.item_name); + starButton = (ImageButton) findViewById(R.id.item_star); + starButton.setFocusable(false); + moreButton = (ImageView) findViewById(R.id.item_more); + moreButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + v.showContextMenu(); + } + }); + } + + protected void setObjectImpl(Object obj) { + this.playlist = (Playlist) obj; + titleView.setText(playlist.getName()); + } + + @Override + protected void updateBackground() { + pinned = SyncUtil.isSyncedPlaylist(context, playlist.getId()); + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/view/PodcastChannelView.java b/app/src/main/java/github/daneren2005/dsub/view/PodcastChannelView.java new file mode 100644 index 00000000..ada8019e --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/view/PodcastChannelView.java @@ -0,0 +1,87 @@ +/* + 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.SyncUtil; +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 File file; + + private TextView titleView; + + public PodcastChannelView(Context context) { + super(context); + this.context = context; + LayoutInflater.from(context).inflate(R.layout.basic_list_item, this, true); + + titleView = (TextView) findViewById(R.id.item_name); + starButton = (ImageButton) findViewById(R.id.item_star); + starButton.setFocusable(false); + moreButton = (ImageView) findViewById(R.id.item_more); + moreButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + v.showContextMenu(); + } + }); + } + + protected void setObjectImpl(Object obj) { + channel = (PodcastChannel) obj; + if(channel.getName() != null) { + titleView.setText(channel.getName()); + } else { + titleView.setText(channel.getUrl()); + } + file = FileUtil.getPodcastDirectory(context, channel); + } + + @Override + protected void updateBackground() { + if(SyncUtil.isSyncedPodcast(context, channel.getId())) { + if(exists) { + shaded = false; + exists = false; + } + pinned = true; + } else if(file.exists()) { + if(pinned) { + shaded = false; + pinned = false; + } + exists = true; + } else { + pinned = false; + exists = false; + } + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/view/RecyclingImageView.java b/app/src/main/java/github/daneren2005/dsub/view/RecyclingImageView.java new file mode 100644 index 00000000..0c85697f --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/view/RecyclingImageView.java @@ -0,0 +1,91 @@ +/* + 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 2015 (C) Scott Jackson +*/ + +package github.daneren2005.dsub.view; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.TransitionDrawable; +import android.os.Build; +import android.util.AttributeSet; +import android.widget.ImageView; + +public class RecyclingImageView extends ImageView { + public RecyclingImageView(Context context) { + super(context); + } + + public RecyclingImageView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public RecyclingImageView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public RecyclingImageView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + public void onDraw(Canvas canvas) { + Drawable drawable = this.getDrawable(); + if(drawable != null) { + if(drawable instanceof BitmapDrawable) { + if (isBitmapRecycled(drawable)) { + this.setImageDrawable(null); + } + } else if(drawable instanceof TransitionDrawable) { + TransitionDrawable transitionDrawable = (TransitionDrawable) drawable; + + // If last bitmap in chain is recycled, just blank this out since it would be invalid anyways + Drawable lastDrawable = transitionDrawable.getDrawable(transitionDrawable.getNumberOfLayers() - 1); + if(isBitmapRecycled(lastDrawable)) { + this.setImageDrawable(null); + } else { + // Go through earlier bitmaps and make sure that they are not recycled + for (int i = 0; i < transitionDrawable.getNumberOfLayers(); i++) { + Drawable layerDrawable = transitionDrawable.getDrawable(i); + if (isBitmapRecycled(layerDrawable)) { + // If anything in the chain is broken, just get rid of transition and go to last drawable + this.setImageDrawable(lastDrawable); + break; + } + } + } + } + } + + super.onDraw(canvas); + } + + private boolean isBitmapRecycled(Drawable drawable) { + if(!(drawable instanceof BitmapDrawable)) { + return false; + } + + BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable; + if (bitmapDrawable.getBitmap() != null && bitmapDrawable.getBitmap().isRecycled()) { + return true; + } else { + return false; + } + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/view/SeekBarPreference.java b/app/src/main/java/github/daneren2005/dsub/view/SeekBarPreference.java new file mode 100644 index 00000000..fa8e8b3a --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/view/SeekBarPreference.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2012 Christopher Eby <kreed@kreed.org> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package github.daneren2005.dsub.view; + +import android.content.Context; +import android.content.res.TypedArray; +import android.preference.DialogPreference; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.SeekBar; +import android.widget.TextView; + +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.util.Constants; + +/** + * SeekBar preference to set the shake force threshold. + */ +public class SeekBarPreference extends DialogPreference implements SeekBar.OnSeekBarChangeListener { + private static final String TAG = SeekBarPreference.class.getSimpleName(); + /** + * The current value. + */ + private String mValue; + private int mMin; + private int mMax; + private float mStepSize; + private String mDisplay; + + /** + * Our context (needed for getResources()) + */ + private Context mContext; + + /** + * TextView to display current threshold. + */ + private TextView mValueText; + + public SeekBarPreference(Context context, AttributeSet attrs) + { + super(context, attrs); + mContext = context; + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SeekBarPreference); + mMin = a.getInteger(R.styleable.SeekBarPreference_min, 0); + mMax = a.getInteger(R.styleable.SeekBarPreference_max, 100); + mStepSize = a.getFloat(R.styleable.SeekBarPreference_stepSize, 1f); + mDisplay = a.getString(R.styleable.SeekBarPreference_display); + if(mDisplay == null) { + mDisplay = "%.0f"; + } + } + + @Override + public CharSequence getSummary() + { + return getSummary(mValue); + } + + @Override + protected Object onGetDefaultValue(TypedArray a, int index) + { + return a.getString(index); + } + + @Override + protected void onSetInitialValue(boolean restoreValue, Object defaultValue) + { + mValue = restoreValue ? getPersistedString((String) defaultValue) : (String)defaultValue; + } + + /** + * Create the summary for the given value. + * + * @param value The force threshold. + * @return A string representation of the threshold. + */ + private String getSummary(String value) { + try { + int val = Integer.parseInt(value); + return String.format(mDisplay, (val + mMin) / mStepSize); + } catch (Exception e) { + return ""; + } + } + + @Override + protected View onCreateDialogView() + { + View view = super.onCreateDialogView(); + + mValueText = (TextView)view.findViewById(R.id.value); + mValueText.setText(getSummary(mValue)); + + SeekBar seekBar = (SeekBar)view.findViewById(R.id.seek_bar); + seekBar.setMax(mMax - mMin); + try { + seekBar.setProgress(Integer.parseInt(mValue)); + } catch(Exception e) { + seekBar.setProgress(0); + } + seekBar.setOnSeekBarChangeListener(this); + + return view; + } + + @Override + protected void onDialogClosed(boolean positiveResult) + { + if(positiveResult) { + persistString(mValue); + notifyChanged(); + } + } + + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) + { + if (fromUser) { + mValue = String.valueOf(progress); + mValueText.setText(getSummary(mValue)); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) + { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) + { + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/view/SettingView.java b/app/src/main/java/github/daneren2005/dsub/view/SettingView.java new file mode 100644 index 00000000..1c78706e --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/view/SettingView.java @@ -0,0 +1,102 @@ +/* + 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 2014 (C) Scott Jackson +*/ + +package github.daneren2005.dsub.view; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.CheckedTextView; + +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.domain.User; + +import static github.daneren2005.dsub.domain.User.Setting; + +public class SettingView extends UpdateView { + Setting setting; + + CheckedTextView view; + + public SettingView(Context context) { + super(context, false); + this.context = context; + LayoutInflater.from(context).inflate(android.R.layout.simple_list_item_multiple_choice, this, true); + + view = (CheckedTextView) findViewById(android.R.id.text1); + } + + protected void setObjectImpl(Object obj, Object editable) { + this.setting = (Setting) obj; + + // Can't edit non-role parts + String name = setting.getName(); + if(name.indexOf("Role") == -1) { + editable = false; + } + + int res = -1; + if(User.SCROBBLING.equals(name)) { + res = R.string.admin_scrobblingEnabled; + } else if(User.ADMIN.equals(name)) { + res = R.string.admin_role_admin; + } else if(User.SETTINGS.equals(name)) { + res = R.string.admin_role_settings; + } else if(User.DOWNLOAD.equals(name)) { + res = R.string.admin_role_download; + } else if(User.UPLOAD.equals(name)) { + res = R.string.admin_role_upload; + } else if(User.COVERART.equals(name)) { + res = R.string.admin_role_coverArt; + } else if(User.COMMENT.equals(name)) { + res = R.string.admin_role_comment; + } else if(User.PODCAST.equals(name)) { + res = R.string.admin_role_podcast; + } else if(User.STREAM.equals(name)) { + res = R.string.admin_role_stream; + } else if(User.JUKEBOX.equals(name)) { + res = R.string.admin_role_jukebox; + } else if(User.SHARE.equals(name)) { + res = R.string.admin_role_share; + } else if(User.LASTFM.equals(name)) { + res = R.string.admin_role_lastfm; + } else { + // Last resort to display the raw value + view.setText(name); + } + + if(res != -1) { + view.setText(res); + } + + if(setting.getValue()) { + view.setChecked(setting.getValue()); + } else { + view.setChecked(false); + } + + if((Boolean) editable) { + view.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + view.toggle(); + setting.setValue(view.isChecked()); + } + }); + } else { + view.setOnClickListener(null); + } + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/view/ShareView.java b/app/src/main/java/github/daneren2005/dsub/view/ShareView.java new file mode 100644 index 00000000..bfb5b198 --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/view/ShareView.java @@ -0,0 +1,65 @@ +/* + 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 java.text.SimpleDateFormat; +import java.util.Locale; + +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.domain.Share; + +public class ShareView extends UpdateView { + private static final String TAG = ShareView.class.getSimpleName(); + + private TextView titleView; + private TextView descriptionView; + + public ShareView(Context context) { + super(context, false); + LayoutInflater.from(context).inflate(R.layout.complex_list_item, this, true); + + titleView = (TextView) findViewById(R.id.item_name); + descriptionView = (TextView) findViewById(R.id.item_description); + starButton = (ImageButton) findViewById(R.id.item_star); + starButton.setFocusable(false); + moreButton = (ImageView) findViewById(R.id.item_more); + moreButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + v.showContextMenu(); + } + }); + } + + public void setObjectImpl(Object obj) { + Share share = (Share) obj; + titleView.setText(share.getName()); + if(share.getExpires() != null) { + descriptionView.setText(context.getResources().getString(R.string.share_expires, new SimpleDateFormat("E MMM d, yyyy", Locale.ENGLISH).format(share.getExpires()))); + } else { + descriptionView.setText(context.getResources().getString(R.string.share_expires_never)); + } + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/view/SongView.java b/app/src/main/java/github/daneren2005/dsub/view/SongView.java new file mode 100644 index 00000000..2fbaedc3 --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/view/SongView.java @@ -0,0 +1,318 @@ +/* + 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.content.res.TypedArray; +import android.graphics.Color; +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.DownloadFile; +import github.daneren2005.dsub.util.Util; + +import java.io.File; + +/** + * 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 MusicDirectory.Entry song; + + private CheckedTextView checkedTextView; + private TextView titleTextView; + private TextView artistTextView; + private TextView durationTextView; + private TextView statusTextView; + private ImageView statusImageView; + private ImageView bookmarkButton; + private View bottomRowView; + + private DownloadService downloadService; + private long revision = -1; + private DownloadFile downloadFile; + private boolean dontChangeDownloadFile = false; + + private boolean playing = false; + private boolean rightImage = false; + private int moreImage = 0; + private boolean isWorkDone = false; + private boolean isSaved = false; + private File partialFile; + private boolean partialFileExists = false; + private boolean loaded = false; + private boolean isBookmarked = false; + private boolean bookmarked = false; + + public SongView(Context context) { + super(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); + statusImageView = (ImageView) findViewById(R.id.song_status_icon); + ratingBar = (RatingBar) findViewById(R.id.song_rating); + starButton = (ImageButton) findViewById(R.id.song_star); + starButton.setFocusable(false); + bookmarkButton = (ImageButton) findViewById(R.id.song_bookmark); + bookmarkButton.setFocusable(false); + moreButton = (ImageView) findViewById(R.id.artist_more); + moreButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + v.showContextMenu(); + } + }); + bottomRowView = findViewById(R.id.song_bottom); + } + + public void setObjectImpl(Object obj1, Object obj2) { + this.song = (MusicDirectory.Entry) obj1; + boolean checkable = (Boolean) obj2; + + StringBuilder artist = new StringBuilder(40); + + boolean isPodcast = song instanceof PodcastEpisode; + if(!song.isVideo() || isPodcast) { + if(isPodcast) { + 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()); + } + + if(isPodcast) { + String status = ((PodcastEpisode) song).getStatus(); + int statusRes = -1; + + if("error".equals(status)) { + statusRes = R.string.song_details_error; + } else if("skipped".equals(status)) { + statusRes = R.string.song_details_skipped; + } else if("downloading".equals(status)) { + statusRes = R.string.song_details_downloading; + } + + if(statusRes != -1) { + artist.append(" ("); + artist.append(getContext().getString(statusRes)); + artist.append(")"); + } + } + + durationTextView.setText(Util.formatDuration(song.getDuration())); + bottomRowView.setVisibility(View.VISIBLE); + } else { + bottomRowView.setVisibility(View.GONE); + statusTextView.setText(Util.formatDuration(song.getDuration())); + } + + 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); + checkedTextView.setVisibility(checkable && !song.isVideo() ? View.VISIBLE : View.GONE); + + this.setBackgroundColor(0x00000000); + ratingBar.setVisibility(View.GONE); + rating = 0; + + revision = -1; + loaded = false; + dontChangeDownloadFile = false; + } + + public void setDownloadFile(DownloadFile downloadFile) { + this.downloadFile = downloadFile; + dontChangeDownloadFile = true; + } + + public DownloadFile getDownloadFile() { + return downloadFile; + } + + @Override + protected void updateBackground() { + if (downloadService == null) { + downloadService = DownloadService.getInstance(); + if(downloadService == null) { + return; + } + } + + long newRevision = downloadService.getDownloadListUpdateRevision(); + if((revision != newRevision && dontChangeDownloadFile == false) || downloadFile == null) { + downloadFile = downloadService.forSong(song); + revision = newRevision; + } + + isWorkDone = downloadFile.isWorkDone(); + isSaved = downloadFile.isSaved(); + partialFile = downloadFile.getPartialFile(); + partialFileExists = partialFile.exists(); + isStarred = song.isStarred(); + isBookmarked = song.getBookmark() != null; + isRated = song.getRating(); + + // Check if needs to load metadata: check against all fields that we know are null in offline mode + if(song.getBitRate() == null && song.getDuration() == null && song.getDiscNumber() == null && isWorkDone) { + song.loadMetadata(downloadFile.getCompleteFile()); + loaded = true; + } + } + + @Override + protected void update() { + if(loaded) { + setObjectImpl(song, checkedTextView.getVisibility() == View.VISIBLE); + } + if (downloadService == null || downloadFile == null) { + return; + } + + if(song.isStarred()) { + if(!starred) { + starButton.setVisibility(View.VISIBLE); + starred = true; + } + } else { + if(starred) { + starButton.setVisibility(View.GONE); + starred = false; + } + } + + if (isWorkDone) { + int moreImage = isSaved ? R.drawable.download_pinned : R.drawable.download_cached; + if(moreImage != this.moreImage) { + moreButton.setImageResource(moreImage); + this.moreImage = moreImage; + } + } else if(this.moreImage != R.drawable.download_none_light) { + int[] attrs = new int[] {R.attr.download_none}; + TypedArray typedArray = context.obtainStyledAttributes(attrs); + moreButton.setImageResource(typedArray.getResourceId(0, 0)); + typedArray.recycle(); + this.moreImage = R.drawable.download_none_light; + } + + if (downloadFile.isDownloading() && !downloadFile.isDownloadCancelled() && partialFileExists) { + double percentage = (partialFile.length() * 100.0) / downloadFile.getEstimatedSize(); + percentage = Math.min(percentage, 100); + statusTextView.setText((int)percentage + " %"); + if(!rightImage) { + statusImageView.setVisibility(View.VISIBLE); + rightImage = true; + } + } else if(rightImage) { + statusTextView.setText(null); + statusImageView.setVisibility(View.GONE); + rightImage = false; + } + + boolean playing = downloadService.getCurrentPlaying() == downloadFile; + if (playing) { + if(!this.playing) { + this.playing = playing; + int[] attrs = new int[] {R.attr.media_button_start}; + TypedArray typedArray = context.obtainStyledAttributes(attrs); + titleTextView.setCompoundDrawablesWithIntrinsicBounds(typedArray.getResourceId(0, 0), 0, 0, 0); + } + } else { + if(this.playing) { + this.playing = playing; + titleTextView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); + } + } + + if(isBookmarked) { + if(!bookmarked) { + bookmarkButton.setVisibility(View.VISIBLE); + bookmarked = true; + } + } else { + if(bookmarked) { + bookmarkButton.setVisibility(View.GONE); + bookmarked = false; + } + } + + if(isRated != rating) { + if(isRated > 1) { + if(rating <= 1) { + ratingBar.setVisibility(View.VISIBLE); + } + + ratingBar.setNumStars(isRated); + ratingBar.setRating(isRated); + } else if(isRated <= 1) { + if(rating > 1) { + ratingBar.setVisibility(View.GONE); + } + } + + // Still highlight red if a 1-star + if(isRated == 1) { + this.setBackgroundColor(Color.RED); + this.getBackground().setAlpha(20); + } else if(rating == 1) { + this.setBackgroundColor(0x00000000); + } + + rating = isRated; + } + } + + @Override + public void setChecked(boolean b) { + checkedTextView.setChecked(b); + } + + @Override + public boolean isChecked() { + return checkedTextView.isChecked(); + } + + @Override + public void toggle() { + checkedTextView.toggle(); + } + + public MusicDirectory.Entry getEntry() { + return song; + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/view/SquareImageView.java b/app/src/main/java/github/daneren2005/dsub/view/SquareImageView.java new file mode 100644 index 00000000..66ab7d8d --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/view/SquareImageView.java @@ -0,0 +1,32 @@ +/* + 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 2014 (C) Scott Jackson +*/ + +package github.daneren2005.dsub.view; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.ImageView; + +public class SquareImageView extends RecyclingImageView { + public SquareImageView(final Context context, final AttributeSet attrs) { + super(context, attrs); + } + + @Override + public void onMeasure(final int widthSpec, final int heightSpec) { + super.onMeasure(widthSpec, heightSpec); + setMeasuredDimension(getMeasuredWidth(), getMeasuredWidth()); + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/view/UnscrollableGridView.java b/app/src/main/java/github/daneren2005/dsub/view/UnscrollableGridView.java new file mode 100644 index 00000000..3047d5d7 --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/view/UnscrollableGridView.java @@ -0,0 +1,128 @@ +package github.daneren2005.dsub.view; + +import android.annotation.TargetApi; +import android.content.Context; +import android.os.Build; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.AbsListView; +import android.widget.GridView; +import android.widget.ListAdapter; + +import java.lang.reflect.Field; + +/** + * Created by Scott on 4/26/2014. + */ +public class UnscrollableGridView extends GridView { + private static final String TAG = UnscrollableGridView.class.getSimpleName(); + + public UnscrollableGridView(Context context) { + super(context); + } + + public UnscrollableGridView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public UnscrollableGridView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + public int getColumnWidth() { + // This method will be called from onMeasure() too. + // It's better to use getMeasuredWidth(), as it is safe in this case. + + int hSpacing = 20; + try { + Field field = GridView.class.getDeclaredField("mHorizontalSpacing"); + field.setAccessible(true); + hSpacing = field.getInt(this); + } catch(Exception e) { + + } + + final int totalHorizontalSpacing = getNumColumnsCompat() > 0 ? (getNumColumnsCompat() - 1) * hSpacing : 0; + return (getMeasuredWidth() - getPaddingLeft() - getPaddingRight() - totalHorizontalSpacing) / getNumColumnsCompat(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // Sets the padding for this view. + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + final int measuredWidth = getMeasuredWidth(); + final int childWidth = getColumnWidth(); + int childHeight = 0; + + // If there's an adapter, use it to calculate the height of this view. + final ListAdapter adapter = getAdapter(); + final int count; + + // There shouldn't be any inherent size (due to padding) if there are no child views. + if (adapter == null || (count = adapter.getCount()) == 0) { + setMeasuredDimension(0, 0); + return; + } + + // Get the first child from the adapter. + final View child = adapter.getView(0, null, this); + if (child != null) { + // Set a default LayoutParams on the child, if it doesn't have one on its own. + AbsListView.LayoutParams params = (AbsListView.LayoutParams) child.getLayoutParams(); + if (params == null) { + params = new AbsListView.LayoutParams(AbsListView.LayoutParams.WRAP_CONTENT, + AbsListView.LayoutParams.WRAP_CONTENT); + child.setLayoutParams(params); + } + + // Measure the exact width of the child, and the height based on the width. + // Note: the child takes care of calculating its height. + int childWidthSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY); + int childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + child.measure(childWidthSpec, childHeightSpec); + childHeight = child.getMeasuredHeight(); + } + + int vSpacing = 10; + try { + Field field = GridView.class.getDeclaredField("mVerticalSpacing"); + field.setAccessible(true); + vSpacing = field.getInt(this); + } catch(Exception e) { + + } + + // Number of rows required to 'mTotal' items. + final int rows = (int) Math.ceil((double) getCount() / getNumColumnsCompat()); + final int childrenHeight = childHeight * rows; + final int totalVerticalSpacing = rows > 0 ? (rows - 1) * vSpacing : 0; + + // Total height of this view. + final int measuredHeight = Math.abs(childrenHeight + getPaddingTop() + getPaddingBottom() + totalVerticalSpacing); + setMeasuredDimension(measuredWidth, measuredHeight); + } + + private int getNumColumnsCompat() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + return getNumColumnsCompat11(); + } else { + int columns = 0; + int children = getChildCount(); + if (children > 0) { + int width = getChildAt(0).getMeasuredWidth(); + if (width > 0) { + columns = getWidth() / width; + } + } + return columns > 0 ? columns : AUTO_FIT; + } + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + private int getNumColumnsCompat11() { + return getNumColumns(); + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/view/UpdateView.java b/app/src/main/java/github/daneren2005/dsub/view/UpdateView.java new file mode 100644 index 00000000..f9c62121 --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/view/UpdateView.java @@ -0,0 +1,286 @@ +/* + 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.content.res.TypedArray; +import android.graphics.Color; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.RatingBar; + +import java.util.ArrayList; +import java.util.List; +import java.util.WeakHashMap; + +import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.util.ImageLoader; +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.util.SilentBackgroundTask; + +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; + private static int activeActivities = 0; + + protected Context context; + protected RatingBar ratingBar; + protected ImageButton starButton; + protected ImageView moreButton; + + protected boolean exists = false; + protected boolean pinned = false; + protected boolean shaded = false; + protected boolean starred = false; + protected boolean isStarred = false; + protected int isRated = 0; + protected int rating = 0; + protected SilentBackgroundTask<Void> imageTask = null; + + protected final boolean autoUpdate; + + public UpdateView(Context context) { + this(context, true); + } + public UpdateView(Context context, boolean autoUpdate) { + super(context); + this.context = context; + this.autoUpdate = autoUpdate; + + setLayoutParams(new AbsListView.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + + if(autoUpdate) { + INSTANCES.put(this, null); + } + startUpdater(); + } + + @Override + public void setPressed(boolean pressed) { + + } + + public void setObject(Object obj) { + setObjectImpl(obj); + updateBackground(); + update(); + } + public void setObject(Object obj1, Object obj2) { + if(imageTask != null) { + imageTask.cancel(); + imageTask = null; + } + + setObjectImpl(obj1, obj2); + backgroundHandler.post(new Runnable() { + @Override + public void run() { + updateBackground(); + uiHandler.post(new Runnable() { + @Override + public void run() { + update(); + } + }); + } + }); + } + protected void setObjectImpl(Object obj) { + + } + protected void setObjectImpl(Object obj1, Object obj2) { + + } + + private static synchronized void startUpdater() { + if(uiHandler != null) { + return; + } + + uiHandler = new Handler(); + // Needed so handler is never null until thread creates it + backgroundHandler = uiHandler; + 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(); + } + }, "UpdateView").start(); + } + + public static synchronized void triggerUpdate() { + if(backgroundHandler != null) { + uiHandler.removeCallbacksAndMessages(null); + backgroundHandler.removeCallbacksAndMessages(null); + uiHandler.post(updateRunnable); + } + } + + private static void updateAll() { + try { + // If nothing can see this, stop updating + if(activeActivities == 0) { + activeActivities--; + return; + } + + List<UpdateView> views = new ArrayList<UpdateView>(); + for (UpdateView view : INSTANCES.keySet()) { + if (view.isShown()) { + views.add(view); + } + } + if(views.size() > 0) { + updateAllLive(views); + } else { + uiHandler.postDelayed(updateRunnable, 2000L); + } + } 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); + } + } + }); + } + + public static void addActiveActivity() { + activeActivities++; + + if(activeActivities == 0 && uiHandler != null && updateRunnable != null) { + activeActivities++; + uiHandler.post(updateRunnable); + } + } + public static void removeActiveActivity() { + activeActivities--; + } + + public static MusicDirectory.Entry findEntry(MusicDirectory.Entry entry) { + for(UpdateView view: INSTANCES.keySet()) { + MusicDirectory.Entry check = null; + if(view instanceof SongView) { + check = ((SongView) view).getEntry(); + } else if(view instanceof AlbumCell) { + check = ((AlbumCell) view).getEntry(); + } else if(view instanceof AlbumView) { + check = ((AlbumView) view).getEntry(); + } + + if(check != null && entry != check && check.getId().equals(entry.getId())) { + return check; + } + } + + return null; + } + + protected void updateBackground() { + + } + protected void update() { + if(moreButton != null) { + if(exists || pinned) { + if(!shaded) { + moreButton.setImageResource(exists ? R.drawable.download_cached : R.drawable.download_pinned); + shaded = true; + } + } else { + if(shaded) { + int[] attrs = new int[] {R.attr.download_none}; + TypedArray typedArray = context.obtainStyledAttributes(attrs); + moreButton.setImageResource(typedArray.getResourceId(0, 0)); + shaded = false; + } + } + } + + if(starButton != null) { + if(isStarred) { + if(!starred) { + starButton.setVisibility(View.VISIBLE); + starred = true; + } + } else { + if(starred) { + starButton.setVisibility(View.GONE); + starred = false; + } + } + } + + if(ratingBar != null && isRated != rating) { + if(isRated > 0 && rating == 0) { + ratingBar.setVisibility(View.VISIBLE); + } else if(isRated == 0 && rating > 0) { + ratingBar.setVisibility(View.GONE); + } + + ratingBar.setRating(isRated); + rating = isRated; + } + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/view/UserView.java b/app/src/main/java/github/daneren2005/dsub/view/UserView.java new file mode 100644 index 00000000..dec8dbef --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/view/UserView.java @@ -0,0 +1,54 @@ +/* + 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 2014 (C) Scott Jackson +*/ + +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.User; +import github.daneren2005.dsub.util.ImageLoader; + +public class UserView extends UpdateView { + private User user; + + private TextView usernameView; + private ImageView avatarView; + + public UserView(Context context) { + super(context, false); + this.context = context; + LayoutInflater.from(context).inflate(R.layout.user_list_item, this, true); + + usernameView = (TextView) findViewById(R.id.item_name); + avatarView = (ImageView) findViewById(R.id.item_avatar); + moreButton = (ImageView) findViewById(R.id.item_more); + moreButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + v.showContextMenu(); + } + }); + } + + protected void setObjectImpl(Object obj, Object obj2) { + this.user = (User) obj; + usernameView.setText(user.getUsername()); + imageTask = ((ImageLoader)obj2).loadAvatar(context, avatarView, user.getUsername()); + } +} |