diff options
10 files changed, 344 insertions, 26 deletions
diff --git a/app/src/main/java/github/daneren2005/dsub/domain/MusicDirectory.java b/app/src/main/java/github/daneren2005/dsub/domain/MusicDirectory.java index 42c97b9e..6dc56bb3 100644 --- a/app/src/main/java/github/daneren2005/dsub/domain/MusicDirectory.java +++ b/app/src/main/java/github/daneren2005/dsub/domain/MusicDirectory.java @@ -107,6 +107,15 @@ public class MusicDirectory implements Serializable { } return result; } + public List<Entry> getSongs() { + List<Entry> result = new ArrayList<Entry>(); + for (Entry child : children) { + if (child != null && !child.isDirectory() && !child.isVideo()) { + result.add(child); + } + } + return result; + } public int getChildrenSize() { return children.size(); diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SubsonicFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SubsonicFragment.java index 6d25bc0b..88c8363b 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/SubsonicFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/SubsonicFragment.java @@ -77,6 +77,7 @@ import github.daneren2005.dsub.util.MenuUtil; import github.daneren2005.dsub.util.ProgressListener; import github.daneren2005.dsub.util.SilentBackgroundTask; import github.daneren2005.dsub.util.LoadingTask; +import github.daneren2005.dsub.util.SongDBHandler; import github.daneren2005.dsub.util.UpdateHelper; import github.daneren2005.dsub.util.UserUtil; import github.daneren2005.dsub.util.Util; @@ -1293,6 +1294,16 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR headers.add(R.string.details_starred); details.add(Util.formatBoolean(context, song.isStarred())); + try { + Long[] dates = SongDBHandler.getHandler(context).getLastPlayed(song); + if(dates != null && dates[0] != null && dates[0] > 0) { + headers.add(R.string.details_last_played); + details.add(Util.formatDate(dates[0])); + } + } catch(Exception e) { + Log.e(TAG, "Failed to get last played", e); + } + if(song instanceof PodcastEpisode) { headers.add(R.string.details_description); details.add(song.getAlbum()); diff --git a/app/src/main/java/github/daneren2005/dsub/service/CachedMusicService.java b/app/src/main/java/github/daneren2005/dsub/service/CachedMusicService.java index 68ca36a7..c0851009 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/CachedMusicService.java +++ b/app/src/main/java/github/daneren2005/dsub/service/CachedMusicService.java @@ -53,6 +53,7 @@ import github.daneren2005.dsub.domain.Share; import github.daneren2005.dsub.domain.User; import github.daneren2005.dsub.util.SilentBackgroundTask; import github.daneren2005.dsub.util.ProgressListener; +import github.daneren2005.dsub.util.SongDBHandler; import github.daneren2005.dsub.util.SyncUtil; import github.daneren2005.dsub.util.TimeLimitedCache; import github.daneren2005.dsub.util.FileUtil; @@ -171,6 +172,7 @@ public class CachedMusicService implements MusicService { protected Void doInBackground() throws Throwable { Util.sleepQuietly(2000L); MusicDirectory refreshed = musicService.getMusicDirectory(id, name, true, context, null); + updateAllSongs(context, refreshed); cached.updateDifferences(context, musicService.getInstance(context), refreshed); FileUtil.serialize(context, refreshed, getCacheName(context, "directory", id)); return null; @@ -193,6 +195,7 @@ public class CachedMusicService implements MusicService { if(dir == null) { dir = musicService.getMusicDirectory(id, name, refresh, context, progressListener); + updateAllSongs(context, dir); FileUtil.serialize(context, dir, getCacheName(context, "directory", id)); // If a cached copy exists to check against, look for removes @@ -231,6 +234,7 @@ public class CachedMusicService implements MusicService { if(dir == null) { dir = musicService.getAlbum(id, name, refresh, context, progressListener); + updateAllSongs(context, dir); FileUtil.serialize(context, dir, getCacheName(context, "album", id)); // If a cached copy exists to check against, look for removes @@ -254,6 +258,7 @@ public class CachedMusicService implements MusicService { } if(dir == null) { dir = musicService.getPlaylist(refresh, id, name, context, progressListener); + updateAllSongs(context, dir); FileUtil.serialize(context, dir, getCacheName(context, "playlist", id)); File playlistFile = FileUtil.getPlaylistFile(context, Util.getServerName(context, musicService.getInstance(context)), dir.getName()); @@ -1500,6 +1505,13 @@ public class CachedMusicService implements MusicService { } } + protected void updateAllSongs(Context context, MusicDirectory dir) { + List<Entry> songs = dir.getSongs(); + if(!songs.isEmpty()) { + SongDBHandler.getHandler(context).addSongs(musicService.getInstance(context), songs); + } + } + private void checkSettingsChanged(Context context) { int instance = musicService.getInstance(context); String newUrl = musicService.getRestUrl(context, null, false); diff --git a/app/src/main/java/github/daneren2005/dsub/service/DownloadFile.java b/app/src/main/java/github/daneren2005/dsub/service/DownloadFile.java index 505e4a6d..3febfaea 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/DownloadFile.java +++ b/app/src/main/java/github/daneren2005/dsub/service/DownloadFile.java @@ -85,6 +85,10 @@ public class DownloadFile implements BufferFile { return song; } + public Context getContext() { + return context; + } + /** * Returns the effective bit rate. */ @@ -206,6 +210,9 @@ public class DownloadFile implements BufferFile { return saveFile; } + public File getSaveFile() { + return saveFile; + } public File getPartialFile() { return partialFile; diff --git a/app/src/main/java/github/daneren2005/dsub/service/OfflineMusicService.java b/app/src/main/java/github/daneren2005/dsub/service/OfflineMusicService.java index c8253c91..11a723ce 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/OfflineMusicService.java +++ b/app/src/main/java/github/daneren2005/dsub/service/OfflineMusicService.java @@ -56,8 +56,10 @@ import github.daneren2005.dsub.domain.Share; import github.daneren2005.dsub.domain.User; import github.daneren2005.dsub.util.Constants; import github.daneren2005.dsub.util.FileUtil; +import github.daneren2005.dsub.util.Pair; import github.daneren2005.dsub.util.ProgressListener; import github.daneren2005.dsub.util.SilentBackgroundTask; +import github.daneren2005.dsub.util.SongDBHandler; import github.daneren2005.dsub.util.Util; import java.io.*; import java.util.Comparator; @@ -556,9 +558,15 @@ public class OfflineMusicService implements MusicService { SharedPreferences.Editor offlineEditor = offline.edit(); if(id.indexOf(cacheLocn) != -1) { - String scrobbleSearchCriteria = Util.parseOfflineIDSearch(context, id, cacheLocn); - offlineEditor.putString(Constants.OFFLINE_SCROBBLE_SEARCH + scrobbles, scrobbleSearchCriteria); - offlineEditor.remove(Constants.OFFLINE_SCROBBLE_ID + scrobbles); + Pair<Integer, String> cachedSongId = SongDBHandler.getHandler(context).getIdFromPath(id); + if(cachedSongId != null) { + offlineEditor.putString(Constants.OFFLINE_SCROBBLE_ID + scrobbles, cachedSongId.getSecond()); + offlineEditor.remove(Constants.OFFLINE_SCROBBLE_SEARCH + scrobbles); + } else { + String scrobbleSearchCriteria = Util.parseOfflineIDSearch(context, id, cacheLocn); + offlineEditor.putString(Constants.OFFLINE_SCROBBLE_SEARCH + scrobbles, scrobbleSearchCriteria); + offlineEditor.remove(Constants.OFFLINE_SCROBBLE_ID + scrobbles); + } } else { offlineEditor.putString(Constants.OFFLINE_SCROBBLE_ID + scrobbles, id); offlineEditor.remove(Constants.OFFLINE_SCROBBLE_SEARCH + scrobbles); diff --git a/app/src/main/java/github/daneren2005/dsub/service/RESTMusicService.java b/app/src/main/java/github/daneren2005/dsub/service/RESTMusicService.java index 9ec77897..9af512e3 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/RESTMusicService.java +++ b/app/src/main/java/github/daneren2005/dsub/service/RESTMusicService.java @@ -69,6 +69,7 @@ import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.Looper; import android.util.Log; + import github.daneren2005.dsub.R; import github.daneren2005.dsub.domain.*; import github.daneren2005.dsub.service.parser.AlbumListParser; @@ -99,10 +100,12 @@ import github.daneren2005.dsub.service.parser.VideosParser; import github.daneren2005.dsub.service.ssl.SSLSocketFactory; import github.daneren2005.dsub.service.ssl.TrustSelfSignedStrategy; import github.daneren2005.dsub.util.BackgroundTask; +import github.daneren2005.dsub.util.Pair; import github.daneren2005.dsub.util.SilentBackgroundTask; import github.daneren2005.dsub.util.Constants; import github.daneren2005.dsub.util.FileUtil; import github.daneren2005.dsub.util.ProgressListener; +import github.daneren2005.dsub.util.SongDBHandler; import github.daneren2005.dsub.util.Util; import java.io.*; import java.util.zip.GZIPInputStream; @@ -1796,11 +1799,16 @@ public class RESTMusicService implements MusicService { SharedPreferences prefs = Util.getPreferences(context); String cacheLocn = prefs.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, null); if(cacheLocn != null && id.indexOf(cacheLocn) != -1) { - String searchCriteria = Util.parseOfflineIDSearch(context, id, cacheLocn); - SearchCritera critera = new SearchCritera(searchCriteria, 0, 0, 1); - SearchResult result = searchNew(critera, context, progressListener); - if(result.getSongs().size() == 1){ - id = result.getSongs().get(0).getId(); + Pair<Integer, String> cachedSongId = SongDBHandler.getHandler(context).getIdFromPath(Util.getRestUrlHash(context, getInstance(context)), id); + if(cachedSongId != null) { + id = cachedSongId.getSecond(); + } else { + String searchCriteria = Util.parseOfflineIDSearch(context, id, cacheLocn); + SearchCritera critera = new SearchCritera(searchCriteria, 0, 0, 1); + SearchResult result = searchNew(critera, context, progressListener); + if (result.getSongs().size() == 1) { + id = result.getSongs().get(0).getId(); + } } } diff --git a/app/src/main/java/github/daneren2005/dsub/service/Scrobbler.java b/app/src/main/java/github/daneren2005/dsub/service/Scrobbler.java index 1f8538c9..1d9fecef 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/Scrobbler.java +++ b/app/src/main/java/github/daneren2005/dsub/service/Scrobbler.java @@ -5,6 +5,7 @@ import android.util.Log; import github.daneren2005.dsub.domain.PodcastEpisode; import github.daneren2005.dsub.util.SilentBackgroundTask; +import github.daneren2005.dsub.util.SongDBHandler; import github.daneren2005.dsub.util.Util; /** @@ -32,28 +33,11 @@ public class Scrobbler { } public void scrobble(final Context context, final DownloadFile song, final boolean submission) { - if (song == null || !Util.isScrobblingEnabled(context)) { - return; - } - - // Ignore if online with no network access - if(!Util.isOffline(context) && !Util.isNetworkConnected(context)) { - return; - } - - // Ignore podcasts - if(song.getSong() instanceof PodcastEpisode) { - return; - } - - // Ignore songs which are under 30 seconds per Last.FM guidelines - Integer duration = song.getSong().getDuration(); - if(duration != null && duration > 0 && duration < 30) { + if(song == null) { return; } final String id = song.getSong().getId(); - // Avoid duplicate registrations. if (submission && id.equals(lastSubmission)) { return; @@ -71,6 +55,27 @@ public class Scrobbler { new SilentBackgroundTask<Void>(context) { @Override protected Void doInBackground() { + SongDBHandler.getHandler(context).setSongPlayed(song, submission); + + // Scrobbling disabled + if (!Util.isScrobblingEnabled(context)) { + return null; + } + // Ignore if online with no network access + else if(!Util.isOffline(context) && !Util.isNetworkConnected(context)) { + return null; + } + // Ignore podcasts + else if(song.getSong() instanceof PodcastEpisode) { + return null; + } + + // Ignore songs which are under 30 seconds per Last.FM guidelines + Integer duration = song.getSong().getDuration(); + if(duration != null && duration > 0 && duration < 30) { + return null; + } + MusicService service = MusicServiceFactory.getMusicService(context); try { service.scrobble(id, submission, context, null); diff --git a/app/src/main/java/github/daneren2005/dsub/util/SongDBHandler.java b/app/src/main/java/github/daneren2005/dsub/util/SongDBHandler.java new file mode 100644 index 00000000..08f6f461 --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/util/SongDBHandler.java @@ -0,0 +1,238 @@ +/* + 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.util; + +import android.content.ContentValues; +import android.content.Context; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +import java.util.ArrayList; +import java.util.List; + +import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.service.DownloadFile; + +public class SongDBHandler extends SQLiteOpenHelper { + private static final String TAG = SongDBHandler.class.getSimpleName(); + private static SongDBHandler dbHandler; + + private static final int DATABASE_VERSION = 2; + public static final String DATABASE_NAME = "SongsDB"; + + public static final String TABLE_SONGS = "RegisteredSongs"; + public static final String SONGS_ID = "id"; + public static final String SONGS_SERVER_KEY = "serverKey"; + public static final String SONGS_SERVER_ID = "serverId"; + public static final String SONGS_COMPLETE_PATH = "completePath"; + public static final String SONGS_LAST_PLAYED = "lastPlayed"; + public static final String SONGS_LAST_COMPLETED = "lastCompleted"; + + private Context context; + + private SongDBHandler(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + this.context = context; + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL("CREATE TABLE " + TABLE_SONGS + " ( " + + SONGS_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + + SONGS_SERVER_KEY + " INTEGER NOT NULL, " + + SONGS_SERVER_ID + " TEXT NOT NULL, " + + SONGS_COMPLETE_PATH + " TEXT NOT NULL, " + + SONGS_LAST_PLAYED + " INTEGER, " + + SONGS_LAST_COMPLETED + " INTEGER, " + + "UNIQUE(" + SONGS_SERVER_KEY + ", " + SONGS_SERVER_ID + "))"); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + db.execSQL("DROP TABLE IF EXISTS " + TABLE_SONGS); + this.onCreate(db); + } + + public synchronized void addSong(DownloadFile downloadFile) { + addSong(Util.getMostRecentActiveServer(context), downloadFile); + } + public synchronized void addSong(int instance, DownloadFile downloadFile) { + SQLiteDatabase db = this.getWritableDatabase(); + addSong(db, instance, downloadFile); + db.close(); + } + protected synchronized void addSong(SQLiteDatabase db, DownloadFile downloadFile) { + addSong(db, Util.getMostRecentActiveServer(context), downloadFile); + } + protected synchronized void addSong(SQLiteDatabase db, int instance, DownloadFile downloadFile) { + addSong(db, instance, downloadFile.getSong().getId(), downloadFile.getSaveFile().getAbsolutePath()); + } + + protected synchronized void addSong(SQLiteDatabase db, String id, String absolutePath) { + addSong(db, Util.getMostRecentActiveServer(context), id, absolutePath); + } + protected synchronized void addSong(SQLiteDatabase db, int instance, String id, String absolutePath) { + addSongImpl(db, Util.getRestUrlHash(context, instance), id, absolutePath); + } + protected synchronized void addSongImpl(SQLiteDatabase db, int serverKey, String id, String absolutePath) { + ContentValues values = new ContentValues(); + values.put(SONGS_SERVER_KEY, serverKey); + values.put(SONGS_SERVER_ID, id); + values.put(SONGS_COMPLETE_PATH, absolutePath); + + db.insertWithOnConflict(TABLE_SONGS, null, values, SQLiteDatabase.CONFLICT_IGNORE); + } + + public synchronized void addSongs(int instance, List<MusicDirectory.Entry> entries) { + SQLiteDatabase db = this.getWritableDatabase(); + + List<Pair<String, String>> pairs = new ArrayList<>(); + for(MusicDirectory.Entry entry: entries) { + pairs.add(new Pair<>(entry.getId(), FileUtil.getSongFile(context, entry).getAbsolutePath())); + } + addSongs(db, instance, pairs); + + db.close(); + } + public synchronized void addSongs(SQLiteDatabase db, int instance, List<Pair<String, String>> entries) { + addSongsImpl(db, Util.getRestUrlHash(context, instance), entries); + } + protected synchronized void addSongsImpl(SQLiteDatabase db, int serverKey, List<Pair<String, String>> entries) { + db.beginTransaction(); + try { + for (Pair<String, String> entry : entries) { + ContentValues values = new ContentValues(); + values.put(SONGS_SERVER_KEY, serverKey); + values.put(SONGS_SERVER_ID, entry.getFirst()); + values.put(SONGS_COMPLETE_PATH, entry.getSecond()); + + db.insertWithOnConflict(TABLE_SONGS, null, values, SQLiteDatabase.CONFLICT_IGNORE); + } + + db.setTransactionSuccessful(); + } catch(Exception e) {} + + db.endTransaction(); + } + + public synchronized void setSongPlayed(DownloadFile downloadFile, boolean submission) { + // TODO: In case of offline want to update all matches + Pair<Integer, String> pair = getOnlineSongId(downloadFile); + if(pair == null) { + return; + } + int serverKey = pair.getFirst(); + String id = pair.getSecond(); + + // Open and make sure song is in db + SQLiteDatabase db = this.getWritableDatabase(); + addSongImpl(db, serverKey, id, downloadFile.getSaveFile().getAbsolutePath()); + + // Update song's last played + ContentValues values = new ContentValues(); + values.put(submission ? SONGS_LAST_COMPLETED : SONGS_LAST_PLAYED, System.currentTimeMillis()); + db.update(TABLE_SONGS, values, SONGS_SERVER_KEY + " = ? AND " + SONGS_SERVER_ID + " = ?", new String[]{Integer.toString(serverKey), id}); + db.close(); + } + + public synchronized Long[] getLastPlayed(MusicDirectory.Entry entry) { + return getLastPlayed(getOnlineSongId(entry)); + } + protected synchronized Long[] getLastPlayed(Pair<Integer, String> pair) { + return getLastPlayed(pair.getFirst(), pair.getSecond()); + } + public synchronized Long[] getLastPlayed(int serverKey, String id) { + SQLiteDatabase db = this.getReadableDatabase(); + + String[] columns = {SONGS_LAST_PLAYED, SONGS_LAST_COMPLETED}; + Cursor cursor = db.query(TABLE_SONGS, columns, SONGS_SERVER_KEY + " = ? AND " + SONGS_SERVER_ID + " = ?", new String[]{Integer.toString(serverKey), id}, null, null, null, null); + + try { + cursor.moveToFirst(); + + Long[] dates = new Long[2]; + dates[0] = cursor.getLong(0); + dates[1] = cursor.getLong(1); + return dates; + } catch(Exception e) {} + + return null; + } + + public synchronized Pair<Integer, String> getOnlineSongId(MusicDirectory.Entry entry) { + return getOnlineSongId(new DownloadFile(context, entry, true)); + } + public synchronized Pair<Integer, String> getOnlineSongId(DownloadFile downloadFile) { + return getOnlineSongId(Util.getRestUrlHash(context), downloadFile.getSong().getId(), downloadFile.getSaveFile().getAbsolutePath(), Util.isOffline(context) ? false : true); + } + + public synchronized Pair<Integer, String> getOnlineSongId(int serverKey, MusicDirectory.Entry entry) { + return getOnlineSongId(serverKey, new DownloadFile(context, entry, true)); + } + public synchronized Pair<Integer, String> getOnlineSongId(int serverKey, DownloadFile downloadFile) { + return getOnlineSongId(serverKey, downloadFile.getSong().getId(), downloadFile.getSaveFile().getAbsolutePath(), true); + } + public synchronized Pair<Integer, String> getOnlineSongId(int serverKey, String id, String savePath, boolean requireServerKey) { + SharedPreferences prefs = Util.getPreferences(context); + String cacheLocn = prefs.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, null); + if(cacheLocn != null && id.indexOf(cacheLocn) != -1) { + if(requireServerKey) { + return getIdFromPath(serverKey, savePath); + } else { + return getIdFromPath(savePath); + } + } else { + return new Pair<>(serverKey, id); + } + } + + public synchronized Pair<Integer, String> getIdFromPath(String path) { + SQLiteDatabase db = this.getReadableDatabase(); + + String[] columns = {SONGS_SERVER_KEY, SONGS_SERVER_ID}; + Cursor cursor = db.query(TABLE_SONGS, columns, SONGS_COMPLETE_PATH + " = ?", new String[] { path }, null, null, SONGS_LAST_PLAYED + " DESC", null); + + try { + cursor.moveToFirst(); + return new Pair(cursor.getInt(0), cursor.getString(1)); + } catch(Exception e) {} + + return null; + } + public synchronized Pair<Integer, String> getIdFromPath(int serverKey, String path) { + SQLiteDatabase db = this.getReadableDatabase(); + + String[] columns = {SONGS_SERVER_KEY, SONGS_SERVER_ID}; + Cursor cursor = db.query(TABLE_SONGS, columns, SONGS_SERVER_KEY + " = ? AND " + SONGS_COMPLETE_PATH + " = ?", new String[] {Integer.toString(serverKey), path }, null, null, null, null); + + try { + cursor.moveToFirst(); + return new Pair(cursor.getInt(0), cursor.getString(1)); + } catch(Exception e) {} + + return null; + } + + public static SongDBHandler getHandler(Context context) { + if(dbHandler == null) { + dbHandler = new SongDBHandler(context); + } + + return dbHandler; + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/util/Util.java b/app/src/main/java/github/daneren2005/dsub/util/Util.java index 490e922b..bf4af20d 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/Util.java +++ b/app/src/main/java/github/daneren2005/dsub/util/Util.java @@ -176,6 +176,10 @@ public final class Util { // Don't allow the SERVER_INSTANCE to ever be 0 return prefs.getBoolean(Constants.PREFERENCES_KEY_OFFLINE, false) ? 0 : Math.max(1, prefs.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1)); } + public static int getMostRecentActiveServer(Context context) { + SharedPreferences prefs = getPreferences(context); + return Math.max(1, prefs.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1)); + } public static int getServerCount(Context context) { SharedPreferences prefs = getPreferences(context); @@ -450,6 +454,18 @@ public final class Util { return builder.toString(); } + public static int getRestUrlHash(Context context) { + return getRestUrlHash(context, Util.getMostRecentActiveServer(context)); + } + public static int getRestUrlHash(Context context, int instance) { + StringBuilder builder = new StringBuilder(); + + SharedPreferences prefs = Util.getPreferences(context); + builder.append(prefs.getString(Constants.PREFERENCES_KEY_SERVER_URL + instance, null)); + builder.append(prefs.getString(Constants.PREFERENCES_KEY_USERNAME + instance, null)); + + return builder.toString().hashCode(); + } public static String replaceInternalUrl(Context context, String url) { // Only change to internal when using https @@ -908,6 +924,9 @@ public final class Util { } } } + public static String formatDate(long millis) { + return formatDate(new Date(millis)); + } public static String formatBoolean(Context context, boolean value) { return context.getResources().getString(value ? R.string.common_true : R.string.common_false); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 303a8e0e..3acbff3e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -642,6 +642,7 @@ <string name="details.position">Position</string> <string name="details.updated">Updated</string> <string name="details.starred">Starred</string> + <string name="details.last_played">Last Played</string> <plurals name="select_album_n_songs"> <item quantity="zero">No songs</item> |