aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/java/github/daneren2005/dsub/view
diff options
context:
space:
mode:
authorScott Jackson <daneren2005@gmail.com>2015-04-25 17:03:02 -0700
committerScott Jackson <daneren2005@gmail.com>2015-04-25 17:03:05 -0700
commitcfd014d38cba03ba05f571597b361ab253bff578 (patch)
tree4256723561dec7ef3ed3507382eb7020724ec570 /app/src/main/java/github/daneren2005/dsub/view
parent8a332a20ec272d59fe74520825b18017a8f0cac3 (diff)
downloaddsub-cfd014d38cba03ba05f571597b361ab253bff578.tar.gz
dsub-cfd014d38cba03ba05f571597b361ab253bff578.tar.bz2
dsub-cfd014d38cba03ba05f571597b361ab253bff578.zip
Update to gradle
Diffstat (limited to 'app/src/main/java/github/daneren2005/dsub/view')
-rw-r--r--app/src/main/java/github/daneren2005/dsub/view/AlbumCell.java108
-rw-r--r--app/src/main/java/github/daneren2005/dsub/view/AlbumView.java107
-rw-r--r--app/src/main/java/github/daneren2005/dsub/view/ArtistEntryView.java79
-rw-r--r--app/src/main/java/github/daneren2005/dsub/view/ArtistView.java78
-rw-r--r--app/src/main/java/github/daneren2005/dsub/view/AutoRepeatButton.java86
-rw-r--r--app/src/main/java/github/daneren2005/dsub/view/ChangeLog.java546
-rw-r--r--app/src/main/java/github/daneren2005/dsub/view/ErrorDialog.java75
-rw-r--r--app/src/main/java/github/daneren2005/dsub/view/FadeOutAnimation.java77
-rw-r--r--app/src/main/java/github/daneren2005/dsub/view/GenreView.java58
-rw-r--r--app/src/main/java/github/daneren2005/dsub/view/HeaderGridView.java836
-rw-r--r--app/src/main/java/github/daneren2005/dsub/view/MyLeadingMarginSpan2.java34
-rw-r--r--app/src/main/java/github/daneren2005/dsub/view/MyViewFlipper.java53
-rw-r--r--app/src/main/java/github/daneren2005/dsub/view/PlaylistSongView.java102
-rw-r--r--app/src/main/java/github/daneren2005/dsub/view/PlaylistView.java69
-rw-r--r--app/src/main/java/github/daneren2005/dsub/view/PodcastChannelView.java87
-rw-r--r--app/src/main/java/github/daneren2005/dsub/view/RecyclingImageView.java91
-rw-r--r--app/src/main/java/github/daneren2005/dsub/view/SeekBarPreference.java156
-rw-r--r--app/src/main/java/github/daneren2005/dsub/view/SettingView.java102
-rw-r--r--app/src/main/java/github/daneren2005/dsub/view/ShareView.java65
-rw-r--r--app/src/main/java/github/daneren2005/dsub/view/SongView.java318
-rw-r--r--app/src/main/java/github/daneren2005/dsub/view/SquareImageView.java32
-rw-r--r--app/src/main/java/github/daneren2005/dsub/view/UnscrollableGridView.java128
-rw-r--r--app/src/main/java/github/daneren2005/dsub/view/UpdateView.java286
-rw-r--r--app/src/main/java/github/daneren2005/dsub/view/UserView.java54
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());
+ }
+}