aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/java
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
parent8a332a20ec272d59fe74520825b18017a8f0cac3 (diff)
downloaddsub-cfd014d38cba03ba05f571597b361ab253bff578.tar.gz
dsub-cfd014d38cba03ba05f571597b361ab253bff578.tar.bz2
dsub-cfd014d38cba03ba05f571597b361ab253bff578.zip
Update to gradle
Diffstat (limited to 'app/src/main/java')
-rw-r--r--app/src/main/java/github/daneren2005/dsub/activity/DownloadActivity.java62
-rw-r--r--app/src/main/java/github/daneren2005/dsub/activity/EditPlayActionActivity.java246
-rw-r--r--app/src/main/java/github/daneren2005/dsub/activity/QueryReceiverActivity.java85
-rw-r--r--app/src/main/java/github/daneren2005/dsub/activity/SettingsActivity.java91
-rw-r--r--app/src/main/java/github/daneren2005/dsub/activity/SubsonicActivity.java860
-rw-r--r--app/src/main/java/github/daneren2005/dsub/activity/SubsonicFragmentActivity.java686
-rw-r--r--app/src/main/java/github/daneren2005/dsub/activity/VoiceQueryReceiverActivity.java61
-rw-r--r--app/src/main/java/github/daneren2005/dsub/adapter/AlbumGridAdapter.java73
-rw-r--r--app/src/main/java/github/daneren2005/dsub/adapter/AlbumListAdapter.java154
-rw-r--r--app/src/main/java/github/daneren2005/dsub/adapter/ArtistAdapter.java97
-rw-r--r--app/src/main/java/github/daneren2005/dsub/adapter/BookmarkAdapter.java64
-rw-r--r--app/src/main/java/github/daneren2005/dsub/adapter/ChatAdapter.java109
-rw-r--r--app/src/main/java/github/daneren2005/dsub/adapter/DownloadFileAdapter.java49
-rw-r--r--app/src/main/java/github/daneren2005/dsub/adapter/DrawerAdapter.java126
-rw-r--r--app/src/main/java/github/daneren2005/dsub/adapter/EntryAdapter.java82
-rw-r--r--app/src/main/java/github/daneren2005/dsub/adapter/GenreAdapter.java60
-rw-r--r--app/src/main/java/github/daneren2005/dsub/adapter/MergeAdapter.java290
-rw-r--r--app/src/main/java/github/daneren2005/dsub/adapter/PlaylistAdapter.java70
-rw-r--r--app/src/main/java/github/daneren2005/dsub/adapter/PodcastChannelAdapter.java60
-rw-r--r--app/src/main/java/github/daneren2005/dsub/adapter/SackOfViewsAdapter.java181
-rw-r--r--app/src/main/java/github/daneren2005/dsub/adapter/SettingsAdapter.java59
-rw-r--r--app/src/main/java/github/daneren2005/dsub/adapter/ShareAdapter.java56
-rw-r--r--app/src/main/java/github/daneren2005/dsub/adapter/UserAdapter.java52
-rw-r--r--app/src/main/java/github/daneren2005/dsub/audiofx/AudioEffectsController.java69
-rw-r--r--app/src/main/java/github/daneren2005/dsub/audiofx/EqualizerController.java198
-rw-r--r--app/src/main/java/github/daneren2005/dsub/audiofx/LoudnessEnhancerController.java77
-rw-r--r--app/src/main/java/github/daneren2005/dsub/domain/Artist.java145
-rw-r--r--app/src/main/java/github/daneren2005/dsub/domain/ArtistInfo.java76
-rw-r--r--app/src/main/java/github/daneren2005/dsub/domain/Bookmark.java105
-rw-r--r--app/src/main/java/github/daneren2005/dsub/domain/ChatMessage.java51
-rw-r--r--app/src/main/java/github/daneren2005/dsub/domain/DLNADevice.java78
-rw-r--r--app/src/main/java/github/daneren2005/dsub/domain/Genre.java69
-rw-r--r--app/src/main/java/github/daneren2005/dsub/domain/Indexes.java94
-rw-r--r--app/src/main/java/github/daneren2005/dsub/domain/Lyrics.java57
-rw-r--r--app/src/main/java/github/daneren2005/dsub/domain/MusicDirectory.java559
-rw-r--r--app/src/main/java/github/daneren2005/dsub/domain/MusicFolder.java49
-rw-r--r--app/src/main/java/github/daneren2005/dsub/domain/PlayerQueue.java30
-rw-r--r--app/src/main/java/github/daneren2005/dsub/domain/PlayerState.java47
-rw-r--r--app/src/main/java/github/daneren2005/dsub/domain/Playlist.java128
-rw-r--r--app/src/main/java/github/daneren2005/dsub/domain/PodcastChannel.java145
-rw-r--r--app/src/main/java/github/daneren2005/dsub/domain/PodcastEpisode.java54
-rw-r--r--app/src/main/java/github/daneren2005/dsub/domain/RemoteControlState.java38
-rw-r--r--app/src/main/java/github/daneren2005/dsub/domain/RemoteStatus.java63
-rw-r--r--app/src/main/java/github/daneren2005/dsub/domain/RepeatMode.java28
-rw-r--r--app/src/main/java/github/daneren2005/dsub/domain/SearchCritera.java55
-rw-r--r--app/src/main/java/github/daneren2005/dsub/domain/SearchResult.java52
-rw-r--r--app/src/main/java/github/daneren2005/dsub/domain/ServerInfo.java213
-rw-r--r--app/src/main/java/github/daneren2005/dsub/domain/Share.java165
-rw-r--r--app/src/main/java/github/daneren2005/dsub/domain/User.java117
-rw-r--r--app/src/main/java/github/daneren2005/dsub/domain/Version.java181
-rw-r--r--app/src/main/java/github/daneren2005/dsub/fragments/AdminFragment.java147
-rw-r--r--app/src/main/java/github/daneren2005/dsub/fragments/ChatFragment.java250
-rw-r--r--app/src/main/java/github/daneren2005/dsub/fragments/DownloadFragment.java189
-rw-r--r--app/src/main/java/github/daneren2005/dsub/fragments/EqualizerFragment.java441
-rw-r--r--app/src/main/java/github/daneren2005/dsub/fragments/LyricsFragment.java107
-rw-r--r--app/src/main/java/github/daneren2005/dsub/fragments/MainFragment.java586
-rw-r--r--app/src/main/java/github/daneren2005/dsub/fragments/NowPlayingFragment.java1568
-rw-r--r--app/src/main/java/github/daneren2005/dsub/fragments/PreferenceCompatFragment.java313
-rw-r--r--app/src/main/java/github/daneren2005/dsub/fragments/SearchFragment.java368
-rw-r--r--app/src/main/java/github/daneren2005/dsub/fragments/SelectArtistFragment.java333
-rw-r--r--app/src/main/java/github/daneren2005/dsub/fragments/SelectBookmarkFragment.java131
-rw-r--r--app/src/main/java/github/daneren2005/dsub/fragments/SelectDirectoryFragment.java1597
-rw-r--r--app/src/main/java/github/daneren2005/dsub/fragments/SelectGenreFragment.java71
-rw-r--r--app/src/main/java/github/daneren2005/dsub/fragments/SelectListFragment.java163
-rw-r--r--app/src/main/java/github/daneren2005/dsub/fragments/SelectPlaylistFragment.java303
-rw-r--r--app/src/main/java/github/daneren2005/dsub/fragments/SelectPodcastsFragment.java308
-rw-r--r--app/src/main/java/github/daneren2005/dsub/fragments/SelectShareFragment.java216
-rw-r--r--app/src/main/java/github/daneren2005/dsub/fragments/SelectVideoFragment.java82
-rw-r--r--app/src/main/java/github/daneren2005/dsub/fragments/SelectYearFragment.java78
-rw-r--r--app/src/main/java/github/daneren2005/dsub/fragments/SettingsFragment.java724
-rw-r--r--app/src/main/java/github/daneren2005/dsub/fragments/SimilarArtistFragment.java169
-rw-r--r--app/src/main/java/github/daneren2005/dsub/fragments/SubsonicFragment.java1817
-rw-r--r--app/src/main/java/github/daneren2005/dsub/fragments/UserFragment.java125
-rw-r--r--app/src/main/java/github/daneren2005/dsub/provider/DLNARouteProvider.java425
-rw-r--r--app/src/main/java/github/daneren2005/dsub/provider/DSubSearchProvider.java191
-rw-r--r--app/src/main/java/github/daneren2005/dsub/provider/DSubWidget4x1.java28
-rw-r--r--app/src/main/java/github/daneren2005/dsub/provider/DSubWidget4x2.java28
-rw-r--r--app/src/main/java/github/daneren2005/dsub/provider/DSubWidget4x3.java28
-rw-r--r--app/src/main/java/github/daneren2005/dsub/provider/DSubWidget4x4.java28
-rw-r--r--app/src/main/java/github/daneren2005/dsub/provider/DSubWidgetProvider.java304
-rw-r--r--app/src/main/java/github/daneren2005/dsub/provider/JukeboxRouteProvider.java131
-rw-r--r--app/src/main/java/github/daneren2005/dsub/provider/MostRecentStubProvider.java61
-rw-r--r--app/src/main/java/github/daneren2005/dsub/provider/PlaylistStubProvider.java61
-rw-r--r--app/src/main/java/github/daneren2005/dsub/provider/PodcastStubProvider.java61
-rw-r--r--app/src/main/java/github/daneren2005/dsub/provider/StarredStubProvider.java61
-rw-r--r--app/src/main/java/github/daneren2005/dsub/receiver/A2dpIntentReceiver.java47
-rw-r--r--app/src/main/java/github/daneren2005/dsub/receiver/AudioNoisyReceiver.java51
-rw-r--r--app/src/main/java/github/daneren2005/dsub/receiver/BootReceiver.java34
-rw-r--r--app/src/main/java/github/daneren2005/dsub/receiver/HeadphonePlugReceiver.java42
-rw-r--r--app/src/main/java/github/daneren2005/dsub/receiver/MediaButtonIntentReceiver.java57
-rw-r--r--app/src/main/java/github/daneren2005/dsub/receiver/PlayActionReceiver.java46
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/CachedMusicService.java1424
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/ChromeCastController.java522
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/DLNAController.java687
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/DownloadFile.java607
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/DownloadService.java2410
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/DownloadServiceLifecycleSupport.java445
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/HeadphoneListenerService.java66
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/JukeboxController.java307
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/MediaStoreService.java187
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/MusicService.java197
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/MusicServiceFactory.java36
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/OfflineException.java32
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/OfflineMusicService.java836
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/RESTMusicService.java1991
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/RemoteController.java116
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/Scrobbler.java85
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/ServerTooOldException.java60
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/parser/AbstractParser.java150
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/parser/AlbumListParser.java61
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/parser/ArtistInfoParser.java82
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/parser/BookmarkParser.java100
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/parser/ChatMessageParser.java65
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/parser/ErrorParser.java49
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/parser/GenreParser.java122
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/parser/IndexesParser.java134
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/parser/JukeboxStatusParser.java62
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/parser/LicenseParser.java62
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/parser/LyricsParser.java64
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/parser/MusicDirectoryEntryParser.java94
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/parser/MusicDirectoryParser.java108
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/parser/MusicFoldersParser.java65
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/parser/PlayQueueParser.java85
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/parser/PlaylistParser.java63
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/parser/PlaylistsParser.java70
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/parser/PodcastChannelParser.java66
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/parser/PodcastEntryParser.java112
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/parser/RandomSongsParser.java60
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/parser/ScanStatusParser.java56
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/parser/SearchResult2Parser.java75
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/parser/SearchResultParser.java65
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/parser/ShareParser.java126
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/parser/StarredListParser.java69
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/parser/SubsonicRESTException.java19
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/parser/UserParser.java73
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/parser/VideosParser.java53
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/ssl/SSLSocketFactory.java549
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/ssl/TrustManagerDecorator.java65
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/ssl/TrustSelfSignedStrategy.java44
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/ssl/TrustStrategy.java57
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/sync/AuthenticatorService.java90
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/sync/MostRecentSyncAdapter.java105
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/sync/MostRecentSyncService.java48
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/sync/PlaylistSyncAdapter.java153
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/sync/PlaylistSyncService.java48
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/sync/PodcastSyncAdapter.java113
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/sync/PodcastSyncService.java48
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/sync/StarredSyncAdapter.java80
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/sync/StarredSyncService.java48
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/sync/SubsonicSyncAdapter.java174
-rw-r--r--app/src/main/java/github/daneren2005/dsub/updates/Updater.java98
-rw-r--r--app/src/main/java/github/daneren2005/dsub/updates/Updater403.java58
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/ArtistRadioBuffer.java148
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/BackgroundTask.java307
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/CacheCleaner.java292
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/Constants.java206
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/FileUtil.java860
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/ImageLoader.java600
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/LoadingTask.java73
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/MediaRouteManager.java181
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/Notifications.java348
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/Pair.java54
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/ProgressListener.java27
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/SettingsBackupAgent.java31
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/ShufflePlayBuffer.java212
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/SilentBackgroundTask.java48
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/SimpleServiceBinder.java37
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/SyncUtil.java222
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/TabBackgroundTask.java51
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/TimeLimitedCache.java55
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/UserUtil.java452
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/Util.java1339
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/compat/CastCompat.java57
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientBase.java43
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientHelper.java32
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientICS.java104
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientJB.java58
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/tags/Bastp.java85
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/tags/BastpUtil.java73
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/tags/Common.java111
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/tags/FlacFile.java85
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/tags/ID3v2File.java176
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/tags/LameHeader.java70
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/tags/OggFile.java114
-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
208 files changed, 42560 insertions, 0 deletions
diff --git a/app/src/main/java/github/daneren2005/dsub/activity/DownloadActivity.java b/app/src/main/java/github/daneren2005/dsub/activity/DownloadActivity.java
new file mode 100644
index 00000000..e13a8b8c
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/activity/DownloadActivity.java
@@ -0,0 +1,62 @@
+/*
+ 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.activity;
+
+import github.daneren2005.dsub.R;
+import android.os.Bundle;
+import android.view.MotionEvent;
+import github.daneren2005.dsub.fragments.NowPlayingFragment;
+
+import android.widget.EditText;
+
+import github.daneren2005.dsub.util.Constants;
+
+public class DownloadActivity extends SubsonicActivity {
+ private static final String TAG = DownloadActivity.class.getSimpleName();
+ private EditText playlistNameView;
+
+ /**
+ * Called when the activity is first created.
+ */
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.download_activity);
+
+ if (findViewById(R.id.fragment_container) != null && savedInstanceState == null) {
+ currentFragment = new NowPlayingFragment();
+ if(getIntent().hasExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD_VIEW)) {
+ Bundle args = new Bundle();
+ args.putBoolean(Constants.INTENT_EXTRA_NAME_DOWNLOAD_VIEW, true);
+ currentFragment.setArguments(args);
+ }
+ currentFragment.setPrimaryFragment(true);
+ getSupportFragmentManager().beginTransaction().add(R.id.fragment_container, currentFragment, currentFragment.getSupportTag() + "").commit();
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent me) {
+ if(currentFragment != null && currentFragment.getGestureDetector() != null) {
+ return currentFragment.getGestureDetector().onTouchEvent(me);
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/activity/EditPlayActionActivity.java b/app/src/main/java/github/daneren2005/dsub/activity/EditPlayActionActivity.java
new file mode 100644
index 00000000..e1f2cad3
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/activity/EditPlayActionActivity.java
@@ -0,0 +1,246 @@
+/*
+ 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.activity;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v4.widget.DrawerLayout;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.EditText;
+import android.widget.Spinner;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.Genre;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import github.daneren2005.dsub.service.OfflineException;
+import github.daneren2005.dsub.service.ServerTooOldException;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.LoadingTask;
+import github.daneren2005.dsub.util.Util;
+
+public class EditPlayActionActivity extends SubsonicActivity {
+ private CheckBox shuffleCheckbox;
+ private CheckBox startYearCheckbox;
+ private EditText startYearBox;
+ private CheckBox endYearCheckbox;
+ private EditText endYearBox;
+ private Button genreButton;
+ private Spinner offlineSpinner;
+
+ private String doNothing;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setTitle(R.string.tasker_start_playing_title);
+ setContentView(R.layout.edit_play_action);
+ final Activity context = this;
+ doNothing = context.getResources().getString(R.string.tasker_edit_do_nothing);
+
+ shuffleCheckbox = (CheckBox) findViewById(R.id.edit_shuffle_checkbox);
+ shuffleCheckbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton view, boolean isChecked) {
+ startYearCheckbox.setEnabled(isChecked);
+ endYearCheckbox.setEnabled(isChecked);
+ genreButton.setEnabled(isChecked);
+ }
+ });
+
+ startYearCheckbox = (CheckBox) findViewById(R.id.edit_start_year_checkbox);
+ startYearBox = (EditText) findViewById(R.id.edit_start_year);
+ // Disable/enable number box if checked
+ startYearCheckbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton view, boolean isChecked) {
+ startYearBox.setEnabled(isChecked);
+ }
+ });
+
+ endYearCheckbox = (CheckBox) findViewById(R.id.edit_end_year_checkbox);
+ endYearBox = (EditText) findViewById(R.id.edit_end_year);
+ endYearCheckbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton view, boolean isChecked) {
+ endYearBox.setEnabled(isChecked);
+ }
+ });
+
+ genreButton = (Button) findViewById(R.id.edit_genre_spinner);
+ genreButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ new LoadingTask<List<Genre>>(context, true) {
+ @Override
+ protected List<Genre> doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ return musicService.getGenres(false, context, this);
+ }
+
+ @Override
+ protected void done(final List<Genre> genres) {
+ List<String> names = new ArrayList<String>();
+ String blank = context.getResources().getString(R.string.select_genre_blank);
+ names.add(doNothing);
+ names.add(blank);
+ for(Genre genre: genres) {
+ names.add(genre.getName());
+ }
+ final List<String> finalNames = names;
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.shuffle_pick_genre)
+ .setItems(names.toArray(new CharSequence[names.size()]), new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ if(which == 1) {
+ genreButton.setText("");
+ } else {
+ genreButton.setText(finalNames.get(which));
+ }
+ }
+ });
+ AlertDialog dialog = builder.create();
+ dialog.show();
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.playlist_error) + " " + getErrorMessage(error);
+ }
+
+ Util.toast(context, msg, false);
+ }
+ }.execute();
+ }
+ });
+ genreButton.setText(doNothing);
+
+ offlineSpinner = (Spinner) findViewById(R.id.edit_offline_spinner);
+ ArrayAdapter<CharSequence> offlineAdapter = ArrayAdapter.createFromResource(this, R.array.editServerOptions, android.R.layout.simple_spinner_item);
+ offlineAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ offlineSpinner.setAdapter(offlineAdapter);
+
+ // Setup default for everything
+ Bundle extras = getIntent().getBundleExtra(Constants.TASKER_EXTRA_BUNDLE);
+ if(extras != null) {
+ if(extras.getBoolean(Constants.INTENT_EXTRA_NAME_SHUFFLE)) {
+ shuffleCheckbox.setChecked(true);
+ }
+
+ String startYear = extras.getString(Constants.PREFERENCES_KEY_SHUFFLE_START_YEAR, null);
+ if(startYear != null) {
+ startYearCheckbox.setEnabled(true);
+ startYearBox.setText(startYear);
+ }
+ String endYear = extras.getString(Constants.PREFERENCES_KEY_SHUFFLE_END_YEAR, null);
+ if(endYear != null) {
+ endYearCheckbox.setEnabled(true);
+ endYearBox.setText(endYear);
+ }
+
+ String genre = extras.getString(Constants.PREFERENCES_KEY_SHUFFLE_GENRE, doNothing);
+ if(genre != null) {
+ genreButton.setText(genre);
+ }
+
+ int offline = extras.getInt(Constants.PREFERENCES_KEY_OFFLINE, 0);
+ if(offline != 0) {
+ offlineSpinner.setSelection(offline);
+ }
+ }
+
+ drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuInflater menuInflater = getMenuInflater();
+ menuInflater.inflate(R.menu.tasker_configuration, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if(item.getItemId() == android.R.id.home) {
+ cancel();
+ return true;
+ } else if(item.getItemId() == R.id.menu_accept) {
+ accept();
+ return true;
+ } else if(item.getItemId() == R.id.menu_cancel) {
+ cancel();
+ return true;
+ }
+
+ return false;
+ }
+
+ private void accept() {
+ Intent intent = new Intent();
+
+ String blurb = getResources().getString(shuffleCheckbox.isChecked() ? R.string.tasker_start_playing_shuffled : R.string.tasker_start_playing);
+ intent.putExtra("com.twofortyfouram.locale.intent.extra.BLURB", blurb);
+
+ // Get settings user specified
+ Bundle data = new Bundle();
+ boolean shuffle = shuffleCheckbox.isChecked();
+ data.putBoolean(Constants.INTENT_EXTRA_NAME_SHUFFLE, shuffle);
+ if(shuffle) {
+ if(startYearCheckbox.isChecked()) {
+ data.putString(Constants.PREFERENCES_KEY_SHUFFLE_START_YEAR, startYearBox.getText().toString());
+ }
+ if(endYearCheckbox.isChecked()) {
+ data.putString(Constants.PREFERENCES_KEY_SHUFFLE_END_YEAR, endYearBox.getText().toString());
+ }
+ String genre = genreButton.getText().toString();
+ if(!genre.equals(doNothing)) {
+ data.putString(Constants.PREFERENCES_KEY_SHUFFLE_GENRE, genre);
+ }
+ }
+
+ int offline = offlineSpinner.getSelectedItemPosition();
+ if(offline != 0) {
+ data.putInt(Constants.PREFERENCES_KEY_OFFLINE, offline);
+ }
+
+ intent.putExtra(Constants.TASKER_EXTRA_BUNDLE, data);
+
+ setResult(Activity.RESULT_OK, intent);
+ finish();
+ }
+ private void cancel() {
+ setResult(Activity.RESULT_CANCELED);
+ finish();
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/activity/QueryReceiverActivity.java b/app/src/main/java/github/daneren2005/dsub/activity/QueryReceiverActivity.java
new file mode 100644
index 00000000..eefb9c56
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/activity/QueryReceiverActivity.java
@@ -0,0 +1,85 @@
+/*
+ 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.activity;
+
+import android.app.Activity;
+import android.app.SearchManager;
+import android.content.Intent;
+import android.os.Bundle;
+import android.provider.SearchRecentSuggestions;
+import android.util.Log;
+
+import github.daneren2005.dsub.fragments.SubsonicFragment;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.Util;
+import github.daneren2005.dsub.provider.DSubSearchProvider;
+
+/**
+ * Receives search queries and forwards to the SearchFragment.
+ *
+ * @author Sindre Mehus
+ */
+public class QueryReceiverActivity extends Activity {
+
+ private static final String TAG = QueryReceiverActivity.class.getSimpleName();
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Intent intent = getIntent();
+ if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
+ doSearch();
+ } else if(Intent.ACTION_VIEW.equals(intent.getAction())) {
+ showResult(intent.getDataString(), intent.getStringExtra(SearchManager.EXTRA_DATA_KEY));
+ }
+ finish();
+ Util.disablePendingTransition(this);
+ }
+
+ private void doSearch() {
+ String query = getIntent().getStringExtra(SearchManager.QUERY);
+ if (query != null) {
+ Intent intent = new Intent(QueryReceiverActivity.this, SubsonicFragmentActivity.class);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_QUERY, query);
+ intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ Util.startActivityWithoutTransition(QueryReceiverActivity.this, intent);
+ }
+ }
+ private void showResult(String albumId, String name) {
+ if (albumId != null) {
+ Intent intent = new Intent(this, SubsonicFragmentActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ intent.putExtra(Constants.INTENT_EXTRA_VIEW_ALBUM, true);
+ if(albumId.indexOf("ar-") == 0) {
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_ARTIST, true);
+ albumId = albumId.replace("ar-", "");
+ } else if(albumId.indexOf("so-") == 0) {
+ intent.putExtra(Constants.INTENT_EXTRA_SEARCH_SONG, name);
+ albumId = albumId.replace("so-", "");
+ }
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_ID, albumId);
+ if (name != null) {
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_NAME, name);
+ }
+ Util.startActivityWithoutTransition(this, intent);
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/activity/SettingsActivity.java b/app/src/main/java/github/daneren2005/dsub/activity/SettingsActivity.java
new file mode 100644
index 00000000..d5ac60d3
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/activity/SettingsActivity.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 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.activity;
+
+import android.annotation.TargetApi;
+import android.accounts.Account;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.preference.CheckBoxPreference;
+import android.preference.EditTextPreference;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.PreferenceCategory;
+import android.preference.PreferenceScreen;
+import android.support.v7.app.ActionBarActivity;
+import android.text.InputType;
+import android.util.Log;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.FrameLayout;
+
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.fragments.PreferenceCompatFragment;
+import github.daneren2005.dsub.fragments.SettingsFragment;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.LoadingTask;
+import github.daneren2005.dsub.util.SyncUtil;
+import github.daneren2005.dsub.view.ErrorDialog;
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.Util;
+
+import java.io.File;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.text.DecimalFormat;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public class SettingsActivity extends SubsonicActivity {
+ private static final String TAG = SettingsActivity.class.getSimpleName();
+ private PreferenceCompatFragment fragment;
+
+ @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.download_activity);
+
+ if (savedInstanceState == null) {
+ fragment = new SettingsFragment();
+ Bundle args = new Bundle();
+ args.putInt(Constants.INTENT_EXTRA_FRAGMENT_TYPE, R.xml.settings);
+
+ fragment.setArguments(args);
+ fragment.setRetainInstance(true);
+
+ currentFragment = fragment;
+ currentFragment.setPrimaryFragment(true);
+ getSupportFragmentManager().beginTransaction().add(R.id.fragment_container, currentFragment, currentFragment.getSupportTag() + "").commit();
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/activity/SubsonicActivity.java b/app/src/main/java/github/daneren2005/dsub/activity/SubsonicActivity.java
new file mode 100644
index 00000000..4651eb0b
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/activity/SubsonicActivity.java
@@ -0,0 +1,860 @@
+/*
+ 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.activity;
+
+import android.app.UiModeManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Configuration;
+import android.content.res.TypedArray;
+import android.media.AudioManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Environment;
+import android.support.v7.app.ActionBarDrawerToggle;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v4.widget.DrawerLayout;
+import android.support.v7.app.ActionBarActivity;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.animation.AnimationUtils;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import java.io.File;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.ServerInfo;
+import github.daneren2005.dsub.fragments.SubsonicFragment;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.service.HeadphoneListenerService;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.ImageLoader;
+import github.daneren2005.dsub.util.Util;
+import github.daneren2005.dsub.adapter.DrawerAdapter;
+import github.daneren2005.dsub.view.UpdateView;
+import github.daneren2005.dsub.util.UserUtil;
+
+public class SubsonicActivity extends ActionBarActivity implements OnItemSelectedListener {
+ private static final String TAG = SubsonicActivity.class.getSimpleName();
+ private static ImageLoader IMAGE_LOADER;
+ protected static String theme;
+ protected static boolean fullScreen;
+ private String[] drawerItemsDescriptions;
+ private String[] drawerItems;
+ private boolean drawerIdle = true;
+ private boolean[] enabledItems = {true, true, true, true, true};
+ private boolean destroyed = false;
+ private boolean finished = false;
+ protected List<SubsonicFragment> backStack = new ArrayList<SubsonicFragment>();
+ protected SubsonicFragment currentFragment;
+ protected View primaryContainer;
+ protected View secondaryContainer;
+ protected boolean tv = false;
+ protected boolean touchscreen = true;
+ Spinner actionBarSpinner;
+ ArrayAdapter<CharSequence> spinnerAdapter;
+ ViewGroup rootView;
+ DrawerLayout drawer;
+ ActionBarDrawerToggle drawerToggle;
+ DrawerAdapter drawerAdapter;
+ ListView drawerList;
+ TextView lastSelectedView = null;
+ int lastSelectedPosition = 0;
+ boolean drawerOpen = false;
+
+ @Override
+ protected void onCreate(Bundle bundle) {
+ UiModeManager uiModeManager = (UiModeManager) getSystemService(UI_MODE_SERVICE);
+ if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION) {
+ // tv = true;
+ }
+ PackageManager pm = getPackageManager();
+ if(!pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)) {
+ touchscreen = false;
+ }
+
+ setUncaughtExceptionHandler();
+ applyTheme();
+ applyFullscreen();
+ super.onCreate(bundle);
+ startService(new Intent(this, DownloadService.class));
+ setVolumeControlStream(AudioManager.STREAM_MUSIC);
+
+ View actionbar = getLayoutInflater().inflate(R.layout.actionbar_spinner, null);
+ actionBarSpinner = (Spinner)actionbar.findViewById(R.id.spinner);
+ spinnerAdapter = new ArrayAdapter(this, android.R.layout.simple_spinner_item);
+ spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ actionBarSpinner.setOnItemSelectedListener(this);
+ actionBarSpinner.setAdapter(spinnerAdapter);
+
+ getSupportActionBar().setCustomView(actionbar);
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ getSupportActionBar().setHomeButtonEnabled(true);
+
+ if(getIntent().hasExtra(Constants.FRAGMENT_POSITION)) {
+ lastSelectedPosition = getIntent().getIntExtra(Constants.FRAGMENT_POSITION, 0);
+ }
+ }
+
+ @Override
+ protected void onPostCreate(Bundle savedInstanceState) {
+ super.onPostCreate(savedInstanceState);
+ // Sync the toggle state after onRestoreInstanceState has occurred.
+ if(drawerToggle != null) {
+ drawerToggle.syncState();
+ }
+
+ if(Util.shouldStartOnHeadphones(this)) {
+ Intent serviceIntent = new Intent();
+ serviceIntent.setClassName(this.getPackageName(), HeadphoneListenerService.class.getName());
+ this.startService(serviceIntent);
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ Util.registerMediaButtonEventReceiver(this);
+
+ // Make sure to update theme
+ if (theme != null && !theme.equals(Util.getTheme(this)) || fullScreen != Util.getPreferences(this).getBoolean(Constants.PREFERENCES_KEY_FULL_SCREEN, false)) {
+ restart();
+ overridePendingTransition(R.anim.fade_in, R.anim.fade_out);
+ }
+
+ populateDrawer();
+ UpdateView.addActiveActivity();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+
+ UpdateView.removeActiveActivity();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ destroyed = true;
+ }
+
+ @Override
+ public void finish() {
+ super.finish();
+ Util.disablePendingTransition(this);
+ }
+
+ @Override
+ public void startActivity(Intent intent) {
+ if(intent.getComponent() != null) {
+ String name = intent.getComponent().getClassName();
+ if(name != null && name.indexOf("DownloadActivity") != -1) {
+ intent.putExtra(Constants.FRAGMENT_POSITION, lastSelectedPosition);
+ } else if(name != null && name.indexOf("SettingsActivity") != -1) {
+ intent.putExtra(Constants.FRAGMENT_POSITION, drawerItems.length - 1);
+ }
+ }
+ super.startActivity(intent);
+ }
+
+ @Override
+ public void setContentView(int viewId) {
+ if(isTv()) {
+ super.setContentView(R.layout.static_drawer_activity);
+ } else {
+ super.setContentView(R.layout.abstract_activity);
+ }
+ rootView = (ViewGroup) findViewById(R.id.content_frame);
+
+ if(viewId != 0) {
+ LayoutInflater layoutInflater = getLayoutInflater();
+ layoutInflater.inflate(viewId, rootView);
+ }
+
+ drawerList = (ListView) findViewById(R.id.left_drawer);
+ drawerList.setOnItemClickListener(new ListView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, final View view, final int position, long id) {
+ final int actualPosition = drawerAdapter.getActualPosition(position);
+ if("Settings".equals(drawerItemsDescriptions[actualPosition])) {
+ startActivity(new Intent(SubsonicActivity.this, SettingsActivity.class));
+ drawer.closeDrawers();
+ } else if("Admin".equals(drawerItemsDescriptions[actualPosition]) && UserUtil.isCurrentAdmin()) {
+ UserUtil.confirmCredentials(SubsonicActivity.this, new Runnable() {
+ @Override
+ public void run() {
+ drawerItemSelected(actualPosition, view);
+ }
+ });
+ } else {
+ drawerItemSelected(actualPosition, view);
+ }
+ }
+ });
+
+
+
+ if(!isTv()) {
+ drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
+
+ drawerToggle = new ActionBarDrawerToggle(this, drawer, R.string.common_appname, R.string.common_appname) {
+ @Override
+ public void onDrawerClosed(View view) {
+ setTitle(currentFragment.getTitle());
+
+ drawerIdle = true;
+ drawerOpen = false;
+
+ supportInvalidateOptionsMenu();
+ }
+
+ @Override
+ public void onDrawerOpened(View view) {
+ DownloadService downloadService = getDownloadService();
+ if (downloadService == null || downloadService.getBackgroundDownloads().isEmpty()) {
+ drawerAdapter.setDownloadVisible(false);
+ } else {
+ drawerAdapter.setDownloadVisible(true);
+ }
+
+ if (lastSelectedView == null && drawerList.getCount() > lastSelectedPosition) {
+ lastSelectedView = (TextView) drawerList.getChildAt(lastSelectedPosition).findViewById(R.id.drawer_name);
+ if (lastSelectedView != null) {
+ lastSelectedView.setTextAppearance(SubsonicActivity.this, R.style.DSub_TextViewStyle_Bold);
+ }
+ }
+
+ getSupportActionBar().setTitle(R.string.common_appname);
+ getSupportActionBar().setDisplayShowCustomEnabled(false);
+
+ drawerIdle = true;
+ drawerOpen = true;
+
+ supportInvalidateOptionsMenu();
+ }
+
+ @Override
+ public void onDrawerSlide(View drawerView, float slideOffset) {
+ super.onDrawerSlide(drawerView, slideOffset);
+ drawerIdle = false;
+ }
+ };
+ drawer.setDrawerListener(drawerToggle);
+ drawerToggle.setDrawerIndicatorEnabled(false);
+
+ drawer.setOnTouchListener(new View.OnTouchListener() {
+ public boolean onTouch(View v, MotionEvent event) {
+ if (drawerIdle && currentFragment != null && currentFragment.getGestureDetector() != null) {
+ return currentFragment.getGestureDetector().onTouchEvent(event);
+ } else {
+ return false;
+ }
+ }
+ });
+ }
+
+ // Check whether this is a tablet or not
+ secondaryContainer = findViewById(R.id.fragment_second_container);
+ if(secondaryContainer != null) {
+ primaryContainer = findViewById(R.id.fragment_container);
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle savedInstanceState) {
+ super.onSaveInstanceState(savedInstanceState);
+ String[] ids = new String[backStack.size() + 1];
+ ids[0] = currentFragment.getTag();
+ int i = 1;
+ for(SubsonicFragment frag: backStack) {
+ ids[i] = frag.getTag();
+ i++;
+ }
+ savedInstanceState.putStringArray(Constants.MAIN_BACK_STACK, ids);
+ savedInstanceState.putInt(Constants.MAIN_BACK_STACK_SIZE, backStack.size() + 1);
+ savedInstanceState.putInt(Constants.FRAGMENT_POSITION, lastSelectedPosition);
+ }
+ @Override
+ public void onRestoreInstanceState(Bundle savedInstanceState) {
+ super.onRestoreInstanceState(savedInstanceState);
+ int size = savedInstanceState.getInt(Constants.MAIN_BACK_STACK_SIZE);
+ String[] ids = savedInstanceState.getStringArray(Constants.MAIN_BACK_STACK);
+ FragmentManager fm = getSupportFragmentManager();
+ currentFragment = (SubsonicFragment)fm.findFragmentByTag(ids[0]);
+ currentFragment.setPrimaryFragment(true);
+ currentFragment.setSupportTag(ids[0]);
+ supportInvalidateOptionsMenu();
+ FragmentTransaction trans = getSupportFragmentManager().beginTransaction();
+ for(int i = 1; i < size; i++) {
+ SubsonicFragment frag = (SubsonicFragment)fm.findFragmentByTag(ids[i]);
+ frag.setSupportTag(ids[i]);
+ if(secondaryContainer != null) {
+ frag.setPrimaryFragment(false, true);
+ }
+ trans.hide(frag);
+ backStack.add(frag);
+ }
+ trans.commit();
+
+ // Current fragment is hidden in secondaryContainer
+ if(secondaryContainer == null && !currentFragment.isVisible()) {
+ trans = getSupportFragmentManager().beginTransaction();
+ trans.remove(currentFragment);
+ trans.commit();
+ getSupportFragmentManager().executePendingTransactions();
+
+ trans = getSupportFragmentManager().beginTransaction();
+ trans.add(R.id.fragment_container, currentFragment, ids[0]);
+ trans.commit();
+ }
+ // Current fragment needs to be moved over to secondaryContainer
+ else if(secondaryContainer != null && secondaryContainer.findViewById(currentFragment.getRootId()) == null && backStack.size() > 0) {
+ trans = getSupportFragmentManager().beginTransaction();
+ trans.remove(currentFragment);
+ trans.show(backStack.get(backStack.size() - 1));
+ trans.commit();
+ getSupportFragmentManager().executePendingTransactions();
+
+ trans = getSupportFragmentManager().beginTransaction();
+ trans.add(R.id.fragment_second_container, currentFragment, ids[0]);
+ trans.commit();
+
+ secondaryContainer.setVisibility(View.VISIBLE);
+ }
+
+ lastSelectedPosition = savedInstanceState.getInt(Constants.FRAGMENT_POSITION);
+ recreateSpinner();
+ }
+
+ @Override
+ public void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuInflater menuInflater = getMenuInflater();
+ if(drawerOpen) {
+ menuInflater.inflate(R.menu.drawer_menu, menu);
+ } else if(currentFragment != null) {
+ try {
+ currentFragment.setContext(this);
+ currentFragment.onCreateOptionsMenu(menu, menuInflater);
+
+ if(isTouchscreen()) {
+ menu.setGroupVisible(R.id.not_touchscreen, false);
+ }
+ } catch(Exception e) {
+ Log.w(TAG, "Error on creating options menu", e);
+ }
+ }
+ return true;
+ }
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if(drawerToggle != null && drawerToggle.onOptionsItemSelected(item)) {
+ return true;
+ } else if(item.getItemId() == android.R.id.home) {
+ onBackPressed();
+ return true;
+ }
+
+ return currentFragment.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ boolean isVolumeDown = keyCode == KeyEvent.KEYCODE_VOLUME_DOWN;
+ boolean isVolumeUp = keyCode == KeyEvent.KEYCODE_VOLUME_UP;
+ boolean isVolumeAdjust = isVolumeDown || isVolumeUp;
+ boolean isJukebox = getDownloadService() != null && getDownloadService().isRemoteEnabled();
+
+ if (isVolumeAdjust && isJukebox) {
+ getDownloadService().updateRemoteVolume(isVolumeUp);
+ return true;
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public void setTitle(CharSequence title) {
+ if(title != null && !title.equals(getSupportActionBar().getTitle())) {
+ getSupportActionBar().setTitle(title);
+ recreateSpinner();
+ }
+ }
+ public void setSubtitle(CharSequence title) {
+ getSupportActionBar().setSubtitle(title);
+ }
+
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ int top = spinnerAdapter.getCount() - 1;
+ if(position < top) {
+ for(int i = top; i > position; i--) {
+ removeCurrent();
+ }
+ }
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {
+
+ }
+
+ private void populateDrawer() {
+ SharedPreferences prefs = Util.getPreferences(this);
+ boolean podcastsEnabled = prefs.getBoolean(Constants.PREFERENCES_KEY_PODCASTS_ENABLED, true);
+ boolean bookmarksEnabled = prefs.getBoolean(Constants.PREFERENCES_KEY_BOOKMARKS_ENABLED, true) && !Util.isOffline(this) && ServerInfo.canBookmark(this);
+ boolean sharedEnabled = prefs.getBoolean(Constants.PREFERENCES_KEY_SHARED_ENABLED, true) && !Util.isOffline(this);
+ boolean chatEnabled = prefs.getBoolean(Constants.PREFERENCES_KEY_CHAT_ENABLED, true) && !Util.isOffline(this);
+ boolean adminEnabled = prefs.getBoolean(Constants.PREFERENCES_KEY_ADMIN_ENABLED, true) && !Util.isOffline(this);
+
+ if(drawerItems == null || !enabledItems[0] == podcastsEnabled || !enabledItems[1] == bookmarksEnabled || !enabledItems[2] == sharedEnabled || !enabledItems[3] == chatEnabled || !enabledItems[4] == adminEnabled) {
+ drawerItems = getResources().getStringArray(R.array.drawerItems);
+ drawerItemsDescriptions = getResources().getStringArray(R.array.drawerItemsDescriptions);
+
+ List<String> drawerItemsList = new ArrayList<String>(Arrays.asList(drawerItems));
+ List<Integer> drawerItemsIconsList = new ArrayList<Integer>();
+ List<Boolean> drawerItemsVisibleList = new ArrayList<Boolean>();
+
+ int[] arrayAttr = {R.attr.drawerItemsIcons};
+ TypedArray arrayType = obtainStyledAttributes(arrayAttr);
+ int arrayId = arrayType.getResourceId(0, 0);
+ TypedArray iconType = getResources().obtainTypedArray(arrayId);
+ for(int i = 0; i < drawerItemsList.size(); i++) {
+ drawerItemsIconsList.add(iconType.getResourceId(i, 0));
+ drawerItemsVisibleList.add(true);
+ }
+ iconType.recycle();
+ arrayType.recycle();
+
+ // Hide listings user doesn't want to see
+ if(!podcastsEnabled) {
+ drawerItemsVisibleList.set(3, false);
+ }
+ if(!bookmarksEnabled) {
+ drawerItemsVisibleList.set(4, false);
+ }
+ if(!sharedEnabled) {
+ drawerItemsVisibleList.set(5, false);
+ }
+ if(!chatEnabled) {
+ drawerItemsVisibleList.set(6, false);
+ }
+ if(!adminEnabled) {
+ drawerItemsVisibleList.set(7, false);
+ }
+ if(!getIntent().hasExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD_VIEW)) {
+ drawerItemsVisibleList.set(8, false);
+ }
+
+ drawerList.setAdapter(drawerAdapter = new DrawerAdapter(this, drawerItemsList, drawerItemsIconsList, drawerItemsVisibleList));
+ enabledItems[0] = podcastsEnabled;
+ enabledItems[1] = bookmarksEnabled;
+ enabledItems[2] = sharedEnabled;
+ enabledItems[3] = chatEnabled;
+ enabledItems[4] = adminEnabled;
+
+ String fragmentType = getIntent().getStringExtra(Constants.INTENT_EXTRA_FRAGMENT_TYPE);
+ if(fragmentType != null && lastSelectedPosition == 0) {
+ for(int i = 0; i < drawerItemsDescriptions.length; i++) {
+ if(fragmentType.equals(drawerItemsDescriptions[i])) {
+ lastSelectedPosition = drawerAdapter.getAdapterPosition(i);
+ break;
+ }
+ }
+ }
+
+ if(drawerList.getChildAt(lastSelectedPosition) == null) {
+ lastSelectedView = null;
+ drawerAdapter.setSelectedPosition(lastSelectedPosition);
+ } else {
+ lastSelectedView = (TextView) drawerList.getChildAt(lastSelectedPosition).findViewById(R.id.drawer_name);
+ if(lastSelectedView != null) {
+ lastSelectedView.setTextAppearance(SubsonicActivity.this, R.style.DSub_TextViewStyle_Bold);
+ }
+ }
+ }
+ }
+
+ private void drawerItemSelected(int position, View view) {
+ startFragmentActivity(drawerItemsDescriptions[position]);
+
+ if(lastSelectedView != view) {
+ if(lastSelectedView != null) {
+ lastSelectedView.setTextAppearance(this, R.style.DSub_TextViewStyle);
+ }
+
+ lastSelectedView = (TextView) view.findViewById(R.id.drawer_name);
+ lastSelectedView.setTextAppearance(this, R.style.DSub_TextViewStyle_Bold);
+ lastSelectedPosition = position;
+ }
+ }
+
+ public void startFragmentActivity(String fragmentType) {
+ Intent intent = new Intent();
+ intent.setClass(SubsonicActivity.this, SubsonicFragmentActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ if(!"".equals(fragmentType)) {
+ intent.putExtra(Constants.INTENT_EXTRA_FRAGMENT_TYPE, fragmentType);
+ }
+ startActivity(intent);
+ finish();
+ }
+
+ protected void exit() {
+ if(((Object) this).getClass() != SubsonicFragmentActivity.class) {
+ Intent intent = new Intent(this, SubsonicFragmentActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_EXIT, true);
+ Util.startActivityWithoutTransition(this, intent);
+ } else {
+ finished = true;
+ this.stopService(new Intent(this, DownloadService.class));
+ this.finish();
+ }
+ }
+
+ public boolean onBackPressedSupport() {
+ if(drawerOpen) {
+ drawer.closeDrawers();
+ return false;
+ } else if(backStack.size() > 0) {
+ removeCurrent();
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ @Override
+ public void onBackPressed() {
+ if(onBackPressedSupport()) {
+ super.onBackPressed();
+ }
+ }
+
+ public void replaceFragment(SubsonicFragment fragment, int tag) {
+ replaceFragment(fragment, tag, false);
+ }
+ public void replaceFragment(SubsonicFragment fragment, int tag, boolean replaceCurrent) {
+ SubsonicFragment oldFragment = currentFragment;
+ if(currentFragment != null) {
+ currentFragment.setPrimaryFragment(false, secondaryContainer != null);
+ }
+ backStack.add(currentFragment);
+
+ currentFragment = fragment;
+ currentFragment.setPrimaryFragment(true);
+ supportInvalidateOptionsMenu();
+
+ if(secondaryContainer == null) {
+ FragmentTransaction trans = getSupportFragmentManager().beginTransaction();
+ trans.setCustomAnimations(R.anim.enter_from_right, R.anim.exit_to_left, R.anim.enter_from_left, R.anim.exit_to_right);
+ trans.hide(oldFragment);
+ trans.add(R.id.fragment_container, fragment, tag + "");
+ trans.commit();
+ } else {
+ // Make sure secondary container is visible now
+ secondaryContainer.setVisibility(View.VISIBLE);
+
+ FragmentTransaction trans = getSupportFragmentManager().beginTransaction();
+
+ // Check to see if you need to put on top of old left or not
+ if(backStack.size() > 1) {
+ // Move old right to left if there is a backstack already
+ SubsonicFragment newLeftFragment = backStack.get(backStack.size() - 1);
+ if(replaceCurrent) {
+ // trans.setCustomAnimations(R.anim.enter_from_right, R.anim.exit_to_left, R.anim.enter_from_left, R.anim.exit_to_right);
+ }
+ trans.remove(newLeftFragment);
+
+ // Only move right to left if replaceCurrent is false
+ if(!replaceCurrent) {
+ SubsonicFragment oldLeftFragment = backStack.get(backStack.size() - 2);
+ oldLeftFragment.setSecondaryFragment(false);
+ // trans.setCustomAnimations(R.anim.enter_from_right, R.anim.exit_to_left, R.anim.enter_from_left, R.anim.exit_to_right);
+ trans.hide(oldLeftFragment);
+
+ // Make sure remove is finished before adding
+ trans.commit();
+ getSupportFragmentManager().executePendingTransactions();
+
+ trans = getSupportFragmentManager().beginTransaction();
+ // trans.setCustomAnimations(R.anim.enter_from_right, R.anim.exit_to_left, R.anim.enter_from_left, R.anim.exit_to_right);
+ trans.add(R.id.fragment_container, newLeftFragment, newLeftFragment.getSupportTag() + "");
+ } else {
+ backStack.remove(backStack.size() - 1);
+ }
+ }
+
+ // Add fragment to the right container
+ trans.setCustomAnimations(R.anim.enter_from_right, R.anim.exit_to_left, R.anim.enter_from_left, R.anim.exit_to_right);
+ trans.add(R.id.fragment_second_container, fragment, tag + "");
+
+ // Commit it all
+ trans.commit();
+ }
+ recreateSpinner();
+ }
+ public void removeCurrent() {
+ if(currentFragment != null) {
+ currentFragment.setPrimaryFragment(false);
+ }
+ Fragment oldFrag = currentFragment;
+
+ currentFragment = backStack.remove(backStack.size() - 1);
+ currentFragment.setPrimaryFragment(true, false);
+ supportInvalidateOptionsMenu();
+
+ if(secondaryContainer == null) {
+ FragmentTransaction trans = getSupportFragmentManager().beginTransaction();
+ trans.setCustomAnimations(R.anim.enter_from_left, R.anim.exit_to_right, R.anim.enter_from_right, R.anim.exit_to_left);
+ trans.remove(oldFrag);
+ trans.show(currentFragment);
+ trans.commit();
+ } else {
+ FragmentTransaction trans = getSupportFragmentManager().beginTransaction();
+
+ // Remove old right fragment
+ trans.setCustomAnimations(R.anim.enter_from_left, R.anim.exit_to_right, R.anim.enter_from_right, R.anim.exit_to_left);
+ trans.remove(oldFrag);
+
+ // Only switch places if there is a backstack, otherwise primary container is correct
+ if(backStack.size() > 0) {
+ trans.setCustomAnimations(0, 0, 0, 0);
+ // Add current left fragment to right side
+ trans.remove(currentFragment);
+
+ // Make sure remove is finished before adding
+ trans.commit();
+ getSupportFragmentManager().executePendingTransactions();
+
+ trans = getSupportFragmentManager().beginTransaction();
+ // trans.setCustomAnimations(R.anim.enter_from_left, R.anim.exit_to_right, R.anim.enter_from_right, R.anim.exit_to_left);
+ trans.add(R.id.fragment_second_container, currentFragment, currentFragment.getSupportTag() + "");
+
+ SubsonicFragment newLeftFragment = backStack.get(backStack.size() - 1);
+ newLeftFragment.setSecondaryFragment(true);
+ trans.show(newLeftFragment);
+ } else {
+ secondaryContainer.startAnimation(AnimationUtils.loadAnimation(this, R.anim.exit_to_right));
+ secondaryContainer.setVisibility(View.GONE);
+ }
+
+ trans.commit();
+ }
+ recreateSpinner();
+ }
+
+ public void invalidate() {
+ if(currentFragment != null) {
+ while(backStack.size() > 0) {
+ removeCurrent();
+ }
+
+ currentFragment.invalidate();
+ populateDrawer();
+ }
+
+ supportInvalidateOptionsMenu();
+ }
+
+ protected void recreateSpinner() {
+ if(currentFragment == null || currentFragment.getTitle() == null) {
+ return;
+ }
+
+ if(backStack.size() > 0) {
+ spinnerAdapter.clear();
+ for(int i = 0; i < backStack.size(); i++) {
+ CharSequence title = backStack.get(i).getTitle();
+ if(title != null) {
+ spinnerAdapter.add(title);
+ } else {
+ spinnerAdapter.add("null");
+ }
+ }
+ if(currentFragment.getTitle() != null) {
+ spinnerAdapter.add(currentFragment.getTitle());
+ } else {
+ spinnerAdapter.add("null");
+ }
+ spinnerAdapter.notifyDataSetChanged();
+ actionBarSpinner.setSelection(spinnerAdapter.getCount() - 1);
+ if(!isTv()) {
+ getSupportActionBar().setDisplayShowCustomEnabled(true);
+ }
+ } else if(!isTv()) {
+ getSupportActionBar().setDisplayShowCustomEnabled(false);
+ }
+ }
+
+ protected void restart() {
+ Intent intent = new Intent(this, ((Object) this).getClass());
+ intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ intent.putExtras(getIntent());
+ Util.startActivityWithoutTransition(this, intent);
+ }
+
+ private void applyTheme() {
+ theme = Util.getTheme(this);
+
+ if(theme != null && theme.indexOf("fullscreen") != -1) {
+ theme = theme.substring(0, theme.indexOf("_fullscreen"));
+ Util.setTheme(this, theme);
+ }
+
+ Util.applyTheme(this, theme);
+ }
+ private void applyFullscreen() {
+ fullScreen = Util.getPreferences(this).getBoolean(Constants.PREFERENCES_KEY_FULL_SCREEN, false);
+ if(fullScreen || isTv()) {
+ // Hide additional elements on higher Android versions
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ int flags = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
+ View.SYSTEM_UI_FLAG_FULLSCREEN |
+ View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
+
+ getWindow().getDecorView().setSystemUiVisibility(flags);
+ } else if(Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ getWindow().requestFeature(Window.FEATURE_NO_TITLE);
+ }
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
+ }
+ }
+
+ public boolean isDestroyedCompat() {
+ return destroyed;
+ }
+
+ public synchronized ImageLoader getImageLoader() {
+ if (IMAGE_LOADER == null) {
+ IMAGE_LOADER = new ImageLoader(this);
+ }
+ return IMAGE_LOADER;
+ }
+ public synchronized static ImageLoader getStaticImageLoader(Context context) {
+ if (IMAGE_LOADER == null) {
+ IMAGE_LOADER = new ImageLoader(context);
+ }
+ return IMAGE_LOADER;
+ }
+
+ public DownloadService getDownloadService() {
+ if(finished) {
+ return null;
+ }
+
+ // If service is not available, request it to start and wait for it.
+ for (int i = 0; i < 5; i++) {
+ DownloadService downloadService = DownloadService.getInstance();
+ if (downloadService != null) {
+ return downloadService;
+ }
+ Log.w(TAG, "DownloadService not running. Attempting to start it.");
+ startService(new Intent(this, DownloadService.class));
+ Util.sleepQuietly(50L);
+ }
+ return DownloadService.getInstance();
+ }
+
+ public static String getThemeName() {
+ return theme;
+ }
+
+ public boolean isTv() {
+ return tv;
+ }
+ public boolean isTouchscreen() {
+ return touchscreen;
+ }
+
+ private void setUncaughtExceptionHandler() {
+ Thread.UncaughtExceptionHandler handler = Thread.getDefaultUncaughtExceptionHandler();
+ if (!(handler instanceof SubsonicActivity.SubsonicUncaughtExceptionHandler)) {
+ Thread.setDefaultUncaughtExceptionHandler(new SubsonicActivity.SubsonicUncaughtExceptionHandler(this));
+ }
+ }
+
+ /**
+ * Logs the stack trace of uncaught exceptions to a file on the SD card.
+ */
+ private static class SubsonicUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
+
+ private final Thread.UncaughtExceptionHandler defaultHandler;
+ private final Context context;
+
+ private SubsonicUncaughtExceptionHandler(Context context) {
+ this.context = context;
+ defaultHandler = Thread.getDefaultUncaughtExceptionHandler();
+ }
+
+ @Override
+ public void uncaughtException(Thread thread, Throwable throwable) {
+ File file = null;
+ PrintWriter printWriter = null;
+ try {
+
+ PackageInfo packageInfo = context.getPackageManager().getPackageInfo("github.daneren2005.dsub", 0);
+ file = new File(Environment.getExternalStorageDirectory(), "dsub-stacktrace.txt");
+ printWriter = new PrintWriter(file);
+ printWriter.println("Android API level: " + Build.VERSION.SDK);
+ printWriter.println("Subsonic version name: " + packageInfo.versionName);
+ printWriter.println("Subsonic version code: " + packageInfo.versionCode);
+ printWriter.println();
+ throwable.printStackTrace(printWriter);
+ Log.i(TAG, "Stack trace written to " + file);
+ } catch (Throwable x) {
+ Log.e(TAG, "Failed to write stack trace to " + file, x);
+ } finally {
+ Util.close(printWriter);
+ if (defaultHandler != null) {
+ defaultHandler.uncaughtException(thread, throwable);
+ }
+
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/activity/SubsonicFragmentActivity.java b/app/src/main/java/github/daneren2005/dsub/activity/SubsonicFragmentActivity.java
new file mode 100644
index 00000000..6614e09d
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/activity/SubsonicFragmentActivity.java
@@ -0,0 +1,686 @@
+/*
+ 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.activity;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.app.Dialog;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.TypedArray;
+import android.os.Bundle;
+import android.os.Handler;
+import android.preference.PreferenceManager;
+import android.support.v4.app.FragmentTransaction;
+import android.util.Log;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.TextView;
+
+import java.io.File;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.PlayerQueue;
+import github.daneren2005.dsub.domain.PlayerState;
+import github.daneren2005.dsub.domain.ServerInfo;
+import github.daneren2005.dsub.fragments.AdminFragment;
+import github.daneren2005.dsub.fragments.ChatFragment;
+import github.daneren2005.dsub.fragments.DownloadFragment;
+import github.daneren2005.dsub.fragments.MainFragment;
+import github.daneren2005.dsub.fragments.SearchFragment;
+import github.daneren2005.dsub.fragments.SelectArtistFragment;
+import github.daneren2005.dsub.fragments.SelectBookmarkFragment;
+import github.daneren2005.dsub.fragments.SelectDirectoryFragment;
+import github.daneren2005.dsub.fragments.SelectPlaylistFragment;
+import github.daneren2005.dsub.fragments.SelectPodcastsFragment;
+import github.daneren2005.dsub.fragments.SelectShareFragment;
+import github.daneren2005.dsub.fragments.SubsonicFragment;
+import github.daneren2005.dsub.service.DownloadFile;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import github.daneren2005.dsub.updates.Updater;
+import github.daneren2005.dsub.util.BackgroundTask;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.SilentBackgroundTask;
+import github.daneren2005.dsub.util.UserUtil;
+import github.daneren2005.dsub.util.Util;
+import github.daneren2005.dsub.view.ChangeLog;
+
+/**
+ * Created by Scott on 10/14/13.
+ */
+public class SubsonicFragmentActivity extends SubsonicActivity {
+ private static String TAG = SubsonicFragmentActivity.class.getSimpleName();
+ private static boolean infoDialogDisplayed;
+ private static boolean sessionInitialized = false;
+ private static long ALLOWED_SKEW = 30000L;
+
+ private ScheduledExecutorService executorService;
+ private View bottomBar;
+ private View coverArtView;
+ private TextView trackView;
+ private TextView artistView;
+ private ImageButton startButton;
+ private long lastBackPressTime = 0;
+ private DownloadFile currentPlaying;
+ private PlayerState currentState;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (getIntent().hasExtra(Constants.INTENT_EXTRA_NAME_EXIT)) {
+ stopService(new Intent(this, DownloadService.class));
+ finish();
+ getImageLoader().clearCache();
+ } else if(getIntent().hasExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD_VIEW)) {
+ getIntent().putExtra(Constants.INTENT_EXTRA_FRAGMENT_TYPE, "Download");
+ if(drawerAdapter != null) {
+ drawerAdapter.setDownloadVisible(true);
+ }
+ } else if(getIntent().hasExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD)) {
+ DownloadService service = getDownloadService();
+ if((service != null && service.getCurrentPlaying() != null)) {
+ getIntent().removeExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD);
+ Intent intent = new Intent();
+ intent.setClass(this, DownloadActivity.class);
+ startActivity(intent);
+ }
+ }
+ setContentView(R.layout.abstract_fragment_activity);
+
+ UserUtil.seedCurrentUser(this);
+ if (findViewById(R.id.fragment_container) != null && savedInstanceState == null) {
+ String fragmentType = getIntent().getStringExtra(Constants.INTENT_EXTRA_FRAGMENT_TYPE);
+ boolean firstRun = false;
+ if(fragmentType == null) {
+ fragmentType = Util.openToTab(this);
+ if(fragmentType != null) {
+ getIntent().putExtra(Constants.INTENT_EXTRA_FRAGMENT_TYPE, fragmentType);
+ firstRun = true;
+ }
+ }
+ currentFragment = getNewFragment(fragmentType);
+
+ if("".equals(fragmentType) || fragmentType == null || firstRun) {
+ // Initial startup stuff
+ if(!sessionInitialized) {
+ loadSession();
+ }
+ }
+
+ currentFragment.setPrimaryFragment(true);
+ getSupportFragmentManager().beginTransaction().add(R.id.fragment_container, currentFragment, currentFragment.getSupportTag() + "").commit();
+
+ if(getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_QUERY) != null) {
+ SearchFragment fragment = new SearchFragment();
+ replaceFragment(fragment, fragment.getSupportTag());
+ }
+
+ // If a album type is set, switch to that album type view
+ String albumType = getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE);
+ if(albumType != null) {
+ SubsonicFragment fragment = new SelectDirectoryFragment();
+
+ Bundle args = new Bundle();
+ args.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, albumType);
+ args.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 20);
+ args.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0);
+
+ fragment.setArguments(args);
+ replaceFragment(fragment, fragment.getSupportTag());
+ }
+ }
+
+ bottomBar = findViewById(R.id.bottom_bar);
+ bottomBar.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ Intent intent = new Intent();
+ intent.setClass(v.getContext(), DownloadActivity.class);
+ startActivity(intent);
+ }
+ });
+ coverArtView = bottomBar.findViewById(R.id.album_art);
+ trackView = (TextView) bottomBar.findViewById(R.id.track_name);
+ artistView = (TextView) bottomBar.findViewById(R.id.artist_name);
+
+ ImageButton previousButton = (ImageButton) findViewById(R.id.download_previous);
+ previousButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ new SilentBackgroundTask<Void>(SubsonicFragmentActivity.this) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ if(getDownloadService() == null) {
+ return null;
+ }
+
+ getDownloadService().previous();
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ update();
+ }
+ }.execute();
+ }
+ });
+
+ startButton = (ImageButton) findViewById(R.id.download_start);
+ startButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ new SilentBackgroundTask<Void>(SubsonicFragmentActivity.this) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ PlayerState state = getDownloadService().getPlayerState();
+ if(state == PlayerState.STARTED) {
+ getDownloadService().pause();
+ } else {
+ getDownloadService().start();
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ update();
+ }
+ }.execute();
+ }
+ });
+
+ ImageButton nextButton = (ImageButton) findViewById(R.id.download_next);
+ nextButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ new SilentBackgroundTask<Void>(SubsonicFragmentActivity.this) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ if(getDownloadService() == null) {
+ return null;
+ }
+
+ getDownloadService().next();
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ update();
+ }
+ }.execute();
+ }
+ });
+ }
+
+ @Override
+ protected void onPostCreate(Bundle bundle) {
+ super.onPostCreate(bundle);
+
+ showInfoDialog();
+ checkUpdates();
+
+ ChangeLog changeLog = new ChangeLog(this, Util.getPreferences(this));
+ if(changeLog.isFirstRun()) {
+ if(changeLog.isFirstRunEver()) {
+ changeLog.updateVersionInPreferences();
+ } else {
+ Dialog log = changeLog.getLogDialog();
+ if (log != null) {
+ log.show();
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+
+ if(currentFragment != null && intent.getStringExtra(Constants.INTENT_EXTRA_NAME_QUERY) != null) {
+ if(currentFragment instanceof SearchFragment) {
+ String query = intent.getStringExtra(Constants.INTENT_EXTRA_NAME_QUERY);
+ boolean autoplay = intent.getBooleanExtra(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false);
+ boolean requestsearch = intent.getBooleanExtra(Constants.INTENT_EXTRA_REQUEST_SEARCH, false);
+
+ if (query != null) {
+ ((SearchFragment)currentFragment).search(query, autoplay);
+ } else {
+ ((SearchFragment)currentFragment).populateList();
+ if (requestsearch) {
+ onSearchRequested();
+ }
+ }
+ getIntent().removeExtra(Constants.INTENT_EXTRA_NAME_QUERY);
+ } else {
+ setIntent(intent);
+
+ SearchFragment fragment = new SearchFragment();
+ replaceFragment(fragment, fragment.getSupportTag());
+ }
+ } else {
+ setIntent(intent);
+ }
+ if(drawer != null) {
+ drawer.closeDrawers();
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ final Handler handler = new Handler();
+ Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ update();
+ }
+ });
+ }
+ };
+
+ if(getIntent().hasExtra(Constants.INTENT_EXTRA_VIEW_ALBUM)) {
+ SubsonicFragment fragment = new SelectDirectoryFragment();
+ Bundle args = new Bundle();
+ args.putString(Constants.INTENT_EXTRA_NAME_ID, getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_ID));
+ args.putString(Constants.INTENT_EXTRA_NAME_NAME, getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_NAME));
+ args.putString(Constants.INTENT_EXTRA_SEARCH_SONG, getIntent().getStringExtra(Constants.INTENT_EXTRA_SEARCH_SONG));
+ if(getIntent().hasExtra(Constants.INTENT_EXTRA_NAME_ARTIST)) {
+ args.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, true);
+ }
+ if(getIntent().hasExtra(Constants.INTENT_EXTRA_NAME_CHILD_ID)) {
+ args.putString(Constants.INTENT_EXTRA_NAME_CHILD_ID, getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_CHILD_ID));
+ }
+ fragment.setArguments(args);
+
+ replaceFragment(fragment, fragment.getSupportTag());
+ getIntent().removeExtra(Constants.INTENT_EXTRA_VIEW_ALBUM);
+ if("Artist".equals(getIntent().getStringExtra(Constants.INTENT_EXTRA_FRAGMENT_TYPE))) {
+ lastSelectedPosition = 1;
+ }
+ }
+
+ createAccount();
+
+ executorService = Executors.newSingleThreadScheduledExecutor();
+ executorService.scheduleWithFixedDelay(runnable, 0L, 1000L, TimeUnit.MILLISECONDS);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ executorService.shutdown();
+ }
+
+ @Override
+ public void onRestoreInstanceState(Bundle savedInstanceState) {
+ super.onRestoreInstanceState(savedInstanceState);
+ if(drawerToggle != null && backStack.size() > 0) {
+ drawerToggle.setDrawerIndicatorEnabled(false);
+ }
+ }
+
+ @Override
+ public void setContentView(int viewId) {
+ super.setContentView(viewId);
+ if(drawerToggle != null){
+ drawerToggle.setDrawerIndicatorEnabled(true);
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void onBackPressed() {
+ if(onBackPressedSupport()) {
+ if(!Util.disableExitPrompt(this) && lastBackPressTime < (System.currentTimeMillis() - 4000)) {
+ lastBackPressTime = System.currentTimeMillis();
+ Util.toast(this, R.string.main_back_confirm);
+ } else {
+ finish();
+ }
+ }
+ }
+
+ @Override
+ public void replaceFragment(SubsonicFragment fragment, int tag, boolean replaceCurrent) {
+ super.replaceFragment(fragment, tag, replaceCurrent);
+ if(drawerToggle != null) {
+ drawerToggle.setDrawerIndicatorEnabled(false);
+ }
+ }
+ @Override
+ public void removeCurrent() {
+ super.removeCurrent();
+ if(drawerToggle != null && backStack.isEmpty()) {
+ drawerToggle.setDrawerIndicatorEnabled(true);
+ }
+ }
+
+ @Override
+ public void startFragmentActivity(String fragmentType) {
+ // Create a transaction that does all of this
+ FragmentTransaction trans = getSupportFragmentManager().beginTransaction();
+
+ // Clear existing stack
+ for(int i = backStack.size() - 1; i >= 0; i--) {
+ trans.remove(backStack.get(i));
+ }
+ trans.remove(currentFragment);
+ backStack.clear();
+
+ // Create new stack
+ currentFragment = getNewFragment(fragmentType);
+ currentFragment.setPrimaryFragment(true);
+ trans.add(R.id.fragment_container, currentFragment, currentFragment.getSupportTag() + "");
+
+ // Done, cleanup
+ trans.commit();
+ supportInvalidateOptionsMenu();
+ recreateSpinner();
+ if(drawer != null) {
+ drawer.closeDrawers();
+ }
+
+ if(secondaryContainer != null) {
+ secondaryContainer.setVisibility(View.GONE);
+ }
+ if(drawerToggle != null) {
+ drawerToggle.setDrawerIndicatorEnabled(true);
+ }
+ }
+
+ private SubsonicFragment getNewFragment(String fragmentType) {
+ if("Artist".equals(fragmentType)) {
+ return new SelectArtistFragment();
+ } else if("Playlist".equals(fragmentType)) {
+ return new SelectPlaylistFragment();
+ } else if("Chat".equals(fragmentType)) {
+ return new ChatFragment();
+ } else if("Podcast".equals(fragmentType)) {
+ return new SelectPodcastsFragment();
+ } else if("Bookmark".equals(fragmentType)) {
+ return new SelectBookmarkFragment();
+ } else if("Share".equals(fragmentType)) {
+ return new SelectShareFragment();
+ } else if("Admin".equals(fragmentType)) {
+ return new AdminFragment();
+ } else if("Download".equals(fragmentType)) {
+ return new DownloadFragment();
+ } else {
+ return new MainFragment();
+ }
+ }
+
+ private void update() {
+ DownloadService downloadService = getDownloadService();
+ if (downloadService == null) {
+ return;
+ }
+
+ DownloadFile current = downloadService.getCurrentPlaying();
+ PlayerState state = downloadService.getPlayerState();
+ if(current == currentPlaying && state == currentState) {
+ return;
+ } else {
+ currentPlaying = current;
+ currentState = state;
+ }
+
+ MusicDirectory.Entry song = null;
+ if(current != null) {
+ song = current.getSong();
+ trackView.setText(song.getTitle());
+ artistView.setText(song.getArtist());
+ } else {
+ trackView.setText("Title");
+ artistView.setText("Artist");
+ }
+
+ getImageLoader().loadImage(coverArtView, song, false, false);
+ int[] attrs = new int[] {(state == PlayerState.STARTED) ? R.attr.media_button_pause : R.attr.media_button_start};
+ TypedArray typedArray = this.obtainStyledAttributes(attrs);
+ startButton.setImageResource(typedArray.getResourceId(0, 0));
+ typedArray.recycle();
+ }
+
+ public void checkUpdates() {
+ try {
+ String version = getPackageManager().getPackageInfo(getPackageName(), 0).versionName;
+ int ver = Integer.parseInt(version.replace(".", ""));
+ Updater updater = new Updater(ver);
+ updater.checkUpdates(this);
+ }
+ catch(Exception e) {
+
+ }
+ }
+
+ private void loadSession() {
+ loadSettings();
+ if(!Util.isOffline(this) && ServerInfo.canBookmark(this)) {
+ loadBookmarks();
+ }
+ // If we are on Subsonic 5.2+, save play queue
+ if(ServerInfo.canSavePlayQueue(this) && !Util.isOffline(this)) {
+ loadRemotePlayQueue();
+ }
+
+ sessionInitialized = true;
+ }
+ private void loadSettings() {
+ PreferenceManager.setDefaultValues(this, R.xml.settings, false);
+ SharedPreferences prefs = Util.getPreferences(this);
+ if (!prefs.contains(Constants.PREFERENCES_KEY_CACHE_LOCATION) || prefs.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, null) == null) {
+ resetCacheLocation(prefs);
+ } else {
+ String path = prefs.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, null);
+ File cacheLocation = new File(path);
+ if(!FileUtil.verifyCanWrite(cacheLocation)) {
+ // Only warn user if there is a difference saved
+ if(resetCacheLocation(prefs)) {
+ Util.info(this, R.string.common_warning, R.string.settings_cache_location_reset);
+ }
+ }
+ }
+
+ if (!prefs.contains(Constants.PREFERENCES_KEY_OFFLINE)) {
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putBoolean(Constants.PREFERENCES_KEY_OFFLINE, false);
+
+ editor.putString(Constants.PREFERENCES_KEY_SERVER_NAME + 1, "Demo Server");
+ editor.putString(Constants.PREFERENCES_KEY_SERVER_URL + 1, "http://demo.subsonic.org");
+ editor.putString(Constants.PREFERENCES_KEY_USERNAME + 1, "android-guest");
+ editor.putString(Constants.PREFERENCES_KEY_PASSWORD + 1, "guest");
+ editor.putInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1);
+ editor.commit();
+ }
+ if(!prefs.contains(Constants.PREFERENCES_KEY_SERVER_COUNT)) {
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putInt(Constants.PREFERENCES_KEY_SERVER_COUNT, 1);
+ editor.commit();
+ }
+ }
+
+ private boolean resetCacheLocation(SharedPreferences prefs) {
+ String newDirectory = FileUtil.getDefaultMusicDirectory(this).getPath();
+ String oldDirectory = prefs.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, null);
+ if(newDirectory == null || (oldDirectory != null && newDirectory.equals(oldDirectory))) {
+ return false;
+ } else {
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putString(Constants.PREFERENCES_KEY_CACHE_LOCATION, newDirectory);
+ editor.commit();
+ return true;
+ }
+ }
+
+ private void loadBookmarks() {
+ final Context context = this;
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ public Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.getBookmarks(true, context, null);
+
+ return null;
+ }
+
+ @Override
+ public void error(Throwable error) {
+ Log.e(TAG, "Failed to get bookmarks", error);
+ }
+ }.execute();
+ }
+ private void loadRemotePlayQueue() {
+ final SubsonicActivity context = this;
+ new SilentBackgroundTask<Void>(this) {
+ private PlayerQueue playerQueue;
+
+ @Override
+ protected Void doInBackground() throws Throwable {
+ try {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ PlayerQueue remoteState = musicService.getPlayQueue(context, null);
+
+ // Make sure we wait until download service is ready
+ DownloadService downloadService = getDownloadService();
+ while(downloadService == null || !downloadService.isInitialized()) {
+ Util.sleepQuietly(100L);
+ downloadService = getDownloadService();
+ }
+
+ // If we had a remote state and it's changed is more recent than our existing state
+ if(remoteState != null && remoteState.changed != null) {
+ // Check if changed + 30 seconds since some servers have slight skew
+ Date remoteChange = new Date(remoteState.changed.getTime() - ALLOWED_SKEW);
+ Date localChange = downloadService.getLastStateChanged();
+ if(localChange == null || localChange.before(remoteChange)) {
+ playerQueue = remoteState;
+ }
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to get playing queue to server", e);
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void done(Void arg) {
+ if(!context.isDestroyedCompat() && playerQueue != null) {
+ promptRestoreFromRemoteQueue(playerQueue);
+ }
+ }
+ }.execute();
+ }
+ private void promptRestoreFromRemoteQueue(final PlayerQueue remoteState) {
+ Util.confirmDialog(this, R.string.download_restore_play_queue, Util.formatDate(remoteState.changed), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ new SilentBackgroundTask<Void>(SubsonicFragmentActivity.this) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ DownloadService downloadService = getDownloadService();
+ downloadService.clear();
+ downloadService.download(remoteState.songs, false, false, false, false, remoteState.currentPlayingIndex, remoteState.currentPlayingPosition);
+ return null;
+ }
+ }.execute();
+ }
+ }, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ new SilentBackgroundTask<Void>(SubsonicFragmentActivity.this) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ DownloadService downloadService = getDownloadService();
+ downloadService.serializeQueue(false);
+ return null;
+ }
+ }.execute();
+ }
+ });
+ }
+
+ private void createAccount() {
+ final Context context = this;
+
+ new SilentBackgroundTask<Void>(this) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ AccountManager accountManager = (AccountManager) context.getSystemService(ACCOUNT_SERVICE);
+ Account account = new Account(Constants.SYNC_ACCOUNT_NAME, Constants.SYNC_ACCOUNT_TYPE);
+ accountManager.addAccountExplicitly(account, null, null);
+
+ SharedPreferences prefs = Util.getPreferences(context);
+ boolean syncEnabled = prefs.getBoolean(Constants.PREFERENCES_KEY_SYNC_ENABLED, true);
+ int syncInterval = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_SYNC_INTERVAL, "60"));
+
+ // Add enabled/frequency to playlist/podcasts syncing
+ ContentResolver.setSyncAutomatically(account, Constants.SYNC_ACCOUNT_PLAYLIST_AUTHORITY, syncEnabled);
+ ContentResolver.addPeriodicSync(account, Constants.SYNC_ACCOUNT_PLAYLIST_AUTHORITY, new Bundle(), 60L * syncInterval);
+ ContentResolver.setSyncAutomatically(account, Constants.SYNC_ACCOUNT_PODCAST_AUTHORITY, syncEnabled);
+ ContentResolver.addPeriodicSync(account, Constants.SYNC_ACCOUNT_PODCAST_AUTHORITY, new Bundle(), 60L * syncInterval);
+
+ // Add for starred/recently added
+ ContentResolver.setSyncAutomatically(account, Constants.SYNC_ACCOUNT_STARRED_AUTHORITY, (syncEnabled && prefs.getBoolean(Constants.PREFERENCES_KEY_SYNC_STARRED, false)));
+ ContentResolver.addPeriodicSync(account, Constants.SYNC_ACCOUNT_STARRED_AUTHORITY, new Bundle(), 60L * syncInterval);
+ ContentResolver.setSyncAutomatically(account, Constants.SYNC_ACCOUNT_MOST_RECENT_AUTHORITY, (syncEnabled && prefs.getBoolean(Constants.PREFERENCES_KEY_SYNC_MOST_RECENT, false)));
+ ContentResolver.addPeriodicSync(account, Constants.SYNC_ACCOUNT_MOST_RECENT_AUTHORITY, new Bundle(), 60L * syncInterval);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+
+ }
+ }.execute();
+ }
+
+ private void showInfoDialog() {
+ if (!infoDialogDisplayed) {
+ infoDialogDisplayed = true;
+ if (Util.getRestUrl(this, null).contains("demo.subsonic.org")) {
+ Util.info(this, R.string.main_welcome_title, R.string.main_welcome_text);
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/activity/VoiceQueryReceiverActivity.java b/app/src/main/java/github/daneren2005/dsub/activity/VoiceQueryReceiverActivity.java
new file mode 100644
index 00000000..7ae0ba77
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/activity/VoiceQueryReceiverActivity.java
@@ -0,0 +1,61 @@
+/*
+ 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.activity;
+
+import android.app.Activity;
+import android.app.SearchManager;
+import android.content.Intent;
+import android.os.Bundle;
+import android.provider.SearchRecentSuggestions;
+
+import github.daneren2005.dsub.fragments.SubsonicFragment;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.Util;
+import github.daneren2005.dsub.provider.DSubSearchProvider;
+
+/**
+ * Receives voice search queries and forwards to the SearchFragment.
+ *
+ * http://android-developers.blogspot.com/2010/09/supporting-new-music-voice-action.html
+ *
+ * @author Sindre Mehus
+ */
+public class VoiceQueryReceiverActivity extends Activity {
+ private static String GMS_SEARCH_ACTION = "com.google.android.gms.actions.SEARCH_ACTION";
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ String query = getIntent().getStringExtra(SearchManager.QUERY);
+
+ if (query != null) {
+ Intent intent = new Intent(VoiceQueryReceiverActivity.this, SubsonicFragmentActivity.class);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_QUERY, query);
+ if(!GMS_SEARCH_ACTION.equals(getIntent().getAction())) {
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_AUTOPLAY, true);
+ }
+ intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ Util.startActivityWithoutTransition(VoiceQueryReceiverActivity.this, intent);
+ }
+ finish();
+ Util.disablePendingTransition(this);
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/AlbumGridAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/AlbumGridAdapter.java
new file mode 100644
index 00000000..eb187569
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/adapter/AlbumGridAdapter.java
@@ -0,0 +1,73 @@
+/*
+ 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.adapter;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+
+import java.util.List;
+
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.util.ImageLoader;
+import github.daneren2005.dsub.view.AlbumCell;
+
+public class AlbumGridAdapter extends ArrayAdapter<MusicDirectory.Entry> {
+ private final static String TAG = AlbumGridAdapter.class.getSimpleName();
+ private final Context activity;
+ private final ImageLoader imageLoader;
+ private List<MusicDirectory.Entry> entries;
+ private boolean showArtist;
+
+ public AlbumGridAdapter(Context activity, ImageLoader imageLoader, List<MusicDirectory.Entry> entries, boolean showArtist) {
+ super(activity, android.R.layout.simple_list_item_1, entries);
+ this.entries = entries;
+ this.activity = activity;
+ this.imageLoader = imageLoader;
+
+ // Always show artist if they aren't all the same
+ if(!showArtist) {
+ String artist = null;
+ for(MusicDirectory.Entry entry: entries) {
+ if(artist == null) {
+ artist = entry.getArtist();
+ }
+
+ if(artist != null && !artist.equals(entry.getArtist())) {
+ showArtist = true;
+ }
+ }
+ }
+ this.showArtist = showArtist;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ MusicDirectory.Entry entry = getItem(position);
+
+ AlbumCell view;
+ if(convertView instanceof AlbumCell) {
+ view = (AlbumCell) convertView;
+ } else {
+ view = new AlbumCell(activity);
+ }
+
+ view.setShowArtist(showArtist);
+ view.setObject(entry, imageLoader);
+ return view;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/AlbumListAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/AlbumListAdapter.java
new file mode 100644
index 00000000..b2fcded3
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/adapter/AlbumListAdapter.java
@@ -0,0 +1,154 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.adapter;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.SectionIndexer;
+
+import com.commonsware.cwac.endless.EndlessAdapter;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.ServerInfo;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+public class AlbumListAdapter extends EndlessAdapter implements SectionIndexer {
+ Context context;
+ ArrayAdapter<MusicDirectory.Entry> adapter;
+ String type;
+ String extra;
+ int size;
+ int offset;
+ List<MusicDirectory.Entry> entries;
+
+ private boolean shouldIndex = false;
+ private Object[] sections;
+ private Integer[] positions;
+
+ public AlbumListAdapter(Context context, ArrayAdapter<MusicDirectory.Entry> adapter, String type, String extra, int size) {
+ super(adapter);
+ this.context = context;
+ this.adapter = adapter;
+ this.type = type;
+ this.extra = extra;
+ this.size = size;
+ this.offset = size;
+
+ if("alphabeticalByName".equals(this.type)) {
+ shouldIndex = true;
+ recreateIndexes();
+ }
+ }
+
+ @Override
+ protected boolean cacheInBackground() throws Exception {
+ MusicService service = MusicServiceFactory.getMusicService(context);
+ MusicDirectory result;
+ if(("genres".equals(type) && ServerInfo.checkServerVersion(context, "1.10.0")) || "years".equals(type)) {
+ result = service.getAlbumList(type, extra, size, offset, context, null);
+ } else if("genres".equals(type) || "genres-songs".equals(type)) {
+ result = service.getSongsByGenre(extra, size, offset, context, null);
+ } else {
+ result = service.getAlbumList(type, size, offset, context, null);
+ }
+ entries = result.getChildren();
+ return entries.size() > 0;
+ }
+
+ @Override
+ protected void appendCachedData() {
+ for(MusicDirectory.Entry entry: entries) {
+ adapter.add(entry);
+ }
+ offset += entries.size();
+ recreateIndexes();
+ }
+
+ @Override
+ protected View getPendingView(ViewGroup parent) {
+ View progress = LayoutInflater.from(context).inflate(R.layout.tab_progress, null);
+ progress.setVisibility(View.VISIBLE);
+ return progress;
+ }
+
+ private void recreateIndexes() {
+ if(!shouldIndex) {
+ return;
+ }
+
+ Set<String> sectionSet = new LinkedHashSet<String>(30);
+ List<Integer> positionList = new ArrayList<Integer>(30);
+ for (int i = 0; i < adapter.getCount(); i++) {
+ MusicDirectory.Entry entry = adapter.getItem(i);
+ String index = entry.getAlbum().substring(0, 1);
+ if(!Character.isLetter(index.charAt(0))) {
+ index = "#";
+ }
+
+ if (!sectionSet.contains(index)) {
+ sectionSet.add(index);
+ positionList.add(i);
+ }
+ }
+ sections = sectionSet.toArray(new Object[sectionSet.size()]);
+ positions = positionList.toArray(new Integer[positionList.size()]);
+ }
+
+ @Override
+ public Object[] getSections() {
+ if(sections != null) {
+ return sections;
+ } else {
+ return new Object[0];
+ }
+ }
+
+ @Override
+ public int getPositionForSection(int section) {
+ if(sections != null) {
+ section = Math.min(section, positions.length - 1);
+ return positions[section];
+ } else {
+ return 0;
+ }
+ }
+
+ @Override
+ public int getSectionForPosition(int pos) {
+ if(sections != null) {
+ for (int i = 0; i < sections.length - 1; i++) {
+ if (pos < positions[i + 1]) {
+ return i;
+ }
+ }
+ return sections.length - 1;
+ } else {
+ return 0;
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/ArtistAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/ArtistAdapter.java
new file mode 100644
index 00000000..4d469faf
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/adapter/ArtistAdapter.java
@@ -0,0 +1,97 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.adapter;
+
+import android.content.Context;
+import github.daneren2005.dsub.R;
+import java.util.List;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.SectionIndexer;
+import github.daneren2005.dsub.domain.Artist;
+import github.daneren2005.dsub.view.ArtistView;
+
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+/**
+ * @author Sindre Mehus
+ */
+public class ArtistAdapter extends ArrayAdapter<Artist> implements SectionIndexer {
+
+ private final Context activity;
+
+ // Both arrays are indexed by section ID.
+ private final Object[] sections;
+ private final Integer[] positions;
+
+ public ArtistAdapter(Context activity, List<Artist> artists) {
+ super(activity, R.layout.basic_list_item, artists);
+ this.activity = activity;
+
+ Set<String> sectionSet = new LinkedHashSet<String>(30);
+ List<Integer> positionList = new ArrayList<Integer>(30);
+ for (int i = 0; i < artists.size(); i++) {
+ Artist artist = artists.get(i);
+ String index = artist.getIndex();
+ if (!sectionSet.contains(index)) {
+ sectionSet.add(index);
+ positionList.add(i);
+ }
+ }
+ sections = sectionSet.toArray(new Object[sectionSet.size()]);
+ positions = positionList.toArray(new Integer[positionList.size()]);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ Artist entry = getItem(position);
+ ArtistView view;
+ if (convertView != null && convertView instanceof ArtistView) {
+ view = (ArtistView) convertView;
+ } else {
+ view = new ArtistView(activity);
+ }
+ view.setObject(entry);
+ return view;
+ }
+
+ @Override
+ public Object[] getSections() {
+ return sections;
+ }
+
+ @Override
+ public int getPositionForSection(int section) {
+ section = Math.min(section, positions.length - 1);
+ return positions[section];
+ }
+
+ @Override
+ public int getSectionForPosition(int pos) {
+ for (int i = 0; i < sections.length - 1; i++) {
+ if (pos < positions[i + 1]) {
+ return i;
+ }
+ }
+ return sections.length - 1;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/BookmarkAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/BookmarkAdapter.java
new file mode 100644
index 00000000..26d3e16a
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/adapter/BookmarkAdapter.java
@@ -0,0 +1,64 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+*/
+
+package github.daneren2005.dsub.adapter;
+
+import android.content.Context;
+
+import java.util.List;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.TextView;
+
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.Bookmark;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.util.Util;
+import github.daneren2005.dsub.view.SongView;
+
+public class BookmarkAdapter extends ArrayAdapter<MusicDirectory.Entry> {
+ private final static String TAG = BookmarkAdapter.class.getSimpleName();
+ private Context activity;
+
+ public BookmarkAdapter(Context activity, List<MusicDirectory.Entry> bookmarks) {
+ super(activity, android.R.layout.simple_list_item_1, bookmarks);
+ this.activity = activity;
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ MusicDirectory.Entry entry = getItem(position);
+ Bookmark bookmark = entry.getBookmark();
+
+ SongView view;
+ if (convertView != null) {
+ view = (SongView) convertView;
+ } else {
+ view = new SongView(activity);
+ }
+ view.setObject(entry, false);
+
+ // Add current position to duration
+ TextView durationTextView = (TextView) view.findViewById(R.id.song_duration);
+ String duration = durationTextView.getText().toString();
+ durationTextView.setText(Util.formatDuration(bookmark.getPosition() / 1000) + " / " + duration);
+
+ return view;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/ChatAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/ChatAdapter.java
new file mode 100644
index 00000000..0c116d39
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/adapter/ChatAdapter.java
@@ -0,0 +1,109 @@
+package github.daneren2005.dsub.adapter;
+
+import android.text.method.LinkMovementMethod;
+import android.text.util.Linkify;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.activity.SubsonicActivity;
+import github.daneren2005.dsub.domain.ChatMessage;
+import github.daneren2005.dsub.util.ImageLoader;
+import github.daneren2005.dsub.util.UserUtil;
+import github.daneren2005.dsub.util.Util;
+
+import java.text.DateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.regex.Pattern;
+
+public class ChatAdapter extends ArrayAdapter<ChatMessage> {
+
+ private final SubsonicActivity activity;
+ private ArrayList<ChatMessage> messages;
+ private final ImageLoader imageLoader;
+
+ private static final String phoneRegex = "1?\\W*([2-9][0-8][0-9])\\W*([2-9][0-9]{2})\\W*([0-9]{4})"; //you can just place your support phone here
+ private static final Pattern phoneMatcher = Pattern.compile(phoneRegex);
+
+ public ChatAdapter(SubsonicActivity activity, ArrayList<ChatMessage> messages, ImageLoader imageLoader) {
+ super(activity, R.layout.chat_item, messages);
+ this.activity = activity;
+ this.messages = messages;
+ this.imageLoader = imageLoader;
+ }
+
+ @Override
+ public int getCount() {
+ return messages.size();
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ ChatMessage message = this.getItem(position);
+
+ ViewHolder holder;
+ int layout;
+
+ String messageUser = message.getUsername();
+ Date messageTime = new java.util.Date(message.getTime());
+ String messageText = message.getMessage();
+
+ String me = UserUtil.getCurrentUsername(activity);
+
+ if (messageUser.equals(me)) {
+ layout = R.layout.chat_item_reverse;
+ } else {
+ layout = R.layout.chat_item;
+ }
+
+ if (convertView == null)
+ {
+ holder = new ViewHolder();
+
+ convertView = LayoutInflater.from(activity).inflate(layout, parent, false);
+
+ TextView usernameView = (TextView) convertView.findViewById(R.id.chat_username);
+ TextView timeView = (TextView) convertView.findViewById(R.id.chat_time);
+ TextView messageView = (TextView) convertView.findViewById(R.id.chat_message);
+
+ messageView.setMovementMethod(LinkMovementMethod.getInstance());
+ Linkify.addLinks(messageView, Linkify.EMAIL_ADDRESSES);
+ Linkify.addLinks(messageView, Linkify.WEB_URLS);
+ Linkify.addLinks(messageView, phoneMatcher, "tel:");
+
+ holder.message = messageView;
+ holder.username = usernameView;
+ holder.time = timeView;
+ holder.avatar = (ImageView) convertView.findViewById(R.id.chat_avatar);
+
+ convertView.setTag(holder);
+ }
+ else
+ {
+ holder = (ViewHolder) convertView.getTag();
+ }
+
+ DateFormat timeFormat = android.text.format.DateFormat.getTimeFormat(activity);
+ String messageTimeFormatted = String.format("[%s]", timeFormat.format(messageTime));
+
+ holder.username.setText(messageUser);
+ holder.message.setText(messageText);
+ holder.time.setText(messageTimeFormatted);
+
+ imageLoader.loadAvatar(activity, holder.avatar, messageUser);
+
+ return convertView;
+ }
+
+ private static class ViewHolder
+ {
+ TextView message;
+ TextView username;
+ TextView time;
+ ImageView avatar;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/DownloadFileAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/DownloadFileAdapter.java
new file mode 100644
index 00000000..be9b4cb9
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/adapter/DownloadFileAdapter.java
@@ -0,0 +1,49 @@
+/*
+ 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.adapter;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+
+import java.util.List;
+
+import github.daneren2005.dsub.service.DownloadFile;
+import github.daneren2005.dsub.view.SongView;
+
+public class DownloadFileAdapter extends ArrayAdapter<DownloadFile> {
+ Context context;
+
+ public DownloadFileAdapter(Context context, List<DownloadFile> entries) {
+ super(context, android.R.layout.simple_list_item_1, entries);
+ this.context = context;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ SongView view;
+ if (convertView != null && convertView instanceof SongView) {
+ view = (SongView) convertView;
+ } else {
+ view = new SongView(context);
+ }
+ DownloadFile downloadFile = getItem(position);
+ view.setObject(downloadFile.getSong(), false);
+ view.setDownloadFile(downloadFile);
+ return view;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/DrawerAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/DrawerAdapter.java
new file mode 100644
index 00000000..b0a4a33d
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/adapter/DrawerAdapter.java
@@ -0,0 +1,126 @@
+/*
+ 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.adapter;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import java.util.List;
+
+import github.daneren2005.dsub.R;
+
+/**
+ * Created by Scott on 11/8/13.
+ */
+public class DrawerAdapter extends ArrayAdapter<String> {
+ private static String TAG = DrawerAdapter.class.getSimpleName();
+ private Context context;
+ private List<String> items;
+ private List<Integer> icons;
+ private List<Boolean> visible;
+ private int selectedPosition = -1;
+
+ public DrawerAdapter(Context context, List<String> items, List<Integer> icons, List<Boolean> visible) {
+ super(context, R.layout.drawer_list_item, items);
+
+ this.context = context;
+ this.items = items;
+ this.icons = icons;
+ this.visible = visible;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ position = getActualPosition(position);
+ String item = items.get(position);
+ Integer icon = icons.get(position);
+
+ if(convertView == null) {
+ convertView = LayoutInflater.from(context).inflate(R.layout.drawer_list_item, null);
+ }
+
+ TextView textView = (TextView) convertView.findViewById(R.id.drawer_name);
+ textView.setText(item);
+
+ if(selectedPosition == position) {
+ textView.setTextAppearance(context, R.style.DSub_TextViewStyle_Bold);
+ selectedPosition = -1;
+ }
+
+ ImageView iconView = (ImageView) convertView.findViewById(R.id.drawer_icon);
+ iconView.setImageResource(icon);
+
+ return convertView;
+ }
+
+ @Override
+ public int getCount() {
+ int count = 0;
+ for(int i = 0; i < visible.size(); i++) {
+ if(visible.get(i)) {
+ count++;
+ }
+ }
+
+ return count;
+ }
+
+ public int getActualPosition(int position) {
+ for(int i = 0; i <= position; i++) {
+ if(!visible.get(i)) {
+ position++;
+ }
+ }
+
+ return position;
+ }
+ public int getAdapterPosition(int position) {
+ if(!visible.get(position)) {
+ visible.set(position, true);
+ notifyDataSetChanged();
+ }
+
+ for(int i = position; i >= 0; i--) {
+ if(!visible.get(i)) {
+ position--;
+ }
+ }
+
+ return position;
+ }
+
+ public void setItemVisible(int position, boolean visible) {
+ if(this.visible.get(position) != visible) {
+ this.visible.set(position, visible);
+ notifyDataSetInvalidated();
+ }
+ }
+ public void setDownloadVisible(boolean visible) {
+ setItemVisible(items.size() - 2, visible);
+ }
+
+ public void setSelectedPosition(int position) {
+ selectedPosition = position;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/EntryAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/EntryAdapter.java
new file mode 100644
index 00000000..9e506e5a
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/adapter/EntryAdapter.java
@@ -0,0 +1,82 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.adapter;
+
+import android.content.Context;
+
+import java.util.List;
+
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.util.ImageLoader;
+import github.daneren2005.dsub.view.AlbumView;
+import github.daneren2005.dsub.view.ArtistEntryView;
+import github.daneren2005.dsub.view.SongView;
+
+/**
+ * @author Sindre Mehus
+ */
+public class EntryAdapter extends ArrayAdapter<MusicDirectory.Entry> {
+ private final static String TAG = EntryAdapter.class.getSimpleName();
+ private final Context activity;
+ private final ImageLoader imageLoader;
+ private final boolean checkable;
+ private List<MusicDirectory.Entry> entries;
+
+ public EntryAdapter(Context activity, ImageLoader imageLoader, List<MusicDirectory.Entry> entries, boolean checkable) {
+ super(activity, android.R.layout.simple_list_item_1, entries);
+ this.entries = entries;
+ this.activity = activity;
+ this.imageLoader = imageLoader;
+ this.checkable = checkable;
+ }
+
+ public void removeAt(int position) {
+ entries.remove(position);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ MusicDirectory.Entry entry = getItem(position);
+
+ if (entry.isDirectory()) {
+ if(entry.isAlbum()) {
+ AlbumView view;
+ view = new AlbumView(activity);
+ view.setObject(entry, imageLoader);
+ return view;
+ } else {
+ ArtistEntryView view = new ArtistEntryView(activity);
+ view.setObject(entry);
+ return view;
+ }
+ } else {
+ SongView view;
+ if (convertView != null && convertView instanceof SongView) {
+ view = (SongView) convertView;
+ } else {
+ view = new SongView(activity);
+ }
+ view.setObject(entry, checkable);
+ return view;
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/GenreAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/GenreAdapter.java
new file mode 100644
index 00000000..abb208c9
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/adapter/GenreAdapter.java
@@ -0,0 +1,60 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.adapter;
+
+import android.widget.ArrayAdapter;
+import android.widget.SectionIndexer;
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.Genre;
+import github.daneren2005.dsub.view.GenreView;
+
+import java.util.List;
+import java.util.Set;
+import java.util.LinkedHashSet;
+import java.util.ArrayList;
+
+/**
+ * @author Sindre Mehus
+*/
+public class GenreAdapter extends ArrayAdapter<Genre>{
+ private Context activity;
+ private List<Genre> genres;
+
+ public GenreAdapter(Context context, List<Genre> genres) {
+ super(context, android.R.layout.simple_list_item_1, genres);
+ this.activity = context;
+ this.genres = genres;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ Genre genre = genres.get(position);
+ GenreView view;
+ if (convertView != null && convertView instanceof GenreView) {
+ view = (GenreView) convertView;
+ } else {
+ view = new GenreView(activity);
+ }
+ view.setObject(genre);
+ return view;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/MergeAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/MergeAdapter.java
new file mode 100644
index 00000000..a2db4cf0
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/adapter/MergeAdapter.java
@@ -0,0 +1,290 @@
+/***
+ Copyright (c) 2008-2009 CommonsWare, LLC
+ Portions (c) 2009 Google, Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License"); you may
+ not use this file except in compliance with the License. You may obtain
+ a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+package github.daneren2005.dsub.adapter;
+
+import android.database.DataSetObserver;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ListAdapter;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Arrays;
+
+/**
+ * Adapter that merges multiple child adapters and views
+ * into a single contiguous whole.
+ * <p/>
+ * Adapters used as pieces within MergeAdapter must
+ * have view type IDs monotonically increasing from 0. Ideally,
+ * adapters also have distinct ranges for their row ids, as
+ * returned by getItemId().
+ */
+public class MergeAdapter extends BaseAdapter {
+
+ private final CascadeDataSetObserver observer = new CascadeDataSetObserver();
+ private final ArrayList<ListAdapter> pieces = new ArrayList<ListAdapter>();
+
+ /**
+ * Stock constructor, simply chaining to the superclass.
+ */
+ public MergeAdapter() {
+ super();
+ }
+
+ /**
+ * Adds a new adapter to the roster of things to appear
+ * in the aggregate list.
+ *
+ * @param adapter Source for row views for this section
+ */
+ public void addAdapter(ListAdapter adapter) {
+ pieces.add(adapter);
+ adapter.registerDataSetObserver(observer);
+ }
+
+ public void removeAdapter(ListAdapter adapter) {
+ adapter.unregisterDataSetObserver(observer);
+ pieces.remove(adapter);
+ }
+
+ /**
+ * Adds a new View to the roster of things to appear
+ * in the aggregate list.
+ *
+ * @param view Single view to add
+ */
+ public ListAdapter addView(View view) {
+ return addView(view, false);
+ }
+
+ /**
+ * Adds a new View to the roster of things to appear
+ * in the aggregate list.
+ *
+ * @param view Single view to add
+ * @param enabled false if views are disabled, true if enabled
+ */
+ public ListAdapter addView(View view, boolean enabled) {
+ return addViews(Arrays.asList(view), enabled);
+ }
+
+ /**
+ * Adds a list of views to the roster of things to appear
+ * in the aggregate list.
+ *
+ * @param views List of views to add
+ */
+ public ListAdapter addViews(List<View> views) {
+ return addViews(views, false);
+ }
+
+ /**
+ * Adds a list of views to the roster of things to appear
+ * in the aggregate list.
+ *
+ * @param views List of views to add
+ * @param enabled false if views are disabled, true if enabled
+ */
+ public ListAdapter addViews(List<View> views, boolean enabled) {
+ ListAdapter adapter = enabled ? new EnabledSackAdapter(views) : new SackOfViewsAdapter(views);
+ addAdapter(adapter);
+ return adapter;
+ }
+
+ /**
+ * Get the data item associated with the specified
+ * position in the data set.
+ *
+ * @param position Position of the item whose data we want
+ */
+ @Override
+ public Object getItem(int position) {
+ for (ListAdapter piece : pieces) {
+ int size = piece.getCount();
+
+ if (position < size) {
+ return (piece.getItem(position));
+ }
+
+ position -= size;
+ }
+
+ return (null);
+ }
+
+ /**
+ * How many items are in the data set represented by this
+ * Adapter.
+ */
+ @Override
+ public int getCount() {
+ int total = 0;
+
+ for (ListAdapter piece : pieces) {
+ total += piece.getCount();
+ }
+
+ return (total);
+ }
+
+ /**
+ * Returns the number of types of Views that will be
+ * created by getView().
+ */
+ @Override
+ public int getViewTypeCount() {
+ int total = 0;
+
+ for (ListAdapter piece : pieces) {
+ total += piece.getViewTypeCount();
+ }
+
+ return (Math.max(total, 1)); // needed for setListAdapter() before content add'
+ }
+
+ /**
+ * Get the type of View that will be created by getView()
+ * for the specified item.
+ *
+ * @param position Position of the item whose data we want
+ */
+ @Override
+ public int getItemViewType(int position) {
+ int typeOffset = 0;
+ int result = -1;
+
+ for (ListAdapter piece : pieces) {
+ int size = piece.getCount();
+
+ if (position < size) {
+ result = typeOffset + piece.getItemViewType(position);
+ break;
+ }
+
+ position -= size;
+ typeOffset += piece.getViewTypeCount();
+ }
+
+ return (result);
+ }
+
+ /**
+ * Are all items in this ListAdapter enabled? If yes it
+ * means all items are selectable and clickable.
+ */
+ @Override
+ public boolean areAllItemsEnabled() {
+ return (false);
+ }
+
+ /**
+ * Returns true if the item at the specified position is
+ * not a separator.
+ *
+ * @param position Position of the item whose data we want
+ */
+ @Override
+ public boolean isEnabled(int position) {
+ for (ListAdapter piece : pieces) {
+ int size = piece.getCount();
+
+ if (position < size) {
+ return (piece.isEnabled(position));
+ }
+
+ position -= size;
+ }
+
+ return (false);
+ }
+
+ /**
+ * Get a View that displays the data at the specified
+ * position in the data set.
+ *
+ * @param position Position of the item whose data we want
+ * @param convertView View to recycle, if not null
+ * @param parent ViewGroup containing the returned View
+ */
+ @Override
+ public View getView(int position, View convertView,
+ ViewGroup parent) {
+ for (ListAdapter piece : pieces) {
+ int size = piece.getCount();
+
+ if (position < size) {
+
+ return (piece.getView(position, convertView, parent));
+ }
+
+ position -= size;
+ }
+
+ return (null);
+ }
+
+ /**
+ * Get the row id associated with the specified position
+ * in the list.
+ *
+ * @param position Position of the item whose data we want
+ */
+ @Override
+ public long getItemId(int position) {
+ for (ListAdapter piece : pieces) {
+ int size = piece.getCount();
+
+ if (position < size) {
+ return (piece.getItemId(position));
+ }
+
+ position -= size;
+ }
+
+ return (-1);
+ }
+
+ private static class EnabledSackAdapter extends SackOfViewsAdapter {
+ public EnabledSackAdapter(List<View> views) {
+ super(views);
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ return (true);
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ return (true);
+ }
+ }
+
+ private class CascadeDataSetObserver extends DataSetObserver {
+ @Override
+ public void onChanged() {
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public void onInvalidated() {
+ notifyDataSetInvalidated();
+ }
+ }
+}
+
diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/PlaylistAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/PlaylistAdapter.java
new file mode 100644
index 00000000..d56a6b97
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/adapter/PlaylistAdapter.java
@@ -0,0 +1,70 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.adapter;
+
+import android.content.Context;
+import github.daneren2005.dsub.R;
+import java.util.List;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import github.daneren2005.dsub.domain.Playlist;
+import github.daneren2005.dsub.view.PlaylistView;
+
+import java.util.Collections;
+import java.util.Comparator;
+
+/**
+ * @author Sindre Mehus
+ */
+public class PlaylistAdapter extends ArrayAdapter<Playlist> {
+
+ private final Context activity;
+
+ public PlaylistAdapter(Context activity, List<Playlist> Playlists) {
+ super(activity, R.layout.basic_list_item, Playlists);
+ this.activity = activity;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ Playlist entry = getItem(position);
+ PlaylistView view;
+ if (convertView != null && convertView instanceof PlaylistView) {
+ view = (PlaylistView) convertView;
+ } else {
+ view = new PlaylistView(activity);
+ }
+ view.setObject(entry);
+ return view;
+ }
+
+ public static class PlaylistComparator implements Comparator<Playlist> {
+ @Override
+ public int compare(Playlist playlist1, Playlist playlist2) {
+ return playlist1.getName().compareToIgnoreCase(playlist2.getName());
+ }
+
+ public static List<Playlist> sort(List<Playlist> playlists) {
+ Collections.sort(playlists, new PlaylistComparator());
+ return playlists;
+ }
+
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/PodcastChannelAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/PodcastChannelAdapter.java
new file mode 100644
index 00000000..8ee39a10
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/adapter/PodcastChannelAdapter.java
@@ -0,0 +1,60 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.adapter;
+
+import android.widget.ArrayAdapter;
+import android.widget.SectionIndexer;
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.PodcastChannel;
+import github.daneren2005.dsub.view.PodcastChannelView;
+
+import java.util.List;
+import java.util.Set;
+import java.util.LinkedHashSet;
+import java.util.ArrayList;
+
+/**
+ * @author Sindre Mehus
+*/
+public class PodcastChannelAdapter extends ArrayAdapter<PodcastChannel>{
+ private Context activity;
+ private List<PodcastChannel> podcasts;
+
+ public PodcastChannelAdapter(Context context, List<PodcastChannel> podcasts) {
+ super(context, android.R.layout.simple_list_item_1, podcasts);
+ this.activity = context;
+ this.podcasts = podcasts;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ PodcastChannel podcast = podcasts.get(position);
+ PodcastChannelView view;
+ if (convertView != null && convertView instanceof PodcastChannelView) {
+ view = (PodcastChannelView) convertView;
+ } else {
+ view = new PodcastChannelView(activity);
+ }
+ view.setObject(podcast);
+ return view;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/SackOfViewsAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/SackOfViewsAdapter.java
new file mode 100644
index 00000000..e4744cc5
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/adapter/SackOfViewsAdapter.java
@@ -0,0 +1,181 @@
+/***
+ Copyright (c) 2008-2009 CommonsWare, LLC
+ Portions (c) 2009 Google, Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License"); you may
+ not use this file except in compliance with the License. You may obtain
+ a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+package github.daneren2005.dsub.adapter;
+
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ListView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Adapter that simply returns row views from a list.
+ * <p/>
+ * If you supply a size, you must implement newView(), to
+ * create a required view. The adapter will then cache these
+ * views.
+ * <p/>
+ * If you supply a list of views in the constructor, that
+ * list will be used directly. If any elements in the list
+ * are null, then newView() will be called just for those
+ * slots.
+ * <p/>
+ * Subclasses may also wish to override areAllItemsEnabled()
+ * (default: false) and isEnabled() (default: false), if some
+ * of their rows should be selectable.
+ * <p/>
+ * It is assumed each view is unique, and therefore will not
+ * get recycled.
+ * <p/>
+ * Note that this adapter is not designed for long lists. It
+ * is more for screens that should behave like a list. This
+ * is particularly useful if you combine this with other
+ * adapters (e.g., SectionedAdapter) that might have an
+ * arbitrary number of rows, so it all appears seamless.
+ */
+public class SackOfViewsAdapter extends BaseAdapter {
+ private List<View> views = null;
+
+ /**
+ * Constructor creating an empty list of views, but with
+ * a specified count. Subclasses must override newView().
+ */
+ public SackOfViewsAdapter(int count) {
+ super();
+
+ views = new ArrayList<View>(count);
+
+ for (int i = 0; i < count; i++) {
+ views.add(null);
+ }
+ }
+
+ /**
+ * Constructor wrapping a supplied list of views.
+ * Subclasses must override newView() if any of the elements
+ * in the list are null.
+ */
+ public SackOfViewsAdapter(List<View> views) {
+ for (View view : views) {
+ view.setLayoutParams(new ListView.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
+ }
+ this.views = views;
+ }
+
+ /**
+ * Get the data item associated with the specified
+ * position in the data set.
+ *
+ * @param position Position of the item whose data we want
+ */
+ @Override
+ public Object getItem(int position) {
+ return (views.get(position));
+ }
+
+ /**
+ * How many items are in the data set represented by this
+ * Adapter.
+ */
+ @Override
+ public int getCount() {
+ return (views.size());
+ }
+
+ /**
+ * Returns the number of types of Views that will be
+ * created by getView().
+ */
+ @Override
+ public int getViewTypeCount() {
+ return (getCount());
+ }
+
+ /**
+ * Get the type of View that will be created by getView()
+ * for the specified item.
+ *
+ * @param position Position of the item whose data we want
+ */
+ @Override
+ public int getItemViewType(int position) {
+ return (position);
+ }
+
+ /**
+ * Are all items in this ListAdapter enabled? If yes it
+ * means all items are selectable and clickable.
+ */
+ @Override
+ public boolean areAllItemsEnabled() {
+ return (false);
+ }
+
+ /**
+ * Returns true if the item at the specified position is
+ * not a separator.
+ *
+ * @param position Position of the item whose data we want
+ */
+ @Override
+ public boolean isEnabled(int position) {
+ return (false);
+ }
+
+ /**
+ * Get a View that displays the data at the specified
+ * position in the data set.
+ *
+ * @param position Position of the item whose data we want
+ * @param convertView View to recycle, if not null
+ * @param parent ViewGroup containing the returned View
+ */
+ @Override
+ public View getView(int position, View convertView,
+ ViewGroup parent) {
+ View result = views.get(position);
+
+ if (result == null) {
+ result = newView(position, parent);
+ views.set(position, result);
+ }
+
+ return (result);
+ }
+
+ /**
+ * Get the row id associated with the specified position
+ * in the list.
+ *
+ * @param position Position of the item whose data we want
+ */
+ @Override
+ public long getItemId(int position) {
+ return (position);
+ }
+
+ /**
+ * Create a new View to go into the list at the specified
+ * position.
+ *
+ * @param position Position of the item whose data we want
+ * @param parent ViewGroup containing the returned View
+ */
+ protected View newView(int position, ViewGroup parent) {
+ throw new RuntimeException("You must override newView()!");
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/SettingsAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/SettingsAdapter.java
new file mode 100644
index 00000000..45c3ead1
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/adapter/SettingsAdapter.java
@@ -0,0 +1,59 @@
+/*
+ This file is part of Subsonic.
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+ Copyright 2014 (C) Scott Jackson
+*/
+
+package github.daneren2005.dsub.adapter;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+
+import java.util.List;
+
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.User;
+import github.daneren2005.dsub.view.SettingView;
+
+import static github.daneren2005.dsub.domain.User.Setting;
+
+public class SettingsAdapter extends ArrayAdapter<Setting> {
+ private final Context context;
+ private final boolean editable;
+
+ public SettingsAdapter(Context context, User user, boolean editable) {
+ super(context, R.layout.basic_list_item, user.getSettings());
+ this.context = context;
+ this.editable = editable;
+ }
+
+ public SettingsAdapter(Context context, List<Setting> settings, boolean editable) {
+ super(context, R.layout.basic_list_item, settings);
+ this.context = context;
+ this.editable = editable;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ Setting entry = getItem(position);
+ SettingView view;
+ if (convertView != null && convertView instanceof SettingView) {
+ view = (SettingView) convertView;
+ } else {
+ view = new SettingView(context);
+ }
+ view.setObject(entry, editable);
+ return view;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/ShareAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/ShareAdapter.java
new file mode 100644
index 00000000..4121a85a
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/adapter/ShareAdapter.java
@@ -0,0 +1,56 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.adapter;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+
+import java.util.List;
+
+import github.daneren2005.dsub.domain.Share;
+import github.daneren2005.dsub.view.ShareView;
+
+/**
+ * @author Sindre Mehus
+*/
+public class ShareAdapter extends ArrayAdapter<Share>{
+ private Context activity;
+ private List<Share> shares;
+
+ public ShareAdapter(Context context, List<Share> shares) {
+ super(context, android.R.layout.simple_list_item_1, shares);
+ this.activity = context;
+ this.shares = shares;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ Share share = shares.get(position);
+ ShareView view;
+ if (convertView != null && convertView instanceof ShareView) {
+ view = (ShareView) convertView;
+ } else {
+ view = new ShareView(activity);
+ }
+ view.setObject(share);
+ return view;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/UserAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/UserAdapter.java
new file mode 100644
index 00000000..f0f78d97
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/adapter/UserAdapter.java
@@ -0,0 +1,52 @@
+/*
+ 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.adapter;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+
+import java.util.List;
+
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.User;
+import github.daneren2005.dsub.util.ImageLoader;
+import github.daneren2005.dsub.view.UserView;
+
+public class UserAdapter extends ArrayAdapter<User> {
+ private final Context activity;
+ private final ImageLoader imageLoader;
+
+ public UserAdapter(Context activity, List<User> users, ImageLoader imageLoader) {
+ super(activity, R.layout.basic_list_item, users);
+ this.activity = activity;
+ this.imageLoader = imageLoader;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ User entry = getItem(position);
+ UserView view;
+ if (convertView != null && convertView instanceof UserView) {
+ view = (UserView) convertView;
+ } else {
+ view = new UserView(activity);
+ }
+ view.setObject(entry, imageLoader);
+ return view;
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/github/daneren2005/dsub/audiofx/AudioEffectsController.java b/app/src/main/java/github/daneren2005/dsub/audiofx/AudioEffectsController.java
new file mode 100644
index 00000000..1933bd64
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/audiofx/AudioEffectsController.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 2014 (C) Scott Jackson
+*/
+package github.daneren2005.dsub.audiofx;
+
+import android.content.Context;
+import android.media.MediaPlayer;
+import android.media.audiofx.AudioEffect;
+import android.media.audiofx.LoudnessEnhancer;
+import android.os.Build;
+import android.util.Log;
+
+public class AudioEffectsController {
+ private static final String TAG = AudioEffectsController.class.getSimpleName();
+
+ private final Context context;
+ private int audioSessionId = 0;
+
+ private boolean available = false;
+
+ private EqualizerController equalizerController;
+
+ public AudioEffectsController(Context context, int audioSessionId) {
+ this.context = context;
+ this.audioSessionId = audioSessionId;
+
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
+ available = true;
+ }
+ }
+
+ public boolean isAvailable() {
+ return available;
+ }
+
+ public void release() {
+ if(equalizerController != null) {
+ equalizerController.release();
+ }
+ }
+
+ public EqualizerController getEqualizerController() {
+ if (available && equalizerController == null) {
+ equalizerController = new EqualizerController(context, audioSessionId);
+ if (!equalizerController.isAvailable()) {
+ equalizerController = null;
+ } else {
+ equalizerController.loadSettings();
+ }
+ }
+ return equalizerController;
+ }
+}
+
diff --git a/app/src/main/java/github/daneren2005/dsub/audiofx/EqualizerController.java b/app/src/main/java/github/daneren2005/dsub/audiofx/EqualizerController.java
new file mode 100644
index 00000000..f170af0b
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/audiofx/EqualizerController.java
@@ -0,0 +1,198 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2011 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.audiofx;
+
+import java.io.Serializable;
+
+import android.content.Context;
+import android.media.audiofx.BassBoost;
+import android.media.audiofx.Equalizer;
+import android.os.Build;
+import android.util.Log;
+import github.daneren2005.dsub.util.FileUtil;
+
+/**
+ * Backward-compatible wrapper for {@link Equalizer}, which is API Level 9.
+ *
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class EqualizerController {
+
+ private static final String TAG = EqualizerController.class.getSimpleName();
+
+ private final Context context;
+ private Equalizer equalizer;
+ private BassBoost bass;
+ private boolean loudnessAvailable = false;
+ private LoudnessEnhancerController loudnessEnhancerController;
+ private boolean released = false;
+ private int audioSessionId = 0;
+
+ public EqualizerController(Context context, int audioSessionId) {
+ this.context = context;
+ this.audioSessionId = audioSessionId;
+ init();
+ }
+
+ private void init() {
+ equalizer = new Equalizer(0, audioSessionId);
+ bass = new BassBoost(0, audioSessionId);
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ loudnessAvailable = true;
+ loudnessEnhancerController = new LoudnessEnhancerController(context, audioSessionId);
+ }
+ }
+
+ public void saveSettings() {
+ try {
+ if (isAvailable()) {
+ FileUtil.serialize(context, new EqualizerSettings(equalizer, bass, loudnessEnhancerController), "equalizer.dat");
+ }
+ } catch (Throwable x) {
+ Log.w(TAG, "Failed to save equalizer settings.", x);
+ }
+ }
+
+ public void loadSettings() {
+ try {
+ if (isAvailable()) {
+ EqualizerSettings settings = FileUtil.deserialize(context, "equalizer.dat", EqualizerSettings.class);
+ if (settings != null) {
+ settings.apply(equalizer, bass, loudnessEnhancerController);
+ }
+ }
+ } catch (Throwable x) {
+ Log.w(TAG, "Failed to load equalizer settings.", x);
+ }
+ }
+
+ public boolean isAvailable() {
+ return equalizer != null && bass != null;
+ }
+
+ public boolean isEnabled() {
+ try {
+ return isAvailable() && equalizer.getEnabled();
+ } catch(Exception e) {
+ return false;
+ }
+ }
+
+ public void release() {
+ if (isAvailable()) {
+ released = true;
+ equalizer.release();
+ bass.release();
+ if(loudnessEnhancerController != null && loudnessEnhancerController.isAvailable()) {
+ loudnessEnhancerController.release();
+ }
+ }
+ }
+
+ public Equalizer getEqualizer() {
+ if(released) {
+ released = false;
+ try {
+ init();
+ } catch (Throwable x) {
+ equalizer = null;
+ released = true;
+ Log.w(TAG, "Failed to create equalizer.", x);
+ }
+ }
+ return equalizer;
+ }
+ public BassBoost getBassBoost() {
+ if(released) {
+ released = false;
+ try {
+ init();
+ } catch (Throwable x) {
+ bass = null;
+ Log.w(TAG, "Failed to create bass booster.", x);
+ }
+ }
+ return bass;
+ }
+ public LoudnessEnhancerController getLoudnessEnhancerController() {
+ if(loudnessAvailable && released) {
+ released = false;
+ try {
+ init();
+ } catch (Throwable x) {
+ loudnessEnhancerController = null;
+ Log.w(TAG, "Failed to create loudness enhancer.", x);
+ }
+ }
+ return loudnessEnhancerController;
+ }
+
+ private static class EqualizerSettings implements Serializable {
+
+ private short[] bandLevels;
+ private short preset;
+ private boolean enabled;
+ private short bass;
+ private int loudness;
+
+ public EqualizerSettings() {
+
+ }
+ public EqualizerSettings(Equalizer equalizer, BassBoost boost, LoudnessEnhancerController loudnessEnhancerController) {
+ enabled = equalizer.getEnabled();
+ bandLevels = new short[equalizer.getNumberOfBands()];
+ for (short i = 0; i < equalizer.getNumberOfBands(); i++) {
+ bandLevels[i] = equalizer.getBandLevel(i);
+ }
+ try {
+ preset = equalizer.getCurrentPreset();
+ } catch (Exception x) {
+ preset = -1;
+ }
+ try {
+ bass = boost.getRoundedStrength();
+ } catch(Exception e) {
+ bass = 0;
+ }
+
+ try {
+ loudness = (int) loudnessEnhancerController.getGain();
+ } catch(Exception e) {
+ loudness = 0;
+ }
+ }
+
+ public void apply(Equalizer equalizer, BassBoost boost, LoudnessEnhancerController loudnessController) {
+ for (short i = 0; i < bandLevels.length; i++) {
+ equalizer.setBandLevel(i, bandLevels[i]);
+ }
+ equalizer.setEnabled(enabled);
+ if(bass != 0) {
+ boost.setEnabled(true);
+ boost.setStrength(bass);
+ }
+ if(loudness != 0) {
+ loudnessController.enable();
+ loudnessController.setGain(loudness);
+ }
+ }
+ }
+}
+
diff --git a/app/src/main/java/github/daneren2005/dsub/audiofx/LoudnessEnhancerController.java b/app/src/main/java/github/daneren2005/dsub/audiofx/LoudnessEnhancerController.java
new file mode 100644
index 00000000..df6fdb1c
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/audiofx/LoudnessEnhancerController.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 2014 (C) Scott Jackson
+*/
+package github.daneren2005.dsub.audiofx;
+
+import android.content.Context;
+import android.media.audiofx.LoudnessEnhancer;
+import android.util.Log;
+
+public class LoudnessEnhancerController {
+ private static final String TAG = LoudnessEnhancerController.class.getSimpleName();
+
+ private final Context context;
+ private LoudnessEnhancer enhancer;
+ private boolean released = false;
+ private int audioSessionId = 0;
+
+ public LoudnessEnhancerController(Context context, int audioSessionId) {
+ this.context = context;
+ try {
+ this.audioSessionId = audioSessionId;
+ enhancer = new LoudnessEnhancer(audioSessionId);
+ } catch (Throwable x) {
+ Log.w(TAG, "Failed to create enhancer", x);
+ }
+ }
+
+ public boolean isAvailable() {
+ return enhancer != null;
+ }
+
+ public boolean isEnabled() {
+ try {
+ return isAvailable() && enhancer.getEnabled();
+ } catch(Exception e) {
+ return false;
+ }
+ }
+
+ public void enable() {
+ enhancer.setEnabled(true);
+ }
+ public void disable() {
+ enhancer.setEnabled(false);
+ }
+
+ public float getGain() {
+ return enhancer.getTargetGain();
+ }
+ public void setGain(int gain) {
+ enhancer.setTargetGain(gain);
+ }
+
+ public void release() {
+ if (isAvailable()) {
+ enhancer.release();
+ released = true;
+ }
+ }
+
+}
+
diff --git a/app/src/main/java/github/daneren2005/dsub/domain/Artist.java b/app/src/main/java/github/daneren2005/dsub/domain/Artist.java
new file mode 100644
index 00000000..f30147e6
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/domain/Artist.java
@@ -0,0 +1,145 @@
+/*
+ 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.domain;
+
+import android.util.Log;
+
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * @author Sindre Mehus
+ */
+public class Artist implements Serializable {
+ private static final String TAG = Artist.class.getSimpleName();
+
+ private String id;
+ private String name;
+ private String index;
+ private boolean starred;
+ private int closeness;
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getIndex() {
+ return index;
+ }
+
+ public void setIndex(String index) {
+ this.index = index;
+ }
+
+ public boolean isStarred() {
+ return starred;
+ }
+
+ public void setStarred(boolean starred) {
+ this.starred = starred;
+ }
+
+ public int getCloseness() {
+ return closeness;
+ }
+
+ public void setCloseness(int closeness) {
+ this.closeness = closeness;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ Artist entry = (Artist) o;
+ return id.equals(entry.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return id.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+
+ public static class ArtistComparator implements Comparator<Artist> {
+ private String[] ignoredArticles;
+
+ public ArtistComparator(String[] ignoredArticles) {
+ this.ignoredArticles = ignoredArticles;
+ }
+
+ public int compare(Artist lhsArtist, Artist rhsArtist) {
+ String lhs = lhsArtist.getName().toLowerCase();
+ String rhs = rhsArtist.getName().toLowerCase();
+
+ char lhs1 = lhs.charAt(0);
+ char rhs1 = rhs.charAt(0);
+
+ if (Character.isDigit(lhs1) && !Character.isDigit(rhs1)) {
+ return 1;
+ } else if (Character.isDigit(rhs1) && !Character.isDigit(lhs1)) {
+ return -1;
+ }
+
+ for (String article : ignoredArticles) {
+ int index = lhs.indexOf(article.toLowerCase() + " ");
+ if (index == 0) {
+ lhs = lhs.substring(article.length() + 1);
+ }
+ index = rhs.indexOf(article.toLowerCase() + " ");
+ if (index == 0) {
+ rhs = rhs.substring(article.length() + 1);
+ }
+ }
+
+ return lhs.compareTo(rhs);
+ }
+ }
+
+ public static void sort(List<Artist> artists, String[] ignoredArticles) {
+ try {
+ Collections.sort(artists, new ArtistComparator(ignoredArticles));
+ } catch (Exception e) {
+ Log.w(TAG, "Failed to sort artists", e);
+ }
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/github/daneren2005/dsub/domain/ArtistInfo.java b/app/src/main/java/github/daneren2005/dsub/domain/ArtistInfo.java
new file mode 100644
index 00000000..2205d561
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/domain/ArtistInfo.java
@@ -0,0 +1,76 @@
+/*
+ This file is part of Subsonic.
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+ Copyright 2014 (C) Scott Jackson
+*/
+
+package github.daneren2005.dsub.domain;
+
+import java.io.Serializable;
+import java.util.List;
+
+public class ArtistInfo implements Serializable {
+ private String biography;
+ private String musicBrainzId;
+ private String lastFMUrl;
+ private String imageUrl;
+ private List<Artist> similarArtists;
+ private List<String> missingArtists;
+
+ public String getBiography() {
+ return biography;
+ }
+
+ public void setBiography(String biography) {
+ this.biography = biography;
+ }
+
+ public String getMusicBrainzId() {
+ return musicBrainzId;
+ }
+
+ public void setMusicBrainzId(String musicBrainzId) {
+ this.musicBrainzId = musicBrainzId;
+ }
+
+ public String getLastFMUrl() {
+ return lastFMUrl;
+ }
+
+ public void setLastFMUrl(String lastFMUrl) {
+ this.lastFMUrl = lastFMUrl;
+ }
+
+ public String getImageUrl() {
+ return imageUrl;
+ }
+
+ public void setImageUrl(String imageUrl) {
+ this.imageUrl = imageUrl;
+ }
+
+ public List<Artist> getSimilarArtists() {
+ return similarArtists;
+ }
+
+ public void setSimilarArtists(List<Artist> similarArtists) {
+ this.similarArtists = similarArtists;
+ }
+
+ public List<String> getMissingArtists() {
+ return missingArtists;
+ }
+
+ public void setMissingArtists(List<String> missingArtists) {
+ this.missingArtists = missingArtists;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/domain/Bookmark.java b/app/src/main/java/github/daneren2005/dsub/domain/Bookmark.java
new file mode 100644
index 00000000..3c0c5835
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/domain/Bookmark.java
@@ -0,0 +1,105 @@
+/*
+ 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 2013 (C) Scott Jackson
+ */
+package github.daneren2005.dsub.domain;
+
+import java.io.Serializable;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+/**
+ * Created by Scott on 11/4/13.
+ */
+public class Bookmark implements Serializable {
+ private int position;
+ private String username;
+ private String comment;
+ private Date created;
+ private Date changed;
+
+ public Bookmark() {
+
+ }
+ public Bookmark(int position) {
+ this.position = position;
+ }
+
+ public int getPosition() {
+ return position;
+ }
+
+ public void setPosition(int position) {
+ this.position = position;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public String getComment() {
+ return comment;
+ }
+
+ public void setComment(String comment) {
+ this.comment = comment;
+ }
+
+ public Date getCreated() {
+ return created;
+ }
+
+ public void setCreated(String created) {
+ if (created != null) {
+ try {
+ this.created = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH).parse(created);
+ } catch (ParseException e) {
+ this.created = null;
+ }
+ } else {
+ this.created = null;
+ }
+ }
+ public void setCreated(Date created) {
+ this.created = created;
+ }
+
+ public Date getChanged() {
+ return changed;
+ }
+
+ public void setChanged(String changed) {
+ if (changed != null) {
+ try {
+ this.changed = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH).parse(changed);
+ } catch (ParseException e) {
+ this.changed = null;
+ }
+ } else {
+ this.changed = null;
+ }
+ }
+ public void setChanged(Date changed) {
+ this.changed = changed;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/domain/ChatMessage.java b/app/src/main/java/github/daneren2005/dsub/domain/ChatMessage.java
new file mode 100644
index 00000000..04b9effc
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/domain/ChatMessage.java
@@ -0,0 +1,51 @@
+/*
+ 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.domain;
+
+import java.io.Serializable;
+
+public class ChatMessage implements Serializable {
+ private String username;
+ private Long time;
+ private String message;
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public Long getTime() {
+ return time;
+ }
+
+ public void setTime(Long time) {
+ this.time = time;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public void setMessage(String message) {
+ this.message = message;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/domain/DLNADevice.java b/app/src/main/java/github/daneren2005/dsub/domain/DLNADevice.java
new file mode 100644
index 00000000..d50a398a
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/domain/DLNADevice.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 2014 (C) Scott Jackson
+*/
+
+package github.daneren2005.dsub.domain;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import org.fourthline.cling.model.meta.Device;
+
+/**
+ * Created by Scott on 11/1/2014.
+ */
+public class DLNADevice implements Parcelable {
+ public Device renderer;
+ public String id;
+ public String name;
+ public String description;
+ public int volume;
+ public int volumeMax;
+
+ public static final Parcelable.Creator<DLNADevice> CREATOR = new Parcelable.Creator<DLNADevice>() {
+ public DLNADevice createFromParcel(Parcel in) {
+ return new DLNADevice(in);
+ }
+
+ public DLNADevice[] newArray(int size) {
+ return new DLNADevice[size];
+ }
+ };
+
+ private DLNADevice(Parcel in) {
+ id = in.readString();
+ name = in.readString();
+ description = in.readString();
+ volume = in.readInt();
+ volumeMax = in.readInt();
+ }
+
+ public DLNADevice(Device renderer, String id, String name, String description, int volume, int volumeMax) {
+ this.renderer = renderer;
+ this.id = id;
+ this.name = name;
+ this.description = description;
+ this.volume = volume;
+ this.volumeMax = volumeMax;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(id);
+ dest.writeString(name);
+ dest.writeString(description);
+ dest.writeInt(volume);
+ dest.writeInt(volumeMax);
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/domain/Genre.java b/app/src/main/java/github/daneren2005/dsub/domain/Genre.java
new file mode 100644
index 00000000..4b6ac344
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/domain/Genre.java
@@ -0,0 +1,69 @@
+package github.daneren2005.dsub.domain;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.Util;
+
+public class Genre implements Serializable {
+ private String name;
+ private String index;
+ private Integer albumCount;
+ private Integer songCount;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getIndex() {
+ return index;
+ }
+
+ public void setIndex(String index) {
+ this.index = index;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+
+ public Integer getAlbumCount() {
+ return albumCount;
+ }
+
+ public void setAlbumCount(Integer albumCount) {
+ this.albumCount = albumCount;
+ }
+
+ public Integer getSongCount() {
+ return songCount;
+ }
+
+ public void setSongCount(Integer songCount) {
+ this.songCount = songCount;
+ }
+
+ public static class GenreComparator implements Comparator<Genre> {
+ @Override
+ public int compare(Genre genre1, Genre genre2) {
+ return genre1.getName().compareToIgnoreCase(genre2.getName());
+ }
+
+ public static List<Genre> sort(List<Genre> genres) {
+ Collections.sort(genres, new GenreComparator());
+ return genres;
+ }
+
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/domain/Indexes.java b/app/src/main/java/github/daneren2005/dsub/domain/Indexes.java
new file mode 100644
index 00000000..e15ccf9f
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/domain/Indexes.java
@@ -0,0 +1,94 @@
+/*
+ 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.domain;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.io.Serializable;
+
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.Util;
+
+/**
+ * @author Sindre Mehus
+ */
+public class Indexes implements Serializable {
+
+ private long lastModified;
+ private List<Artist> shortcuts;
+ private List<Artist> artists;
+ private List<MusicDirectory.Entry> entries;
+
+ public Indexes() {
+
+ }
+ public Indexes(long lastModified, List<Artist> shortcuts, List<Artist> artists) {
+ this.lastModified = lastModified;
+ this.shortcuts = shortcuts;
+ this.artists = artists;
+ this.entries = new ArrayList<MusicDirectory.Entry>();
+ }
+ public Indexes(long lastModified, List<Artist> shortcuts, List<Artist> artists, List<MusicDirectory.Entry> entries) {
+ this.lastModified = lastModified;
+ this.shortcuts = shortcuts;
+ this.artists = artists;
+ this.entries = entries;
+ if(!entries.isEmpty()) {
+ Artist root = new Artist();
+ root.setId("root");
+ root.setName("Root");
+ root.setIndex("#");
+ artists.add(root);
+ }
+ }
+
+ public long getLastModified() {
+ return lastModified;
+ }
+
+ public List<Artist> getShortcuts() {
+ return shortcuts;
+ }
+
+ public List<Artist> getArtists() {
+ return artists;
+ }
+
+ public void setArtists(List<Artist> artists) {
+ this.shortcuts = new ArrayList<Artist>();
+ this.artists.clear();
+ this.artists.addAll(artists);
+ }
+
+ public List<MusicDirectory.Entry> getEntries() {
+ return entries;
+ }
+
+ public void sortChildren(Context context) {
+ SharedPreferences prefs = Util.getPreferences(context);
+ String ignoredArticlesString = prefs.getString(Constants.CACHE_KEY_IGNORE, "The El La Los Las Le Les");
+ final String[] ignoredArticles = ignoredArticlesString.split(" ");
+
+ Artist.sort(shortcuts, ignoredArticles);
+ Artist.sort(artists, ignoredArticles);
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/github/daneren2005/dsub/domain/Lyrics.java b/app/src/main/java/github/daneren2005/dsub/domain/Lyrics.java
new file mode 100644
index 00000000..5272920d
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/domain/Lyrics.java
@@ -0,0 +1,57 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.domain;
+
+import java.io.Serializable;
+
+/**
+ * Song lyrics.
+ *
+ * @author Sindre Mehus
+ */
+public class Lyrics implements Serializable {
+
+ private String artist;
+ private String title;
+ private String text;
+
+ public String getArtist() {
+ return artist;
+ }
+
+ public void setArtist(String artist) {
+ this.artist = artist;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getText() {
+ return text;
+ }
+
+ public void setText(String text) {
+ this.text = text;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/domain/MusicDirectory.java b/app/src/main/java/github/daneren2005/dsub/domain/MusicDirectory.java
new file mode 100644
index 00000000..ad819763
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/domain/MusicDirectory.java
@@ -0,0 +1,559 @@
+/*
+ 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.domain;
+
+import android.content.Context;
+import android.media.MediaMetadataRetriever;
+import android.util.Log;
+import java.util.ArrayList;
+import java.util.List;
+import java.io.File;
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.Comparator;
+
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.Util;
+
+/**
+ * @author Sindre Mehus
+ */
+public class MusicDirectory implements Serializable {
+ private static final String TAG = MusicDirectory.class.getSimpleName();
+
+ private String name;
+ private String id;
+ private String parent;
+ private List<Entry> children;
+
+ public MusicDirectory() {
+ children = new ArrayList<Entry>();
+ }
+ public MusicDirectory(List<Entry> children) {
+ this.children = children;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getParent() {
+ return parent;
+ }
+
+ public void setParent(String parent) {
+ this.parent = parent;
+ }
+
+ public void addChild(Entry child) {
+ if(child != null) {
+ children.add(child);
+ }
+ }
+ public void addChildren(List<Entry> children) {
+ this.children.addAll(children);
+ }
+
+ public void replaceChildren(List<Entry> children) {
+ this.children = children;
+ }
+
+ public List<Entry> getChildren() {
+ return getChildren(true, true);
+ }
+
+ public List<Entry> getChildren(boolean includeDirs, boolean includeFiles) {
+ if (includeDirs && includeFiles) {
+ return children;
+ }
+
+ List<Entry> result = new ArrayList<Entry>(children.size());
+ for (Entry child : children) {
+ if (child != null && child.isDirectory() && includeDirs || !child.isDirectory() && includeFiles) {
+ result.add(child);
+ }
+ }
+ return result;
+ }
+
+ public int getChildrenSize() {
+ return children.size();
+ }
+
+ public void shuffleChildren() {
+ Collections.shuffle(this.children);
+ }
+
+ public void sortChildren(Context context, int instance) {
+ if(ServerInfo.checkServerVersion(context, "1.8", instance)) {
+ sortChildren(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_CUSTOM_SORT_ENABLED, true));
+ }
+ }
+ public void sortChildren(boolean byYear) {
+ EntryComparator.sort(children, byYear);
+ }
+
+ public static class Entry implements Serializable {
+ public static final int TYPE_SONG = 0;
+ public static final int TYPE_PODCAST = 1;
+ public static final int TYPE_AUDIO_BOOK = 2;
+
+ private String id;
+ private String parent;
+ private String grandParent;
+ private String albumId;
+ private String artistId;
+ private boolean directory;
+ private String title;
+ private String album;
+ private String artist;
+ private Integer track;
+ private Integer year;
+ private String genre;
+ private String contentType;
+ private String suffix;
+ private String transcodedContentType;
+ private String transcodedSuffix;
+ private String coverArt;
+ private Long size;
+ private Integer duration;
+ private Integer bitRate;
+ private String path;
+ private boolean video;
+ private Integer discNumber;
+ private boolean starred;
+ private Integer rating;
+ private Bookmark bookmark;
+ private int type = 0;
+ private int closeness;
+
+ public Entry() {
+
+ }
+ public Entry(String id) {
+ this.id = id;
+ }
+ public Entry(Artist artist) {
+ this.id = artist.getId();
+ this.title = artist.getName();
+ this.directory = true;
+ }
+
+ public void loadMetadata(File file) {
+ try {
+ MediaMetadataRetriever metadata = new MediaMetadataRetriever();
+ metadata.setDataSource(file.getAbsolutePath());
+ String discNumber = metadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DISC_NUMBER);
+ if(discNumber == null) {
+ discNumber = "1/1";
+ }
+ int slashIndex = discNumber.indexOf("/");
+ if(slashIndex > 0) {
+ discNumber = discNumber.substring(0, slashIndex);
+ }
+ try {
+ setDiscNumber(Integer.parseInt(discNumber));
+ } catch(Exception e) {
+ Log.w(TAG, "Non numbers in disc field!");
+ }
+ String bitrate = metadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE);
+ setBitRate(Integer.parseInt((bitrate != null) ? bitrate : "0") / 1000);
+ String length = metadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
+ setDuration(Integer.parseInt(length) / 1000);
+ String artist = metadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST);
+ if(artist != null) {
+ setArtist(artist);
+ }
+ String album = metadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM);
+ if(album != null) {
+ setAlbum(album);
+ }
+ metadata.release();
+ } catch(Exception e) {
+ Log.i(TAG, "Device doesn't properly support MediaMetadataRetreiver", e);
+ }
+ }
+ public void rebaseTitleOffPath() {
+ try {
+ String filename = getPath();
+ int index = filename.lastIndexOf('/');
+ if (index != -1) {
+ filename = filename.substring(index + 1);
+ if (getTrack() != null) {
+ filename = filename.replace(String.format("%02d ", getTrack()), "");
+ }
+
+ index = filename.lastIndexOf('.');
+ if(index != -1) {
+ filename = filename.substring(0, index);
+ }
+
+ setTitle(filename);
+ }
+ } catch(Exception e) {
+ Log.w(TAG, "Failed to update title based off of path", e);
+ }
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getParent() {
+ return parent;
+ }
+
+ public void setParent(String parent) {
+ this.parent = parent;
+ }
+
+ public String getGrandParent() {
+ return grandParent;
+ }
+
+ public void setGrandParent(String grandParent) {
+ this.grandParent = grandParent;
+ }
+
+ public String getAlbumId() {
+ return albumId;
+ }
+
+ public void setAlbumId(String albumId) {
+ this.albumId = albumId;
+ }
+
+ public String getArtistId() {
+ return artistId;
+ }
+
+ public void setArtistId(String artistId) {
+ this.artistId = artistId;
+ }
+
+ public boolean isDirectory() {
+ return directory;
+ }
+
+ public void setDirectory(boolean directory) {
+ this.directory = directory;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getAlbum() {
+ return album;
+ }
+
+ public boolean isAlbum() {
+ return getParent() != null || getArtist() != null;
+ }
+
+ public String getAlbumDisplay() {
+ if(album != null && title.startsWith("Disc ")) {
+ return album;
+ } else {
+ return title;
+ }
+ }
+
+ public void setAlbum(String album) {
+ this.album = album;
+ }
+
+ public String getArtist() {
+ return artist;
+ }
+
+ public void setArtist(String artist) {
+ this.artist = artist;
+ }
+
+ public Integer getTrack() {
+ return track;
+ }
+
+ public void setTrack(Integer track) {
+ this.track = track;
+ }
+
+ public Integer getYear() {
+ return year;
+ }
+
+ public void setYear(Integer year) {
+ this.year = year;
+ }
+
+ public String getGenre() {
+ return genre;
+ }
+
+ public void setGenre(String genre) {
+ this.genre = genre;
+ }
+
+ public String getContentType() {
+ return contentType;
+ }
+
+ public void setContentType(String contentType) {
+ this.contentType = contentType;
+ }
+
+ public String getSuffix() {
+ return suffix;
+ }
+
+ public void setSuffix(String suffix) {
+ this.suffix = suffix;
+ }
+
+ public String getTranscodedContentType() {
+ return transcodedContentType;
+ }
+
+ public void setTranscodedContentType(String transcodedContentType) {
+ this.transcodedContentType = transcodedContentType;
+ }
+
+ public String getTranscodedSuffix() {
+ return transcodedSuffix;
+ }
+
+ public void setTranscodedSuffix(String transcodedSuffix) {
+ this.transcodedSuffix = transcodedSuffix;
+ }
+
+ public Long getSize() {
+ return size;
+ }
+
+ public void setSize(Long size) {
+ this.size = size;
+ }
+
+ public Integer getDuration() {
+ return duration;
+ }
+
+ public void setDuration(Integer duration) {
+ this.duration = duration;
+ }
+
+ public Integer getBitRate() {
+ return bitRate;
+ }
+
+ public void setBitRate(Integer bitRate) {
+ this.bitRate = bitRate;
+ }
+
+ public String getCoverArt() {
+ return coverArt;
+ }
+
+ public void setCoverArt(String coverArt) {
+ this.coverArt = coverArt;
+ }
+
+ public String getPath() {
+ return path;
+ }
+
+ public void setPath(String path) {
+ this.path = path;
+ }
+
+ public boolean isVideo() {
+ return video;
+ }
+
+ public void setVideo(boolean video) {
+ this.video = video;
+ }
+
+ public Integer getDiscNumber() {
+ return discNumber;
+ }
+
+ public void setDiscNumber(Integer discNumber) {
+ this.discNumber = discNumber;
+ }
+
+ public boolean isStarred() {
+ return starred;
+ }
+
+ public void setStarred(boolean starred) {
+ this.starred = starred;
+ }
+
+ public int getRating() {
+ return rating == null ? 0 : rating;
+ }
+ public void setRating(Integer rating) {
+ if(rating == null || rating == 0) {
+ this.rating = null;
+ } else {
+ this.rating = rating;
+ }
+ }
+
+ public Bookmark getBookmark() {
+ return bookmark;
+ }
+ public void setBookmark(Bookmark bookmark) {
+ this.bookmark = bookmark;
+ }
+
+ public int getType() {
+ return type;
+ }
+ public void setType(int type) {
+ this.type = type;
+ }
+ public boolean isSong() {
+ return type == TYPE_SONG;
+ }
+ public boolean isPodcast() {
+ return this instanceof PodcastEpisode || type == TYPE_PODCAST;
+ }
+ public boolean isAudioBook() {
+ return type == TYPE_AUDIO_BOOK;
+ }
+
+ public int getCloseness() {
+ return closeness;
+ }
+
+ public void setCloseness(int closeness) {
+ this.closeness = closeness;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ Entry entry = (Entry) o;
+ return id.equals(entry.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return id.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return title;
+ }
+ }
+
+ public static class EntryComparator implements Comparator<Entry> {
+ private boolean byYear;
+
+ public EntryComparator(boolean byYear) {
+ this.byYear = byYear;
+ }
+
+ public int compare(Entry lhs, Entry rhs) {
+ if(lhs.isDirectory() && !rhs.isDirectory()) {
+ return -1;
+ } else if(!lhs.isDirectory() && rhs.isDirectory()) {
+ return 1;
+ } else if(lhs.isDirectory() && rhs.isDirectory()) {
+ if(byYear) {
+ Integer lhsYear = lhs.getYear();
+ Integer rhsYear = rhs.getYear();
+ if(lhsYear != null && rhsYear != null) {
+ return lhsYear.compareTo(rhsYear);
+ } else if(lhsYear != null) {
+ return -1;
+ } else if(rhsYear != null) {
+ return 1;
+ }
+ }
+
+ return lhs.getTitle().compareToIgnoreCase(rhs.getTitle());
+ }
+
+ Integer lhsDisc = lhs.getDiscNumber();
+ Integer rhsDisc = rhs.getDiscNumber();
+
+ if(lhsDisc != null && rhsDisc != null) {
+ if(lhsDisc < rhsDisc) {
+ return -1;
+ } else if(lhsDisc > rhsDisc) {
+ return 1;
+ }
+ }
+
+ Integer lhsTrack = lhs.getTrack();
+ Integer rhsTrack = rhs.getTrack();
+ if(lhsTrack != null && rhsTrack != null) {
+ return lhsTrack.compareTo(rhsTrack);
+ } else if(lhsTrack != null) {
+ return -1;
+ } else if(rhsTrack != null) {
+ return 1;
+ }
+
+ return lhs.getTitle().compareToIgnoreCase(rhs.getTitle());
+ }
+
+ public static void sort(List<Entry> entries) {
+ sort(entries, true);
+ }
+ public static void sort(List<Entry> entries, boolean byYear) {
+ try {
+ Collections.sort(entries, new EntryComparator(byYear));
+ } catch (Exception e) {
+ Log.w(TAG, "Failed to sort MusicDirectory");
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/domain/MusicFolder.java b/app/src/main/java/github/daneren2005/dsub/domain/MusicFolder.java
new file mode 100644
index 00000000..99e86e23
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/domain/MusicFolder.java
@@ -0,0 +1,49 @@
+/*
+ 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.domain;
+
+import java.io.Serializable;
+
+/**
+ * Represents a top level directory in which music or other media is stored.
+ *
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class MusicFolder implements Serializable {
+
+ private String id;
+ private String name;
+
+ public MusicFolder() {
+
+ }
+ public MusicFolder(String id, String name) {
+ this.id = id;
+ this.name = name;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/domain/PlayerQueue.java b/app/src/main/java/github/daneren2005/dsub/domain/PlayerQueue.java
new file mode 100644
index 00000000..32f29725
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/domain/PlayerQueue.java
@@ -0,0 +1,30 @@
+/*
+ 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.domain;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+public class PlayerQueue implements Serializable {
+ public List<MusicDirectory.Entry> songs = new ArrayList<MusicDirectory.Entry>();
+ public List<MusicDirectory.Entry> toDelete = new ArrayList<MusicDirectory.Entry>();
+ public int currentPlayingIndex;
+ public int currentPlayingPosition;
+ public boolean renameCurrent = false;
+ public Date changed = null;
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/domain/PlayerState.java b/app/src/main/java/github/daneren2005/dsub/domain/PlayerState.java
new file mode 100644
index 00000000..21f1b1a4
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/domain/PlayerState.java
@@ -0,0 +1,47 @@
+/*
+ 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.domain;
+
+import android.media.RemoteControlClient;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public enum PlayerState {
+ IDLE(RemoteControlClient.PLAYSTATE_STOPPED),
+ DOWNLOADING(RemoteControlClient.PLAYSTATE_BUFFERING),
+ PREPARING(RemoteControlClient.PLAYSTATE_BUFFERING),
+ PREPARED(RemoteControlClient.PLAYSTATE_STOPPED),
+ STARTED(RemoteControlClient.PLAYSTATE_PLAYING),
+ STOPPED(RemoteControlClient.PLAYSTATE_STOPPED),
+ PAUSED(RemoteControlClient.PLAYSTATE_PAUSED),
+ PAUSED_TEMP(RemoteControlClient.PLAYSTATE_PAUSED),
+ COMPLETED(RemoteControlClient.PLAYSTATE_STOPPED);
+
+ private final int mRemoteControlClientPlayState;
+
+ private PlayerState(int playState) {
+ mRemoteControlClientPlayState = playState;
+ }
+
+ public int getRemoteControlClientPlayState() {
+ return mRemoteControlClientPlayState;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/domain/Playlist.java b/app/src/main/java/github/daneren2005/dsub/domain/Playlist.java
new file mode 100644
index 00000000..7cd820c0
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/domain/Playlist.java
@@ -0,0 +1,128 @@
+/*
+ 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.domain;
+
+import java.io.Serializable;
+
+/**
+ * @author Sindre Mehus
+ */
+public class Playlist implements Serializable {
+
+ private String id;
+ private String name;
+ private String owner;
+ private String comment;
+ private String songCount;
+ private String created;
+ private Boolean pub;
+
+ public Playlist() {
+
+ }
+ public Playlist(String id, String name) {
+ this.id = id;
+ this.name = name;
+ }
+ public Playlist(String id, String name, String owner, String comment, String songCount, String created, String pub) {
+ this.id = id;
+ this.name = name;
+ this.owner = (owner == null) ? "" : owner;
+ this.comment = (comment == null) ? "" : comment;
+ this.songCount = (songCount == null) ? "" : songCount;
+ this.created = (created == null) ? "" : created;
+ this.pub = (pub == null) ? null : (pub.equals("true"));
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getOwner() {
+ return this.owner;
+ }
+
+ public void setOwner(String owner) {
+ this.owner = owner;
+ }
+
+ public String getComment() {
+ return this.comment;
+ }
+
+ public void setComment(String comment) {
+ this.comment = comment;
+ }
+
+ public String getSongCount() {
+ return this.songCount;
+ }
+
+ public void setSongCount(String songCount) {
+ this.songCount = songCount;
+ }
+
+ public String getCreated() {
+ return this.created;
+ }
+
+ public void setCreated(String created) {
+ this.created = created;
+ }
+
+ public Boolean getPublic() {
+ return this.pub;
+ }
+ public void setPublic(Boolean pub) {
+ this.pub = pub;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if(o == this) {
+ return true;
+ } else if(o == null) {
+ return false;
+ } else if(o instanceof String) {
+ return o.equals(this.id);
+ } else if(o.getClass() != getClass()) {
+ return false;
+ }
+
+ Playlist playlist = (Playlist) o;
+ return playlist.id.equals(this.id);
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/domain/PodcastChannel.java b/app/src/main/java/github/daneren2005/dsub/domain/PodcastChannel.java
new file mode 100644
index 00000000..545f76c6
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/domain/PodcastChannel.java
@@ -0,0 +1,145 @@
+/*
+ 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.domain;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.Util;
+
+/**
+ *
+ * @author Scott
+ */
+public class PodcastChannel implements Serializable {
+ private String id;
+ private String name;
+ private String url;
+ private String description;
+ private String status;
+ private String errorMessage;
+
+ public PodcastChannel() {
+
+ }
+
+ public String getId() {
+ return id;
+ }
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+ public void setStatus(String status) {
+ this.status = status;
+ }
+
+ public String getErrorMessage() {
+ return errorMessage;
+ }
+ public void setErrorMessage(String errorMessage) {
+ this.errorMessage = errorMessage;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ PodcastChannel entry = (PodcastChannel) o;
+ return id.equals(entry.id);
+ }
+
+ public static class PodcastComparator implements Comparator<PodcastChannel> {
+ private static String[] ignoredArticles;
+
+ @Override
+ public int compare(PodcastChannel podcast1, PodcastChannel podcast2) {
+ String lhs = podcast1.getName();
+ String rhs = podcast2.getName();
+ if(lhs == null && rhs == null) {
+ return 0;
+ } else if(lhs == null) {
+ return 1;
+ } else if(rhs == null) {
+ return -1;
+ }
+
+ lhs = lhs.toLowerCase();
+ rhs = rhs.toLowerCase();
+
+ for(String article: ignoredArticles) {
+ int index = lhs.indexOf(article.toLowerCase() + " ");
+ if(index == 0) {
+ lhs = lhs.substring(article.length() + 1);
+ }
+ index = rhs.indexOf(article.toLowerCase() + " ");
+ if(index == 0) {
+ rhs = rhs.substring(article.length() + 1);
+ }
+ }
+
+ return lhs.compareToIgnoreCase(rhs);
+ }
+
+ public static List<PodcastChannel> sort(List<PodcastChannel> podcasts, Context context) {
+ SharedPreferences prefs = Util.getPreferences(context);
+ String ignoredArticlesString = prefs.getString(Constants.CACHE_KEY_IGNORE, "The El La Los Las Le Les");
+ ignoredArticles = ignoredArticlesString.split(" ");
+
+ Collections.sort(podcasts, new PodcastComparator());
+ return podcasts;
+ }
+
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/domain/PodcastEpisode.java b/app/src/main/java/github/daneren2005/dsub/domain/PodcastEpisode.java
new file mode 100644
index 00000000..4181b3d3
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/domain/PodcastEpisode.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 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.domain;
+
+/**
+ *
+ * @author Scott
+ */
+public class PodcastEpisode extends MusicDirectory.Entry {
+ private String episodeId;
+ private String date;
+ private String status;
+
+ public PodcastEpisode() {
+ setDirectory(false);
+ }
+
+ public String getEpisodeId() {
+ return episodeId;
+ }
+ public void setEpisodeId(String episodeId) {
+ this.episodeId = episodeId;
+ }
+
+ public String getDate() {
+ return date;
+ }
+ public void setDate(String date) {
+ this.date = date;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+ public void setStatus(String status) {
+ this.status = status;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/domain/RemoteControlState.java b/app/src/main/java/github/daneren2005/dsub/domain/RemoteControlState.java
new file mode 100644
index 00000000..47895984
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/domain/RemoteControlState.java
@@ -0,0 +1,38 @@
+/*
+ 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.domain;
+
+public enum RemoteControlState {
+ LOCAL(0),
+ JUKEBOX_SERVER(1),
+ CHROMECAST(2),
+ REMOTE_CLIENT(3),
+ DLNA(4);
+
+ private final int mRemoteControlState;
+
+ private RemoteControlState(int value) {
+ mRemoteControlState = value;
+ }
+
+ public int getValue() {
+ return mRemoteControlState;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/domain/RemoteStatus.java b/app/src/main/java/github/daneren2005/dsub/domain/RemoteStatus.java
new file mode 100644
index 00000000..e9749120
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/domain/RemoteStatus.java
@@ -0,0 +1,63 @@
+/*
+ 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.domain;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class RemoteStatus {
+
+ private Integer positionSeconds;
+ private Integer currentPlayingIndex;
+ private Float gain;
+ private boolean playing;
+
+ public Integer getPositionSeconds() {
+ return positionSeconds;
+ }
+
+ public void setPositionSeconds(Integer positionSeconds) {
+ this.positionSeconds = positionSeconds;
+ }
+
+ public Integer getCurrentPlayingIndex() {
+ return currentPlayingIndex;
+ }
+
+ public void setCurrentIndex(Integer currentPlayingIndex) {
+ this.currentPlayingIndex = currentPlayingIndex;
+ }
+
+ public boolean isPlaying() {
+ return playing;
+ }
+
+ public void setPlaying(boolean playing) {
+ this.playing = playing;
+ }
+
+ public Float getGain() {
+ return gain;
+ }
+
+ public void setGain(float gain) {
+ this.gain = gain;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/domain/RepeatMode.java b/app/src/main/java/github/daneren2005/dsub/domain/RepeatMode.java
new file mode 100644
index 00000000..7139029c
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/domain/RepeatMode.java
@@ -0,0 +1,28 @@
+package github.daneren2005.dsub.domain;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public enum RepeatMode {
+ OFF {
+ @Override
+ public RepeatMode next() {
+ return ALL;
+ }
+ },
+ ALL {
+ @Override
+ public RepeatMode next() {
+ return SINGLE;
+ }
+ },
+ SINGLE {
+ @Override
+ public RepeatMode next() {
+ return OFF;
+ }
+ };
+
+ public abstract RepeatMode next();
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/domain/SearchCritera.java b/app/src/main/java/github/daneren2005/dsub/domain/SearchCritera.java
new file mode 100644
index 00000000..20d46aa0
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/domain/SearchCritera.java
@@ -0,0 +1,55 @@
+/*
+ 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.domain;
+
+/**
+ * The criteria for a music search.
+ *
+ * @author Sindre Mehus
+ */
+public class SearchCritera {
+
+ private final String query;
+ private final int artistCount;
+ private final int albumCount;
+ private final int songCount;
+
+ public SearchCritera(String query, int artistCount, int albumCount, int songCount) {
+ this.query = query;
+ this.artistCount = artistCount;
+ this.albumCount = albumCount;
+ this.songCount = songCount;
+ }
+
+ public String getQuery() {
+ return query;
+ }
+
+ public int getArtistCount() {
+ return artistCount;
+ }
+
+ public int getAlbumCount() {
+ return albumCount;
+ }
+
+ public int getSongCount() {
+ return songCount;
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/github/daneren2005/dsub/domain/SearchResult.java b/app/src/main/java/github/daneren2005/dsub/domain/SearchResult.java
new file mode 100644
index 00000000..3427f2ca
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/domain/SearchResult.java
@@ -0,0 +1,52 @@
+/*
+ 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.domain;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * The result of a search. Contains matching artists, albums and songs.
+ *
+ * @author Sindre Mehus
+ */
+public class SearchResult implements Serializable {
+
+ private final List<Artist> artists;
+ private final List<MusicDirectory.Entry> albums;
+ private final List<MusicDirectory.Entry> songs;
+
+ public SearchResult(List<Artist> artists, List<MusicDirectory.Entry> albums, List<MusicDirectory.Entry> songs) {
+ this.artists = artists;
+ this.albums = albums;
+ this.songs = songs;
+ }
+
+ public List<Artist> getArtists() {
+ return artists;
+ }
+
+ public List<MusicDirectory.Entry> getAlbums() {
+ return albums;
+ }
+
+ public List<MusicDirectory.Entry> getSongs() {
+ return songs;
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/github/daneren2005/dsub/domain/ServerInfo.java b/app/src/main/java/github/daneren2005/dsub/domain/ServerInfo.java
new file mode 100644
index 00000000..3ece6af9
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/domain/ServerInfo.java
@@ -0,0 +1,213 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.domain;
+
+import android.content.Context;
+
+import java.io.Serializable;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.Util;
+
+/**
+ * Information about the Subsonic server.
+ *
+ * @author Sindre Mehus
+ */
+public class ServerInfo implements Serializable {
+ public static final int TYPE_SUBSONIC = 1;
+ public static final int TYPE_MADSONIC = 2;
+ private static final Map<Integer, ServerInfo> SERVERS = new ConcurrentHashMap<Integer, ServerInfo>();
+
+ private boolean isLicenseValid;
+ private Version restVersion;
+ private int type;
+
+ public ServerInfo() {
+ type = TYPE_SUBSONIC;
+ }
+
+ public boolean isLicenseValid() {
+ return isLicenseValid;
+ }
+
+ public void setLicenseValid(boolean licenseValid) {
+ isLicenseValid = licenseValid;
+ }
+
+ public Version getRestVersion() {
+ return restVersion;
+ }
+
+ public void setRestVersion(Version restVersion) {
+ this.restVersion = restVersion;
+ }
+
+ public int getRestType() {
+ return type;
+ }
+ public void setRestType(int type) {
+ this.type = type;
+ }
+
+ public boolean isStockSubsonic() {
+ return type == TYPE_SUBSONIC;
+ }
+ public boolean isMadsonic() {
+ return type == TYPE_MADSONIC;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if(this == o) {
+ return true;
+ } else if(o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final ServerInfo info = (ServerInfo) o;
+
+ if(this.type != info.type) {
+ return false;
+ } else if(this.restVersion == null || info.restVersion == null) {
+ // Should never be null unless just starting up
+ return false;
+ } else {
+ return this.restVersion.equals(info.restVersion);
+ }
+ }
+
+ // Stub to make sure this is never used, too easy to screw up
+ private void saveServerInfo(Context context) {
+
+ }
+ public void saveServerInfo(Context context, int instance) {
+ ServerInfo current = SERVERS.get(instance);
+ if(!this.equals(current)) {
+ SERVERS.put(instance, this);
+ FileUtil.serialize(context, this, getCacheName(context, instance));
+ }
+ }
+
+ public static ServerInfo getServerInfo(Context context) {
+ return getServerInfo(context, Util.getActiveServer(context));
+ }
+ public static ServerInfo getServerInfo(Context context, int instance) {
+ ServerInfo current = SERVERS.get(instance);
+ if(current != null) {
+ return current;
+ }
+
+ current = FileUtil.deserialize(context, getCacheName(context, instance), ServerInfo.class);
+ if(current != null) {
+ SERVERS.put(instance, current);
+ }
+
+ return current;
+ }
+
+ public static Version getServerVersion(Context context) {
+ return getServerVersion(context, Util.getActiveServer(context));
+ }
+ public static Version getServerVersion(Context context, int instance) {
+ ServerInfo server = getServerInfo(context, instance);
+ if(server == null) {
+ return null;
+ }
+
+ return server.getRestVersion();
+ }
+
+ public static boolean checkServerVersion(Context context, String requiredVersion) {
+ return checkServerVersion(context, requiredVersion, Util.getActiveServer(context));
+ }
+ public static boolean checkServerVersion(Context context, String requiredVersion, int instance) {
+ ServerInfo server = getServerInfo(context, instance);
+ if(server == null) {
+ return false;
+ }
+
+ Version version = server.getRestVersion();
+ if(version == null) {
+ return false;
+ }
+
+ Version required = new Version(requiredVersion);
+ return version.compareTo(required) >= 0;
+ }
+
+ public static int getServerType(Context context) {
+ return getServerType(context, Util.getActiveServer(context));
+ }
+ public static int getServerType(Context context, int instance) {
+ if(Util.isOffline(context)) {
+ return 0;
+ }
+
+ ServerInfo server = getServerInfo(context, instance);
+ if(server == null) {
+ return 0;
+ }
+
+ return server.getRestType();
+ }
+
+ public static boolean isStockSubsonic(Context context) {
+ return isStockSubsonic(context, Util.getActiveServer(context));
+ }
+ public static boolean isStockSubsonic(Context context, int instance) {
+ return getServerType(context, instance) == TYPE_SUBSONIC;
+ }
+
+ public static boolean isMadsonic(Context context) {
+ return isMadsonic(context, Util.getActiveServer(context));
+ }
+ public static boolean isMadsonic(Context context, int instance) {
+ return getServerType(context, instance) == TYPE_MADSONIC;
+ }
+
+ private static String getCacheName(Context context, int instance) {
+ return "server-" + Util.getRestUrl(context, null, instance, false).hashCode() + ".ser";
+ }
+
+ public static boolean hasArtistInfo(Context context) {
+ if(isStockSubsonic(context) && ServerInfo.checkServerVersion(context, "1.11")) {
+ return true;
+ } else if(isMadsonic(context)) {
+ // TODO: When madsonic adds support, figure out what REST version it is added on
+ return false;
+ } else {
+ return false;
+ }
+ }
+
+ public static boolean canBookmark(Context context) {
+ return checkServerVersion(context, "1.9");
+ }
+
+ public static boolean canSavePlayQueue(Context context) {
+ return ServerInfo.checkServerVersion(context, "1.12") && !ServerInfo.isMadsonic(context);
+ }
+
+ public static boolean canAlbumListPerFolder(Context context) {
+ return ServerInfo.checkServerVersion(context, "1.11") && !ServerInfo.isMadsonic(context);
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/domain/Share.java b/app/src/main/java/github/daneren2005/dsub/domain/Share.java
new file mode 100644
index 00000000..380811a7
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/domain/Share.java
@@ -0,0 +1,165 @@
+/*
+ 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.domain;
+
+import github.daneren2005.dsub.domain.MusicDirectory.Entry;
+import java.io.Serializable;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+
+public class Share implements Serializable {
+ private String id;
+ private String url;
+ private String description;
+ private String username;
+ private Date created;
+ private Date lastVisited;
+ private Date expires;
+ private Long visitCount;
+ private List<Entry> entries;
+
+ public Share() {
+ entries = new ArrayList<Entry>();
+ }
+
+ public String getName() {
+ if(description != null && !"".equals(description)) {
+ return description;
+ } else {
+ return url.replaceFirst(".*/([^/?]+).*", "$1");
+ }
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public Date getCreated() {
+ return created;
+ }
+
+ public void setCreated(String created) {
+ if (created != null) {
+ try {
+ this.created = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH).parse(created);
+ } catch (ParseException e) {
+ this.created = null;
+ }
+ } else {
+ this.created = null;
+ }
+ }
+ public void setCreated(Date created) {
+ this.created = created;
+ }
+
+ public Date getLastVisited() {
+ return lastVisited;
+ }
+
+ public void setLastVisited(String lastVisited) {
+ if (lastVisited != null) {
+ try {
+ this.lastVisited = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH).parse(lastVisited);
+ } catch (ParseException e) {
+ this.lastVisited = null;
+ }
+ } else {
+ this.lastVisited = null;
+ }
+ }
+ public void setLastVisited(Date lastVisited) {
+ this.lastVisited = lastVisited;
+ }
+
+ public Date getExpires() {
+ return expires;
+ }
+
+ public void setExpires(String expires) {
+ if (expires != null) {
+ try {
+ this.expires = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH).parse(expires);
+ } catch (ParseException e) {
+ this.expires = null;
+ }
+ } else {
+ this.expires = null;
+ }
+ }
+ public void setExpires(Date expires) {
+ this.expires = expires;
+ }
+
+ public Long getVisitCount() {
+ return visitCount;
+ }
+
+ public void setVisitCount(Long visitCount) {
+ this.visitCount = visitCount;
+ }
+
+ public MusicDirectory getMusicDirectory() {
+ MusicDirectory dir = new MusicDirectory();
+ dir.addChildren(entries);
+ dir.setId(getId());
+ dir.setName(getName());
+ return dir;
+ }
+
+ public List<Entry> getEntries() {
+ return this.entries;
+ }
+
+ public void addEntry(Entry entry) {
+ entries.add(entry);
+ }
+ }
diff --git a/app/src/main/java/github/daneren2005/dsub/domain/User.java b/app/src/main/java/github/daneren2005/dsub/domain/User.java
new file mode 100644
index 00000000..797a1271
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/domain/User.java
@@ -0,0 +1,117 @@
+/*
+ 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.domain;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+public class User implements Serializable {
+ public static final String SCROBBLING = "scrobblingEnabled";
+ public static final String ADMIN = "adminRole";
+ public static final String SETTINGS = "settingsRole";
+ public static final String DOWNLOAD = "downloadRole";
+ public static final String UPLOAD = "uploadRole";
+ public static final String COVERART = "coverArtRole";
+ public static final String COMMENT = "commentRole";
+ public static final String PODCAST = "podcastRole";
+ public static final String STREAM = "streamRole";
+ public static final String JUKEBOX = "jukeboxRole";
+ public static final String SHARE = "shareRole";
+ public static final String LASTFM = "lastFMRole";
+ public static final List<String> ROLES = new ArrayList<String>();
+
+ static {
+ ROLES.add(ADMIN);
+ ROLES.add(SETTINGS);
+ ROLES.add(STREAM);
+ ROLES.add(DOWNLOAD);
+ ROLES.add(UPLOAD);
+ ROLES.add(COVERART);
+ ROLES.add(COMMENT);
+ ROLES.add(PODCAST);
+ ROLES.add(JUKEBOX);
+ ROLES.add(SHARE);
+ }
+
+ private String username;
+ private String password;
+ private String email;
+
+ private List<Setting> settings = new ArrayList<Setting>();
+
+ public User() {
+
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+
+ public List<Setting> getSettings() {
+ return settings;
+ }
+ public void setSettings(List<Setting> settings) {
+ this.settings.clear();
+ this.settings.addAll(settings);
+ }
+ public void addSetting(String name, Boolean value) {
+ settings.add(new Setting(name, value));
+ }
+
+ public static class Setting implements Serializable {
+ String name;
+ Boolean value;
+
+ public Setting() {
+
+ }
+ public Setting(String name, Boolean value) {
+ this.name = name;
+ this.value = value;
+ }
+
+ public String getName() {
+ return name;
+ }
+ public Boolean getValue() {
+ return value;
+ }
+ public void setValue(Boolean value) {
+ this.value = value;
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/domain/Version.java b/app/src/main/java/github/daneren2005/dsub/domain/Version.java
new file mode 100644
index 00000000..6b82ea99
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/domain/Version.java
@@ -0,0 +1,181 @@
+/*
+ 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.domain;
+
+import java.io.Serializable;
+
+/**
+ * Represents the version number of the Subsonic Android app.
+ *
+ * @author Sindre Mehus
+ * @version $Revision: 1.3 $ $Date: 2006/01/20 21:25:16 $
+ */
+public class Version implements Comparable<Version>, Serializable {
+ private int major;
+ private int minor;
+ private int beta;
+ private int bugfix;
+
+ public Version() {
+ // For Kryo
+ }
+
+ /**
+ * Creates a new version instance by parsing the given string.
+ * @param version A string of the format "1.27", "1.27.2" or "1.27.beta3".
+ */
+ public Version(String version) {
+ String[] s = version.split("\\.");
+ major = Integer.valueOf(s[0]);
+ minor = Integer.valueOf(s[1]);
+
+ if (s.length > 2) {
+ if (s[2].contains("beta")) {
+ beta = Integer.valueOf(s[2].replace("beta", ""));
+ } else {
+ bugfix = Integer.valueOf(s[2]);
+ }
+ }
+ }
+
+ public int getMajor() {
+ return major;
+ }
+
+ public int getMinor() {
+ return minor;
+ }
+
+ public String getVersion() {
+ switch(major) {
+ case 1:
+ switch(minor) {
+ case 0:
+ return "3.8";
+ case 1:
+ return "3.9";
+ case 2:
+ return "4.0";
+ case 3:
+ return "4.1";
+ case 4:
+ return "4.2";
+ case 5:
+ return "4.3.1";
+ case 6:
+ return "4.5";
+ case 7:
+ return "4.6";
+ case 8:
+ return "4.7";
+ case 9:
+ return "4.8";
+ case 10:
+ return "4.9";
+ case 11:
+ return "5.1";
+ }
+ }
+ return "";
+ }
+
+ /**
+ * Return whether this object is equal to another.
+ * @param o Object to compare to.
+ * @return Whether this object is equals to another.
+ */
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ final Version version = (Version) o;
+
+ if (beta != version.beta) return false;
+ if (bugfix != version.bugfix) return false;
+ if (major != version.major) return false;
+ return minor == version.minor;
+ }
+
+ /**
+ * Returns a hash code for this object.
+ * @return A hash code for this object.
+ */
+ public int hashCode() {
+ int result;
+ result = major;
+ result = 29 * result + minor;
+ result = 29 * result + beta;
+ result = 29 * result + bugfix;
+ return result;
+ }
+
+ /**
+ * Returns a string representation of the form "1.27", "1.27.2" or "1.27.beta3".
+ * @return A string representation of the form "1.27", "1.27.2" or "1.27.beta3".
+ */
+ public String toString() {
+ StringBuffer buf = new StringBuffer();
+ buf.append(major).append('.').append(minor);
+ if (beta != 0) {
+ buf.append(".beta").append(beta);
+ } else if (bugfix != 0) {
+ buf.append('.').append(bugfix);
+ }
+
+ return buf.toString();
+ }
+
+ /**
+ * Compares this object with the specified object for order.
+ * @param version The object to compare to.
+ * @return A negative integer, zero, or a positive integer as this object is less than, equal to, or
+ * greater than the specified object.
+ */
+ @Override
+ public int compareTo(Version version) {
+ if (major < version.major) {
+ return -1;
+ } else if (major > version.major) {
+ return 1;
+ }
+
+ if (minor < version.minor) {
+ return -1;
+ } else if (minor > version.minor) {
+ return 1;
+ }
+
+ if (bugfix < version.bugfix) {
+ return -1;
+ } else if (bugfix > version.bugfix) {
+ return 1;
+ }
+
+ int thisBeta = beta == 0 ? Integer.MAX_VALUE : beta;
+ int otherBeta = version.beta == 0 ? Integer.MAX_VALUE : version.beta;
+
+ if (thisBeta < otherBeta) {
+ return -1;
+ } else if (thisBeta > otherBeta) {
+ return 1;
+ }
+
+ return 0;
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/AdminFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/AdminFragment.java
new file mode 100644
index 00000000..66ce5f15
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/fragments/AdminFragment.java
@@ -0,0 +1,147 @@
+/*
+ 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.fragments;
+
+import android.os.Bundle;
+import android.view.ContextMenu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.User;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.parser.SubsonicRESTException;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.ProgressListener;
+import github.daneren2005.dsub.util.UserUtil;
+import github.daneren2005.dsub.util.Util;
+import github.daneren2005.dsub.adapter.UserAdapter;
+
+public class AdminFragment extends SelectListFragment<User> {
+ private static String TAG = AdminFragment.class.getSimpleName();
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if(super.onOptionsItemSelected(item)) {
+ return true;
+ }
+
+ switch (item.getItemId()) {
+ case R.id.menu_add_user:
+ UserUtil.addNewUser(context, this);
+ break;
+ }
+
+ return false;
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, view, menuInfo);
+
+ MenuInflater inflater = context.getMenuInflater();
+ if(UserUtil.isCurrentAdmin()) {
+ inflater.inflate(R.menu.admin_context, menu);
+ } else if(UserUtil.isCurrentRole(User.SETTINGS)) {
+ inflater.inflate(R.menu.admin_context_user, menu);
+ }
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem menuItem) {
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
+ User user = objects.get(info.position);
+
+ switch(menuItem.getItemId()) {
+ case R.id.admin_change_email:
+ UserUtil.changeEmail(context, user);
+ break;
+ case R.id.admin_change_password:
+ UserUtil.changePassword(context, user);
+ break;
+ case R.id.admin_delete_user:
+ UserUtil.deleteUser(context, user, adapter);
+ break;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int getOptionsMenu() {
+ if(UserUtil.isCurrentAdmin()) {
+ return R.menu.admin;
+ } else {
+ return R.menu.empty;
+ }
+ }
+
+ @Override
+ public ArrayAdapter getAdapter(List<User> objs) {
+ return new UserAdapter(context, objs, getImageLoader());
+ }
+
+ @Override
+ public List<User> getObjects(MusicService musicService, boolean refresh, ProgressListener listener) throws Exception {
+ try {
+ // Will only work if user is admin
+ List<User> users = musicService.getUsers(refresh, context, listener);
+ if(refresh) {
+ UserUtil.refreshCurrentUser(context, true);
+ }
+ return users;
+ } catch(SubsonicRESTException e) {
+ // Delete cached users if not allowed to get them
+ String s = Util.getRestUrl(context, null, false);
+ String cache = "users-" + s.hashCode() + ".ser";
+ File file = new File(context.getCacheDir(), cache);
+ file.delete();
+
+ List<User> users = new ArrayList<User>();
+ User user = musicService.getUser(refresh, UserUtil.getCurrentUsername(context), context, listener);
+ if(user != null) {
+ users.add(user);
+ }
+
+ UserUtil.refreshCurrentUser(context, false);
+ return users;
+ }
+ }
+
+ @Override
+ public int getTitleResource() {
+ return R.string.button_bar_admin;
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ User user = (User) parent.getItemAtPosition(position);
+
+ SubsonicFragment fragment = new UserFragment();
+ Bundle args = new Bundle();
+ args.putSerializable(Constants.INTENT_EXTRA_NAME_ID, user);
+ fragment.setArguments(args);
+
+ replaceFragment(fragment);
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/ChatFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/ChatFragment.java
new file mode 100644
index 00000000..3e48f1a6
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/fragments/ChatFragment.java
@@ -0,0 +1,250 @@
+package github.daneren2005.dsub.fragments;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.v4.widget.SwipeRefreshLayout;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.ListView;
+import android.widget.TextView;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.ChatMessage;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import github.daneren2005.dsub.util.BackgroundTask;
+import github.daneren2005.dsub.util.TabBackgroundTask;
+import github.daneren2005.dsub.util.Util;
+import github.daneren2005.dsub.adapter.ChatAdapter;
+import github.daneren2005.dsub.util.Constants;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author Joshua Bahnsen
+ */
+public class ChatFragment extends SubsonicFragment {
+ private static final String TAG = ChatFragment.class.getSimpleName();
+ private ListView chatListView;
+ private EditText messageEditText;
+ private ImageButton sendButton;
+ private Long lastChatMessageTime = (long) 0;
+ private ArrayList<ChatMessage> messageList;
+ private ScheduledExecutorService executorService;
+
+ @Override
+ public void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+
+ if(bundle != null) {
+ List<ChatMessage> abstractList = (List<ChatMessage>) bundle.getSerializable(Constants.FRAGMENT_LIST);
+ messageList = new ArrayList<ChatMessage>(abstractList);
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putSerializable(Constants.FRAGMENT_LIST, (Serializable) messageList);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
+ rootView = inflater.inflate(R.layout.chat, container, false);
+
+ messageEditText = (EditText) rootView.findViewById(R.id.chat_edittext);
+ sendButton = (ImageButton) rootView.findViewById(R.id.chat_send);
+
+ sendButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ sendMessage();
+ }
+ });
+
+ chatListView = (ListView) rootView.findViewById(R.id.chat_entries);
+
+ messageEditText.setImeActionLabel("Send", KeyEvent.KEYCODE_ENTER);
+ messageEditText.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+ }
+
+ @Override
+ public void afterTextChanged(Editable editable) {
+ sendButton.setEnabled(!Util.isNullOrWhiteSpace(editable.toString()));
+ }
+ });
+
+ messageEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
+
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (actionId == EditorInfo.IME_ACTION_DONE || (actionId == EditorInfo.IME_NULL && event.getAction() == KeyEvent.ACTION_DOWN)) {
+ sendMessage();
+ return true;
+ }
+
+ return false;
+ }
+ });
+
+ if(messageList == null) {
+ messageList = new ArrayList<ChatMessage>();
+ refresh(true);
+ } else {
+ for (ChatMessage message : messageList) {
+ if (message.getTime() > lastChatMessageTime) {
+ lastChatMessageTime = message.getTime();
+ }
+ }
+
+ ChatAdapter chatAdapter = new ChatAdapter(context, messageList, getImageLoader());
+ chatListView.setAdapter(chatAdapter);
+ }
+ setTitle(R.string.button_bar_chat);
+
+ refreshLayout = (SwipeRefreshLayout) rootView.findViewById(R.id.refresh_layout);
+ refreshLayout.setOnRefreshListener(this);
+ setupScrollList(chatListView);
+
+ return rootView;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ final Handler handler = new Handler();
+ Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ if(primaryFragment) {
+ load(false);
+ } else {
+ invalidated = true;
+ }
+ }
+ });
+ }
+ };
+
+ SharedPreferences prefs = Util.getPreferences(context);
+ long refreshRate = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_CHAT_REFRESH, "30"));
+ if(refreshRate > 0) {
+ executorService = Executors.newSingleThreadScheduledExecutor();
+ executorService.scheduleWithFixedDelay(runnable, refreshRate * 1000L, refreshRate * 1000L, TimeUnit.MILLISECONDS);
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ if(executorService != null) {
+ executorService.shutdown();
+ executorService = null;
+ }
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) {
+ menuInflater.inflate(R.menu.abstract_top_menu, menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ return super.onOptionsItemSelected(item);
+
+ }
+
+ @Override
+ protected void refresh(boolean refresh) {
+ load(refresh);
+ }
+
+ private synchronized void load(final boolean refresh) {
+ BackgroundTask<List<ChatMessage>> task = new TabBackgroundTask<List<ChatMessage>>(this) {
+ @Override
+ protected List<ChatMessage> doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ return musicService.getChatMessages(refresh ? 0L : lastChatMessageTime, context, this);
+ }
+
+ @Override
+ protected void done(List<ChatMessage> result) {
+ if (result != null && !result.isEmpty()) {
+ if(refresh) {
+ messageList.clear();
+ }
+
+ // Reset lastChatMessageTime if we have a newer message
+ for (ChatMessage message : result) {
+ if (message.getTime() > lastChatMessageTime) {
+ lastChatMessageTime = message.getTime();
+ }
+ }
+
+ // Reverse results to show them on the bottom
+ Collections.reverse(result);
+ messageList.addAll(result);
+
+ ChatAdapter chatAdapter = new ChatAdapter(context, messageList, getImageLoader());
+ chatListView.setAdapter(chatAdapter);
+ }
+ }
+ };
+
+ task.execute();
+ }
+
+ private void sendMessage() {
+ final String message = messageEditText.getText().toString();
+
+ if (!Util.isNullOrWhiteSpace(message)) {
+ messageEditText.setText("");
+ InputMethodManager mgr = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
+ mgr.hideSoftInputFromWindow(messageEditText.getWindowToken(), 0);
+
+ BackgroundTask<Void> task = new TabBackgroundTask<Void>(this) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.addChatMessage(message, context, this);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ load(false);
+ }
+ };
+
+ task.execute();
+ }
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/DownloadFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/DownloadFragment.java
new file mode 100644
index 00000000..59229c3f
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/fragments/DownloadFragment.java
@@ -0,0 +1,189 @@
+/*
+ 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.fragments;
+
+import android.content.DialogInterface;
+import android.os.Handler;
+import android.view.ContextMenu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.service.DownloadFile;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.util.ProgressListener;
+import github.daneren2005.dsub.util.SilentBackgroundTask;
+import github.daneren2005.dsub.util.Util;
+import github.daneren2005.dsub.adapter.DownloadFileAdapter;
+
+public class DownloadFragment extends SelectListFragment<DownloadFile> {
+ private long currentRevision;
+ private ScheduledExecutorService executorService;
+
+ public DownloadFragment() {
+ serialize = false;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ final Handler handler = new Handler();
+ Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ update();
+ }
+ });
+ }
+ };
+
+ executorService = Executors.newSingleThreadScheduledExecutor();
+ executorService.scheduleWithFixedDelay(runnable, 0L, 1000L, TimeUnit.MILLISECONDS);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ executorService.shutdown();
+ }
+
+ @Override
+ public int getOptionsMenu() {
+ return R.menu.downloading;
+ }
+
+ @Override
+ public ArrayAdapter getAdapter(List<DownloadFile> objs) {
+ return new DownloadFileAdapter(context, objs);
+ }
+
+ @Override
+ public List<DownloadFile> getObjects(MusicService musicService, boolean refresh, ProgressListener listener) throws Exception {
+ DownloadService downloadService = getDownloadService();
+ if(downloadService == null) {
+ return new ArrayList<DownloadFile>();
+ }
+
+ listView.setOnScrollListener(null);
+ refreshLayout.setEnabled(false);
+
+ List<DownloadFile> songList = new ArrayList<DownloadFile>();
+ songList.addAll(downloadService.getBackgroundDownloads());
+ currentRevision = downloadService.getDownloadListUpdateRevision();
+ return songList;
+ }
+
+ @Override
+ public int getTitleResource() {
+ return R.string.button_bar_downloading;
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem menuItem) {
+ if(super.onOptionsItemSelected(menuItem)) {
+ return true;
+ }
+
+ switch (menuItem.getItemId()) {
+ case R.id.menu_remove_all:
+ Util.confirmDialog(context, R.string.download_menu_remove_all, "", new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ getDownloadService().clearBackground();
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ update();
+ }
+ }.execute();
+ }
+ });
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public void onCreateContextMenu(android.view.ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, view, menuInfo);
+
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
+ Object selectedItem = ((DownloadFile) listView.getItemAtPosition(info.position)).getSong();
+ onCreateContextMenu(menu, view, menuInfo, selectedItem);
+ if(selectedItem instanceof MusicDirectory.Entry && !((MusicDirectory.Entry) selectedItem).isVideo() && !Util.isOffline(context)) {
+ menu.removeItem(R.id.song_menu_remove_playlist);
+ }
+
+ recreateContextMenu(menu);
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem menuItem) {
+ if(menuItem.getGroupId() != getSupportTag()) {
+ return false;
+ }
+
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
+ Object selectedItem = ((DownloadFile) listView.getItemAtPosition(info.position)).getSong();
+
+ if(onContextItemSelected(menuItem, selectedItem)) {
+ return true;
+ }
+
+ return true;
+ }
+
+ private void update() {
+ DownloadService downloadService = getDownloadService();
+ if (downloadService == null || objects == null || adapter == null) {
+ return;
+ }
+
+ if (currentRevision != downloadService.getDownloadListUpdateRevision()) {
+ List<DownloadFile> downloadFileList = downloadService.getBackgroundDownloads();
+ objects.clear();
+ objects.addAll(downloadFileList);
+ adapter.notifyDataSetChanged();
+
+ currentRevision = downloadService.getDownloadListUpdateRevision();
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/EqualizerFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/EqualizerFragment.java
new file mode 100644
index 00000000..b7080a8e
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/fragments/EqualizerFragment.java
@@ -0,0 +1,441 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.fragments;
+
+import android.content.SharedPreferences;
+import android.media.audiofx.BassBoost;
+import android.media.audiofx.Equalizer;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.LinearLayout;
+import android.widget.SeekBar;
+import android.widget.TextView;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.audiofx.EqualizerController;
+import github.daneren2005.dsub.audiofx.LoudnessEnhancerController;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.Util;
+
+/**
+ * Created by Scott on 10/27/13.
+ */
+public class EqualizerFragment extends SubsonicFragment {
+ private static final String TAG = EqualizerFragment.class.getSimpleName();
+
+ private static final int MENU_GROUP_PRESET = 100;
+
+ private final Map<Short, SeekBar> bars = new HashMap<Short, SeekBar>();
+ private SeekBar bassBar;
+ private SeekBar loudnessBar;
+ private EqualizerController equalizerController;
+ private Equalizer equalizer;
+ private BassBoost bass;
+ private LoudnessEnhancerController loudnessEnhancer;
+ private short masterLevel = 0;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
+ rootView = inflater.inflate(R.layout.equalizer, container, false);
+
+ try {
+ DownloadService service = DownloadService.getInstance();
+ equalizerController = service.getEqualizerController();
+ equalizer = equalizerController.getEqualizer();
+ bass = equalizerController.getBassBoost();
+ loudnessEnhancer = equalizerController.getLoudnessEnhancerController();
+
+ initEqualizer();
+ } catch(Exception e) {
+ Log.e(TAG, "Failed to initialize EQ", e);
+ Util.toast(context, "Failed to initialize EQ");
+ context.onBackPressed();
+ }
+
+ final View presetButton = rootView.findViewById(R.id.equalizer_preset);
+ registerForContextMenu(presetButton);
+ presetButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ presetButton.showContextMenu();
+ }
+ });
+
+ CheckBox enabledCheckBox = (CheckBox) rootView.findViewById(R.id.equalizer_enabled);
+ enabledCheckBox.setChecked(equalizer.getEnabled());
+ enabledCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
+ try {
+ setEqualizerEnabled(b);
+ } catch(Exception e) {
+ Log.e(TAG, "Failed to set EQ enabled", e);
+ Util.toast(context, "Failed to set EQ enabled");
+ context.onBackPressed();
+ }
+ }
+ });
+
+ setTitle(R.string.equalizer_label);
+
+ return rootView;
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ equalizerController.saveSettings();
+
+ if(!equalizer.getEnabled()) {
+ equalizerController.release();
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ equalizerController = DownloadService.getInstance().getEqualizerController();
+ equalizer = equalizerController.getEqualizer();
+ bass = equalizerController.getBassBoost();
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, view, menuInfo);
+ if(!primaryFragment) {
+ return;
+ }
+
+ short currentPreset;
+ try {
+ currentPreset = equalizer.getCurrentPreset();
+ } catch (Exception x) {
+ currentPreset = -1;
+ }
+
+ for (short preset = 0; preset < equalizer.getNumberOfPresets(); preset++) {
+ MenuItem menuItem = menu.add(MENU_GROUP_PRESET, preset, preset, equalizer.getPresetName(preset));
+ if (preset == currentPreset) {
+ menuItem.setChecked(true);
+ }
+ }
+ menu.setGroupCheckable(MENU_GROUP_PRESET, true, true);
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem menuItem) {
+ short preset = (short) menuItem.getItemId();
+ for(int i = 0; i < 10; i++) {
+ try {
+ equalizer.usePreset(preset);
+ i = 10;
+ } catch (UnsupportedOperationException e) {
+ equalizerController.release();
+ equalizer = equalizerController.getEqualizer();
+ bass = equalizerController.getBassBoost();
+ loudnessEnhancer = equalizerController.getLoudnessEnhancerController();
+ }
+ }
+ updateBars(false);
+ return true;
+ }
+
+ private void setEqualizerEnabled(boolean enabled) {
+ SharedPreferences prefs = Util.getPreferences(context);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putBoolean(Constants.PREFERENCES_EQUALIZER_ON, enabled);
+ editor.commit();
+ for(int i = 0; i < 10; i++) {
+ try {
+ equalizer.setEnabled(enabled);
+ updateBars(true);
+ i = 10;
+ } catch (UnsupportedOperationException e) {
+ equalizerController.release();
+ equalizer = equalizerController.getEqualizer();
+ bass = equalizerController.getBassBoost();
+ loudnessEnhancer = equalizerController.getLoudnessEnhancerController();
+ }
+ }
+ }
+
+ private void updateBars(boolean changedEnabled) {
+ boolean isEnabled = equalizer.getEnabled();
+ short minEQLevel = equalizer.getBandLevelRange()[0];
+ short maxEQLevel = equalizer.getBandLevelRange()[1];
+ for (Map.Entry<Short, SeekBar> entry : bars.entrySet()) {
+ short band = entry.getKey();
+ SeekBar bar = entry.getValue();
+ bar.setEnabled(isEnabled);
+ if(band >= (short)0) {
+ short setLevel;
+ if(changedEnabled) {
+ setLevel = (short)(equalizer.getBandLevel(band) - masterLevel);
+ if(isEnabled) {
+ bar.setProgress(equalizer.getBandLevel(band) - minEQLevel);
+ } else {
+ bar.setProgress(-minEQLevel);
+ }
+ } else {
+ bar.setProgress(equalizer.getBandLevel(band) - minEQLevel);
+ setLevel = (short)(equalizer.getBandLevel(band) + masterLevel);
+ }
+ if(setLevel < minEQLevel) {
+ setLevel = minEQLevel;
+ } else if(setLevel > maxEQLevel) {
+ setLevel = maxEQLevel;
+ }
+ equalizer.setBandLevel(band, setLevel);
+ } else if(!isEnabled) {
+ bar.setProgress(-minEQLevel);
+ }
+ }
+
+ bassBar.setEnabled(isEnabled);
+ if(loudnessBar != null) {
+ loudnessBar.setEnabled(isEnabled);
+ }
+ if(changedEnabled && !isEnabled) {
+ bass.setStrength((short) 0);
+ bassBar.setProgress(0);
+ if(loudnessBar != null) {
+ loudnessEnhancer.setGain(0);
+ loudnessBar.setProgress(0);
+ }
+ }
+
+ if(!isEnabled) {
+ masterLevel = 0;
+ SharedPreferences prefs = Util.getPreferences(context);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putInt(Constants.PREFERENCES_EQUALIZER_SETTINGS, masterLevel);
+ editor.commit();
+ }
+ }
+
+ private void initEqualizer() {
+ LinearLayout layout = (LinearLayout) rootView.findViewById(R.id.equalizer_layout);
+
+ final short minEQLevel = equalizer.getBandLevelRange()[0];
+ final short maxEQLevel = equalizer.getBandLevelRange()[1];
+
+ // Setup Pregain
+ SharedPreferences prefs = Util.getPreferences(context);
+ masterLevel = (short)prefs.getInt(Constants.PREFERENCES_EQUALIZER_SETTINGS, 0);
+ initPregain(layout, minEQLevel, maxEQLevel);
+
+ for (short i = 0; i < equalizer.getNumberOfBands(); i++) {
+ final short band = i;
+
+ View bandBar = LayoutInflater.from(context).inflate(R.layout.equalizer_bar, null);
+ TextView freqTextView = (TextView) bandBar.findViewById(R.id.equalizer_frequency);
+ final TextView levelTextView = (TextView) bandBar.findViewById(R.id.equalizer_level);
+ SeekBar bar = (SeekBar) bandBar.findViewById(R.id.equalizer_bar);
+
+ freqTextView.setText((equalizer.getCenterFreq(band) / 1000) + " Hz");
+
+ bars.put(band, bar);
+ bar.setMax(maxEQLevel - minEQLevel);
+ short level = equalizer.getBandLevel(band);
+ if(equalizer.getEnabled()) {
+ level = (short) (level - masterLevel);
+ }
+ bar.setProgress(level - minEQLevel);
+ bar.setEnabled(equalizer.getEnabled());
+ updateLevelText(levelTextView, level);
+
+ bar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ short level = (short) (progress + minEQLevel);
+ if (fromUser) {
+ equalizer.setBandLevel(band, (short)(level + masterLevel));
+ }
+ updateLevelText(levelTextView, level);
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ }
+ });
+ layout.addView(bandBar);
+ }
+
+ LinearLayout specialLayout = (LinearLayout) rootView.findViewById(R.id.special_effects_layout);
+
+ // Setup bass booster
+ View bandBar = LayoutInflater.from(context).inflate(R.layout.equalizer_bar, null);
+ TextView freqTextView = (TextView) bandBar.findViewById(R.id.equalizer_frequency);
+ final TextView bassTextView = (TextView) bandBar.findViewById(R.id.equalizer_level);
+ bassBar = (SeekBar) bandBar.findViewById(R.id.equalizer_bar);
+
+ freqTextView.setText(R.string.equalizer_bass_booster);
+ bassBar.setEnabled(equalizer.getEnabled());
+ short bassLevel = 0;
+ if(bass.getEnabled()) {
+ bassLevel = bass.getRoundedStrength();
+ }
+ bassTextView.setText(context.getResources().getString(R.string.equalizer_bass_size, bassLevel));
+ bassBar.setMax(1000);
+ bassBar.setProgress(bassLevel);
+ bassBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ try {
+ bassTextView.setText(context.getResources().getString(R.string.equalizer_bass_size, progress));
+ if (fromUser) {
+ if (progress > 0) {
+ if (!bass.getEnabled()) {
+ bass.setEnabled(true);
+ }
+ bass.setStrength((short) progress);
+ } else if (progress == 0 && bass.getEnabled()) {
+ bass.setStrength((short) progress);
+ bass.setEnabled(false);
+ }
+ }
+ } catch(Exception e) {
+ Log.w(TAG, "Error on changing bass: ", e);
+ }
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+
+ }
+ });
+ specialLayout.addView(bandBar);
+
+ if(loudnessEnhancer != null && loudnessEnhancer.isAvailable()) {
+ // Setup loudness enhancer
+ bandBar = LayoutInflater.from(context).inflate(R.layout.equalizer_bar, null);
+ freqTextView = (TextView) bandBar.findViewById(R.id.equalizer_frequency);
+ final TextView loudnessTextView = (TextView) bandBar.findViewById(R.id.equalizer_level);
+ loudnessBar = (SeekBar) bandBar.findViewById(R.id.equalizer_bar);
+
+ freqTextView.setText(R.string.equalizer_voice_booster);
+ loudnessBar.setEnabled(equalizer.getEnabled());
+ int loudnessLevel = 0;
+ if(loudnessEnhancer.isEnabled()) {
+ loudnessLevel = (int) loudnessEnhancer.getGain();
+ }
+ loudnessBar.setProgress(loudnessLevel / 100);
+ loudnessTextView.setText(context.getResources().getString(R.string.equalizer_db_size, loudnessLevel / 100));
+ loudnessBar.setMax(15);
+ loudnessBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ try {
+ loudnessTextView.setText(context.getResources().getString(R.string.equalizer_db_size, progress));
+ if(fromUser) {
+ if(progress > 0) {
+ if(!loudnessEnhancer.isEnabled()) {
+ loudnessEnhancer.enable();
+ }
+ loudnessEnhancer.setGain(progress * 100);
+ } else if(progress == 0 && loudnessEnhancer.isEnabled()) {
+ loudnessEnhancer.setGain(progress * 100);
+ loudnessEnhancer.disable();
+ }
+ }
+ } catch(Exception e) {
+ Log.w(TAG, "Error on changing loudness: ", e);
+ }
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+
+ }
+ });
+ specialLayout.addView(bandBar);
+ }
+ }
+
+ private void initPregain(LinearLayout layout, final short minEQLevel, final short maxEQLevel) {
+ View bandBar = LayoutInflater.from(context).inflate(R.layout.equalizer_bar, null);
+ TextView freqTextView = (TextView) bandBar.findViewById(R.id.equalizer_frequency);
+ final TextView levelTextView = (TextView) bandBar.findViewById(R.id.equalizer_level);
+ SeekBar bar = (SeekBar) bandBar.findViewById(R.id.equalizer_bar);
+
+ freqTextView.setText("Master");
+
+ bars.put((short)-1, bar);
+ bar.setMax(maxEQLevel - minEQLevel);
+ bar.setProgress(masterLevel - minEQLevel);
+ bar.setEnabled(equalizer.getEnabled());
+ updateLevelText(levelTextView, masterLevel);
+
+ bar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ masterLevel = (short) (progress + minEQLevel);
+ if (fromUser) {
+ SharedPreferences prefs = Util.getPreferences(context);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putInt(Constants.PREFERENCES_EQUALIZER_SETTINGS, masterLevel);
+ editor.commit();
+ for (short i = 0; i < equalizer.getNumberOfBands(); i++) {
+ short level = (short) ((bars.get(i).getProgress() + minEQLevel) + masterLevel);
+ equalizer.setBandLevel(i, level);
+ }
+ }
+ updateLevelText(levelTextView, masterLevel);
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ }
+ });
+ layout.addView(bandBar);
+ }
+
+ private void updateLevelText(TextView levelTextView, short level) {
+ levelTextView.setText((level > 0 ? "+" : "") + context.getResources().getString(R.string.equalizer_db_size, level / 100));
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/LyricsFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/LyricsFragment.java
new file mode 100644
index 00000000..826029f5
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/fragments/LyricsFragment.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.fragments;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.Lyrics;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import github.daneren2005.dsub.util.BackgroundTask;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.TabBackgroundTask;
+
+/**
+ * Displays song lyrics.
+ *
+ * @author Sindre Mehus
+ */
+public final class LyricsFragment extends SubsonicFragment {
+ private TextView artistView;
+ private TextView titleView;
+ private TextView textView;
+
+ private Lyrics lyrics;
+
+ @Override
+ public void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+
+ if(bundle != null) {
+ lyrics = (Lyrics) bundle.getSerializable(Constants.FRAGMENT_LIST);
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putSerializable(Constants.FRAGMENT_LIST, lyrics);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
+ setTitle(R.string.download_menu_lyrics);
+ rootView = inflater.inflate(R.layout.lyrics, container, false);
+ artistView = (TextView) rootView.findViewById(R.id.lyrics_artist);
+ titleView = (TextView) rootView.findViewById(R.id.lyrics_title);
+ textView = (TextView) rootView.findViewById(R.id.lyrics_text);
+
+ if(lyrics == null) {
+ load();
+ } else {
+ setLyrics();
+ }
+
+ return rootView;
+ }
+
+ private void load() {
+ BackgroundTask<Lyrics> task = new TabBackgroundTask<Lyrics>(this) {
+ @Override
+ protected Lyrics doInBackground() throws Throwable {
+ String artist = getArguments().getString(Constants.INTENT_EXTRA_NAME_ARTIST);
+ String title = getArguments().getString(Constants.INTENT_EXTRA_NAME_TITLE);
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ return musicService.getLyrics(artist, title, context, this);
+ }
+
+ @Override
+ protected void done(Lyrics result) {
+ lyrics = result;
+ setLyrics();
+ }
+ };
+ task.execute();
+ }
+
+ private void setLyrics() {
+ if (lyrics != null && lyrics.getArtist() != null) {
+ artistView.setText(lyrics.getArtist());
+ titleView.setText(lyrics.getTitle());
+ textView.setText(lyrics.getText());
+ } else {
+ artistView.setText(R.string.lyrics_nomatch);
+ }
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/MainFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/MainFragment.java
new file mode 100644
index 00000000..ae38534a
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/fragments/MainFragment.java
@@ -0,0 +1,586 @@
+package github.daneren2005.dsub.fragments;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.StatFs;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.ListView;
+import android.widget.TextView;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.ServerInfo;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.LoadingTask;
+import github.daneren2005.dsub.util.Pair;
+import github.daneren2005.dsub.util.UserUtil;
+import github.daneren2005.dsub.adapter.MergeAdapter;
+import github.daneren2005.dsub.util.Util;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import github.daneren2005.dsub.util.SilentBackgroundTask;
+import github.daneren2005.dsub.view.ChangeLog;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class MainFragment extends SubsonicFragment {
+ private static final String TAG = MainFragment.class.getSimpleName();
+ private LayoutInflater inflater;
+ private TextView countView;
+
+ private static final int MENU_GROUP_SERVER = 10;
+ private static final int MENU_ITEM_SERVER_BASE = 100;
+
+ @Override
+ public void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
+ this.inflater = inflater;
+ rootView = inflater.inflate(R.layout.home, container, false);
+
+ createLayout();
+
+ return rootView;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) {
+ menuInflater.inflate(R.menu.main, menu);
+
+ try {
+ if (!ServerInfo.isMadsonic(context) || !UserUtil.isCurrentAdmin()) {
+ menu.setGroupVisible(R.id.madsonic, false);
+ }
+ } catch(Exception e) {
+ Log.w(TAG, "Error on setting madsonic invisible", e);
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if(super.onOptionsItemSelected(item)) {
+ return true;
+ }
+
+ switch (item.getItemId()) {
+ case R.id.menu_log:
+ getLogs();
+ return true;
+ case R.id.menu_about:
+ showAboutDialog();
+ return true;
+ case R.id.menu_changelog:
+ ChangeLog changeLog = new ChangeLog(context, Util.getPreferences(context));
+ changeLog.getFullLogDialog().show();
+ return true;
+ case R.id.menu_faq:
+ showFAQDialog();
+ return true;
+ case R.id.menu_rescan:
+ rescanServer();
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, view, menuInfo);
+
+ int serverCount = Util.getServerCount(context);
+ int activeServer = Util.getActiveServer(context);
+ for(int i = 1; i <= serverCount; i++) {
+ android.view.MenuItem menuItem = menu.add(MENU_GROUP_SERVER, MENU_ITEM_SERVER_BASE + i, MENU_ITEM_SERVER_BASE + i, Util.getServerName(context, i));
+ if(i == activeServer) {
+ menuItem.setChecked(true);
+ }
+ }
+ menu.setGroupCheckable(MENU_GROUP_SERVER, true, true);
+ menu.setHeaderTitle(R.string.main_select_server);
+
+ recreateContextMenu(menu);
+ }
+
+ @Override
+ public boolean onContextItemSelected(android.view.MenuItem menuItem) {
+ if(menuItem.getGroupId() != getSupportTag()) {
+ return false;
+ }
+
+ int activeServer = menuItem.getItemId() - MENU_ITEM_SERVER_BASE;
+ setActiveServer(activeServer);
+ return true;
+ }
+
+ @Override
+ protected void refresh(boolean refresh) {
+ createLayout();
+ }
+
+ private void createLayout() {
+ View buttons = inflater.inflate(R.layout.main_buttons, null);
+
+ final View serverButton = buttons.findViewById(R.id.main_select_server);
+ final TextView serverTextView = (TextView) serverButton.findViewById(R.id.main_select_server_2);
+ final TextView offlineButton = (TextView) buttons.findViewById(R.id.main_offline);
+ offlineButton.setText(Util.isOffline(context) ? R.string.main_online : R.string.main_offline);
+
+ final View albumsTitle = buttons.findViewById(R.id.main_albums);
+ final View videoTitle = buttons.findViewById(R.id.main_video_section);
+ final View albumsNewestButton = buttons.findViewById(R.id.main_albums_newest);
+ countView = (TextView) buttons.findViewById(R.id.main_albums_recent_count);
+ final View albumsRandomButton = buttons.findViewById(R.id.main_albums_random);
+ final View albumsHighestButton = buttons.findViewById(R.id.main_albums_highest);
+ final View albumsRecentButton = buttons.findViewById(R.id.main_albums_recent);
+ final View albumsFrequentButton = buttons.findViewById(R.id.main_albums_frequent);
+ final View albumsStarredButton = buttons.findViewById(R.id.main_albums_starred);
+ final View albumsGenresButton = buttons.findViewById(R.id.main_albums_genres);
+ final View albumsYearButton = buttons.findViewById(R.id.main_albums_year);
+ final View albumsAlphabeticalButton = buttons.findViewById(R.id.main_albums_alphabetical);
+ final View videosButton = buttons.findViewById(R.id.main_videos);
+
+ final View dummyView = rootView.findViewById(R.id.main_dummy);
+
+ final CheckBox albumsPerFolderCheckbox = (CheckBox) buttons.findViewById(R.id.main_albums_per_folder);
+ if(!Util.isOffline(context) && ServerInfo.canAlbumListPerFolder(context)) {
+ albumsPerFolderCheckbox.setChecked(Util.getAlbumListsPerFolder(context));
+ albumsPerFolderCheckbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ Util.setAlbumListsPerFolder(context, isChecked);
+ }
+ });
+ } else {
+ albumsPerFolderCheckbox.setVisibility(View.GONE);
+ }
+
+ int instance = Util.getActiveServer(context);
+ String name = Util.getServerName(context, instance);
+ serverTextView.setText(name);
+
+ ListView list = (ListView) rootView.findViewById(R.id.main_list);
+
+ MergeAdapter adapter = new MergeAdapter();
+ if (!Util.isOffline(context)) {
+ adapter.addViews(Arrays.asList(serverButton), true);
+ }
+ adapter.addView(offlineButton, true);
+ if (!Util.isOffline(context)) {
+ adapter.addView(albumsTitle, false);
+ adapter.addViews(Arrays.asList(albumsNewestButton, albumsRandomButton), true);
+ if(ServerInfo.checkServerVersion(context, "1.8")) {
+ adapter.addView(albumsAlphabeticalButton, true);
+ }
+ if(!Util.isTagBrowsing(context)) {
+ adapter.addView(albumsHighestButton, true);
+ }
+ adapter.addViews(Arrays.asList(albumsStarredButton, albumsGenresButton, albumsYearButton, albumsRecentButton, albumsFrequentButton), true);
+ if(ServerInfo.checkServerVersion(context, "1.8") && !Util.isTagBrowsing(context)) {
+ adapter.addView(videoTitle, false);
+ adapter.addView(videosButton, true);
+ }
+ }
+ list.setAdapter(adapter);
+ registerForContextMenu(dummyView);
+
+ list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ if (view == serverButton) {
+ dummyView.showContextMenu();
+ } else if (view == offlineButton) {
+ toggleOffline();
+ } else if (view == albumsNewestButton) {
+ showAlbumList("newest");
+ } else if (view == albumsRandomButton) {
+ showAlbumList("random");
+ } else if (view == albumsHighestButton) {
+ showAlbumList("highest");
+ } else if (view == albumsRecentButton) {
+ showAlbumList("recent");
+ } else if (view == albumsFrequentButton) {
+ showAlbumList("frequent");
+ } else if (view == albumsStarredButton) {
+ showAlbumList("starred");
+ } else if(view == albumsGenresButton) {
+ showAlbumList("genres");
+ } else if(view == albumsYearButton) {
+ showAlbumList("years");
+ } else if(view == albumsAlphabeticalButton) {
+ showAlbumList("alphabeticalByName");
+ } else if(view == videosButton) {
+ showVideos();
+ }
+ }
+ });
+ setTitle(R.string.common_appname);
+
+ if(!Util.isOffline(context)) {
+ getMostRecentCount();
+ }
+ }
+
+ private void setActiveServer(int instance) {
+ if (Util.getActiveServer(context) != instance) {
+ final DownloadService service = getDownloadService();
+ if (service != null) {
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ service.clearIncomplete();
+ return null;
+ }
+ }.execute();
+
+ }
+ Util.setActiveServer(context, instance);
+ context.invalidate();
+ UserUtil.refreshCurrentUser(context, false, true);
+ }
+ }
+
+ private void toggleOffline() {
+ boolean isOffline = Util.isOffline(context);
+ Util.setOffline(context, !isOffline);
+ context.invalidate();
+ DownloadService service = getDownloadService();
+ if (service != null) {
+ service.setOnline(isOffline);
+ }
+
+ // Coming back online
+ if(isOffline) {
+ int scrobblesCount = Util.offlineScrobblesCount(context);
+ int starsCount = Util.offlineStarsCount(context);
+ if(scrobblesCount > 0 || starsCount > 0){
+ showOfflineSyncDialog(scrobblesCount, starsCount);
+ }
+ }
+
+ UserUtil.seedCurrentUser(context);
+ }
+
+ private void showAlbumList(String type) {
+ if("genres".equals(type)) {
+ SubsonicFragment fragment = new SelectGenreFragment();
+ replaceFragment(fragment);
+ } else if("years".equals(type)) {
+ SubsonicFragment fragment = new SelectYearFragment();
+ replaceFragment(fragment);
+ } else {
+ // Clear out recently added count when viewing
+ if("newest".equals(type)) {
+ SharedPreferences.Editor editor = Util.getPreferences(context).edit();
+ editor.putInt(Constants.PREFERENCES_KEY_RECENT_COUNT + Util.getActiveServer(context), 0);
+ editor.commit();
+
+ // Clear immediately so doesn't still show when pressing back
+ setMostRecentCount(0);
+ }
+
+ SubsonicFragment fragment = new SelectDirectoryFragment();
+ Bundle args = new Bundle();
+ args.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, type);
+ args.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 20);
+ args.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0);
+ fragment.setArguments(args);
+
+ replaceFragment(fragment);
+ }
+ }
+ private void showVideos() {
+ SubsonicFragment fragment = new SelectVideoFragment();
+ replaceFragment(fragment);
+ }
+
+ private void showOfflineSyncDialog(final int scrobbleCount, final int starsCount) {
+ String syncDefault = Util.getSyncDefault(context);
+ if(syncDefault != null) {
+ if("sync".equals(syncDefault)) {
+ syncOffline(scrobbleCount, starsCount);
+ return;
+ } else if("delete".equals(syncDefault)) {
+ deleteOffline();
+ return;
+ }
+ }
+
+ View checkBoxView = context.getLayoutInflater().inflate(R.layout.sync_dialog, null);
+ final CheckBox checkBox = (CheckBox)checkBoxView.findViewById(R.id.sync_default);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setIcon(android.R.drawable.ic_dialog_info)
+ .setTitle(R.string.offline_sync_dialog_title)
+ .setMessage(context.getResources().getString(R.string.offline_sync_dialog_message, scrobbleCount, starsCount))
+ .setView(checkBoxView)
+ .setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ if(checkBox.isChecked()) {
+ Util.setSyncDefault(context, "sync");
+ }
+ syncOffline(scrobbleCount, starsCount);
+ }
+ }).setNeutralButton(R.string.common_cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ dialogInterface.dismiss();
+ }
+ }).setNegativeButton(R.string.common_delete, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ if(checkBox.isChecked()) {
+ Util.setSyncDefault(context, "delete");
+ }
+ deleteOffline();
+ }
+ });
+
+ builder.create().show();
+ }
+
+ private void syncOffline(final int scrobbleCount, final int starsCount) {
+ new SilentBackgroundTask<Integer>(context) {
+ @Override
+ protected Integer doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ return musicService.processOfflineSyncs(context, null);
+ }
+
+ @Override
+ protected void done(Integer result) {
+ if(result == scrobbleCount) {
+ Util.toast(context, context.getResources().getString(R.string.offline_sync_success, result));
+ } else {
+ Util.toast(context, context.getResources().getString(R.string.offline_sync_partial, result, scrobbleCount + starsCount));
+ }
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ Log.w(TAG, "Failed to sync offline stats", error);
+ String msg = context.getResources().getString(R.string.offline_sync_error) + " " + getErrorMessage(error);
+ Util.toast(context, msg);
+ }
+ }.execute();
+ }
+ private void deleteOffline() {
+ SharedPreferences.Editor offline = Util.getOfflineSync(context).edit();
+ offline.putInt(Constants.OFFLINE_SCROBBLE_COUNT, 0);
+ offline.putInt(Constants.OFFLINE_STAR_COUNT, 0);
+ offline.commit();
+ }
+
+ private void showAboutDialog() {
+ new LoadingTask<String>(context) {
+ @Override
+ protected String doInBackground() throws Throwable {
+ File rootFolder = FileUtil.getMusicDirectory(context);
+ StatFs stat = new StatFs(rootFolder.getPath());
+ long bytesTotalFs = (long) stat.getBlockCount() * (long) stat.getBlockSize();
+ long bytesAvailableFs = (long) stat.getAvailableBlocks() * (long) stat.getBlockSize();
+
+ Pair<Long, Long> used = FileUtil.getUsedSize(context, rootFolder);
+
+ return getResources().getString(R.string.main_about_text,
+ context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName,
+ used.getFirst(),
+ Util.formatLocalizedBytes(used.getSecond(), context),
+ Util.formatLocalizedBytes(Util.getCacheSizeMB(context) * 1024L * 1024L, context),
+ Util.formatLocalizedBytes(bytesAvailableFs, context),
+ Util.formatLocalizedBytes(bytesTotalFs, context));
+ }
+
+ @Override
+ protected void done(String msg) {
+ try {
+ Util.info(context, R.string.main_about_title, msg);
+ } catch(Exception e) {
+ Util.toast(context, "Failed to open dialog");
+ }
+ }
+ }.execute();
+ }
+
+ private void showFAQDialog() {
+ Util.showHTMLDialog(context, R.string.main_faq_title, R.string.main_faq_text);
+ }
+
+ private void rescanServer() {
+ new LoadingTask<Void>(context, false) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.startRescan(context, this);
+ return null;
+ }
+
+ @Override
+ protected void done(Void value) {
+ Util.toast(context, R.string.main_scan_complete);
+ }
+ }.execute();
+ }
+
+ private void getLogs() {
+ try {
+ final String version = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName;
+ new LoadingTask<File>(context) {
+ @Override
+ protected File doInBackground() throws Throwable {
+ updateProgress("Gathering Logs");
+ File logcat = new File(FileUtil.getSubsonicDirectory(context), "logcat.txt");
+ Util.delete(logcat);
+ Process logcatProc = null;
+
+ try {
+ List<String> progs = new ArrayList<String>();
+ progs.add("logcat");
+ progs.add("-v");
+ progs.add("time");
+ progs.add("-d");
+ progs.add("-f");
+ progs.add(logcat.getCanonicalPath());
+ progs.add("*:I");
+
+ logcatProc = Runtime.getRuntime().exec(progs.toArray(new String[progs.size()]));
+ logcatProc.waitFor();
+ } catch(Exception e) {
+ Util.toast(context, "Failed to gather logs");
+ } finally {
+ if(logcatProc != null) {
+ logcatProc.destroy();
+ }
+ }
+
+ return logcat;
+ }
+
+ @Override
+ protected void done(File logcat) {
+ String footer = "Android SDK: " + Build.VERSION.SDK;
+ footer += "\nDevice Model: " + Build.MODEL;
+ footer += "\nDevice Name: " + Build.MANUFACTURER + " " + Build.PRODUCT;
+ footer += "\nROM: " + Build.DISPLAY;
+
+ Intent email = new Intent(Intent.ACTION_SENDTO,
+ Uri.fromParts("mailto", "dsub.android@gmail.com", null));
+ email.putExtra(Intent.EXTRA_SUBJECT, "DSub " + version + " Error Logs");
+ email.putExtra(Intent.EXTRA_TEXT, "Describe the problem here\n\n\n" + footer);
+ Uri attachment = Uri.fromFile(logcat);
+ email.putExtra(Intent.EXTRA_STREAM, attachment);
+ startActivity(email);
+ }
+ }.execute();
+ } catch(Exception e) {}
+ }
+
+ private void getMostRecentCount() {
+ // Use stashed value until after refresh occurs
+ SharedPreferences prefs = Util.getPreferences(context);
+ final int startCount = prefs.getInt(Constants.PREFERENCES_KEY_RECENT_COUNT + Util.getActiveServer(context), 0);
+ setMostRecentCount(startCount);
+
+ new SilentBackgroundTask<Integer>(context) {
+ @Override
+ public Integer doInBackground() throws Exception {
+ String recentAddedFile = Util.getCacheName(context, "recent_count");
+ ArrayList<String> recents = FileUtil.deserialize(context, recentAddedFile, ArrayList.class);
+ if(recents == null) {
+ recents = new ArrayList<String>();
+ }
+
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ MusicDirectory recentlyAdded = musicService.getAlbumList("newest", 20, 0, context, null);
+
+ // If first run, just put everything in it and return 0
+ boolean firstRun = recents.isEmpty();
+
+ // Count how many new albums are in the list
+ int count = 0;
+ for(MusicDirectory.Entry album: recentlyAdded.getChildren()) {
+ if(!recents.contains(album.getId())) {
+ recents.add(album.getId());
+ count++;
+ }
+ }
+
+ // Keep recents list from growing infinitely
+ while(recents.size() > 40) {
+ recents.remove(0);
+ }
+ FileUtil.serialize(context, recents, recentAddedFile);
+
+ if(firstRun) {
+ return 0;
+ } else {
+ // Add the old count which will get cleared out after viewing recents
+ count += startCount;
+ SharedPreferences.Editor editor = Util.getPreferences(context).edit();
+ editor.putInt(Constants.PREFERENCES_KEY_RECENT_COUNT + Util.getActiveServer(context), count);
+ editor.commit();
+
+ return count;
+ }
+ }
+
+ @Override
+ public void done(Integer result) {
+ setMostRecentCount(result);
+ }
+
+ @Override
+ public void error(Throwable x) {
+ Log.w(TAG, "Failed to refresh most recent count", x);
+ }
+ }.execute();
+ }
+
+ private void setMostRecentCount(int count) {
+ if(count <= 0) {
+ countView.setVisibility(View.GONE);
+ } else {
+ String displayValue;
+ if(count < 10) {
+ displayValue = "0" + count;
+ } else {
+ displayValue = "" + count;
+ }
+
+ countView.setText(displayValue);
+ countView.setVisibility(View.VISIBLE);
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/NowPlayingFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/NowPlayingFragment.java
new file mode 100644
index 00000000..fa7e3404
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/fragments/NowPlayingFragment.java
@@ -0,0 +1,1568 @@
+/*
+ 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.fragments;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.Configuration;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.v4.view.MenuItemCompat;
+import android.support.v7.app.MediaRouteButton;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.Display;
+import android.view.GestureDetector;
+import android.view.GestureDetector.OnGestureListener;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.view.animation.AnimationUtils;
+import android.widget.AdapterView;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.SeekBar;
+import android.widget.TextView;
+import android.widget.ViewFlipper;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.activity.SubsonicFragmentActivity;
+import github.daneren2005.dsub.audiofx.EqualizerController;
+import github.daneren2005.dsub.domain.Bookmark;
+import github.daneren2005.dsub.domain.PlayerState;
+import github.daneren2005.dsub.domain.RepeatMode;
+import github.daneren2005.dsub.domain.ServerInfo;
+import github.daneren2005.dsub.service.DownloadFile;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import github.daneren2005.dsub.service.OfflineException;
+import github.daneren2005.dsub.service.ServerTooOldException;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.SilentBackgroundTask;
+import github.daneren2005.dsub.adapter.DownloadFileAdapter;
+import github.daneren2005.dsub.view.FadeOutAnimation;
+import github.daneren2005.dsub.view.UpdateView;
+import github.daneren2005.dsub.util.Util;
+
+import static github.daneren2005.dsub.domain.MusicDirectory.Entry;
+import static github.daneren2005.dsub.domain.PlayerState.*;
+import github.daneren2005.dsub.util.*;
+import github.daneren2005.dsub.view.AutoRepeatButton;
+import java.util.ArrayList;
+import java.util.concurrent.ScheduledFuture;
+import com.mobeta.android.dslv.*;
+import github.daneren2005.dsub.activity.SubsonicActivity;
+
+public class NowPlayingFragment extends SubsonicFragment implements OnGestureListener {
+ private static final String TAG = NowPlayingFragment.class.getSimpleName();
+ private static final int PERCENTAGE_OF_SCREEN_FOR_SWIPE = 10;
+ private static final int INCREMENT_TIME = 5000;
+ private static final int SERVICE_BACKOFF = 200;
+
+ private static final int ACTION_PREVIOUS = 1;
+ private static final int ACTION_NEXT = 2;
+ private static final int ACTION_REWIND = 3;
+ private static final int ACTION_FORWARD = 4;
+
+ private ViewFlipper playlistFlipper;
+ private TextView emptyTextView;
+ private TextView songTitleTextView;
+ private ImageView albumArtImageView;
+ private DragSortListView playlistView;
+ private TextView positionTextView;
+ private TextView durationTextView;
+ private TextView statusTextView;
+ private SeekBar progressBar;
+ private AutoRepeatButton previousButton;
+ private AutoRepeatButton nextButton;
+ private View pauseButton;
+ private View stopButton;
+ private View startButton;
+ private ImageButton repeatButton;
+ private View toggleListButton;
+ private ImageButton starButton;
+ private ImageButton bookmarkButton;
+ private ImageButton rateBadButton;
+ private ImageButton rateGoodButton;
+ private View mainLayout;
+ private ScheduledExecutorService executorService;
+ private DownloadFile currentPlaying;
+ private long currentRevision;
+ private int swipeDistance;
+ private int swipeVelocity;
+ private ScheduledFuture<?> hideControlsFuture;
+ private List<DownloadFile> songList;
+ private DownloadFileAdapter songListAdapter;
+ private SilentBackgroundTask<Void> onProgressChangedTask;
+ private SilentBackgroundTask<Void> onCurrentChangedTask;
+ private SilentBackgroundTask<Void> onDownloadListChangedTask;
+ private boolean seekInProgress = false;
+ private boolean startFlipped = false;
+ private boolean scrollWhenLoaded = false;
+ private int lastY = 0;
+
+ /**
+ * Called when the activity is first created.
+ */
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ if(savedInstanceState != null) {
+ if(savedInstanceState.getInt(Constants.FRAGMENT_DOWNLOAD_FLIPPER) == 1) {
+ startFlipped = true;
+ }
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putInt(Constants.FRAGMENT_DOWNLOAD_FLIPPER, playlistFlipper.getDisplayedChild());
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
+ rootView = inflater.inflate(R.layout.download, container, false);
+ setTitle(R.string.button_bar_now_playing);
+
+ mainLayout = rootView.findViewById(R.id.download_layout);
+ if(!primaryFragment) {
+ mainLayout.setVisibility(View.GONE);
+ }
+
+ WindowManager w = context.getWindowManager();
+ Display d = w.getDefaultDisplay();
+ swipeDistance = (d.getWidth() + d.getHeight()) * PERCENTAGE_OF_SCREEN_FOR_SWIPE / 100;
+ swipeVelocity = (d.getWidth() + d.getHeight()) * PERCENTAGE_OF_SCREEN_FOR_SWIPE / 100;
+ gestureScanner = new GestureDetector(this);
+
+ playlistFlipper = (ViewFlipper)rootView.findViewById(R.id.download_playlist_flipper);
+ emptyTextView = (TextView)rootView.findViewById(R.id.download_empty);
+ songTitleTextView = (TextView)rootView.findViewById(R.id.download_song_title);
+ albumArtImageView = (ImageView)rootView.findViewById(R.id.download_album_art_image);
+ positionTextView = (TextView)rootView.findViewById(R.id.download_position);
+ durationTextView = (TextView)rootView.findViewById(R.id.download_duration);
+ statusTextView = (TextView)rootView.findViewById(R.id.download_status);
+ progressBar = (SeekBar)rootView.findViewById(R.id.download_progress_bar);
+ playlistView = (DragSortListView)rootView.findViewById(R.id.download_list);
+ previousButton = (AutoRepeatButton)rootView.findViewById(R.id.download_previous);
+ nextButton = (AutoRepeatButton)rootView.findViewById(R.id.download_next);
+ pauseButton =rootView.findViewById(R.id.download_pause);
+ stopButton =rootView.findViewById(R.id.download_stop);
+ startButton =rootView.findViewById(R.id.download_start);
+ repeatButton = (ImageButton)rootView.findViewById(R.id.download_repeat);
+ bookmarkButton = (ImageButton) rootView.findViewById(R.id.download_bookmark);
+ rateBadButton = (ImageButton) rootView.findViewById(R.id.download_rating_bad);
+ rateGoodButton = (ImageButton) rootView.findViewById(R.id.download_rating_good);
+ toggleListButton =rootView.findViewById(R.id.download_toggle_list);
+
+ starButton = (ImageButton)rootView.findViewById(R.id.download_star);
+ if(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_MENU_STAR, true)) {
+ starButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ DownloadFile currentDownload = getDownloadService().getCurrentPlaying();
+ if (currentDownload != null) {
+ final Entry currentSong = currentDownload.getSong();
+ toggleStarred(currentSong, new OnStarChange() {
+ @Override
+ void starChange(boolean starred) {
+ starButton.setImageResource(currentSong.isStarred() ? android.R.drawable.btn_star_big_on : android.R.drawable.btn_star_big_off);
+ }
+ });
+ }
+ }
+ });
+ } else {
+ starButton.setVisibility(View.GONE);
+ }
+
+ View.OnTouchListener touchListener = new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent me) {
+ return gestureScanner.onTouchEvent(me);
+ }
+ };
+ pauseButton.setOnTouchListener(touchListener);
+ stopButton.setOnTouchListener(touchListener);
+ startButton.setOnTouchListener(touchListener);
+ bookmarkButton.setOnTouchListener(touchListener);
+ rateBadButton.setOnTouchListener(touchListener);
+ rateGoodButton.setOnTouchListener(touchListener);
+ emptyTextView.setOnTouchListener(touchListener);
+ albumArtImageView.setOnTouchListener(new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent me) {
+ if(me.getAction() == MotionEvent.ACTION_DOWN) {
+ lastY = (int) me.getRawY();
+ }
+ return gestureScanner.onTouchEvent(me);
+ }
+ });
+
+ previousButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ warnIfStorageUnavailable();
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ getDownloadService().previous();
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ onCurrentChanged();
+ onProgressChanged();
+ }
+ }.execute();
+ setControlsVisible(true);
+ }
+ });
+ previousButton.setOnRepeatListener(new Runnable() {
+ public void run() {
+ changeProgress(-INCREMENT_TIME);
+ }
+ });
+
+ nextButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ warnIfStorageUnavailable();
+ new SilentBackgroundTask<Boolean>(context) {
+ @Override
+ protected Boolean doInBackground() throws Throwable {
+ getDownloadService().next();
+ return true;
+ }
+
+ @Override
+ protected void done(Boolean result) {
+ if(result) {
+ onCurrentChanged();
+ onProgressChanged();
+ }
+ }
+ }.execute();
+ setControlsVisible(true);
+ }
+ });
+ nextButton.setOnRepeatListener(new Runnable() {
+ public void run() {
+ changeProgress(INCREMENT_TIME);
+ }
+ });
+
+ pauseButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ getDownloadService().pause();
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ onCurrentChanged();
+ onProgressChanged();
+ }
+ }.execute();
+ }
+ });
+
+ stopButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ getDownloadService().reset();
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ onCurrentChanged();
+ onProgressChanged();
+ }
+ }.execute();
+ }
+ });
+
+ startButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ warnIfStorageUnavailable();
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ start();
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ onCurrentChanged();
+ onProgressChanged();
+ }
+ }.execute();
+ }
+ });
+
+ repeatButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ RepeatMode repeatMode = getDownloadService().getRepeatMode().next();
+ getDownloadService().setRepeatMode(repeatMode);
+ onDownloadListChanged();
+ switch (repeatMode) {
+ case OFF:
+ Util.toast(context, R.string.download_repeat_off);
+ break;
+ case ALL:
+ Util.toast(context, R.string.download_repeat_all);
+ break;
+ case SINGLE:
+ Util.toast(context, R.string.download_repeat_single);
+ break;
+ default:
+ break;
+ }
+ setControlsVisible(true);
+ }
+ });
+
+ bookmarkButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ createBookmark();
+ }
+ });
+
+ rateBadButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ DownloadService downloadService = getDownloadService();
+ if(downloadService == null) {
+ return;
+ }
+
+ DownloadFile downloadFile = downloadService.getCurrentPlaying();
+ if(downloadFile == null) {
+ return;
+ }
+ Entry entry = downloadFile.getSong();
+
+ // If rating == 1, already set so unset
+ if(entry.getRating() == 1) {
+ setRating(entry, 0);
+
+ if(context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
+ rateBadButton.setImageResource(R.drawable.ic_action_rating_bad_dark);
+ } else {
+ rateBadButton.setImageResource(Util.getAttribute(context, R.attr.rating_bad));
+ }
+ } else {
+ // Immediately skip to the next song
+ downloadService.next(true);
+
+ // Otherwise set rating to 1
+ setRating(entry, 1);
+ rateBadButton.setImageResource(R.drawable.ic_action_rating_bad_selected);
+
+ // Make sure good rating is blank
+ if (context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
+ rateGoodButton.setImageResource(R.drawable.ic_action_rating_good_dark);
+ } else {
+ rateGoodButton.setImageResource(Util.getAttribute(context, R.attr.rating_good));
+ }
+ }
+ }
+ });
+ rateGoodButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ DownloadService downloadService = getDownloadService();
+ if(downloadService == null) {
+ return;
+ }
+
+ DownloadFile downloadFile = downloadService.getCurrentPlaying();
+ if(downloadFile == null) {
+ return;
+ }
+ Entry entry = downloadFile.getSong();
+
+ // If rating == 5, already set so unset
+ if(entry.getRating() == 5) {
+ setRating(entry, 0);
+
+ if (context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
+ rateGoodButton.setImageResource(R.drawable.ic_action_rating_good_dark);
+ } else {
+ rateGoodButton.setImageResource(Util.getAttribute(context, R.attr.rating_good));
+ }
+ } else {
+ // Otherwise set rating to maximum
+ setRating(entry, 5);
+ rateGoodButton.setImageResource(R.drawable.ic_action_rating_good_selected);
+
+ // Make sure bad rating is blank
+ if (context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
+ rateBadButton.setImageResource(R.drawable.ic_action_rating_bad_dark);
+ } else {
+ rateBadButton.setImageResource(Util.getAttribute(context, R.attr.rating_bad));
+ }
+ }
+ }
+ });
+
+ toggleListButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ toggleFullscreenAlbumArt();
+ setControlsVisible(true);
+ }
+ });
+
+ View overlay = rootView.findViewById(R.id.download_overlay_buttons);
+ final int overlayHeight = overlay != null ? overlay.getHeight() : -1;
+ albumArtImageView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if(overlayHeight == -1 || lastY < (view.getBottom() - overlayHeight)) {
+ toggleFullscreenAlbumArt();
+ setControlsVisible(true);
+ }
+ }
+ });
+
+ progressBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onStopTrackingTouch(final SeekBar seekBar) {
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ getDownloadService().seekTo(progressBar.getProgress());
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ seekInProgress = false;
+ NowPlayingFragment.this.onProgressChanged();
+ }
+ }.execute();
+ }
+
+ @Override
+ public void onStartTrackingTouch(final SeekBar seekBar) {
+ seekInProgress = true;
+ }
+
+ @Override
+ public void onProgressChanged(final SeekBar seekBar, final int position, final boolean fromUser) {
+ if (fromUser) {
+ Util.toast(context, Util.formatDuration(position / 1000), true);
+ setControlsVisible(true);
+ }
+ }
+ });
+ playlistView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, final int position, long id) {
+ warnIfStorageUnavailable();
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ getDownloadService().play(position);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ onCurrentChanged();
+ onProgressChanged();
+ }
+ }.execute();
+ }
+ });
+ playlistView.setDropListener(new DragSortListView.DropListener() {
+ @Override
+ public void drop(final int from, final int to) {
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ getDownloadService().swap(true, from, to);
+ onDownloadListChanged();
+
+ return null;
+ }
+ }.execute();
+ }
+ });
+ playlistView.setRemoveListener(new DragSortListView.RemoveListener() {
+ @Override
+ public void remove(int which) {
+ getDownloadService().remove(which);
+ onDownloadListChanged();
+ }
+ });
+
+ registerForContextMenu(playlistView);
+
+ DownloadService downloadService = getDownloadService();
+ if (downloadService != null && context.getIntent().getBooleanExtra(Constants.INTENT_EXTRA_NAME_SHUFFLE, false)) {
+ context.getIntent().removeExtra(Constants.INTENT_EXTRA_NAME_SHUFFLE);
+ warnIfStorageUnavailable();
+ downloadService.setShufflePlayEnabled(true);
+ }
+
+ if(Build.MODEL.equals("Nexus 4") || Build.MODEL.equals("GT-I9100")) {
+ View slider = rootView.findViewById(R.id.download_slider);
+ slider.setPadding(0, 0, 0, 0);
+ }
+
+ return rootView;
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) {
+ DownloadService downloadService = getDownloadService();
+ if(Util.isOffline(context)) {
+ menuInflater.inflate(R.menu.nowplaying_offline, menu);
+ } else {
+ menuInflater.inflate(R.menu.nowplaying, menu);
+
+ if(downloadService != null && downloadService.getSleepTimer()) {
+ menu.findItem(R.id.menu_toggle_timer).setTitle(R.string.download_stop_timer);
+ }
+ }
+ if(downloadService != null && downloadService.getKeepScreenOn()) {
+ menu.findItem(R.id.menu_screen_on_off).setChecked(true);
+ }
+ if(downloadService != null && downloadService.isRemovePlayed()) {
+ menu.findItem(R.id.menu_remove_played).setChecked(true);
+ }
+
+ boolean equalizerAvailable = downloadService != null && downloadService.getEqualizerAvailable();
+ if(equalizerAvailable && !downloadService.isRemoteEnabled()) {
+ SharedPreferences prefs = Util.getPreferences(context);
+ boolean equalizerOn = prefs.getBoolean(Constants.PREFERENCES_EQUALIZER_ON, false);
+ if (equalizerOn && getDownloadService() != null && getDownloadService().getEqualizerController() != null &&
+ getDownloadService().getEqualizerController().isEnabled()) {
+ menu.findItem(R.id.menu_equalizer).setChecked(true);
+ }
+ } else {
+ menu.removeItem(R.id.menu_equalizer);
+ }
+
+ if(downloadService != null) {
+ MenuItem mediaRouteItem = menu.findItem(R.id.menu_mediaroute);
+ if(mediaRouteItem != null) {
+ MediaRouteButton mediaRouteButton = (MediaRouteButton) MenuItemCompat.getActionView(mediaRouteItem);
+ mediaRouteButton.setRouteSelector(downloadService.getRemoteSelector());
+ }
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem menuItem) {
+ if(menuItemSelected(menuItem.getItemId(), null)) {
+ return true;
+ }
+
+ return super.onOptionsItemSelected(menuItem);
+ }
+
+ @Override
+ public void onCreateContextMenu(android.view.ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, view, menuInfo);
+ if(!primaryFragment) {
+ return;
+ }
+
+ if (view == playlistView) {
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
+ DownloadFile downloadFile = (DownloadFile) playlistView.getItemAtPosition(info.position);
+
+ android.view.MenuInflater inflater = context.getMenuInflater();
+ if(Util.isOffline(context)) {
+ inflater.inflate(R.menu.nowplaying_context_offline, menu);
+ } else {
+ inflater.inflate(R.menu.nowplaying_context, menu);
+ menu.findItem(R.id.menu_star).setTitle(downloadFile.getSong().isStarred() ? R.string.common_unstar : R.string.common_star);
+ }
+
+ if (downloadFile.getSong().getParent() == null) {
+ menu.findItem(R.id.menu_show_album).setVisible(false);
+ menu.findItem(R.id.menu_show_artist).setVisible(false);
+ }
+
+ hideMenuItems(menu, (AdapterView.AdapterContextMenuInfo) menuInfo);
+ }
+ }
+
+ @Override
+ public boolean onContextItemSelected(android.view.MenuItem menuItem) {
+ if(!primaryFragment) {
+ return false;
+ }
+
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
+ DownloadFile downloadFile = (DownloadFile) playlistView.getItemAtPosition(info.position);
+ return menuItemSelected(menuItem.getItemId(), downloadFile) || super.onContextItemSelected(menuItem);
+ }
+
+ private boolean menuItemSelected(int menuItemId, final DownloadFile song) {
+ switch (menuItemId) {
+ case R.id.menu_show_album: case R.id.menu_show_artist:
+ Entry entry = song.getSong();
+
+ Intent intent = new Intent(context, SubsonicFragmentActivity.class);
+ intent.putExtra(Constants.INTENT_EXTRA_VIEW_ALBUM, true);
+ String albumId;
+ String albumName;
+ if(menuItemId == R.id.menu_show_album) {
+ if(Util.isTagBrowsing(context)) {
+ albumId = entry.getAlbumId();
+ } else {
+ albumId = entry.getParent();
+ }
+ albumName = entry.getAlbum();
+ } else {
+ if(Util.isTagBrowsing(context)) {
+ albumId = entry.getArtistId();
+ } else {
+ albumId = entry.getGrandParent();
+ if(albumId == null) {
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_CHILD_ID, entry.getParent());
+ }
+ }
+ albumName = entry.getArtist();
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_ARTIST, true);
+ }
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_ID, albumId);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_NAME, albumName);
+ intent.putExtra(Constants.INTENT_EXTRA_FRAGMENT_TYPE, "Artist");
+
+ if(Util.isOffline(context)) {
+ try {
+ // This should only be successful if this is a online song in offline mode
+ Integer.parseInt(entry.getParent());
+ String root = FileUtil.getMusicDirectory(context).getPath();
+ String id = root + "/" + entry.getPath();
+ id = id.substring(0, id.lastIndexOf("/"));
+ if(menuItemId == R.id.menu_show_album) {
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_ID, id);
+ }
+ id = id.substring(0, id.lastIndexOf("/"));
+ if(menuItemId != R.id.menu_show_album) {
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_ID, id);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_NAME, entry.getArtist());
+ intent.removeExtra(Constants.INTENT_EXTRA_NAME_CHILD_ID);
+ }
+ } catch(Exception e) {
+ // Do nothing, entry.getParent() is fine
+ }
+ }
+
+ intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ Util.startActivityWithoutTransition(context, intent);
+ return true;
+ case R.id.menu_lyrics: {
+ SubsonicFragment fragment = new LyricsFragment();
+ Bundle args = new Bundle();
+ args.putString(Constants.INTENT_EXTRA_NAME_ARTIST, song.getSong().getArtist());
+ args.putString(Constants.INTENT_EXTRA_NAME_TITLE, song.getSong().getTitle());
+ fragment.setArguments(args);
+
+ replaceFragment(fragment);
+ return true;
+ } case R.id.menu_remove:
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ getDownloadService().remove(song);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ onDownloadListChanged();
+ }
+ }.execute();
+ return true;
+ case R.id.menu_delete:
+ List<Entry> songs = new ArrayList<Entry>(1);
+ songs.add(song.getSong());
+ getDownloadService().delete(songs);
+ return true;
+ case R.id.menu_remove_all:
+ Util.confirmDialog(context, R.string.download_menu_remove_all, "", new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ getDownloadService().setShufflePlayEnabled(false);
+ getDownloadService().clear();
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ onDownloadListChanged();
+ }
+ }.execute();
+ }
+ });
+ return true;
+ case R.id.menu_screen_on_off:
+ if (getDownloadService().getKeepScreenOn()) {
+ context.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ getDownloadService().setKeepScreenOn(false);
+ } else {
+ context.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ getDownloadService().setKeepScreenOn(true);
+ }
+ context.supportInvalidateOptionsMenu();
+ return true;
+ case R.id.menu_remove_played:
+ if (getDownloadService().isRemovePlayed()) {
+ getDownloadService().setRemovePlayed(false);
+ } else {
+ getDownloadService().setRemovePlayed(true);
+ }
+ context.supportInvalidateOptionsMenu();
+ return true;
+ case R.id.menu_shuffle:
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ getDownloadService().shuffle();
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ Util.toast(context, R.string.download_menu_shuffle_notification);
+ }
+ }.execute();
+ return true;
+ case R.id.menu_save_playlist:
+ List<Entry> entries = new LinkedList<Entry>();
+ for (DownloadFile downloadFile : getDownloadService().getSongs()) {
+ entries.add(downloadFile.getSong());
+ }
+ createNewPlaylist(entries, true);
+ return true;
+ case R.id.menu_star:
+ toggleStarred(song.getSong());
+ return true;
+ case R.id.menu_rate:
+ setRating(song.getSong());
+ return true;
+ case R.id.menu_toggle_timer:
+ if(getDownloadService().getSleepTimer()) {
+ getDownloadService().stopSleepTimer();
+ context.supportInvalidateOptionsMenu();
+ } else {
+ startTimer();
+ }
+ return true;
+ case R.id.menu_add_playlist:
+ songs = new ArrayList<Entry>(1);
+ songs.add(song.getSong());
+ addToPlaylist(songs);
+ return true;
+ case R.id.menu_info:
+ displaySongInfo(song.getSong());
+ return true;
+ case R.id.menu_share:
+ songs = new ArrayList<Entry>(1);
+ songs.add(song.getSong());
+ createShare(songs);
+ return true;
+ case R.id.menu_equalizer: {
+ DownloadService downloadService = getDownloadService();
+ if (downloadService != null) {
+ EqualizerController controller = downloadService.getEqualizerController();
+ if(controller != null) {
+ SubsonicFragment fragment = new EqualizerFragment();
+ replaceFragment(fragment);
+ setControlsVisible(true);
+
+ return true;
+ }
+ }
+
+ // Any failed condition will get here
+ Util.toast(context, "Failed to start equalizer. Try restarting.");
+ return true;
+ } default:
+ return false;
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ final Handler handler = new Handler();
+ Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ update();
+ }
+ });
+ }
+ };
+
+ executorService = Executors.newSingleThreadScheduledExecutor();
+ executorService.scheduleWithFixedDelay(runnable, 0L, 1000L, TimeUnit.MILLISECONDS);
+
+ setControlsVisible(true);
+
+ DownloadService downloadService = getDownloadService();
+ if (downloadService == null || downloadService.getCurrentPlaying() == null || startFlipped) {
+ playlistFlipper.setDisplayedChild(1);
+ }
+ if (downloadService != null && downloadService.getKeepScreenOn()) {
+ context.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ } else {
+ context.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ }
+
+ updateButtons();
+
+ if(currentPlaying == null && downloadService != null && currentPlaying == downloadService.getCurrentPlaying()) {
+ getImageLoader().loadImage(albumArtImageView, (Entry) null, true, false);
+ }
+ if(downloadService != null) {
+ downloadService.startRemoteScan();
+ } else {
+ // Make sure to call remote scan once the service is ready
+ final Runnable waitForService = new Runnable() {
+ @Override
+ public void run() {
+ DownloadService service = getDownloadService();
+ if(service != null) {
+ service.startRemoteScan();
+ } else {
+ handler.postDelayed(this, SERVICE_BACKOFF);
+ }
+ }
+ };
+
+ handler.postDelayed(waitForService, SERVICE_BACKOFF);
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ executorService.shutdown();
+ if(getDownloadService() != null) {
+ getDownloadService().stopRemoteScan();
+ }
+ }
+
+ @Override
+ public void setPrimaryFragment(boolean primary) {
+ super.setPrimaryFragment(primary);
+ if(rootView != null) {
+ if(primary) {
+ mainLayout.setVisibility(View.VISIBLE);
+ updateButtons();
+ } else {
+ mainLayout.setVisibility(View.GONE);
+ }
+ }
+ }
+
+ private void scheduleHideControls() {
+ if (hideControlsFuture != null) {
+ hideControlsFuture.cancel(false);
+ }
+
+ final Handler handler = new Handler();
+ Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ setControlsVisible(false);
+ }
+ });
+ }
+ };
+ hideControlsFuture = executorService.schedule(runnable, 3000L, TimeUnit.MILLISECONDS);
+ }
+
+ private void setControlsVisible(boolean visible) {
+ try {
+ long duration = 1700L;
+ FadeOutAnimation.createAndStart(rootView.findViewById(R.id.download_overlay_buttons), !visible, duration);
+
+ if (visible) {
+ scheduleHideControls();
+ }
+ } catch(Exception e) {
+
+ }
+ }
+
+ private void updateButtons() {
+ if(context == null) {
+ return;
+ }
+
+ if(Util.isOffline(context)) {
+ bookmarkButton.setVisibility(View.GONE);
+ rateBadButton.setVisibility(View.GONE);
+ rateGoodButton.setVisibility(View.GONE);
+ } else {
+ if(ServerInfo.canBookmark(context)) {
+ bookmarkButton.setVisibility(View.VISIBLE);
+ } else {
+ bookmarkButton.setVisibility(View.GONE);
+ }
+ rateBadButton.setVisibility(View.VISIBLE);
+ rateGoodButton.setVisibility(View.VISIBLE);
+ }
+ }
+
+ // Scroll to current playing/downloading.
+ private void scrollToCurrent() {
+ if (getDownloadService() == null || songListAdapter == null) {
+ scrollWhenLoaded = true;
+ return;
+ }
+
+ for (int i = 0; i < songListAdapter.getCount(); i++) {
+ if (currentPlaying == playlistView.getItemAtPosition(i)) {
+ playlistView.setSelectionFromTop(i, 40);
+ return;
+ }
+ }
+ DownloadFile currentDownloading = getDownloadService().getCurrentDownloading();
+ for (int i = 0; i < songListAdapter.getCount(); i++) {
+ if (currentDownloading == playlistView.getItemAtPosition(i)) {
+ playlistView.setSelectionFromTop(i, 40);
+ return;
+ }
+ }
+ }
+
+ private void update() {
+ if (getDownloadService() == null) {
+ return;
+ }
+
+ if (currentRevision != getDownloadService().getDownloadListUpdateRevision()) {
+ onDownloadListChanged();
+ }
+
+ if (currentPlaying != getDownloadService().getCurrentPlaying()) {
+ onCurrentChanged();
+ }
+
+ if(startFlipped) {
+ startFlipped = false;
+ scrollToCurrent();
+ }
+
+ onProgressChanged();
+ }
+
+ protected void startTimer() {
+ View dialogView = context.getLayoutInflater().inflate(R.layout.start_timer, null);
+
+ // Setup length label
+ final TextView lengthBox = (TextView) dialogView.findViewById(R.id.timer_length_label);
+ final SharedPreferences prefs = Util.getPreferences(context);
+ String lengthString = prefs.getString(Constants.PREFERENCES_KEY_SLEEP_TIMER_DURATION, "5");
+ int length = Integer.parseInt(lengthString);
+ lengthBox.setText(Util.formatDuration(length));
+
+ // Setup length slider
+ final SeekBar lengthBar = (SeekBar) dialogView.findViewById(R.id.timer_length_bar);
+ lengthBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ if (fromUser) {
+ int length = getMinutes(progress);
+ lengthBox.setText(Util.formatDuration(length));
+ seekBar.setProgress(progress);
+ }
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ }
+ });
+ lengthBar.setProgress(length - 1);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.menu_set_timer)
+ .setView(dialogView)
+ .setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ int length = getMinutes(lengthBar.getProgress());
+
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putString(Constants.PREFERENCES_KEY_SLEEP_TIMER_DURATION, Integer.toString(length));
+ editor.commit();
+
+ getDownloadService().setSleepTimerDuration(length);
+ getDownloadService().startSleepTimer();
+ context.supportInvalidateOptionsMenu();
+ }
+ })
+ .setNegativeButton(R.string.common_cancel, null);
+ AlertDialog dialog = builder.create();
+ dialog.show();
+ }
+
+ private int getMinutes(int progress) {
+ if(progress < 30) {
+ return progress + 1;
+ } else if(progress < 61) {
+ return (progress - 30) * 5 + getMinutes(29);
+ } else {
+ return (progress - 61) * 15 + getMinutes(60);
+ }
+ }
+
+ private void toggleFullscreenAlbumArt() {
+ if (playlistFlipper.getDisplayedChild() == 1) {
+ playlistFlipper.setInAnimation(AnimationUtils.loadAnimation(context, R.anim.push_down_in));
+ playlistFlipper.setOutAnimation(AnimationUtils.loadAnimation(context, R.anim.push_down_out));
+ playlistFlipper.setDisplayedChild(0);
+ } else {
+ scrollToCurrent();
+ playlistFlipper.setInAnimation(AnimationUtils.loadAnimation(context, R.anim.push_up_in));
+ playlistFlipper.setOutAnimation(AnimationUtils.loadAnimation(context, R.anim.push_up_out));
+ playlistFlipper.setDisplayedChild(1);
+
+ UpdateView.triggerUpdate();
+ }
+ }
+
+ private void start() {
+ DownloadService service = getDownloadService();
+ PlayerState state = service.getPlayerState();
+ if (state == PAUSED || state == COMPLETED || state == STOPPED) {
+ service.start();
+ } else if (state == STOPPED || state == IDLE) {
+ warnIfStorageUnavailable();
+ int current = service.getCurrentPlayingIndex();
+ // TODO: Use play() method.
+ if (current == -1) {
+ service.play(0);
+ } else {
+ service.play(current);
+ }
+ }
+ }
+ private void onDownloadListChanged() {
+ onDownloadListChanged(false);
+ }
+ private void onDownloadListChanged(final boolean refresh) {
+ final DownloadService downloadService = getDownloadService();
+ if (downloadService == null || onDownloadListChangedTask != null) {
+ return;
+ }
+
+ onDownloadListChangedTask = new SilentBackgroundTask<Void>(context) {
+ int currentPlayingIndex;
+ int size;
+
+ @Override
+ protected Void doInBackground() throws Throwable {
+ currentPlayingIndex = downloadService.getCurrentPlayingIndex() + 1;
+ size = downloadService.size();
+
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ List<DownloadFile> list;
+ list = downloadService.getSongs();
+
+ if(downloadService.isShufflePlayEnabled()) {
+ emptyTextView.setText(R.string.download_shuffle_loading);
+ }
+ else {
+ emptyTextView.setText(R.string.download_empty);
+ }
+
+ if(songListAdapter == null || refresh) {
+ songList = new ArrayList<DownloadFile>();
+ songList.addAll(list);
+ playlistView.setAdapter(songListAdapter = new DownloadFileAdapter(context, songList));
+ } else {
+ songList.clear();
+ songList.addAll(list);
+ songListAdapter.notifyDataSetChanged();
+ }
+
+ emptyTextView.setVisibility(list.isEmpty() ? View.VISIBLE : View.GONE);
+ currentRevision = downloadService.getDownloadListUpdateRevision();
+
+ switch (downloadService.getRepeatMode()) {
+ case OFF:
+ if("light".equals(SubsonicActivity.getThemeName()) | "light_fullscreen".equals(SubsonicActivity.getThemeName())) {
+ repeatButton.setImageResource(R.drawable.media_repeat_off_light);
+ } else {
+ repeatButton.setImageResource(R.drawable.media_repeat_off);
+ }
+ break;
+ case ALL:
+ repeatButton.setImageResource(R.drawable.media_repeat_all);
+ break;
+ case SINGLE:
+ repeatButton.setImageResource(R.drawable.media_repeat_single);
+ break;
+ default:
+ break;
+ }
+
+ if(scrollWhenLoaded) {
+ scrollToCurrent();
+ scrollWhenLoaded = false;
+ }
+
+ setSubtitle(context.getResources().getString(R.string.download_playing_out_of, currentPlayingIndex, size));
+ onDownloadListChangedTask = null;
+ if(onCurrentChangedTask != null) {
+ onCurrentChangedTask.execute();
+ } else if(onProgressChangedTask != null) {
+ onProgressChangedTask.execute();
+ }
+ }
+ };
+ onDownloadListChangedTask.execute();
+ }
+
+ private void onCurrentChanged() {
+ final DownloadService downloadService = getDownloadService();
+ if (downloadService == null || onCurrentChangedTask != null) {
+ return;
+ }
+
+ onCurrentChangedTask = new SilentBackgroundTask<Void>(context) {
+ int currentPlayingIndex;
+ int currentPlayingSize;
+
+ @Override
+ protected Void doInBackground() throws Throwable {
+ currentPlaying = downloadService.getCurrentPlaying();
+ currentPlayingIndex = downloadService.getCurrentPlayingIndex() + 1;
+ currentPlayingSize = downloadService.size();
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ if (currentPlaying != null) {
+ Entry song = currentPlaying.getSong();
+ songTitleTextView.setText(song.getTitle());
+ getImageLoader().loadImage(albumArtImageView, song, true, true);
+ starButton.setImageResource(song.isStarred() ? android.R.drawable.btn_star_big_on : android.R.drawable.btn_star_big_off);
+ setSubtitle(context.getResources().getString(R.string.download_playing_out_of, currentPlayingIndex, currentPlayingSize));
+
+ int badRating, goodRating, bookmark;
+ if(song.getRating() == 1) {
+ badRating = R.drawable.ic_action_rating_bad_selected;
+ } else if(context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
+ badRating = R.drawable.ic_action_rating_bad_dark;
+ } else {
+ badRating = Util.getAttribute(context, R.attr.rating_bad);
+ }
+ rateBadButton.setImageResource(badRating);
+
+ if(song.getRating() == 5) {
+ goodRating = R.drawable.ic_action_rating_good_selected;
+ } else if(context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
+ goodRating = R.drawable.ic_action_rating_good_dark;
+ } else {
+ goodRating = Util.getAttribute(context, R.attr.rating_good);
+ }
+ rateGoodButton.setImageResource(goodRating);
+
+ if(song.getBookmark() != null) {
+ bookmark = R.drawable.ic_menu_bookmark_selected;
+ } else if(context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
+ bookmark = R.drawable.ic_menu_bookmark_dark;
+ } else {
+ bookmark = Util.getAttribute(context, R.attr.bookmark);
+ }
+ bookmarkButton.setImageResource(bookmark);
+ } else {
+ songTitleTextView.setText(null);
+ getImageLoader().loadImage(albumArtImageView, (Entry) null, true, false);
+ starButton.setImageResource(android.R.drawable.btn_star_big_off);
+ setSubtitle(null);
+ }
+ onCurrentChangedTask = null;
+ if(onProgressChangedTask != null) {
+ onProgressChangedTask.execute();
+ }
+ }
+ };
+
+ if(onDownloadListChangedTask == null) {
+ onCurrentChangedTask.execute();
+ }
+ }
+
+ private void onProgressChanged() {
+ // Make sure to only be trying to run one of these at a time
+ if (getDownloadService() == null || onProgressChangedTask != null) {
+ return;
+ }
+
+ onProgressChangedTask = new SilentBackgroundTask<Void>(context) {
+ DownloadService downloadService;
+ int millisPlayed;
+ Integer duration;
+ PlayerState playerState;
+ boolean isSeekable;
+
+ @Override
+ protected Void doInBackground() throws Throwable {
+ downloadService = getDownloadService();
+ millisPlayed = Math.max(0, downloadService.getPlayerPosition());
+ duration = downloadService.getPlayerDuration();
+ playerState = getDownloadService().getPlayerState();
+ isSeekable = downloadService.isSeekable();
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ if (currentPlaying != null) {
+ int millisTotal = duration == null ? 0 : duration;
+
+ positionTextView.setText(Util.formatDuration(millisPlayed / 1000));
+ if(millisTotal > 0) {
+ durationTextView.setText(Util.formatDuration(millisTotal / 1000));
+ } else {
+ durationTextView.setText("-:--");
+ }
+ progressBar.setMax(millisTotal == 0 ? 100 : millisTotal); // Work-around for apparent bug.
+ if(!seekInProgress) {
+ progressBar.setProgress(millisPlayed);
+ }
+ progressBar.setEnabled(isSeekable);
+ } else {
+ positionTextView.setText("0:00");
+ durationTextView.setText("-:--");
+ progressBar.setProgress(0);
+ progressBar.setEnabled(false);
+ }
+
+ switch (playerState) {
+ case DOWNLOADING:
+ if(currentPlaying != null) {
+ long bytes = currentPlaying.getPartialFile().length();
+ statusTextView.setText(context.getResources().getString(R.string.download_playerstate_downloading, Util.formatLocalizedBytes(bytes, context)));
+ }
+ break;
+ case PREPARING:
+ statusTextView.setText(R.string.download_playerstate_buffering);
+ break;
+ default:
+ if(currentPlaying != null) {
+ String artist = "";
+ if(currentPlaying.getSong().getArtist() != null) {
+ artist = currentPlaying.getSong().getArtist() + " - ";
+ }
+ statusTextView.setText(artist + currentPlaying.getSong().getAlbum());
+ } else {
+ statusTextView.setText(null);
+ }
+ break;
+ }
+
+ switch (playerState) {
+ case STARTED:
+ pauseButton.setVisibility(View.VISIBLE);
+ stopButton.setVisibility(View.INVISIBLE);
+ startButton.setVisibility(View.INVISIBLE);
+ break;
+ case DOWNLOADING:
+ case PREPARING:
+ pauseButton.setVisibility(View.INVISIBLE);
+ stopButton.setVisibility(View.VISIBLE);
+ startButton.setVisibility(View.INVISIBLE);
+ break;
+ default:
+ pauseButton.setVisibility(View.INVISIBLE);
+ stopButton.setVisibility(View.INVISIBLE);
+ startButton.setVisibility(View.VISIBLE);
+ break;
+ }
+
+ onProgressChangedTask = null;
+ }
+ };
+ if(onDownloadListChangedTask == null && onCurrentChangedTask == null) {
+ onProgressChangedTask.execute();
+ }
+ }
+
+ private void changeProgress(final int ms) {
+ final DownloadService downloadService = getDownloadService();
+ if(downloadService == null) {
+ return;
+ }
+
+ new SilentBackgroundTask<Void>(context) {
+ boolean isJukeboxEnabled;
+ int msPlayed;
+ Integer duration;
+ PlayerState playerState;
+ int seekTo;
+
+ @Override
+ protected Void doInBackground() throws Throwable {
+ msPlayed = Math.max(0, downloadService.getPlayerPosition());
+ duration = downloadService.getPlayerDuration();
+ playerState = getDownloadService().getPlayerState();
+ int msTotal = duration == null ? 0 : duration;
+ if(msPlayed + ms > msTotal) {
+ seekTo = msTotal;
+ } else {
+ seekTo = msPlayed + ms;
+ }
+ downloadService.seekTo(seekTo);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ progressBar.setProgress(seekTo);
+ }
+ }.execute();
+ }
+
+ private void createBookmark() {
+ DownloadService downloadService = getDownloadService();
+ if(downloadService == null) {
+ return;
+ }
+
+ final DownloadFile currentDownload = downloadService.getCurrentPlaying();
+ if(currentDownload == null) {
+ return;
+ }
+
+ View dialogView = context.getLayoutInflater().inflate(R.layout.create_bookmark, null);
+ final EditText commentBox = (EditText)dialogView.findViewById(R.id.comment_text);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.download_save_bookmark_title)
+ .setView(dialogView)
+ .setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ String comment = commentBox.getText().toString();
+
+ createBookmark(currentDownload, comment);
+ }
+ })
+ .setNegativeButton(R.string.common_cancel, null);
+ AlertDialog dialog = builder.create();
+ dialog.show();
+ }
+ private void createBookmark(final DownloadFile currentDownload, final String comment) {
+ DownloadService downloadService = getDownloadService();
+ if(downloadService == null) {
+ return;
+ }
+
+ final Entry currentSong = currentDownload.getSong();
+ final int position = downloadService.getPlayerPosition();
+ final Bookmark oldBookmark = currentSong.getBookmark();
+ currentSong.setBookmark(new Bookmark(position));
+ bookmarkButton.setImageResource(R.drawable.ic_menu_bookmark_selected);
+
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.createBookmark(currentSong, position, comment, context, null);
+
+ new EntryInstanceUpdater(currentSong) {
+ @Override
+ public void update(Entry found) {
+ found.setBookmark(new Bookmark(position));
+ }
+ }.execute();
+
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ Util.toast(context, R.string.download_save_bookmark);
+ setControlsVisible(true);
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ Log.w(TAG, "Failed to create bookmark", error);
+ currentSong.setBookmark(oldBookmark);
+
+ // If no bookmark at start, then return to no bookmark
+ if(oldBookmark == null) {
+ int bookmark;
+ if(context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
+ bookmark = R.drawable.ic_menu_bookmark_dark;
+ } else {
+ bookmark = Util.getAttribute(context, R.attr.bookmark);
+ }
+ bookmarkButton.setImageResource(bookmark);
+ }
+
+ String msg;
+ if(error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.download_save_bookmark_failed) + getErrorMessage(error);
+ }
+
+ Util.toast(context, msg, false);
+ }
+ }.execute();
+ }
+
+ @Override
+ public boolean onDown(MotionEvent me) {
+ setControlsVisible(true);
+ return false;
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+ final DownloadService downloadService = getDownloadService();
+ if (downloadService == null || e1 == null || e2 == null) {
+ return false;
+ }
+
+ // Right to Left swipe
+ int action = 0;
+ if (e1.getX() - e2.getX() > swipeDistance && Math.abs(velocityX) > swipeVelocity) {
+ action = ACTION_NEXT;
+ }
+ // Left to Right swipe
+ else if (e2.getX() - e1.getX() > swipeDistance && Math.abs(velocityX) > swipeVelocity) {
+ action = ACTION_PREVIOUS;
+ }
+ // Top to Bottom swipe
+ else if (e2.getY() - e1.getY() > swipeDistance && Math.abs(velocityY) > swipeVelocity) {
+ action = ACTION_FORWARD;
+ }
+ // Bottom to Top swipe
+ else if (e1.getY() - e2.getY() > swipeDistance && Math.abs(velocityY) > swipeVelocity) {
+ action = ACTION_REWIND;
+ }
+
+ if(action > 0) {
+ final int performAction = action;
+ warnIfStorageUnavailable();
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ switch(performAction) {
+ case ACTION_NEXT:
+ downloadService.next();
+ break;
+ case ACTION_PREVIOUS:
+ downloadService.previous();
+ break;
+ case ACTION_FORWARD:
+ downloadService.seekTo(downloadService.getPlayerPosition() + DownloadService.FAST_FORWARD);
+ break;
+ case ACTION_REWIND:
+ downloadService.seekTo(downloadService.getPlayerPosition() - DownloadService.REWIND);
+ break;
+ }
+
+ onProgressChanged();
+ if(performAction == ACTION_NEXT || performAction == ACTION_PREVIOUS) {
+ onCurrentChanged();
+ }
+ return null;
+ }
+ }.execute();
+
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public void onLongPress(MotionEvent e) {
+ }
+
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+ return false;
+ }
+
+ @Override
+ public void onShowPress(MotionEvent e) {
+ }
+
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ return false;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/PreferenceCompatFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/PreferenceCompatFragment.java
new file mode 100644
index 00000000..9f413b3b
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/fragments/PreferenceCompatFragment.java
@@ -0,0 +1,313 @@
+/*
+ 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.fragments;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.preference.Preference;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceManager;
+import android.preference.PreferenceScreen;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ListView;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.util.Constants;
+
+public class PreferenceCompatFragment extends SubsonicFragment {
+ private static final int FIRST_REQUEST_CODE = 100;
+ private static final int MSG_BIND_PREFERENCES = 1;
+ private static final String PREFERENCES_TAG = "android:preferences";
+ private boolean mHavePrefs;
+ private boolean mInitDone;
+ private ListView mList;
+ private PreferenceManager mPreferenceManager;
+
+ private Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+
+ case MSG_BIND_PREFERENCES:
+ bindPreferences();
+ break;
+ }
+ }
+ };
+
+ final private Runnable mRequestFocus = new Runnable() {
+ public void run() {
+ mList.focusableViewAvailable(mList);
+ }
+ };
+
+ private void bindPreferences() {
+ PreferenceScreen localPreferenceScreen = getPreferenceScreen();
+ if (localPreferenceScreen != null) {
+ ListView localListView = getListView();
+ localPreferenceScreen.bind(localListView);
+ }
+ }
+
+ private void ensureList() {
+ if (mList == null) {
+ View view = getView();
+ if (view == null) {
+ throw new IllegalStateException("Content view not yet created");
+ }
+
+ View listView = view.findViewById(android.R.id.list);
+ if (!(listView instanceof ListView)) {
+ throw new RuntimeException("Content has view with id attribute 'android.R.id.list' that is not a ListView class");
+ }
+
+ mList = (ListView)listView;
+ if (mList == null) {
+ throw new RuntimeException("Your content must have a ListView whose id attribute is 'android.R.id.list'");
+ }
+
+ mHandler.post(mRequestFocus);
+ }
+ }
+
+ private void postBindPreferences() {
+ if (mHandler.hasMessages(MSG_BIND_PREFERENCES)) {
+ mHandler.obtainMessage(MSG_BIND_PREFERENCES).sendToTarget();
+ }
+ }
+
+ private void requirePreferenceManager() {
+ if (this.mPreferenceManager == null) {
+ throw new RuntimeException("This should be called after super.onCreate.");
+ }
+ }
+
+ public void addPreferencesFromIntent(Intent intent) {
+ requirePreferenceManager();
+ PreferenceScreen screen = inflateFromIntent(intent, getPreferenceScreen());
+ setPreferenceScreen(screen);
+ }
+
+ public void addPreferencesFromResource(int resId) {
+ requirePreferenceManager();
+ PreferenceScreen screen = inflateFromResource(getActivity(), resId, getPreferenceScreen());
+ setPreferenceScreen(screen);
+ }
+
+ public Preference findPreference(CharSequence key) {
+ if (mPreferenceManager == null) {
+ return null;
+ }
+ return mPreferenceManager.findPreference(key);
+ }
+
+ public ListView getListView() {
+ ensureList();
+ return mList;
+ }
+
+ public PreferenceManager getPreferenceManager() {
+ return mPreferenceManager;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ getListView().setScrollBarStyle(0);
+ if (mHavePrefs) {
+ bindPreferences();
+ }
+ mInitDone = true;
+ if (savedInstanceState != null) {
+ Bundle localBundle = savedInstanceState.getBundle(PREFERENCES_TAG);
+ if (localBundle != null) {
+ PreferenceScreen screen = getPreferenceScreen();
+ if (screen != null) {
+ screen.restoreHierarchyState(localBundle);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ dispatchActivityResult(requestCode, resultCode, data);
+ }
+
+ @Override
+ public void onCreate(Bundle paramBundle) {
+ super.onCreate(paramBundle);
+ mPreferenceManager = createPreferenceManager();
+
+ int res = this.getArguments().getInt(Constants.INTENT_EXTRA_FRAGMENT_TYPE, 0);
+ if(res != 0) {
+ addPreferencesFromResource(res);
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater paramLayoutInflater, ViewGroup paramViewGroup, Bundle paramBundle) {
+ return paramLayoutInflater.inflate(R.layout.preferences, paramViewGroup, false);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ dispatchActivityDestroy();
+ }
+
+ @Override
+ public void onDestroyView() {
+ mList = null;
+ mHandler.removeCallbacks(mRequestFocus);
+ mHandler.removeMessages(MSG_BIND_PREFERENCES);
+ super.onDestroyView();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle bundle) {
+ super.onSaveInstanceState(bundle);
+ PreferenceScreen screen = getPreferenceScreen();
+ if (screen != null) {
+ Bundle localBundle = new Bundle();
+ screen.saveHierarchyState(localBundle);
+ bundle.putBundle(PREFERENCES_TAG, localBundle);
+ }
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ dispatchActivityStop();
+ }
+
+ /** Access methods with visibility private **/
+
+ private PreferenceManager createPreferenceManager() {
+ try {
+ Constructor<PreferenceManager> c = PreferenceManager.class.getDeclaredConstructor(Activity.class, int.class);
+ c.setAccessible(true);
+ return c.newInstance(this.getActivity(), FIRST_REQUEST_CODE);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private PreferenceScreen getPreferenceScreen() {
+ try {
+ Method m = PreferenceManager.class.getDeclaredMethod("getPreferenceScreen");
+ m.setAccessible(true);
+ return (PreferenceScreen) m.invoke(mPreferenceManager);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void setPreferenceScreen(PreferenceScreen preferenceScreen) {
+ try {
+ Method m = PreferenceManager.class.getDeclaredMethod("setPreferences", PreferenceScreen.class);
+ m.setAccessible(true);
+ boolean result = (Boolean) m.invoke(mPreferenceManager, preferenceScreen);
+ if (result && preferenceScreen != null) {
+ mHavePrefs = true;
+ if (mInitDone) {
+ postBindPreferences();
+ }
+ }
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void dispatchActivityResult(int requestCode, int resultCode, Intent data) {
+ try {
+ Method m = PreferenceManager.class.getDeclaredMethod("dispatchActivityResult", int.class, int.class, Intent.class);
+ m.setAccessible(true);
+ m.invoke(mPreferenceManager, requestCode, resultCode, data);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void dispatchActivityDestroy() {
+ try {
+ Method m = PreferenceManager.class.getDeclaredMethod("dispatchActivityDestroy");
+ m.setAccessible(true);
+ m.invoke(mPreferenceManager);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void dispatchActivityStop() {
+ try {
+ Method m = PreferenceManager.class.getDeclaredMethod("dispatchActivityStop");
+ m.setAccessible(true);
+ m.invoke(mPreferenceManager);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+
+ private void setFragment(PreferenceFragment preferenceFragment) {
+ try {
+ Method m = PreferenceManager.class.getDeclaredMethod("setFragment", PreferenceFragment.class);
+ m.setAccessible(true);
+ m.invoke(mPreferenceManager, preferenceFragment);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public PreferenceScreen inflateFromResource(Context context, int resId, PreferenceScreen rootPreferences) {
+ PreferenceScreen preferenceScreen ;
+ try {
+ Method m = PreferenceManager.class.getDeclaredMethod("inflateFromResource", Context.class, int.class, PreferenceScreen.class);
+ m.setAccessible(true);
+ preferenceScreen = (PreferenceScreen) m.invoke(mPreferenceManager, context, resId, rootPreferences);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ return preferenceScreen;
+ }
+
+ public PreferenceScreen inflateFromIntent(Intent queryIntent, PreferenceScreen rootPreferences) {
+ PreferenceScreen preferenceScreen ;
+ try {
+ Method m = PreferenceManager.class.getDeclaredMethod("inflateFromIntent", Intent.class, PreferenceScreen.class);
+ m.setAccessible(true);
+ preferenceScreen = (PreferenceScreen) m.invoke(mPreferenceManager, queryIntent, rootPreferences);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ return preferenceScreen;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SearchFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SearchFragment.java
new file mode 100644
index 00000000..0f1598dd
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/fragments/SearchFragment.java
@@ -0,0 +1,368 @@
+package github.daneren2005.dsub.fragments;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Arrays;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v4.widget.SwipeRefreshLayout;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.View;
+import android.view.MenuItem;
+import android.widget.AdapterView;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.net.Uri;
+import android.view.ViewGroup;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.Artist;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.SearchCritera;
+import github.daneren2005.dsub.domain.SearchResult;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.adapter.ArtistAdapter;
+import github.daneren2005.dsub.util.BackgroundTask;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.adapter.EntryAdapter;
+import github.daneren2005.dsub.adapter.MergeAdapter;
+import github.daneren2005.dsub.util.TabBackgroundTask;
+import github.daneren2005.dsub.util.Util;
+
+public class SearchFragment extends SubsonicFragment {
+ private static final String TAG = SearchFragment.class.getSimpleName();
+
+ private static final int DEFAULT_ARTISTS = 3;
+ private static final int DEFAULT_ALBUMS = 5;
+ private static final int DEFAULT_SONGS = 10;
+
+ private static final int MAX_ARTISTS = 10;
+ private static final int MAX_ALBUMS = 20;
+ private static final int MAX_SONGS = 25;
+ private ListView list;
+
+ private View artistsHeading;
+ private View albumsHeading;
+ private View songsHeading;
+ private View moreArtistsButton;
+ private View moreAlbumsButton;
+ private View moreSongsButton;
+ private SearchResult searchResult;
+ private MergeAdapter mergeAdapter;
+ private ArtistAdapter artistAdapter;
+ private ListAdapter moreArtistsAdapter;
+ private EntryAdapter albumAdapter;
+ private ListAdapter moreAlbumsAdapter;
+ private ListAdapter moreSongsAdapter;
+ private EntryAdapter songAdapter;
+ private boolean skipSearch = false;
+ private String currentQuery;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ if(savedInstanceState != null) {
+ searchResult = (SearchResult) savedInstanceState.getSerializable(Constants.FRAGMENT_LIST);
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putSerializable(Constants.FRAGMENT_LIST, searchResult);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
+ rootView = inflater.inflate(R.layout.abstract_list_fragment, container, false);
+ setTitle(R.string.search_title);
+
+ View buttons = inflater.inflate(R.layout.search_buttons, null);
+
+ artistsHeading = buttons.findViewById(R.id.search_artists);
+ albumsHeading = buttons.findViewById(R.id.search_albums);
+ songsHeading = buttons.findViewById(R.id.search_songs);
+
+ moreArtistsButton = buttons.findViewById(R.id.search_more_artists);
+ moreAlbumsButton = buttons.findViewById(R.id.search_more_albums);
+ moreSongsButton = buttons.findViewById(R.id.search_more_songs);
+
+ refreshLayout = (SwipeRefreshLayout) rootView.findViewById(R.id.refresh_layout);
+ refreshLayout.setEnabled(false);
+
+ list = (ListView) rootView.findViewById(R.id.fragment_list);
+
+ list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ if (view == moreArtistsButton) {
+ expandArtists();
+ } else if (view == moreAlbumsButton) {
+ expandAlbums();
+ } else if (view == moreSongsButton) {
+ expandSongs();
+ } else {
+ Object item = parent.getItemAtPosition(position);
+ if (item instanceof Artist) {
+ onArtistSelected((Artist) item, false);
+ } else if (item instanceof MusicDirectory.Entry) {
+ MusicDirectory.Entry entry = (MusicDirectory.Entry) item;
+ if (entry.isDirectory()) {
+ onAlbumSelected(entry, false);
+ } else if (entry.isVideo()) {
+ onVideoSelected(entry);
+ } else {
+ onSongSelected(entry, false, true, true, false);
+ }
+
+ }
+ }
+ }
+ });
+ registerForContextMenu(list);
+ context.onNewIntent(context.getIntent());
+
+ if(searchResult != null) {
+ skipSearch = true;
+ populateList();
+ }
+
+ return rootView;
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) {
+ menuInflater.inflate(R.menu.search, menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.menu_search:
+ context.startSearch(currentQuery, false, null, false);
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, view, menuInfo);
+
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
+ Object selectedItem = list.getItemAtPosition(info.position);
+ onCreateContextMenu(menu, view, menuInfo, selectedItem);
+ if(selectedItem instanceof MusicDirectory.Entry && !((MusicDirectory.Entry) selectedItem).isVideo() && !Util.isOffline(context)) {
+ menu.removeItem(R.id.song_menu_remove_playlist);
+ }
+
+ recreateContextMenu(menu);
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem menuItem) {
+ if(menuItem.getGroupId() != getSupportTag()) {
+ return false;
+ }
+
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
+ Object selectedItem = list.getItemAtPosition(info.position);
+
+ if(onContextItemSelected(menuItem, selectedItem)) {
+ return true;
+ }
+
+ return true;
+ }
+
+ @Override
+ public void setPrimaryFragment(boolean primary) {
+ super.setPrimaryFragment(primary);
+ }
+
+ @Override
+ public void refresh(boolean refresh) {
+ context.onNewIntent(context.getIntent());
+ }
+
+ public void search(final String query, final boolean autoplay) {
+ if(skipSearch) {
+ skipSearch = false;
+ return;
+ }
+ currentQuery = query;
+
+ mergeAdapter = new MergeAdapter();
+ list.setAdapter(mergeAdapter);
+
+ BackgroundTask<SearchResult> task = new TabBackgroundTask<SearchResult>(this) {
+ @Override
+ protected SearchResult doInBackground() throws Throwable {
+ SearchCritera criteria = new SearchCritera(query, MAX_ARTISTS, MAX_ALBUMS, MAX_SONGS);
+ MusicService service = MusicServiceFactory.getMusicService(context);
+ return service.search(criteria, context, this);
+ }
+
+ @Override
+ protected void done(SearchResult result) {
+ searchResult = result;
+ populateList();
+ if (autoplay) {
+ autoplay(query);
+ }
+
+ }
+ };
+ task.execute();
+ }
+
+ public void populateList() {
+ mergeAdapter = new MergeAdapter();
+
+ if (searchResult != null) {
+ List<Artist> artists = searchResult.getArtists();
+ if (!artists.isEmpty()) {
+ mergeAdapter.addView(artistsHeading);
+ List<Artist> displayedArtists = new ArrayList<Artist>(artists.subList(0, Math.min(DEFAULT_ARTISTS, artists.size())));
+ artistAdapter = new ArtistAdapter(context, displayedArtists);
+ mergeAdapter.addAdapter(artistAdapter);
+ if (artists.size() > DEFAULT_ARTISTS) {
+ moreArtistsAdapter = mergeAdapter.addView(moreArtistsButton, true);
+ }
+ }
+
+ List<MusicDirectory.Entry> albums = searchResult.getAlbums();
+ if (!albums.isEmpty()) {
+ mergeAdapter.addView(albumsHeading);
+ List<MusicDirectory.Entry> displayedAlbums = new ArrayList<MusicDirectory.Entry>(albums.subList(0, Math.min(DEFAULT_ALBUMS, albums.size())));
+ albumAdapter = new EntryAdapter(context, getImageLoader(), displayedAlbums, false);
+ mergeAdapter.addAdapter(albumAdapter);
+ if (albums.size() > DEFAULT_ALBUMS) {
+ moreAlbumsAdapter = mergeAdapter.addView(moreAlbumsButton, true);
+ }
+ }
+
+ List<MusicDirectory.Entry> songs = searchResult.getSongs();
+ if (!songs.isEmpty()) {
+ mergeAdapter.addView(songsHeading);
+ List<MusicDirectory.Entry> displayedSongs = new ArrayList<MusicDirectory.Entry>(songs.subList(0, Math.min(DEFAULT_SONGS, songs.size())));
+ songAdapter = new EntryAdapter(context, getImageLoader(), displayedSongs, false);
+ mergeAdapter.addAdapter(songAdapter);
+ if (songs.size() > DEFAULT_SONGS) {
+ moreSongsAdapter = mergeAdapter.addView(moreSongsButton, true);
+ }
+ }
+
+ boolean empty = searchResult.getArtists().isEmpty() && searchResult.getAlbums().isEmpty() && searchResult.getSongs().isEmpty();
+ if(empty) {
+ setEmpty(true);
+ }
+ }
+
+ list.setAdapter(mergeAdapter);
+ }
+
+ private void expandArtists() {
+ artistAdapter.clear();
+ for (Artist artist : searchResult.getArtists()) {
+ artistAdapter.add(artist);
+ }
+ artistAdapter.notifyDataSetChanged();
+ mergeAdapter.removeAdapter(moreArtistsAdapter);
+ mergeAdapter.notifyDataSetChanged();
+ }
+
+ private void expandAlbums() {
+ albumAdapter.clear();
+ for (MusicDirectory.Entry album : searchResult.getAlbums()) {
+ albumAdapter.add(album);
+ }
+ albumAdapter.notifyDataSetChanged();
+ mergeAdapter.removeAdapter(moreAlbumsAdapter);
+ mergeAdapter.notifyDataSetChanged();
+ }
+
+ private void expandSongs() {
+ songAdapter.clear();
+ for (MusicDirectory.Entry song : searchResult.getSongs()) {
+ songAdapter.add(song);
+ }
+ songAdapter.notifyDataSetChanged();
+ mergeAdapter.removeAdapter(moreSongsAdapter);
+ mergeAdapter.notifyDataSetChanged();
+ }
+
+ private void onArtistSelected(Artist artist, boolean autoplay) {
+ SubsonicFragment fragment = new SelectDirectoryFragment();
+ Bundle args = new Bundle();
+ args.putString(Constants.INTENT_EXTRA_NAME_ID, artist.getId());
+ args.putString(Constants.INTENT_EXTRA_NAME_NAME, artist.getName());
+ if(autoplay) {
+ args.putBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, true);
+ }
+ args.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, true);
+ fragment.setArguments(args);
+
+ replaceFragment(fragment);
+ }
+
+ private void onAlbumSelected(MusicDirectory.Entry album, boolean autoplay) {
+ SubsonicFragment fragment = new SelectDirectoryFragment();
+ Bundle args = new Bundle();
+ args.putString(Constants.INTENT_EXTRA_NAME_ID, album.getId());
+ args.putString(Constants.INTENT_EXTRA_NAME_NAME, album.getTitle());
+ if(autoplay) {
+ args.putBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, true);
+ }
+ fragment.setArguments(args);
+
+ replaceFragment(fragment);
+ }
+
+ private void onSongSelected(MusicDirectory.Entry song, boolean save, boolean append, boolean autoplay, boolean playNext) {
+ DownloadService downloadService = getDownloadService();
+ if (downloadService != null) {
+ if (!append) {
+ downloadService.clear();
+ }
+ downloadService.download(Arrays.asList(song), save, false, playNext, false);
+ if (autoplay) {
+ downloadService.play(downloadService.size() - 1);
+ }
+
+ Util.toast(context, getResources().getQuantityString(R.plurals.select_album_n_songs_added, 1, 1));
+ }
+ }
+
+ private void onVideoSelected(MusicDirectory.Entry entry) {
+ int maxBitrate = Util.getMaxVideoBitrate(context);
+
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(Uri.parse(MusicServiceFactory.getMusicService(context).getVideoUrl(maxBitrate, context, entry.getId())));
+ startActivity(intent);
+ }
+
+ private void autoplay(String query) {
+ Artist artist = searchResult.getArtists().isEmpty() ? null : searchResult.getArtists().get(0);
+ MusicDirectory.Entry album = searchResult.getAlbums().isEmpty() ? null : searchResult.getAlbums().get(0);
+ MusicDirectory.Entry song = searchResult.getSongs().isEmpty() ? null : searchResult.getSongs().get(0);
+
+ if(artist != null && query.equals(artist.getName())) {
+ onArtistSelected(artist, true);
+ } else if(album != null && query.equals(album.getTitle())) {
+ onAlbumSelected(album, true);
+ } else if(song != null) {
+ onSongSelected(song, false, false, true, false);
+ } else if(album != null) {
+ onAlbumSelected(album, true);
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SelectArtistFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SelectArtistFragment.java
new file mode 100644
index 00000000..5488c95b
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/fragments/SelectArtistFragment.java
@@ -0,0 +1,333 @@
+package github.daneren2005.dsub.fragments;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.Artist;
+import github.daneren2005.dsub.domain.Indexes;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.MusicFolder;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.ProgressListener;
+import github.daneren2005.dsub.util.Util;
+import github.daneren2005.dsub.adapter.ArtistAdapter;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+public class SelectArtistFragment extends SelectListFragment<Artist> {
+ private static final String TAG = SelectArtistFragment.class.getSimpleName();
+ private static final int MENU_GROUP_MUSIC_FOLDER = 10;
+
+ private View folderButtonParent;
+ private View folderButton;
+ private TextView folderName;
+ private List<MusicFolder> musicFolders = null;
+ private List<MusicDirectory.Entry> entries;
+ private String groupId;
+ private String groupName;
+
+ public SelectArtistFragment() {
+ super();
+ }
+
+ @Override
+ public void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+
+ if(bundle != null) {
+ musicFolders = (List<MusicFolder>) bundle.getSerializable(Constants.FRAGMENT_LIST2);
+ }
+ artist = true;
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putSerializable(Constants.FRAGMENT_LIST2, (Serializable) musicFolders);
+ }
+
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
+ Bundle args = getArguments();
+ if(args != null) {
+ groupId = args.getString(Constants.INTENT_EXTRA_NAME_ID);
+ groupName = args.getString(Constants.INTENT_EXTRA_NAME_NAME);
+
+ if(groupName != null) {
+ setTitle(groupName);
+ context.invalidateOptionsMenu();
+ }
+ }
+
+ folderButton = null;
+ super.onCreateView(inflater, container, bundle);
+
+ if("4.4.2".equals(Build.VERSION.RELEASE)) {
+ listView.setFastScrollAlwaysVisible(true);
+ }
+
+ if(objects != null && currentTask == null) {
+ if (Util.isOffline(context) || Util.isTagBrowsing(context) || groupId != null) {
+ folderButton.setVisibility(View.GONE);
+ }
+ setMusicFolders();
+ }
+
+ return rootView;
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, view, menuInfo);
+
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
+ Object entry = listView.getItemAtPosition(info.position);
+
+ if (entry instanceof Artist) {
+ onCreateContextMenu(menu, view, menuInfo, entry);
+ } else if (info.position == 0) {
+ String musicFolderId = Util.getSelectedMusicFolderId(context);
+ MenuItem menuItem = menu.add(MENU_GROUP_MUSIC_FOLDER, -1, 0, R.string.select_artist_all_folders);
+ if (musicFolderId == null) {
+ menuItem.setChecked(true);
+ }
+ if (musicFolders != null) {
+ for (int i = 0; i < musicFolders.size(); i++) {
+ MusicFolder musicFolder = musicFolders.get(i);
+ menuItem = menu.add(MENU_GROUP_MUSIC_FOLDER, i, i + 1, musicFolder.getName());
+ if (musicFolder.getId().equals(musicFolderId)) {
+ menuItem.setChecked(true);
+ }
+ }
+ }
+ menu.setGroupCheckable(MENU_GROUP_MUSIC_FOLDER, true, true);
+ }
+
+ recreateContextMenu(menu);
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem menuItem) {
+ if(menuItem.getGroupId() != getSupportTag()) {
+ return false;
+ }
+
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
+ Artist artist = (Artist) listView.getItemAtPosition(info.position);
+
+ if (artist != null) {
+ return onContextItemSelected(menuItem, artist);
+ } else if (info.position == 0) {
+ MusicFolder selectedFolder = menuItem.getItemId() == -1 ? null : musicFolders.get(menuItem.getItemId());
+ String musicFolderId = selectedFolder == null ? null : selectedFolder.getId();
+ String musicFolderName = selectedFolder == null ? context.getString(R.string.select_artist_all_folders)
+ : selectedFolder.getName();
+ Util.setSelectedMusicFolderId(context, musicFolderId);
+ folderName.setText(musicFolderName);
+ context.invalidate();
+ }
+
+ return true;
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ if (view == folderButtonParent) {
+ selectFolder();
+ } else {
+ Artist artist = (Artist) parent.getItemAtPosition(position);
+
+ SubsonicFragment fragment;
+ if((Util.isFirstLevelArtist(context) || Util.isOffline(context) || Util.isTagBrowsing(context)) || "root".equals(artist.getId()) || groupId != null) {
+ fragment = new SelectDirectoryFragment();
+ Bundle args = new Bundle();
+ args.putString(Constants.INTENT_EXTRA_NAME_ID, artist.getId());
+ args.putString(Constants.INTENT_EXTRA_NAME_NAME, artist.getName());
+ if ("root".equals(artist.getId())) {
+ args.putSerializable(Constants.FRAGMENT_LIST, (Serializable) entries);
+ }
+ args.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, true);
+ fragment.setArguments(args);
+ } else {
+ fragment = new SelectArtistFragment();
+ Bundle args = new Bundle();
+ args.putString(Constants.INTENT_EXTRA_NAME_ID, artist.getId());
+ args.putString(Constants.INTENT_EXTRA_NAME_NAME, artist.getName());
+ fragment.setArguments(args);
+ }
+
+ replaceFragment(fragment);
+ }
+ }
+
+ @Override
+ public void onFinishRefresh() {
+ setMusicFolders();
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) {
+ super.onCreateOptionsMenu(menu, menuInflater);
+
+ if(Util.isOffline(context) || Util.isTagBrowsing(context) || groupId != null) {
+ menu.removeItem(R.id.menu_first_level_artist);
+ } else {
+ if (Util.isFirstLevelArtist(context)) {
+ menu.findItem(R.id.menu_first_level_artist).setChecked(true);
+ }
+ }
+ }
+
+ @Override
+ public int getOptionsMenu() {
+ return R.menu.select_artist;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if(super.onOptionsItemSelected(item)) {
+ return true;
+ }
+
+ switch (item.getItemId()) {
+ case R.id.menu_first_level_artist:
+ toggleFirstLevelArtist();
+ break;
+ }
+
+ return false;
+ }
+
+ @Override
+ public ArrayAdapter getAdapter(List<Artist> objects) {
+ createMusicFolderButton();
+ return new ArtistAdapter(context, objects);
+ }
+
+ @Override
+ public List<Artist> getObjects(MusicService musicService, boolean refresh, ProgressListener listener) throws Exception {
+ List<Artist> artists;
+ if(groupId == null) {
+ if (!Util.isOffline(context) && !Util.isTagBrowsing(context)) {
+ musicFolders = musicService.getMusicFolders(refresh, context, listener);
+
+ // Hide folders option if there is only one
+ if (musicFolders.size() == 1) {
+ musicFolders = null;
+ Util.setSelectedMusicFolderId(context, null);
+ }
+ }
+ String musicFolderId = Util.getSelectedMusicFolderId(context);
+
+ Indexes indexes = musicService.getIndexes(musicFolderId, refresh, context, listener);
+ artists = new ArrayList<Artist>(indexes.getShortcuts().size() + indexes.getArtists().size());
+ artists.addAll(indexes.getShortcuts());
+ artists.addAll(indexes.getArtists());
+ entries = indexes.getEntries();
+ } else {
+ artists = new ArrayList<Artist>();
+ MusicDirectory dir = musicService.getMusicDirectory(groupId, groupName, refresh, context, listener);
+ for(MusicDirectory.Entry entry: dir.getChildren(true, false)) {
+ Artist artist = new Artist();
+ artist.setId(entry.getId());
+ artist.setName(entry.getTitle());
+ artist.setStarred(entry.isStarred());
+ artists.add(artist);
+ }
+
+ entries = new ArrayList<MusicDirectory.Entry>();
+ entries.addAll(dir.getChildren(false, true));
+ if(!entries.isEmpty()) {
+ Artist root = new Artist();
+ root.setId("root");
+ root.setName("Root");
+ root.setIndex("#");
+ artists.add(root);
+ }
+ }
+
+ return artists;
+ }
+
+ @Override
+ public int getTitleResource() {
+ return groupId == null ? R.string.button_bar_browse : 0;
+ }
+
+ private void createMusicFolderButton() {
+ if(folderButton == null) {
+ folderButtonParent = context.getLayoutInflater().inflate(R.layout.select_artist_header, listView, false);
+ folderName = (TextView) folderButtonParent.findViewById(R.id.select_artist_folder_2);
+ listView.addHeaderView(folderButtonParent);
+ folderButton = folderButtonParent.findViewById(R.id.select_artist_folder);
+ }
+
+ if (Util.isOffline(context) || Util.isTagBrowsing(context) || musicFolders == null) {
+ folderButton.setVisibility(View.GONE);
+ } else {
+ folderButton.setVisibility(View.VISIBLE);
+ }
+ }
+
+ @Override
+ public void setEmpty(boolean empty) {
+ super.setEmpty(empty);
+
+ if(empty && !Util.isOffline(context)) {
+ createMusicFolderButton();
+ setMusicFolders();
+
+ objects.clear();
+ listView.setAdapter(new ArtistAdapter(context, objects));
+ listView.setVisibility(View.VISIBLE);
+
+ View view = rootView.findViewById(R.id.tab_progress);
+ LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) view.getLayoutParams();
+ params.height = 0;
+ params.weight = 5;
+ view.setLayoutParams(params);
+ }
+ }
+
+ private void setMusicFolders() {
+ // Display selected music folder
+ if (musicFolders != null) {
+ String musicFolderId = Util.getSelectedMusicFolderId(context);
+ if (musicFolderId == null) {
+ folderName.setText(R.string.select_artist_all_folders);
+ } else {
+ for (MusicFolder musicFolder : musicFolders) {
+ if (musicFolder.getId().equals(musicFolderId)) {
+ folderName.setText(musicFolder.getName());
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ private void selectFolder() {
+ folderButton.showContextMenu();
+ }
+
+ private void toggleFirstLevelArtist() {
+ Util.toggleFirstLevelArtist(context);
+ context.invalidateOptionsMenu();
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SelectBookmarkFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SelectBookmarkFragment.java
new file mode 100644
index 00000000..c71d99f6
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/fragments/SelectBookmarkFragment.java
@@ -0,0 +1,131 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+*/
+package github.daneren2005.dsub.fragments;
+
+import android.view.ContextMenu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.activity.DownloadActivity;
+import github.daneren2005.dsub.domain.Bookmark;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.util.ProgressListener;
+import github.daneren2005.dsub.util.SilentBackgroundTask;
+import github.daneren2005.dsub.util.Util;
+import github.daneren2005.dsub.adapter.BookmarkAdapter;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class SelectBookmarkFragment extends SelectListFragment<MusicDirectory.Entry> {
+ private static final String TAG = SelectBookmarkFragment.class.getSimpleName();
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, view, menuInfo);
+
+ MenuInflater inflater = context.getMenuInflater();
+ inflater.inflate(R.menu.select_bookmark_context, menu);
+
+ hideMenuItems(menu, (AdapterView.AdapterContextMenuInfo) menuInfo);
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem menuItem) {
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
+ MusicDirectory.Entry bookmark = objects.get(info.position);
+
+ switch(menuItem.getItemId()) {
+ case R.id.bookmark_menu_info:
+ displayBookmarkInfo(bookmark);
+ return true;
+ case R.id.bookmark_menu_delete:
+ deleteBookmark(bookmark, adapter);
+ return true;
+ }
+
+ if(onContextItemSelected(menuItem, bookmark)) {
+ return true;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int getOptionsMenu() {
+ return R.menu.abstract_top_menu;
+ }
+
+ @Override
+ public ArrayAdapter getAdapter(List<MusicDirectory.Entry> bookmarks) {
+ return new BookmarkAdapter(context, bookmarks);
+ }
+
+ @Override
+ public List<MusicDirectory.Entry> getObjects(MusicService musicService, boolean refresh, ProgressListener listener) throws Exception {
+ return musicService.getBookmarks(refresh, context, listener).getChildren();
+ }
+
+ @Override
+ public int getTitleResource() {
+ return R.string.button_bar_bookmarks;
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ final DownloadService downloadService = getDownloadService();
+ if(downloadService == null) {
+ return;
+ }
+
+ final MusicDirectory.Entry bookmark = (MusicDirectory.Entry) parent.getItemAtPosition(position);
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ downloadService.clear();
+ downloadService.download(Arrays.asList(bookmark), false, true, false, false, 0, bookmark.getBookmark().getPosition());
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ Util.startActivityWithoutTransition(context, DownloadActivity.class);
+ }
+ }.execute();
+ }
+
+ private void displayBookmarkInfo(final MusicDirectory.Entry entry) {
+ Bookmark bookmark = entry.getBookmark();
+ String comment = bookmark.getComment();
+ if(comment == null) {
+ comment = "";
+ }
+
+ String msg = context.getResources().getString(R.string.bookmark_details,
+ entry.getTitle(), Util.formatDuration(bookmark.getPosition() / 1000),
+ Util.formatDate(bookmark.getCreated()), Util.formatDate(bookmark.getChanged()), comment);
+
+ Util.info(context, R.string.bookmark_details_title, msg, false);
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SelectDirectoryFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SelectDirectoryFragment.java
new file mode 100644
index 00000000..841a6369
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/fragments/SelectDirectoryFragment.java
@@ -0,0 +1,1597 @@
+package github.daneren2005.dsub.fragments;
+
+import android.annotation.TargetApi;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.v4.widget.SwipeRefreshLayout;
+import android.text.Html;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.text.method.LinkMovementMethod;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.Display;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.GridView;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.RatingBar;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.ArtistInfo;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.ServerInfo;
+import github.daneren2005.dsub.domain.Share;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.util.ImageLoader;
+import github.daneren2005.dsub.adapter.AlbumGridAdapter;
+import github.daneren2005.dsub.adapter.EntryAdapter;
+
+import java.io.Serializable;
+import java.util.Iterator;
+import java.util.List;
+
+import github.daneren2005.dsub.activity.DownloadActivity;
+import github.daneren2005.dsub.domain.PodcastEpisode;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import github.daneren2005.dsub.service.OfflineException;
+import github.daneren2005.dsub.service.ServerTooOldException;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.LoadingTask;
+import github.daneren2005.dsub.util.Pair;
+import github.daneren2005.dsub.util.SilentBackgroundTask;
+import github.daneren2005.dsub.util.TabBackgroundTask;
+import github.daneren2005.dsub.util.UserUtil;
+import github.daneren2005.dsub.util.Util;
+import github.daneren2005.dsub.adapter.AlbumListAdapter;
+import github.daneren2005.dsub.view.HeaderGridView;
+import github.daneren2005.dsub.view.MyLeadingMarginSpan2;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import static github.daneren2005.dsub.domain.MusicDirectory.Entry;
+
+public class SelectDirectoryFragment extends SubsonicFragment implements AdapterView.OnItemClickListener {
+ private static final String TAG = SelectDirectoryFragment.class.getSimpleName();
+
+ private GridView albumList;
+ private ListView entryList;
+ private Boolean licenseValid;
+ private EntryAdapter entryAdapter;
+ private List<Entry> albums;
+ private List<Entry> entries;
+ private boolean albumContext = false;
+ private boolean addAlbumHeader = false;
+ private LoadTask currentTask;
+ private ArtistInfo artistInfo;
+ private String artistInfoDelayed;
+
+ String id;
+ String name;
+ Entry directory;
+ String playlistId;
+ String playlistName;
+ boolean playlistOwner;
+ String podcastId;
+ String podcastName;
+ String podcastDescription;
+ String albumListType;
+ String albumListExtra;
+ int albumListSize;
+ boolean refreshListing = false;
+ boolean showAll = false;
+ boolean restoredInstance = false;
+ boolean lookupParent = false;
+ boolean largeAlbums = false;
+ boolean topTracks = false;
+ String lookupEntry;
+
+ public SelectDirectoryFragment() {
+ super();
+ }
+
+ @Override
+ public void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ if(bundle != null) {
+ entries = (List<Entry>) bundle.getSerializable(Constants.FRAGMENT_LIST);
+ albums = (List<Entry>) bundle.getSerializable(Constants.FRAGMENT_LIST2);
+ artistInfo = (ArtistInfo) bundle.getSerializable(Constants.FRAGMENT_EXTRA);
+ restoredInstance = true;
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putSerializable(Constants.FRAGMENT_LIST, (Serializable) entries);
+ outState.putSerializable(Constants.FRAGMENT_LIST2, (Serializable) albums);
+ outState.putSerializable(Constants.FRAGMENT_EXTRA, (Serializable) artistInfo);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
+ Bundle args = getArguments();
+ if(args != null) {
+ id = args.getString(Constants.INTENT_EXTRA_NAME_ID);
+ name = args.getString(Constants.INTENT_EXTRA_NAME_NAME);
+ directory = (Entry) args.getSerializable(Constants.INTENT_EXTRA_NAME_DIRECTORY);
+ playlistId = args.getString(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID);
+ playlistName = args.getString(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME);
+ playlistOwner = args.getBoolean(Constants.INTENT_EXTRA_NAME_PLAYLIST_OWNER, false);
+ podcastId = args.getString(Constants.INTENT_EXTRA_NAME_PODCAST_ID);
+ podcastName = args.getString(Constants.INTENT_EXTRA_NAME_PODCAST_NAME);
+ podcastDescription = args.getString(Constants.INTENT_EXTRA_NAME_PODCAST_DESCRIPTION);
+ Object shareObj = args.getSerializable(Constants.INTENT_EXTRA_NAME_SHARE);
+ share = (shareObj != null) ? (Share) shareObj : null;
+ albumListType = args.getString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE);
+ albumListExtra = args.getString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_EXTRA);
+ albumListSize = args.getInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0);
+ refreshListing = args.getBoolean(Constants.INTENT_EXTRA_REFRESH_LISTINGS);
+ artist = args.getBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, false);
+ lookupEntry = args.getString(Constants.INTENT_EXTRA_SEARCH_SONG);
+ topTracks = args.getBoolean(Constants.INTENT_EXTRA_TOP_TRACKS);
+ showAll = args.getBoolean(Constants.INTENT_EXTRA_SHOW_ALL);
+
+ String childId = args.getString(Constants.INTENT_EXTRA_NAME_CHILD_ID);
+ if(childId != null) {
+ id = childId;
+ lookupParent = true;
+ }
+ if(entries == null) {
+ entries = (List<Entry>) args.getSerializable(Constants.FRAGMENT_LIST);
+ albums = (List<Entry>) args.getSerializable(Constants.FRAGMENT_LIST2);
+
+ if(albums == null) {
+ albums = new ArrayList<Entry>();
+ }
+ }
+ }
+
+ rootView = inflater.inflate(R.layout.select_album, container, false);
+
+ refreshLayout = (SwipeRefreshLayout) rootView.findViewById(R.id.refresh_layout);
+ refreshLayout.setOnRefreshListener(this);
+
+ entryList = (ListView) rootView.findViewById(R.id.select_album_entries);
+ entryList.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
+ entryList.setOnItemClickListener(this);
+ setupScrollList(entryList);
+
+ if(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_LARGE_ALBUM_ART, true)) {
+ largeAlbums = true;
+ }
+
+ if(albumListType == null || "starred".equals(albumListType) || !largeAlbums) {
+ albumList = (GridView) inflater.inflate(R.layout.unscrollable_grid_view, entryList, false);
+ addAlbumHeader = true;
+ } else {
+ ViewGroup rootGroup = (ViewGroup) rootView.findViewById(R.id.select_album_layout);
+ albumList = (GridView) inflater.inflate(R.layout.grid_view, rootGroup, false);
+ rootGroup.removeView(entryList);
+ rootGroup.addView(albumList);
+
+ setupScrollList(albumList);
+ }
+ registerForContextMenu(entryList);
+ setupAlbumList();
+
+ if(entries == null) {
+ if(primaryFragment || secondaryFragment) {
+ load(false);
+ } else {
+ invalidated = true;
+ }
+ } else {
+ licenseValid = true;
+ finishLoading();
+ }
+
+ if(name != null) {
+ setTitle(name);
+ }
+
+ return rootView;
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) {
+ if(licenseValid == null) {
+ menuInflater.inflate(R.menu.empty, menu);
+ } else if(albumListType != null && !"starred".equals(albumListType)) {
+ menuInflater.inflate(R.menu.select_album_list, menu);
+ } else if(artist && !showAll) {
+ menuInflater.inflate(R.menu.select_album, menu);
+
+ if(!ServerInfo.isMadsonic(context)) {
+ menu.removeItem(R.id.menu_top_tracks);
+ }
+ if(!ServerInfo.checkServerVersion(context, "1.11") || (id != null && "root".equals(id))) {
+ menu.removeItem(R.id.menu_radio);
+ menu.removeItem(R.id.menu_similar_artists);
+ } else if(ServerInfo.isMadsonic(context)) {
+ menu.removeItem(R.id.menu_similar_artists);
+ }
+ } else {
+ if(podcastId == null) {
+ if(Util.isOffline(context)) {
+ menuInflater.inflate(R.menu.select_song_offline, menu);
+ }
+ else {
+ menuInflater.inflate(R.menu.select_song, menu);
+
+ if(playlistId == null || !playlistOwner) {
+ menu.removeItem(R.id.menu_remove_playlist);
+ }
+ }
+
+ SharedPreferences prefs = Util.getPreferences(context);
+ if(!prefs.getBoolean(Constants.PREFERENCES_KEY_MENU_PLAY_NEXT, true)) {
+ menu.setGroupVisible(R.id.hide_play_next, false);
+ }
+ if(!prefs.getBoolean(Constants.PREFERENCES_KEY_MENU_PLAY_LAST, true)) {
+ menu.setGroupVisible(R.id.hide_play_last, false);
+ }
+ } else {
+ if(Util.isOffline(context)) {
+ menuInflater.inflate(R.menu.select_podcast_episode_offline, menu);
+ }
+ else {
+ menuInflater.inflate(R.menu.select_podcast_episode, menu);
+
+ if(!UserUtil.canPodcast()) {
+ menu.removeItem(R.id.menu_download_all);
+ }
+ }
+ }
+ }
+
+ if("starred".equals(albumListType)) {
+ menuInflater.inflate(R.menu.unstar, menu);
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.menu_play_now:
+ playNow(false, false);
+ return true;
+ case R.id.menu_play_last:
+ playNow(false, true);
+ return true;
+ case R.id.menu_play_next:
+ playNow(false, true, true);
+ return true;
+ case R.id.menu_shuffle:
+ playNow(true, false);
+ return true;
+ case R.id.menu_download:
+ downloadBackground(false);
+ selectAll(false, false);
+ return true;
+ case R.id.menu_cache:
+ downloadBackground(true);
+ selectAll(false, false);
+ return true;
+ case R.id.menu_delete:
+ delete();
+ selectAll(false, false);
+ return true;
+ case R.id.menu_add_playlist:
+ if(getSelectedSongs().isEmpty()) {
+ selectAll(true, false);
+ }
+ addToPlaylist(getSelectedSongs());
+ return true;
+ case R.id.menu_remove_playlist:
+ removeFromPlaylist(playlistId, playlistName, getSelectedIndexes());
+ return true;
+ case R.id.menu_download_all:
+ downloadAllPodcastEpisodes();
+ return true;
+ case R.id.menu_show_all:
+ setShowAll();
+ return true;
+ case R.id.menu_unstar:
+ unstarSelected();
+ return true;
+ case R.id.menu_top_tracks:
+ showTopTracks();
+ return true;
+ case R.id.menu_similar_artists:
+ showSimilarArtists(id);
+ return true;
+ case R.id.menu_radio:
+ startArtistRadio(id);
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, view, menuInfo);
+
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
+
+ Entry entry;
+ if(view.getId() == R.id.select_album_entries) {
+ if(info.position == 0) {
+ return;
+ }
+ entry = (Entry) entryList.getItemAtPosition(info.position);
+ // When List has Grid embedded in header, this is called against the header as well
+ if(entry != null) {
+ albumContext = false;
+ }
+ } else {
+ entry = (Entry) albumList.getItemAtPosition(info.position);
+ albumContext = true;
+ }
+
+ // Don't try to display a context menu if error here
+ if(entry == null) {
+ return;
+ }
+
+ onCreateContextMenu(menu, view, menuInfo, entry);
+ if(!entry.isVideo() && !Util.isOffline(context) && (playlistId == null || !playlistOwner) && (podcastId == null || Util.isOffline(context) && podcastId != null)) {
+ menu.removeItem(R.id.song_menu_remove_playlist);
+ }
+ // Remove show artists if parent is not set and if not on a album list
+ if((albumListType == null || (entry.getParent() == null && entry.getArtistId() == null)) && !Util.isOffline(context)) {
+ menu.removeItem(R.id.album_menu_show_artist);
+ }
+ if(podcastId != null && !Util.isOffline(context)) {
+ if(UserUtil.canPodcast()) {
+ String status = ((PodcastEpisode)entry).getStatus();
+ if("completed".equals(status)) {
+ menu.removeItem(R.id.song_menu_server_download);
+ }
+ } else {
+ menu.removeItem(R.id.song_menu_server_download);
+ menu.removeItem(R.id.song_menu_server_delete);
+ }
+ }
+
+ recreateContextMenu(menu);
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem menuItem) {
+ if(menuItem.getGroupId() != getSupportTag()) {
+ return false;
+ }
+
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
+ Object selectedItem;
+ int headers = entryList.getHeaderViewsCount();
+ if(albumContext) {
+ selectedItem = albumList.getItemAtPosition(info.position);
+ } else {
+ if(info.position == 0) {
+ return false;
+ }
+ selectedItem = entries.get(info.position - headers);
+ }
+
+ if(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_PLAY_NOW_AFTER, false) && menuItem.getItemId() == R.id.song_menu_play_now) {
+ List<Entry> songs = new ArrayList<Entry>();
+ Iterator it = entries.listIterator(info.position - headers);
+ while(it.hasNext()) {
+ songs.add((Entry) it.next());
+ }
+
+ playNow(songs);
+ return true;
+ }
+
+ if(onContextItemSelected(menuItem, selectedItem)) {
+ return true;
+ }
+
+ switch (menuItem.getItemId()) {
+ case R.id.song_menu_remove_playlist:
+ removeFromPlaylist(playlistId, playlistName, Arrays.<Integer>asList(info.position - headers));
+ break;
+ case R.id.song_menu_server_download:
+ downloadPodcastEpisode((PodcastEpisode)selectedItem);
+ break;
+ case R.id.song_menu_server_delete:
+ deletePodcastEpisode((PodcastEpisode)selectedItem);
+ break;
+ }
+
+ return true;
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ if (position >= 0) {
+ Entry entry = (Entry) parent.getItemAtPosition(position);
+ if (entry.isDirectory()) {
+ SubsonicFragment fragment = new SelectDirectoryFragment();
+ Bundle args = new Bundle();
+ args.putString(Constants.INTENT_EXTRA_NAME_ID, entry.getId());
+ args.putString(Constants.INTENT_EXTRA_NAME_NAME, entry.getTitle());
+ args.putSerializable(Constants.INTENT_EXTRA_NAME_DIRECTORY, entry);
+ if ("newest".equals(albumListType)) {
+ args.putBoolean(Constants.INTENT_EXTRA_REFRESH_LISTINGS, true);
+ }
+ if(entry.getArtist() == null && entry.getParent() == null) {
+ args.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, true);
+ }
+ fragment.setArguments(args);
+
+ replaceFragment(fragment, true);
+ } else if (entry.isVideo()) {
+ playVideo(entry);
+ } else if(entry instanceof PodcastEpisode) {
+ String status = ((PodcastEpisode)entry).getStatus();
+ if("error".equals(status)) {
+ Util.toast(context, R.string.select_podcasts_error);
+ return;
+ } else if(!"completed".equals(status)) {
+ Util.toast(context, R.string.select_podcasts_skipped);
+ return;
+ }
+
+ playNow(Arrays.asList(entry));
+ }
+ }
+ }
+
+ @Override
+ protected void refresh(boolean refresh) {
+ if(!"root".equals(id)) {
+ load(refresh);
+ }
+ }
+
+ private void load(boolean refresh) {
+ if(refreshListing) {
+ refresh = true;
+ }
+
+ if(currentTask != null) {
+ currentTask.cancel();
+ }
+
+ entryList.setVisibility(View.INVISIBLE);
+ if (playlistId != null) {
+ getPlaylist(playlistId, playlistName, refresh);
+ } else if(podcastId != null) {
+ getPodcast(podcastId, podcastName, refresh);
+ } else if (share != null) {
+ if(showAll) {
+ getRecursiveMusicDirectory(share.getId(), share.getName(), refresh);
+ } else {
+ getShare(share, refresh);
+ }
+ } else if (albumListType != null) {
+ getAlbumList(albumListType, albumListSize);
+ } else {
+ if(showAll) {
+ getRecursiveMusicDirectory(id, name, refresh);
+ } else if(topTracks) {
+ getTopTracks(id, name, refresh);
+ } else {
+ getMusicDirectory(id, name, refresh);
+ }
+ }
+ }
+
+ private void getMusicDirectory(final String id, final String name, final boolean refresh) {
+ setTitle(name);
+
+ new LoadTask(refresh) {
+ @Override
+ protected MusicDirectory load(MusicService service) throws Exception {
+ MusicDirectory dir = getMusicDirectory(id, name, refresh, service, this);
+
+ if(lookupParent && dir.getParent() != null) {
+ dir = getMusicDirectory(dir.getParent(), name, refresh, service, this);
+
+ // Update the fragment pointers so other stuff works correctly
+ SelectDirectoryFragment.this.id = dir.getId();
+ SelectDirectoryFragment.this.name = dir.getName();
+ } else if(id != null && directory == null && dir.getParent() != null && !artist) {
+ // View Album, try to lookup parent to get a complete entry to use for starring
+ MusicDirectory parentDir = getMusicDirectory(dir.getParent(), name, refresh, true, service, this);
+ for(Entry child: parentDir.getChildren()) {
+ if(id.equals(child.getId())) {
+ directory = child;
+ break;
+ }
+ }
+ }
+
+ return dir;
+ }
+
+ @Override
+ protected void done(Pair<MusicDirectory, Boolean> result) {
+ SelectDirectoryFragment.this.name = result.getFirst().getName();
+ setTitle(SelectDirectoryFragment.this.name);
+ super.done(result);
+ }
+ }.execute();
+ }
+
+ private void getRecursiveMusicDirectory(final String id, final String name, final boolean refresh) {
+ setTitle(name);
+
+ new LoadTask(refresh) {
+ @Override
+ protected MusicDirectory load(MusicService service) throws Exception {
+ MusicDirectory root;
+ if(share == null) {
+ root = getMusicDirectory(id, name, refresh, service, this);
+ } else {
+ root = share.getMusicDirectory();
+ }
+ List<Entry> songs = new ArrayList<Entry>();
+ getSongsRecursively(root, songs);
+ root.replaceChildren(songs);
+ return root;
+ }
+
+ private void getSongsRecursively(MusicDirectory parent, List<Entry> songs) throws Exception {
+ songs.addAll(parent.getChildren(false, true));
+ for (Entry dir : parent.getChildren(true, false)) {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+
+ MusicDirectory musicDirectory;
+ if(Util.isTagBrowsing(context) && !Util.isOffline(context)) {
+ musicDirectory = musicService.getAlbum(dir.getId(), dir.getTitle(), false, context, this);
+ } else {
+ musicDirectory = musicService.getMusicDirectory(dir.getId(), dir.getTitle(), false, context, this);
+ }
+ getSongsRecursively(musicDirectory, songs);
+ }
+ }
+
+ @Override
+ protected void done(Pair<MusicDirectory, Boolean> result) {
+ SelectDirectoryFragment.this.name = result.getFirst().getName();
+ setTitle(SelectDirectoryFragment.this.name);
+ super.done(result);
+ }
+ }.execute();
+ }
+
+ private void getPlaylist(final String playlistId, final String playlistName, final boolean refresh) {
+ setTitle(playlistName);
+
+ new LoadTask(refresh) {
+ @Override
+ protected MusicDirectory load(MusicService service) throws Exception {
+ return service.getPlaylist(refresh, playlistId, playlistName, context, this);
+ }
+ }.execute();
+ }
+
+ private void getPodcast(final String podcastId, final String podcastName, final boolean refresh) {
+ setTitle(podcastName);
+
+ new LoadTask(refresh) {
+ @Override
+ protected MusicDirectory load(MusicService service) throws Exception {
+ return service.getPodcastEpisodes(refresh, podcastId, context, this);
+ }
+ }.execute();
+ }
+
+ private void getShare(final Share share, final boolean refresh) {
+ setTitle(share.getName());
+
+ new LoadTask(refresh) {
+ @Override
+ protected MusicDirectory load(MusicService service) throws Exception {
+ return share.getMusicDirectory();
+ }
+ }.execute();
+ }
+
+ private void getTopTracks(final String id, final String name, final boolean refresh) {
+ setTitle(name);
+
+ new LoadTask(refresh) {
+ @Override
+ protected MusicDirectory load(MusicService service) throws Exception {
+ return service.getTopTrackSongs(name, 20, context, this);
+ }
+ }.execute();
+ }
+
+ private void getAlbumList(final String albumListType, final int size) {
+ if ("newest".equals(albumListType)) {
+ setTitle(R.string.main_albums_newest);
+ } else if ("random".equals(albumListType)) {
+ setTitle(R.string.main_albums_random);
+ } else if ("highest".equals(albumListType)) {
+ setTitle(R.string.main_albums_highest);
+ } else if ("recent".equals(albumListType)) {
+ setTitle(R.string.main_albums_recent);
+ } else if ("frequent".equals(albumListType)) {
+ setTitle(R.string.main_albums_frequent);
+ } else if ("starred".equals(albumListType)) {
+ setTitle(R.string.main_albums_starred);
+ } else if("genres".equals(albumListType) || "years".equals(albumListType)) {
+ setTitle(albumListExtra);
+ } else if("alphabeticalByName".equals(albumListType)) {
+ setTitle(R.string.main_albums_alphabetical);
+ }
+
+ new LoadTask(true) {
+ @Override
+ protected MusicDirectory load(MusicService service) throws Exception {
+ MusicDirectory result;
+ if ("starred".equals(albumListType)) {
+ result = service.getStarredList(context, this);
+ } else if(("genres".equals(albumListType) && ServerInfo.checkServerVersion(context, "1.10.0")) || "years".equals(albumListType)) {
+ result = service.getAlbumList(albumListType, albumListExtra, size, 0, context, this);
+ if(result.getChildrenSize() == 0 && "genres".equals(albumListType)) {
+ SelectDirectoryFragment.this.albumListType = "genres-songs";
+ result = service.getSongsByGenre(albumListExtra, size, 0, context, this);
+ }
+ } else if("genres".equals(albumListType) || "genres-songs".equals(albumListType)) {
+ result = service.getSongsByGenre(albumListExtra, size, 0, context, this);
+ } else {
+ result = service.getAlbumList(albumListType, size, 0, context, this);
+ }
+ return result;
+ }
+ }.execute();
+ }
+
+ private abstract class LoadTask extends TabBackgroundTask<Pair<MusicDirectory, Boolean>> {
+ private boolean refresh;
+
+ public LoadTask(boolean refresh) {
+ super(SelectDirectoryFragment.this);
+ this.refresh = refresh;
+
+ currentTask = this;
+ }
+
+ protected abstract MusicDirectory load(MusicService service) throws Exception;
+
+ @Override
+ protected Pair<MusicDirectory, Boolean> doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ MusicDirectory dir = load(musicService);
+ licenseValid = musicService.isLicenseValid(context, this);
+
+ albums = dir.getChildren(true, false);
+ if(largeAlbums) {
+ entries = dir.getChildren(false, true);
+ } else {
+ entries = dir.getChildren();
+ }
+
+ // This isn't really an artist if no albums on it!
+ if(albums.size() == 0) {
+ artist = false;
+ }
+
+ // If artist, we want to load the artist info to use later
+ if(artist && ServerInfo.hasArtistInfo(context) && !Util.isOffline(context)) {
+ try {
+ String artistId;
+ if(id.indexOf(';') == -1) {
+ artistId = id;
+ } else {
+ artistId = id.substring(0, id.indexOf(';'));
+ }
+
+ artistInfo = musicService.getArtistInfo(artistId, refresh, false, context, this);
+
+ if(artistInfo == null) {
+ artistInfoDelayed = artistId;
+ }
+ } catch(Exception e) {
+ Log.w(TAG, "Failed to get Artist Info even though it should be supported");
+ }
+ }
+
+ return new Pair<MusicDirectory, Boolean>(dir, licenseValid);
+ }
+
+ @Override
+ protected void done(Pair<MusicDirectory, Boolean> result) {
+ finishLoading();
+ currentTask = null;
+ }
+ }
+
+ private void finishLoading() {
+ // Show header if not album list type and not root and not artist
+ // For Subsonic 5.1+ display a header for artists with getArtistInfo data if it exists
+ View header = null;
+ if(albumListType == null && !"root".equals(id) && (!artist || artistInfo != null || artistInfoDelayed != null)) {
+ header = createHeader();
+
+ if(header != null && artistInfoDelayed != null) {
+ final View finalHeader = header.findViewById(R.id.select_album_header);
+ final View headerProgress = header.findViewById(R.id.header_progress);
+
+ finalHeader.setVisibility(View.INVISIBLE);
+ headerProgress.setVisibility(View.VISIBLE);
+
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ artistInfo = musicService.getArtistInfo(artistInfoDelayed, false, true, context, this);
+
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ /*if(albumList instanceof HeaderGridView) {
+ HeaderGridView headerGridView = (HeaderGridView) albumList;
+ headerGridView.invalidateRowHeight();
+ ((BaseAdapter) headerGridView.getAdapter()).notifyDataSetChanged();
+ }*/
+
+ setupCoverArt(finalHeader);
+ setupTextDisplay(finalHeader);
+ setupButtonEvents(finalHeader);
+
+ finalHeader.setVisibility(View.VISIBLE);
+ headerProgress.setVisibility(View.GONE);
+ }
+ }.execute();
+ }
+
+ // Only add header to entry list if we aren't going recreate album grid as root anyways
+ if(header != null && entryList != null && (!addAlbumHeader || entries.size() > 0)) {
+ entryList.addHeaderView(header, null, false);
+ header = null;
+ }
+ }
+
+ // Needs to be added here, GB crashes if you to try to remove the header view before adapter is set
+ if(addAlbumHeader) {
+ if(entries.size() > 0 || playlistId != null || podcastId != null) {
+ entryList.addHeaderView(albumList);
+ } else {
+ ViewGroup rootGroup = (ViewGroup) rootView.findViewById(R.id.select_album_layout);
+ albumList = (GridView) context.getLayoutInflater().inflate(R.layout.grid_view, rootGroup, false);
+ rootGroup.removeView(entryList);
+ rootGroup.addView(albumList);
+
+ setupScrollList(albumList);
+ setupAlbumList();
+
+ // This should only not be null for a artist with only albums
+ if(header != null) {
+ HeaderGridView headerGridView = (HeaderGridView) albumList;
+ headerGridView.addHeaderView(header);
+ }
+ }
+ addAlbumHeader = false;
+ }
+
+ boolean validData = !entries.isEmpty() || !albums.isEmpty();
+ if(!validData) {
+ setEmpty(true);
+ }
+ // Always going to have entries in entryAdapter
+ entryAdapter = new EntryAdapter(context, getImageLoader(), entries, (podcastId == null));
+ ListAdapter listAdapter = entryAdapter;
+ // Song-only genre needs to always be entry list + infinite adapter
+ if("genres-songs".equals(albumListType)) {
+ ViewGroup rootGroup = (ViewGroup) rootView.findViewById(R.id.select_album_layout);
+ if(rootGroup.findViewById(R.id.gridview) != null && largeAlbums) {
+ rootGroup.removeView(albumList);
+ rootGroup.addView(entryList);
+ }
+
+ listAdapter = new AlbumListAdapter(context, entryAdapter, albumListType, albumListExtra, albumListSize);
+ } else if(albumListType == null || "starred".equals(albumListType)) {
+ // Only set standard album adapter if not album list and largeAlbums is true
+ if(largeAlbums) {
+ albumList.setAdapter(new AlbumGridAdapter(context, getImageLoader(), albums, !artist));
+ }
+ } else {
+ // If album list, use infinite adapters for either depending on whether or not largeAlbums is true
+ if(largeAlbums) {
+ albumList.setAdapter(new AlbumListAdapter(context, new AlbumGridAdapter(context, getImageLoader(), albums, true), albumListType, albumListExtra, albumListSize));
+ } else {
+ listAdapter = new AlbumListAdapter(context, entryAdapter, albumListType, albumListExtra, albumListSize);
+ }
+ }
+ entryList.setAdapter(listAdapter);
+ if(validData) {
+ entryList.setVisibility(View.VISIBLE);
+ }
+ context.supportInvalidateOptionsMenu();
+
+ if(lookupEntry != null) {
+ for(int i = 0; i < entries.size(); i++) {
+ if(lookupEntry.equals(entries.get(i).getTitle())) {
+ entryList.setSelection(i + entryList.getHeaderViewsCount());
+ lookupEntry = null;
+ break;
+ }
+ }
+ }
+
+ Bundle args = getArguments();
+ boolean playAll = args.getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false);
+ if (playAll && !restoredInstance) {
+ playAll(args.getBoolean(Constants.INTENT_EXTRA_NAME_SHUFFLE, false), false);
+ }
+ }
+
+ private void setupAlbumList() {
+ albumList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ Entry entry = (Entry) parent.getItemAtPosition(position);
+ SubsonicFragment fragment = new SelectDirectoryFragment();
+ Bundle args = new Bundle();
+ args.putString(Constants.INTENT_EXTRA_NAME_ID, entry.getId());
+ args.putString(Constants.INTENT_EXTRA_NAME_NAME, entry.getTitle());
+ args.putSerializable(Constants.INTENT_EXTRA_NAME_DIRECTORY, entry);
+ if ("newest".equals(albumListType)) {
+ args.putBoolean(Constants.INTENT_EXTRA_REFRESH_LISTINGS, true);
+ }
+ if(entry.getArtist() == null && entry.getParent() == null) {
+ args.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, true);
+ }
+ fragment.setArguments(args);
+
+ replaceFragment(fragment, true);
+ }
+ });
+
+ registerForContextMenu(entryList);
+ registerForContextMenu(albumList);
+ }
+
+ private void playNow(final boolean shuffle, final boolean append) {
+ playNow(shuffle, append, false);
+ }
+ private void playNow(final boolean shuffle, final boolean append, final boolean playNext) {
+ if(getSelectedSongs().size() > 0) {
+ download(append, false, !append, playNext, shuffle);
+ selectAll(false, false);
+ }
+ else {
+ playAll(shuffle, append);
+ }
+ }
+ private void playAll(final boolean shuffle, final boolean append) {
+ boolean hasSubFolders = false;
+ for (int i = 0; i < entryList.getCount(); i++) {
+ Entry entry = (Entry) entryList.getItemAtPosition(i);
+ if (entry != null && entry.isDirectory()) {
+ hasSubFolders = true;
+ break;
+ }
+ }
+ if(albums.size() > 0) {
+ hasSubFolders = true;
+ }
+
+ if (hasSubFolders && (id != null || share != null || "starred".equals(albumListType))) {
+ downloadRecursively(id, false, append, !append, shuffle, false);
+ } else if(hasSubFolders && albumListType != null) {
+ downloadRecursively(albums, shuffle, append);
+ } else {
+ selectAll(true, false);
+ download(append, false, !append, false, shuffle);
+ selectAll(false, false);
+ }
+ }
+
+ private void selectAll(boolean selected, boolean toast) {
+ int count = entryList.getCount();
+ int selectedCount = 0;
+ for (int i = 0; i < count; i++) {
+ Entry entry = (Entry) entryList.getItemAtPosition(i);
+ if (entry != null && !entry.isDirectory() && !entry.isVideo()) {
+ entryList.setItemChecked(i, selected);
+ selectedCount++;
+ }
+ }
+
+ // Display toast: N tracks selected / N tracks unselected
+ if (toast) {
+ int toastResId = selected ? R.string.select_album_n_selected
+ : R.string.select_album_n_unselected;
+ Util.toast(context, context.getString(toastResId, selectedCount));
+ }
+ }
+
+ private List<Entry> getSelectedSongs() {
+ List<Entry> songs = new ArrayList<Entry>(10);
+ int count = entryList.getCount();
+ for (int i = 0; i < count; i++) {
+ if (entryList.isItemChecked(i)) {
+ Entry entry = (Entry) entryList.getItemAtPosition(i);
+ // Don't try to add directories or 1-starred songs
+ if(!entry.isDirectory() && entry.getRating() != 1) {
+ songs.add(entry);
+ }
+ }
+ }
+ return songs;
+ }
+
+ private List<Integer> getSelectedIndexes() {
+ List<Integer> indexes = new ArrayList<Integer>();
+
+ int count = entryList.getCount();
+ int headers = entryList.getHeaderViewsCount();
+ for (int i = 0; i < count; i++) {
+ if (entryList.isItemChecked(i)) {
+ indexes.add(i - headers);
+ }
+ }
+
+ return indexes;
+ }
+
+ private void download(final boolean append, final boolean save, final boolean autoplay, final boolean playNext, final boolean shuffle) {
+ if (getDownloadService() == null) {
+ return;
+ }
+
+ final List<Entry> songs = getSelectedSongs();
+ warnIfStorageUnavailable();
+
+ // Conditions for using play now button
+ if(!append && !save && autoplay && !playNext && !shuffle) {
+ // Call playNow which goes through and tries to use bookmark information
+ playNow(songs, playlistName, playlistId);
+ return;
+ }
+
+ LoadingTask<Void> onValid = new LoadingTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ if (!append) {
+ getDownloadService().clear();
+ }
+
+ getDownloadService().download(songs, save, autoplay, playNext, shuffle);
+ if (playlistName != null) {
+ getDownloadService().setSuggestedPlaylistName(playlistName, playlistId);
+ } else {
+ getDownloadService().setSuggestedPlaylistName(null, null);
+ }
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ if (autoplay) {
+ Util.startActivityWithoutTransition(context, DownloadActivity.class);
+ } else if (save) {
+ Util.toast(context,
+ context.getResources().getQuantityString(R.plurals.select_album_n_songs_downloading, songs.size(), songs.size()));
+ } else if (append) {
+ Util.toast(context,
+ context.getResources().getQuantityString(R.plurals.select_album_n_songs_added, songs.size(), songs.size()));
+ }
+ }
+ };
+
+ checkLicenseAndTrialPeriod(onValid);
+ }
+ private void downloadBackground(final boolean save) {
+ if(playlistId != null) {
+ selectAll(true, false);
+ }
+
+ List<Entry> songs = getSelectedSongs();
+ if(songs.isEmpty()) {
+ // Get both songs and albums
+ downloadRecursively(id, save, false, false, false, true);
+ } else {
+ downloadBackground(save, songs);
+ }
+ }
+ private void downloadBackground(final boolean save, final List<Entry> songs) {
+ if (getDownloadService() == null) {
+ return;
+ }
+
+ warnIfStorageUnavailable();
+ LoadingTask<Void> onValid = new LoadingTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ getDownloadService().downloadBackground(songs, save);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ Util.toast(context, context.getResources().getQuantityString(R.plurals.select_album_n_songs_downloading, songs.size(), songs.size()));
+ }
+ };
+
+ checkLicenseAndTrialPeriod(onValid);
+ }
+
+ private void delete() {
+ List<Entry> songs = getSelectedSongs();
+ if(songs.isEmpty()) {
+ selectAll(true, false);
+ songs = getSelectedSongs();
+
+ // Also delete all directories
+ for(Entry album: albums) {
+ deleteRecursively(album);
+ }
+ }
+ if (getDownloadService() != null) {
+ getDownloadService().delete(songs);
+ }
+ }
+
+ public void removeFromPlaylist(final String id, final String name, final List<Integer> indexes) {
+ new LoadingTask<Void>(context, true) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.removeFromPlaylist(id, indexes, context, null);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ for(int i = indexes.size() - 1; i >= 0; i--) {
+ entryList.setItemChecked(indexes.get(i) + 1, false);
+ entryAdapter.removeAt(indexes.get(i));
+ }
+ entryAdapter.notifyDataSetChanged();
+ Util.toast(context, context.getResources().getString(R.string.removed_playlist, indexes.size(), name));
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.updated_playlist_error, name) + " " + getErrorMessage(error);
+ }
+
+ Util.toast(context, msg, false);
+ }
+ }.execute();
+ }
+
+ public void downloadAllPodcastEpisodes() {
+ new LoadingTask<Void>(context, true) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+
+ for(int i = 0; i < entries.size(); i++) {
+ PodcastEpisode episode = (PodcastEpisode) entries.get(i);
+ if("skipped".equals(episode.getStatus())) {
+ musicService.downloadPodcastEpisode(episode.getEpisodeId(), context, null);
+ }
+ }
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ Util.toast(context, context.getResources().getString(R.string.select_podcasts_downloading, podcastName));
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ Util.toast(context, getErrorMessage(error), false);
+ }
+ }.execute();
+ }
+
+ public void downloadPodcastEpisode(final PodcastEpisode episode) {
+ new LoadingTask<Void>(context, true) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.downloadPodcastEpisode(episode.getEpisodeId(), context, null);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ Util.toast(context, context.getResources().getString(R.string.select_podcasts_downloading, episode.getTitle()));
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ Util.toast(context, getErrorMessage(error), false);
+ }
+ }.execute();
+ }
+
+ public void deletePodcastEpisode(final PodcastEpisode episode) {
+ Util.confirmDialog(context, R.string.common_delete, episode.getTitle(), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ new LoadingTask<Void>(context, true) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.deletePodcastEpisode(episode.getEpisodeId(), episode.getParent(), null, context);
+ if (getDownloadService() != null) {
+ List<Entry> episodeList = new ArrayList<Entry>(1);
+ episodeList.add(episode);
+ getDownloadService().delete(episodeList);
+ }
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ entries.remove(episode);
+ entryAdapter.notifyDataSetChanged();
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ Log.w(TAG, "Failed to delete podcast episode", error);
+ Util.toast(context, getErrorMessage(error), false);
+ }
+ }.execute();
+ }
+ });
+ }
+
+ public void unstarSelected() {
+ List<Entry> selected = getSelectedSongs();
+ if(selected.size() == 0) {
+ selected = entries;
+ }
+ if(selected.size() == 0) {
+ return;
+ }
+ final List<Entry> unstar = new ArrayList<Entry>();
+ unstar.addAll(selected);
+
+ new LoadingTask<Void>(context, true) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ List<Entry> entries = new ArrayList<Entry>();
+ List<Entry> artists = new ArrayList<Entry>();
+ List<Entry> albums = new ArrayList<Entry>();
+ for(Entry entry: unstar) {
+ if(entry.isDirectory()) {
+ if(entry.isAlbum()) {
+ albums.add(entry);
+ } else {
+ artists.add(entry);
+ }
+ } else {
+ entries.add(entry);
+ }
+ }
+ musicService.setStarred(entries, artists, albums, false, this, context);
+
+ for(Entry entry: unstar) {
+ new EntryInstanceUpdater(entry) {
+ @Override
+ public void update(Entry found) {
+ found.setStarred(false);
+ }
+ }.execute();
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ Util.toast(context, context.getResources().getString(R.string.starring_content_unstarred, Integer.toString(unstar.size())));
+
+ for(Entry entry: unstar) {
+ entries.remove(entry);
+ }
+ entryAdapter.notifyDataSetChanged();
+ selectAll(false, false);
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.starring_content_error, Integer.toString(unstar.size())) + " " + getErrorMessage(error);
+ }
+
+ Util.toast(context, msg, false);
+ }
+ }.execute();
+ }
+
+ private void checkLicenseAndTrialPeriod(LoadingTask onValid) {
+ if (licenseValid) {
+ onValid.execute();
+ return;
+ }
+
+ int trialDaysLeft = Util.getRemainingTrialDays(context);
+ Log.i(TAG, trialDaysLeft + " trial days left.");
+
+ if (trialDaysLeft == 0) {
+ showDonationDialog(trialDaysLeft, null);
+ } else if (trialDaysLeft < Constants.FREE_TRIAL_DAYS / 2) {
+ showDonationDialog(trialDaysLeft, onValid);
+ } else {
+ Util.toast(context, context.getResources().getString(R.string.select_album_not_licensed, trialDaysLeft));
+ onValid.execute();
+ }
+ }
+
+ private void showDonationDialog(int trialDaysLeft, final LoadingTask onValid) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setIcon(android.R.drawable.ic_dialog_info);
+
+ if (trialDaysLeft == 0) {
+ builder.setTitle(R.string.select_album_donate_dialog_0_trial_days_left);
+ } else {
+ builder.setTitle(context.getResources().getQuantityString(R.plurals.select_album_donate_dialog_n_trial_days_left,
+ trialDaysLeft, trialDaysLeft));
+ }
+
+ builder.setMessage(R.string.select_album_donate_dialog_message);
+
+ builder.setPositiveButton(R.string.select_album_donate_dialog_now,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(Constants.DONATION_URL)));
+ }
+ });
+
+ builder.setNegativeButton(R.string.select_album_donate_dialog_later,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ dialogInterface.dismiss();
+ if (onValid != null) {
+ onValid.execute();
+ }
+ }
+ });
+
+ builder.create().show();
+ }
+
+ private void showTopTracks() {
+ SubsonicFragment fragment = new SelectDirectoryFragment();
+ Bundle args = new Bundle(getArguments());
+ args.putBoolean(Constants.INTENT_EXTRA_TOP_TRACKS, true);
+ fragment.setArguments(args);
+
+ replaceFragment(fragment, true);
+ }
+
+ private void setShowAll() {
+ SubsonicFragment fragment = new SelectDirectoryFragment();
+ Bundle args = new Bundle(getArguments());
+ args.putBoolean(Constants.INTENT_EXTRA_SHOW_ALL, true);
+ fragment.setArguments(args);
+
+ replaceFragment(fragment, true);
+ }
+
+ private void showSimilarArtists(String artistId) {
+ SubsonicFragment fragment = new SimilarArtistFragment();
+ Bundle args = new Bundle();
+ args.putString(Constants.INTENT_EXTRA_NAME_ARTIST, artistId);
+ fragment.setArguments(args);
+
+ replaceFragment(fragment, true);
+ }
+
+ private void startArtistRadio(final String artistId) {
+ new LoadingTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ DownloadService downloadService = getDownloadService();
+ downloadService.setArtistRadio(artistId);
+ if(downloadService.size() == 0) {
+ Log.e(TAG, "Failed to create artist radio");
+ throw new Exception("Failed to create artist radio");
+ }
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ Util.startActivityWithoutTransition(context, DownloadActivity.class);
+ }
+ }.execute();
+ }
+
+ private View createHeader() {
+ View header = entryList.findViewById(R.id.select_album_header_wrapper);
+ boolean add = false;
+ if(header == null) {
+ header = LayoutInflater.from(context).inflate(R.layout.select_album_header, entryList, false);
+ add = true;
+ }
+
+ setupCoverArt(header);
+ setupTextDisplay(header);
+
+ if(add) {
+ setupButtonEvents(header);
+ }
+
+ if(add) {
+ return header;
+ } else {
+ return null;
+ }
+ }
+
+ private void setupCoverArt(View header) {
+ final ImageLoader imageLoader = getImageLoader();
+ View coverArtView = header.findViewById(R.id.select_album_art);
+
+ // Try a few times to get a random cover art
+ if(artistInfo != null) {
+ final String url = artistInfo.getImageUrl();
+ coverArtView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (url == null) {
+ return;
+ }
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ ImageView fullScreenView = new ImageView(context);
+ imageLoader.loadImage(fullScreenView, url, true);
+ builder.setCancelable(true);
+
+ AlertDialog imageDialog = builder.create();
+ // Set view here with unecessary 0's to remove top/bottom border
+ imageDialog.setView(fullScreenView, 0, 0, 0, 0);
+ imageDialog.show();
+ }
+ });
+ imageLoader.loadImage(coverArtView, url, false);
+ } else if(entries.size() > 0) {
+ Entry coverArt = null;
+ for (int i = 0; (i < 3) && (coverArt == null || coverArt.getCoverArt() == null); i++) {
+ coverArt = entries.get(random.nextInt(entries.size()));
+ }
+
+ final Entry albumRep = coverArt;
+ coverArtView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (albumRep.getCoverArt() == null) {
+ return;
+ }
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ ImageView fullScreenView = new ImageView(context);
+ imageLoader.loadImage(fullScreenView, albumRep, true, true);
+ builder.setCancelable(true);
+
+ AlertDialog imageDialog = builder.create();
+ // Set view here with unecessary 0's to remove top/bottom border
+ imageDialog.setView(fullScreenView, 0, 0, 0, 0);
+ imageDialog.show();
+ }
+ });
+ imageLoader.loadImage(coverArtView, albumRep, false, true);
+ }
+ }
+ private void setupTextDisplay(final View header) {
+ final TextView titleView = (TextView) header.findViewById(R.id.select_album_title);
+ if(playlistName != null) {
+ titleView.setText(playlistName);
+ } else if(podcastName != null) {
+ titleView.setText(podcastName);
+ titleView.setPadding(0, 6, 4, 8);
+ } else if(name != null) {
+ titleView.setText(name);
+
+ if(artistInfo != null) {
+ titleView.setPadding(0, 6, 4, 8);
+ }
+ } else if(share != null) {
+ titleView.setVisibility(View.GONE);
+ }
+
+ int songCount = 0;
+
+ Set<String> artists = new HashSet<String>();
+ Set<Integer> years = new HashSet<Integer>();
+ Integer totalDuration = 0;
+ for (Entry entry : entries) {
+ if (!entry.isDirectory()) {
+ songCount++;
+ if (entry.getArtist() != null) {
+ artists.add(entry.getArtist());
+ }
+ if(entry.getYear() != null) {
+ years.add(entry.getYear());
+ }
+ Integer duration = entry.getDuration();
+ if(duration != null) {
+ totalDuration += duration;
+ }
+ }
+ }
+
+ final TextView artistView = (TextView) header.findViewById(R.id.select_album_artist);
+ if(podcastDescription != null || artistInfo != null) {
+ artistView.setVisibility(View.VISIBLE);
+ String text = podcastDescription != null ? podcastDescription : artistInfo.getBiography();
+ Spanned spanned = null;
+ if(text != null) {
+ spanned = Html.fromHtml(text);
+ }
+ artistView.setText(spanned);
+ artistView.setSingleLine(false);
+ final int minLines = context.getResources().getInteger(R.integer.TextDescriptionLength);
+ artistView.setLines(minLines);
+ artistView.setTextAppearance(context, android.R.style.TextAppearance_Small);
+
+ final Spanned spannedText = spanned;
+ artistView.setOnClickListener(new View.OnClickListener() {
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ @Override
+ public void onClick(View v) {
+ if(artistView.getMaxLines() == minLines) {
+ // Use LeadingMarginSpan2 to try to make text flow around image
+ Display display = context.getWindowManager().getDefaultDisplay();
+ ImageView coverArtView = (ImageView) header.findViewById(R.id.select_album_art);
+ coverArtView.measure(display.getWidth(), display.getHeight());
+
+ int height, width;
+ ViewGroup.MarginLayoutParams vlp = (ViewGroup.MarginLayoutParams) coverArtView.getLayoutParams();
+ if(coverArtView.getDrawable() != null) {
+ height = coverArtView.getMeasuredHeight() + coverArtView.getPaddingBottom();
+ width = coverArtView.getWidth() + coverArtView.getPaddingRight();
+ } else {
+ height = coverArtView.getHeight();
+ width = coverArtView.getWidth() + coverArtView.getPaddingRight();
+ }
+ float textLineHeight = artistView.getPaint().getTextSize();
+ int lines = (int) Math.ceil(height / textLineHeight);
+
+ SpannableString ss = new SpannableString(spannedText);
+ ss.setSpan(new MyLeadingMarginSpan2(lines, width), 0, ss.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+ View linearLayout = header.findViewById(R.id.select_album_text_layout);
+ RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) linearLayout.getLayoutParams();
+ int[]rules = params.getRules();
+ rules[RelativeLayout.RIGHT_OF] = 0;
+ params.leftMargin = vlp.rightMargin;
+
+ artistView.setText(ss);
+ artistView.setMaxLines(100);
+
+ vlp = (ViewGroup.MarginLayoutParams) titleView.getLayoutParams();
+ vlp.leftMargin = width;
+ } else {
+ artistView.setMaxLines(minLines);
+ }
+
+ if(albumList instanceof HeaderGridView) {
+ HeaderGridView headerGridView = (HeaderGridView) albumList;
+ ((BaseAdapter) headerGridView.getAdapter()).notifyDataSetChanged();
+ }
+ }
+ });
+ artistView.setMovementMethod(LinkMovementMethod.getInstance());
+ } else if(topTracks) {
+ artistView.setText(R.string.menu_top_tracks);
+ artistView.setVisibility(View.VISIBLE);
+ } else if(showAll) {
+ artistView.setText(R.string.menu_show_all);
+ artistView.setVisibility(View.VISIBLE);
+ } else if (artists.size() == 1) {
+ String artistText = artists.iterator().next();
+ if(years.size() == 1) {
+ artistText += " - " + years.iterator().next();
+ }
+ artistView.setText(artistText);
+ artistView.setVisibility(View.VISIBLE);
+ } else {
+ artistView.setVisibility(View.GONE);
+ }
+
+ TextView songCountView = (TextView) header.findViewById(R.id.select_album_song_count);
+ TextView songLengthView = (TextView) header.findViewById(R.id.select_album_song_length);
+ if(podcastDescription != null || artistInfo != null) {
+ songCountView.setVisibility(View.GONE);
+ songLengthView.setVisibility(View.GONE);
+ } else {
+ String s = context.getResources().getQuantityString(R.plurals.select_album_n_songs, songCount, songCount);
+ songCountView.setText(s.toUpperCase());
+ songLengthView.setText(Util.formatDuration(totalDuration));
+ }
+ }
+ private void setupButtonEvents(View header) {
+ ImageView shareButton = (ImageView) header.findViewById(R.id.select_album_share);
+ if(share != null || podcastId != null || !Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_MENU_SHARED, true) || Util.isOffline(context) || !UserUtil.canShare() || artistInfo != null) {
+ shareButton.setVisibility(View.GONE);
+ } else {
+ shareButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ createShare(SelectDirectoryFragment.this.entries);
+ }
+ });
+ }
+
+ final ImageButton starButton = (ImageButton) header.findViewById(R.id.select_album_star);
+ if(directory != null && Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_MENU_STAR, true) && artistInfo == null) {
+ starButton.setImageResource(directory.isStarred() ? android.R.drawable.btn_star_big_on : android.R.drawable.btn_star_big_off);
+ starButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ toggleStarred(directory, new OnStarChange() {
+ @Override
+ void starChange(boolean starred) {
+ starButton.setImageResource(directory.isStarred() ? android.R.drawable.btn_star_big_on : android.R.drawable.btn_star_big_off);
+ }
+ });
+ }
+ });
+ } else {
+ starButton.setVisibility(View.GONE);
+ }
+
+ View ratingBarWrapper = header.findViewById(R.id.select_album_rate_wrapper);
+ final RatingBar ratingBar = (RatingBar) header.findViewById(R.id.select_album_rate);
+ if(directory != null && Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_MENU_RATING, true) && !Util.isOffline(context) && artistInfo == null) {
+ ratingBar.setRating(directory.getRating());
+ ratingBarWrapper.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ setRating(directory, new OnRatingChange() {
+ @Override
+ void ratingChange(int rating) {
+ ratingBar.setRating(directory.getRating());
+ }
+ });
+ }
+ });
+ } else {
+ ratingBar.setVisibility(View.GONE);
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SelectGenreFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SelectGenreFragment.java
new file mode 100644
index 00000000..2d310172
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/fragments/SelectGenreFragment.java
@@ -0,0 +1,71 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.fragments;
+
+import android.os.Bundle;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.Genre;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.ProgressListener;
+import github.daneren2005.dsub.adapter.GenreAdapter;
+
+import java.util.List;
+
+public class SelectGenreFragment extends SelectListFragment<Genre> {
+ private static final String TAG = SelectGenreFragment.class.getSimpleName();
+
+ @Override
+ public int getOptionsMenu() {
+ return R.menu.empty;
+ }
+
+ @Override
+ public ArrayAdapter getAdapter(List<Genre> objs) {
+ return new GenreAdapter(context, objs);
+ }
+
+ @Override
+ public List<Genre> getObjects(MusicService musicService, boolean refresh, ProgressListener listener) throws Exception {
+ return musicService.getGenres(refresh, context, listener);
+ }
+
+ @Override
+ public int getTitleResource() {
+ return R.string.main_albums_genres;
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ Genre genre = (Genre) parent.getItemAtPosition(position);
+
+ SubsonicFragment fragment = new SelectDirectoryFragment();
+ Bundle args = new Bundle();
+ args.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, "genres");
+ args.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 20);
+ args.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0);
+ args.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_EXTRA, genre.getName());
+ fragment.setArguments(args);
+
+ replaceFragment(fragment);
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SelectListFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SelectListFragment.java
new file mode 100644
index 00000000..6f73f6e8
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/fragments/SelectListFragment.java
@@ -0,0 +1,163 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.fragments;
+
+import android.os.Bundle;
+import android.support.v4.widget.SwipeRefreshLayout;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import github.daneren2005.dsub.util.BackgroundTask;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.ProgressListener;
+import github.daneren2005.dsub.util.TabBackgroundTask;
+
+public abstract class SelectListFragment<T> extends SubsonicFragment implements AdapterView.OnItemClickListener {
+ private static final String TAG = SelectListFragment.class.getSimpleName();
+ protected ListView listView;
+ protected ArrayAdapter adapter;
+ protected BackgroundTask<List<T>> currentTask;
+ protected List<T> objects;
+ protected boolean serialize = true;
+
+ @Override
+ public void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+
+ if(bundle != null && serialize) {
+ objects = (List<T>) bundle.getSerializable(Constants.FRAGMENT_LIST);
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ if(serialize) {
+ outState.putSerializable(Constants.FRAGMENT_LIST, (Serializable) objects);
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
+ rootView = inflater.inflate(R.layout.abstract_list_fragment, container, false);
+
+ refreshLayout = (SwipeRefreshLayout) rootView.findViewById(R.id.refresh_layout);
+ refreshLayout.setOnRefreshListener(this);
+
+ listView = (ListView)rootView.findViewById(R.id.fragment_list);
+ listView.setOnItemClickListener(this);
+ setupScrollList(listView);
+ registerForContextMenu(listView);
+
+ if(objects == null) {
+ refresh(false);
+ } else {
+ listView.setAdapter(adapter = getAdapter(objects));
+ }
+
+ return rootView;
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) {
+ if(!primaryFragment) {
+ return;
+ }
+
+ menuInflater.inflate(getOptionsMenu(), menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ protected void refresh(final boolean refresh) {
+ int titleRes = getTitleResource();
+ if(titleRes != 0) {
+ setTitle(getTitleResource());
+ }
+ listView.setVisibility(View.GONE);
+
+ // Cancel current running task before starting another one
+ if(currentTask != null) {
+ currentTask.cancel();
+ }
+
+ currentTask = new TabBackgroundTask<List<T>>(this) {
+ @Override
+ protected List<T> doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+
+ objects = new ArrayList<T>();
+
+ try {
+ objects = getObjects(musicService, refresh, this);
+ } catch (Exception x) {
+ Log.e(TAG, "Failed to load", x);
+ }
+
+ return objects;
+ }
+
+ @Override
+ protected void done(List<T> result) {
+ if (result != null && !result.isEmpty()) {
+ // Toggle fast scroll to get around issue when length of list changes
+ listView.setFastScrollEnabled(false);
+ listView.setAdapter(adapter = getAdapter(result));
+ listView.setFastScrollEnabled(true);
+
+ onFinishRefresh();
+ listView.setVisibility(View.VISIBLE);
+ } else {
+ setEmpty(true);
+ }
+
+ currentTask = null;
+ }
+ };
+ currentTask.execute();
+ }
+
+ public abstract int getOptionsMenu();
+ public abstract ArrayAdapter getAdapter(List<T> objs);
+ public abstract List<T> getObjects(MusicService musicService, boolean refresh, ProgressListener listener) throws Exception;
+ public abstract int getTitleResource();
+
+ public void onFinishRefresh() {
+
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SelectPlaylistFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SelectPlaylistFragment.java
new file mode 100644
index 00000000..3d7e664f
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/fragments/SelectPlaylistFragment.java
@@ -0,0 +1,303 @@
+package github.daneren2005.dsub.fragments;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.support.v4.app.FragmentTransaction;
+import android.view.ContextMenu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.CheckBox;
+import android.widget.EditText;
+
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.Playlist;
+import github.daneren2005.dsub.domain.ServerInfo;
+import github.daneren2005.dsub.service.DownloadFile;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import github.daneren2005.dsub.service.OfflineException;
+import github.daneren2005.dsub.service.ServerTooOldException;
+import github.daneren2005.dsub.util.ProgressListener;
+import github.daneren2005.dsub.util.SyncUtil;
+import github.daneren2005.dsub.util.CacheCleaner;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.LoadingTask;
+import github.daneren2005.dsub.util.UserUtil;
+import github.daneren2005.dsub.util.Util;
+import github.daneren2005.dsub.adapter.PlaylistAdapter;
+
+import java.util.List;
+
+public class SelectPlaylistFragment extends SelectListFragment<Playlist> {
+ private static final String TAG = SelectPlaylistFragment.class.getSimpleName();
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, view, menuInfo);
+
+ MenuInflater inflater = context.getMenuInflater();
+ if (Util.isOffline(context)) {
+ inflater.inflate(R.menu.select_playlist_context_offline, menu);
+ }
+ else {
+ inflater.inflate(R.menu.select_playlist_context, menu);
+
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
+ Playlist playlist = (Playlist) listView.getItemAtPosition(info.position);
+ if(SyncUtil.isSyncedPlaylist(context, playlist.getId())) {
+ menu.removeItem(R.id.playlist_menu_sync);
+ } else {
+ menu.removeItem(R.id.playlist_menu_stop_sync);
+ }
+
+ if(!ServerInfo.checkServerVersion(context, "1.8")) {
+ menu.removeItem(R.id.playlist_update_info);
+ } else if(playlist.getPublic() != null && playlist.getPublic() == true && playlist.getId().indexOf(".m3u") == -1 && !UserUtil.getCurrentUsername(context).equals(playlist.getOwner())) {
+ menu.removeItem(R.id.playlist_update_info);
+ menu.removeItem(R.id.playlist_menu_delete);
+ }
+ }
+
+ recreateContextMenu(menu);
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem menuItem) {
+ if(menuItem.getGroupId() != getSupportTag()) {
+ return false;
+ }
+
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
+ Playlist playlist = (Playlist) listView.getItemAtPosition(info.position);
+
+ SubsonicFragment fragment;
+ Bundle args;
+ FragmentTransaction trans;
+ switch (menuItem.getItemId()) {
+ case R.id.playlist_menu_download:
+ downloadPlaylist(playlist.getId(), playlist.getName(), false, true, false, false, true);
+ break;
+ case R.id.playlist_menu_sync:
+ syncPlaylist(playlist);
+ break;
+ case R.id.playlist_menu_stop_sync:
+ stopSyncPlaylist(playlist);
+ break;
+ case R.id.playlist_menu_play_now:
+ fragment = new SelectDirectoryFragment();
+ args = new Bundle();
+ args.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, playlist.getId());
+ args.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME, playlist.getName());
+ args.putBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, true);
+ fragment.setArguments(args);
+
+ replaceFragment(fragment);
+ break;
+ case R.id.playlist_menu_play_shuffled:
+ fragment = new SelectDirectoryFragment();
+ args = new Bundle();
+ args.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, playlist.getId());
+ args.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME, playlist.getName());
+ args.putBoolean(Constants.INTENT_EXTRA_NAME_SHUFFLE, true);
+ args.putBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, true);
+ fragment.setArguments(args);
+
+ replaceFragment(fragment);
+ break;
+ case R.id.playlist_menu_delete:
+ deletePlaylist(playlist);
+ break;
+ case R.id.playlist_info:
+ displayPlaylistInfo(playlist);
+ break;
+ case R.id.playlist_update_info:
+ updatePlaylistInfo(playlist);
+ break;
+ default:
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int getOptionsMenu() {
+ return R.menu.abstract_top_menu;
+ }
+
+ @Override
+ public ArrayAdapter getAdapter(List<Playlist> playlists) {
+ return new PlaylistAdapter(context, playlists);
+ }
+
+ @Override
+ public List<Playlist> getObjects(MusicService musicService, boolean refresh, ProgressListener listener) throws Exception {
+ List<Playlist> playlists = musicService.getPlaylists(refresh, context, listener);
+ if(!Util.isOffline(context) && refresh) {
+ new CacheCleaner(context, getDownloadService()).cleanPlaylists(playlists);
+ }
+ return playlists;
+ }
+
+ @Override
+ public int getTitleResource() {
+ return R.string.playlist_label;
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ Playlist playlist = (Playlist) parent.getItemAtPosition(position);
+
+ SubsonicFragment fragment = new SelectDirectoryFragment();
+ Bundle args = new Bundle();
+ args.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, playlist.getId());
+ args.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME, playlist.getName());
+ if(ServerInfo.checkServerVersion(context, "1.8") && (playlist.getOwner() != null && playlist.getOwner().equals(UserUtil.getCurrentUsername(context)) || playlist.getId().indexOf(".m3u") != -1)) {
+ args.putBoolean(Constants.INTENT_EXTRA_NAME_PLAYLIST_OWNER, true);
+ }
+ fragment.setArguments(args);
+
+ replaceFragment(fragment);
+ }
+
+ private void deletePlaylist(final Playlist playlist) {
+ Util.confirmDialog(context, R.string.common_delete, playlist.getName(), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ new LoadingTask<Void>(context, false) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.deletePlaylist(playlist.getId(), context, null);
+ SyncUtil.removeSyncedPlaylist(context, playlist.getId());
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ adapter.remove(playlist);
+ adapter.notifyDataSetChanged();
+ Util.toast(context, context.getResources().getString(R.string.menu_deleted_playlist, playlist.getName()));
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.menu_deleted_playlist_error, playlist.getName()) + " " + getErrorMessage(error);
+ }
+
+ Util.toast(context, msg, false);
+ }
+ }.execute();
+ }
+ });
+ }
+
+ private void displayPlaylistInfo(final Playlist playlist) {
+ String message = "Owner: " + playlist.getOwner() + "\nComments: " +
+ ((playlist.getComment() == null) ? "" : playlist.getComment()) +
+ "\nSong Count: " + playlist.getSongCount() +
+ ((playlist.getPublic() == null) ? "" : ("\nPublic: " + playlist.getPublic())) +
+ "\nCreation Date: " + playlist.getCreated().replace('T', ' ');
+ Util.info(context, playlist.getName(), message);
+ }
+
+ private void updatePlaylistInfo(final Playlist playlist) {
+ View dialogView = context.getLayoutInflater().inflate(R.layout.update_playlist, null);
+ final EditText nameBox = (EditText)dialogView.findViewById(R.id.get_playlist_name);
+ final EditText commentBox = (EditText)dialogView.findViewById(R.id.get_playlist_comment);
+ final CheckBox publicBox = (CheckBox)dialogView.findViewById(R.id.get_playlist_public);
+
+ nameBox.setText(playlist.getName());
+ commentBox.setText(playlist.getComment());
+ Boolean pub = playlist.getPublic();
+ if(pub == null) {
+ publicBox.setEnabled(false);
+ } else {
+ publicBox.setChecked(pub);
+ }
+
+ new AlertDialog.Builder(context)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setTitle(R.string.playlist_update_info)
+ .setView(dialogView)
+ .setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ new LoadingTask<Void>(context, false) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ String name = nameBox.getText().toString();
+ String comment = commentBox.getText().toString();
+ boolean isPublic = publicBox.isChecked();
+
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.updatePlaylist(playlist.getId(), name, comment, isPublic, context, null);
+
+ playlist.setName(name);
+ playlist.setComment(comment);
+ playlist.setPublic(isPublic);
+
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ Util.toast(context, context.getResources().getString(R.string.playlist_updated_info, playlist.getName()));
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.playlist_updated_info_error, playlist.getName()) + " " + getErrorMessage(error);
+ }
+
+ Util.toast(context, msg, false);
+ }
+ }.execute();
+ }
+
+ })
+ .setNegativeButton(R.string.common_cancel, null)
+ .show();
+ }
+
+ private void syncPlaylist(Playlist playlist) {
+ SyncUtil.addSyncedPlaylist(context, playlist.getId());
+ downloadPlaylist(playlist.getId(), playlist.getName(), true, true, false, false, true);
+ }
+
+ private void stopSyncPlaylist(final Playlist playlist) {
+ SyncUtil.removeSyncedPlaylist(context, playlist.getId());
+
+ new LoadingTask<Void>(context, false) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ // Unpin all of the songs in playlist
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ MusicDirectory root = musicService.getPlaylist(true, playlist.getId(), playlist.getName(), context, this);
+ for(MusicDirectory.Entry entry: root.getChildren()) {
+ DownloadFile file = new DownloadFile(context, entry, false);
+ file.unpin();
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+
+ }
+ }.execute();
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SelectPodcastsFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SelectPodcastsFragment.java
new file mode 100644
index 00000000..3a564f1c
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/fragments/SelectPodcastsFragment.java
@@ -0,0 +1,308 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.fragments;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.view.ContextMenu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.TextView;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.PodcastChannel;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import github.daneren2005.dsub.service.OfflineException;
+import github.daneren2005.dsub.service.ServerTooOldException;
+import github.daneren2005.dsub.util.ProgressListener;
+import github.daneren2005.dsub.util.SyncUtil;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.LoadingTask;
+import github.daneren2005.dsub.util.SilentBackgroundTask;
+import github.daneren2005.dsub.util.UserUtil;
+import github.daneren2005.dsub.util.Util;
+import github.daneren2005.dsub.adapter.PodcastChannelAdapter;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ *
+ * @author Scott
+ */
+public class SelectPodcastsFragment extends SelectListFragment<PodcastChannel> {
+ private static final String TAG = SelectPodcastsFragment.class.getSimpleName();
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if(super.onOptionsItemSelected(item)) {
+ return true;
+ }
+
+ switch (item.getItemId()) {
+ case R.id.menu_check:
+ refreshPodcasts();
+ break;
+ case R.id.menu_add_podcast:
+ addNewPodcast();
+ break;
+ }
+
+ return false;
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, view, menuInfo);
+
+ android.view.MenuInflater inflater = context.getMenuInflater();
+ if(!Util.isOffline(context) && UserUtil.canPodcast()) {
+ inflater.inflate(R.menu.select_podcasts_context, menu);
+
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
+ PodcastChannel podcast = (PodcastChannel) listView.getItemAtPosition(info.position);
+ if(SyncUtil.isSyncedPodcast(context, podcast.getId())) {
+ menu.removeItem(R.id.podcast_menu_sync);
+ } else {
+ menu.removeItem(R.id.podcast_menu_stop_sync);
+ }
+ } else {
+ inflater.inflate(R.menu.select_podcasts_context_offline, menu);
+ }
+
+ recreateContextMenu(menu);
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem menuItem) {
+ if(menuItem.getGroupId() != getSupportTag()) {
+ return false;
+ }
+
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
+ PodcastChannel channel = (PodcastChannel) listView.getItemAtPosition(info.position);
+
+ switch (menuItem.getItemId()) {
+ case R.id.podcast_menu_sync:
+ syncPodcast(channel);
+ break;
+ case R.id.podcast_menu_stop_sync:
+ stopSyncPodcast(channel);
+ break;
+ case R.id.podcast_channel_info:
+ displayPodcastInfo(channel);
+ break;
+ case R.id.podcast_channel_delete:
+ deletePodcast(channel);
+ break;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int getOptionsMenu() {
+ return (UserUtil.canPodcast() && !Util.isOffline(context)) ? R.menu.select_podcasts : R.menu.abstract_top_menu;
+ }
+
+ @Override
+ public ArrayAdapter getAdapter(List<PodcastChannel> channels) {
+ return new PodcastChannelAdapter(context, channels);
+ }
+
+ @Override
+ public List<PodcastChannel> getObjects(MusicService musicService, boolean refresh, ProgressListener listener) throws Exception {
+ return musicService.getPodcastChannels(refresh, context, listener);
+ }
+
+ @Override
+ public int getTitleResource() {
+ return R.string.button_bar_podcasts;
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ PodcastChannel channel = (PodcastChannel) parent.getItemAtPosition(position);
+
+ if("error".equals(channel.getStatus())) {
+ Util.toast(context, context.getResources().getString(R.string.select_podcasts_invalid_podcast_channel, channel.getErrorMessage() == null ? "error" : channel.getErrorMessage()));
+ } else if("downloading".equals(channel.getStatus())) {
+ Util.toast(context, R.string.select_podcasts_initializing);
+ } else {
+ SubsonicFragment fragment = new SelectDirectoryFragment();
+ Bundle args = new Bundle();
+ args.putString(Constants.INTENT_EXTRA_NAME_PODCAST_ID, channel.getId());
+ args.putString(Constants.INTENT_EXTRA_NAME_PODCAST_NAME, channel.getName());
+ args.putString(Constants.INTENT_EXTRA_NAME_PODCAST_DESCRIPTION, channel.getDescription());
+ fragment.setArguments(args);
+
+ replaceFragment(fragment);
+ }
+ }
+
+ public void refreshPodcasts() {
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.refreshPodcasts(context, null);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ Util.toast(context, R.string.select_podcasts_refreshing);
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ Util.toast(context, getErrorMessage(error), false);
+ }
+ }.execute();
+ }
+
+ private void addNewPodcast() {
+ View dialogView = context.getLayoutInflater().inflate(R.layout.create_podcast, null);
+ final TextView urlBox = (TextView) dialogView.findViewById(R.id.create_podcast_url);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.menu_add_podcast)
+ .setView(dialogView)
+ .setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ addNewPodcast(urlBox.getText().toString());
+ }
+ })
+ .setNegativeButton(R.string.common_cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.cancel();
+ }
+ })
+ .setCancelable(true);
+
+ AlertDialog dialog = builder.create();
+ dialog.show();
+ }
+ private void addNewPodcast(final String url) {
+ new LoadingTask<Void>(context, false) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.createPodcastChannel(url, context, null);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ refresh();
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.select_podcasts_created_error) + " " + getErrorMessage(error);
+ }
+
+ Util.toast(context, msg, false);
+ }
+ }.execute();
+ }
+
+ private void displayPodcastInfo(final PodcastChannel channel) {
+ String message = ((channel.getName()) == null ? "" : "Title: " + channel.getName()) +
+ "\nURL: " + channel.getUrl() +
+ "\nStatus: " + channel.getStatus() +
+ ((channel.getErrorMessage()) == null ? "" : "\nError Message: " + channel.getErrorMessage()) +
+ ((channel.getDescription()) == null ? "" : "\n\nDescription: " + channel.getDescription());
+
+ Util.info(context, channel.getName(), message);
+ }
+
+ private void deletePodcast(final PodcastChannel channel) {
+ Util.confirmDialog(context, R.string.common_delete, channel.getName(), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ new LoadingTask<Void>(context, false) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.deletePodcastChannel(channel.getId(), context, null);
+ stopSyncPodcast(channel);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ adapter.remove(channel);
+ adapter.notifyDataSetChanged();
+ Util.toast(context, context.getResources().getString(R.string.select_podcasts_deleted, channel.getName()));
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.select_podcasts_deleted_error, channel.getName()) + " " + getErrorMessage(error);
+ }
+
+ Util.toast(context, msg, false);
+ }
+ }.execute();
+ }
+ });
+ }
+
+ private void syncPodcast(final PodcastChannel podcast) {
+ new LoadingTask<MusicDirectory>(context, false) {
+ @Override
+ protected MusicDirectory doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ return musicService.getPodcastEpisodes(true, podcast.getId(), context, this);
+ }
+
+ @Override
+ protected void done(MusicDirectory result) {
+ List<String> existingEpisodes = new ArrayList<String>();
+ for(MusicDirectory.Entry entry: result.getChildren()) {
+ String id = entry.getId();
+ if(id != null) {
+ existingEpisodes.add(entry.getId());
+ }
+ }
+
+ SyncUtil.addSyncedPodcast(context, podcast.getId(), existingEpisodes);
+ }
+ }.execute();
+ }
+
+ private void stopSyncPodcast(PodcastChannel podcast) {
+ SyncUtil.removeSyncedPodcast(context, podcast.getId());
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SelectShareFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SelectShareFragment.java
new file mode 100644
index 00000000..07cd3bef
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/fragments/SelectShareFragment.java
@@ -0,0 +1,216 @@
+package github.daneren2005.dsub.fragments;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.view.ContextMenu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.DatePicker;
+import android.widget.EditText;
+
+import java.util.Date;
+import java.util.List;
+
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.Share;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import github.daneren2005.dsub.service.OfflineException;
+import github.daneren2005.dsub.service.ServerTooOldException;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.LoadingTask;
+import github.daneren2005.dsub.util.ProgressListener;
+import github.daneren2005.dsub.util.Util;
+import github.daneren2005.dsub.adapter.ShareAdapter;
+
+/**
+ * Created by Scott on 12/28/13.
+ */
+public class SelectShareFragment extends SelectListFragment<Share> {
+ private static final String TAG = SelectShareFragment.class.getSimpleName();
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, view, menuInfo);
+ android.view.MenuInflater inflater = context.getMenuInflater();
+ inflater.inflate(R.menu.select_share_context, menu);
+ recreateContextMenu(menu);
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem menuItem) {
+ if(menuItem.getGroupId() != getSupportTag()) {
+ return false;
+ }
+
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
+ Share share = (Share) listView.getItemAtPosition(info.position);
+
+ switch (menuItem.getItemId()) {
+ case R.id.share_menu_share:
+ shareExternal(share);
+ break;
+ case R.id.share_menu_info:
+ displayShareInfo(share);
+ break;
+ case R.id.share_menu_delete:
+ deleteShare(share);
+ break;
+ case R.id.share_update_info:
+ updateShareInfo(share);
+ break;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int getOptionsMenu() {
+ return R.menu.abstract_top_menu;
+ }
+
+ @Override
+ public ArrayAdapter getAdapter(List<Share> objs) {
+ return new ShareAdapter(context, objs);
+ }
+
+ @Override
+ public List<Share> getObjects(MusicService musicService, boolean refresh, ProgressListener listener) throws Exception {
+ return musicService.getShares(context, listener);
+ }
+
+ @Override
+ public int getTitleResource() {
+ return R.string.button_bar_shares;
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ Share share = (Share) parent.getItemAtPosition(position);
+
+ SubsonicFragment fragment = new SelectDirectoryFragment();
+ Bundle args = new Bundle();
+ args.putSerializable(Constants.INTENT_EXTRA_NAME_SHARE, share);
+ fragment.setArguments(args);
+
+ replaceFragment(fragment);
+ }
+
+ private void displayShareInfo(final Share share) {
+ String message = context.getResources().getString(R.string.share_info,
+ share.getUsername(), (share.getDescription() != null) ? share.getDescription() : "", share.getUrl(),
+ Util.formatDate(share.getCreated()), Util.formatDate(share.getLastVisited()), Util.formatDate(share.getExpires()), share.getVisitCount());
+ Util.info(context, share.getName(), message);
+ }
+
+ private void updateShareInfo(final Share share) {
+ View dialogView = context.getLayoutInflater().inflate(R.layout.update_share, null);
+ final EditText nameBox = (EditText)dialogView.findViewById(R.id.get_share_name);
+ final DatePicker expireBox = (DatePicker)dialogView.findViewById(R.id.get_share_expire);
+ final CheckBox noExpiresBox = (CheckBox)dialogView.findViewById(R.id.get_share_no_expire);
+
+ nameBox.setText(share.getDescription());
+ Date expires = share.getExpires();
+ if(expires != null) {
+ expireBox.updateDate(expires.getYear() + 1900, expires.getMonth(), expires.getDate());
+ }
+
+ boolean noExpires = share.getExpires() == null;
+ if(noExpires) {
+ expireBox.setEnabled(false);
+ noExpiresBox.setChecked(true);
+ }
+
+ noExpiresBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ expireBox.setEnabled(!isChecked);
+ }
+ });
+
+ new AlertDialog.Builder(context)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setTitle(R.string.playlist_update_info)
+ .setView(dialogView)
+ .setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ new LoadingTask<Void>(context, false) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ Long expiresIn = 0L;
+ if (!noExpiresBox.isChecked()) {
+ Date expires = new Date(expireBox.getYear() - 1900, expireBox.getMonth(), expireBox.getDayOfMonth());
+ expiresIn = expires.getTime();
+ }
+
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.updateShare(share.getId(), nameBox.getText().toString(), expiresIn, context, null);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ refresh();
+ Util.toast(context, context.getResources().getString(R.string.share_updated_info, share.getName()));
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.share_updated_info_error, share.getName()) + " " + getErrorMessage(error);
+ }
+
+ Util.toast(context, msg, false);
+ }
+ }.execute();
+ }
+
+ })
+ .setNegativeButton(R.string.common_cancel, null)
+ .show();
+ }
+
+ private void deleteShare(final Share share) {
+ Util.confirmDialog(context, R.string.common_delete, share.getName(), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ new LoadingTask<Void>(context, false) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.deleteShare(share.getId(), context, null);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ adapter.remove(share);
+ adapter.notifyDataSetChanged();
+ Util.toast(context, context.getResources().getString(R.string.share_deleted, share.getName()));
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.share_deleted_error, share.getName()) + " " + getErrorMessage(error);
+ }
+
+ Util.toast(context, msg, false);
+ }
+ }.execute();
+ }
+ });
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SelectVideoFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SelectVideoFragment.java
new file mode 100644
index 00000000..b4d34ff9
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/fragments/SelectVideoFragment.java
@@ -0,0 +1,82 @@
+/*
+ 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.fragments;
+
+import android.view.ContextMenu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+
+import java.util.List;
+
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.util.ProgressListener;
+import github.daneren2005.dsub.adapter.EntryAdapter;
+
+public class SelectVideoFragment extends SelectListFragment<MusicDirectory.Entry> {
+ @Override
+ public int getOptionsMenu() {
+ return R.menu.empty;
+ }
+
+ @Override
+ public ArrayAdapter getAdapter(List<MusicDirectory.Entry> objs) {
+ return new EntryAdapter(context, null, objs, false);
+ }
+
+ @Override
+ public List<MusicDirectory.Entry> getObjects(MusicService musicService, boolean refresh, ProgressListener listener) throws Exception {
+ MusicDirectory dir = musicService.getVideos(refresh, context, listener);
+ return dir.getChildren();
+ }
+
+ @Override
+ public int getTitleResource() {
+ return R.string.main_videos;
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ MusicDirectory.Entry entry = (MusicDirectory.Entry) parent.getItemAtPosition(position);
+ playVideo(entry);
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, view, menuInfo);
+
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
+ Object entry = listView.getItemAtPosition(info.position);
+
+ onCreateContextMenu(menu, view, menuInfo, entry);
+ recreateContextMenu(menu);
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem menuItem) {
+ if (menuItem.getGroupId() != getSupportTag()) {
+ return false;
+ }
+
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
+ Object entry = listView.getItemAtPosition(info.position);
+
+ return onContextItemSelected(menuItem, entry);
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SelectYearFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SelectYearFragment.java
new file mode 100644
index 00000000..dc19acad
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/fragments/SelectYearFragment.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 2010 (C) Sindre Mehus
+*/
+package github.daneren2005.dsub.fragments;
+
+import android.os.Bundle;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.ProgressListener;
+
+/**
+ * Created by Scott on 12/23/13.
+ */
+public class SelectYearFragment extends SelectListFragment<Integer> {
+
+ @Override
+ public int getOptionsMenu() {
+ return R.menu.empty;
+ }
+
+ @Override
+ public ArrayAdapter getAdapter(List<Integer> objs) {
+ return new ArrayAdapter<Integer>(context, android.R.layout.simple_list_item_1, objs);
+ }
+
+ @Override
+ public List<Integer> getObjects(MusicService musicService, boolean refresh, ProgressListener listener) throws Exception {
+ List<Integer> decades = new ArrayList<Integer>();
+ for(int i = 2010; i >= 1920; i -= 10) {
+ decades.add(i);
+ }
+
+ return decades;
+ }
+
+ @Override
+ public int getTitleResource() {
+ return R.string.main_albums_year;
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ Integer decade = (Integer) parent.getItemAtPosition(position);
+
+ SubsonicFragment fragment = new SelectDirectoryFragment();
+ Bundle args = new Bundle();
+ args.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, "years");
+ args.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 20);
+ args.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0);
+ args.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_EXTRA, Integer.toString(decade));
+ fragment.setArguments(args);
+
+ replaceFragment(fragment);
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SettingsFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SettingsFragment.java
new file mode 100644
index 00000000..3be21a67
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/fragments/SettingsFragment.java
@@ -0,0 +1,724 @@
+/*
+ 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.fragments;
+
+import android.accounts.Account;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.Bundle;
+import android.preference.CheckBoxPreference;
+import android.preference.EditTextPreference;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.PreferenceCategory;
+import android.preference.PreferenceScreen;
+import android.text.InputType;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.EditText;
+
+import java.io.File;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.text.DecimalFormat;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.service.HeadphoneListenerService;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.LoadingTask;
+import github.daneren2005.dsub.util.SyncUtil;
+import github.daneren2005.dsub.util.Util;
+import github.daneren2005.dsub.view.ErrorDialog;
+
+public class SettingsFragment extends PreferenceCompatFragment implements SharedPreferences.OnSharedPreferenceChangeListener {
+ private final static String TAG = SettingsFragment.class.getSimpleName();
+ private final Map<String, ServerSettings> serverSettings = new LinkedHashMap<String, ServerSettings>();
+ private boolean testingConnection;
+ private ListPreference theme;
+ private ListPreference maxBitrateWifi;
+ private ListPreference maxBitrateMobile;
+ private ListPreference maxVideoBitrateWifi;
+ private ListPreference maxVideoBitrateMobile;
+ private ListPreference networkTimeout;
+ private EditTextPreference cacheLocation;
+ private ListPreference preloadCountWifi;
+ private ListPreference preloadCountMobile;
+ private ListPreference tempLoss;
+ private ListPreference pauseDisconnect;
+ private Preference addServerPreference;
+ private PreferenceCategory serversCategory;
+ private ListPreference videoPlayer;
+ private ListPreference syncInterval;
+ private CheckBoxPreference syncEnabled;
+ private CheckBoxPreference syncWifi;
+ private CheckBoxPreference syncNotification;
+ private CheckBoxPreference syncStarred;
+ private CheckBoxPreference syncMostRecent;
+ private CheckBoxPreference replayGain;
+ private ListPreference replayGainType;
+ private Preference replayGainBump;
+ private Preference replayGainUntagged;
+ private String internalSSID;
+ private String internalSSIDDisplay;
+ private EditTextPreference cacheSize;
+ private ListPreference openToTab;
+
+ private int serverCount = 3;
+ private SharedPreferences settings;
+ private DecimalFormat megabyteFromat;
+
+ @Override
+ public void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
+ View root = super.onCreateView(inflater, container, bundle);
+
+ this.setTitle(getResources().getString(R.string.settings_title));
+ initSettings();
+
+ return root;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+
+ SharedPreferences prefs = Util.getPreferences(context);
+ prefs.unregisterOnSharedPreferenceChangeListener(this);
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+ // Random error I have no idea how to reproduce
+ if(sharedPreferences == null) {
+ return;
+ }
+
+ update();
+
+ if (Constants.PREFERENCES_KEY_HIDE_MEDIA.equals(key)) {
+ setHideMedia(sharedPreferences.getBoolean(key, false));
+ }
+ else if (Constants.PREFERENCES_KEY_MEDIA_BUTTONS.equals(key)) {
+ setMediaButtonsEnabled(sharedPreferences.getBoolean(key, true));
+ }
+ else if (Constants.PREFERENCES_KEY_CACHE_LOCATION.equals(key)) {
+ setCacheLocation(sharedPreferences.getString(key, ""));
+ }
+ else if (Constants.PREFERENCES_KEY_SLEEP_TIMER_DURATION.equals(key)){
+ DownloadService downloadService = DownloadService.getInstance();
+ downloadService.setSleepTimerDuration(Integer.parseInt(sharedPreferences.getString(key, "60")));
+ }
+ else if(Constants.PREFERENCES_KEY_SYNC_MOST_RECENT.equals(key)) {
+ SyncUtil.removeMostRecentSyncFiles(context);
+ } else if(Constants.PREFERENCES_KEY_REPLAY_GAIN.equals(key) || Constants.PREFERENCES_KEY_REPLAY_GAIN_BUMP.equals(key) || Constants.PREFERENCES_KEY_REPLAY_GAIN_UNTAGGED.equals(key)) {
+ DownloadService downloadService = DownloadService.getInstance();
+ if(downloadService != null) {
+ downloadService.reapplyVolume();
+ }
+ } else if(Constants.PREFERENCES_KEY_START_ON_HEADPHONES.equals(key)) {
+ Intent serviceIntent = new Intent();
+ serviceIntent.setClassName(context.getPackageName(), HeadphoneListenerService.class.getName());
+
+ if(sharedPreferences.getBoolean(key, false)) {
+ context.startService(serviceIntent);
+ } else {
+ context.stopService(serviceIntent);
+ }
+ }
+
+ scheduleBackup();
+ }
+
+ private void initSettings() {
+ internalSSID = Util.getSSID(context);
+ if(internalSSID == null) {
+ internalSSID = "";
+ }
+ internalSSIDDisplay = context.getResources().getString(R.string.settings_server_local_network_ssid_hint, internalSSID);
+
+ theme = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_THEME);
+ maxBitrateWifi = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_MAX_BITRATE_WIFI);
+ maxBitrateMobile = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_MAX_BITRATE_MOBILE);
+ maxVideoBitrateWifi = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_MAX_VIDEO_BITRATE_WIFI);
+ maxVideoBitrateMobile = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_MAX_VIDEO_BITRATE_MOBILE);
+ networkTimeout = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_NETWORK_TIMEOUT);
+ cacheLocation = (EditTextPreference) this.findPreference(Constants.PREFERENCES_KEY_CACHE_LOCATION);
+ preloadCountWifi = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_PRELOAD_COUNT_WIFI);
+ preloadCountMobile = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_PRELOAD_COUNT_MOBILE);
+ tempLoss = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_TEMP_LOSS);
+ pauseDisconnect = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_PAUSE_DISCONNECT);
+ serversCategory = (PreferenceCategory) this.findPreference(Constants.PREFERENCES_KEY_SERVER_KEY);
+ addServerPreference = this.findPreference(Constants.PREFERENCES_KEY_SERVER_ADD);
+ videoPlayer = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_VIDEO_PLAYER);
+ syncInterval = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_SYNC_INTERVAL);
+ syncEnabled = (CheckBoxPreference) this.findPreference(Constants.PREFERENCES_KEY_SYNC_ENABLED);
+ syncWifi = (CheckBoxPreference) this.findPreference(Constants.PREFERENCES_KEY_SYNC_WIFI);
+ syncNotification = (CheckBoxPreference) this.findPreference(Constants.PREFERENCES_KEY_SYNC_NOTIFICATION);
+ syncStarred = (CheckBoxPreference) this.findPreference(Constants.PREFERENCES_KEY_SYNC_STARRED);
+ syncMostRecent = (CheckBoxPreference) this.findPreference(Constants.PREFERENCES_KEY_SYNC_MOST_RECENT);
+ replayGain = (CheckBoxPreference) this.findPreference(Constants.PREFERENCES_KEY_REPLAY_GAIN);
+ replayGainType = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_REPLAY_GAIN_TYPE);
+ replayGainBump = this.findPreference(Constants.PREFERENCES_KEY_REPLAY_GAIN_BUMP);
+ replayGainUntagged = this.findPreference(Constants.PREFERENCES_KEY_REPLAY_GAIN_UNTAGGED);
+ cacheSize = (EditTextPreference) this.findPreference(Constants.PREFERENCES_KEY_CACHE_SIZE);
+ openToTab = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_OPEN_TO_TAB);
+
+ settings = Util.getPreferences(context);
+ serverCount = settings.getInt(Constants.PREFERENCES_KEY_SERVER_COUNT, 1);
+
+ this.findPreference("clearCache").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ Util.confirmDialog(context, R.string.common_delete, R.string.common_confirm_message_cache, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ new LoadingTask<Void>(context, false) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ FileUtil.deleteMusicDirectory(context);
+ FileUtil.deleteSerializedCache(context);
+ FileUtil.deleteArtworkCache(context);
+ FileUtil.deleteAvatarCache(context);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ Util.toast(context, R.string.settings_cache_clear_complete);
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ Util.toast(context, getErrorMessage(error), false);
+ }
+ }.execute();
+ }
+ });
+ return false;
+ }
+ });
+
+ addServerPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ serverCount++;
+ String instance = String.valueOf(serverCount);
+ serversCategory.addPreference(addServer(serverCount));
+
+ SharedPreferences.Editor editor = settings.edit();
+ editor.putInt(Constants.PREFERENCES_KEY_SERVER_COUNT, serverCount);
+ // Reset set folder ID
+ editor.putString(Constants.PREFERENCES_KEY_MUSIC_FOLDER_ID + instance, null);
+ editor.commit();
+
+ serverSettings.put(instance, new ServerSettings(instance));
+
+ return true;
+ }
+ });
+
+ this.findPreference(Constants.PREFERENCES_KEY_SYNC_ENABLED).setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ Boolean syncEnabled = (Boolean) newValue;
+
+ Account account = new Account(Constants.SYNC_ACCOUNT_NAME, Constants.SYNC_ACCOUNT_TYPE);
+ ContentResolver.setSyncAutomatically(account, Constants.SYNC_ACCOUNT_PLAYLIST_AUTHORITY, syncEnabled);
+ ContentResolver.setSyncAutomatically(account, Constants.SYNC_ACCOUNT_PODCAST_AUTHORITY, syncEnabled);
+
+ return true;
+ }
+ });
+ syncInterval.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ Integer syncInterval = Integer.parseInt(((String) newValue));
+
+ Account account = new Account(Constants.SYNC_ACCOUNT_NAME, Constants.SYNC_ACCOUNT_TYPE);
+ ContentResolver.addPeriodicSync(account, Constants.SYNC_ACCOUNT_PLAYLIST_AUTHORITY, new Bundle(), 60L * syncInterval);
+ ContentResolver.addPeriodicSync(account, Constants.SYNC_ACCOUNT_PODCAST_AUTHORITY, new Bundle(), 60L * syncInterval);
+
+ return true;
+ }
+ });
+
+ serversCategory.setOrderingAsAdded(false);
+ for (int i = 1; i <= serverCount; i++) {
+ String instance = String.valueOf(i);
+ serversCategory.addPreference(addServer(i));
+ serverSettings.put(instance, new ServerSettings(instance));
+ }
+
+ SharedPreferences prefs = Util.getPreferences(context);
+ prefs.registerOnSharedPreferenceChangeListener(this);
+
+ update();
+ }
+
+ private void scheduleBackup() {
+ try {
+ Class managerClass = Class.forName("android.app.backup.BackupManager");
+ Constructor managerConstructor = managerClass.getConstructor(Context.class);
+ Object manager = managerConstructor.newInstance(context);
+ Method m = managerClass.getMethod("dataChanged");
+ m.invoke(manager);
+ } catch(ClassNotFoundException e) {
+ Log.e(TAG, "No backup manager found");
+ } catch(Throwable t) {
+ Log.e(TAG, "Scheduling backup failed " + t);
+ t.printStackTrace();
+ }
+ }
+
+ private void update() {
+ if (testingConnection) {
+ return;
+ }
+
+ theme.setSummary(theme.getEntry());
+ maxBitrateWifi.setSummary(maxBitrateWifi.getEntry());
+ maxBitrateMobile.setSummary(maxBitrateMobile.getEntry());
+ maxVideoBitrateWifi.setSummary(maxVideoBitrateWifi.getEntry());
+ maxVideoBitrateMobile.setSummary(maxVideoBitrateMobile.getEntry());
+ networkTimeout.setSummary(networkTimeout.getEntry());
+ cacheLocation.setSummary(cacheLocation.getText());
+ preloadCountWifi.setSummary(preloadCountWifi.getEntry());
+ preloadCountMobile.setSummary(preloadCountMobile.getEntry());
+ tempLoss.setSummary(tempLoss.getEntry());
+ pauseDisconnect.setSummary(pauseDisconnect.getEntry());
+ videoPlayer.setSummary(videoPlayer.getEntry());
+ syncInterval.setSummary(syncInterval.getEntry());
+ openToTab.setSummary(openToTab.getEntry());
+ try {
+ if(megabyteFromat == null) {
+ megabyteFromat = new DecimalFormat(getResources().getString(R.string.util_bytes_format_megabyte));
+ }
+
+ cacheSize.setSummary(megabyteFromat.format((double) Integer.parseInt(cacheSize.getText())).replace(".00", ""));
+ } catch(Exception e) {
+ Log.e(TAG, "Failed to format cache size", e);
+ cacheSize.setSummary(cacheSize.getText());
+ }
+ if(syncEnabled.isChecked()) {
+ if(!syncInterval.isEnabled()) {
+ syncInterval.setEnabled(true);
+ syncWifi.setEnabled(true);
+ syncNotification.setEnabled(true);
+ syncStarred.setEnabled(true);
+ syncMostRecent.setEnabled(true);
+ }
+ } else {
+ if(syncInterval.isEnabled()) {
+ syncInterval.setEnabled(false);
+ syncWifi.setEnabled(false);
+ syncNotification.setEnabled(false);
+ syncStarred.setEnabled(false);
+ syncMostRecent.setEnabled(false);
+ }
+ }
+ if(replayGain.isChecked()) {
+ replayGainType.setEnabled(true);
+ replayGainBump.setEnabled(true);
+ replayGainUntagged.setEnabled(true);
+ } else {
+ replayGainType.setEnabled(false);
+ replayGainBump.setEnabled(false);
+ replayGainUntagged.setEnabled(false);
+ }
+ replayGainType.setSummary(replayGainType.getEntry());
+
+ for (ServerSettings ss : serverSettings.values()) {
+ ss.update();
+ }
+ }
+
+ private PreferenceScreen addServer(final int instance) {
+ final PreferenceScreen screen = this.getPreferenceManager().createPreferenceScreen(context);
+ screen.setTitle(R.string.settings_server_unused);
+ screen.setKey(Constants.PREFERENCES_KEY_SERVER_KEY + instance);
+
+ final EditTextPreference serverNamePreference = new EditTextPreference(context);
+ serverNamePreference.setKey(Constants.PREFERENCES_KEY_SERVER_NAME + instance);
+ serverNamePreference.setDefaultValue(getResources().getString(R.string.settings_server_unused));
+ serverNamePreference.setTitle(R.string.settings_server_name);
+ serverNamePreference.setDialogTitle(R.string.settings_server_name);
+
+ if (serverNamePreference.getText() == null) {
+ serverNamePreference.setText(getResources().getString(R.string.settings_server_unused));
+ }
+
+ serverNamePreference.setSummary(serverNamePreference.getText());
+
+ final EditTextPreference serverUrlPreference = new EditTextPreference(context);
+ serverUrlPreference.setKey(Constants.PREFERENCES_KEY_SERVER_URL + instance);
+ serverUrlPreference.getEditText().setInputType(InputType.TYPE_TEXT_VARIATION_URI);
+ serverUrlPreference.setDefaultValue("http://yourhost");
+ serverUrlPreference.setTitle(R.string.settings_server_address);
+ serverUrlPreference.setDialogTitle(R.string.settings_server_address);
+
+ if (serverUrlPreference.getText() == null) {
+ serverUrlPreference.setText("http://yourhost");
+ }
+
+ serverUrlPreference.setSummary(serverUrlPreference.getText());
+ screen.setSummary(serverUrlPreference.getText());
+
+ final EditTextPreference serverLocalNetworkSSIDPreference = new EditTextPreference(context) {
+ @Override
+ protected void onAddEditTextToDialogView(View dialogView, final EditText editText) {
+ super.onAddEditTextToDialogView(dialogView, editText);
+ ViewGroup root = (ViewGroup) ((ViewGroup) dialogView).getChildAt(0);
+
+ Button defaultButton = new Button(getContext());
+ defaultButton.setText(internalSSIDDisplay);
+ defaultButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ editText.setText(internalSSID);
+ }
+ });
+ root.addView(defaultButton);
+ }
+ };
+ serverLocalNetworkSSIDPreference.setKey(Constants.PREFERENCES_KEY_SERVER_LOCAL_NETWORK_SSID + instance);
+ serverLocalNetworkSSIDPreference.setTitle(R.string.settings_server_local_network_ssid);
+ serverLocalNetworkSSIDPreference.setDialogTitle(R.string.settings_server_local_network_ssid);
+
+ final EditTextPreference serverInternalUrlPreference = new EditTextPreference(context);
+ serverInternalUrlPreference.setKey(Constants.PREFERENCES_KEY_SERVER_INTERNAL_URL + instance);
+ serverInternalUrlPreference.getEditText().setInputType(InputType.TYPE_TEXT_VARIATION_URI);
+ serverInternalUrlPreference.setDefaultValue("");
+ serverInternalUrlPreference.setTitle(R.string.settings_server_internal_address);
+ serverInternalUrlPreference.setDialogTitle(R.string.settings_server_internal_address);
+ serverInternalUrlPreference.setSummary(serverInternalUrlPreference.getText());
+
+ final EditTextPreference serverUsernamePreference = new EditTextPreference(context);
+ serverUsernamePreference.setKey(Constants.PREFERENCES_KEY_USERNAME + instance);
+ serverUsernamePreference.setTitle(R.string.settings_server_username);
+ serverUsernamePreference.setDialogTitle(R.string.settings_server_username);
+
+ final EditTextPreference serverPasswordPreference = new EditTextPreference(context);
+ serverPasswordPreference.setKey(Constants.PREFERENCES_KEY_PASSWORD + instance);
+ serverPasswordPreference.getEditText().setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
+ serverPasswordPreference.setSummary("***");
+ serverPasswordPreference.setTitle(R.string.settings_server_password);
+
+ final CheckBoxPreference serverTagPreference = new CheckBoxPreference(context);
+ serverTagPreference.setKey(Constants.PREFERENCES_KEY_BROWSE_TAGS + instance);
+ serverTagPreference.setChecked(Util.isTagBrowsing(context, instance));
+ serverTagPreference.setSummary(R.string.settings_browse_by_tags_summary);
+ serverTagPreference.setTitle(R.string.settings_browse_by_tags);
+ serverPasswordPreference.setDialogTitle(R.string.settings_server_password);
+
+ final CheckBoxPreference serverSyncPreference = new CheckBoxPreference(context);
+ serverSyncPreference.setKey(Constants.PREFERENCES_KEY_SERVER_SYNC + instance);
+ serverSyncPreference.setChecked(Util.isSyncEnabled(context, instance));
+ serverSyncPreference.setSummary(R.string.settings_server_sync_summary);
+ serverSyncPreference.setTitle(R.string.settings_server_sync);
+
+ final Preference serverOpenBrowser = new Preference(context);
+ serverOpenBrowser.setKey(Constants.PREFERENCES_KEY_OPEN_BROWSER);
+ serverOpenBrowser.setPersistent(false);
+ serverOpenBrowser.setTitle(R.string.settings_server_open_browser);
+ serverOpenBrowser.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ openInBrowser(instance);
+ return true;
+ }
+ });
+
+ Preference serverRemoveServerPreference = new Preference(context);
+ serverRemoveServerPreference.setKey(Constants.PREFERENCES_KEY_SERVER_REMOVE + instance);
+ serverRemoveServerPreference.setPersistent(false);
+ serverRemoveServerPreference.setTitle(R.string.settings_servers_remove);
+
+ serverRemoveServerPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ Util.confirmDialog(context, R.string.common_delete, screen.getTitle().toString(), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ // Reset values to null so when we ask for them again they are new
+ serverNamePreference.setText(null);
+ serverUrlPreference.setText(null);
+ serverUsernamePreference.setText(null);
+ serverPasswordPreference.setText(null);
+
+ int activeServer = Util.getActiveServer(context);
+ for (int i = instance; i <= serverCount; i++) {
+ Util.removeInstanceName(context, i, activeServer);
+ }
+
+ serverCount--;
+ SharedPreferences.Editor editor = settings.edit();
+ editor.putInt(Constants.PREFERENCES_KEY_SERVER_COUNT, serverCount);
+ editor.commit();
+
+ serversCategory.removePreference(screen);
+ screen.getDialog().dismiss();
+ }
+ });
+
+ return true;
+ }
+ });
+
+ Preference serverTestConnectionPreference = new Preference(context);
+ serverTestConnectionPreference.setKey(Constants.PREFERENCES_KEY_TEST_CONNECTION + instance);
+ serverTestConnectionPreference.setPersistent(false);
+ serverTestConnectionPreference.setTitle(R.string.settings_test_connection_title);
+ serverTestConnectionPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ testConnection(instance);
+ return false;
+ }
+ });
+
+ screen.addPreference(serverNamePreference);
+ screen.addPreference(serverUrlPreference);
+ screen.addPreference(serverInternalUrlPreference);
+ screen.addPreference(serverLocalNetworkSSIDPreference);
+ screen.addPreference(serverUsernamePreference);
+ screen.addPreference(serverPasswordPreference);
+ screen.addPreference(serverTagPreference);
+ screen.addPreference(serverSyncPreference);
+ screen.addPreference(serverTestConnectionPreference);
+ screen.addPreference(serverOpenBrowser);
+ screen.addPreference(serverRemoveServerPreference);
+
+ screen.setOrder(instance);
+
+ return screen;
+ }
+
+ private void setHideMedia(boolean hide) {
+ File nomediaDir = new File(FileUtil.getSubsonicDirectory(context), ".nomedia");
+ File musicNoMedia = new File(FileUtil.getMusicDirectory(context), ".nomedia");
+ if (hide && !nomediaDir.exists()) {
+ try {
+ if (!nomediaDir.createNewFile()) {
+ Log.w(TAG, "Failed to create " + nomediaDir);
+ }
+ } catch(Exception e) {
+ Log.w(TAG, "Failed to create " + nomediaDir, e);
+ }
+
+ try {
+ if(!musicNoMedia.createNewFile()) {
+ Log.w(TAG, "Failed to create " + musicNoMedia);
+ }
+ } catch(Exception e) {
+ Log.w(TAG, "Failed to create " + musicNoMedia, e);
+ }
+ } else if (nomediaDir.exists()) {
+ if (!nomediaDir.delete()) {
+ Log.w(TAG, "Failed to delete " + nomediaDir);
+ }
+ if(!musicNoMedia.delete()) {
+ Log.w(TAG, "Failed to delete " + musicNoMedia);
+ }
+ }
+ Util.toast(context, R.string.settings_hide_media_toast, false);
+ }
+
+ private void setMediaButtonsEnabled(boolean enabled) {
+ if (enabled) {
+ Util.registerMediaButtonEventReceiver(context);
+ } else {
+ Util.unregisterMediaButtonEventReceiver(context);
+ }
+ }
+
+ private void setCacheLocation(String path) {
+ File dir = new File(path);
+ if (!FileUtil.verifyCanWrite(dir)) {
+ Util.toast(context, R.string.settings_cache_location_error, false);
+
+ // Reset it to the default.
+ String defaultPath = FileUtil.getDefaultMusicDirectory(context).getPath();
+ if (!defaultPath.equals(path)) {
+ SharedPreferences prefs = Util.getPreferences(context);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putString(Constants.PREFERENCES_KEY_CACHE_LOCATION, defaultPath);
+ editor.commit();
+ cacheLocation.setSummary(defaultPath);
+ cacheLocation.setText(defaultPath);
+ }
+
+ // Clear download queue.
+ DownloadService downloadService = DownloadService.getInstance();
+ downloadService.clear();
+ }
+ }
+
+ private void testConnection(final int instance) {
+ LoadingTask<Boolean> task = new LoadingTask<Boolean>(context) {
+ private int previousInstance;
+
+ @Override
+ protected Boolean doInBackground() throws Throwable {
+ updateProgress(R.string.settings_testing_connection);
+
+ previousInstance = Util.getActiveServer(context);
+ testingConnection = true;
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ try {
+ musicService.setInstance(instance);
+ musicService.ping(context, this);
+ return musicService.isLicenseValid(context, null);
+ } finally {
+ musicService.setInstance(null);
+ testingConnection = false;
+ }
+ }
+
+ @Override
+ protected void done(Boolean licenseValid) {
+ if (licenseValid) {
+ Util.toast(context, R.string.settings_testing_ok);
+ } else {
+ Util.toast(context, R.string.settings_testing_unlicensed);
+ }
+ }
+
+ @Override
+ public void cancel() {
+ super.cancel();
+ Util.setActiveServer(context, previousInstance);
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ Log.w(TAG, error.toString(), error);
+ new ErrorDialog(context, getResources().getString(R.string.settings_connection_failure) +
+ " " + getErrorMessage(error), false);
+ }
+ };
+ task.execute();
+ }
+
+ private void openInBrowser(final int instance) {
+ SharedPreferences prefs = Util.getPreferences(context);
+ String url = prefs.getString(Constants.PREFERENCES_KEY_SERVER_URL + instance, null);
+ if(url == null) {
+ new ErrorDialog(context, R.string.settings_invalid_url, false);
+ return;
+ }
+ Uri uriServer = Uri.parse(url);
+
+ Intent browserIntent = new Intent(Intent.ACTION_VIEW, uriServer);
+ startActivity(browserIntent);
+ }
+
+ private class ServerSettings {
+ private EditTextPreference serverName;
+ private EditTextPreference serverUrl;
+ private EditTextPreference serverLocalNetworkSSID;
+ private EditTextPreference serverInternalUrl;
+ private EditTextPreference username;
+ private PreferenceScreen screen;
+
+ private ServerSettings(String instance) {
+ screen = (PreferenceScreen) SettingsFragment.this.findPreference("server" + instance);
+ serverName = (EditTextPreference) SettingsFragment.this.findPreference(Constants.PREFERENCES_KEY_SERVER_NAME + instance);
+ serverUrl = (EditTextPreference) SettingsFragment.this.findPreference(Constants.PREFERENCES_KEY_SERVER_URL + instance);
+ serverLocalNetworkSSID = (EditTextPreference) SettingsFragment.this.findPreference(Constants.PREFERENCES_KEY_SERVER_LOCAL_NETWORK_SSID + instance);
+ serverInternalUrl = (EditTextPreference) SettingsFragment.this.findPreference(Constants.PREFERENCES_KEY_SERVER_INTERNAL_URL + instance);
+ username = (EditTextPreference) SettingsFragment.this.findPreference(Constants.PREFERENCES_KEY_USERNAME + instance);
+
+ serverUrl.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object value) {
+ try {
+ String url = (String) value;
+ new URL(url);
+ if (url.contains(" ") || url.contains("@") || url.contains("_")) {
+ throw new Exception();
+ }
+ } catch (Exception x) {
+ new ErrorDialog(context, R.string.settings_invalid_url, false);
+ return false;
+ }
+ return true;
+ }
+ });
+ serverInternalUrl.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object value) {
+ try {
+ String url = (String) value;
+ // Allow blank internal IP address
+ if("".equals(url) || url == null) {
+ return true;
+ }
+
+ new URL(url);
+ if (url.contains(" ") || url.contains("@") || url.contains("_")) {
+ throw new Exception();
+ }
+ } catch (Exception x) {
+ new ErrorDialog(context, R.string.settings_invalid_url, false);
+ return false;
+ }
+ return true;
+ }
+ });
+
+ username.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object value) {
+ String username = (String) value;
+ if (username == null || !username.equals(username.trim())) {
+ new ErrorDialog(context, R.string.settings_invalid_username, false);
+ return false;
+ }
+ return true;
+ }
+ });
+ }
+
+ public void update() {
+ serverName.setSummary(serverName.getText());
+ serverUrl.setSummary(serverUrl.getText());
+ serverLocalNetworkSSID.setSummary(serverLocalNetworkSSID.getText());
+ serverInternalUrl.setSummary(serverInternalUrl.getText());
+ username.setSummary(username.getText());
+ screen.setSummary(serverUrl.getText());
+ screen.setTitle(serverName.getText());
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SimilarArtistFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SimilarArtistFragment.java
new file mode 100644
index 00000000..79e759cc
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/fragments/SimilarArtistFragment.java
@@ -0,0 +1,169 @@
+/*
+ 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.fragments;
+
+import android.os.Bundle;
+import android.view.ContextMenu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.Artist;
+import github.daneren2005.dsub.domain.ArtistInfo;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.ProgressListener;
+import github.daneren2005.dsub.util.Util;
+import github.daneren2005.dsub.adapter.ArtistAdapter;
+
+import java.net.URLEncoder;
+import java.util.LinkedList;
+import java.util.List;
+
+public class SimilarArtistFragment extends SelectListFragment<Artist> {
+ private static final String TAG = SimilarArtistFragment.class.getSimpleName();
+ private ArtistInfo info;
+ private String artistId;
+
+ @Override
+ public void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ artist = true;
+
+ artistId = getArguments().getString(Constants.INTENT_EXTRA_NAME_ARTIST);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.menu_play_now:
+ playAll(false);
+ return true;
+ case R.id.menu_shuffle:
+ playAll(true);
+ return true;
+ case R.id.menu_show_missing:
+ showMissingArtists();
+ break;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, view, menuInfo);
+
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
+ Object entry = listView.getItemAtPosition(info.position);
+ onCreateContextMenu(menu, view, menuInfo, entry);
+
+ recreateContextMenu(menu);
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem menuItem) {
+ if(menuItem.getGroupId() != getSupportTag()) {
+ return false;
+ }
+
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
+ Artist artist = (Artist) listView.getItemAtPosition(info.position);
+ return onContextItemSelected(menuItem, artist);
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ Artist artist = (Artist) parent.getItemAtPosition(position);
+ SubsonicFragment fragment = new SelectDirectoryFragment();
+ Bundle args = new Bundle();
+ args.putString(Constants.INTENT_EXTRA_NAME_ID, artist.getId());
+ args.putString(Constants.INTENT_EXTRA_NAME_NAME, artist.getName());
+ args.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, true);
+ fragment.setArguments(args);
+
+ replaceFragment(fragment);
+ }
+
+ @Override
+ public int getOptionsMenu() {
+ return R.menu.similar_artists;
+ }
+
+ @Override
+ public ArrayAdapter getAdapter(List<Artist> objects) {
+ return new ArtistAdapter(context, objects);
+ }
+
+ @Override
+ public List<Artist> getObjects(MusicService musicService, boolean refresh, ProgressListener listener) throws Exception {
+ info = musicService.getArtistInfo(artistId, refresh, true, context, listener);
+ return info.getSimilarArtists();
+ }
+
+ @Override
+ public int getTitleResource() {
+ return R.string.menu_similar_artists;
+ }
+
+ private void showMissingArtists() {
+ StringBuilder b = new StringBuilder();
+
+ for(String name: info.getMissingArtists()) {
+ b.append("<h3><a href=\"https://www.google.com/#q=" + URLEncoder.encode(name) + "\">" + name + "</a></h3> ");
+ }
+
+ Util.showHTMLDialog(context, R.string.menu_similar_artists, b.toString());
+ }
+
+ private void playAll(final boolean shuffle) {
+ new RecursiveLoader(context) {
+ @Override
+ protected Boolean doInBackground() throws Throwable {
+ musicService = MusicServiceFactory.getMusicService(context);
+
+ MusicDirectory root = new MusicDirectory();
+ for(Artist artist: objects) {
+ if(Util.isTagBrowsing(context) && !Util.isOffline(context)) {
+ root.addChildren(musicService.getArtist(artist.getId(), artist.getName(), false, context, this).getChildren());
+ } else {
+ root.addChildren(musicService.getMusicDirectory(artist.getId(), artist.getName(), false, context, this).getChildren());
+ }
+ }
+
+ if(shuffle) {
+ root.shuffleChildren();
+ }
+
+ songs = new LinkedList<MusicDirectory.Entry>();
+ getSongsRecursively(root, songs);
+
+ DownloadService downloadService = getDownloadService();
+ if (!songs.isEmpty() && downloadService != null) {
+ downloadService.clear();
+ downloadService.download(songs, false, true, false, false);
+ }
+
+ return true;
+ }
+ }.execute();
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SubsonicFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SubsonicFragment.java
new file mode 100644
index 00000000..109983ba
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/fragments/SubsonicFragment.java
@@ -0,0 +1,1817 @@
+/*
+ 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.fragments;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.media.MediaMetadataRetriever;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.StatFs;
+import android.support.v4.app.Fragment;
+import android.support.v4.widget.SwipeRefreshLayout;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.GestureDetector;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.RatingBar;
+import android.widget.TextView;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.activity.DownloadActivity;
+import github.daneren2005.dsub.activity.SubsonicActivity;
+import github.daneren2005.dsub.activity.SubsonicFragmentActivity;
+import github.daneren2005.dsub.domain.Artist;
+import github.daneren2005.dsub.domain.Bookmark;
+import github.daneren2005.dsub.domain.Genre;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.Playlist;
+import github.daneren2005.dsub.domain.PodcastEpisode;
+import github.daneren2005.dsub.domain.ServerInfo;
+import github.daneren2005.dsub.domain.Share;
+import github.daneren2005.dsub.service.DownloadFile;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.service.MediaStoreService;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import github.daneren2005.dsub.service.OfflineException;
+import github.daneren2005.dsub.service.ServerTooOldException;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.ImageLoader;
+import github.daneren2005.dsub.util.ProgressListener;
+import github.daneren2005.dsub.util.SilentBackgroundTask;
+import github.daneren2005.dsub.util.LoadingTask;
+import github.daneren2005.dsub.util.UserUtil;
+import github.daneren2005.dsub.util.Util;
+import github.daneren2005.dsub.view.AlbumCell;
+import github.daneren2005.dsub.view.AlbumView;
+import github.daneren2005.dsub.view.ArtistEntryView;
+import github.daneren2005.dsub.view.ArtistView;
+import github.daneren2005.dsub.view.PlaylistSongView;
+import github.daneren2005.dsub.view.SongView;
+import github.daneren2005.dsub.view.UpdateView;
+
+import java.io.File;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Random;
+
+import static github.daneren2005.dsub.domain.MusicDirectory.Entry;
+
+public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener {
+ private static final String TAG = SubsonicFragment.class.getSimpleName();
+ private static int TAG_INC = 10;
+ private int tag;
+
+ protected SubsonicActivity context;
+ protected CharSequence title = null;
+ protected CharSequence subtitle = null;
+ protected View rootView;
+ protected boolean primaryFragment = false;
+ protected boolean secondaryFragment = false;
+ protected boolean invalidated = false;
+ protected static Random random = new Random();
+ protected GestureDetector gestureScanner;
+ protected Share share;
+ protected boolean artist = false;
+ protected boolean artistOverride = false;
+ protected SwipeRefreshLayout refreshLayout;
+ protected boolean firstRun;
+
+ public SubsonicFragment() {
+ super();
+ tag = TAG_INC++;
+ }
+
+ @Override
+ public void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+
+ if(bundle != null) {
+ String name = bundle.getString(Constants.FRAGMENT_NAME);
+ if(name != null) {
+ title = name;
+ }
+ }
+ firstRun = true;
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ if(title != null) {
+ outState.putString(Constants.FRAGMENT_NAME, title.toString());
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if(firstRun) {
+ firstRun = false;
+ } else {
+ UpdateView.triggerUpdate();
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ context = (SubsonicActivity)activity;
+ }
+
+ public void setContext(SubsonicActivity context) {
+ this.context = context;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.menu_shuffle:
+ onShuffleRequested();
+ return true;
+ case R.id.menu_search:
+ context.onSearchRequested();
+ return true;
+ case R.id.menu_exit:
+ exit();
+ return true;
+ case R.id.menu_refresh:
+ refresh();
+ return true;
+ }
+
+ return false;
+ }
+
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo, Object selected) {
+ MenuInflater inflater = context.getMenuInflater();
+
+ if(selected instanceof Entry) {
+ Entry entry = (Entry) selected;
+ if(entry instanceof PodcastEpisode && !entry.isVideo()) {
+ if(Util.isOffline(context)) {
+ inflater.inflate(R.menu.select_podcast_episode_context_offline, menu);
+ }
+ else {
+ inflater.inflate(R.menu.select_podcast_episode_context, menu);
+
+ if(entry.getBookmark() == null) {
+ menu.removeItem(R.id.bookmark_menu_delete);
+ }
+ }
+ }
+ else if (entry.isDirectory()) {
+ if(Util.isOffline(context)) {
+ inflater.inflate(R.menu.select_album_context_offline, menu);
+ }
+ else {
+ inflater.inflate(R.menu.select_album_context, menu);
+
+ if(Util.isTagBrowsing(context)) {
+ menu.removeItem(R.id.menu_rate);
+ }
+ }
+ menu.findItem(entry.isDirectory() ? R.id.album_menu_star : R.id.song_menu_star).setTitle(entry.isStarred() ? R.string.common_unstar : R.string.common_star);
+ } else if(!entry.isVideo()) {
+ if(Util.isOffline(context)) {
+ inflater.inflate(R.menu.select_song_context_offline, menu);
+ }
+ else {
+ inflater.inflate(R.menu.select_song_context, menu);
+
+ if(entry.getBookmark() == null) {
+ menu.removeItem(R.id.bookmark_menu_delete);
+ }
+ }
+ menu.findItem(entry.isDirectory() ? R.id.album_menu_star : R.id.song_menu_star).setTitle(entry.isStarred() ? R.string.common_unstar : R.string.common_star);
+ } else {
+ if(Util.isOffline(context)) {
+ inflater.inflate(R.menu.select_video_context_offline, menu);
+ }
+ else {
+ inflater.inflate(R.menu.select_video_context, menu);
+ }
+ }
+ } else if(selected instanceof Artist) {
+ Artist artist = (Artist) selected;
+ if(Util.isOffline(context)) {
+ inflater.inflate(R.menu.select_artist_context_offline, menu);
+ }
+ else {
+ inflater.inflate(R.menu.select_artist_context, menu);
+
+ menu.findItem(R.id.artist_menu_star).setTitle(artist.isStarred() ? R.string.common_unstar : R.string.common_star);
+ }
+ }
+
+ hideMenuItems(menu, (AdapterView.AdapterContextMenuInfo) menuInfo);
+ }
+
+ protected void hideMenuItems(ContextMenu menu, AdapterView.AdapterContextMenuInfo info) {
+ if(!ServerInfo.checkServerVersion(context, "1.8")) {
+ menu.setGroupVisible(R.id.server_1_8, false);
+ menu.setGroupVisible(R.id.hide_star, false);
+ }
+ if(!ServerInfo.checkServerVersion(context, "1.9")) {
+ menu.setGroupVisible(R.id.server_1_9, false);
+ }
+ if(!ServerInfo.checkServerVersion(context, "1.10.1")) {
+ menu.setGroupVisible(R.id.server_1_10, false);
+ }
+
+ SharedPreferences prefs = Util.getPreferences(context);
+ if(!prefs.getBoolean(Constants.PREFERENCES_KEY_MENU_PLAY_NEXT, true)) {
+ menu.setGroupVisible(R.id.hide_play_next, false);
+ }
+ if(!prefs.getBoolean(Constants.PREFERENCES_KEY_MENU_PLAY_LAST, true)) {
+ menu.setGroupVisible(R.id.hide_play_last, false);
+ }
+ if(!prefs.getBoolean(Constants.PREFERENCES_KEY_MENU_STAR, true)) {
+ menu.setGroupVisible(R.id.hide_star, false);
+ }
+ if(!prefs.getBoolean(Constants.PREFERENCES_KEY_MENU_SHARED, true) || !UserUtil.canShare()) {
+ menu.setGroupVisible(R.id.hide_share, false);
+ }
+ if(!prefs.getBoolean(Constants.PREFERENCES_KEY_MENU_RATING, true)) {
+ menu.setGroupVisible(R.id.hide_rating, false);
+ }
+
+ if(!Util.isOffline(context)) {
+ // If we are looking at a standard song view, get downloadFile to cache what options to show
+ if(info.targetView instanceof SongView) {
+ SongView songView = (SongView) info.targetView;
+ DownloadFile downloadFile = songView.getDownloadFile();
+
+ try {
+ if(downloadFile != null) {
+ if(downloadFile.isWorkDone()) {
+ // Remove permanent cache menu if already perma cached
+ if(downloadFile.isSaved()) {
+ menu.removeItem(R.id.song_menu_pin);
+ }
+
+ // Remove cache option no matter what if already downloaded
+ menu.removeItem(R.id.song_menu_download);
+ } else {
+ // Remove delete option if nothing to delete
+ menu.removeItem(R.id.song_menu_delete);
+ }
+ }
+ } catch(Exception e) {
+ Log.w(TAG, "Failed to lookup downloadFile info", e);
+ }
+ }
+ // Apply similar logic to album views
+ else if(info.targetView instanceof AlbumCell || info.targetView instanceof AlbumView
+ || info.targetView instanceof ArtistView || info.targetView instanceof ArtistEntryView) {
+ File folder = null;
+ int id = 0;
+ if(info.targetView instanceof AlbumCell) {
+ folder = ((AlbumCell) info.targetView).getFile();
+ id = R.id.album_menu_delete;
+ } else if(info.targetView instanceof AlbumView) {
+ folder = ((AlbumView) info.targetView).getFile();
+ id = R.id.album_menu_delete;
+ } else if(info.targetView instanceof ArtistView) {
+ folder = ((ArtistView) info.targetView).getFile();
+ id = R.id.artist_menu_delete;
+ } else if(info.targetView instanceof ArtistEntryView) {
+ folder = ((ArtistEntryView) info.targetView).getFile();
+ id = R.id.artist_menu_delete;
+ }
+
+ try {
+ if(folder != null && !folder.exists()) {
+ menu.removeItem(id);
+ }
+ } catch(Exception e) {
+ Log.w(TAG, "Failed to lookup album directory info", e);
+ }
+ }
+ }
+ }
+
+ protected void recreateContextMenu(ContextMenu menu) {
+ List<MenuItem> menuItems = new ArrayList<MenuItem>();
+ for(int i = 0; i < menu.size(); i++) {
+ MenuItem item = menu.getItem(i);
+ if(item.isVisible()) {
+ menuItems.add(item);
+ }
+ }
+ menu.clear();
+ for(int i = 0; i < menuItems.size(); i++) {
+ MenuItem item = menuItems.get(i);
+ menu.add(tag, item.getItemId(), Menu.NONE, item.getTitle());
+ }
+ }
+
+ public boolean onContextItemSelected(MenuItem menuItem, Object selectedItem) {
+ Artist artist = selectedItem instanceof Artist ? (Artist) selectedItem : null;
+ Entry entry = selectedItem instanceof Entry ? (Entry) selectedItem : null;
+ List<Entry> songs = new ArrayList<Entry>(1);
+ songs.add(entry);
+
+ switch (menuItem.getItemId()) {
+ case R.id.artist_menu_play_now:
+ downloadRecursively(artist.getId(), false, false, true, false, false);
+ break;
+ case R.id.artist_menu_play_shuffled:
+ downloadRecursively(artist.getId(), false, false, true, true, false);
+ break;
+ case R.id.artist_menu_play_next:
+ downloadRecursively(artist.getId(), false, true, false, false, false, true);
+ break;
+ case R.id.artist_menu_play_last:
+ downloadRecursively(artist.getId(), false, true, false, false, false);
+ break;
+ case R.id.artist_menu_download:
+ downloadRecursively(artist.getId(), false, true, false, false, true);
+ break;
+ case R.id.artist_menu_pin:
+ downloadRecursively(artist.getId(), true, true, false, false, true);
+ break;
+ case R.id.artist_menu_delete:
+ deleteRecursively(artist);
+ break;
+ case R.id.artist_menu_star:
+ toggleStarred(artist);
+ break;
+ case R.id.album_menu_play_now:
+ artistOverride = true;
+ downloadRecursively(entry.getId(), false, false, true, false, false);
+ break;
+ case R.id.album_menu_play_shuffled:
+ artistOverride = true;
+ downloadRecursively(entry.getId(), false, false, true, true, false);
+ break;
+ case R.id.album_menu_play_next:
+ artistOverride = true;
+ downloadRecursively(entry.getId(), false, true, false, false, false, true);
+ break;
+ case R.id.album_menu_play_last:
+ artistOverride = true;
+ downloadRecursively(entry.getId(), false, true, false, false, false);
+ break;
+ case R.id.album_menu_download:
+ artistOverride = true;
+ downloadRecursively(entry.getId(), false, true, false, false, true);
+ break;
+ case R.id.album_menu_pin:
+ artistOverride = true;
+ downloadRecursively(entry.getId(), true, true, false, false, true);
+ break;
+ case R.id.album_menu_star:
+ toggleStarred(entry);
+ break;
+ case R.id.album_menu_delete:
+ deleteRecursively(entry);
+ break;
+ case R.id.album_menu_info:
+ displaySongInfo(entry);
+ break;
+ case R.id.album_menu_show_artist:
+ showAlbumArtist((Entry) selectedItem);
+ break;
+ case R.id.album_menu_share:
+ createShare(songs);
+ break;
+ case R.id.song_menu_play_now:
+ playNow(songs);
+ break;
+ case R.id.song_menu_play_next:
+ getDownloadService().download(songs, false, false, true, false);
+ break;
+ case R.id.song_menu_play_last:
+ getDownloadService().download(songs, false, false, false, false);
+ break;
+ case R.id.song_menu_download:
+ getDownloadService().downloadBackground(songs, false);
+ break;
+ case R.id.song_menu_pin:
+ getDownloadService().downloadBackground(songs, true);
+ break;
+ case R.id.song_menu_delete:
+ getDownloadService().delete(songs);
+ break;
+ case R.id.song_menu_add_playlist:
+ addToPlaylist(songs);
+ break;
+ case R.id.song_menu_star:
+ toggleStarred(entry);
+ break;
+ case R.id.song_menu_play_external:
+ playExternalPlayer(entry);
+ break;
+ case R.id.song_menu_info:
+ displaySongInfo(entry);
+ break;
+ case R.id.song_menu_stream_external:
+ streamExternalPlayer(entry);
+ break;
+ case R.id.song_menu_share:
+ createShare(songs);
+ break;
+ case R.id.song_menu_show_album:
+ showAlbum((Entry) selectedItem);
+ break;
+ case R.id.song_menu_show_artist:
+ showArtist((Entry) selectedItem);
+ break;
+ case R.id.bookmark_menu_delete:
+ deleteBookmark(entry, null);
+ break;
+ case R.id.menu_rate:
+ setRating(entry);
+ break;
+ default:
+ return false;
+ }
+
+ return true;
+ }
+
+ public void replaceFragment(SubsonicFragment fragment) {
+ replaceFragment(fragment, true);
+ }
+ public void replaceFragment(SubsonicFragment fragment, boolean replaceCurrent) {
+ context.replaceFragment(fragment, fragment.getSupportTag(), secondaryFragment && replaceCurrent);
+ }
+
+ public int getRootId() {
+ return rootView.getId();
+ }
+
+ public void setSupportTag(int tag) { this.tag = tag; }
+ public void setSupportTag(String tag) { this.tag = Integer.parseInt(tag); }
+ public int getSupportTag() {
+ return tag;
+ }
+
+ public void setPrimaryFragment(boolean primary) {
+ primaryFragment = primary;
+ if(primary) {
+ if(context != null && title != null) {
+ context.setTitle(title);
+ context.setSubtitle(subtitle);
+ }
+ if(invalidated) {
+ invalidated = false;
+ refresh(false);
+ }
+ }
+ }
+ public void setPrimaryFragment(boolean primary, boolean secondary) {
+ setPrimaryFragment(primary);
+ secondaryFragment = secondary;
+ }
+ public void setSecondaryFragment(boolean secondary) {
+ secondaryFragment = secondary;
+ }
+
+ public void invalidate() {
+ if(primaryFragment) {
+ refresh(true);
+ } else {
+ invalidated = true;
+ }
+ }
+
+ public DownloadService getDownloadService() {
+ return context != null ? context.getDownloadService() : null;
+ }
+
+ protected void refresh() {
+ refresh(true);
+ }
+ protected void refresh(boolean refresh) {
+
+ }
+
+ @Override
+ public void onRefresh() {
+ refreshLayout.setRefreshing(false);
+ refresh();
+ }
+
+ protected void exit() {
+ if(((Object) context).getClass() != SubsonicFragmentActivity.class) {
+ Intent intent = new Intent(context, SubsonicFragmentActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_EXIT, true);
+ Util.startActivityWithoutTransition(context, intent);
+ } else {
+ context.stopService(new Intent(context, DownloadService.class));
+ context.finish();
+ }
+ }
+
+ public void setProgressVisible(boolean visible) {
+ View view = rootView.findViewById(R.id.tab_progress);
+ if (view != null) {
+ view.setVisibility(visible ? View.VISIBLE : View.GONE);
+
+ if(visible) {
+ View progress = rootView.findViewById(R.id.tab_progress_spinner);
+ progress.setVisibility(View.VISIBLE);
+ }
+ }
+ }
+
+ public void updateProgress(String message) {
+ TextView view = (TextView) rootView.findViewById(R.id.tab_progress_message);
+ if (view != null) {
+ view.setText(message);
+ }
+ }
+
+ public void setEmpty(boolean empty) {
+ View view = rootView.findViewById(R.id.tab_progress);
+ if(empty) {
+ view.setVisibility(View.VISIBLE);
+
+ View progress = view.findViewById(R.id.tab_progress_spinner);
+ progress.setVisibility(View.GONE);
+
+ TextView text = (TextView) view.findViewById(R.id.tab_progress_message);
+ text.setText(R.string.common_empty);
+ } else {
+ view.setVisibility(View.GONE);
+ }
+ }
+
+ protected synchronized ImageLoader getImageLoader() {
+ return context.getImageLoader();
+ }
+ public synchronized static ImageLoader getStaticImageLoader(Context context) {
+ return SubsonicActivity.getStaticImageLoader(context);
+ }
+
+ public void setTitle(CharSequence title) {
+ this.title = title;
+ context.setTitle(title);
+ }
+ public void setTitle(int title) {
+ this.title = context.getResources().getString(title);
+ context.setTitle(this.title);
+ }
+ public void setSubtitle(CharSequence title) {
+ this.subtitle = title;
+ context.setSubtitle(title);
+ }
+ public CharSequence getTitle() {
+ return this.title;
+ }
+
+ protected void setupScrollList(final AbsListView listView) {
+ if(!context.isTouchscreen()) {
+ refreshLayout.setEnabled(false);
+ } else {
+ listView.setOnScrollListener(new AbsListView.OnScrollListener() {
+ @Override
+ public void onScrollStateChanged(AbsListView view, int scrollState) {
+ }
+
+ @Override
+ public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
+ int topRowVerticalPosition = (listView.getChildCount() == 0) ? 0 : listView.getChildAt(0).getTop();
+ refreshLayout.setEnabled(topRowVerticalPosition >= 0 && listView.getFirstVisiblePosition() == 0);
+ }
+ });
+
+ refreshLayout.setColorScheme(
+ R.color.holo_blue_light,
+ R.color.holo_orange_light,
+ R.color.holo_green_light,
+ R.color.holo_red_light);
+ }
+ }
+
+ protected void warnIfStorageUnavailable() {
+ if (!Util.isExternalStoragePresent()) {
+ Util.toast(context, R.string.select_album_no_sdcard);
+ }
+
+ try {
+ StatFs stat = new StatFs(FileUtil.getMusicDirectory(context).getPath());
+ long bytesAvailableFs = (long) stat.getAvailableBlocks() * (long) stat.getBlockSize();
+ if (bytesAvailableFs < 50000000L) {
+ Util.toast(context, context.getResources().getString(R.string.select_album_no_room, Util.formatBytes(bytesAvailableFs)));
+ }
+ } catch(Exception e) {
+ Log.w(TAG, "Error while checking storage space for music directory", e);
+ }
+ }
+
+ protected void onShuffleRequested() {
+ if(Util.isOffline(context)) {
+ Intent intent = new Intent(context, DownloadActivity.class);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_SHUFFLE, true);
+ Util.startActivityWithoutTransition(context, intent);
+ return;
+ }
+
+ View dialogView = context.getLayoutInflater().inflate(R.layout.shuffle_dialog, null);
+ final EditText startYearBox = (EditText)dialogView.findViewById(R.id.start_year);
+ final EditText endYearBox = (EditText)dialogView.findViewById(R.id.end_year);
+ final EditText genreBox = (EditText)dialogView.findViewById(R.id.genre);
+ final Button genreCombo = (Button)dialogView.findViewById(R.id.genre_combo);
+
+ final SharedPreferences prefs = Util.getPreferences(context);
+ final String oldStartYear = prefs.getString(Constants.PREFERENCES_KEY_SHUFFLE_START_YEAR, "");
+ final String oldEndYear = prefs.getString(Constants.PREFERENCES_KEY_SHUFFLE_END_YEAR, "");
+ final String oldGenre = prefs.getString(Constants.PREFERENCES_KEY_SHUFFLE_GENRE, "");
+
+ boolean _useCombo = false;
+ if(ServerInfo.checkServerVersion(context, "1.9.0")) {
+ genreBox.setVisibility(View.GONE);
+ genreCombo.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ new LoadingTask<List<Genre>>(context, true) {
+ @Override
+ protected List<Genre> doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ return musicService.getGenres(false, context, this);
+ }
+
+ @Override
+ protected void done(final List<Genre> genres) {
+ List<String> names = new ArrayList<String>();
+ String blank = context.getResources().getString(R.string.select_genre_blank);
+ names.add(blank);
+ for(Genre genre: genres) {
+ names.add(genre.getName());
+ }
+ final List<String> finalNames = names;
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.shuffle_pick_genre)
+ .setItems(names.toArray(new CharSequence[names.size()]), new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ if(which == 0) {
+ genreCombo.setText("");
+ } else {
+ genreCombo.setText(finalNames.get(which));
+ }
+ }
+ });
+ AlertDialog dialog = builder.create();
+ dialog.show();
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.playlist_error) + " " + getErrorMessage(error);
+ }
+
+ Util.toast(context, msg, false);
+ }
+ }.execute();
+ }
+ });
+ _useCombo = true;
+ } else {
+ genreCombo.setVisibility(View.GONE);
+ }
+ final boolean useCombo = _useCombo;
+
+ startYearBox.setText(oldStartYear);
+ endYearBox.setText(oldEndYear);
+ genreBox.setText(oldGenre);
+ genreCombo.setText(oldGenre);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.shuffle_title)
+ .setView(dialogView)
+ .setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ Intent intent = new Intent(context, DownloadActivity.class);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_SHUFFLE, true);
+ String genre;
+ if(useCombo) {
+ genre = genreCombo.getText().toString();
+ } else {
+ genre = genreBox.getText().toString();
+ }
+ String startYear = startYearBox.getText().toString();
+ String endYear = endYearBox.getText().toString();
+
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putString(Constants.PREFERENCES_KEY_SHUFFLE_START_YEAR, startYear);
+ editor.putString(Constants.PREFERENCES_KEY_SHUFFLE_END_YEAR, endYear);
+ editor.putString(Constants.PREFERENCES_KEY_SHUFFLE_GENRE, genre);
+ editor.commit();
+
+ Util.startActivityWithoutTransition(context, intent);
+ }
+ })
+ .setNegativeButton(R.string.common_cancel, null);
+ AlertDialog dialog = builder.create();
+ dialog.show();
+ }
+
+ public void toggleStarred(Entry entry) {
+ toggleStarred(entry, null);
+ }
+ public void toggleStarred(final Entry entry, final OnStarChange onStarChange) {
+ final boolean starred = !entry.isStarred();
+ entry.setStarred(starred);
+ if(onStarChange != null) {
+ onStarChange.starChange(starred);
+ }
+
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ if(entry.isDirectory() && Util.isTagBrowsing(context) && !Util.isOffline(context)) {
+ if(entry.isAlbum()) {
+ musicService.setStarred(null, null, Arrays.asList(entry), starred, null, context);
+ } else {
+ musicService.setStarred(null, Arrays.asList(entry), null, starred, null, context);
+ }
+ } else {
+ musicService.setStarred(Arrays.asList(entry), null, null, starred, null, context);
+ }
+
+ new EntryInstanceUpdater(entry) {
+ @Override
+ public void update(Entry found) {
+ found.setStarred(starred);
+ }
+ }.execute();
+
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ // UpdateView
+ Util.toast(context, context.getResources().getString(starred ? R.string.starring_content_starred : R.string.starring_content_unstarred, entry.getTitle()));
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ Log.w(TAG, "Failed to star", error);
+ entry.setStarred(!starred);
+ if(onStarChange != null) {
+ onStarChange.starChange(!starred);
+ }
+
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.starring_content_error, entry.getTitle()) + " " + getErrorMessage(error);
+ }
+
+ Util.toast(context, msg, false);
+ }
+ }.execute();
+ }
+
+ public void toggleStarred(final Artist entry) {
+ final boolean starred = !entry.isStarred();
+ entry.setStarred(starred);
+
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ if(Util.isTagBrowsing(context) && !Util.isOffline(context)) {
+ musicService.setStarred(null, Arrays.asList(new Entry(entry)), null, starred, null, context);
+ } else {
+ musicService.setStarred(Arrays.asList(new Entry(entry)), null, null, starred, null, context);
+ }
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ // UpdateView
+ Util.toast(context, context.getResources().getString(starred ? R.string.starring_content_starred : R.string.starring_content_unstarred, entry.getName()));
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ Log.w(TAG, "Failed to star", error);
+ entry.setStarred(!starred);
+
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.starring_content_error, entry.getName()) + " " + getErrorMessage(error);
+ }
+
+ Util.toast(context, msg, false);
+ }
+ }.execute();
+ }
+
+ protected void downloadRecursively(final String id, final boolean save, final boolean append, final boolean autoplay, final boolean shuffle, final boolean background) {
+ downloadRecursively(id, "", true, save, append, autoplay, shuffle, background);
+ }
+ protected void downloadRecursively(final String id, final boolean save, final boolean append, final boolean autoplay, final boolean shuffle, final boolean background, final boolean playNext) {
+ downloadRecursively(id, "", true, save, append, autoplay, shuffle, background, playNext);
+ }
+ protected void downloadPlaylist(final String id, final String name, final boolean save, final boolean append, final boolean autoplay, final boolean shuffle, final boolean background) {
+ downloadRecursively(id, name, false, save, append, autoplay, shuffle, background);
+ }
+ protected void downloadRecursively(final String id, final String name, final boolean isDirectory, final boolean save, final boolean append, final boolean autoplay, final boolean shuffle, final boolean background) {
+ downloadRecursively(id, name, isDirectory, save, append, autoplay, shuffle, background, false);
+ }
+ protected void downloadRecursively(final String id, final String name, final boolean isDirectory, final boolean save, final boolean append, final boolean autoplay, final boolean shuffle, final boolean background, final boolean playNext) {
+ new RecursiveLoader(context) {
+ @Override
+ protected Boolean doInBackground() throws Throwable {
+ musicService = MusicServiceFactory.getMusicService(context);
+ MusicDirectory root;
+ if(share != null) {
+ root = share.getMusicDirectory();
+ }
+ else if(isDirectory) {
+ if(id != null) {
+ root = getMusicDirectory(id, name, false, musicService, this);
+ } else {
+ root = musicService.getStarredList(context, this);
+ }
+ }
+ else {
+ root = musicService.getPlaylist(true, id, name, context, this);
+ }
+
+ if(shuffle) {
+ Collections.shuffle(root.getChildren());
+ }
+
+ songs = new LinkedList<Entry>();
+ getSongsRecursively(root, songs);
+
+ DownloadService downloadService = getDownloadService();
+ boolean transition = false;
+ if (!songs.isEmpty() && downloadService != null) {
+ // Conditions for a standard play now operation
+ if(!append && !save && autoplay && !playNext && !shuffle && !background) {
+ playNowOverride = true;
+ return false;
+ }
+
+ if (!append && !background) {
+ downloadService.clear();
+ }
+ if(!background) {
+ downloadService.download(songs, save, autoplay, playNext, false);
+ if(!append) {
+ transition = true;
+ }
+ }
+ else {
+ downloadService.downloadBackground(songs, save);
+ }
+ }
+ artistOverride = false;
+
+ return transition;
+ }
+ }.execute();
+ }
+
+ protected void downloadRecursively(final List<Entry> albums, final boolean shuffle, final boolean append) {
+ new RecursiveLoader(context) {
+ @Override
+ protected Boolean doInBackground() throws Throwable {
+ musicService = MusicServiceFactory.getMusicService(context);
+
+ if(shuffle) {
+ Collections.shuffle(albums);
+ }
+
+ songs = new LinkedList<Entry>();
+ MusicDirectory root = new MusicDirectory();
+ root.addChildren(albums);
+ getSongsRecursively(root, songs);
+
+ DownloadService downloadService = getDownloadService();
+ boolean transition = false;
+ if (!songs.isEmpty() && downloadService != null) {
+ // Conditions for a standard play now operation
+ if(!append && !shuffle) {
+ playNowOverride = true;
+ return false;
+ }
+
+ if (!append) {
+ downloadService.clear();
+ }
+
+ downloadService.download(songs, false, true, false, false);
+ if(!append) {
+ transition = true;
+ }
+ }
+ artistOverride = false;
+
+ return transition;
+ }
+ }.execute();
+ }
+
+ protected MusicDirectory getMusicDirectory(String id, String name, boolean refresh, MusicService service, ProgressListener listener) throws Exception {
+ return getMusicDirectory(id, name, refresh, false, service, listener);
+ }
+ protected MusicDirectory getMusicDirectory(String id, String name, boolean refresh, boolean forceArtist, MusicService service, ProgressListener listener) throws Exception {
+ if(Util.isTagBrowsing(context) && !Util.isOffline(context)) {
+ if(artist && !artistOverride || forceArtist) {
+ return service.getArtist(id, name, refresh, context, listener);
+ } else {
+ return service.getAlbum(id, name, refresh, context, listener);
+ }
+ } else {
+ return service.getMusicDirectory(id, name, refresh, context, listener);
+ }
+ }
+
+ protected void addToPlaylist(final List<Entry> songs) {
+ if(songs.isEmpty()) {
+ Util.toast(context, "No songs selected");
+ return;
+ }
+
+ new LoadingTask<List<Playlist>>(context, true) {
+ @Override
+ protected List<Playlist> doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ List<Playlist> playlists = new ArrayList<Playlist>();
+ playlists.addAll(musicService.getPlaylists(false, context, this));
+
+ // Iterate through and remove all non owned public playlists
+ Iterator<Playlist> it = playlists.iterator();
+ while(it.hasNext()) {
+ Playlist playlist = it.next();
+ if(playlist.getPublic() == true && playlist.getId().indexOf(".m3u") == -1 && !UserUtil.getCurrentUsername(context).equals(playlist.getOwner())) {
+ it.remove();
+ }
+ }
+
+ return playlists;
+ }
+
+ @Override
+ protected void done(final List<Playlist> playlists) {
+ // Create adapter to show playlists
+ Playlist createNew = new Playlist("-1", context.getResources().getString(R.string.playlist_create_new));
+ playlists.add(0, createNew);
+ ArrayAdapter playlistAdapter = new ArrayAdapter<Playlist>(context, R.layout.basic_count_item, playlists) {
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ Playlist playlist = getItem(position);
+
+ // Create new if not getting a convert view to use
+ PlaylistSongView view;
+ if(convertView instanceof PlaylistSongView) {
+ view = (PlaylistSongView) convertView;
+ } else {
+ view = new PlaylistSongView(context);
+ }
+
+ view.setObject(playlist, songs);
+
+ return view;
+ }
+ };
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.playlist_add_to)
+ .setAdapter(playlistAdapter, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ if (which > 0) {
+ addToPlaylist(playlists.get(which), songs);
+ } else {
+ createNewPlaylist(songs, false);
+ }
+ }
+ });
+ AlertDialog dialog = builder.create();
+ dialog.show();
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.playlist_error) + " " + getErrorMessage(error);
+ }
+
+ Util.toast(context, msg, false);
+ }
+ }.execute();
+ }
+
+ private void addToPlaylist(final Playlist playlist, final List<Entry> songs) {
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.addToPlaylist(playlist.getId(), songs, context, null);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ Util.toast(context, context.getResources().getString(R.string.updated_playlist, songs.size(), playlist.getName()));
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.updated_playlist_error, playlist.getName()) + " " + getErrorMessage(error);
+ }
+
+ Util.toast(context, msg, false);
+ }
+ }.execute();
+ }
+
+ protected void createNewPlaylist(final List<Entry> songs, final boolean getSuggestion) {
+ View layout = context.getLayoutInflater().inflate(R.layout.save_playlist, null);
+ final EditText playlistNameView = (EditText) layout.findViewById(R.id.save_playlist_name);
+ final CheckBox overwriteCheckBox = (CheckBox) layout.findViewById(R.id.save_playlist_overwrite);
+ if(getSuggestion) {
+ String playlistName = (getDownloadService() != null) ? getDownloadService().getSuggestedPlaylistName() : null;
+ if (playlistName != null) {
+ playlistNameView.setText(playlistName);
+ try {
+ if(ServerInfo.checkServerVersion(context, "1.8.0") && Integer.parseInt(getDownloadService().getSuggestedPlaylistId()) != -1) {
+ overwriteCheckBox.setChecked(true);
+ overwriteCheckBox.setVisibility(View.VISIBLE);
+ }
+ } catch(Exception e) {
+ Log.d(TAG, "Playlist id isn't a integer, probably MusicCabinet");
+ }
+ } else {
+ DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
+ playlistNameView.setText(dateFormat.format(new Date()));
+ }
+ } else {
+ DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
+ playlistNameView.setText(dateFormat.format(new Date()));
+ }
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.download_playlist_title)
+ .setMessage(R.string.download_playlist_name)
+ .setView(layout)
+ .setPositiveButton(R.string.common_save, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ String playlistName = String.valueOf(playlistNameView.getText());
+ if(overwriteCheckBox.isChecked()) {
+ overwritePlaylist(songs, playlistName, getDownloadService().getSuggestedPlaylistId());
+ } else {
+ createNewPlaylist(songs, playlistName);
+
+ if(getSuggestion) {
+ DownloadService downloadService = getDownloadService();
+ if(downloadService != null) {
+ downloadService.setSuggestedPlaylistName(playlistName, null);
+ }
+ }
+ }
+ }
+ })
+ .setNegativeButton(R.string.common_cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.cancel();
+ }
+ })
+ .setCancelable(true);
+
+ AlertDialog dialog = builder.create();
+ dialog.show();
+ }
+ private void createNewPlaylist(final List<Entry> songs, final String name) {
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.createPlaylist(null, name, songs, context, null);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ Util.toast(context, R.string.download_playlist_done);
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg = context.getResources().getString(R.string.download_playlist_error) + " " + getErrorMessage(error);
+ Util.toast(context, msg);
+ }
+ }.execute();
+ }
+ private void overwritePlaylist(final List<Entry> songs, final String name, final String id) {
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ MusicDirectory playlist = musicService.getPlaylist(true, id, name, context, null);
+ List<Entry> toDelete = playlist.getChildren();
+ musicService.overwritePlaylist(id, name, toDelete.size(), songs, context, null);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ Util.toast(context, R.string.download_playlist_done);
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.download_playlist_error) + " " + getErrorMessage(error);
+ }
+
+ Util.toast(context, msg, false);
+ }
+ }.execute();
+ }
+
+ public void displaySongInfo(final Entry song) {
+ Integer duration = null;
+ Integer bitrate = null;
+ String format = null;
+ long size = 0;
+ if(!song.isDirectory()) {
+ try {
+ DownloadFile downloadFile = new DownloadFile(context, song, false);
+ File file = downloadFile.getCompleteFile();
+ if(file.exists()) {
+ MediaMetadataRetriever metadata = new MediaMetadataRetriever();
+ metadata.setDataSource(file.getAbsolutePath());
+
+ String tmp = metadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
+ duration = Integer.parseInt((tmp != null) ? tmp : "0") / 1000;
+ format = FileUtil.getExtension(file.getName());
+ size = file.length();
+
+ // If no duration try to read bitrate tag
+ if(duration == null) {
+ tmp = metadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE);
+ bitrate = Integer.parseInt((tmp != null) ? tmp : "0") / 1000;
+ } else {
+ // Otherwise do a calculation for it
+ // Divide by 1000 so in kbps
+ bitrate = (int) (size / duration) / 1000 * 8;
+ }
+
+ if(Util.isOffline(context)) {
+ song.setGenre(metadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_GENRE));
+ String year = metadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_YEAR);
+ song.setYear(Integer.parseInt((year != null) ? year : "0"));
+ }
+ }
+ } catch(Exception e) {
+ Log.i(TAG, "Device doesn't properly support MediaMetadataRetreiver");
+ }
+ }
+
+ String msg = "";
+ if(song instanceof PodcastEpisode) {
+ msg += "Podcast: " + song.getArtist() + "\nStatus: " + ((PodcastEpisode)song).getStatus();
+ } else if(!song.isVideo()) {
+ if(song.getArtist() != null && !"".equals(song.getArtist())) {
+ msg += "Artist: " + song.getArtist();
+ }
+ if(song.getAlbum() != null && !"".equals(song.getAlbum())) {
+ msg += "\nAlbum: " + song.getAlbum();
+ }
+ }
+ if(song.getTrack() != null && song.getTrack() != 0) {
+ msg += "\nTrack: " + song.getTrack();
+ }
+ if(song.getGenre() != null && !"".equals(song.getGenre())) {
+ msg += "\nGenre: " + song.getGenre();
+ }
+ if(song.getYear() != null && song.getYear() != 0) {
+ msg += "\nYear: " + song.getYear();
+ }
+ if(!Util.isOffline(context) && song.getSuffix() != null) {
+ msg += "\nServer Format: " + song.getSuffix();
+ if(song.getBitRate() != null && song.getBitRate() != 0) {
+ msg += "\nServer Bitrate: " + song.getBitRate() + " kbps";
+ }
+ }
+ if(format != null && !"".equals(format)) {
+ msg += "\nCached Format: " + format;
+ }
+ if(bitrate != null && bitrate != 0) {
+ msg += "\nCached Bitrate: " + bitrate + " kbps";
+ }
+ if(size != 0) {
+ msg += "\nSize: " + Util.formatLocalizedBytes(size, context);
+ }
+ if(song.getDuration() != null && song.getDuration() != 0) {
+ msg += "\nLength: " + Util.formatDuration(song.getDuration());
+ }
+ if(song.getBookmark() != null) {
+ msg += "\nBookmark Position: " + Util.formatDuration(song.getBookmark().getPosition() / 1000);
+ }
+ if(song.getRating() != 0) {
+ msg += "\nRating: " + song.getRating() + " stars";
+ }
+ if(song instanceof PodcastEpisode) {
+ msg += "\n\nDescription: " + song.getAlbum();
+ }
+
+ Util.info(context, song.getTitle(), msg);
+ }
+
+ protected void playVideo(Entry entry) {
+ if(entryExists(entry)) {
+ playExternalPlayer(entry);
+ } else {
+ streamExternalPlayer(entry);
+ }
+ }
+
+ protected void playWebView(Entry entry) {
+ int maxBitrate = Util.getMaxVideoBitrate(context);
+
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(Uri.parse(MusicServiceFactory.getMusicService(context).getVideoUrl(maxBitrate, context, entry.getId())));
+
+ startActivity(intent);
+ }
+ protected void playExternalPlayer(Entry entry) {
+ if(!entryExists(entry)) {
+ Util.toast(context, R.string.download_need_download);
+ } else {
+ DownloadFile check = new DownloadFile(context, entry, false);
+ File file = check.getCompleteFile();
+
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setDataAndType(Uri.fromFile(file), "video/*");
+ intent.putExtra(Intent.EXTRA_TITLE, entry.getTitle());
+
+ List<ResolveInfo> intents = context.getPackageManager()
+ .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
+ if(intents != null && intents.size() > 0) {
+ startActivity(intent);
+ }else {
+ Util.toast(context, R.string.download_no_streaming_player);
+ }
+ }
+ }
+ protected void streamExternalPlayer(Entry entry) {
+ String videoPlayerType = Util.getVideoPlayerType(context);
+ if("flash".equals(videoPlayerType)) {
+ playWebView(entry);
+ } else if("hls".equals(videoPlayerType)) {
+ streamExternalPlayer(entry, "hls");
+ } else if("raw".equals(videoPlayerType)) {
+ streamExternalPlayer(entry, "raw");
+ } else {
+ streamExternalPlayer(entry, entry.getTranscodedSuffix());
+ }
+ }
+ protected void streamExternalPlayer(Entry entry, String format) {
+ try {
+ int maxBitrate = Util.getMaxVideoBitrate(context);
+
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ if("hls".equals(format)) {
+ intent.setDataAndType(Uri.parse(MusicServiceFactory.getMusicService(context).getHlsUrl(entry.getId(), maxBitrate, context)), "application/x-mpegURL");
+ } else {
+ intent.setDataAndType(Uri.parse(MusicServiceFactory.getMusicService(context).getVideoStreamUrl(format, maxBitrate, context, entry.getId())), "video/*");
+ }
+ intent.putExtra("title", entry.getTitle());
+
+ List<ResolveInfo> intents = context.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
+ if(intents != null && intents.size() > 0) {
+ startActivity(intent);
+ } else {
+ Util.toast(context, R.string.download_no_streaming_player);
+ }
+ } catch(Exception error) {
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = error.getMessage();
+ } else {
+ msg = context.getResources().getString(R.string.download_no_streaming_player) + " " + error.getMessage();
+ }
+
+ Util.toast(context, msg, false);
+ }
+ }
+
+ protected boolean entryExists(Entry entry) {
+ DownloadFile check = new DownloadFile(context, entry, false);
+ return check.isCompleteFileAvailable();
+ }
+
+ public void deleteRecursively(Artist artist) {
+ deleteRecursively(FileUtil.getArtistDirectory(context, artist));
+ }
+
+ public void deleteRecursively(Entry album) {
+ deleteRecursively(FileUtil.getAlbumDirectory(context, album));
+
+ }
+ public void deleteRecursively(final File dir) {
+ if(dir == null) {
+ return;
+ }
+
+ new LoadingTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MediaStoreService mediaStore = new MediaStoreService(context);
+ FileUtil.recursiveDelete(dir, mediaStore);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ if(Util.isOffline(context)) {
+ refresh();
+ } else {
+ UpdateView.triggerUpdate();
+ }
+ }
+ }.execute();
+ }
+
+ public void showAlbumArtist(Entry entry) {
+ SubsonicFragment fragment = new SelectDirectoryFragment();
+ Bundle args = new Bundle();
+ if(Util.isTagBrowsing(context)) {
+ args.putString(Constants.INTENT_EXTRA_NAME_ID, entry.getArtistId());
+ } else {
+ args.putString(Constants.INTENT_EXTRA_NAME_ID, entry.getParent());
+ }
+ args.putString(Constants.INTENT_EXTRA_NAME_NAME, entry.getArtist());
+ args.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, true);
+ fragment.setArguments(args);
+
+ replaceFragment(fragment, true);
+ }
+ public void showArtist(Entry entry) {
+ SubsonicFragment fragment = new SelectDirectoryFragment();
+ Bundle args = new Bundle();
+ if(Util.isTagBrowsing(context)) {
+ args.putString(Constants.INTENT_EXTRA_NAME_ID, entry.getArtistId());
+ } else {
+ if(entry.getGrandParent() == null) {
+ args.putString(Constants.INTENT_EXTRA_NAME_CHILD_ID, entry.getParent());
+ } else {
+ args.putString(Constants.INTENT_EXTRA_NAME_ID, entry.getGrandParent());
+ }
+ }
+ args.putString(Constants.INTENT_EXTRA_NAME_NAME, entry.getArtist());
+ args.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, true);
+ fragment.setArguments(args);
+
+ replaceFragment(fragment, true);
+ }
+ public void showAlbum(Entry entry) {
+ SubsonicFragment fragment = new SelectDirectoryFragment();
+ Bundle args = new Bundle();
+ if(Util.isTagBrowsing(context)) {
+ args.putString(Constants.INTENT_EXTRA_NAME_ID, entry.getAlbumId());
+ } else {
+ args.putString(Constants.INTENT_EXTRA_NAME_ID, entry.getParent());
+ }
+ args.putString(Constants.INTENT_EXTRA_NAME_NAME, entry.getAlbum());
+ fragment.setArguments(args);
+
+ replaceFragment(fragment, true);
+ }
+
+ public void createShare(final List<Entry> entries) {
+ new LoadingTask<List<Share>>(context, true) {
+ @Override
+ protected List<Share> doInBackground() throws Throwable {
+ List<String> ids = new ArrayList<String>(entries.size());
+ for(Entry entry: entries) {
+ ids.add(entry.getId());
+ }
+
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ return musicService.createShare(ids, null, 0L, context, this);
+ }
+
+ @Override
+ protected void done(final List<Share> shares) {
+ if(shares.size() > 0) {
+ Share share = shares.get(0);
+ shareExternal(share);
+ } else {
+ Util.toast(context, context.getResources().getString(R.string.playlist_error), false);
+ }
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.playlist_error) + " " + getErrorMessage(error);
+ }
+
+ Util.toast(context, msg, false);
+ }
+ }.execute();
+ }
+ public void shareExternal(Share share) {
+ Intent intent = new Intent(Intent.ACTION_SEND);
+ intent.setType("text/plain");
+ intent.putExtra(Intent.EXTRA_TEXT, share.getUrl());
+ context.startActivity(Intent.createChooser(intent, context.getResources().getString(R.string.share_via)));
+ }
+
+ public GestureDetector getGestureDetector() {
+ return gestureScanner;
+ }
+
+ protected void playBookmark(List<Entry> songs, Entry song) {
+ playBookmark(songs, song, null, null);
+ }
+ protected void playBookmark(final List<Entry> songs, final Entry song, final String playlistName, final String playlistId) {
+ final Integer position = song.getBookmark().getPosition();
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.bookmark_resume_title)
+ .setMessage(getResources().getString(R.string.bookmark_resume, song.getTitle(), Util.formatDuration(position / 1000)))
+ .setPositiveButton(R.string.bookmark_action_resume, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ playNow(songs, song, position);
+ }
+ })
+ .setNegativeButton(R.string.bookmark_action_start_over, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ final Bookmark oldBookmark = song.getBookmark();
+ song.setBookmark(null);
+
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.deleteBookmark(song, context, null);
+
+ return null;
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ song.setBookmark(oldBookmark);
+
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.bookmark_deleted_error, song.getTitle()) + " " + getErrorMessage(error);
+ }
+
+ Util.toast(context, msg, false);
+ }
+ }.execute();
+
+ playNow(songs, 0);
+ }
+ });
+ AlertDialog dialog = builder.create();
+ dialog.show();
+ }
+
+ protected void playNow(List<Entry> entries) {
+ playNow(entries, null, null);
+ }
+ protected void playNow(List<Entry> entries, String playlistName, String playlistId) {
+ Entry bookmark = null;
+ for(Entry entry: entries) {
+ if(entry.getBookmark() != null) {
+ bookmark = entry;
+ break;
+ }
+ }
+
+ // If no bookmark found, just play from start
+ if(bookmark == null) {
+ playNow(entries, 0, playlistName, playlistId);
+ } else {
+ // If bookmark found, then give user choice to start from there or to start over
+ playBookmark(entries, bookmark, playlistName, playlistId);
+ }
+ }
+ protected void playNow(List<Entry> entries, int position) {
+ playNow(entries, position, null, null);
+ }
+ protected void playNow(List<Entry> entries, int position, String playlistName, String playlistId) {
+ Entry selected = entries.isEmpty() ? null : entries.get(0);
+ playNow(entries, selected, position, playlistName, playlistId);
+ }
+ protected void playNow(List<Entry> entries, Entry song, int position) {
+ playNow(entries, song, position, null, null);
+ }
+ protected void playNow(final List<Entry> entries, final Entry song, final int position, final String playlistName, final String playlistId) {
+ new LoadingTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ DownloadService downloadService = getDownloadService();
+ if(downloadService == null) {
+ return null;
+ }
+
+ downloadService.clear();
+ downloadService.download(entries, false, true, true, false, entries.indexOf(song), position);
+ downloadService.setSuggestedPlaylistName(playlistName, playlistId);
+
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ Util.startActivityWithoutTransition(context, DownloadActivity.class);
+ }
+ }.execute();
+ }
+
+ protected void deleteBookmark(final MusicDirectory.Entry entry, final ArrayAdapter adapter) {
+ Util.confirmDialog(context, R.string.bookmark_delete_title, entry.getTitle(), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ final Bookmark oldBookmark = entry.getBookmark();
+ entry.setBookmark(null);
+
+ new LoadingTask<Void>(context, false) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.deleteBookmark(entry, context, null);
+
+ new EntryInstanceUpdater(entry) {
+ @Override
+ public void update(Entry found) {
+ found.setBookmark(null);
+ }
+ }.execute();
+
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ if (adapter != null) {
+ adapter.remove(entry);
+ adapter.notifyDataSetChanged();
+ }
+ Util.toast(context, context.getResources().getString(R.string.bookmark_deleted, entry.getTitle()));
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ entry.setBookmark(oldBookmark);
+
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.bookmark_deleted_error, entry.getTitle()) + " " + getErrorMessage(error);
+ }
+
+ Util.toast(context, msg, false);
+ }
+ }.execute();
+ }
+ });
+ }
+
+ protected void setRating(Entry entry) {
+ setRating(entry, null);
+ }
+ protected void setRating(final Entry entry, final OnRatingChange onRatingChange) {
+ View layout = context.getLayoutInflater().inflate(R.layout.rating, null);
+ final RatingBar ratingBar = (RatingBar) layout.findViewById(R.id.rating_bar);
+ ratingBar.setRating((float) entry.getRating());
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(context.getResources().getString(R.string.rating_title, entry.getTitle()))
+ .setView(layout)
+ .setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ int rating = (int) ratingBar.getRating();
+ setRating(entry, rating, onRatingChange);
+ }
+ })
+ .setNegativeButton(R.string.common_cancel, null);
+
+ AlertDialog dialog = builder.create();
+ dialog.show();
+ }
+
+ protected void setRating(Entry entry, int rating) {
+ setRating(entry, rating, null);
+ }
+ protected void setRating(final Entry entry, final int rating, final OnRatingChange onRatingChange) {
+ final int oldRating = entry.getRating();
+ entry.setRating(rating);
+
+ if(onRatingChange != null) {
+ onRatingChange.ratingChange(rating);
+ }
+
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.setRating(entry, rating, context, null);
+
+ new EntryInstanceUpdater(entry) {
+ @Override
+ public void update(Entry found) {
+ found.setRating(rating);
+ }
+ }.execute();
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ Util.toast(context, context.getResources().getString(rating > 0 ? R.string.rating_set_rating : R.string.rating_remove_rating, entry.getTitle()));
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ entry.setRating(oldRating);
+ if(onRatingChange != null) {
+ onRatingChange.ratingChange(oldRating);
+ }
+
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(rating > 0 ? R.string.rating_set_rating_failed : R.string.rating_remove_rating_failed, entry.getTitle()) + " " + getErrorMessage(error);
+ }
+
+ Util.toast(context, msg, false);
+ }
+ }.execute();
+ }
+
+ protected abstract class EntryInstanceUpdater {
+ private Entry entry;
+
+ public EntryInstanceUpdater(Entry entry) {
+ this.entry = entry;
+ }
+
+ public abstract void update(Entry found);
+
+ public void execute() {
+ DownloadService downloadService = getDownloadService();
+ if(downloadService != null && !entry.isDirectory()) {
+ boolean serializeChanges = false;
+ List<DownloadFile> downloadFiles = downloadService.getDownloads();
+ for(DownloadFile file: downloadFiles) {
+ Entry check = file.getSong();
+ if(entry.getId().equals(check.getId())) {
+ update(entry);
+ serializeChanges = true;
+ }
+ }
+
+ if(serializeChanges) {
+ downloadService.serializeQueue();
+ }
+ }
+
+ Entry find = UpdateView.findEntry(entry);
+ if(find != null) {
+ update(find);
+ }
+ }
+ }
+
+ public abstract class OnRatingChange {
+ abstract void ratingChange(int rating);
+ }
+ public abstract class OnStarChange {
+ abstract void starChange(boolean starred);
+ }
+
+ public abstract class RecursiveLoader extends LoadingTask<Boolean> {
+ protected MusicService musicService;
+ protected static final int MAX_SONGS = 500;
+ protected boolean playNowOverride = false;
+ protected List<Entry> songs;
+
+ public RecursiveLoader(Activity context) {
+ super(context);
+ }
+
+ protected void getSongsRecursively(MusicDirectory parent, List<Entry> songs) throws Exception {
+ if (songs.size() > MAX_SONGS) {
+ return;
+ }
+
+ for (Entry song : parent.getChildren(false, true)) {
+ if (!song.isVideo() && song.getRating() != 1) {
+ songs.add(song);
+ }
+ }
+ for (Entry dir : parent.getChildren(true, false)) {
+ if(dir.getRating() == 1) {
+ continue;
+ }
+
+ MusicDirectory musicDirectory;
+ if(Util.isTagBrowsing(context) && !Util.isOffline(context)) {
+ musicDirectory = musicService.getAlbum(dir.getId(), dir.getTitle(), false, context, this);
+ } else {
+ musicDirectory = musicService.getMusicDirectory(dir.getId(), dir.getTitle(), false, context, this);
+ }
+ getSongsRecursively(musicDirectory, songs);
+ }
+ }
+
+ @Override
+ protected void done(Boolean result) {
+ warnIfStorageUnavailable();
+
+ if(playNowOverride) {
+ playNow(songs);
+ return;
+ }
+
+ if(result) {
+ Util.startActivityWithoutTransition(context, DownloadActivity.class);
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/UserFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/UserFragment.java
new file mode 100644
index 00000000..00c7c603
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/fragments/UserFragment.java
@@ -0,0 +1,125 @@
+/*
+ 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.fragments;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.support.v4.widget.SwipeRefreshLayout;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.activity.SubsonicActivity;
+import github.daneren2005.dsub.domain.ServerInfo;
+import github.daneren2005.dsub.domain.User;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.ImageLoader;
+import github.daneren2005.dsub.util.UserUtil;
+import github.daneren2005.dsub.adapter.SettingsAdapter;
+
+public class UserFragment extends SubsonicFragment{
+ private ListView listView;
+ private User user;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
+ rootView = inflater.inflate(R.layout.abstract_list_fragment, container, false);
+
+ refreshLayout = (SwipeRefreshLayout) rootView.findViewById(R.id.refresh_layout);
+ refreshLayout.setEnabled(false);
+
+ Bundle args = getArguments();
+ user = (User) args.getSerializable(Constants.INTENT_EXTRA_NAME_ID);
+
+ listView = (ListView)rootView.findViewById(R.id.fragment_list);
+ createHeader();
+ listView.setAdapter(new SettingsAdapter(context, user.getSettings(), UserUtil.isCurrentAdmin() && ServerInfo.checkServerVersion(context, "1.10")));
+
+ setTitle(user.getUsername());
+
+ return rootView;
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ ((SubsonicActivity) activity).supportInvalidateOptionsMenu();
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) {
+ // For some reason this is called before onAttach
+ if(!primaryFragment || context == null) {
+ return;
+ }
+
+ if(UserUtil.isCurrentAdmin() && ServerInfo.checkServerVersion(context, "1.10")) {
+ menuInflater.inflate(R.menu.user, menu);
+ } else if(UserUtil.isCurrentRole(User.SETTINGS)) {
+ menuInflater.inflate(R.menu.user_user, menu);
+ } else {
+ menuInflater.inflate(R.menu.empty, menu);
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if(super.onOptionsItemSelected(item)) {
+ return true;
+ }
+
+ switch (item.getItemId()) {
+ case R.id.menu_update_permissions:
+ UserUtil.updateSettings(context, user);
+ return true;
+ case R.id.menu_change_password:
+ UserUtil.changePassword(context, user);
+ return true;
+ case R.id.menu_change_email:
+ UserUtil.changeEmail(context, user);
+ return true;
+ }
+
+ return false;
+ }
+
+ private void createHeader() {
+ View header = LayoutInflater.from(context).inflate(R.layout.user_header, listView, false);
+
+ final ImageLoader imageLoader = getImageLoader();
+ ImageView coverArtView = (ImageView) header.findViewById(R.id.user_avatar);
+ imageLoader.loadAvatar(context, coverArtView, user.getUsername());
+
+ TextView usernameView = (TextView) header.findViewById(R.id.user_username);
+ usernameView.setText(user.getUsername());
+
+ final TextView emailView = (TextView) header.findViewById(R.id.user_email);
+ if(user.getEmail() != null) {
+ emailView.setText(user.getEmail());
+ } else {
+ emailView.setVisibility(View.GONE);
+ }
+
+ listView.addHeaderView(header);
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/provider/DLNARouteProvider.java b/app/src/main/java/github/daneren2005/dsub/provider/DLNARouteProvider.java
new file mode 100644
index 00000000..73d4b5de
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/provider/DLNARouteProvider.java
@@ -0,0 +1,425 @@
+/*
+ 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.provider;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.media.AudioManager;
+import android.media.MediaRouter;
+import android.os.IBinder;
+import android.support.v7.media.MediaControlIntent;
+import android.support.v7.media.MediaRouteDescriptor;
+import android.support.v7.media.MediaRouteDiscoveryRequest;
+import android.support.v7.media.MediaRouteProvider;
+import android.support.v7.media.MediaRouteProviderDescriptor;
+import android.util.Log;
+
+import org.eclipse.jetty.util.log.Logger;
+import org.fourthline.cling.android.AndroidUpnpService;
+import org.fourthline.cling.android.AndroidUpnpServiceImpl;
+import org.fourthline.cling.model.action.ActionInvocation;
+import org.fourthline.cling.model.message.UpnpResponse;
+import org.fourthline.cling.model.meta.Device;
+import org.fourthline.cling.model.meta.LocalDevice;
+import org.fourthline.cling.model.meta.RemoteDevice;
+import org.fourthline.cling.model.meta.StateVariable;
+import org.fourthline.cling.model.meta.StateVariableAllowedValueRange;
+import org.fourthline.cling.model.types.ServiceType;
+import org.fourthline.cling.registry.Registry;
+import org.fourthline.cling.registry.RegistryListener;
+import org.fourthline.cling.support.renderingcontrol.callback.GetVolume;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import github.daneren2005.dsub.domain.DLNADevice;
+import github.daneren2005.dsub.domain.RemoteControlState;
+import github.daneren2005.dsub.service.DLNAController;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.service.RemoteController;
+
+public class DLNARouteProvider extends MediaRouteProvider {
+ private static final String TAG = DLNARouteProvider.class.getSimpleName();
+ public static final String CATEGORY_DLNA = "github.daneren2005.dsub.DLNA";
+
+ private DownloadService downloadService;
+ private RemoteController controller;
+
+ private HashMap<String, DLNADevice> devices = new HashMap<String, DLNADevice>();
+ private List<String> adding = new ArrayList<String>();
+ private List<String> removing = new ArrayList<String>();
+ private AndroidUpnpService dlnaService;
+ private ServiceConnection dlnaServiceConnection;
+ private boolean searchOnConnect = false;
+
+ public DLNARouteProvider(Context context) {
+ super(context);
+
+ // Use custom logger
+ org.eclipse.jetty.util.log.Log.setLog(new JettyAndroidLog());
+
+ this.downloadService = (DownloadService) context;
+ dlnaServiceConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ dlnaService = (AndroidUpnpService) service;
+ dlnaService.getRegistry().addListener(new RegistryListener() {
+ @Override
+ public void remoteDeviceDiscoveryStarted(Registry registry, RemoteDevice remoteDevice) {
+
+ }
+
+ @Override
+ public void remoteDeviceDiscoveryFailed(Registry registry, RemoteDevice remoteDevice, Exception e) {
+ // Error is displayed in log anyways under W/trieveRemoteDescriptors
+ }
+
+ @Override
+ public void remoteDeviceAdded(Registry registry, RemoteDevice remoteDevice) {
+ deviceAdded(remoteDevice);
+ }
+
+ @Override
+ public void remoteDeviceUpdated(Registry registry, RemoteDevice remoteDevice) {
+ deviceAdded(remoteDevice);
+ }
+
+ @Override
+ public void remoteDeviceRemoved(Registry registry, RemoteDevice remoteDevice) {
+ deviceRemoved(remoteDevice);
+ }
+
+ @Override
+ public void localDeviceAdded(Registry registry, LocalDevice localDevice) {
+ deviceAdded(localDevice);
+ }
+
+ @Override
+ public void localDeviceRemoved(Registry registry, LocalDevice localDevice) {
+ deviceRemoved(localDevice);
+ }
+
+ @Override
+ public void beforeShutdown(Registry registry) {
+
+ }
+
+ @Override
+ public void afterShutdown() {
+
+ }
+ });
+
+ for (Device<?, ?, ?> device : dlnaService.getControlPoint().getRegistry().getDevices()) {
+ deviceAdded(device);
+ }
+ if(searchOnConnect) {
+ dlnaService.getControlPoint().search();
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ dlnaService = null;
+ }
+ };
+
+ if(!context.getApplicationContext().bindService(new Intent(context, AndroidUpnpServiceImpl.class), dlnaServiceConnection, Context.BIND_AUTO_CREATE)) {
+ Log.e(TAG, "Failed to bind to DLNA service");
+ }
+ }
+
+ private void broadcastDescriptors() {
+ // Create intents
+ IntentFilter routeIntentFilter = new IntentFilter();
+ routeIntentFilter.addCategory(CATEGORY_DLNA);
+ routeIntentFilter.addAction(MediaControlIntent.ACTION_START_SESSION);
+ routeIntentFilter.addAction(MediaControlIntent.ACTION_GET_SESSION_STATUS);
+ routeIntentFilter.addAction(MediaControlIntent.ACTION_END_SESSION);
+
+ // Create descriptor
+ MediaRouteProviderDescriptor.Builder providerBuilder = new MediaRouteProviderDescriptor.Builder();
+
+ // Create route descriptor
+ for(Map.Entry<String, DLNADevice> deviceEntry: devices.entrySet()) {
+ DLNADevice device = deviceEntry.getValue();
+
+ int volume;
+ if(device.volumeMax == 0) {
+ volume = 5;
+ } else {
+ int increments = device.volumeMax / 10;
+ volume = controller == null ? device.volume : (int) controller.getVolume();
+ volume = volume / increments;
+ }
+
+ MediaRouteDescriptor.Builder routeBuilder = new MediaRouteDescriptor.Builder(device.id, device.name);
+ routeBuilder.addControlFilter(routeIntentFilter)
+ .setPlaybackStream(AudioManager.STREAM_MUSIC)
+ .setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE)
+ .setDescription(device.description)
+ .setVolume(volume)
+ .setVolumeMax(10)
+ .setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE);
+ providerBuilder.addRoute(routeBuilder.build());
+ }
+
+ setDescriptor(providerBuilder.build());
+ }
+
+ @Override
+ public void onDiscoveryRequestChanged(MediaRouteDiscoveryRequest request) {
+ if (request != null && request.isActiveScan()) {
+ if(dlnaService != null) {
+ dlnaService.getControlPoint().search();
+ } else {
+ searchOnConnect = true;
+ }
+ }
+ }
+
+ @Override
+ public RouteController onCreateRouteController(String routeId) {
+ DLNADevice device = devices.get(routeId);
+ if(device == null) {
+ Log.w(TAG, "No device exists for " + routeId);
+ return null;
+ }
+
+ return new DLNARouteController(device);
+ }
+
+ private void deviceAdded(final Device device) {
+ final org.fourthline.cling.model.meta.Service renderingControl = device.findService(new ServiceType("schemas-upnp-org", "RenderingControl"));
+ if(renderingControl == null) {
+ return;
+ }
+
+ final String id = device.getIdentity().getUdn().toString();
+ // In the process of looking up it's details already
+ if(adding.contains(id)) {
+ return;
+ }
+ // Just a temp disconnect, already have it's info
+ if(removing.contains(id)) {
+ removing.remove(id);
+ return;
+ }
+ adding.add(id);
+
+ if(device.getType().getType().equals("MediaRenderer") && device instanceof RemoteDevice) {
+ try {
+ dlnaService.getControlPoint().execute(new GetVolume(renderingControl) {
+ @Override
+ public void received(ActionInvocation actionInvocation, int currentVolume) {
+ int maxVolume = 100;
+ StateVariable volume = renderingControl.getStateVariable("Volume");
+ if (volume != null) {
+ StateVariableAllowedValueRange volumeRange = volume.getTypeDetails().getAllowedValueRange();
+ maxVolume = (int) volumeRange.getMaximum();
+ }
+
+ // Create a new DLNADevice to represent this item
+ String id = device.getIdentity().getUdn().toString();
+ String name = device.getDetails().getFriendlyName();
+ String displayName = device.getDisplayString();
+
+ DLNADevice newDevice = new DLNADevice(device, id, name, displayName, currentVolume, maxVolume);
+ devices.put(id, newDevice);
+ downloadService.post(new Runnable() {
+ @Override
+ public void run() {
+ broadcastDescriptors();
+ }
+ });
+ adding.remove(id);
+ }
+
+ @Override
+ public void failure(ActionInvocation actionInvocation, UpnpResponse upnpResponse, String s) {
+ Log.w(TAG, "Failed to get default volume for DLNA route");
+ Log.w(TAG, "Reason: " + s);
+ adding.remove(id);
+ }
+ });
+ } catch(Exception e) {
+ Log.e(TAG, "Failed to add device", e);
+ }
+ } else {
+ adding.remove(id);
+ }
+ }
+ private void deviceRemoved(Device device) {
+ if(device.getType().getType().equals("MediaRenderer") && device instanceof RemoteDevice) {
+ final String id = device.getIdentity().getUdn().toString();
+ removing.add(id);
+
+ // Delay removal for a few seconds to make sure that it isn't just a temp disconnect
+ dlnaService.getControlPoint().search();
+ downloadService.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if(removing.contains(id)) {
+ devices.remove(id);
+ removing.remove(id);
+ broadcastDescriptors();
+ }
+ }
+ }, 5000L);
+ }
+ }
+
+ private class DLNARouteController extends RouteController {
+ private DLNADevice device;
+
+ public DLNARouteController(DLNADevice device) {
+ this.device = device;
+ }
+
+ @Override
+ public boolean onControlRequest(Intent intent, android.support.v7.media.MediaRouter.ControlRequestCallback callback) {
+ if (intent.hasCategory(CATEGORY_DLNA)) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public void onRelease() {
+ downloadService.setRemoteEnabled(RemoteControlState.LOCAL);
+ controller = null;
+ }
+
+ @Override
+ public void onSelect() {
+ controller = new DLNAController(downloadService, dlnaService.getControlPoint(), device);
+ downloadService.setRemoteEnabled(RemoteControlState.DLNA, controller);
+ }
+
+ @Override
+ public void onUnselect() {
+ downloadService.setRemoteEnabled(RemoteControlState.LOCAL);
+ controller = null;
+ }
+
+ @Override
+ public void onUpdateVolume(int delta) {
+ if(controller != null) {
+ controller.updateVolume(delta > 0);
+ }
+ broadcastDescriptors();
+ }
+
+ @Override
+ public void onSetVolume(int volume) {
+ if(controller != null) {
+ controller.setVolume(volume);
+ }
+ broadcastDescriptors();
+ }
+ }
+
+ public static class JettyAndroidLog implements Logger {
+ final private static java.util.logging.Logger log = java.util.logging.Logger.getLogger("Jetty");
+
+ public static boolean __isIgnoredEnabled = false;
+ public String _name;
+
+ public JettyAndroidLog() {
+ this (JettyAndroidLog.class.getName());
+ }
+
+ public JettyAndroidLog(String name) {
+ _name = name;
+ }
+
+ public String getName () {
+ return _name;
+ }
+
+ public void debug(Throwable th) {
+ // Log.d(TAG, "", th);
+ }
+
+ public void debug(String msg, Throwable th) {
+ // Log.d(TAG, msg, th);
+ }
+
+ public void debug(String msg, Object... args) {
+ // Log.d(TAG, msg);
+ }
+
+ public Logger getLogger(String name) {
+ return new JettyAndroidLog(name);
+ }
+
+ public void info(String msg, Object... args) {
+ // Log.i(TAG, msg);
+ }
+
+ public void info(Throwable th) {
+ // Log.i(TAG, "", th);
+ }
+
+ public void info(String msg, Throwable th) {
+ // Log.i(TAG, msg, th);
+ }
+
+ public boolean isDebugEnabled() {
+ return false;
+ }
+
+ public void warn(Throwable th) {
+ // Log.w(TAG, "", th);
+ }
+
+ public void warn(String msg, Object... args) {
+ // Log.w(TAG, msg);
+ }
+
+ public void warn(String msg, Throwable th) {
+ // Log.w(TAG, msg, th);
+ }
+
+ public boolean isIgnoredEnabled () {
+ return __isIgnoredEnabled;
+ }
+
+
+ public void ignore(Throwable ignored) {
+ if (__isIgnoredEnabled) {
+ warn("IGNORED", ignored);
+ }
+ }
+
+ public void setIgnoredEnabled(boolean enabled) {
+ __isIgnoredEnabled = enabled;
+ }
+
+ public void setDebugEnabled(boolean enabled) {
+
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/provider/DSubSearchProvider.java b/app/src/main/java/github/daneren2005/dsub/provider/DSubSearchProvider.java
new file mode 100644
index 00000000..63bbaaa4
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/provider/DSubSearchProvider.java
@@ -0,0 +1,191 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.provider;
+
+import android.app.SearchManager;
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.Artist;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.SearchCritera;
+import github.daneren2005.dsub.domain.SearchResult;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import github.daneren2005.dsub.util.Util;
+
+/**
+ * Provides search suggestions based on recent searches.
+ *
+ * @author Sindre Mehus
+ */
+public class DSubSearchProvider extends ContentProvider {
+ private static final String TAG = DSubSearchProvider.class.getSimpleName();
+
+ private static final String RESOURCE_PREFIX = "android.resource://github.daneren2005.dsub/";
+ private static final String[] COLUMNS = {"_id",
+ SearchManager.SUGGEST_COLUMN_TEXT_1,
+ SearchManager.SUGGEST_COLUMN_TEXT_2,
+ SearchManager.SUGGEST_COLUMN_INTENT_DATA,
+ SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA,
+ SearchManager.SUGGEST_COLUMN_ICON_1};
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
+ String query = selectionArgs[0] + "*";
+ SearchResult searchResult = search(query);
+ return createCursor(selectionArgs[0], searchResult);
+ }
+
+ private SearchResult search(String query) {
+ MusicService musicService = MusicServiceFactory.getMusicService(getContext());
+ if (musicService == null) {
+ return null;
+ }
+
+ try {
+ return musicService.search(new SearchCritera(query, 5, 10, 10), getContext(), null);
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ private Cursor createCursor(String query, SearchResult searchResult) {
+ MatrixCursor cursor = new MatrixCursor(COLUMNS);
+ if (searchResult == null) {
+ return cursor;
+ }
+
+ // Add all results into one pot
+ List<Object> results = new ArrayList<Object>();
+ results.addAll(searchResult.getArtists());
+ results.addAll(searchResult.getAlbums());
+ results.addAll(searchResult.getSongs());
+
+ // For each, calculate its string distance to the query
+ for(Object obj: results) {
+ if(obj instanceof Artist) {
+ Artist artist = (Artist) obj;
+ artist.setCloseness(Util.getStringDistance(query, artist.getName()));
+ } else {
+ MusicDirectory.Entry entry = (MusicDirectory.Entry) obj;
+ entry.setCloseness(Util.getStringDistance(query, entry.getTitle()));
+ }
+ }
+
+ // Sort based on the closeness paramater
+ Collections.sort(results, new Comparator<Object>() {
+ @Override
+ public int compare(Object lhs, Object rhs) {
+ // Get the closeness of the two objects
+ int left, right;
+ boolean leftArtist = lhs instanceof Artist;
+ boolean rightArtist = rhs instanceof Artist;
+ if (leftArtist) {
+ left = ((Artist) lhs).getCloseness();
+ } else {
+ left = ((MusicDirectory.Entry) lhs).getCloseness();
+ }
+ if (rightArtist) {
+ right = ((Artist) rhs).getCloseness();
+ } else {
+ right = ((MusicDirectory.Entry) rhs).getCloseness();
+ }
+
+ if (left == right) {
+ if(leftArtist && rightArtist) {
+ return 0;
+ } else if(leftArtist) {
+ return -1;
+ } else if(rightArtist) {
+ return 1;
+ } else {
+ return 0;
+ }
+ } else if (left > right) {
+ return 1;
+ } else {
+ return -1;
+ }
+ }
+ });
+
+ // Done sorting, add results to cursor
+ for(Object obj: results) {
+ if(obj instanceof Artist) {
+ Artist artist = (Artist) obj;
+ String icon = RESOURCE_PREFIX + R.drawable.ic_action_artist;
+ cursor.addRow(new Object[]{artist.getId().hashCode(), artist.getName(), null, "ar-" + artist.getId(), artist.getName(), icon});
+ } else {
+ MusicDirectory.Entry entry = (MusicDirectory.Entry) obj;
+
+ if(entry.isDirectory()) {
+ String icon = RESOURCE_PREFIX + R.drawable.ic_action_album;
+ cursor.addRow(new Object[]{entry.getId().hashCode(), entry.getTitle(), entry.getArtist(), entry.getId(), entry.getTitle(), icon});
+ } else {
+ String icon = RESOURCE_PREFIX + R.drawable.ic_action_song;
+ String id;
+ if(Util.isTagBrowsing(getContext())) {
+ id = entry.getAlbumId();
+ } else {
+ id = entry.getParent();
+ }
+ cursor.addRow(new Object[]{entry.getId().hashCode(), entry.getTitle(), entry.getArtist(), "so-" + id, entry.getTitle(), icon});
+ }
+ }
+ }
+ return cursor;
+ }
+
+ @Override
+ public boolean onCreate() {
+ return false;
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ return null;
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues contentValues) {
+ return null;
+ }
+
+ @Override
+ public int delete(Uri uri, String s, String[] strings) {
+ return 0;
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues contentValues, String s, String[] strings) {
+ return 0;
+ }
+
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/provider/DSubWidget4x1.java b/app/src/main/java/github/daneren2005/dsub/provider/DSubWidget4x1.java
new file mode 100644
index 00000000..c78257c4
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/provider/DSubWidget4x1.java
@@ -0,0 +1,28 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.provider;
+
+import github.daneren2005.dsub.R;
+
+public class DSubWidget4x1 extends DSubWidgetProvider {
+ @Override
+ protected int getLayout() {
+ return R.layout.appwidget4x1;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/provider/DSubWidget4x2.java b/app/src/main/java/github/daneren2005/dsub/provider/DSubWidget4x2.java
new file mode 100644
index 00000000..4c7d637c
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/provider/DSubWidget4x2.java
@@ -0,0 +1,28 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.provider;
+
+import github.daneren2005.dsub.R;
+
+public class DSubWidget4x2 extends DSubWidgetProvider {
+ @Override
+ protected int getLayout() {
+ return R.layout.appwidget4x2;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/provider/DSubWidget4x3.java b/app/src/main/java/github/daneren2005/dsub/provider/DSubWidget4x3.java
new file mode 100644
index 00000000..b4f91a45
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/provider/DSubWidget4x3.java
@@ -0,0 +1,28 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.provider;
+
+import github.daneren2005.dsub.R;
+
+public class DSubWidget4x3 extends DSubWidgetProvider {
+ @Override
+ protected int getLayout() {
+ return R.layout.appwidget4x3;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/provider/DSubWidget4x4.java b/app/src/main/java/github/daneren2005/dsub/provider/DSubWidget4x4.java
new file mode 100644
index 00000000..a591ff29
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/provider/DSubWidget4x4.java
@@ -0,0 +1,28 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.provider;
+
+import github.daneren2005.dsub.R;
+
+public class DSubWidget4x4 extends DSubWidgetProvider {
+ @Override
+ protected int getLayout() {
+ return R.layout.appwidget4x4;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/provider/DSubWidgetProvider.java b/app/src/main/java/github/daneren2005/dsub/provider/DSubWidgetProvider.java
new file mode 100644
index 00000000..444b6cff
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/provider/DSubWidgetProvider.java
@@ -0,0 +1,304 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.provider;
+
+import android.app.PendingIntent;
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProvider;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.content.SharedPreferences;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Environment;
+import android.util.Log;
+import android.view.View;
+import android.widget.RemoteViews;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.activity.DownloadActivity;
+import github.daneren2005.dsub.activity.SubsonicActivity;
+import github.daneren2005.dsub.activity.SubsonicFragmentActivity;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.PlayerQueue;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.service.DownloadServiceLifecycleSupport;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.ImageLoader;
+import github.daneren2005.dsub.util.Util;
+
+/**
+ * Simple widget to show currently playing album art along
+ * with play/pause and next track buttons.
+ * <p/>
+ * Based on source code from the stock Android Music app.
+ *
+ * @author Sindre Mehus
+ */
+public class DSubWidgetProvider extends AppWidgetProvider {
+ private static final String TAG = DSubWidgetProvider.class.getSimpleName();
+ private static DSubWidget4x1 instance4x1;
+ private static DSubWidget4x2 instance4x2;
+ private static DSubWidget4x3 instance4x3;
+ private static DSubWidget4x4 instance4x4;
+
+ public static synchronized void notifyInstances(Context context, DownloadService service, boolean playing) {
+ if(instance4x1 == null) {
+ instance4x1 = new DSubWidget4x1();
+ }
+ if(instance4x2 == null) {
+ instance4x2 = new DSubWidget4x2();
+ }
+ if(instance4x3 == null) {
+ instance4x3 = new DSubWidget4x3();
+ }
+ if(instance4x4 == null) {
+ instance4x4 = new DSubWidget4x4();
+ }
+
+ instance4x1.notifyChange(context, service, playing);
+ instance4x2.notifyChange(context, service, playing);
+ instance4x3.notifyChange(context, service, playing);
+ instance4x4.notifyChange(context, service, playing);
+ }
+
+ @Override
+ public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
+ defaultAppWidget(context, appWidgetIds);
+ }
+
+ @Override
+ public void onEnabled(Context context) {
+ notifyInstances(context, DownloadService.getInstance(), false);
+ }
+
+ protected int getLayout() {
+ return 0;
+ }
+
+ /**
+ * Initialize given widgets to default state, where we launch Subsonic on default click
+ * and hide actions if service not running.
+ */
+ private void defaultAppWidget(Context context, int[] appWidgetIds) {
+ final Resources res = context.getResources();
+ final RemoteViews views = new RemoteViews(context.getPackageName(), getLayout());
+
+ views.setTextViewText(R.id.artist, res.getText(R.string.widget_initial_text));
+ if(getLayout() == R.layout.appwidget4x2) {
+ views.setTextViewText(R.id.album, "");
+ }
+
+ linkButtons(context, views, false);
+ performUpdate(context, null, appWidgetIds, false);
+ }
+
+ private void pushUpdate(Context context, int[] appWidgetIds, RemoteViews views) {
+ // Update specific list of appWidgetIds if given, otherwise default to all
+ final AppWidgetManager manager = AppWidgetManager.getInstance(context);
+ if (appWidgetIds != null) {
+ manager.updateAppWidget(appWidgetIds, views);
+ } else {
+ manager.updateAppWidget(new ComponentName(context, this.getClass()), views);
+ }
+ }
+
+ /**
+ * Handle a change notification coming over from {@link DownloadService}
+ */
+ public void notifyChange(Context context, DownloadService service, boolean playing) {
+ if (hasInstances(context)) {
+ performUpdate(context, service, null, playing);
+ }
+ }
+
+ /**
+ * Check against {@link AppWidgetManager} if there are any instances of this widget.
+ */
+ private boolean hasInstances(Context context) {
+ AppWidgetManager manager = AppWidgetManager.getInstance(context);
+ int[] appWidgetIds = manager.getAppWidgetIds(new ComponentName(context, getClass()));
+ return (appWidgetIds.length > 0);
+ }
+
+ /**
+ * Update all active widget instances by pushing changes
+ */
+ private void performUpdate(Context context, DownloadService service, int[] appWidgetIds, boolean playing) {
+ final Resources res = context.getResources();
+ final RemoteViews views = new RemoteViews(context.getPackageName(), getLayout());
+
+ if(playing) {
+ views.setViewVisibility(R.id.widget_root, View.VISIBLE);
+ } else {
+ // Hide widget
+ SharedPreferences prefs = Util.getPreferences(context);
+ if(prefs.getBoolean(Constants.PREFERENCES_KEY_HIDE_WIDGET, false)) {
+ views.setViewVisibility(R.id.widget_root, View.GONE);
+ }
+ }
+
+ // Get Entry from current playing DownloadFile
+ MusicDirectory.Entry currentPlaying = null;
+ if(service == null) {
+ // Deserialize from playling list to setup
+ PlayerQueue state = FileUtil.deserialize(context, DownloadServiceLifecycleSupport.FILENAME_DOWNLOADS_SER, PlayerQueue.class);
+ if(state != null && state.currentPlayingIndex != -1) {
+ currentPlaying = state.songs.get(state.currentPlayingIndex);
+ }
+ } else {
+ currentPlaying = service.getCurrentPlaying() == null ? null : service.getCurrentPlaying().getSong();
+ }
+
+ String title = currentPlaying == null ? null : currentPlaying.getTitle();
+ CharSequence artist = currentPlaying == null ? null : currentPlaying.getArtist();
+ CharSequence album = currentPlaying == null ? null : currentPlaying.getAlbum();
+ CharSequence errorState = null;
+
+ // Show error message?
+ String status = Environment.getExternalStorageState();
+ if (status.equals(Environment.MEDIA_SHARED) ||
+ status.equals(Environment.MEDIA_UNMOUNTED)) {
+ errorState = res.getText(R.string.widget_sdcard_busy);
+ } else if (status.equals(Environment.MEDIA_REMOVED)) {
+ errorState = res.getText(R.string.widget_sdcard_missing);
+ } else if (currentPlaying == null) {
+ errorState = res.getText(R.string.widget_initial_text);
+ }
+
+ if (errorState != null) {
+ // Show error state to user
+ views.setTextViewText(R.id.title,null);
+ views.setTextViewText(R.id.artist, errorState);
+ views.setTextViewText(R.id.album, "");
+ if(getLayout() != R.layout.appwidget4x1) {
+ views.setImageViewResource(R.id.appwidget_coverart, R.drawable.appwidget_art_default);
+ }
+ } else {
+ // No error, so show normal titles
+ views.setTextViewText(R.id.title, title);
+ views.setTextViewText(R.id.artist, artist);
+ if(getLayout() != R.layout.appwidget4x1) {
+ views.setTextViewText(R.id.album, album);
+ }
+ }
+
+ // Set correct drawable for pause state
+ if (playing) {
+ views.setImageViewResource(R.id.control_play, R.drawable.ic_appwidget_music_pause);
+ } else {
+ views.setImageViewResource(R.id.control_play, R.drawable.ic_appwidget_music_play);
+ }
+
+ // Set the cover art
+ try {
+ boolean large = false;
+ if(getLayout() != R.layout.appwidget4x1 && getLayout() != R.layout.appwidget4x2) {
+ large = true;
+ }
+ ImageLoader imageLoader = SubsonicActivity.getStaticImageLoader(context);
+ Bitmap bitmap = imageLoader == null ? null : imageLoader.getCachedImage(context, currentPlaying, large);
+
+ if (bitmap == null) {
+ // Set default cover art
+ views.setImageViewResource(R.id.appwidget_coverart, R.drawable.appwidget_art_unknown);
+ } else {
+ bitmap = getRoundedCornerBitmap(bitmap);
+ views.setImageViewBitmap(R.id.appwidget_coverart, bitmap);
+ }
+ } catch (Exception x) {
+ Log.e(TAG, "Failed to load cover art", x);
+ views.setImageViewResource(R.id.appwidget_coverart, R.drawable.appwidget_art_unknown);
+ }
+
+ // Link actions buttons to intents
+ linkButtons(context, views, currentPlaying != null);
+
+ pushUpdate(context, appWidgetIds, views);
+ }
+
+ /**
+ * Round the corners of a bitmap for the cover art image
+ */
+ private static Bitmap getRoundedCornerBitmap(Bitmap bitmap) {
+ Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Config.ARGB_8888);
+ Canvas canvas = new Canvas(output);
+
+ final int color = 0xff424242;
+ final Paint paint = new Paint();
+ final float roundPx = 10;
+
+ // Add extra width to the rect so the right side wont be rounded.
+ final Rect rect = new Rect(0, 0, bitmap.getWidth() + (int) roundPx, bitmap.getHeight());
+ final RectF rectF = new RectF(rect);
+
+ paint.setAntiAlias(true);
+ canvas.drawARGB(0, 0, 0, 0);
+ paint.setColor(color);
+ canvas.drawRoundRect(rectF, roundPx, roundPx, paint);
+
+ paint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN));
+ canvas.drawBitmap(bitmap, rect, rect, paint);
+
+ return output;
+ }
+
+ /**
+ * Link up various button actions using {@link PendingIntent}.
+ *
+ * @param playerActive True if player is active in background, which means
+ * widget click will launch {@link DownloadActivity},
+ * otherwise we launch {@link github.daneren2005.dsub.activity.SubsonicFragmentActivity}.
+ */
+ private void linkButtons(Context context, RemoteViews views, boolean playerActive) {
+ Intent intent = new Intent(context, SubsonicFragmentActivity.class);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD, true);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
+ views.setOnClickPendingIntent(R.id.appwidget_coverart, pendingIntent);
+ views.setOnClickPendingIntent(R.id.appwidget_top, pendingIntent);
+
+ // Emulate media button clicks.
+ intent = new Intent("DSub.PLAY_PAUSE");
+ intent.setComponent(new ComponentName(context, DownloadService.class));
+ intent.setAction(DownloadService.CMD_TOGGLEPAUSE);
+ pendingIntent = PendingIntent.getService(context, 0, intent, 0);
+ views.setOnClickPendingIntent(R.id.control_play, pendingIntent);
+
+ intent = new Intent("DSub.NEXT"); // Use a unique action name to ensure a different PendingIntent to be created.
+ intent.setComponent(new ComponentName(context, DownloadService.class));
+ intent.setAction(DownloadService.CMD_NEXT);
+ pendingIntent = PendingIntent.getService(context, 0, intent, 0);
+ views.setOnClickPendingIntent(R.id.control_next, pendingIntent);
+
+ intent = new Intent("DSub.PREVIOUS"); // Use a unique action name to ensure a different PendingIntent to be created.
+ intent.setComponent(new ComponentName(context, DownloadService.class));
+ intent.setAction(DownloadService.CMD_PREVIOUS);
+ pendingIntent = PendingIntent.getService(context, 0, intent, 0);
+ views.setOnClickPendingIntent(R.id.control_previous, pendingIntent);
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/provider/JukeboxRouteProvider.java b/app/src/main/java/github/daneren2005/dsub/provider/JukeboxRouteProvider.java
new file mode 100644
index 00000000..0d2a5ff5
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/provider/JukeboxRouteProvider.java
@@ -0,0 +1,131 @@
+/*
+ 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.provider;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.AudioManager;
+import android.media.MediaRouter;
+import android.support.v7.media.MediaControlIntent;
+import android.support.v7.media.MediaRouteDescriptor;
+import android.support.v7.media.MediaRouteProvider;
+import android.support.v7.media.MediaRouteProviderDescriptor;
+
+import github.daneren2005.dsub.domain.RemoteControlState;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.service.RemoteController;
+
+/**
+ * Created by Scott on 11/28/13.
+ */
+public class JukeboxRouteProvider extends MediaRouteProvider {
+ public static final String CATEGORY_JUKEBOX_ROUTE = "github.daneren2005.dsub.SERVER_JUKEBOX";
+ private RemoteController controller;
+ private static final int MAX_VOLUME = 10;
+
+ private DownloadService downloadService;
+
+ public JukeboxRouteProvider(Context context) {
+ super(context);
+ this.downloadService = (DownloadService) context;
+
+ broadcastDescriptor();
+ }
+
+ private void broadcastDescriptor() {
+ // Create intents
+ IntentFilter routeIntentFilter = new IntentFilter();
+ routeIntentFilter.addCategory(CATEGORY_JUKEBOX_ROUTE);
+ routeIntentFilter.addAction(MediaControlIntent.ACTION_START_SESSION);
+ routeIntentFilter.addAction(MediaControlIntent.ACTION_GET_SESSION_STATUS);
+ routeIntentFilter.addAction(MediaControlIntent.ACTION_END_SESSION);
+
+ // Create route descriptor
+ MediaRouteDescriptor.Builder routeBuilder = new MediaRouteDescriptor.Builder("Jukebox Route", "Subsonic Jukebox");
+ routeBuilder.addControlFilter(routeIntentFilter)
+ .setPlaybackStream(AudioManager.STREAM_MUSIC)
+ .setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE)
+ .setDescription("Subsonic Jukebox")
+ .setVolume(controller == null ? 5 : (int) (controller.getVolume() * 10))
+ .setVolumeMax(MAX_VOLUME)
+ .setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE);
+
+ // Create descriptor
+ MediaRouteProviderDescriptor.Builder providerBuilder = new MediaRouteProviderDescriptor.Builder();
+ providerBuilder.addRoute(routeBuilder.build());
+ setDescriptor(providerBuilder.build());
+ }
+
+ @Override
+ public MediaRouteProvider.RouteController onCreateRouteController(String routeId) {
+ return new JukeboxRouteController(downloadService);
+ }
+
+ private class JukeboxRouteController extends RouteController {
+ private DownloadService downloadService;
+
+ public JukeboxRouteController(DownloadService downloadService) {
+ this.downloadService = downloadService;
+ }
+
+ @Override
+ public boolean onControlRequest(Intent intent, android.support.v7.media.MediaRouter.ControlRequestCallback callback) {
+ if (intent.hasCategory(CATEGORY_JUKEBOX_ROUTE)) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public void onRelease() {
+ downloadService.setRemoteEnabled(RemoteControlState.LOCAL);
+ controller = null;
+ }
+
+ @Override
+ public void onSelect() {
+ downloadService.setRemoteEnabled(RemoteControlState.JUKEBOX_SERVER);
+ controller = downloadService.getRemoteController();
+ }
+
+ @Override
+ public void onUnselect() {
+ downloadService.setRemoteEnabled(RemoteControlState.LOCAL);
+ controller = null;
+ }
+
+ @Override
+ public void onUpdateVolume(int delta) {
+ if(controller != null) {
+ controller.updateVolume(delta > 0);
+ }
+ broadcastDescriptor();
+ }
+
+ @Override
+ public void onSetVolume(int volume) {
+ if(controller != null) {
+ controller.setVolume(volume);
+ }
+ broadcastDescriptor();
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/provider/MostRecentStubProvider.java b/app/src/main/java/github/daneren2005/dsub/provider/MostRecentStubProvider.java
new file mode 100644
index 00000000..80dd93cc
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/provider/MostRecentStubProvider.java
@@ -0,0 +1,61 @@
+/*
+ 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.provider;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+
+/**
+ * Created by Scott on 8/28/13.
+ */
+
+public class MostRecentStubProvider extends ContentProvider {
+ @Override
+ public boolean onCreate() {
+ return true;
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
+ return null;
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ return "";
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ return null;
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ return 0;
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ return 0;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/provider/PlaylistStubProvider.java b/app/src/main/java/github/daneren2005/dsub/provider/PlaylistStubProvider.java
new file mode 100644
index 00000000..1481557f
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/provider/PlaylistStubProvider.java
@@ -0,0 +1,61 @@
+/*
+ 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.provider;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+
+/**
+ * Created by Scott on 8/28/13.
+ */
+
+public class PlaylistStubProvider extends ContentProvider {
+ @Override
+ public boolean onCreate() {
+ return true;
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
+ return null;
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ return "";
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ return null;
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ return 0;
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ return 0;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/provider/PodcastStubProvider.java b/app/src/main/java/github/daneren2005/dsub/provider/PodcastStubProvider.java
new file mode 100644
index 00000000..7d9bfca1
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/provider/PodcastStubProvider.java
@@ -0,0 +1,61 @@
+/*
+ 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.provider;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+
+/**
+ * Created by Scott on 8/28/13.
+ */
+
+public class PodcastStubProvider extends ContentProvider {
+ @Override
+ public boolean onCreate() {
+ return true;
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
+ return null;
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ return "";
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ return null;
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ return 0;
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ return 0;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/provider/StarredStubProvider.java b/app/src/main/java/github/daneren2005/dsub/provider/StarredStubProvider.java
new file mode 100644
index 00000000..f638c348
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/provider/StarredStubProvider.java
@@ -0,0 +1,61 @@
+/*
+ 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.provider;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+
+/**
+ * Created by Scott on 8/28/13.
+ */
+
+public class StarredStubProvider extends ContentProvider {
+ @Override
+ public boolean onCreate() {
+ return true;
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
+ return null;
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ return "";
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ return null;
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ return 0;
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ return 0;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/receiver/A2dpIntentReceiver.java b/app/src/main/java/github/daneren2005/dsub/receiver/A2dpIntentReceiver.java
new file mode 100644
index 00000000..8f6ab810
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/receiver/A2dpIntentReceiver.java
@@ -0,0 +1,47 @@
+package github.daneren2005.dsub.receiver;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+import github.daneren2005.dsub.service.DownloadService;
+
+public class A2dpIntentReceiver extends BroadcastReceiver {
+ private static final String PLAYSTATUS_RESPONSE = "com.android.music.playstatusresponse";
+ private String TAG = A2dpIntentReceiver.class.getSimpleName();
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.i(TAG, "GOT INTENT " + intent);
+
+ DownloadService downloadService = DownloadService.getInstance();
+
+ if (downloadService != null){
+
+ Intent avrcpIntent = new Intent(PLAYSTATUS_RESPONSE);
+
+ avrcpIntent.putExtra("duration", (long) downloadService.getPlayerDuration());
+ avrcpIntent.putExtra("position", (long) downloadService.getPlayerPosition());
+ avrcpIntent.putExtra("ListSize", (long) downloadService.getSongs().size());
+
+ switch (downloadService.getPlayerState()){
+ case STARTED:
+ avrcpIntent.putExtra("playing", true);
+ break;
+ case STOPPED:
+ avrcpIntent.putExtra("playing", false);
+ break;
+ case PAUSED:
+ avrcpIntent.putExtra("playing", false);
+ break;
+ case COMPLETED:
+ avrcpIntent.putExtra("playing", false);
+ break;
+ default:
+ return;
+ }
+
+ context.sendBroadcast(avrcpIntent);
+ }
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/github/daneren2005/dsub/receiver/AudioNoisyReceiver.java b/app/src/main/java/github/daneren2005/dsub/receiver/AudioNoisyReceiver.java
new file mode 100644
index 00000000..cb2a433e
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/receiver/AudioNoisyReceiver.java
@@ -0,0 +1,51 @@
+/*
+ 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.receiver;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.media.AudioManager;
+import android.util.Log;
+
+import github.daneren2005.dsub.domain.PlayerState;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.Util;
+
+public class AudioNoisyReceiver extends BroadcastReceiver {
+ private static final String TAG = AudioNoisyReceiver.class.getSimpleName();
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ DownloadService downloadService = DownloadService.getInstance();
+ // Don't do anything if downloadService is not started
+ if(downloadService == null) {
+ return;
+ }
+
+ if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals (intent.getAction ())) {
+ if(!downloadService.isRemoteEnabled() && (downloadService.getPlayerState() == PlayerState.STARTED || downloadService.getPlayerState() == PlayerState.PAUSED_TEMP)) {
+ SharedPreferences prefs = Util.getPreferences(downloadService);
+ int pausePref = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_PAUSE_DISCONNECT, "0"));
+ if(pausePref == 0) {
+ downloadService.pause();
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/receiver/BootReceiver.java b/app/src/main/java/github/daneren2005/dsub/receiver/BootReceiver.java
new file mode 100644
index 00000000..634aeeee
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/receiver/BootReceiver.java
@@ -0,0 +1,34 @@
+/*
+ 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.receiver;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+import github.daneren2005.dsub.service.HeadphoneListenerService;
+import github.daneren2005.dsub.util.Util;
+
+public class BootReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if(Util.shouldStartOnHeadphones(context)) {
+ Intent serviceIntent = new Intent();
+ serviceIntent.setClassName(context.getPackageName(), HeadphoneListenerService.class.getName());
+ context.startService(serviceIntent);
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/receiver/HeadphonePlugReceiver.java b/app/src/main/java/github/daneren2005/dsub/receiver/HeadphonePlugReceiver.java
new file mode 100644
index 00000000..77948c41
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/receiver/HeadphonePlugReceiver.java
@@ -0,0 +1,42 @@
+/*
+ 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.receiver;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.util.Util;
+
+public class HeadphonePlugReceiver extends BroadcastReceiver {
+ private static final String TAG = HeadphonePlugReceiver.class.getSimpleName();
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if(Intent.ACTION_HEADSET_PLUG.equals(intent.getAction())) {
+ int headphoneState = intent.getIntExtra("state", -1);
+ Log.d(TAG, "State: " + headphoneState);
+ if(headphoneState == 1 && Util.shouldStartOnHeadphones(context)) {
+ Log.d(TAG, "Fired play event");
+ Intent start = new Intent(context, DownloadService.class);
+ start.setAction(DownloadService.START_PLAY);
+ context.startService(start);
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/receiver/MediaButtonIntentReceiver.java b/app/src/main/java/github/daneren2005/dsub/receiver/MediaButtonIntentReceiver.java
new file mode 100644
index 00000000..8119ef2d
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/receiver/MediaButtonIntentReceiver.java
@@ -0,0 +1,57 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.receiver;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+import android.view.KeyEvent;
+
+import github.daneren2005.dsub.service.DownloadService;
+
+/**
+ * @author Sindre Mehus
+ */
+public class MediaButtonIntentReceiver extends BroadcastReceiver {
+
+ private static final String TAG = MediaButtonIntentReceiver.class.getSimpleName();
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ KeyEvent event = (KeyEvent) intent.getExtras().get(Intent.EXTRA_KEY_EVENT);
+ if(DownloadService.getInstance() == null && (event.getKeyCode() == KeyEvent.KEYCODE_MEDIA_STOP ||
+ event.getKeyCode() == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE || event.getKeyCode() == KeyEvent.KEYCODE_HEADSETHOOK)) {
+ Log.w(TAG, "Ignore keycode event because downloadService is off");
+ return;
+ }
+ Log.i(TAG, "Got MEDIA_BUTTON key event: " + event);
+
+ Intent serviceIntent = new Intent(context, DownloadService.class);
+ serviceIntent.putExtra(Intent.EXTRA_KEY_EVENT, event);
+ context.startService(serviceIntent);
+ if (isOrderedBroadcast()) {
+ try {
+ abortBroadcast();
+ } catch (Exception x) {
+ // Ignored.
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/receiver/PlayActionReceiver.java b/app/src/main/java/github/daneren2005/dsub/receiver/PlayActionReceiver.java
new file mode 100644
index 00000000..2c04d829
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/receiver/PlayActionReceiver.java
@@ -0,0 +1,46 @@
+/*
+ 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.receiver;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.util.Constants;
+
+public class PlayActionReceiver extends BroadcastReceiver {
+ private static final String TAG = PlayActionReceiver.class.getSimpleName();
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if(intent.hasExtra(Constants.TASKER_EXTRA_BUNDLE)) {
+ Bundle data = intent.getBundleExtra(Constants.TASKER_EXTRA_BUNDLE);
+ Boolean startShuffled = data.getBoolean(Constants.INTENT_EXTRA_NAME_SHUFFLE);
+
+ Intent start = new Intent(context, DownloadService.class);
+ start.setAction(DownloadService.START_PLAY);
+ start.putExtra(Constants.INTENT_EXTRA_NAME_SHUFFLE, startShuffled);
+ start.putExtra(Constants.PREFERENCES_KEY_SHUFFLE_START_YEAR, data.getString(Constants.PREFERENCES_KEY_SHUFFLE_START_YEAR));
+ start.putExtra(Constants.PREFERENCES_KEY_SHUFFLE_END_YEAR, data.getString(Constants.PREFERENCES_KEY_SHUFFLE_END_YEAR));
+ start.putExtra(Constants.PREFERENCES_KEY_SHUFFLE_GENRE, data.getString(Constants.PREFERENCES_KEY_SHUFFLE_GENRE));
+ start.putExtra(Constants.PREFERENCES_KEY_OFFLINE, data.getInt(Constants.PREFERENCES_KEY_OFFLINE));
+ context.startService(start);
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/CachedMusicService.java b/app/src/main/java/github/daneren2005/dsub/service/CachedMusicService.java
new file mode 100644
index 00000000..61d6205a
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/CachedMusicService.java
@@ -0,0 +1,1424 @@
+/*
+ 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.service;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.http.HttpResponse;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+
+import github.daneren2005.dsub.domain.Artist;
+import github.daneren2005.dsub.domain.ArtistInfo;
+import github.daneren2005.dsub.domain.Bookmark;
+import github.daneren2005.dsub.domain.ChatMessage;
+import github.daneren2005.dsub.domain.Genre;
+import github.daneren2005.dsub.domain.Indexes;
+import github.daneren2005.dsub.domain.PlayerQueue;
+import github.daneren2005.dsub.domain.PodcastEpisode;
+import github.daneren2005.dsub.domain.RemoteStatus;
+import github.daneren2005.dsub.domain.Lyrics;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.MusicFolder;
+import github.daneren2005.dsub.domain.Playlist;
+import github.daneren2005.dsub.domain.PodcastChannel;
+import github.daneren2005.dsub.domain.SearchCritera;
+import github.daneren2005.dsub.domain.SearchResult;
+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.TimeLimitedCache;
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.Util;
+
+import static github.daneren2005.dsub.domain.MusicDirectory.Entry;
+
+/**
+ * @author Sindre Mehus
+ */
+public class CachedMusicService implements MusicService {
+ private static final String TAG = CachedMusicService.class.getSimpleName();
+
+ private static final int MUSIC_DIR_CACHE_SIZE = 20;
+ private static final int TTL_MUSIC_DIR = 5 * 60; // Five minutes
+
+ private final RESTMusicService musicService;
+ private final TimeLimitedCache<Boolean> cachedLicenseValid = new TimeLimitedCache<Boolean>(120, TimeUnit.SECONDS);
+ private final TimeLimitedCache<Indexes> cachedIndexes = new TimeLimitedCache<Indexes>(60 * 60, TimeUnit.SECONDS);
+ private final TimeLimitedCache<List<Playlist>> cachedPlaylists = new TimeLimitedCache<List<Playlist>>(3600, TimeUnit.SECONDS);
+ private final TimeLimitedCache<List<MusicFolder>> cachedMusicFolders = new TimeLimitedCache<List<MusicFolder>>(10 * 3600, TimeUnit.SECONDS);
+ private final TimeLimitedCache<List<PodcastChannel>> cachedPodcastChannels = new TimeLimitedCache<List<PodcastChannel>>(10 * 3600, TimeUnit.SECONDS);
+ private String restUrl;
+ private String musicFolderId;
+ private boolean isTagBrowsing = false;
+
+ public CachedMusicService(RESTMusicService musicService) {
+ this.musicService = musicService;
+ }
+
+ @Override
+ public void ping(Context context, ProgressListener progressListener) throws Exception {
+ checkSettingsChanged(context);
+ musicService.ping(context, progressListener);
+ }
+
+ @Override
+ public boolean isLicenseValid(Context context, ProgressListener progressListener) throws Exception {
+ checkSettingsChanged(context);
+ Boolean result = cachedLicenseValid.get();
+ if (result == null) {
+ result = FileUtil.deserialize(context, getCacheName(context, "license"), Boolean.class);
+
+ if(result == null) {
+ result = musicService.isLicenseValid(context, progressListener);
+
+ // Only save a copy license is valid
+ if(result) {
+ FileUtil.serialize(context, (Boolean) result, getCacheName(context, "license"));
+ }
+ }
+ cachedLicenseValid.set(result, result ? 30L * 60L : 2L * 60L, TimeUnit.SECONDS);
+ }
+ return result;
+ }
+
+ @Override
+ public List<MusicFolder> getMusicFolders(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ checkSettingsChanged(context);
+ if (refresh) {
+ cachedMusicFolders.clear();
+ }
+ List<MusicFolder> result = cachedMusicFolders.get();
+ if (result == null) {
+ if(!refresh) {
+ result = FileUtil.deserialize(context, getCacheName(context, "musicFolders"), ArrayList.class);
+ }
+
+ if(result == null) {
+ result = musicService.getMusicFolders(refresh, context, progressListener);
+ FileUtil.serialize(context, new ArrayList<MusicFolder>(result), getCacheName(context, "musicFolders"));
+ }
+ cachedMusicFolders.set(result);
+ }
+ return result;
+ }
+
+ @Override
+ public void startRescan(Context context, ProgressListener listener) throws Exception {
+ musicService.startRescan(context, listener);
+ }
+
+ @Override
+ public Indexes getIndexes(String musicFolderId, boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ checkSettingsChanged(context);
+ if (refresh) {
+ cachedIndexes.clear();
+ cachedMusicFolders.clear();
+ }
+ Indexes result = cachedIndexes.get();
+ if (result == null) {
+ String name = Util.isTagBrowsing(context, musicService.getInstance(context)) ? "artists" : "indexes";
+ name = getCacheName(context, name, musicFolderId);
+ if(!refresh) {
+ result = FileUtil.deserialize(context, name, Indexes.class);
+ }
+
+ if(result == null) {
+ result = musicService.getIndexes(musicFolderId, refresh, context, progressListener);
+ FileUtil.serialize(context, result, name);
+ }
+ cachedIndexes.set(result);
+ }
+ return result;
+ }
+
+ @Override
+ public MusicDirectory getMusicDirectory(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ MusicDirectory dir = null;
+ MusicDirectory cached = FileUtil.deserialize(context, getCacheName(context, "directory", id), MusicDirectory.class);
+ if(!refresh) {
+ dir = cached;
+ }
+
+ if(dir == null) {
+ dir = musicService.getMusicDirectory(id, name, refresh, context, progressListener);
+ FileUtil.serialize(context, dir, getCacheName(context, "directory", id));
+
+ // If a cached copy exists to check against, look for removes
+ deleteRemovedEntries(context, dir, cached);
+ }
+
+ return dir;
+ }
+
+ @Override
+ public MusicDirectory getArtist(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ MusicDirectory dir = null;
+ MusicDirectory cached = FileUtil.deserialize(context, getCacheName(context, "artist", id), MusicDirectory.class);
+ if(!refresh) {
+ dir = cached;
+ }
+
+ if(dir == null) {
+ dir = musicService.getArtist(id, name, refresh, context, progressListener);
+ FileUtil.serialize(context, dir, getCacheName(context, "artist", id));
+
+ // If a cached copy exists to check against, look for removes
+ deleteRemovedEntries(context, dir, cached);
+ }
+
+ return dir;
+ }
+
+ @Override
+ public MusicDirectory getAlbum(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ MusicDirectory dir = null;
+ MusicDirectory cached = FileUtil.deserialize(context, getCacheName(context, "album", id), MusicDirectory.class);
+ if(!refresh) {
+ dir = cached;
+ }
+
+ if(dir == null) {
+ dir = musicService.getAlbum(id, name, refresh, context, progressListener);
+ FileUtil.serialize(context, dir, getCacheName(context, "album", id));
+
+ // If a cached copy exists to check against, look for removes
+ deleteRemovedEntries(context, dir, cached);
+ }
+
+ return dir;
+ }
+
+ @Override
+ public SearchResult search(SearchCritera criteria, Context context, ProgressListener progressListener) throws Exception {
+ return musicService.search(criteria, context, progressListener);
+ }
+
+ @Override
+ public MusicDirectory getPlaylist(boolean refresh, String id, String name, Context context, ProgressListener progressListener) throws Exception {
+ MusicDirectory dir = null;
+ MusicDirectory cachedPlaylist = FileUtil.deserialize(context, getCacheName(context, "playlist", id), MusicDirectory.class);
+ if(!refresh) {
+ dir = cachedPlaylist;
+ }
+ if(dir == null) {
+ dir = musicService.getPlaylist(refresh, id, name, context, progressListener);
+ FileUtil.serialize(context, dir, getCacheName(context, "playlist", id));
+
+ File playlistFile = FileUtil.getPlaylistFile(context, Util.getServerName(context, musicService.getInstance(context)), dir.getName());
+ if(cachedPlaylist == null || !playlistFile.exists() || !cachedPlaylist.getChildren().equals(dir.getChildren())) {
+ FileUtil.writePlaylistFile(context, playlistFile, dir);
+ }
+ }
+ return dir;
+ }
+
+ @Override
+ public List<Playlist> getPlaylists(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ checkSettingsChanged(context);
+ List<Playlist> result = refresh ? null : cachedPlaylists.get();
+ if (result == null) {
+ if(!refresh) {
+ result = FileUtil.deserialize(context, getCacheName(context, "playlist"), ArrayList.class);
+ }
+
+ if(result == null) {
+ result = musicService.getPlaylists(refresh, context, progressListener);
+ FileUtil.serialize(context, new ArrayList<Playlist>(result), getCacheName(context, "playlist"));
+ }
+ cachedPlaylists.set(result);
+ }
+ return result;
+ }
+
+ @Override
+ public void createPlaylist(String id, String name, List<Entry> entries, Context context, ProgressListener progressListener) throws Exception {
+ cachedPlaylists.clear();
+ Util.delete(new File(context.getCacheDir(), getCacheName(context, "playlist")));
+ musicService.createPlaylist(id, name, entries, context, progressListener);
+ }
+
+ @Override
+ public void deletePlaylist(String id, Context context, ProgressListener progressListener) throws Exception {
+ musicService.deletePlaylist(id, context, progressListener);
+
+ new PlaylistUpdater(context, id) {
+ @Override
+ public void updateResult(List<Playlist> objects, Playlist result) {
+ objects.remove(result);
+ cachedPlaylists.set(objects);
+ }
+ }.execute();
+ }
+
+ @Override
+ public void addToPlaylist(String id, final List<Entry> toAdd, Context context, ProgressListener progressListener) throws Exception {
+ musicService.addToPlaylist(id, toAdd, context, progressListener);
+
+ new MusicDirectoryUpdater(context, "playlist", id) {
+ @Override
+ public boolean checkResult(Entry check) {
+ return true;
+ }
+
+ @Override
+ public void updateResult(List<Entry> objects, Entry result) {
+ objects.addAll(toAdd);
+ }
+ }.execute();
+ }
+
+ @Override
+ public void removeFromPlaylist(String id, final List<Integer> toRemove, Context context, ProgressListener progressListener) throws Exception {
+ musicService.removeFromPlaylist(id, toRemove, context, progressListener);
+
+ new MusicDirectoryUpdater(context, "playlist", id) {
+ @Override
+ public boolean checkResult(Entry check) {
+ return true;
+ }
+
+ @Override
+ public void updateResult(List<Entry> objects, Entry result) {
+ // Remove in reverse order so indexes are still correct as we iterate through
+ for(ListIterator<Integer> iterator = toRemove.listIterator(toRemove.size()); iterator.hasPrevious(); ) {
+ objects.remove((int) iterator.previous());
+ }
+ }
+ }.execute();
+ }
+
+ @Override
+ public void overwritePlaylist(String id, String name, int toRemove, final List<Entry> toAdd, Context context, ProgressListener progressListener) throws Exception {
+ musicService.overwritePlaylist(id, name, toRemove, toAdd, context, progressListener);
+
+ new MusicDirectoryUpdater(context, "playlist", id) {
+ @Override
+ public boolean checkResult(Entry check) {
+ return true;
+ }
+
+ @Override
+ public void updateResult(List<Entry> objects, Entry result) {
+ objects.clear();
+ objects.addAll(toAdd);
+ }
+ }.execute();
+ }
+
+ @Override
+ public void updatePlaylist(String id, final String name, final String comment, final boolean pub, Context context, ProgressListener progressListener) throws Exception {
+ musicService.updatePlaylist(id, name, comment, pub, context, progressListener);
+
+ new PlaylistUpdater(context, id) {
+ @Override
+ public void updateResult(List<Playlist> objects, Playlist result) {
+ result.setName(name);
+ result.setComment(comment);
+ result.setPublic(pub);
+
+ cachedPlaylists.set(objects);
+ }
+ }.execute();
+ }
+
+ @Override
+ public Lyrics getLyrics(String artist, String title, Context context, ProgressListener progressListener) throws Exception {
+ return musicService.getLyrics(artist, title, context, progressListener);
+ }
+
+ @Override
+ public void scrobble(String id, boolean submission, Context context, ProgressListener progressListener) throws Exception {
+ musicService.scrobble(id, submission, context, progressListener);
+ }
+
+ @Override
+ public MusicDirectory getAlbumList(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception {
+ try {
+ MusicDirectory dir = musicService.getAlbumList(type, size, offset, context, progressListener);
+
+ // Do some serialization updates for changes to recently added
+ if ("newest".equals(type) && offset == 0) {
+ String recentlyAddedFile = getCacheName(context, type);
+ ArrayList<String> recents = FileUtil.deserialize(context, recentlyAddedFile, ArrayList.class);
+ if (recents == null) {
+ recents = new ArrayList<String>();
+ }
+
+ // Add any new items
+ final int instance = musicService.getInstance(context);
+ isTagBrowsing = Util.isTagBrowsing(context, instance);
+ for (final Entry album : dir.getChildren()) {
+ if (!recents.contains(album.getId())) {
+ recents.add(album.getId());
+
+ String cacheName, parent;
+ if (isTagBrowsing) {
+ cacheName = "artist";
+ parent = album.getArtistId();
+ } else {
+ cacheName = "directory";
+ parent = album.getParent();
+ }
+
+ // Add album to artist
+ if (parent != null) {
+ new MusicDirectoryUpdater(context, cacheName, parent) {
+ private boolean changed = false;
+
+ @Override
+ public boolean checkResult(Entry check) {
+ return true;
+ }
+
+ @Override
+ public void updateResult(List<Entry> objects, Entry result) {
+ // Only add if it doesn't already exist in it!
+ if (!objects.contains(album)) {
+ objects.add(album);
+ changed = true;
+ }
+ }
+
+ @Override
+ public void save(ArrayList<Entry> objects) {
+ // Only save if actually added to artist
+ if (changed) {
+ musicDirectory.replaceChildren(objects);
+ // Reapply sort after addition
+ musicDirectory.sortChildren(context, instance);
+ FileUtil.serialize(context, musicDirectory, cacheName);
+ }
+ }
+ }.execute();
+ } else {
+ // If parent is null, then this is a root level album
+ final Artist artist = new Artist();
+ artist.setId(album.getId());
+ artist.setName(album.getTitle());
+
+ new IndexesUpdater(context, isTagBrowsing ? "artists" : "indexes") {
+ private boolean changed = false;
+
+ @Override
+ public boolean checkResult(Artist check) {
+ return true;
+ }
+
+ @Override
+ public void updateResult(List<Artist> objects, Artist result) {
+ if (!objects.contains(artist)) {
+ objects.add(artist);
+ changed = true;
+ }
+ }
+
+ @Override
+ public void save(ArrayList<Artist> objects) {
+ if (changed) {
+ indexes.setArtists(objects);
+ // Reapply sort after addition
+ indexes.sortChildren(context);
+ FileUtil.serialize(context, indexes, cacheName);
+ cachedIndexes.set(indexes);
+ }
+ }
+ }.execute();
+ }
+ }
+ }
+
+ // Keep list from growing into infinity
+ while (recents.size() > 0) {
+ recents.remove(0);
+ }
+ FileUtil.serialize(context, recents, recentlyAddedFile);
+ }
+
+ FileUtil.serialize(context, dir, getCacheName(context, type, Integer.toString(offset)));
+ return dir;
+ } catch(IOException e) {
+ MusicDirectory dir = FileUtil.deserialize(context, getCacheName(context, type, Integer.toString(offset)), MusicDirectory.class);
+
+ if(dir == null) {
+ // If we are at start and no cache, throw error higher
+ if(offset == 0) {
+ throw e;
+ } else {
+ // Otherwise just pretend we are at the end of the list
+ return new MusicDirectory();
+ }
+ } else {
+ return dir;
+ }
+ }
+ }
+
+ @Override
+ public MusicDirectory getAlbumList(String type, String extra, int size, int offset, Context context, ProgressListener progressListener) throws Exception {
+ try {
+ MusicDirectory dir = musicService.getAlbumList(type, extra, size, offset, context, progressListener);
+ FileUtil.serialize(context, dir, getCacheName(context, type + extra, Integer.toString(offset)));
+ return dir;
+ } catch(IOException e) {
+ MusicDirectory dir = FileUtil.deserialize(context, getCacheName(context, type + extra, Integer.toString(offset)), MusicDirectory.class);
+
+ if(dir == null) {
+ // If we are at start and no cache, throw error higher
+ if(offset == 0) {
+ throw e;
+ } else {
+ // Otherwise just pretend we are at the end of the list
+ return new MusicDirectory();
+ }
+ } else {
+ return dir;
+ }
+ }
+ }
+
+ @Override
+ public MusicDirectory getRandomSongs(int size, String artistId, Context context, ProgressListener progressListener) throws Exception {
+ return musicService.getRandomSongs(size, artistId, context, progressListener);
+ }
+
+ @Override
+ public MusicDirectory getStarredList(Context context, ProgressListener progressListener) throws Exception {
+ try {
+ MusicDirectory dir = musicService.getStarredList(context, progressListener);
+
+ MusicDirectory oldDir = FileUtil.deserialize(context, "starred", MusicDirectory.class);
+ if (oldDir != null) {
+ final List<Entry> newList = new ArrayList<Entry>();
+ newList.addAll(dir.getChildren());
+ final List<Entry> oldList = oldDir.getChildren();
+
+ for (Iterator<Entry> it = oldList.iterator(); it.hasNext(); ) {
+ Entry oldEntry = it.next();
+
+ // Remove entries from newList
+ if (newList.remove(oldEntry)) {
+ // If it was removed, then remove it from old list as well
+ it.remove();
+ } else {
+ oldEntry.setStarred(false);
+ }
+ }
+
+ List<Entry> totalList = new ArrayList<Entry>();
+ totalList.addAll(oldList);
+ totalList.addAll(newList);
+
+ new StarUpdater(context, totalList).execute();
+ }
+ FileUtil.serialize(context, dir, "starred");
+
+ return dir;
+ } catch(IOException e) {
+ MusicDirectory dir = FileUtil.deserialize(context, "starred", MusicDirectory.class);
+ if(dir == null) {
+ throw e;
+ } else {
+ return dir;
+ }
+ }
+ }
+
+ @Override
+ public MusicDirectory getRandomSongs(int size, String folder, String genre, String startYear, String endYear, Context context, ProgressListener progressListener) throws Exception {
+ return musicService.getRandomSongs(size, folder, genre, startYear, endYear, context, progressListener);
+ }
+
+ @Override
+ public String getCoverArtUrl(Context context, Entry entry) throws Exception {
+ return musicService.getCoverArtUrl(context, entry);
+ }
+
+ @Override
+ public Bitmap getCoverArt(Context context, Entry entry, int size, ProgressListener progressListener, SilentBackgroundTask task) throws Exception {
+ return musicService.getCoverArt(context, entry, size, progressListener, task);
+ }
+
+ @Override
+ public HttpResponse getDownloadInputStream(Context context, Entry song, long offset, int maxBitrate, SilentBackgroundTask task) throws Exception {
+ return musicService.getDownloadInputStream(context, song, offset, maxBitrate, task);
+ }
+
+ @Override
+ public String getMusicUrl(Context context, Entry song, int maxBitrate) throws Exception {
+ return musicService.getMusicUrl(context, song, maxBitrate);
+ }
+
+ @Override
+ public String getVideoUrl(int maxBitrate, Context context, String id) {
+ return musicService.getVideoUrl(maxBitrate, context, id);
+ }
+
+ @Override
+ public String getVideoStreamUrl(String format, int maxBitrate, Context context, String id) throws Exception {
+ return musicService.getVideoStreamUrl(format, maxBitrate, context, id);
+ }
+
+ @Override
+ public String getHlsUrl(String id, int bitRate, Context context) throws Exception {
+ return musicService.getHlsUrl(id, bitRate, context);
+ }
+
+ @Override
+ public RemoteStatus updateJukeboxPlaylist(List<String> ids, Context context, ProgressListener progressListener) throws Exception {
+ return musicService.updateJukeboxPlaylist(ids, context, progressListener);
+ }
+
+ @Override
+ public RemoteStatus skipJukebox(int index, int offsetSeconds, Context context, ProgressListener progressListener) throws Exception {
+ return musicService.skipJukebox(index, offsetSeconds, context, progressListener);
+ }
+
+ @Override
+ public RemoteStatus stopJukebox(Context context, ProgressListener progressListener) throws Exception {
+ return musicService.stopJukebox(context, progressListener);
+ }
+
+ @Override
+ public RemoteStatus startJukebox(Context context, ProgressListener progressListener) throws Exception {
+ return musicService.startJukebox(context, progressListener);
+ }
+
+ @Override
+ public RemoteStatus getJukeboxStatus(Context context, ProgressListener progressListener) throws Exception {
+ return musicService.getJukeboxStatus(context, progressListener);
+ }
+
+ @Override
+ public RemoteStatus setJukeboxGain(float gain, Context context, ProgressListener progressListener) throws Exception {
+ return musicService.setJukeboxGain(gain, context, progressListener);
+ }
+
+ @Override
+ public void setStarred(List<Entry> entries, List<Entry> artists, List<Entry> albums, final boolean starred, ProgressListener progressListener, Context context) throws Exception {
+ musicService.setStarred(entries, artists, albums, starred, progressListener, context);
+
+ // Fuzzy logic to update parents serialization
+ List<Entry> allEntries = new ArrayList<Entry>();
+ if(artists != null) {
+ allEntries.addAll(artists);
+ }
+ if(albums != null) {
+ allEntries.addAll(albums);
+ }
+ if (entries != null) {
+ allEntries.addAll(entries);
+ }
+
+ new StarUpdater(context, allEntries).execute();
+ }
+
+ @Override
+ public List<Share> getShares(Context context, ProgressListener progressListener) throws Exception {
+ return musicService.getShares(context, progressListener);
+ }
+
+ @Override
+ public List<Share> createShare(List<String> ids, String description, Long expires, Context context, ProgressListener progressListener) throws Exception {
+ return musicService.createShare(ids, description, expires, context, progressListener);
+ }
+
+ @Override
+ public void deleteShare(String id, Context context, ProgressListener progressListener) throws Exception {
+ musicService.deleteShare(id, context, progressListener);
+ }
+
+ @Override
+ public void updateShare(String id, String description, Long expires, Context context, ProgressListener progressListener) throws Exception {
+ musicService.updateShare(id, description, expires, context, progressListener);
+ }
+
+ @Override
+ public List<ChatMessage> getChatMessages(Long since, Context context, ProgressListener progressListener) throws Exception {
+ return musicService.getChatMessages(since, context, progressListener);
+ }
+
+ @Override
+ public void addChatMessage(String message, Context context, ProgressListener progressListener) throws Exception {
+ musicService.addChatMessage(message, context, progressListener);
+ }
+
+ @Override
+ public List<Genre> getGenres(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ List<Genre> result = null;
+
+ if(!refresh) {
+ result = FileUtil.deserialize(context, getCacheName(context, "genre"), ArrayList.class);
+ }
+
+ if(result == null) {
+ result = musicService.getGenres(refresh, context, progressListener);
+ FileUtil.serialize(context, new ArrayList<Genre>(result), getCacheName(context, "genre"));
+ }
+
+ return result;
+ }
+
+ @Override
+ public MusicDirectory getSongsByGenre(String genre, int count, int offset, Context context, ProgressListener progressListener) throws Exception {
+ try {
+ MusicDirectory dir = musicService.getSongsByGenre(genre, count, offset, context, progressListener);
+ FileUtil.serialize(context, dir, getCacheName(context, "genreSongs", Integer.toString(offset)));
+
+ return dir;
+ } catch(IOException e) {
+ MusicDirectory dir = FileUtil.deserialize(context, getCacheName(context, "genreSongs", Integer.toString(offset)), MusicDirectory.class);
+
+ if(dir == null) {
+ // If we are at start and no cache, throw error higher
+ if(offset == 0) {
+ throw e;
+ } else {
+ // Otherwise just pretend we are at the end of the list
+ return new MusicDirectory();
+ }
+ } else {
+ return dir;
+ }
+ }
+ }
+
+ @Override
+ public MusicDirectory getTopTrackSongs(String artist, int size, Context context, ProgressListener progressListener) throws Exception {
+ return musicService.getTopTrackSongs(artist, size, context, progressListener);
+ }
+
+ @Override
+ public List<PodcastChannel> getPodcastChannels(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ checkSettingsChanged(context);
+ List<PodcastChannel> result = refresh ? null : cachedPodcastChannels.get();
+
+ if (result == null) {
+ if(!refresh) {
+ result = FileUtil.deserialize(context, getCacheName(context, "podcast"), ArrayList.class);
+ }
+
+ if(result == null) {
+ result = musicService.getPodcastChannels(refresh, context, progressListener);
+ FileUtil.serialize(context, new ArrayList<PodcastChannel>(result), getCacheName(context, "podcast"));
+ }
+ cachedPodcastChannels.set(result);
+ }
+
+ return result;
+ }
+
+ @Override
+ public MusicDirectory getPodcastEpisodes(boolean refresh, String id, Context context, ProgressListener progressListener) throws Exception {
+ String altId = "p-" + id;
+ MusicDirectory result = null;
+
+ if(!refresh) {
+ result = FileUtil.deserialize(context, getCacheName(context, "directory", altId), MusicDirectory.class, 10);
+ }
+
+ if(result == null) {
+ result = musicService.getPodcastEpisodes(refresh, id, context, progressListener);
+ FileUtil.serialize(context, result, getCacheName(context, "directory", altId));
+ }
+
+ return result;
+ }
+
+ @Override
+ public void refreshPodcasts(Context context, ProgressListener progressListener) throws Exception {
+ musicService.refreshPodcasts(context, progressListener);
+ }
+
+ @Override
+ public void createPodcastChannel(String url, Context context, ProgressListener progressListener) throws Exception{
+ musicService.createPodcastChannel(url, context, progressListener);
+ }
+
+ @Override
+ public void deletePodcastChannel(final String id, Context context, ProgressListener progressListener) throws Exception{
+ new SerializeUpdater<PodcastChannel>(context, "podcast") {
+ @Override
+ public boolean checkResult(PodcastChannel check) {
+ return id.equals(check.getId());
+ }
+
+ @Override
+ public void updateResult(List<PodcastChannel> objects, PodcastChannel result) {
+ objects.remove(result);
+ cachedPodcastChannels.set(objects);
+ }
+ }.execute();
+ musicService.deletePodcastChannel(id, context, progressListener);
+ }
+
+ @Override
+ public void downloadPodcastEpisode(String id, Context context, ProgressListener progressListener) throws Exception{
+ musicService.downloadPodcastEpisode(id, context, progressListener);
+ }
+
+ @Override
+ public void deletePodcastEpisode(final String id, String parent, ProgressListener progressListener, Context context) throws Exception{
+ musicService.deletePodcastEpisode(id, parent, progressListener, context);
+
+ new MusicDirectoryUpdater(context, "directory", "p-" + parent) {
+ @Override
+ public boolean checkResult(Entry check) {
+ return id.equals(((PodcastEpisode) check).getEpisodeId());
+ }
+
+ @Override
+ public void updateResult(List<Entry> objects, Entry result) {
+ objects.remove(result);
+ }
+ }.execute();
+ }
+
+ @Override
+ public void setRating(final Entry entry, final int rating, Context context, ProgressListener progressListener) throws Exception {
+ musicService.setRating(entry, rating, context, progressListener);
+
+ new GenericEntryUpdater(context, entry) {
+ @Override
+ public void updateResult(Entry result) {
+ result.setRating(rating);
+ }
+ }.execute();
+ }
+
+ @Override
+ public MusicDirectory getBookmarks(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ MusicDirectory bookmarks = musicService.getBookmarks(refresh, context, progressListener);
+
+ MusicDirectory oldBookmarks = FileUtil.deserialize(context, "bookmarks", MusicDirectory.class);
+ if(oldBookmarks != null) {
+ final List<Entry> oldList = oldBookmarks.getChildren();
+ final List<Entry> newList = new ArrayList<Entry>();
+ newList.addAll(bookmarks.getChildren());
+
+ for(Iterator<Entry> it = oldList.iterator(); it.hasNext(); ) {
+ Entry oldEntry = it.next();
+ // Remove entries from newList
+ int position = newList.indexOf(oldEntry);
+ if(position != -1) {
+ Entry newEntry = newList.get(position);
+ if(newEntry.getBookmark().getPosition() == oldEntry.getBookmark().getPosition()) {
+ newList.remove(position);
+ }
+
+ // Remove from old regardless of whether position is wrong
+ it.remove();
+ } else {
+ oldEntry.setBookmark(null);
+ }
+ }
+
+ List<Entry> totalList = new ArrayList<Entry>();
+ totalList.addAll(oldList);
+ totalList.addAll(newList);
+
+ new BookmarkUpdater(context, totalList).execute();
+ }
+ FileUtil.serialize(context, bookmarks, "bookmarks");
+
+ return bookmarks;
+ }
+
+ @Override
+ public void createBookmark(Entry entry, int position, String comment, Context context, ProgressListener progressListener) throws Exception {
+ musicService.createBookmark(entry, position, comment, context, progressListener);
+
+ new BookmarkUpdater(context, entry).execute();
+ }
+
+ @Override
+ public void deleteBookmark(Entry entry, Context context, ProgressListener progressListener) throws Exception {
+ musicService.deleteBookmark(entry, context, progressListener);
+
+ new BookmarkUpdater(context, entry).execute();
+ }
+
+ @Override
+ public User getUser(boolean refresh, String username, Context context, ProgressListener progressListener) throws Exception {
+ User result = null;
+
+ try {
+ result = musicService.getUser(refresh, username, context, progressListener);
+ FileUtil.serialize(context, result, getCacheName(context, "user-" + username));
+ } catch(Exception e) {
+ // Don't care
+ }
+
+ if(result == null && !refresh) {
+ result = FileUtil.deserialize(context, getCacheName(context, "user-" + username), User.class);
+ }
+
+ return result;
+ }
+
+ @Override
+ public List<User> getUsers(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ List<User> result = null;
+
+ if(!refresh) {
+ result = FileUtil.deserialize(context, getCacheName(context, "users"), ArrayList.class);
+ }
+
+ if(result == null) {
+ result = musicService.getUsers(refresh, context, progressListener);
+ FileUtil.serialize(context, new ArrayList<User>(result), getCacheName(context, "users"));
+ }
+
+ return result;
+ }
+
+ @Override
+ public void createUser(final User user, Context context, ProgressListener progressListener) throws Exception {
+ musicService.createUser(user, context, progressListener);
+
+ new UserUpdater(context, "") {
+ @Override
+ public boolean checkResult(User check) {
+ return true;
+ }
+
+ @Override
+ public void updateResult(List<User> users, User result) {
+ users.add(user);
+ }
+ }.execute();
+ }
+
+ @Override
+ public void updateUser(final User user, Context context, ProgressListener progressListener) throws Exception {
+ musicService.updateUser(user, context, progressListener);
+
+ new UserUpdater(context, user.getUsername()) {
+ @Override
+ public void updateResult(List<User> users, User result) {
+ result.setEmail(user.getEmail());
+ result.setSettings(user.getSettings());
+ }
+ }.execute();
+ }
+
+ @Override
+ public void deleteUser(String username, Context context, ProgressListener progressListener) throws Exception {
+ musicService.deleteUser(username, context, progressListener);
+
+ new UserUpdater(context, username) {
+ @Override
+ public void updateResult(List<User> users, User result) {
+ users.remove(result);
+ }
+ }.execute();
+ }
+
+ @Override
+ public void changeEmail(String username, final String email, Context context, ProgressListener progressListener) throws Exception {
+ musicService.changeEmail(username, email, context, progressListener);
+
+ // Update cached email for user
+ new UserUpdater(context, username) {
+ @Override
+ public void updateResult(List<User> users, User result) {
+ result.setEmail(email);
+ }
+ }.execute();
+ }
+
+ @Override
+ public void changePassword(String username, String password, Context context, ProgressListener progressListener) throws Exception {
+ musicService.changePassword(username, password, context, progressListener);
+ }
+
+ @Override
+ public Bitmap getAvatar(String username, int size, Context context, ProgressListener progressListener, SilentBackgroundTask task) throws Exception {
+ return musicService.getAvatar(username, size, context, progressListener, task);
+ }
+
+ @Override
+ public ArtistInfo getArtistInfo(String id, boolean refresh, boolean allowNetwork, Context context, ProgressListener progressListener) throws Exception {
+ String cacheName = getCacheName(context, "artistInfo", id);
+ ArtistInfo info = null;
+ if(!refresh) {
+ info = FileUtil.deserialize(context, cacheName, ArtistInfo.class);
+ }
+
+ if(info == null && allowNetwork) {
+ info = musicService.getArtistInfo(id, refresh, allowNetwork, context, progressListener);
+ FileUtil.serialize(context, info, cacheName);
+ }
+
+ return info;
+ }
+
+ @Override
+ public Bitmap getBitmap(String url, int size, Context context, ProgressListener progressListener, SilentBackgroundTask task) throws Exception {
+ return musicService.getBitmap(url, size, context, progressListener, task);
+ }
+
+ @Override
+ public MusicDirectory getVideos(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ try {
+ MusicDirectory dir = musicService.getVideos(refresh, context, progressListener);
+ FileUtil.serialize(context, dir, "videos");
+
+ return dir;
+ } catch(IOException e) {
+ MusicDirectory dir = FileUtil.deserialize(context, "videos", MusicDirectory.class);
+ if(dir == null) {
+ throw e;
+ } else {
+ return dir;
+ }
+ }
+ }
+
+ @Override
+ public void savePlayQueue(List<Entry> songs, Entry currentPlaying, int position, Context context, ProgressListener progressListener) throws Exception {
+ musicService.savePlayQueue(songs, currentPlaying, position, context, progressListener);
+ }
+
+ @Override
+ public PlayerQueue getPlayQueue(Context context, ProgressListener progressListener) throws Exception {
+ return musicService.getPlayQueue(context, progressListener);
+ }
+
+ @Override
+ public int processOfflineSyncs(final Context context, final ProgressListener progressListener) throws Exception{
+ return musicService.processOfflineSyncs(context, progressListener);
+ }
+
+ @Override
+ public void setInstance(Integer instance) throws Exception {
+ musicService.setInstance(instance);
+ }
+
+ private String getCacheName(Context context, String name, String id) {
+ String s = musicService.getRestUrl(context, null, false) + id;
+ return name + "-" + s.hashCode() + ".ser";
+ }
+ private String getCacheName(Context context, String name) {
+ String s = musicService.getRestUrl(context, null, false);
+ return name + "-" + s.hashCode() + ".ser";
+ }
+
+ private void deleteRemovedEntries(Context context, MusicDirectory dir, MusicDirectory cached) {
+ if(cached != null) {
+ List<Entry> oldList = new ArrayList<Entry>();
+ oldList.addAll(cached.getChildren());
+
+ // Remove all current items from old list
+ for(Entry entry: dir.getChildren()) {
+ oldList.remove(entry);
+ }
+
+ // Anything remaining has been removed from server
+ MediaStoreService store = new MediaStoreService(context);
+ for(Entry entry: oldList) {
+ File file = FileUtil.getEntryFile(context, entry);
+ FileUtil.recursiveDelete(file, store);
+ }
+ }
+ }
+
+ private abstract class SerializeUpdater<T> {
+ final Context context;
+ final String cacheName;
+ final boolean singleUpdate;
+
+ public SerializeUpdater(Context context, String cacheName) {
+ this(context, cacheName, true);
+ }
+ public SerializeUpdater(Context context, String cacheName, boolean singleUpdate) {
+ this.context = context;
+ this.cacheName = getCacheName(context, cacheName);
+ this.singleUpdate = singleUpdate;
+ }
+ public SerializeUpdater(Context context, String cacheName, String id) {
+ this(context, cacheName, id, true);
+ }
+ public SerializeUpdater(Context context, String cacheName, String id, boolean singleUpdate) {
+ this.context = context;
+ this.cacheName = getCacheName(context, cacheName, id);
+ this.singleUpdate = singleUpdate;
+ }
+
+ public ArrayList<T> getArrayList() {
+ return FileUtil.deserialize(context, cacheName, ArrayList.class);
+ }
+ public abstract boolean checkResult(T check);
+ public abstract void updateResult(List<T> objects, T result);
+ public void save(ArrayList<T> objects) {
+ FileUtil.serialize(context, objects, cacheName);
+ }
+
+ public void execute() {
+ ArrayList<T> objects = getArrayList();
+
+ // Only execute if something to check against
+ if(objects != null) {
+ List<T> results = new ArrayList<T>();
+ for(T check: objects) {
+ if(checkResult(check)) {
+ results.add(check);
+ if(singleUpdate) {
+ break;
+ }
+ }
+ }
+
+ // Iterate through and update each object matched
+ for(T result: results) {
+ updateResult(objects, result);
+ }
+
+ // Only reserialize if at least one match was found
+ if(results.size() > 0) {
+ save(objects);
+ }
+ }
+ }
+ }
+ private abstract class UserUpdater extends SerializeUpdater<User> {
+ String username;
+
+ public UserUpdater(Context context, String username) {
+ super(context, "users");
+ this.username = username;
+ }
+
+ @Override
+ public boolean checkResult(User check) {
+ return username.equals(check.getUsername());
+ }
+ }
+ private abstract class PlaylistUpdater extends SerializeUpdater<Playlist> {
+ String id;
+
+ public PlaylistUpdater(Context context, String id) {
+ super(context, "playlist");
+ this.id = id;
+ }
+
+ @Override
+ public boolean checkResult(Playlist check) {
+ return id.equals(check.getId());
+ }
+ }
+ private abstract class MusicDirectoryUpdater extends SerializeUpdater<Entry> {
+ protected MusicDirectory musicDirectory;
+
+ public MusicDirectoryUpdater(Context context, String cacheName, String id) {
+ super(context, cacheName, id, true);
+ }
+ public MusicDirectoryUpdater(Context context, String cacheName, String id, boolean singleUpdate) {
+ super(context, cacheName, id, singleUpdate);
+ }
+
+ @Override
+ public ArrayList<Entry> getArrayList() {
+ musicDirectory = FileUtil.deserialize(context, cacheName, MusicDirectory.class);
+ if(musicDirectory != null) {
+ return new ArrayList<Entry>(musicDirectory.getChildren());
+ } else {
+ return null;
+ }
+ }
+ public void save(ArrayList<Entry> objects) {
+ musicDirectory.replaceChildren(objects);
+ FileUtil.serialize(context, musicDirectory, cacheName);
+ }
+ }
+ private abstract class PlaylistDirectoryUpdater {
+ Context context;
+
+ public PlaylistDirectoryUpdater(Context context) {
+ this.context = context;
+ }
+
+ public abstract boolean checkResult(Entry check);
+ public abstract void updateResult(Entry result);
+
+ public void execute() {
+ List<Playlist> playlists = FileUtil.deserialize(context, getCacheName(context, "playlist"), ArrayList.class);
+ if(playlists == null) {
+ // No playlist list cache, nothing to update!
+ return;
+ }
+
+ for(Playlist playlist: playlists) {
+ new MusicDirectoryUpdater(context, "playlist", playlist.getId(), false) {
+ @Override
+ public boolean checkResult(Entry check) {
+ return PlaylistDirectoryUpdater.this.checkResult(check);
+ }
+
+ @Override
+ public void updateResult(List<Entry> objects, Entry result) {
+ PlaylistDirectoryUpdater.this.updateResult(result);
+ }
+ }.execute();
+ }
+ }
+ }
+ private abstract class GenericEntryUpdater {
+ Context context;
+ List<Entry> entries;
+
+ public GenericEntryUpdater(Context context, Entry entry) {
+ this.context = context;
+ this.entries = Arrays.asList(entry);
+ }
+ public GenericEntryUpdater(Context context, List<Entry> entries) {
+ this.context = context;
+ this.entries = entries;
+ }
+
+ public boolean checkResult(Entry entry, Entry check) {
+ return entry.getId().equals(check.getId());
+ }
+ public abstract void updateResult(Entry result);
+
+ public void execute() {
+ String cacheName, parent;
+ // Make sure it is up to date
+ isTagBrowsing = Util.isTagBrowsing(context, musicService.getInstance(context));
+
+ // Run through each entry, trying to update the directory it is in
+ final List<Entry> songs = new ArrayList<Entry>();
+ for(final Entry entry: entries) {
+ if(isTagBrowsing) {
+ // If starring album, needs to reference artist instead
+ if(entry.isDirectory()) {
+ if(entry.isAlbum()) {
+ cacheName = "artist";
+ parent = entry.getArtistId();
+ } else {
+ cacheName = "artists";
+ parent = null;
+ }
+ } else {
+ cacheName = "album";
+ parent = entry.getAlbumId();
+ }
+ } else {
+ if(entry.isDirectory() && !entry.isAlbum()) {
+ cacheName = "indexes";
+ parent = null;
+ } else {
+ cacheName = "directory";
+ parent = entry.getParent();
+ }
+ }
+
+ // Parent is only null when it is an artist
+ if(parent == null) {
+ new IndexesUpdater(context, cacheName) {
+ @Override
+ public boolean checkResult(Artist check) {
+ return GenericEntryUpdater.this.checkResult(entry, new Entry(check));
+ }
+
+ @Override
+ public void updateResult(List<Artist> objects, Artist result) {
+ // Don't try to put anything here, as the Entry update method will not be called since it's a artist!
+ }
+ }.execute();
+ } else {
+ new MusicDirectoryUpdater(context, cacheName, parent) {
+ @Override
+ public boolean checkResult(Entry check) {
+ return GenericEntryUpdater.this.checkResult(entry, check);
+ }
+
+ @Override
+ public void updateResult(List<Entry> objects, Entry result) {
+ GenericEntryUpdater.this.updateResult(result);
+ }
+ }.execute();
+ }
+
+ if(entry instanceof PodcastEpisode) {
+ new MusicDirectoryUpdater(context, cacheName, "p-" + entry.getParent()) {
+ @Override
+ public boolean checkResult(Entry check) {
+ return GenericEntryUpdater.this.checkResult(entry, check);
+ }
+
+ @Override
+ public void updateResult(List<Entry> objects, Entry result) {
+ GenericEntryUpdater.this.updateResult(result);
+ }
+ }.execute();
+ } else if(!entry.isDirectory()) {
+ songs.add(entry);
+ }
+ }
+
+ // Only run through playlists once and check each song against it
+ if(songs.size() > 0) {
+ new PlaylistDirectoryUpdater(context) {
+ @Override
+ public boolean checkResult(Entry check) {
+ for(Entry entry: songs) {
+ if(GenericEntryUpdater.this.checkResult(entry, check)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public void updateResult(Entry result) {
+ GenericEntryUpdater.this.updateResult(result);
+ }
+ }.execute();
+ }
+ }
+ }
+ private class BookmarkUpdater extends GenericEntryUpdater {
+ public BookmarkUpdater(Context context, Entry entry) {
+ super(context, entry);
+ }
+ public BookmarkUpdater(Context context, List<Entry> entries) {
+ super(context, entries);
+ }
+
+ @Override
+ public boolean checkResult(Entry entry, Entry check) {
+ if(entry.getId().equals(check.getId())) {
+ int position;
+ if(entry.getBookmark() == null) {
+ position = -1;
+ } else {
+ position = entry.getBookmark().getPosition();
+ }
+
+ if(position == -1 && check.getBookmark() != null) {
+ check.setBookmark(null);
+ return true;
+ } else if(position >= 0 && (check.getBookmark() == null || check.getBookmark().getPosition() != position)) {
+ Bookmark bookmark = check.getBookmark();
+
+ // Create one if empty
+ if(bookmark == null) {
+ bookmark = new Bookmark();
+ check.setBookmark(bookmark);
+ }
+
+ // Update bookmark position no matter what
+ bookmark.setPosition(position);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public void updateResult(Entry result) {
+
+ }
+ }
+ private class StarUpdater extends GenericEntryUpdater {
+ public StarUpdater(Context context, List<Entry> entries) {
+ super(context, entries);
+ }
+
+ @Override
+ public boolean checkResult(Entry entry, Entry check) {
+ if (entry.getId().equals(check.getId())) {
+ if(entry.isStarred() != check.isStarred()) {
+ check.setStarred(entry.isStarred());
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public void updateResult(Entry result) {
+
+ }
+ };
+ private abstract class IndexesUpdater extends SerializeUpdater<Artist> {
+ Indexes indexes;
+
+ IndexesUpdater(Context context, String name) {
+ super(context, name, Util.getSelectedMusicFolderId(context, musicService.getInstance(context)));
+ }
+
+ @Override
+ public ArrayList<Artist> getArrayList() {
+ indexes = FileUtil.deserialize(context, cacheName, Indexes.class);
+ if(indexes == null) {
+ return null;
+ }
+
+ ArrayList<Artist> artists = new ArrayList<Artist>();
+ artists.addAll(indexes.getArtists());
+ artists.addAll(indexes.getShortcuts());
+ return artists;
+ }
+
+ public void save(ArrayList<Artist> objects) {
+ indexes.setArtists(objects);
+ FileUtil.serialize(context, indexes, cacheName);
+ cachedIndexes.set(indexes);
+ }
+ }
+
+ private void checkSettingsChanged(Context context) {
+ int instance = musicService.getInstance(context);
+ String newUrl = musicService.getRestUrl(context, null, false);
+ boolean newIsTagBrowsing = Util.isTagBrowsing(context, instance);
+ if (!Util.equals(newUrl, restUrl) || isTagBrowsing != newIsTagBrowsing) {
+ cachedMusicFolders.clear();
+ cachedLicenseValid.clear();
+ cachedIndexes.clear();
+ cachedPlaylists.clear();
+ cachedPodcastChannels.clear();
+ restUrl = newUrl;
+ isTagBrowsing = newIsTagBrowsing;
+ }
+
+ String newMusicFolderId = Util.getSelectedMusicFolderId(context, instance);
+ if(!Util.equals(newMusicFolderId, musicFolderId)) {
+ cachedIndexes.clear();
+ musicFolderId = newMusicFolderId;
+ }
+ }
+
+ public RESTMusicService getMusicService() {
+ return musicService;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/ChromeCastController.java b/app/src/main/java/github/daneren2005/dsub/service/ChromeCastController.java
new file mode 100644
index 00000000..a729ed4e
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/ChromeCastController.java
@@ -0,0 +1,522 @@
+/*
+ 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.service;
+
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+
+import com.google.android.gms.cast.ApplicationMetadata;
+import com.google.android.gms.cast.Cast;
+import com.google.android.gms.cast.CastDevice;
+import com.google.android.gms.cast.MediaInfo;
+import com.google.android.gms.cast.MediaMetadata;
+import com.google.android.gms.cast.MediaStatus;
+import com.google.android.gms.cast.RemoteMediaPlayer;
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.api.GoogleApiClient;
+import com.google.android.gms.common.api.ResultCallback;
+import com.google.android.gms.common.api.Status;
+import com.google.android.gms.common.images.WebImage;
+
+import java.io.File;
+import java.io.IOException;
+
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.PlayerState;
+import github.daneren2005.dsub.domain.RemoteControlState;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.Util;
+import github.daneren2005.dsub.util.compat.CastCompat;
+import github.daneren2005.serverproxy.FileProxy;
+import github.daneren2005.serverproxy.ServerProxy;
+import github.daneren2005.serverproxy.WebProxy;
+
+/**
+ * Created by owner on 2/9/14.
+ */
+public class ChromeCastController extends RemoteController {
+ private static final String TAG = ChromeCastController.class.getSimpleName();
+
+ private CastDevice castDevice;
+ private GoogleApiClient apiClient;
+
+ private boolean applicationStarted = false;
+ private boolean waitingForReconnect = false;
+ private boolean error = false;
+ private boolean ignoreNextPaused = false;
+ private String sessionId;
+
+ private ServerProxy proxy;
+ private String rootLocation;
+ private RemoteMediaPlayer mediaPlayer;
+ private double gain = 0.5;
+
+ public ChromeCastController(DownloadService downloadService, CastDevice castDevice) {
+ this.downloadService = downloadService;
+ this.castDevice = castDevice;
+
+ SharedPreferences prefs = Util.getPreferences(downloadService);
+ rootLocation = prefs.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, null);
+ }
+
+ @Override
+ public void create(boolean playing, int seconds) {
+ downloadService.setPlayerState(PlayerState.PREPARING);
+
+ ConnectionCallbacks connectionCallbacks = new ConnectionCallbacks(playing, seconds);
+ ConnectionFailedListener connectionFailedListener = new ConnectionFailedListener();
+ Cast.Listener castClientListener = new Cast.Listener() {
+ @Override
+ public void onApplicationStatusChanged() {
+ if (apiClient != null && apiClient.isConnected()) {
+ Log.i(TAG, "onApplicationStatusChanged: " + Cast.CastApi.getApplicationStatus(apiClient));
+ }
+ }
+
+ @Override
+ public void onVolumeChanged() {
+ if (apiClient != null && applicationStarted) {
+ try {
+ gain = Cast.CastApi.getVolume(apiClient);
+ } catch (Exception e) {
+ Log.w(TAG, "Failed to get volume");
+ }
+ }
+ }
+
+ @Override
+ public void onApplicationDisconnected(int errorCode) {
+ shutdownInternal();
+ }
+
+ };
+
+ Cast.CastOptions.Builder apiOptionsBuilder = Cast.CastOptions.builder(castDevice, castClientListener).setVerboseLoggingEnabled(true);
+ apiClient = new GoogleApiClient.Builder(downloadService).useDefaultAccount()
+ .addApi(Cast.API, apiOptionsBuilder.build())
+ .addConnectionCallbacks(connectionCallbacks)
+ .addOnConnectionFailedListener(connectionFailedListener)
+ .build();
+
+ apiClient.connect();
+ }
+
+ @Override
+ public void start() {
+ if(error) {
+ error = false;
+ Log.w(TAG, "Attempting to restart song");
+ startSong(downloadService.getCurrentPlaying(), true, 0);
+ return;
+ }
+
+ try {
+ mediaPlayer.play(apiClient);
+ } catch(Exception e) {
+ Log.e(TAG, "Failed to start");
+ }
+ }
+
+ @Override
+ public void stop() {
+ try {
+ mediaPlayer.pause(apiClient);
+ } catch(Exception e) {
+ Log.e(TAG, "Failed to pause");
+ }
+ }
+
+ @Override
+ public void shutdown() {
+ try {
+ if(mediaPlayer != null && !error) {
+ mediaPlayer.stop(apiClient);
+ }
+ } catch(Exception e) {
+ Log.e(TAG, "Failed to stop mediaPlayer", e);
+ }
+
+ try {
+ if(apiClient != null) {
+ Cast.CastApi.stopApplication(apiClient);
+ Cast.CastApi.removeMessageReceivedCallbacks(apiClient, mediaPlayer.getNamespace());
+ mediaPlayer = null;
+ applicationStarted = false;
+ }
+ } catch(Exception e) {
+ Log.e(TAG, "Failed to shutdown application", e);
+ }
+
+ if(apiClient != null && apiClient.isConnected()) {
+ apiClient.disconnect();
+ }
+ apiClient = null;
+
+ if(proxy != null) {
+ proxy.stop();
+ proxy = null;
+ }
+ }
+
+ private void shutdownInternal() {
+ // This will call this.shutdown() indirectly
+ downloadService.setRemoteEnabled(RemoteControlState.LOCAL, null);
+ }
+
+ @Override
+ public void updatePlaylist() {
+ if(downloadService.getCurrentPlaying() == null) {
+ startSong(null, false, 0);
+ }
+ }
+
+ @Override
+ public void changePosition(int seconds) {
+ try {
+ mediaPlayer.seek(apiClient, seconds * 1000L);
+ } catch(Exception e) {
+ Log.e(TAG, "FAiled to seek to " + seconds);
+ }
+ }
+
+ @Override
+ public void changeTrack(int index, DownloadFile song) {
+ startSong(song, true, 0);
+ }
+
+ @Override
+ public void setVolume(int volume) {
+ gain = volume / 10.0;
+
+ try {
+ Cast.CastApi.setVolume(apiClient, gain);
+ } catch(Exception e) {
+ Log.e(TAG, "Failed to the volume");
+ }
+ }
+ @Override
+ public void updateVolume(boolean up) {
+ double delta = up ? 0.1 : -0.1;
+ gain += delta;
+ gain = Math.max(gain, 0.0);
+ gain = Math.min(gain, 1.0);
+
+ try {
+ Cast.CastApi.setVolume(apiClient, gain);
+ } catch(Exception e) {
+ Log.e(TAG, "Failed to the volume");
+ }
+ }
+ @Override
+ public double getVolume() {
+ return Cast.CastApi.getVolume(apiClient);
+ }
+
+ @Override
+ public int getRemotePosition() {
+ if(mediaPlayer != null) {
+ return (int) (mediaPlayer.getApproximateStreamPosition() / 1000L);
+ } else {
+ return 0;
+ }
+ }
+
+ @Override
+ public int getRemoteDuration() {
+ if(mediaPlayer != null) {
+ return (int) (mediaPlayer.getStreamDuration() / 1000L);
+ } else {
+ return 0;
+ }
+ }
+
+ void startSong(DownloadFile currentPlaying, boolean autoStart, int position) {
+ if(currentPlaying == null) {
+ try {
+ if (mediaPlayer != null && !error) {
+ mediaPlayer.stop(apiClient);
+ }
+ } catch(Exception e) {
+ // Just means it didn't need to be stopped
+ }
+ downloadService.setPlayerState(PlayerState.IDLE);
+ return;
+ }
+ downloadService.setPlayerState(PlayerState.PREPARING);
+ MusicDirectory.Entry song = currentPlaying.getSong();
+
+ try {
+ MusicService musicService = MusicServiceFactory.getMusicService(downloadService);
+ String url;
+ // Offline, use file proxy
+ if(Util.isOffline(downloadService) || song.getId().indexOf(rootLocation) != -1) {
+ if(proxy == null) {
+ proxy = new FileProxy(downloadService);
+ proxy.start();
+ }
+
+ // Offline song
+ if(song.getId().indexOf(rootLocation) != -1) {
+ url = proxy.getPublicAddress(song.getId());
+ } else {
+ // Playing online song in offline mode
+ url = proxy.getPublicAddress(currentPlaying.getCompleteFile().getPath());
+ }
+ } else {
+ // Check if we want a proxy going still
+ if(Util.isCastProxy(downloadService)) {
+ if(proxy instanceof FileProxy) {
+ proxy.stop();
+ proxy = null;
+ }
+
+ if(proxy == null) {
+ proxy = createWebProxy();
+ proxy.start();
+ }
+ } else if(proxy != null) {
+ proxy.stop();
+ proxy = null;
+ }
+
+ if(song.isVideo()) {
+ url = musicService.getHlsUrl(song.getId(), currentPlaying.getBitRate(), downloadService);
+ } else {
+ url = musicService.getMusicUrl(downloadService, song, currentPlaying.getBitRate());
+ }
+
+ // If proxy is going, it is a WebProxy
+ if(proxy != null) {
+ url = proxy.getPublicAddress(url);
+ }
+ }
+
+ // Setup song/video information
+ MediaMetadata meta = new MediaMetadata(song.isVideo() ? MediaMetadata.MEDIA_TYPE_MOVIE : MediaMetadata.MEDIA_TYPE_MUSIC_TRACK);
+ meta.putString(MediaMetadata.KEY_TITLE, song.getTitle());
+ if(song.getTrack() != null) {
+ meta.putInt(MediaMetadata.KEY_TRACK_NUMBER, song.getTrack());
+ }
+ if(!song.isVideo()) {
+ meta.putString(MediaMetadata.KEY_ARTIST, song.getArtist());
+ meta.putString(MediaMetadata.KEY_ALBUM_ARTIST, song.getArtist());
+ meta.putString(MediaMetadata.KEY_ALBUM_TITLE, song.getAlbum());
+
+ String coverArt = "";
+ if(proxy == null || proxy instanceof WebProxy) {
+ coverArt = musicService.getCoverArtUrl(downloadService, song);
+
+ // If proxy is going, it is a web proxy
+ if(proxy != null) {
+ coverArt = proxy.getPublicAddress(coverArt);
+ }
+
+ meta.addImage(new WebImage(Uri.parse(coverArt)));
+ } else {
+ File coverArtFile = FileUtil.getAlbumArtFile(downloadService, song);
+ if(coverArtFile != null && coverArtFile.exists()) {
+ coverArt = proxy.getPublicAddress(coverArtFile.getPath());
+ meta.addImage(new WebImage(Uri.parse(coverArt)));
+ }
+ }
+ }
+
+ String contentType;
+ if(song.isVideo()) {
+ contentType = "application/x-mpegURL";
+ }
+ else if(song.getTranscodedContentType() != null) {
+ contentType = song.getTranscodedContentType();
+ } else if(song.getContentType() != null) {
+ contentType = song.getContentType();
+ } else {
+ contentType = "audio/mpeg";
+ }
+
+ // Load it into a MediaInfo wrapper
+ MediaInfo mediaInfo = new MediaInfo.Builder(url)
+ .setContentType(contentType)
+ .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
+ .setMetadata(meta)
+ .build();
+
+ if(autoStart) {
+ ignoreNextPaused = true;
+ }
+
+ mediaPlayer.load(apiClient, mediaInfo, autoStart, position * 1000L).setResultCallback(new ResultCallback<RemoteMediaPlayer.MediaChannelResult>() {
+ @Override
+ public void onResult(RemoteMediaPlayer.MediaChannelResult result) {
+ if (result.getStatus().isSuccess()) {
+ // Handled in other handler
+ } else {
+ Log.e(TAG, "Failed to load: " + result.getStatus().toString());
+ failedLoad();
+ }
+ }
+ });
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Problem occurred with media during loading", e);
+ failedLoad();
+ } catch (Exception e) {
+ Log.e(TAG, "Problem opening media during loading", e);
+ failedLoad();
+ }
+ }
+
+ private void failedLoad() {
+ Util.toast(downloadService, downloadService.getResources().getString(R.string.download_failed_to_load));
+ downloadService.setPlayerState(PlayerState.STOPPED);
+ error = true;
+ }
+
+
+ private class ConnectionCallbacks implements GoogleApiClient.ConnectionCallbacks {
+ private boolean isPlaying;
+ private int position;
+ private ResultCallback<Cast.ApplicationConnectionResult> resultCallback;
+
+ ConnectionCallbacks(boolean isPlaying, int position) {
+ this.isPlaying = isPlaying;
+ this.position = position;
+
+ resultCallback = new ResultCallback<Cast.ApplicationConnectionResult>() {
+ @Override
+ public void onResult(Cast.ApplicationConnectionResult result) {
+ Status status = result.getStatus();
+ if (status.isSuccess()) {
+ ApplicationMetadata applicationMetadata = result.getApplicationMetadata();
+ sessionId = result.getSessionId();
+ String applicationStatus = result.getApplicationStatus();
+ boolean wasLaunched = result.getWasLaunched();
+
+ applicationStarted = true;
+ setupChannel();
+ } else {
+ shutdownInternal();
+ }
+ }
+ };
+ }
+
+ @Override
+ public void onConnected(Bundle connectionHint) {
+ if (waitingForReconnect) {
+ Log.i(TAG, "Reconnecting");
+ reconnectApplication();
+ } else {
+ launchApplication();
+ }
+ }
+
+ @Override
+ public void onConnectionSuspended(int cause) {
+ Log.w(TAG, "Connection suspended");
+ isPlaying = downloadService.getPlayerState() == PlayerState.STARTED;
+ position = getRemotePosition();
+ waitingForReconnect = true;
+ }
+
+ void launchApplication() {
+ try {
+ Cast.CastApi.launchApplication(apiClient, CastCompat.APPLICATION_ID, false).setResultCallback(resultCallback);
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to launch application", e);
+ }
+ }
+ void reconnectApplication() {
+ try {
+ Cast.CastApi.joinApplication(apiClient, CastCompat.APPLICATION_ID, sessionId).setResultCallback(resultCallback);
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to reconnect application", e);
+ }
+ }
+ void setupChannel() {
+ if(!waitingForReconnect) {
+ mediaPlayer = new RemoteMediaPlayer();
+ mediaPlayer.setOnStatusUpdatedListener(new RemoteMediaPlayer.OnStatusUpdatedListener() {
+ @Override
+ public void onStatusUpdated() {
+ MediaStatus mediaStatus = mediaPlayer.getMediaStatus();
+ if (mediaStatus == null) {
+ return;
+ }
+
+ switch (mediaStatus.getPlayerState()) {
+ case MediaStatus.PLAYER_STATE_PLAYING:
+ if (ignoreNextPaused) {
+ ignoreNextPaused = false;
+ }
+ downloadService.setPlayerState(PlayerState.STARTED);
+ break;
+ case MediaStatus.PLAYER_STATE_PAUSED:
+ if (!ignoreNextPaused) {
+ downloadService.setPlayerState(PlayerState.PAUSED);
+ }
+ break;
+ case MediaStatus.PLAYER_STATE_BUFFERING:
+ downloadService.setPlayerState(PlayerState.PREPARING);
+ break;
+ case MediaStatus.PLAYER_STATE_IDLE:
+ if (mediaStatus.getIdleReason() == MediaStatus.IDLE_REASON_FINISHED) {
+ downloadService.setPlayerState(PlayerState.COMPLETED);
+ downloadService.postPlayCleanup();
+ downloadService.onSongCompleted();
+ } else if (mediaStatus.getIdleReason() == MediaStatus.IDLE_REASON_INTERRUPTED) {
+ if (downloadService.getPlayerState() != PlayerState.PREPARING) {
+ downloadService.setPlayerState(PlayerState.PREPARING);
+ }
+ } else if (mediaStatus.getIdleReason() == MediaStatus.IDLE_REASON_ERROR) {
+ Log.e(TAG, "Idle due to unknown error");
+ downloadService.setPlayerState(PlayerState.COMPLETED);
+ downloadService.next();
+ } else {
+ Log.w(TAG, "Idle reason: " + mediaStatus.getIdleReason());
+ downloadService.setPlayerState(PlayerState.IDLE);
+ }
+ break;
+ }
+ }
+ });
+ }
+
+ try {
+ Cast.CastApi.setMessageReceivedCallbacks(apiClient, mediaPlayer.getNamespace(), mediaPlayer);
+ } catch (IOException e) {
+ Log.e(TAG, "Exception while creating channel", e);
+ }
+
+ if(!waitingForReconnect) {
+ DownloadFile currentPlaying = downloadService.getCurrentPlaying();
+ startSong(currentPlaying, isPlaying, position);
+ }
+ if(waitingForReconnect) {
+ waitingForReconnect = false;
+ }
+ }
+ }
+
+ private class ConnectionFailedListener implements GoogleApiClient.OnConnectionFailedListener {
+ @Override
+ public void onConnectionFailed(ConnectionResult result) {
+ shutdownInternal();
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/DLNAController.java b/app/src/main/java/github/daneren2005/dsub/service/DLNAController.java
new file mode 100644
index 00000000..64abce8a
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/DLNAController.java
@@ -0,0 +1,687 @@
+/*
+ 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.service;
+
+import android.content.SharedPreferences;
+import android.os.Looper;
+import android.util.Log;
+
+import org.fourthline.cling.controlpoint.ActionCallback;
+import org.fourthline.cling.controlpoint.ControlPoint;
+import org.fourthline.cling.controlpoint.SubscriptionCallback;
+import org.fourthline.cling.model.action.ActionInvocation;
+import org.fourthline.cling.model.gena.CancelReason;
+import org.fourthline.cling.model.gena.GENASubscription;
+import org.fourthline.cling.model.message.UpnpResponse;
+import org.fourthline.cling.model.meta.Action;
+import org.fourthline.cling.model.meta.Service;
+import org.fourthline.cling.model.meta.StateVariable;
+import org.fourthline.cling.model.state.StateVariableValue;
+import org.fourthline.cling.model.types.ServiceType;
+import org.fourthline.cling.model.types.UnsignedIntegerFourBytes;
+import org.fourthline.cling.support.avtransport.callback.GetPositionInfo;
+import org.fourthline.cling.support.avtransport.callback.Pause;
+import org.fourthline.cling.support.avtransport.callback.Play;
+import org.fourthline.cling.support.avtransport.callback.Seek;
+import org.fourthline.cling.support.avtransport.callback.SetAVTransportURI;
+import org.fourthline.cling.support.avtransport.callback.Stop;
+import org.fourthline.cling.support.avtransport.lastchange.AVTransportLastChangeParser;
+import org.fourthline.cling.support.avtransport.lastchange.AVTransportVariable;
+import org.fourthline.cling.support.contentdirectory.DIDLParser;
+import org.fourthline.cling.support.lastchange.LastChange;
+import org.fourthline.cling.support.model.DIDLContent;
+import org.fourthline.cling.support.model.DIDLObject;
+import org.fourthline.cling.support.model.PositionInfo;
+import org.fourthline.cling.support.model.Res;
+import org.fourthline.cling.support.model.SeekMode;
+import org.fourthline.cling.support.model.item.Item;
+import org.fourthline.cling.support.model.item.MusicTrack;
+import org.fourthline.cling.support.model.item.VideoItem;
+import org.fourthline.cling.support.renderingcontrol.callback.SetVolume;
+import org.seamless.util.MimeType;
+
+import java.io.File;
+import java.net.URI;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Map;
+import java.util.TimeZone;
+import java.util.concurrent.atomic.AtomicLong;
+
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.DLNADevice;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.PlayerState;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.Pair;
+import github.daneren2005.dsub.util.Util;
+import github.daneren2005.serverproxy.FileProxy;
+import github.daneren2005.serverproxy.ServerProxy;
+import github.daneren2005.serverproxy.WebProxy;
+
+public class DLNAController extends RemoteController {
+ private static final String TAG = DLNAController.class.getSimpleName();
+ private static final long SEARCH_UPDATE_INTERVAL_SECONDS = 10L * 60L * 1000L;
+ private static final long STATUS_UPDATE_INTERVAL_SECONDS = 3000L;
+
+ DLNADevice device;
+ ControlPoint controlPoint;
+ SubscriptionCallback callback;
+ boolean supportsSeek = false;
+ boolean supportsSetupNext = false;
+
+ private ServerProxy proxy;
+ String rootLocation = "";
+ boolean error = false;
+
+ final AtomicLong lastUpdate = new AtomicLong();
+ int currentPosition = 0;
+ String currentPlayingURI;
+ String nextPlayingURI;
+ DownloadFile nextPlaying;
+ boolean running = true;
+ boolean hasDuration = false;
+ Runnable searchDLNA = new Runnable() {
+ @Override
+ public void run() {
+ if(controlPoint == null || !running) {
+ return;
+ }
+
+ controlPoint.search();
+ downloadService.postDelayed(searchDLNA, SEARCH_UPDATE_INTERVAL_SECONDS);
+ }
+ };
+
+ public DLNAController(DownloadService downloadService, ControlPoint controlPoint, DLNADevice device) {
+ this.downloadService = downloadService;
+ this.controlPoint = controlPoint;
+ this.device = device;
+
+ SharedPreferences prefs = Util.getPreferences(downloadService);
+ rootLocation = prefs.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, null);
+ nextSupported = true;
+ }
+
+ @Override
+ public void create(final boolean playing, final int seconds) {
+ downloadService.setPlayerState(PlayerState.PREPARING);
+
+ callback = new SubscriptionCallback(getTransportService(), 600) {
+ @Override
+ protected void failed(GENASubscription genaSubscription, UpnpResponse upnpResponse, Exception e, String msg) {
+ Log.w(TAG, "Register subscription callback failed: " + msg, e);
+ }
+
+ @Override
+ protected void established(GENASubscription genaSubscription) {
+ Action seekAction = genaSubscription.getService().getAction("Seek");
+ if(seekAction != null) {
+ StateVariable seekMode = genaSubscription.getService().getStateVariable("A_ARG_TYPE_SeekMode");
+ for(String allowedValue: seekMode.getTypeDetails().getAllowedValues()) {
+ if("REL_TIME".equals(allowedValue)) {
+ supportsSeek = true;
+ }
+ }
+ }
+ Action setupNextAction = genaSubscription.getService().getAction("SetNextAVTransportURI");
+ if(setupNextAction != null) {
+ supportsSetupNext = true;
+ }
+
+ startSong(downloadService.getCurrentPlaying(), playing, seconds);
+ downloadService.postDelayed(searchDLNA, SEARCH_UPDATE_INTERVAL_SECONDS);
+ }
+
+ @Override
+ protected void ended(GENASubscription genaSubscription, CancelReason cancelReason, UpnpResponse upnpResponse) {
+ Log.i(TAG, "Ended subscription");
+ if(cancelReason != null) {
+ Log.i(TAG, "Cancel Reason: " + cancelReason.toString());
+ }
+ if(upnpResponse != null) {
+ Log.i(TAG, "Reponse Message: " + upnpResponse.getStatusMessage());
+ Log.i(TAG, "Response Details: " + upnpResponse.getResponseDetails());
+ }
+ }
+
+ @Override
+ protected void eventReceived(GENASubscription genaSubscription) {
+ Map<String, StateVariableValue> m = genaSubscription.getCurrentValues();
+ try {
+ LastChange lastChange = new LastChange(new AVTransportLastChangeParser(), m.get("LastChange").toString());
+ if (lastChange.getEventedValue(0, AVTransportVariable.TransportState.class) == null) {
+ return;
+ }
+
+ switch (lastChange.getEventedValue(0, AVTransportVariable.TransportState.class).getValue()) {
+ case PLAYING:
+ downloadService.setPlayerState(PlayerState.STARTED);
+
+ // Try to setup next playing after playback start has been registered
+ if(supportsSetupNext && downloadService.getNextPlayerState() == PlayerState.IDLE) {
+ downloadService.setNextPlaying();
+ }
+ break;
+ case PAUSED_PLAYBACK:
+ downloadService.setPlayerState(PlayerState.PAUSED);
+ break;
+ case STOPPED:
+ boolean failed = false;
+ for(StateVariableValue val: m.values()) {
+ if(val.toString().indexOf("TransportStatus val=\"ERROR_OCCURRED\"") != -1) {
+ Log.w(TAG, "Failed to load with event: " + val.toString());
+ failed = true;
+ }
+ }
+
+ if(failed) {
+ failedLoad();
+ } else if(downloadService.getPlayerState() == PlayerState.STARTED) {
+ // Played until the end
+ downloadService.setPlayerState(PlayerState.COMPLETED);
+ downloadService.postPlayCleanup();
+ downloadService.onSongCompleted();
+ } else {
+ downloadService.setPlayerState(PlayerState.STOPPED);
+ }
+ break;
+ case TRANSITIONING:
+ downloadService.setPlayerState(PlayerState.PREPARING);
+ break;
+ case NO_MEDIA_PRESENT:
+ downloadService.setPlayerState(PlayerState.IDLE);
+ break;
+ default:
+ }
+ }
+ catch (Exception e) {
+ Log.w(TAG, "Failed to parse UPNP event", e);
+ failedLoad();
+ }
+ }
+
+ @Override
+ protected void eventsMissed(GENASubscription genaSubscription, int i) {
+ Log.w(TAG, "Event missed: " + i);
+ }
+ };
+ controlPoint.execute(callback);
+ }
+
+ @Override
+ public void start() {
+ if(error) {
+ Log.w(TAG, "Attempting to restart song");
+ startSong(downloadService.getCurrentPlaying(), true, 0);
+ return;
+ }
+
+ try {
+ controlPoint.execute(new Play(getTransportService()) {
+ @Override
+ public void success(ActionInvocation invocation) {
+ lastUpdate.set(System.currentTimeMillis());
+ downloadService.setPlayerState(PlayerState.STARTED);
+ }
+
+ @Override
+ public void failure(ActionInvocation actionInvocation, UpnpResponse upnpResponse, String msg) {
+ Log.w(TAG, "Failed to start playing: " + msg);
+ failedLoad();
+ }
+ });
+ } catch(Exception e) {
+ Log.w(TAG, "Failed to start", e);
+ }
+ }
+
+ @Override
+ public void stop() {
+ try {
+ controlPoint.execute(new Pause(getTransportService()) {
+ @Override
+ public void success(ActionInvocation invocation) {
+ int secondsSinceLastUpdate = (int) ((System.currentTimeMillis() - lastUpdate.get()) / 1000L);
+ currentPosition += secondsSinceLastUpdate;
+
+ downloadService.setPlayerState(PlayerState.PAUSED);
+ }
+
+ @Override
+ public void failure(ActionInvocation actionInvocation, UpnpResponse upnpResponse, String msg) {
+ Log.w(TAG, "Failed to pause playing: " + msg);
+ }
+ });
+ } catch(Exception e) {
+ Log.w(TAG, "Failed to stop", e);
+ }
+ }
+
+ @Override
+ public void shutdown() {
+ try {
+ controlPoint.execute(new Stop(getTransportService()) {
+ @Override
+ public void failure(ActionInvocation invocation, org.fourthline.cling.model.message.UpnpResponse operation, String defaultMessage) {
+ Log.w(TAG, "Stop failed: " + defaultMessage);
+ }
+ });
+ } catch(Exception e) {
+ Log.w(TAG, "Failed to shutdown", e);
+ }
+
+ if(callback != null) {
+ callback.end();
+ callback = null;
+ }
+
+ if(proxy != null) {
+ proxy.stop();
+ proxy = null;
+ }
+
+ running = false;
+ }
+
+ @Override
+ public void updatePlaylist() {
+ if(downloadService.getCurrentPlaying() == null) {
+ startSong(null, false, 0);
+ }
+ }
+
+ @Override
+ public void changePosition(int seconds) {
+ SimpleDateFormat df = new SimpleDateFormat("HH:mm:ss");
+ df.setTimeZone(TimeZone.getTimeZone("UTC"));
+ controlPoint.execute(new Seek(getTransportService(), SeekMode.REL_TIME, df.format(new Date(seconds * 1000))) {
+ @SuppressWarnings("rawtypes")
+ @Override
+ public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMessage) {
+ Log.w(TAG, "Seek failed: " + defaultMessage);
+ }
+ });
+ }
+
+ @Override
+ public void changeTrack(int index, DownloadFile song) {
+ startSong(song, true, 0);
+ }
+
+ @Override
+ public void changeNextTrack(DownloadFile song) {
+ setupNextSong(song);
+ }
+
+ @Override
+ public void setVolume(int volume) {
+ if(volume < 0) {
+ volume = 0;
+ } else if(volume > device.volumeMax) {
+ volume = device.volumeMax;
+ }
+
+ device.volume = volume;
+ try {
+ controlPoint.execute(new SetVolume(device.renderer.findService(new ServiceType("schemas-upnp-org", "RenderingControl")), volume) {
+ @SuppressWarnings("rawtypes")
+ @Override
+ public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMessage) {
+ Log.w(TAG, "Set volume failed: " + defaultMessage);
+ }
+ });
+ } catch(Exception e) {
+ Log.w(TAG, "Failed to set volume");
+ }
+ }
+
+ @Override
+ public void updateVolume(boolean up) {
+ int increment = device.volumeMax / 10;
+ setVolume(device.volume + (up ? increment : -increment));
+ }
+
+ @Override
+ public double getVolume() {
+ return device.volume;
+ }
+
+ @Override
+ public int getRemotePosition() {
+ if(downloadService.getPlayerState() == PlayerState.STARTED) {
+ int secondsSinceLastUpdate = (int) ((System.currentTimeMillis() - lastUpdate.get()) / 1000L);
+ return currentPosition + secondsSinceLastUpdate;
+ } else {
+ return currentPosition;
+ }
+ }
+
+ @Override
+ public boolean isSeekable() {
+ return supportsSeek && hasDuration;
+ }
+
+ private void startSong(final DownloadFile currentPlaying, final boolean autoStart, final int position) {
+ try {
+ controlPoint.execute(new Stop(getTransportService()) {
+ @Override
+ public void success(ActionInvocation invocation) {
+ startSongRemote(currentPlaying, autoStart, position);
+ }
+
+ @Override
+ public void failure(ActionInvocation invocation, org.fourthline.cling.model.message.UpnpResponse operation, String defaultMessage) {
+ Log.w(TAG, "Stop failed before startSong: " + defaultMessage);
+ startSongRemote(currentPlaying, autoStart, position);
+ }
+ });
+ } catch(Exception e) {
+ Log.w(TAG, "Failed to stop before startSong", e);
+ startSongRemote(currentPlaying, autoStart, position);
+ }
+ }
+ private void startSongRemote(final DownloadFile currentPlaying, final boolean autoStart, final int position) {
+ if(currentPlaying == null) {
+ downloadService.setPlayerState(PlayerState.IDLE);
+ return;
+ }
+ error = false;
+
+ downloadService.setPlayerState(PlayerState.PREPARING);
+
+ try {
+ Pair<String, String> songInfo = getSongInfo(currentPlaying);
+
+ currentPlayingURI = songInfo.getFirst();
+ downloadService.setNextPlayerState(PlayerState.IDLE);
+ controlPoint.execute(new SetAVTransportURI(getTransportService(), songInfo.getFirst(), songInfo.getSecond()) {
+ @Override
+ public void success(ActionInvocation invocation) {
+ if(position != 0) {
+ changePosition(position);
+ }
+
+ if (autoStart) {
+ start();
+ } else {
+ downloadService.setPlayerState(PlayerState.PAUSED);
+ }
+
+ currentPosition = position;
+ lastUpdate.set(System.currentTimeMillis());
+ getUpdatedStatus();
+ }
+
+ @Override
+ public void failure(ActionInvocation actionInvocation, UpnpResponse upnpResponse, String msg) {
+ Log.w(TAG, "Set URI failed: " + msg);
+ failedLoad();
+ }
+ });
+ } catch (Exception e) {
+ Log.w(TAG, "Failed startSong", e);
+ failedLoad();
+ }
+ }
+ private void setupNextSong(final DownloadFile nextPlaying) {
+ this.nextPlaying = nextPlaying;
+ nextPlayingURI = null;
+ if(nextPlaying == null) {
+ downloadService.setNextPlayerState(PlayerState.IDLE);
+ Log.i(TAG, "Nothing to play next");
+ return;
+ }
+
+ downloadService.setNextPlayerState(PlayerState.PREPARING);
+ try {
+ Pair<String, String> songInfo = getSongInfo(nextPlaying);
+
+ nextPlayingURI = songInfo.getFirst();
+ controlPoint.execute(new SetNextAVTransportURI(getTransportService(), songInfo.getFirst(), songInfo.getSecond()) {
+ @Override
+ public void success(ActionInvocation invocation) {
+ downloadService.setNextPlayerState(PlayerState.PREPARED);
+ }
+
+ @Override
+ public void failure(ActionInvocation actionInvocation, UpnpResponse upnpResponse, String msg) {
+ Log.w(TAG, "Set next URI failed: " + msg);
+ nextPlayingURI = null;
+ DLNAController.this.nextPlaying = null;
+ downloadService.setNextPlayerState(PlayerState.IDLE);
+ }
+ });
+ } catch (Exception e) {
+ Log.w(TAG, "Failed to setup next song", e);
+ nextPlayingURI = null;
+ this.nextPlaying = null;
+ downloadService.setNextPlayerState(PlayerState.IDLE);
+ }
+ }
+
+ Pair<String, String> getSongInfo(final DownloadFile downloadFile) throws Exception {
+ MusicDirectory.Entry song = downloadFile.getSong();
+
+ // Get url for entry
+ MusicService musicService = MusicServiceFactory.getMusicService(downloadService);
+ String url;
+ // In offline mode or playing offline song
+ if(Util.isOffline(downloadService) || song.getId().indexOf(rootLocation) != -1) {
+ if(proxy == null) {
+ proxy = new FileProxy(downloadService);
+ proxy.start();
+ }
+
+ // Offline song
+ if(song.getId().indexOf(rootLocation) != -1) {
+ url = proxy.getPublicAddress(song.getId());
+ } else {
+ // Playing online song in offline mode
+ url = proxy.getPublicAddress(downloadFile.getCompleteFile().getPath());
+ }
+ } else {
+ // Check if we want a proxy going still
+ if(Util.isCastProxy(downloadService)) {
+ if(proxy instanceof FileProxy) {
+ proxy.stop();
+ proxy = null;
+ }
+
+ if(proxy == null) {
+ proxy = createWebProxy();
+ proxy.start();
+ }
+ } else if(proxy != null) {
+ proxy.stop();
+ proxy = null;
+ }
+
+ if(song.isVideo()) {
+ url = musicService.getHlsUrl(song.getId(), downloadFile.getBitRate(), downloadService);
+ } else {
+ url = musicService.getMusicUrl(downloadService, song, downloadFile.getBitRate());
+ }
+
+ // If proxy is going, it is a WebProxy
+ if(proxy != null) {
+ url = proxy.getPublicAddress(url);
+ }
+ }
+
+ // Create metadata for entry
+ Item track;
+ if(song.isVideo()) {
+ track = new VideoItem(song.getId(), song.getParent(), song.getTitle(), song.getArtist());
+ } else {
+ String contentType = null;
+ if(song.getTranscodedContentType() != null) {
+ contentType = song.getTranscodedContentType();
+ } else if(song.getContentType() != null) {
+ contentType = song.getContentType();
+ }
+
+ MimeType mimeType;
+ // If we can parse the content type, use it instead of hard coding
+ if(contentType != null && contentType.indexOf("/") != -1 && contentType.indexOf("/") != (contentType.length() - 1)) {
+ String[] typeParts = contentType.split("/");
+ mimeType = new MimeType(typeParts[0], typeParts[1]);
+ } else {
+ mimeType = new MimeType("audio", "mpeg");
+ }
+
+ Res res = new Res(mimeType, song.getSize(), url);
+
+ if(song.getDuration() != null) {
+ SimpleDateFormat df = new SimpleDateFormat("HH:mm:ss");
+ df.setTimeZone(TimeZone.getTimeZone("UTC"));
+ res.setDuration(df.format(new Date(song.getDuration() * 1000)));
+ }
+
+ MusicTrack musicTrack = new MusicTrack(song.getId(), song.getParent(), song.getTitle(), song.getArtist(), song.getAlbum(), song.getArtist(), res);
+ musicTrack.setOriginalTrackNumber(song.getTrack());
+
+ if(song.getCoverArt() != null) {
+ String coverArt = null;
+ if(proxy == null || proxy instanceof WebProxy) {
+ coverArt = musicService.getCoverArtUrl(downloadService, song);
+
+ // If proxy is going, it is a web proxy
+ if(proxy != null) {
+ coverArt = proxy.getPublicAddress(coverArt);
+ }
+ } else {
+ File coverArtFile = FileUtil.getAlbumArtFile(downloadService, song);
+ if(coverArtFile != null && coverArtFile.exists()) {
+ coverArt = proxy.getPublicAddress(coverArtFile.getPath());
+ }
+ }
+
+ if(coverArt != null) {
+ DIDLObject.Property.UPNP.ALBUM_ART_URI albumArtUri = new DIDLObject.Property.UPNP.ALBUM_ART_URI(URI.create(coverArt));
+ musicTrack.addProperty(albumArtUri);
+ }
+ }
+
+ track = musicTrack;
+ }
+
+ DIDLParser parser = new DIDLParser();
+ DIDLContent didl = new DIDLContent();
+ didl.addItem(track);
+
+ String metadata = "";
+ try {
+ metadata = parser.generate(didl);
+ } catch(Exception e) {
+ Log.w(TAG, "Metadata generation failed", e);
+ }
+
+ return new Pair<String, String>(url, metadata);
+ }
+
+ private void failedLoad() {
+ downloadService.setPlayerState(PlayerState.STOPPED);
+ error = true;
+
+ if(Looper.myLooper() != Looper.getMainLooper()) {
+ downloadService.post(new Runnable() {
+ @Override
+ public void run() {
+ Util.toast(downloadService, downloadService.getResources().getString(R.string.download_failed_to_load));
+ }
+ });
+ } else {
+ Util.toast(downloadService, downloadService.getResources().getString(R.string.download_failed_to_load));
+ }
+ }
+
+ private Service getTransportService() {
+ return device.renderer.findService(new ServiceType("schemas-upnp-org", "AVTransport"));
+ }
+
+ private void getUpdatedStatus() {
+ // Don't care if shutdown in the meantime
+ if(!running) {
+ return;
+ }
+
+ controlPoint.execute(new GetPositionInfo(getTransportService()) {
+ @Override
+ public void received(ActionInvocation actionInvocation, PositionInfo positionInfo) {
+ // Don't care if shutdown in the meantime
+ if(!running) {
+ return;
+ }
+
+ long duration = positionInfo.getTrackDurationSeconds();
+ hasDuration = duration > 0;
+
+ lastUpdate.set(System.currentTimeMillis());
+
+ // Let's get the updated position
+ currentPosition = (int) positionInfo.getTrackElapsedSeconds();
+
+ if(positionInfo.getTrackURI() != null && positionInfo.getTrackURI().equals(nextPlayingURI) && downloadService.getNextPlayerState() == PlayerState.PREPARED) {
+ downloadService.setCurrentPlaying(nextPlaying, true);
+ downloadService.setPlayerState(PlayerState.STARTED);
+ downloadService.setNextPlaying();
+ }
+
+ downloadService.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ getUpdatedStatus();
+ }
+ }, STATUS_UPDATE_INTERVAL_SECONDS);
+ }
+
+ @Override
+ public void failure(ActionInvocation actionInvocation, UpnpResponse upnpResponse, String s) {
+ Log.w(TAG, "Failed to get an update");
+
+ downloadService.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ getUpdatedStatus();
+ }
+ }, STATUS_UPDATE_INTERVAL_SECONDS);
+ }
+ });
+ }
+
+ private abstract class SetNextAVTransportURI extends ActionCallback {
+ public SetNextAVTransportURI(Service service, String uri) {
+ this(new UnsignedIntegerFourBytes(0), service, uri, null);
+ }
+
+ public SetNextAVTransportURI(Service service, String uri, String metadata) {
+ this(new UnsignedIntegerFourBytes(0), service, uri, metadata);
+ }
+
+ public SetNextAVTransportURI(UnsignedIntegerFourBytes instanceId, Service service, String uri) {
+ this(instanceId, service, uri, null);
+ }
+
+ public SetNextAVTransportURI(UnsignedIntegerFourBytes instanceId, Service service, String uri, String metadata) {
+ super(new ActionInvocation(service.getAction("SetNextAVTransportURI")));
+ getActionInvocation().setInput("InstanceID", instanceId);
+ getActionInvocation().setInput("NextURI", uri);
+ getActionInvocation().setInput("NextURIMetaData", metadata);
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/DownloadFile.java b/app/src/main/java/github/daneren2005/dsub/service/DownloadFile.java
new file mode 100644
index 00000000..505e4a6d
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/DownloadFile.java
@@ -0,0 +1,607 @@
+/*
+ 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.service;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import android.content.Context;
+import android.net.wifi.WifiManager;
+import android.os.PowerManager;
+import android.util.Log;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.SilentBackgroundTask;
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.Util;
+import github.daneren2005.dsub.util.CacheCleaner;
+import github.daneren2005.serverproxy.BufferFile;
+
+import org.apache.http.Header;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class DownloadFile implements BufferFile {
+ private static final String TAG = DownloadFile.class.getSimpleName();
+ private static final int MAX_FAILURES = 5;
+ private final Context context;
+ private final MusicDirectory.Entry song;
+ private final File partialFile;
+ private final File completeFile;
+ private final File saveFile;
+
+ private final MediaStoreService mediaStoreService;
+ private DownloadTask downloadTask;
+ private boolean save;
+ private boolean failedDownload = false;
+ private int failed = 0;
+ private int bitRate;
+ private boolean isPlaying = false;
+ private boolean saveWhenDone = false;
+ private boolean completeWhenDone = false;
+ private Long contentLength = null;
+ private long currentSpeed = 0;
+ private boolean rateLimit = false;
+
+ public DownloadFile(Context context, MusicDirectory.Entry song, boolean save) {
+ this.context = context;
+ this.song = song;
+ this.save = save;
+ saveFile = FileUtil.getSongFile(context, song);
+ bitRate = getActualBitrate();
+ partialFile = new File(saveFile.getParent(), FileUtil.getBaseName(saveFile.getName()) +
+ ".partial." + FileUtil.getExtension(saveFile.getName()));
+ completeFile = new File(saveFile.getParent(), FileUtil.getBaseName(saveFile.getName()) +
+ ".complete." + FileUtil.getExtension(saveFile.getName()));
+ mediaStoreService = new MediaStoreService(context);
+ }
+
+ public MusicDirectory.Entry getSong() {
+ return song;
+ }
+
+ /**
+ * Returns the effective bit rate.
+ */
+ public int getBitRate() {
+ if(!partialFile.exists()) {
+ bitRate = getActualBitrate();
+ }
+ if (bitRate > 0) {
+ return bitRate;
+ }
+ return song.getBitRate() == null ? 160 : song.getBitRate();
+ }
+ private int getActualBitrate() {
+ int br = song.isVideo() ? Util.getMaxVideoBitrate(context) : Util.getMaxBitrate(context);
+ if(br == 0 && song.getTranscodedSuffix() != null && "mp3".equals(song.getTranscodedSuffix().toLowerCase())) {
+ if(song.getBitRate() != null) {
+ br = Math.min(320, song.getBitRate());
+ } else {
+ br = 320;
+ }
+ } else if(song.getSuffix() != null && (song.getTranscodedSuffix() == null || song.getSuffix().equals(song.getTranscodedSuffix()))) {
+ // If just downsampling, don't try to upsample (ie: 128 kpbs -> 192 kpbs)
+ if(song.getBitRate() != null && br > song.getBitRate()) {
+ br = song.getBitRate();
+ }
+ }
+
+ return br;
+ }
+
+ public Long getContentLength() {
+ return contentLength;
+ }
+
+ public long getCurrentSize() {
+ if(partialFile.exists()) {
+ return partialFile.length();
+ } else {
+ File file = getCompleteFile();
+ if(file.exists()) {
+ return file.length();
+ } else {
+ return 0L;
+ }
+ }
+ }
+
+ @Override
+ public long getEstimatedSize() {
+ if(contentLength != null) {
+ return contentLength;
+ }
+
+ File file = getCompleteFile();
+ if(file.exists()) {
+ return file.length();
+ } else if(song.getDuration() == null) {
+ return 0;
+ } else {
+ int br = (getBitRate() * 1000) / 8;
+ int duration = song.getDuration();
+ return br * duration;
+ }
+ }
+
+ public long getBytesPerSecond() {
+ return currentSpeed;
+ }
+
+ public synchronized void download() {
+ rateLimit = false;
+ preDownload();
+ downloadTask.execute();
+ }
+ public synchronized void downloadNow(MusicService musicService) {
+ rateLimit = true;
+ preDownload();
+ downloadTask.setMusicService(musicService);
+ try {
+ downloadTask.doInBackground();
+ } catch(InterruptedException e) {
+ // This should never be reached
+ }
+ }
+ private void preDownload() {
+ FileUtil.createDirectoryForParent(saveFile);
+ failedDownload = false;
+ if(!partialFile.exists()) {
+ bitRate = getActualBitrate();
+ }
+ downloadTask = new DownloadTask(context);
+ }
+
+ public synchronized void cancelDownload() {
+ if (downloadTask != null) {
+ downloadTask.cancel();
+ }
+ }
+
+ @Override
+ public File getFile() {
+ if (saveFile.exists()) {
+ return saveFile;
+ } else if (completeFile.exists()) {
+ return completeFile;
+ } else {
+ return partialFile;
+ }
+ }
+
+ public File getCompleteFile() {
+ if (saveFile.exists()) {
+ return saveFile;
+ }
+
+ if (completeFile.exists()) {
+ return completeFile;
+ }
+
+ return saveFile;
+ }
+
+ public File getPartialFile() {
+ return partialFile;
+ }
+
+ public boolean isSaved() {
+ return saveFile.exists();
+ }
+
+ public synchronized boolean isCompleteFileAvailable() {
+ return saveFile.exists() || completeFile.exists();
+ }
+
+ @Override
+ public synchronized boolean isWorkDone() {
+ return saveFile.exists() || (completeFile.exists() && !save) || saveWhenDone || completeWhenDone;
+ }
+
+ @Override
+ public void onStart() {
+ setPlaying(true);
+ }
+
+ @Override
+ public void onStop() {
+ setPlaying(false);
+ }
+
+ @Override
+ public synchronized void onResume() {
+ if(!isWorkDone() && !isFailedMax() && !isDownloading() && !isDownloadCancelled()) {
+ download();
+ }
+ }
+
+ public synchronized boolean isDownloading() {
+ return downloadTask != null && downloadTask.isRunning();
+ }
+
+ public synchronized boolean isDownloadCancelled() {
+ return downloadTask != null && downloadTask.isCancelled();
+ }
+
+ public boolean shouldSave() {
+ return save;
+ }
+
+ public boolean isFailed() {
+ return failedDownload;
+ }
+ public boolean isFailedMax() {
+ return failed > MAX_FAILURES;
+ }
+
+ public void delete() {
+ cancelDownload();
+
+ // Remove from mediaStore BEFORE deleting file since it calls getCompleteFile
+ deleteFromStore();
+
+ // Delete all possible versions of the file
+ File parent = partialFile.getParentFile();
+ Util.delete(partialFile);
+ Util.delete(completeFile);
+ Util.delete(saveFile);
+ FileUtil.deleteEmptyDir(parent);
+ }
+
+ public void unpin() {
+ if (saveFile.exists()) {
+ // Delete old store entry before renaming to pinned file
+ saveFile.renameTo(completeFile);
+ renameInStore(saveFile, completeFile);
+ }
+ }
+
+ public boolean cleanup() {
+ boolean ok = true;
+ if (completeFile.exists() || saveFile.exists()) {
+ ok = Util.delete(partialFile);
+ }
+ if (saveFile.exists()) {
+ ok &= Util.delete(completeFile);
+ }
+ return ok;
+ }
+
+ // In support of LRU caching.
+ public void updateModificationDate() {
+ updateModificationDate(saveFile);
+ updateModificationDate(partialFile);
+ updateModificationDate(completeFile);
+ }
+
+ private void updateModificationDate(File file) {
+ if (file.exists()) {
+ boolean ok = file.setLastModified(System.currentTimeMillis());
+ if (!ok) {
+ Log.w(TAG, "Failed to set last-modified date on " + file);
+ }
+ }
+ }
+
+ public void setPlaying(boolean isPlaying) {
+ try {
+ if(saveWhenDone && !isPlaying) {
+ Util.renameFile(completeFile, saveFile);
+ renameInStore(completeFile, saveFile);
+ saveWhenDone = false;
+ } else if(completeWhenDone && !isPlaying) {
+ if(save) {
+ Util.renameFile(partialFile, saveFile);
+ saveToStore();
+ } else {
+ Util.renameFile(partialFile, completeFile);
+ saveToStore();
+ }
+ completeWhenDone = false;
+ }
+ } catch(IOException ex) {
+ Log.w(TAG, "Failed to rename file " + completeFile + " to " + saveFile, ex);
+ }
+
+ this.isPlaying = isPlaying;
+ }
+ public void renamePartial() {
+ try {
+ Util.renameFile(partialFile, completeFile);
+ saveToStore();
+ } catch(IOException ex) {
+ Log.w(TAG, "Failed to rename file " + partialFile + " to " + completeFile, ex);
+ }
+ }
+ public boolean getPlaying() {
+ return isPlaying;
+ }
+
+ private void deleteFromStore() {
+ try {
+ mediaStoreService.deleteFromMediaStore(this);
+ } catch(Exception e) {
+ Log.w(TAG, "Failed to remove from store", e);
+ }
+ }
+ private void saveToStore() {
+ if(!Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_HIDE_MEDIA, false)) {
+ try {
+ mediaStoreService.saveInMediaStore(this);
+ } catch(Exception e) {
+ Log.w(TAG, "Failed to save in media store", e);
+ }
+ }
+ }
+ private void renameInStore(File start, File end) {
+ try {
+ mediaStoreService.renameInMediaStore(start, end);
+ } catch(Exception e) {
+ Log.w(TAG, "Failed to rename in store", e);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "DownloadFile (" + song + ")";
+ }
+
+ private class DownloadTask extends SilentBackgroundTask<Void> {
+ private MusicService musicService;
+
+ public DownloadTask(Context context) {
+ super(context);
+ }
+
+ @Override
+ public Void doInBackground() throws InterruptedException {
+ InputStream in = null;
+ FileOutputStream out = null;
+ PowerManager.WakeLock wakeLock = null;
+ WifiManager.WifiLock wifiLock = null;
+ try {
+
+ if (Util.isScreenLitOnDownload(context)) {
+ PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+ wakeLock = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK | PowerManager.ON_AFTER_RELEASE, toString());
+ wakeLock.acquire();
+ }
+
+ wifiLock = Util.createWifiLock(context, toString());
+ wifiLock.acquire();
+
+ if (saveFile.exists()) {
+ Log.i(TAG, saveFile + " already exists. Skipping.");
+ checkDownloads();
+ return null;
+ }
+ if (completeFile.exists()) {
+ if (save) {
+ if(isPlaying) {
+ saveWhenDone = true;
+ } else {
+ Util.renameFile(completeFile, saveFile);
+ renameInStore(completeFile, saveFile);
+ }
+ } else {
+ Log.i(TAG, completeFile + " already exists. Skipping.");
+ }
+ checkDownloads();
+ return null;
+ }
+
+ if(musicService == null) {
+ musicService = MusicServiceFactory.getMusicService(context);
+ }
+
+ // Some devices seem to throw error on partial file which doesn't exist
+ boolean compare;
+ try {
+ compare = (bitRate == 0) || (song.getDuration() == 0) || (partialFile.length() == 0) || (bitRate * song.getDuration() * 1000 / 8) > partialFile.length();
+ } catch(Exception e) {
+ compare = true;
+ }
+ if(compare) {
+ // Attempt partial HTTP GET, appending to the file if it exists.
+ HttpResponse response = musicService.getDownloadInputStream(context, song, partialFile.length(), bitRate, DownloadTask.this);
+ Header contentLengthHeader = response.getFirstHeader("Content-Length");
+ if(contentLengthHeader != null) {
+ String contentLengthString = contentLengthHeader.getValue();
+ if(contentLengthString != null) {
+ Log.i(TAG, "Content Length: " + contentLengthString);
+ contentLength = Long.parseLong(contentLengthString);
+ }
+ }
+ in = response.getEntity().getContent();
+ boolean partial = response.getStatusLine().getStatusCode() == HttpStatus.SC_PARTIAL_CONTENT;
+ if (partial) {
+ Log.i(TAG, "Executed partial HTTP GET, skipping " + partialFile.length() + " bytes");
+ }
+
+ out = new FileOutputStream(partialFile, partial);
+ long n = copy(in, out);
+ Log.i(TAG, "Downloaded " + n + " bytes to " + partialFile);
+ out.flush();
+ out.close();
+
+ if (isCancelled()) {
+ throw new Exception("Download of '" + song + "' was cancelled");
+ } else if(partialFile.length() == 0) {
+ throw new Exception("Download of '" + song + "' failed. File is 0 bytes long.");
+ }
+
+ downloadAndSaveCoverArt(musicService);
+ }
+
+ if(isPlaying) {
+ completeWhenDone = true;
+ } else {
+ if(save) {
+ Util.renameFile(partialFile, saveFile);
+ } else {
+ Util.renameFile(partialFile, completeFile);
+ }
+ DownloadFile.this.saveToStore();
+ }
+
+ } catch(InterruptedException x) {
+ throw x;
+ } catch(FileNotFoundException x) {
+ Util.delete(completeFile);
+ Util.delete(saveFile);
+ if(!isCancelled()) {
+ failed = MAX_FAILURES + 1;
+ failedDownload = true;
+ Log.w(TAG, "Failed to download '" + song + "'.", x);
+ }
+ } catch(IOException x) {
+ Util.delete(completeFile);
+ Util.delete(saveFile);
+ if(!isCancelled()) {
+ failedDownload = true;
+ Log.w(TAG, "Failed to download '" + song + "'.", x);
+ }
+ } catch (Exception x) {
+ Util.delete(completeFile);
+ Util.delete(saveFile);
+ if (!isCancelled()) {
+ failed++;
+ failedDownload = true;
+ Log.w(TAG, "Failed to download '" + song + "'.", x);
+ }
+ } finally {
+ Util.close(in);
+ Util.close(out);
+ if (wakeLock != null) {
+ wakeLock.release();
+ Log.i(TAG, "Released wake lock " + wakeLock);
+ }
+ if (wifiLock != null) {
+ wifiLock.release();
+ }
+ }
+
+ // Only run these if not interrupted, ie: cancelled
+ if(!isCancelled()) {
+ new CacheCleaner(context, DownloadService.getInstance()).cleanSpace();
+ checkDownloads();
+ }
+
+ return null;
+ }
+
+ private void checkDownloads() {
+ DownloadService downloadService = DownloadService.getInstance();
+ if(downloadService != null) {
+ downloadService.checkDownloads();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "DownloadTask (" + song + ")";
+ }
+
+ public void setMusicService(MusicService musicService) {
+ this.musicService = musicService;
+ }
+
+ private void downloadAndSaveCoverArt(MusicService musicService) throws Exception {
+ try {
+ if (song.getCoverArt() != null) {
+ // Check if album art already exists, don't want to needlessly load into memory
+ File albumArtFile = FileUtil.getAlbumArtFile(context, song);
+ if(!albumArtFile.exists()) {
+ musicService.getCoverArt(context, song, 0, null, null);
+ }
+ }
+ } catch (Exception x) {
+ Log.e(TAG, "Failed to get cover art.", x);
+ }
+ }
+
+ private long copy(final InputStream in, OutputStream out) throws IOException, InterruptedException {
+
+ // Start a thread that will close the input stream if the task is
+ // cancelled, thus causing the copy() method to return.
+ new Thread("DownloadFile_copy") {
+ @Override
+ public void run() {
+ while (true) {
+ Util.sleepQuietly(3000L);
+ if (isCancelled()) {
+ Util.close(in);
+ return;
+ }
+ if (!isRunning()) {
+ return;
+ }
+ }
+ }
+ }.start();
+
+ byte[] buffer = new byte[1024 * 16];
+ long count = 0;
+ int n;
+ long lastLog = System.currentTimeMillis();
+ long lastCount = 0;
+
+ boolean activeLimit = rateLimit;
+ while (!isCancelled() && (n = in.read(buffer)) != -1) {
+ out.write(buffer, 0, n);
+ count += n;
+ lastCount += n;
+
+ long now = System.currentTimeMillis();
+ if (now - lastLog > 3000L) { // Only every so often.
+ Log.i(TAG, "Downloaded " + Util.formatBytes(count) + " of " + song);
+ currentSpeed = lastCount / ((now - lastLog) / 1000L);
+ lastLog = now;
+ lastCount = 0;
+
+ // Re-establish every few seconds whether screen is on or not
+ if(rateLimit) {
+ PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+ if(pm.isScreenOn()) {
+ activeLimit = true;
+ } else {
+ activeLimit = false;
+ }
+ }
+ }
+
+ // If screen is on and rateLimit is true, stop downloading from exhausting bandwidth
+ if(activeLimit) {
+ Thread.sleep(10L);
+ }
+ }
+ return count;
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/DownloadService.java b/app/src/main/java/github/daneren2005/dsub/service/DownloadService.java
new file mode 100644
index 00000000..3f1c022c
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/DownloadService.java
@@ -0,0 +1,2410 @@
+/*
+ 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.service;
+
+import static android.support.v7.media.MediaRouter.RouteInfo;
+import static github.daneren2005.dsub.domain.PlayerState.COMPLETED;
+import static github.daneren2005.dsub.domain.PlayerState.DOWNLOADING;
+import static github.daneren2005.dsub.domain.PlayerState.IDLE;
+import static github.daneren2005.dsub.domain.PlayerState.PAUSED;
+import static github.daneren2005.dsub.domain.PlayerState.PAUSED_TEMP;
+import static github.daneren2005.dsub.domain.PlayerState.PREPARED;
+import static github.daneren2005.dsub.domain.PlayerState.PREPARING;
+import static github.daneren2005.dsub.domain.PlayerState.STARTED;
+import static github.daneren2005.dsub.domain.PlayerState.STOPPED;
+import static github.daneren2005.dsub.domain.RemoteControlState.LOCAL;
+
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.audiofx.AudioEffectsController;
+import github.daneren2005.dsub.audiofx.EqualizerController;
+import github.daneren2005.dsub.domain.Bookmark;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.PlayerState;
+import github.daneren2005.dsub.domain.PodcastEpisode;
+import github.daneren2005.dsub.domain.RemoteControlState;
+import github.daneren2005.dsub.domain.RepeatMode;
+import github.daneren2005.dsub.domain.ServerInfo;
+import github.daneren2005.dsub.receiver.MediaButtonIntentReceiver;
+import github.daneren2005.dsub.util.ArtistRadioBuffer;
+import github.daneren2005.dsub.util.Notifications;
+import github.daneren2005.dsub.util.SilentBackgroundTask;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.MediaRouteManager;
+import github.daneren2005.dsub.util.ShufflePlayBuffer;
+import github.daneren2005.dsub.util.SimpleServiceBinder;
+import github.daneren2005.dsub.util.Util;
+import github.daneren2005.dsub.util.compat.RemoteControlClientHelper;
+import github.daneren2005.dsub.util.tags.BastpUtil;
+import github.daneren2005.dsub.view.UpdateView;
+import github.daneren2005.serverproxy.BufferProxy;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Timer;
+import java.util.TimerTask;
+
+import android.annotation.TargetApi;
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.media.AudioManager;
+import android.media.MediaPlayer;
+import android.media.audiofx.AudioEffect;
+import android.os.Build;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.PowerManager;
+import android.support.v7.media.MediaRouteSelector;
+import android.support.v7.media.MediaRouter;
+import android.util.Log;
+import android.support.v4.util.LruCache;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class DownloadService extends Service {
+ private static final String TAG = DownloadService.class.getSimpleName();
+
+ public static final String CMD_PLAY = "github.daneren2005.dsub.CMD_PLAY";
+ public static final String CMD_TOGGLEPAUSE = "github.daneren2005.dsub.CMD_TOGGLEPAUSE";
+ public static final String CMD_PAUSE = "github.daneren2005.dsub.CMD_PAUSE";
+ public static final String CMD_STOP = "github.daneren2005.dsub.CMD_STOP";
+ public static final String CMD_PREVIOUS = "github.daneren2005.dsub.CMD_PREVIOUS";
+ public static final String CMD_NEXT = "github.daneren2005.dsub.CMD_NEXT";
+ public static final String CANCEL_DOWNLOADS = "github.daneren2005.dsub.CANCEL_DOWNLOADS";
+ public static final String START_PLAY = "github.daneren2005.dsub.START_PLAYING";
+ public static final int FAST_FORWARD = 30000;
+ public static final int REWIND = 10000;
+ private static final double DELETE_CUTOFF = 0.84;
+ private static final int REQUIRED_ALBUM_MATCHES = 4;
+ private static final int SHUFFLE_MODE_NONE = 0;
+ private static final int SHUFFLE_MODE_ALL = 1;
+ private static final int SHUFFLE_MODE_ARTIST = 2;
+
+ private RemoteControlClientHelper mRemoteControl;
+
+ private final IBinder binder = new SimpleServiceBinder<DownloadService>(this);
+ private Looper mediaPlayerLooper;
+ private MediaPlayer mediaPlayer;
+ private MediaPlayer nextMediaPlayer;
+ private int audioSessionId;
+ private boolean nextSetup = false;
+ private final List<DownloadFile> downloadList = new ArrayList<DownloadFile>();
+ private final List<DownloadFile> backgroundDownloadList = new ArrayList<DownloadFile>();
+ private final List<DownloadFile> toDelete = new ArrayList<DownloadFile>();
+ private final Handler handler = new Handler();
+ private Handler mediaPlayerHandler;
+ private final DownloadServiceLifecycleSupport lifecycleSupport = new DownloadServiceLifecycleSupport(this);
+ private ShufflePlayBuffer shufflePlayBuffer;
+ private ArtistRadioBuffer artistRadioBuffer;
+
+ private final LruCache<MusicDirectory.Entry, DownloadFile> downloadFileCache = new LruCache<MusicDirectory.Entry, DownloadFile>(100);
+ private final List<DownloadFile> cleanupCandidates = new ArrayList<DownloadFile>();
+ private final Scrobbler scrobbler = new Scrobbler();
+ private RemoteController remoteController;
+ private DownloadFile currentPlaying;
+ private int currentPlayingIndex = -1;
+ private DownloadFile nextPlaying;
+ private DownloadFile currentDownloading;
+ private SilentBackgroundTask bufferTask;
+ private SilentBackgroundTask nextPlayingTask;
+ private PlayerState playerState = IDLE;
+ private PlayerState nextPlayerState = IDLE;
+ private boolean removePlayed;
+ private boolean shufflePlay;
+ private boolean artistRadio;
+ private long revision;
+ private static DownloadService instance;
+ private String suggestedPlaylistName;
+ private String suggestedPlaylistId;
+ private PowerManager.WakeLock wakeLock;
+ private boolean keepScreenOn;
+ private int cachedPosition = 0;
+ private boolean downloadOngoing = false;
+ private float volume = 1.0f;
+
+ private AudioEffectsController effectsController;
+ private RemoteControlState remoteState = LOCAL;
+ private PositionCache positionCache;
+ private BufferProxy proxy;
+
+ private Timer sleepTimer;
+ private int timerDuration;
+ private boolean autoPlayStart = false;
+
+ private MediaRouteManager mediaRouter;
+
+ // Variables to manage getCurrentPosition sometimes starting from an arbitrary non-zero number
+ private long subtractNextPosition = 0;
+ private int subtractPosition = 0;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ final SharedPreferences prefs = Util.getPreferences(this);
+ new Thread(new Runnable() {
+ public void run() {
+ Looper.prepare();
+
+ mediaPlayer = new MediaPlayer();
+ mediaPlayer.setWakeMode(DownloadService.this, PowerManager.PARTIAL_WAKE_LOCK);
+ mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
+
+ mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
+ @Override
+ public boolean onError(MediaPlayer mediaPlayer, int what, int more) {
+ handleError(new Exception("MediaPlayer error: " + what + " (" + more + ")"));
+ return false;
+ }
+ });
+ try {
+ audioSessionId = mediaPlayer.getAudioSessionId();
+ } catch(Throwable e) {
+ // Froyo or lower
+ }
+
+ try {
+ Intent i = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION);
+ i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, audioSessionId);
+ i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName());
+ sendBroadcast(i);
+ } catch(Throwable e) {
+ // Froyo or lower
+ }
+
+ effectsController = new AudioEffectsController(DownloadService.this, audioSessionId);
+ if(prefs.getBoolean(Constants.PREFERENCES_EQUALIZER_ON, false)) {
+ getEqualizerController();
+ }
+
+ mediaPlayerLooper = Looper.myLooper();
+ mediaPlayerHandler = new Handler(mediaPlayerLooper);
+ Looper.loop();
+ }
+ }, "DownloadService").start();
+
+ Util.registerMediaButtonEventReceiver(this);
+
+ if (mRemoteControl == null) {
+ // Use the remote control APIs (if available) to set the playback state
+ mRemoteControl = RemoteControlClientHelper.createInstance();
+ ComponentName mediaButtonReceiverComponent = new ComponentName(getPackageName(), MediaButtonIntentReceiver.class.getName());
+ mRemoteControl.register(this, mediaButtonReceiverComponent);
+ }
+
+ PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
+ wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, this.getClass().getName());
+ wakeLock.setReferenceCounted(false);
+
+ try {
+ timerDuration = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_SLEEP_TIMER_DURATION, "5"));
+ } catch(Throwable e) {
+ timerDuration = 5;
+ }
+ sleepTimer = null;
+
+ keepScreenOn = prefs.getBoolean(Constants.PREFERENCES_KEY_KEEP_SCREEN_ON, false);
+
+ mediaRouter = new MediaRouteManager(this);
+
+ instance = this;
+ shufflePlayBuffer = new ShufflePlayBuffer(this);
+ artistRadioBuffer = new ArtistRadioBuffer(this);
+ lifecycleSupport.onCreate();
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ super.onStartCommand(intent, flags, startId);
+ lifecycleSupport.onStart(intent);
+ return START_NOT_STICKY;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ instance = null;
+
+ if(currentPlaying != null) currentPlaying.setPlaying(false);
+ if(sleepTimer != null){
+ sleepTimer.cancel();
+ sleepTimer.purge();
+ }
+ lifecycleSupport.onDestroy();
+
+ try {
+ Intent i = new Intent(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION);
+ i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, audioSessionId);
+ i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName());
+ sendBroadcast(i);
+ } catch(Throwable e) {
+ // Froyo or lower
+ }
+
+ mediaPlayer.release();
+ if(nextMediaPlayer != null) {
+ nextMediaPlayer.release();
+ }
+ mediaPlayerLooper.quit();
+ shufflePlayBuffer.shutdown();
+ effectsController.release();
+ if (mRemoteControl != null) {
+ mRemoteControl.unregister(this);
+ mRemoteControl = null;
+ }
+
+ if(bufferTask != null) {
+ bufferTask.cancel();
+ bufferTask = null;
+ }
+ if(nextPlayingTask != null) {
+ nextPlayingTask.cancel();
+ nextPlayingTask = null;
+ }
+ if(remoteController != null) {
+ remoteController.stop();
+ remoteController.shutdown();
+ }
+ if(proxy != null) {
+ proxy.stop();
+ proxy = null;
+ }
+ mediaRouter.destroy();
+ Notifications.hidePlayingNotification(this, this, handler);
+ Notifications.hideDownloadingNotification(this, this, handler);
+ }
+
+ public static DownloadService getInstance() {
+ return instance;
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return binder;
+ }
+
+ public void post(Runnable r) {
+ handler.post(r);
+ }
+ public void postDelayed(Runnable r, long millis) {
+ handler.postDelayed(r, millis);
+ }
+
+ public synchronized void download(List<MusicDirectory.Entry> songs, boolean save, boolean autoplay, boolean playNext, boolean shuffle) {
+ download(songs, save, autoplay, playNext, shuffle, 0, 0);
+ }
+ public synchronized void download(List<MusicDirectory.Entry> songs, boolean save, boolean autoplay, boolean playNext, boolean shuffle, int start, int position) {
+ setShufflePlayEnabled(false);
+ setArtistRadio(null);
+ int offset = 1;
+ boolean noNetwork = !Util.isOffline(this) && !Util.isNetworkConnected(this);
+ boolean warnNetwork = false;
+
+ if (songs.isEmpty()) {
+ return;
+ }
+ if (playNext) {
+ if (autoplay && getCurrentPlayingIndex() >= 0) {
+ offset = 0;
+ }
+ for (MusicDirectory.Entry song : songs) {
+ if(song != null) {
+ DownloadFile downloadFile = new DownloadFile(this, song, save);
+ addToDownloadList(downloadFile, getCurrentPlayingIndex() + offset);
+ if(noNetwork && !warnNetwork) {
+ if(!downloadFile.isCompleteFileAvailable()) {
+ warnNetwork = true;
+ }
+ }
+ offset++;
+ }
+ }
+ setNextPlaying();
+ revision++;
+ } else {
+ int size = size();
+ int index = getCurrentPlayingIndex();
+ for (MusicDirectory.Entry song : songs) {
+ DownloadFile downloadFile = new DownloadFile(this, song, save);
+ addToDownloadList(downloadFile, -1);
+ if(noNetwork && !warnNetwork) {
+ if(!downloadFile.isCompleteFileAvailable()) {
+ warnNetwork = true;
+ }
+ }
+ }
+ if(!autoplay && (size - 1) == index) {
+ setNextPlaying();
+ }
+ revision++;
+ }
+ updateRemotePlaylist();
+
+ if(shuffle) {
+ shuffle();
+ }
+ if(warnNetwork) {
+ Util.toast(this, R.string.select_album_no_network);
+ }
+
+ if (autoplay) {
+ play(start, true, position);
+ } else if(start != 0 || position != 0) {
+ play(start, false, position);
+ } else {
+ if (currentPlaying == null) {
+ currentPlaying = downloadList.get(0);
+ currentPlayingIndex = 0;
+ currentPlaying.setPlaying(true);
+ } else {
+ currentPlayingIndex = downloadList.indexOf(currentPlaying);
+ }
+ checkDownloads();
+ }
+ lifecycleSupport.serializeDownloadQueue();
+ }
+ private void addToDownloadList(DownloadFile file, int offset) {
+ if(offset == -1) {
+ downloadList.add(file);
+ } else {
+ downloadList.add(offset, file);
+ }
+ }
+ public synchronized void downloadBackground(List<MusicDirectory.Entry> songs, boolean save) {
+ for (MusicDirectory.Entry song : songs) {
+ DownloadFile downloadFile = new DownloadFile(this, song, save);
+ if(!downloadFile.isWorkDone() || (downloadFile.shouldSave() && !downloadFile.isSaved())) {
+ // Only add to list if there is work to be done
+ backgroundDownloadList.add(downloadFile);
+ } else if(downloadFile.isSaved() && !save) {
+ // Quickly unpin song instead of adding it to work to be done
+ downloadFile.unpin();
+ }
+ }
+ revision++;
+
+ if(!Util.isOffline(this) && !Util.isNetworkConnected(this)) {
+ Util.toast(this, R.string.select_album_no_network);
+ }
+
+ checkDownloads();
+ lifecycleSupport.serializeDownloadQueue();
+ }
+
+ private void updateRemotePlaylist() {
+ if (remoteState != LOCAL && remoteController != null) {
+ remoteController.updatePlaylist();
+ }
+ }
+
+ public synchronized void restore(List<MusicDirectory.Entry> songs, List<MusicDirectory.Entry> toDelete, int currentPlayingIndex, int currentPlayingPosition) {
+ SharedPreferences prefs = Util.getPreferences(this);
+ RemoteControlState newState = RemoteControlState.values()[prefs.getInt(Constants.PREFERENCES_KEY_CONTROL_MODE, 0)];
+ if(newState != LOCAL) {
+ String id = prefs.getString(Constants.PREFERENCES_KEY_CONTROL_ID, null);
+ setRemoteState(newState, null, id);
+ }
+ if(prefs.getBoolean(Constants.PREFERENCES_KEY_REMOVE_PLAYED, false)) {
+ removePlayed = true;
+ }
+ int startShufflePlay = prefs.getInt(Constants.PREFERENCES_KEY_SHUFFLE_MODE, SHUFFLE_MODE_NONE);
+ download(songs, false, false, false, false);
+ if(startShufflePlay != SHUFFLE_MODE_NONE) {
+ if(startShufflePlay == SHUFFLE_MODE_ALL) {
+ shufflePlay = true;
+ } else if(startShufflePlay == SHUFFLE_MODE_ARTIST) {
+ artistRadio = true;
+ artistRadioBuffer.restoreArtist(prefs.getString(Constants.PREFERENCES_KEY_SHUFFLE_MODE_EXTRA, null));
+ }
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putInt(Constants.PREFERENCES_KEY_SHUFFLE_MODE, startShufflePlay);
+ editor.commit();
+ }
+ if (currentPlayingIndex != -1) {
+ while(mediaPlayer == null) {
+ Util.sleepQuietly(50L);
+ }
+
+ play(currentPlayingIndex, autoPlayStart, currentPlayingPosition);
+ autoPlayStart = false;
+ }
+
+ if(toDelete != null) {
+ for(MusicDirectory.Entry entry: toDelete) {
+ this.toDelete.add(forSong(entry));
+ }
+ }
+
+ suggestedPlaylistName = prefs.getString(Constants.PREFERENCES_KEY_PLAYLIST_NAME, null);
+ suggestedPlaylistId = prefs.getString(Constants.PREFERENCES_KEY_PLAYLIST_ID, null);
+ }
+
+ public boolean isInitialized() {
+ return lifecycleSupport != null && lifecycleSupport.isInitialized();
+ }
+
+ public synchronized Date getLastStateChanged() {
+ return lifecycleSupport.getLastChange();
+ }
+
+ public synchronized void setRemovePlayed(boolean enabled) {
+ removePlayed = enabled;
+ if(removePlayed) {
+ checkDownloads();
+ lifecycleSupport.serializeDownloadQueue();
+ }
+ SharedPreferences.Editor editor = Util.getPreferences(this).edit();
+ editor.putBoolean(Constants.PREFERENCES_KEY_REMOVE_PLAYED, enabled);
+ editor.commit();
+ }
+ public boolean isRemovePlayed() {
+ return removePlayed;
+ }
+
+ public synchronized void setShufflePlayEnabled(boolean enabled) {
+ shufflePlay = enabled;
+ if (shufflePlay) {
+ clear();
+ checkDownloads();
+ }
+ SharedPreferences.Editor editor = Util.getPreferences(this).edit();
+ editor.putInt(Constants.PREFERENCES_KEY_SHUFFLE_MODE, enabled ? SHUFFLE_MODE_ALL : SHUFFLE_MODE_NONE);
+ editor.commit();
+ }
+
+ public boolean isShufflePlayEnabled() {
+ return shufflePlay;
+ }
+
+ public void setArtistRadio(String artistId) {
+ if(artistId == null) {
+ artistRadio = false;
+ } else {
+ artistRadio = true;
+ artistRadioBuffer.setArtist(artistId);
+ }
+
+ SharedPreferences.Editor editor = Util.getPreferences(this).edit();
+ editor.putInt(Constants.PREFERENCES_KEY_SHUFFLE_MODE, (artistId != null) ? SHUFFLE_MODE_ARTIST : SHUFFLE_MODE_NONE);
+ if(artistId != null) {
+ editor.putString(Constants.PREFERENCES_KEY_SHUFFLE_MODE_EXTRA, artistId);
+ }
+ editor.commit();
+ }
+
+ public synchronized void shuffle() {
+ Collections.shuffle(downloadList);
+ currentPlayingIndex = downloadList.indexOf(currentPlaying);
+ if (currentPlaying != null) {
+ downloadList.remove(getCurrentPlayingIndex());
+ downloadList.add(0, currentPlaying);
+ currentPlayingIndex = 0;
+ }
+ revision++;
+ lifecycleSupport.serializeDownloadQueue();
+ updateRemotePlaylist();
+ setNextPlaying();
+ }
+
+ public RepeatMode getRepeatMode() {
+ return Util.getRepeatMode(this);
+ }
+
+ public void setRepeatMode(RepeatMode repeatMode) {
+ Util.setRepeatMode(this, repeatMode);
+ setNextPlaying();
+ }
+
+ public boolean getKeepScreenOn() {
+ return keepScreenOn;
+ }
+
+ public void setKeepScreenOn(boolean keepScreenOn) {
+ this.keepScreenOn = keepScreenOn;
+
+ SharedPreferences prefs = Util.getPreferences(this);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putBoolean(Constants.PREFERENCES_KEY_KEEP_SCREEN_ON, keepScreenOn);
+ editor.commit();
+ }
+
+ public synchronized DownloadFile forSong(MusicDirectory.Entry song) {
+ DownloadFile returnFile = null;
+ for (DownloadFile downloadFile : downloadList) {
+ if (downloadFile.getSong().equals(song)) {
+ if(((downloadFile.isDownloading() && !downloadFile.isDownloadCancelled() && downloadFile.getPartialFile().exists()) || downloadFile.isWorkDone())) {
+ // If downloading, return immediately
+ return downloadFile;
+ } else {
+ // Otherwise, check to make sure there isn't a background download going on first
+ returnFile = downloadFile;
+ }
+ }
+ }
+ for (DownloadFile downloadFile : backgroundDownloadList) {
+ if (downloadFile.getSong().equals(song)) {
+ return downloadFile;
+ }
+ }
+
+ if(returnFile != null) {
+ return returnFile;
+ }
+
+ DownloadFile downloadFile = downloadFileCache.get(song);
+ if (downloadFile == null) {
+ downloadFile = new DownloadFile(this, song, false);
+ downloadFileCache.put(song, downloadFile);
+ }
+ return downloadFile;
+ }
+
+ public synchronized void clear() {
+ clear(true);
+ }
+
+ public synchronized void clearBackground() {
+ if(currentDownloading != null && backgroundDownloadList.contains(currentDownloading)) {
+ currentDownloading.cancelDownload();
+ currentDownloading = null;
+ }
+ backgroundDownloadList.clear();
+ revision++;
+ Notifications.hideDownloadingNotification(this, this, handler);
+ }
+
+ public synchronized void clearIncomplete() {
+ Iterator<DownloadFile> iterator = downloadList.iterator();
+ while (iterator.hasNext()) {
+ DownloadFile downloadFile = iterator.next();
+ if (!downloadFile.isCompleteFileAvailable()) {
+ iterator.remove();
+
+ // Reset if the current playing song has been removed
+ if(currentPlaying == downloadFile) {
+ reset();
+ }
+
+ currentPlayingIndex = downloadList.indexOf(currentPlaying);
+ }
+ }
+ lifecycleSupport.serializeDownloadQueue();
+ updateRemotePlaylist();
+ }
+
+ public void setOnline(final boolean online) {
+ if(online) {
+ mediaRouter.addOnlineProviders();
+ } else {
+ mediaRouter.removeOnlineProviders();
+ }
+ if(shufflePlay) {
+ setShufflePlayEnabled(false);
+ }
+ if(artistRadio) {
+ setArtistRadio(null);
+ }
+
+ lifecycleSupport.post(new Runnable() {
+ @Override
+ public void run() {
+ if (online) {
+ checkDownloads();
+ } else {
+ clearIncomplete();
+ }
+ }
+ });
+ }
+ public void userSettingsChanged() {
+ mediaRouter.buildSelector();
+ }
+
+ public synchronized int size() {
+ return downloadList.size();
+ }
+
+ public synchronized void clear(boolean serialize) {
+ // Delete podcast if fully listened to
+ int position = getPlayerPosition();
+ int duration = getPlayerDuration();
+ boolean cutoff = isPastCutoff(position, duration, true);
+ if(currentPlaying != null && currentPlaying.getSong() instanceof PodcastEpisode) {
+ if(cutoff) {
+ currentPlaying.delete();
+ }
+ }
+ for(DownloadFile podcast: toDelete) {
+ podcast.delete();
+ }
+ toDelete.clear();
+
+ // Clear bookmarks from current playing if past a certain point
+ if(cutoff) {
+ clearCurrentBookmark(true);
+ } else {
+ // Check if we should be adding a new bookmark here
+ checkAddBookmark();
+ }
+ if(currentPlaying != null) {
+ scrobbler.conditionalScrobble(this, currentPlaying, position, duration);
+ }
+
+ reset();
+ downloadList.clear();
+ revision++;
+ if (currentDownloading != null && !backgroundDownloadList.contains(currentDownloading)) {
+ currentDownloading.cancelDownload();
+ currentDownloading = null;
+ }
+ setCurrentPlaying(null, false);
+
+ if (serialize) {
+ lifecycleSupport.serializeDownloadQueue();
+ }
+ updateRemotePlaylist();
+ setNextPlaying();
+ if(proxy != null) {
+ proxy.stop();
+ proxy = null;
+ }
+
+ suggestedPlaylistName = null;
+ suggestedPlaylistId = null;
+ }
+
+ public synchronized void remove(int which) {
+ downloadList.remove(which);
+ currentPlayingIndex = downloadList.indexOf(currentPlaying);
+ }
+
+ public synchronized void remove(DownloadFile downloadFile) {
+ if (downloadFile == currentDownloading) {
+ currentDownloading.cancelDownload();
+ currentDownloading = null;
+ }
+ if (downloadFile == currentPlaying) {
+ reset();
+ setCurrentPlaying(null, false);
+ }
+ downloadList.remove(downloadFile);
+ currentPlayingIndex = downloadList.indexOf(currentPlaying);
+ backgroundDownloadList.remove(downloadFile);
+ revision++;
+ lifecycleSupport.serializeDownloadQueue();
+ updateRemotePlaylist();
+ if(downloadFile == nextPlaying) {
+ setNextPlaying();
+ }
+ }
+
+ public synchronized void delete(List<MusicDirectory.Entry> songs) {
+ for (MusicDirectory.Entry song : songs) {
+ forSong(song).delete();
+ }
+ }
+
+ public synchronized void unpin(List<MusicDirectory.Entry> songs) {
+ for (MusicDirectory.Entry song : songs) {
+ forSong(song).unpin();
+ }
+ }
+
+ synchronized void setCurrentPlaying(int currentPlayingIndex, boolean showNotification) {
+ try {
+ setCurrentPlaying(downloadList.get(currentPlayingIndex), showNotification);
+ } catch (IndexOutOfBoundsException x) {
+ // Ignored
+ }
+ }
+
+ synchronized void setCurrentPlaying(DownloadFile currentPlaying, boolean showNotification) {
+ if(this.currentPlaying != null) {
+ this.currentPlaying.setPlaying(false);
+ }
+ this.currentPlaying = currentPlaying;
+ if(currentPlaying == null) {
+ currentPlayingIndex = -1;
+ setPlayerState(IDLE);
+ } else {
+ currentPlayingIndex = downloadList.indexOf(currentPlaying);
+ }
+
+ if (currentPlaying != null) {
+ Util.broadcastNewTrackInfo(this, currentPlaying.getSong());
+ mRemoteControl.updateMetadata(this, currentPlaying.getSong());
+ } else {
+ Util.broadcastNewTrackInfo(this, null);
+ Notifications.hidePlayingNotification(this, this, handler);
+ }
+ }
+
+ synchronized void setNextPlaying() {
+ SharedPreferences prefs = Util.getPreferences(DownloadService.this);
+
+ // Only obey gapless playback for local
+ if(remoteState == LOCAL) {
+ boolean gaplessPlayback = prefs.getBoolean(Constants.PREFERENCES_KEY_GAPLESS_PLAYBACK, true);
+ if (!gaplessPlayback) {
+ nextPlaying = null;
+ nextPlayerState = IDLE;
+ return;
+ }
+ }
+ setNextPlayerState(IDLE);
+
+ int index = getNextPlayingIndex();
+
+ nextSetup = false;
+ if(nextPlayingTask != null) {
+ nextPlayingTask.cancel();
+ nextPlayingTask = null;
+ }
+ if(index < size() && index != -1 && index != currentPlayingIndex) {
+ nextPlaying = downloadList.get(index);
+
+ if(remoteState == LOCAL) {
+ nextPlayingTask = new CheckCompletionTask(nextPlaying);
+ nextPlayingTask.execute();
+ } else if(remoteController != null) {
+ remoteController.changeNextTrack(nextPlaying);
+ }
+ } else {
+ if(remoteState == LOCAL) {
+ resetNext();
+ } else if(remoteController != null) {
+ remoteController.changeNextTrack(nextPlaying);
+ }
+ nextPlaying = null;
+ }
+ }
+
+ public int getCurrentPlayingIndex() {
+ return currentPlayingIndex;
+ }
+ private int getNextPlayingIndex() {
+ int index = getCurrentPlayingIndex();
+ if (index != -1) {
+ switch (getRepeatMode()) {
+ case OFF:
+ index = index + 1;
+ break;
+ case ALL:
+ index = (index + 1) % size();
+ break;
+ case SINGLE:
+ break;
+ default:
+ break;
+ }
+ }
+ return index;
+ }
+
+ public DownloadFile getCurrentPlaying() {
+ return currentPlaying;
+ }
+
+ public DownloadFile getCurrentDownloading() {
+ return currentDownloading;
+ }
+
+ public DownloadFile getNextPlaying() {
+ return nextPlaying;
+ }
+
+ public List<DownloadFile> getSongs() {
+ return downloadList;
+ }
+
+ public List<DownloadFile> getToDelete() { return toDelete; }
+
+ public synchronized List<DownloadFile> getDownloads() {
+ List<DownloadFile> temp = new ArrayList<DownloadFile>();
+ temp.addAll(downloadList);
+ temp.addAll(backgroundDownloadList);
+ return temp;
+ }
+
+ public List<DownloadFile> getBackgroundDownloads() {
+ return backgroundDownloadList;
+ }
+
+ /** Plays either the current song (resume) or the first/next one in queue. */
+ public synchronized void play()
+ {
+ int current = getCurrentPlayingIndex();
+ if (current == -1) {
+ play(0);
+ } else {
+ play(current);
+ }
+ }
+
+ public synchronized void play(int index) {
+ play(index, true);
+ }
+ private synchronized void play(int index, boolean start) {
+ play(index, start, 0);
+ }
+ private synchronized void play(int index, boolean start, int position) {
+ int size = this.size();
+ if (index < 0 || index >= size) {
+ reset();
+ if(index >= size && size != 0) {
+ setCurrentPlaying(0, false);
+ Notifications.hidePlayingNotification(this, this, handler);
+ } else {
+ setCurrentPlaying(null, false);
+ }
+ lifecycleSupport.serializeDownloadQueue();
+ } else {
+ if(nextPlayingTask != null) {
+ nextPlayingTask.cancel();
+ nextPlayingTask = null;
+ }
+ setCurrentPlaying(index, start);
+ if (start && remoteState != LOCAL) {
+ remoteController.changeTrack(index, currentPlaying);
+ }
+ if (remoteState == LOCAL) {
+ bufferAndPlay(position, start);
+ checkDownloads();
+ setNextPlaying();
+ }
+ }
+ }
+ private synchronized void playNext() {
+ if(nextPlaying != null && nextPlayerState == PlayerState.PREPARED) {
+ if(!nextSetup) {
+ playNext(true);
+ } else {
+ nextSetup = false;
+ playNext(false);
+ }
+ } else {
+ onSongCompleted();
+ }
+ }
+ private synchronized void playNext(boolean start) {
+ Util.broadcastPlaybackStatusChange(this, currentPlaying.getSong(), PlayerState.PREPARED);
+
+ // Swap the media players since nextMediaPlayer is ready to play
+ subtractPosition = 0;
+ if(start) {
+ nextMediaPlayer.start();
+ } else if(!nextMediaPlayer.isPlaying()) {
+ Log.w(TAG, "nextSetup lied about it's state!");
+ nextMediaPlayer.start();
+ } else {
+ Log.i(TAG, "nextMediaPlayer already playing");
+
+ // Next time the cachedPosition is updated, use that as position 0
+ subtractNextPosition = System.currentTimeMillis();
+ }
+ MediaPlayer tmp = mediaPlayer;
+ mediaPlayer = nextMediaPlayer;
+ nextMediaPlayer = tmp;
+ setCurrentPlaying(nextPlaying, true);
+ setPlayerState(PlayerState.STARTED);
+ setupHandlers(currentPlaying, false, start);
+ setNextPlaying();
+
+ // Proxy should not be being used here since the next player was already setup to play
+ if(proxy != null) {
+ proxy.stop();
+ proxy = null;
+ }
+ checkDownloads();
+ }
+
+ /** Plays or resumes the playback, depending on the current player state. */
+ public synchronized void togglePlayPause() {
+ if (playerState == PAUSED || playerState == COMPLETED || playerState == STOPPED) {
+ start();
+ } else if (playerState == STOPPED || playerState == IDLE) {
+ autoPlayStart = true;
+ play();
+ } else if (playerState == STARTED) {
+ pause();
+ }
+ }
+
+ public synchronized void seekTo(int position) {
+ if(position < 0) {
+ position = 0;
+ }
+
+ try {
+ if (remoteState != LOCAL) {
+ remoteController.changePosition(position / 1000);
+ } else {
+ if(proxy != null && currentPlaying.isCompleteFileAvailable()) {
+ doPlay(currentPlaying, position, playerState == STARTED);
+ return;
+ }
+
+ mediaPlayer.seekTo(position);
+ cachedPosition = position;
+ subtractPosition = 0;
+ }
+ } catch (Exception x) {
+ handleError(x);
+ }
+ }
+
+ public synchronized void previous() {
+ int index = getCurrentPlayingIndex();
+ if (index == -1) {
+ return;
+ }
+
+ // If only one song, just skip within song
+ if(size() == 1) {
+ seekTo(getPlayerPosition() - REWIND);
+ return;
+ }
+
+
+ // Restart song if played more than five seconds.
+ if (getPlayerPosition() > 5000 || (index == 0 && getRepeatMode() != RepeatMode.ALL)) {
+ seekTo(0);
+ } else {
+ if(index == 0) {
+ index = size();
+ }
+
+ play(index - 1, playerState != PAUSED && playerState != STOPPED && playerState != IDLE);
+ }
+ }
+
+ public synchronized void next() {
+ next(false);
+ }
+ public synchronized void next(boolean forceCutoff) {
+ next(forceCutoff, false);
+ }
+ public synchronized void next(boolean forceCutoff, boolean forceStart) {
+ // If only one song, just skip within song
+ if(size() == 1) {
+ seekTo(getPlayerPosition() + FAST_FORWARD);
+ return;
+ } else if(playerState == PREPARING || playerState == PREPARED) {
+ return;
+ }
+
+ // Delete podcast if fully listened to
+ int position = getPlayerPosition();
+ int duration = getPlayerDuration();
+ boolean cutoff;
+ if(forceCutoff) {
+ cutoff = true;
+ } else {
+ cutoff = isPastCutoff(position, duration);
+ }
+ if(currentPlaying != null && currentPlaying.getSong() instanceof PodcastEpisode) {
+ if(cutoff) {
+ toDelete.add(currentPlaying);
+ }
+ }
+ if(cutoff) {
+ clearCurrentBookmark(true);
+ }
+ if(currentPlaying != null) {
+ scrobbler.conditionalScrobble(this, currentPlaying, position, duration);
+ }
+
+ int index = getCurrentPlayingIndex();
+ int nextPlayingIndex = getNextPlayingIndex();
+ // Make sure to actually go to next when repeat song is on
+ if(index == nextPlayingIndex) {
+ nextPlayingIndex++;
+ }
+ if (index != -1 && nextPlayingIndex < size()) {
+ play(nextPlayingIndex, playerState != PAUSED && playerState != STOPPED && playerState != IDLE || forceStart);
+ }
+ }
+
+ public void onSongCompleted() {
+ play(getNextPlayingIndex());
+ }
+
+ public synchronized void pause() {
+ pause(false);
+ }
+ public synchronized void pause(boolean temp) {
+ try {
+ if (playerState == STARTED) {
+ if (remoteState != LOCAL) {
+ remoteController.stop();
+ } else {
+ mediaPlayer.pause();
+ }
+ setPlayerState(temp ? PAUSED_TEMP : PAUSED);
+ } else if(playerState == PAUSED_TEMP) {
+ setPlayerState(temp ? PAUSED_TEMP : PAUSED);
+ }
+ } catch (Exception x) {
+ handleError(x);
+ }
+ }
+
+ public synchronized void stop() {
+ try {
+ if (playerState == STARTED) {
+ if (remoteState != LOCAL) {
+ remoteController.stop();
+ setPlayerState(STOPPED);
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ mediaRouter.setDefaultRoute();
+ }
+ });
+ } else {
+ mediaPlayer.pause();
+ setPlayerState(STOPPED);
+ }
+ } else if(playerState == PAUSED) {
+ setPlayerState(STOPPED);
+ }
+ } catch(Exception x) {
+ handleError(x);
+ }
+ }
+
+ public synchronized void start() {
+ try {
+ if (remoteState != LOCAL) {
+ remoteController.start();
+ } else {
+ // Only start if done preparing
+ if(playerState != PREPARING) {
+ mediaPlayer.start();
+ } else {
+ // Otherwise, we need to set it up to start when done preparing
+ autoPlayStart = true;
+ }
+ }
+ setPlayerState(STARTED);
+ } catch (Exception x) {
+ handleError(x);
+ }
+ }
+
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ public synchronized void reset() {
+ if (bufferTask != null) {
+ bufferTask.cancel();
+ bufferTask = null;
+ }
+ try {
+ // Only set to idle if it's not being killed to start RemoteController
+ if(remoteState == LOCAL) {
+ setPlayerState(IDLE);
+ }
+ mediaPlayer.setOnErrorListener(null);
+ mediaPlayer.setOnCompletionListener(null);
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && nextSetup) {
+ mediaPlayer.setNextMediaPlayer(null);
+ nextSetup = false;
+ }
+ mediaPlayer.reset();
+ subtractPosition = 0;
+ } catch (Exception x) {
+ handleError(x);
+ }
+ }
+
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ public synchronized void resetNext() {
+ try {
+ if (nextMediaPlayer != null) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && nextSetup) {
+ mediaPlayer.setNextMediaPlayer(null);
+ nextSetup = false;
+ }
+
+ nextMediaPlayer.setOnCompletionListener(null);
+ nextMediaPlayer.setOnErrorListener(null);
+ nextMediaPlayer.reset();
+ nextMediaPlayer.release();
+ nextMediaPlayer = null;
+ }
+ } catch (Exception e) {
+ Log.w(TAG, "Failed to reset next media player");
+ }
+ }
+
+ public int getPlayerPosition() {
+ try {
+ if (playerState == IDLE || playerState == DOWNLOADING || playerState == PREPARING) {
+ return 0;
+ }
+ if (remoteState != LOCAL) {
+ return remoteController.getRemotePosition() * 1000;
+ } else {
+ return Math.max(0, cachedPosition - subtractPosition);
+ }
+ } catch (Exception x) {
+ handleError(x);
+ return 0;
+ }
+ }
+
+ public synchronized int getPlayerDuration() {
+ if (currentPlaying != null) {
+ Integer duration = currentPlaying.getSong().getDuration();
+ if (duration != null) {
+ return duration * 1000;
+ }
+ }
+ if (playerState != IDLE && playerState != DOWNLOADING && playerState != PlayerState.PREPARING) {
+ if(remoteState == LOCAL) {
+ try {
+ return mediaPlayer.getDuration();
+ } catch (Exception x) {
+ handleError(x);
+ }
+ } else {
+ return remoteController.getRemoteDuration() * 1000;
+ }
+ }
+ return 0;
+ }
+
+ public PlayerState getPlayerState() {
+ return playerState;
+ }
+
+ public PlayerState getNextPlayerState() {
+ return nextPlayerState;
+ }
+
+ public synchronized void setPlayerState(final PlayerState playerState) {
+ Log.i(TAG, this.playerState.name() + " -> " + playerState.name() + " (" + currentPlaying + ")");
+
+ if (playerState == PAUSED) {
+ lifecycleSupport.serializeDownloadQueue();
+ }
+
+ boolean show = playerState == PlayerState.STARTED;
+ boolean pause = playerState == PlayerState.PAUSED;
+ boolean hide = playerState == PlayerState.STOPPED;
+ Util.broadcastPlaybackStatusChange(this, (currentPlaying != null) ? currentPlaying.getSong() : null, playerState);
+
+ this.playerState = playerState;
+
+ if(playerState == STARTED) {
+ Util.requestAudioFocus(this);
+ }
+
+ if (show) {
+ Notifications.showPlayingNotification(this, this, handler, currentPlaying.getSong());
+ } else if (pause) {
+ SharedPreferences prefs = Util.getPreferences(this);
+ if(prefs.getBoolean(Constants.PREFERENCES_KEY_PERSISTENT_NOTIFICATION, false)) {
+ Notifications.showPlayingNotification(this, this, handler, currentPlaying.getSong());
+ } else {
+ Notifications.hidePlayingNotification(this, this, handler);
+ }
+ } else if(hide) {
+ Notifications.hidePlayingNotification(this, this, handler);
+ }
+ if(mRemoteControl != null) {
+ mRemoteControl.setPlaybackState(playerState.getRemoteControlClientPlayState());
+ }
+
+ if (playerState == STARTED) {
+ scrobbler.scrobble(this, currentPlaying, false);
+ } else if (playerState == COMPLETED) {
+ scrobbler.scrobble(this, currentPlaying, true);
+ }
+
+ if(playerState == STARTED && positionCache == null && remoteState == LOCAL) {
+ positionCache = new PositionCache();
+ Thread thread = new Thread(positionCache, "PositionCache");
+ thread.start();
+ } else if(playerState != STARTED && positionCache != null) {
+ positionCache.stop();
+ positionCache = null;
+ }
+ }
+
+ private class PositionCache implements Runnable {
+ boolean isRunning = true;
+
+ public void stop() {
+ isRunning = false;
+ }
+
+ @Override
+ public void run() {
+ // Stop checking position before the song reaches completion
+ while(isRunning) {
+ try {
+ if(mediaPlayer != null && playerState == STARTED) {
+ int newPosition = mediaPlayer.getCurrentPosition();
+
+ // If sudden jump in position, something is wrong
+ if(subtractNextPosition == 0 && newPosition > (cachedPosition + 5000)) {
+ // Only 1 second should have gone by, subtract the rest
+ subtractPosition += (newPosition - cachedPosition) - 1000;
+ }
+
+ cachedPosition = newPosition;
+
+ if(subtractNextPosition > 0) {
+ // Subtraction amount is current position - how long ago onCompletionListener was called
+ subtractPosition = cachedPosition - (int) (System.currentTimeMillis() - subtractNextPosition);
+ if(subtractPosition < 0) {
+ subtractPosition = 0;
+ }
+ subtractNextPosition = 0;
+ }
+ }
+ Thread.sleep(1000L);
+ }
+ catch(Exception e) {
+ Log.w(TAG, "Crashed getting current position", e);
+ isRunning = false;
+ positionCache = null;
+ }
+ }
+ }
+ }
+
+ private void setPlayerStateCompleted() {
+ Log.i(TAG, this.playerState.name() + " -> " + PlayerState.COMPLETED + " (" + currentPlaying + ")");
+ this.playerState = PlayerState.COMPLETED;
+ if(positionCache != null) {
+ positionCache.stop();
+ positionCache = null;
+ }
+ scrobbler.scrobble(this, currentPlaying, true);
+ }
+
+ public synchronized void setNextPlayerState(PlayerState playerState) {
+ Log.i(TAG, "Next: " + this.nextPlayerState.name() + " -> " + playerState.name() + " (" + nextPlaying + ")");
+ this.nextPlayerState = playerState;
+ }
+
+ public void setSuggestedPlaylistName(String name, String id) {
+ this.suggestedPlaylistName = name;
+ this.suggestedPlaylistId = id;
+
+ SharedPreferences.Editor editor = Util.getPreferences(this).edit();
+ editor.putString(Constants.PREFERENCES_KEY_PLAYLIST_NAME, name);
+ editor.putString(Constants.PREFERENCES_KEY_PLAYLIST_ID, id);
+ editor.commit();
+ }
+
+ public String getSuggestedPlaylistName() {
+ return suggestedPlaylistName;
+ }
+
+ public String getSuggestedPlaylistId() {
+ return suggestedPlaylistId;
+ }
+
+ public boolean getEqualizerAvailable() {
+ return effectsController.isAvailable();
+ }
+
+ public EqualizerController getEqualizerController() {
+ EqualizerController controller = null;
+ try {
+ controller = effectsController.getEqualizerController();
+ if(controller.getEqualizer() == null) {
+ throw new Exception("Failed to get EQ");
+ }
+ } catch(Exception e) {
+ Log.w(TAG, "Failed to start EQ, retrying with new mediaPlayer: " + e);
+
+ // If we failed, we are going to try to reinitialize the MediaPlayer
+ boolean playing = playerState == STARTED;
+ int pos = getPlayerPosition();
+ mediaPlayer.pause();
+ Util.sleepQuietly(10L);
+ reset();
+
+ try {
+ // Resetup media player
+ mediaPlayer.setAudioSessionId(audioSessionId);
+ mediaPlayer.setDataSource(currentPlaying.getFile().getCanonicalPath());
+
+ controller = effectsController.getEqualizerController();
+ if(controller.getEqualizer() == null) {
+ throw new Exception("Failed to get EQ");
+ }
+ } catch(Exception e2) {
+ Log.w(TAG, "Failed to setup EQ even after reinitialization");
+ // Don't try again, just resetup media player and continue on
+ controller = null;
+ }
+
+ // Restart from same position and state we left off in
+ play(getCurrentPlayingIndex(), false, pos);
+ }
+
+ return controller;
+ }
+
+ public MediaRouteSelector getRemoteSelector() {
+ return mediaRouter.getSelector();
+ }
+
+ public boolean isSeekable() {
+ if(remoteState == LOCAL) {
+ return currentPlaying != null && currentPlaying.isWorkDone() && playerState != PREPARING;
+ } else if(remoteController != null) {
+ return remoteController.isSeekable();
+ } else {
+ return false;
+ }
+ }
+
+ public boolean isRemoteEnabled() {
+ return remoteState != LOCAL;
+ }
+
+ public RemoteController getRemoteController() {
+ return remoteController;
+ }
+
+ public void setRemoteEnabled(RemoteControlState newState) {
+ if(instance != null) {
+ setRemoteEnabled(newState, null);
+ }
+ }
+ public void setRemoteEnabled(RemoteControlState newState, Object ref) {
+ setRemoteState(newState, ref);
+
+ RouteInfo info = mediaRouter.getSelectedRoute();
+ String routeId = info.getId();
+
+ SharedPreferences.Editor editor = Util.getPreferences(this).edit();
+ editor.putInt(Constants.PREFERENCES_KEY_CONTROL_MODE, newState.getValue());
+ editor.putString(Constants.PREFERENCES_KEY_CONTROL_ID, routeId);
+ editor.commit();
+ }
+ private void setRemoteState(RemoteControlState newState, Object ref) {
+ setRemoteState(newState, ref, null);
+ }
+ private void setRemoteState(final RemoteControlState newState, final Object ref, final String routeId) {
+ // Don't try to do anything if already in the correct state
+ if(remoteState == newState) {
+ return;
+ }
+
+ boolean isPlaying = playerState == STARTED;
+ int position = getPlayerPosition();
+
+ if(remoteController != null) {
+ remoteController.stop();
+ setPlayerState(PlayerState.IDLE);
+ remoteController.shutdown();
+ remoteController = null;
+
+ if(newState == LOCAL) {
+ mediaRouter.setDefaultRoute();
+ }
+ }
+
+ Log.i(TAG, remoteState.name() + " => " + newState.name() + " (" + currentPlaying + ")");
+ remoteState = newState;
+ switch(newState) {
+ case JUKEBOX_SERVER:
+ remoteController = new JukeboxController(this, handler);
+ break;
+ case CHROMECAST: case DLNA:
+ if(ref == null) {
+ remoteState = LOCAL;
+ break;
+ }
+ remoteController = (RemoteController) ref;
+ break;
+ case LOCAL: default:
+ break;
+ }
+
+ if(remoteController != null) {
+ remoteController.create(isPlaying, position / 1000);
+ } else {
+ play(getCurrentPlayingIndex(), isPlaying, position);
+ }
+
+ if (remoteState != LOCAL) {
+ reset();
+
+ // Cancel current download, if necessary.
+ if (currentDownloading != null) {
+ currentDownloading.cancelDownload();
+ }
+
+ // Cancels current setup tasks
+ if(bufferTask != null && bufferTask.isRunning()) {
+ bufferTask.cancel();
+ bufferTask = null;
+ }
+ if(nextPlayingTask != null && nextPlayingTask.isRunning()) {
+ nextPlayingTask.cancel();
+ nextPlayingTask = null;
+ }
+ }
+
+ if(remoteState == LOCAL) {
+ checkDownloads();
+ }
+
+ if(routeId != null) {
+ final Runnable delayedReconnect = new Runnable() {
+ @Override
+ public void run() {
+ RouteInfo info = mediaRouter.getRouteForId(routeId);
+ if(info == null) {
+ setRemoteState(LOCAL, null);
+ } else if(newState == RemoteControlState.CHROMECAST) {
+ RemoteController controller = mediaRouter.getRemoteController(info);
+ if(controller != null) {
+ setRemoteState(RemoteControlState.CHROMECAST, controller);
+ }
+ }
+ mediaRouter.stopScan();
+ }
+ };
+
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ mediaRouter.startScan();
+ RouteInfo info = mediaRouter.getRouteForId(routeId);
+ if(info == null) {
+ handler.postDelayed(delayedReconnect, 2000L);
+ } else if(newState == RemoteControlState.CHROMECAST) {
+ RemoteController controller = mediaRouter.getRemoteController(info);
+ if(controller != null) {
+ setRemoteState(RemoteControlState.CHROMECAST, controller);
+ }
+ }
+ }
+ });
+ }
+ }
+
+ public void registerRoute(MediaRouter router) {
+ if(mRemoteControl != null) {
+ mRemoteControl.registerRoute(router);
+ }
+ }
+ public void unregisterRoute(MediaRouter router) {
+ if(mRemoteControl != null) {
+ mRemoteControl.unregisterRoute(router);
+ }
+ }
+
+ public void updateRemoteVolume(boolean up) {
+ AudioManager audioManager = (AudioManager)getSystemService(Context.AUDIO_SERVICE);
+ audioManager.adjustVolume(up ? AudioManager.ADJUST_RAISE : AudioManager.ADJUST_LOWER, AudioManager.FLAG_SHOW_UI);
+ }
+
+ public void startRemoteScan() {
+ mediaRouter.startScan();
+ }
+
+ public void stopRemoteScan() {
+ mediaRouter.stopScan();
+ }
+
+ private synchronized void bufferAndPlay() {
+ bufferAndPlay(0);
+ }
+ private synchronized void bufferAndPlay(int position) {
+ bufferAndPlay(position, true);
+ }
+ private synchronized void bufferAndPlay(int position, boolean start) {
+ if(!currentPlaying.isCompleteFileAvailable()) {
+ reset();
+
+ bufferTask = new BufferTask(currentPlaying, position, start);
+ bufferTask.execute();
+ } else {
+ doPlay(currentPlaying, position, start);
+ }
+ }
+
+ private synchronized void doPlay(final DownloadFile downloadFile, final int position, final boolean start) {
+ try {
+ downloadFile.setPlaying(true);
+ final File file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteFile() : downloadFile.getPartialFile();
+ boolean isPartial = file.equals(downloadFile.getPartialFile());
+ downloadFile.updateModificationDate();
+
+ subtractPosition = 0;
+ mediaPlayer.setOnCompletionListener(null);
+ mediaPlayer.setOnPreparedListener(null);
+ mediaPlayer.setOnErrorListener(null);
+ mediaPlayer.reset();
+ setPlayerState(IDLE);
+ try {
+ mediaPlayer.setAudioSessionId(audioSessionId);
+ } catch(Throwable e) {
+ mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
+ }
+ String dataSource = file.getAbsolutePath();
+ if(isPartial && !Util.isOffline(this)) {
+ if (proxy == null) {
+ proxy = new BufferProxy(this);
+ proxy.start();
+ }
+ proxy.setBufferFile(downloadFile);
+ dataSource = proxy.getPrivateAddress(dataSource);
+ Log.i(TAG, "Data Source: " + dataSource);
+ } else if(proxy != null) {
+ proxy.stop();
+ proxy = null;
+ }
+ mediaPlayer.setDataSource(dataSource);
+ setPlayerState(PREPARING);
+
+ mediaPlayer.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() {
+ public void onBufferingUpdate(MediaPlayer mp, int percent) {
+ Log.i(TAG, "Buffered " + percent + "%");
+ if (percent == 100) {
+ mediaPlayer.setOnBufferingUpdateListener(null);
+ }
+ }
+ });
+
+ mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
+ public void onPrepared(MediaPlayer mediaPlayer) {
+ try {
+ setPlayerState(PREPARED);
+
+ synchronized (DownloadService.this) {
+ if (position != 0) {
+ Log.i(TAG, "Restarting player from position " + position);
+ mediaPlayer.seekTo(position);
+ }
+ cachedPosition = position;
+
+ applyReplayGain(mediaPlayer, downloadFile);
+
+ if (start || autoPlayStart) {
+ mediaPlayer.start();
+ setPlayerState(STARTED);
+
+ // Disable autoPlayStart after done
+ autoPlayStart = false;
+ } else {
+ setPlayerState(PAUSED);
+ }
+ }
+
+ // Only call when starting, setPlayerState(PAUSED) already calls this
+ if(start) {
+ lifecycleSupport.serializeDownloadQueue();
+ }
+ } catch (Exception x) {
+ handleError(x);
+ }
+ }
+ });
+
+ setupHandlers(downloadFile, isPartial, start);
+
+ mediaPlayer.prepareAsync();
+ } catch (Exception x) {
+ handleError(x);
+ }
+ }
+
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ private synchronized void setupNext(final DownloadFile downloadFile) {
+ try {
+ final File file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteFile() : downloadFile.getPartialFile();
+ resetNext();
+
+ // Exit when using remote controllers
+ if(remoteState != LOCAL) {
+ return;
+ }
+
+ nextMediaPlayer = new MediaPlayer();
+ nextMediaPlayer.setWakeMode(DownloadService.this, PowerManager.PARTIAL_WAKE_LOCK);
+ try {
+ nextMediaPlayer.setAudioSessionId(audioSessionId);
+ } catch(Throwable e) {
+ nextMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
+ }
+ nextMediaPlayer.setDataSource(file.getPath());
+ setNextPlayerState(PREPARING);
+
+ nextMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
+ public void onPrepared(MediaPlayer mp) {
+ try {
+ setNextPlayerState(PREPARED);
+
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && (playerState == PlayerState.STARTED || playerState == PlayerState.PAUSED)) {
+ mediaPlayer.setNextMediaPlayer(nextMediaPlayer);
+ nextSetup = true;
+ }
+
+ applyReplayGain(nextMediaPlayer, downloadFile);
+ } catch (Exception x) {
+ handleErrorNext(x);
+ }
+ }
+ });
+
+ nextMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
+ public boolean onError(MediaPlayer mediaPlayer, int what, int extra) {
+ Log.w(TAG, "Error on playing next " + "(" + what + ", " + extra + "): " + downloadFile);
+ return true;
+ }
+ });
+
+ nextMediaPlayer.prepareAsync();
+ } catch (Exception x) {
+ handleErrorNext(x);
+ }
+ }
+
+ private void setupHandlers(final DownloadFile downloadFile, final boolean isPartial, final boolean isPlaying) {
+ final int duration = downloadFile.getSong().getDuration() == null ? 0 : downloadFile.getSong().getDuration() * 1000;
+ mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
+ public boolean onError(MediaPlayer mediaPlayer, int what, int extra) {
+ Log.w(TAG, "Error on playing file " + "(" + what + ", " + extra + "): " + downloadFile);
+ int pos = getPlayerPosition();
+ reset();
+ if (!isPartial || (downloadFile.isWorkDone() && (Math.abs(duration - pos) < 10000))) {
+ playNext();
+ } else {
+ downloadFile.setPlaying(false);
+ doPlay(downloadFile, pos, isPlaying);
+ downloadFile.setPlaying(true);
+ }
+ return true;
+ }
+ });
+
+ mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
+ @Override
+ public void onCompletion(MediaPlayer mediaPlayer) {
+ // Acquire a temporary wakelock, since when we return from
+ // this callback the MediaPlayer will release its wakelock
+ // and allow the device to go to sleep.
+ wakeLock.acquire(30000);
+
+ setPlayerStateCompleted();
+
+ int pos = getPlayerPosition();
+ Log.i(TAG, "Ending position " + pos + " of " + duration);
+ if (!isPartial || (downloadFile.isWorkDone() && (Math.abs(duration - pos) < 10000)) || nextSetup) {
+ playNext();
+ postPlayCleanup(downloadFile);
+ } else {
+ // If file is not completely downloaded, restart the playback from the current position.
+ synchronized (DownloadService.this) {
+ if (downloadFile.isWorkDone()) {
+ // Complete was called early even though file is fully buffered
+ Log.i(TAG, "Requesting restart from " + pos + " of " + duration);
+ reset();
+ downloadFile.setPlaying(false);
+ doPlay(downloadFile, pos, true);
+ downloadFile.setPlaying(true);
+ } else {
+ Log.i(TAG, "Requesting restart from " + pos + " of " + duration);
+ reset();
+ bufferTask = new BufferTask(downloadFile, pos, true);
+ bufferTask.execute();
+ }
+ }
+ checkDownloads();
+ }
+ }
+ });
+ }
+
+ public void setSleepTimerDuration(int duration){
+ timerDuration = duration;
+ }
+
+ public void startSleepTimer(){
+ if(sleepTimer != null){
+ sleepTimer.cancel();
+ sleepTimer.purge();
+ }
+
+ sleepTimer = new Timer();
+
+ sleepTimer.schedule(new TimerTask() {
+ @Override
+ public void run() {
+ pause();
+ sleepTimer.cancel();
+ sleepTimer.purge();
+ sleepTimer = null;
+ }
+
+ }, timerDuration * 60 * 1000);
+ }
+
+ public void stopSleepTimer() {
+ if(sleepTimer != null){
+ sleepTimer.cancel();
+ sleepTimer.purge();
+ }
+ sleepTimer = null;
+ }
+
+ public boolean getSleepTimer() {
+ return sleepTimer != null;
+ }
+
+ public void setVolume(float volume) {
+ if(mediaPlayer != null && (playerState == STARTED || playerState == PAUSED || playerState == STOPPED)) {
+ try {
+ this.volume = volume;
+ reapplyVolume();
+ } catch(Exception e) {
+ Log.w(TAG, "Failed to set volume");
+ }
+ }
+ }
+ public void reapplyVolume() {
+ applyReplayGain(mediaPlayer, currentPlaying);
+ }
+
+ public synchronized void swap(boolean mainList, int from, int to) {
+ List<DownloadFile> list = mainList ? downloadList : backgroundDownloadList;
+ int max = list.size();
+ if(to >= max) {
+ to = max - 1;
+ }
+ else if(to < 0) {
+ to = 0;
+ }
+
+ DownloadFile movedSong = list.remove(from);
+ list.add(to, movedSong);
+ currentPlayingIndex = downloadList.indexOf(currentPlaying);
+ if(mainList) {
+ if(remoteState == LOCAL || (remoteController != null && remoteController.isNextSupported())) {
+ // Moving next playing, current playing, or moving a song to be next playing
+ if(movedSong == nextPlaying || movedSong == currentPlaying || (currentPlayingIndex + 1) == to) {
+ setNextPlaying();
+ }
+ } else {
+ updateRemotePlaylist();
+ }
+ }
+ }
+
+ public synchronized void serializeQueue() {
+ serializeQueue(true);
+ }
+ public synchronized void serializeQueue(boolean serializeRemote) {
+ if(playerState == PlayerState.PAUSED) {
+ lifecycleSupport.serializeDownloadQueue(serializeRemote);
+ }
+ }
+
+ private void handleError(Exception x) {
+ Log.w(TAG, "Media player error: " + x, x);
+ if(mediaPlayer != null) {
+ try {
+ mediaPlayer.reset();
+ } catch(Exception e) {
+ Log.e(TAG, "Failed to reset player in error handler");
+ }
+ }
+ setPlayerState(IDLE);
+ }
+ private void handleErrorNext(Exception x) {
+ Log.w(TAG, "Next Media player error: " + x, x);
+ try {
+ nextMediaPlayer.reset();
+ } catch(Exception e) {
+ Log.e(TAG, "Failed to reset next media player", x);
+ }
+ setNextPlayerState(IDLE);
+ }
+
+ public synchronized void checkDownloads() {
+ if (!Util.isExternalStoragePresent() || !lifecycleSupport.isExternalStorageAvailable()) {
+ return;
+ }
+
+ if(removePlayed) {
+ checkRemovePlayed();
+ }
+ if (shufflePlay) {
+ checkShufflePlay();
+ }
+ if(artistRadio) {
+ checkArtistRadio();
+ }
+
+ if (!Util.isNetworkConnected(this, true) || Util.isOffline(this)) {
+ return;
+ }
+
+ if (downloadList.isEmpty() && backgroundDownloadList.isEmpty()) {
+ return;
+ }
+
+ // Need to download current playing and not casting?
+ if (currentPlaying != null && remoteState == LOCAL && currentPlaying != currentDownloading && !currentPlaying.isWorkDone()) {
+ // Cancel current download, if necessary.
+ if (currentDownloading != null) {
+ currentDownloading.cancelDownload();
+ }
+
+ currentDownloading = currentPlaying;
+ currentDownloading.download();
+ cleanupCandidates.add(currentDownloading);
+ }
+
+ // Find a suitable target for download.
+ else if (currentDownloading == null || currentDownloading.isWorkDone() || currentDownloading.isFailed() && ((!downloadList.isEmpty() && remoteState == LOCAL) || !backgroundDownloadList.isEmpty())) {
+ currentDownloading = null;
+ int n = size();
+
+ int preloaded = 0;
+
+ if(n != 0 && remoteState == LOCAL) {
+ int start = currentPlaying == null ? 0 : getCurrentPlayingIndex();
+ if(start == -1) {
+ start = 0;
+ }
+ int i = start;
+ do {
+ DownloadFile downloadFile = downloadList.get(i);
+ if (!downloadFile.isWorkDone() && !downloadFile.isFailedMax()) {
+ if (downloadFile.shouldSave() || preloaded < Util.getPreloadCount(this)) {
+ currentDownloading = downloadFile;
+ currentDownloading.download();
+ cleanupCandidates.add(currentDownloading);
+ if(i == (start + 1)) {
+ setNextPlayerState(DOWNLOADING);
+ }
+ break;
+ }
+ } else if (currentPlaying != downloadFile) {
+ preloaded++;
+ }
+
+ i = (i + 1) % n;
+ } while (i != start);
+ }
+
+ if((preloaded + 1 == n || preloaded >= Util.getPreloadCount(this) || downloadList.isEmpty() || remoteState != LOCAL) && !backgroundDownloadList.isEmpty()) {
+ for(int i = 0; i < backgroundDownloadList.size(); i++) {
+ DownloadFile downloadFile = backgroundDownloadList.get(i);
+ if(downloadFile.isWorkDone() && (!downloadFile.shouldSave() || downloadFile.isSaved()) || downloadFile.isFailedMax()) {
+ // Don't need to keep list like active song list
+ backgroundDownloadList.remove(i);
+ revision++;
+ i--;
+ } else {
+ currentDownloading = downloadFile;
+ currentDownloading.download();
+ cleanupCandidates.add(currentDownloading);
+ break;
+ }
+ }
+ }
+ }
+
+ if(!backgroundDownloadList.isEmpty()) {
+ Notifications.showDownloadingNotification(this, this, handler, currentDownloading, backgroundDownloadList.size());
+ downloadOngoing = true;
+ } else if(backgroundDownloadList.isEmpty() && downloadOngoing) {
+ Notifications.hideDownloadingNotification(this, this, handler);
+ downloadOngoing = false;
+ }
+
+ // Delete obsolete .partial and .complete files.
+ cleanup();
+ }
+
+ private synchronized void checkRemovePlayed() {
+ while(currentPlayingIndex > 0) {
+ downloadList.remove(0);
+ currentPlayingIndex = downloadList.indexOf(currentPlaying);
+ revision++;
+ }
+ }
+
+ private synchronized void checkShufflePlay() {
+
+ // Get users desired random playlist size
+ SharedPreferences prefs = Util.getPreferences(this);
+ int listSize = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_RANDOM_SIZE, "20"));
+ boolean wasEmpty = downloadList.isEmpty();
+
+ long revisionBefore = revision;
+
+ // First, ensure that list is at least 20 songs long.
+ int size = size();
+ if (size < listSize) {
+ for (MusicDirectory.Entry song : shufflePlayBuffer.get(listSize - size)) {
+ DownloadFile downloadFile = new DownloadFile(this, song, false);
+ downloadList.add(downloadFile);
+ revision++;
+ }
+ }
+
+ int currIndex = currentPlaying == null ? 0 : getCurrentPlayingIndex();
+
+ // Only shift playlist if playing song #5 or later.
+ if (currIndex > 4) {
+ int songsToShift = currIndex - 2;
+ for (MusicDirectory.Entry song : shufflePlayBuffer.get(songsToShift)) {
+ downloadList.add(new DownloadFile(this, song, false));
+ downloadList.get(0).cancelDownload();
+ downloadList.remove(0);
+ revision++;
+ }
+ }
+ currentPlayingIndex = downloadList.indexOf(currentPlaying);
+
+ if (revisionBefore != revision) {
+ updateRemotePlaylist();
+ }
+
+ if (wasEmpty && !downloadList.isEmpty()) {
+ play(0);
+ }
+ }
+
+ private synchronized void checkArtistRadio() {
+ // Get users desired random playlist size
+ SharedPreferences prefs = Util.getPreferences(this);
+ int listSize = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_RANDOM_SIZE, "20"));
+ boolean wasEmpty = downloadList.isEmpty();
+
+ long revisionBefore = revision;
+
+ // First, ensure that list is at least 20 songs long.
+ int size = size();
+ if (size < listSize) {
+ for (MusicDirectory.Entry song : artistRadioBuffer.get(listSize - size)) {
+ DownloadFile downloadFile = new DownloadFile(this, song, false);
+ downloadList.add(downloadFile);
+ revision++;
+ }
+ }
+
+ int currIndex = currentPlaying == null ? 0 : getCurrentPlayingIndex();
+
+ // Only shift playlist if playing song #5 or later.
+ if (currIndex > 4) {
+ int songsToShift = currIndex - 2;
+ for (MusicDirectory.Entry song : artistRadioBuffer.get(songsToShift)) {
+ downloadList.add(new DownloadFile(this, song, false));
+ downloadList.get(0).cancelDownload();
+ downloadList.remove(0);
+ revision++;
+ }
+ }
+ currentPlayingIndex = downloadList.indexOf(currentPlaying);
+
+ if (revisionBefore != revision) {
+ updateRemotePlaylist();
+ }
+
+ if (wasEmpty && !downloadList.isEmpty()) {
+ play(0);
+ }
+ }
+
+ public long getDownloadListUpdateRevision() {
+ return revision;
+ }
+
+ private synchronized void cleanup() {
+ Iterator<DownloadFile> iterator = cleanupCandidates.iterator();
+ while (iterator.hasNext()) {
+ DownloadFile downloadFile = iterator.next();
+ if (downloadFile != currentPlaying && downloadFile != currentDownloading) {
+ if (downloadFile.cleanup()) {
+ iterator.remove();
+ }
+ }
+ }
+ }
+
+ public void postPlayCleanup() {
+ postPlayCleanup(currentPlaying);
+ }
+ public void postPlayCleanup(DownloadFile downloadFile) {
+ if(downloadFile == null) {
+ return;
+ }
+
+ // Finished loading, delete when list is cleared
+ if (downloadFile.getSong() instanceof PodcastEpisode) {
+ toDelete.add(downloadFile);
+ }
+ clearCurrentBookmark(downloadFile.getSong(), true);
+ }
+
+ private boolean isPastCutoff() {
+ return isPastCutoff(getPlayerPosition(), getPlayerDuration());
+ }
+ private boolean isPastCutoff(int position, int duration) {
+ return isPastCutoff(position, duration, false);
+ }
+ private boolean isPastCutoff(int position, int duration, boolean allowSkipping) {
+ if(currentPlaying == null) {
+ return false;
+ }
+
+ int cutoffPoint = (int) (duration * DELETE_CUTOFF);
+ boolean isPastCutoff = duration > 0 && position > cutoffPoint;
+
+ // Check to make sure song isn't within 10 seconds of where it was created
+ MusicDirectory.Entry entry = currentPlaying.getSong();
+ if(entry != null && entry.getBookmark() != null) {
+ Bookmark bookmark = entry.getBookmark();
+ if(position < (bookmark.getPosition() + 10000)) {
+ isPastCutoff = false;
+ }
+ }
+
+ // Check to make sure we aren't in a series of similar content before deleting bookmark
+ if(isPastCutoff && allowSkipping) {
+ // Check to make sure:
+ // Is an audio book
+ // Next playing exists and is not a wrap around or a shuffle
+ // Next playing is from same context as current playing, so not at end of list
+ if(entry.isAudioBook() && nextPlaying != null && downloadList.indexOf(nextPlaying) != 0 && !shufflePlay
+ && entry.getParent() != null && entry.getParent().equals(nextPlaying.getSong().getParent())) {
+ isPastCutoff = false;
+ }
+ }
+
+ return isPastCutoff;
+ }
+
+ private void clearCurrentBookmark() {
+ clearCurrentBookmark(false);
+ }
+ private void clearCurrentBookmark(boolean checkDelete) {
+ // If current is null, nothing to do
+ if(currentPlaying == null) {
+ return;
+ }
+
+ clearCurrentBookmark(currentPlaying.getSong(), checkDelete);
+ }
+ private void clearCurrentBookmark(final MusicDirectory.Entry entry, boolean checkDelete) {
+ // If no bookmark, move on
+ if(entry.getBookmark() == null) {
+ return;
+ }
+
+ // If delete is not specified, check position
+ if(!checkDelete) {
+ checkDelete = isPastCutoff();
+ }
+
+ // If supposed to delete
+ if(checkDelete) {
+ new SilentBackgroundTask<Void>(this) {
+ @Override
+ public Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(DownloadService.this);
+ entry.setBookmark(null);
+ musicService.deleteBookmark(entry, DownloadService.this, null);
+
+ MusicDirectory.Entry found = UpdateView.findEntry(entry);
+ if(found != null) {
+ found.setBookmark(null);
+ }
+ return null;
+ }
+
+ @Override
+ public void error(Throwable error) {
+ Log.e(TAG, "Failed to delete bookmark", error);
+
+ String msg;
+ if(error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = DownloadService.this.getResources().getString(R.string.bookmark_deleted_error, entry.getTitle()) + " " + getErrorMessage(error);
+ }
+
+ Util.toast(DownloadService.this, msg, false);
+ }
+ }.execute();
+ }
+ }
+
+ private void checkAddBookmark() {
+ // Don't do anything if no current playing
+ if(currentPlaying == null || !ServerInfo.canBookmark(this)) {
+ return;
+ }
+
+ final MusicDirectory.Entry entry = currentPlaying.getSong();
+ int duration = getPlayerDuration();
+
+ // If song is podcast or long go ahead and auto add a bookmark
+ if(entry.isPodcast() || entry.isAudioBook() || duration > (10L * 60L * 1000L)) {
+ final Context context = this;
+ final int position = getPlayerPosition();
+
+ // Don't bother when at beginning
+ if(position < 5000L) {
+ return;
+ }
+
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ public Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ entry.setBookmark(new Bookmark(position));
+ musicService.createBookmark(entry, position, "Auto created by DSub", context, null);
+
+ MusicDirectory.Entry found = UpdateView.findEntry(entry);
+ if(found != null) {
+ found.setBookmark(new Bookmark(position));
+ }
+
+ return null;
+ }
+
+ @Override
+ public void error(Throwable error) {
+ Log.w(TAG, "Failed to create automatic bookmark", error);
+
+ String msg;
+ if(error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.download_save_bookmark_failed) + getErrorMessage(error);
+ }
+
+ Util.toast(context, msg, false);
+ }
+ }.execute();
+ }
+ }
+
+ private void applyReplayGain(MediaPlayer mediaPlayer, DownloadFile downloadFile) {
+ if(currentPlaying == null) {
+ return;
+ }
+
+ SharedPreferences prefs = Util.getPreferences(this);
+ try {
+ float[] rg = BastpUtil.getReplayGainValues(downloadFile.getFile().getCanonicalPath()); /* track, album */
+ float adjust = 0f;
+ if (prefs.getBoolean(Constants.PREFERENCES_KEY_REPLAY_GAIN, false)) {
+ boolean singleAlbum = false;
+
+ String replayGainType = prefs.getString(Constants.PREFERENCES_KEY_REPLAY_GAIN_TYPE, "1");
+ // 1 => Smart replay gain
+ if("1".equals(replayGainType)) {
+ // Check if part of at least <REQUIRED_ALBUM_MATCHES> consequetive songs of the same album
+
+ int index = downloadList.indexOf(downloadFile);
+ if(index != -1) {
+ String albumName = downloadFile.getSong().getAlbum();
+ int matched = 0;
+
+ // Check forwards
+ for(int i = index + 1; i < downloadList.size() && matched < REQUIRED_ALBUM_MATCHES; i++) {
+ if(albumName.equals(downloadList.get(i).getSong().getAlbum())) {
+ matched++;
+ } else {
+ break;
+ }
+ }
+
+ // Check backwards
+ for(int i = index - 1; i >= 0 && matched < REQUIRED_ALBUM_MATCHES; i--) {
+ if(albumName.equals(downloadList.get(i).getSong().getAlbum())) {
+ matched++;
+ } else {
+ break;
+ }
+ }
+
+ if(matched >= REQUIRED_ALBUM_MATCHES) {
+ singleAlbum = true;
+ }
+ }
+ }
+ // 2 => Use album tags
+ else if("2".equals(replayGainType)) {
+ singleAlbum = true;
+ }
+ // 3 => Use track tags
+ // Already false, no need to do anything here
+
+
+ // If playing a single album or no track gain, use album gain
+ if((singleAlbum || rg[0] == 0) && rg[1] != 0) {
+ adjust = rg[1];
+ } else {
+ // Otherwise, give priority to track gain
+ adjust = rg[0];
+ }
+
+ if (adjust == 0) {
+ /* No RG value found: decrease volume for untagged song if requested by user */
+ int untagged = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_REPLAY_GAIN_UNTAGGED, "0"));
+ adjust = (untagged - 150) / 10f;
+ } else {
+ int bump = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_REPLAY_GAIN_BUMP, "150"));
+ adjust += (bump - 150) / 10f;
+ }
+ }
+
+ float rg_result = ((float) Math.pow(10, (adjust / 20))) * volume;
+ if (rg_result > 1.0f) {
+ rg_result = 1.0f; /* android would IGNORE the change if this is > 1 and we would end up with the wrong volume */
+ } else if (rg_result < 0.0f) {
+ rg_result = 0.0f;
+ }
+ mediaPlayer.setVolume(rg_result, rg_result);
+ } catch(IOException e) {
+ Log.w(TAG, "Failed to apply replay gain values", e);
+ }
+ }
+
+ private class BufferTask extends SilentBackgroundTask<Void> {
+ private final DownloadFile downloadFile;
+ private final int position;
+ private final long expectedFileSize;
+ private final File partialFile;
+ private final boolean start;
+
+ public BufferTask(DownloadFile downloadFile, int position, boolean start) {
+ super(instance);
+ this.downloadFile = downloadFile;
+ this.position = position;
+ partialFile = downloadFile.getPartialFile();
+ this.start = start;
+
+ // Calculate roughly how many bytes BUFFER_LENGTH_SECONDS corresponds to.
+ int bitRate = downloadFile.getBitRate();
+ long byteCount = Math.max(100000, bitRate * 1024L / 8L * 5L);
+
+ // Find out how large the file should grow before resuming playback.
+ Log.i(TAG, "Buffering from position " + position + " and bitrate " + bitRate);
+ expectedFileSize = (position * bitRate / 8) + byteCount;
+ }
+
+ @Override
+ public Void doInBackground() throws InterruptedException {
+ setPlayerState(DOWNLOADING);
+
+ while (!bufferComplete()) {
+ Thread.sleep(1000L);
+ if (isCancelled() || downloadFile.isFailedMax()) {
+ return null;
+ } else if(!downloadFile.isFailedMax() && !downloadFile.isDownloading()) {
+ checkDownloads();
+ }
+ }
+ doPlay(downloadFile, position, start);
+
+ return null;
+ }
+
+ private boolean bufferComplete() {
+ boolean completeFileAvailable = downloadFile.isWorkDone();
+ long size = partialFile.length();
+
+ Log.i(TAG, "Buffering " + partialFile + " (" + size + "/" + expectedFileSize + ", " + completeFileAvailable + ")");
+ return completeFileAvailable || size >= expectedFileSize;
+ }
+
+ @Override
+ public String toString() {
+ return "BufferTask (" + downloadFile + ")";
+ }
+ }
+
+ private class CheckCompletionTask extends SilentBackgroundTask<Void> {
+ private final DownloadFile downloadFile;
+ private final File partialFile;
+
+ public CheckCompletionTask(DownloadFile downloadFile) {
+ super(instance);
+ this.downloadFile = downloadFile;
+ if(downloadFile != null) {
+ partialFile = downloadFile.getPartialFile();
+ } else {
+ partialFile = null;
+ }
+ }
+
+ @Override
+ public Void doInBackground() throws InterruptedException {
+ if(downloadFile == null) {
+ return null;
+ }
+
+ // Do an initial sleep so this prepare can't compete with main prepare
+ Thread.sleep(5000L);
+ while (!bufferComplete()) {
+ Thread.sleep(5000L);
+ if (isCancelled()) {
+ return null;
+ }
+ }
+
+ // Start the setup of the next media player
+ mediaPlayerHandler.post(new Runnable() {
+ public void run() {
+ setupNext(downloadFile);
+ }
+ });
+ return null;
+ }
+
+ private boolean bufferComplete() {
+ boolean completeFileAvailable = downloadFile.isWorkDone();
+ Log.i(TAG, "Buffering next " + partialFile + " (" + partialFile.length() + "): " + completeFileAvailable);
+ return completeFileAvailable && (playerState == PlayerState.STARTED || playerState == PlayerState.PAUSED);
+ }
+
+ @Override
+ public String toString() {
+ return "CheckCompletionTask (" + downloadFile + ")";
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/DownloadServiceLifecycleSupport.java b/app/src/main/java/github/daneren2005/dsub/service/DownloadServiceLifecycleSupport.java
new file mode 100644
index 00000000..c9f92f41
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/DownloadServiceLifecycleSupport.java
@@ -0,0 +1,445 @@
+/*
+ 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.service;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.media.RemoteControlClient;
+import android.os.Handler;
+import android.os.Looper;
+import android.telephony.PhoneStateListener;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+import android.view.KeyEvent;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.PlayerQueue;
+import github.daneren2005.dsub.domain.PlayerState;
+import github.daneren2005.dsub.domain.ServerInfo;
+import github.daneren2005.dsub.util.BackgroundTask;
+import github.daneren2005.dsub.util.CacheCleaner;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.SilentBackgroundTask;
+import github.daneren2005.dsub.util.Util;
+
+import static github.daneren2005.dsub.domain.PlayerState.PREPARING;
+
+/**
+ * @author Sindre Mehus
+ */
+public class DownloadServiceLifecycleSupport {
+ private static final String TAG = DownloadServiceLifecycleSupport.class.getSimpleName();
+ public static final String FILENAME_DOWNLOADS_SER = "downloadstate2.ser";
+
+ private final DownloadService downloadService;
+ private Looper eventLooper;
+ private Handler eventHandler;
+ private BroadcastReceiver ejectEventReceiver;
+ private PhoneStateListener phoneStateListener;
+ private boolean externalStorageAvailable= true;
+ private ReentrantLock lock = new ReentrantLock();
+ private final AtomicBoolean setup = new AtomicBoolean(false);
+ private long lastPressTime = 0;
+ private SilentBackgroundTask<Void> currentSavePlayQueueTask = null;
+ private Date lastChange = null;
+
+ /**
+ * This receiver manages the intent that could come from other applications.
+ */
+ private BroadcastReceiver intentReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(final Context context, final Intent intent) {
+ eventHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ String action = intent.getAction();
+ Log.i(TAG, "intentReceiver.onReceive: " + action);
+ if (DownloadService.CMD_PLAY.equals(action)) {
+ downloadService.play();
+ } else if (DownloadService.CMD_NEXT.equals(action)) {
+ downloadService.next();
+ } else if (DownloadService.CMD_PREVIOUS.equals(action)) {
+ downloadService.previous();
+ } else if (DownloadService.CMD_TOGGLEPAUSE.equals(action)) {
+ downloadService.togglePlayPause();
+ } else if (DownloadService.CMD_PAUSE.equals(action)) {
+ downloadService.pause();
+ } else if (DownloadService.CMD_STOP.equals(action)) {
+ downloadService.pause();
+ downloadService.seekTo(0);
+ }
+ }
+ });
+ }
+ };
+
+
+ public DownloadServiceLifecycleSupport(DownloadService downloadService) {
+ this.downloadService = downloadService;
+ }
+
+ public void onCreate() {
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ Looper.prepare();
+ eventLooper = Looper.myLooper();
+ eventHandler = new Handler(eventLooper);
+
+ // Deserialize queue before starting looper
+ try {
+ lock.lock();
+ deserializeDownloadQueueNow();
+
+ // Wait until PREPARING is done to mark lifecycle as ready to receive events
+ while(downloadService.getPlayerState() == PREPARING) {
+ Util.sleepQuietly(50L);
+ }
+
+ setup.set(true);
+ } finally {
+ lock.unlock();
+ }
+
+ Looper.loop();
+ }
+ }, "DownloadServiceLifecycleSupport").start();
+
+ // Stop when SD card is ejected.
+ ejectEventReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ externalStorageAvailable = Intent.ACTION_MEDIA_MOUNTED.equals(intent.getAction());
+ if (!externalStorageAvailable) {
+ Log.i(TAG, "External media is ejecting. Stopping playback.");
+ downloadService.reset();
+ } else {
+ Log.i(TAG, "External media is available.");
+ }
+ }
+ };
+ IntentFilter ejectFilter = new IntentFilter(Intent.ACTION_MEDIA_EJECT);
+ ejectFilter.addAction(Intent.ACTION_MEDIA_MOUNTED);
+ ejectFilter.addDataScheme("file");
+ downloadService.registerReceiver(ejectEventReceiver, ejectFilter);
+
+ // React to media buttons.
+ Util.registerMediaButtonEventReceiver(downloadService);
+
+ // Pause temporarily on incoming phone calls.
+ phoneStateListener = new MyPhoneStateListener();
+ TelephonyManager telephonyManager = (TelephonyManager) downloadService.getSystemService(Context.TELEPHONY_SERVICE);
+ telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
+
+ // Register the handler for outside intents.
+ IntentFilter commandFilter = new IntentFilter();
+ commandFilter.addAction(DownloadService.CMD_PLAY);
+ commandFilter.addAction(DownloadService.CMD_TOGGLEPAUSE);
+ commandFilter.addAction(DownloadService.CMD_PAUSE);
+ commandFilter.addAction(DownloadService.CMD_STOP);
+ commandFilter.addAction(DownloadService.CMD_PREVIOUS);
+ commandFilter.addAction(DownloadService.CMD_NEXT);
+ commandFilter.addAction(DownloadService.CANCEL_DOWNLOADS);
+ downloadService.registerReceiver(intentReceiver, commandFilter);
+
+ new CacheCleaner(downloadService, downloadService).clean();
+ }
+
+ public boolean isInitialized() {
+ return setup.get();
+ }
+
+ public void onStart(final Intent intent) {
+ if (intent != null) {
+ final String action = intent.getAction();
+
+ if(eventHandler == null) {
+ Util.sleepQuietly(100L);
+ }
+ if(eventHandler == null) {
+ return;
+ }
+
+ eventHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if(!setup.get()) {
+ lock.lock();
+ lock.unlock();
+ }
+
+ if(DownloadService.START_PLAY.equals(action)) {
+ int offlinePref = intent.getIntExtra(Constants.PREFERENCES_KEY_OFFLINE, 0);
+ if(offlinePref != 0) {
+ boolean offline = (offlinePref == 2);
+ Util.setOffline(downloadService, offline);
+ if (offline) {
+ downloadService.clearIncomplete();
+ } else {
+ downloadService.checkDownloads();
+ }
+ }
+
+ if(intent.getBooleanExtra(Constants.INTENT_EXTRA_NAME_SHUFFLE, false)) {
+ // Add shuffle parameters
+ SharedPreferences.Editor editor = Util.getPreferences(downloadService).edit();
+ String startYear = intent.getStringExtra(Constants.PREFERENCES_KEY_SHUFFLE_START_YEAR);
+ if(startYear != null) {
+ editor.putString(Constants.PREFERENCES_KEY_SHUFFLE_START_YEAR, startYear);
+ }
+
+ String endYear = intent.getStringExtra(Constants.PREFERENCES_KEY_SHUFFLE_END_YEAR);
+ if(endYear != null) {
+ editor.putString(Constants.PREFERENCES_KEY_SHUFFLE_END_YEAR, endYear);
+ }
+
+ String genre = intent.getStringExtra(Constants.PREFERENCES_KEY_SHUFFLE_GENRE);
+ if(genre != null) {
+ editor.putString(Constants.PREFERENCES_KEY_SHUFFLE_GENRE, genre);
+ }
+ editor.commit();
+
+ downloadService.setShufflePlayEnabled(true);
+ } else {
+ downloadService.start();
+ }
+ } else if(DownloadService.CMD_TOGGLEPAUSE.equals(action)) {
+ downloadService.togglePlayPause();
+ } else if(DownloadService.CMD_NEXT.equals(action)) {
+ downloadService.next();
+ } else if(DownloadService.CMD_PREVIOUS.equals(action)) {
+ downloadService.previous();
+ } else if(DownloadService.CANCEL_DOWNLOADS.equals(action)) {
+ downloadService.clearBackground();
+ } else if(intent.getExtras() != null) {
+ final KeyEvent event = (KeyEvent) intent.getExtras().get(Intent.EXTRA_KEY_EVENT);
+ if (event != null) {
+ handleKeyEvent(event);
+ }
+ }
+ }
+ });
+ }
+ }
+
+ public void onDestroy() {
+ serializeDownloadQueue();
+ eventLooper.quit();
+ downloadService.unregisterReceiver(ejectEventReceiver);
+ downloadService.unregisterReceiver(intentReceiver);
+
+ TelephonyManager telephonyManager = (TelephonyManager) downloadService.getSystemService(Context.TELEPHONY_SERVICE);
+ telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE);
+ }
+
+ public boolean isExternalStorageAvailable() {
+ return externalStorageAvailable;
+ }
+
+ public void serializeDownloadQueue() {
+ serializeDownloadQueue(true);
+ }
+ public void serializeDownloadQueue(final boolean serializeRemote) {
+ if(!setup.get()) {
+ return;
+ }
+
+ final List<DownloadFile> songs = new ArrayList<DownloadFile>(downloadService.getSongs());
+ eventHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if(lock.tryLock()) {
+ try {
+ serializeDownloadQueueNow(songs, serializeRemote);
+ } finally {
+ lock.unlock();
+ }
+ }
+ }
+ });
+ }
+
+ public void serializeDownloadQueueNow(List<DownloadFile> songs, boolean serializeRemote) {
+ final PlayerQueue state = new PlayerQueue();
+ for (DownloadFile downloadFile : songs) {
+ state.songs.add(downloadFile.getSong());
+ }
+ for (DownloadFile downloadFile : downloadService.getToDelete()) {
+ state.toDelete.add(downloadFile.getSong());
+ }
+ state.currentPlayingIndex = downloadService.getCurrentPlayingIndex();
+ state.currentPlayingPosition = downloadService.getPlayerPosition();
+
+ DownloadFile currentPlaying = downloadService.getCurrentPlaying();
+ if(currentPlaying != null) {
+ state.renameCurrent = currentPlaying.isWorkDone() && !currentPlaying.isCompleteFileAvailable();
+ }
+ state.changed = lastChange = new Date();
+
+ Log.i(TAG, "Serialized currentPlayingIndex: " + state.currentPlayingIndex + ", currentPlayingPosition: " + state.currentPlayingPosition);
+ FileUtil.serialize(downloadService, state, FILENAME_DOWNLOADS_SER);
+
+ // If we are on Subsonic 5.2+, save play queue
+ if(serializeRemote && ServerInfo.canSavePlayQueue(downloadService) && !Util.isOffline(downloadService) && state.songs.size() > 0) {
+ // Cancel any currently running tasks
+ if(currentSavePlayQueueTask != null) {
+ currentSavePlayQueueTask.cancel();
+ }
+
+ currentSavePlayQueueTask = new SilentBackgroundTask<Void>(downloadService) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ try {
+ MusicService musicService = MusicServiceFactory.getMusicService(downloadService);
+ musicService.savePlayQueue(state.songs, state.songs.get(state.currentPlayingIndex), state.currentPlayingPosition, downloadService, null);
+ currentSavePlayQueueTask = null;
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to save playing queue to server", e);
+ currentSavePlayQueueTask = null;
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ currentSavePlayQueueTask = null;
+ super.error(error);
+ }
+ };
+ currentSavePlayQueueTask.execute();
+ }
+ }
+
+ public void post(Runnable runnable) {
+ eventHandler.post(runnable);
+ }
+
+ private void deserializeDownloadQueueNow() {
+ PlayerQueue state = FileUtil.deserialize(downloadService, FILENAME_DOWNLOADS_SER, PlayerQueue.class);
+ if (state == null) {
+ return;
+ }
+ Log.i(TAG, "Deserialized currentPlayingIndex: " + state.currentPlayingIndex + ", currentPlayingPosition: " + state.currentPlayingPosition);
+
+ // Rename first thing before anything else starts
+ if(state.renameCurrent && state.currentPlayingIndex != -1 && state.currentPlayingIndex < state.songs.size()) {
+ DownloadFile currentPlaying = new DownloadFile(downloadService, state.songs.get(state.currentPlayingIndex), false);
+ currentPlaying.renamePartial();
+ }
+
+ downloadService.restore(state.songs, state.toDelete, state.currentPlayingIndex, state.currentPlayingPosition);
+
+ if(state != null) {
+ lastChange = state.changed;
+ }
+ }
+
+ public Date getLastChange() {
+ return lastChange;
+ }
+
+ private void handleKeyEvent(KeyEvent event) {
+ if(event.getAction() == KeyEvent.ACTION_DOWN) {
+ switch (event.getKeyCode()) {
+ case RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE:
+ case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
+ downloadService.togglePlayPause();
+ break;
+ case KeyEvent.KEYCODE_HEADSETHOOK:
+ if(lastPressTime < (System.currentTimeMillis() - 500)) {
+ lastPressTime = System.currentTimeMillis();
+ downloadService.togglePlayPause();
+ } else {
+ downloadService.next(false, true);
+ }
+ break;
+ case RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS:
+ case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
+ downloadService.previous();
+ break;
+ case RemoteControlClient.FLAG_KEY_MEDIA_NEXT:
+ case KeyEvent.KEYCODE_MEDIA_NEXT:
+ downloadService.next();
+ break;
+ case RemoteControlClient.FLAG_KEY_MEDIA_STOP:
+ case KeyEvent.KEYCODE_MEDIA_STOP:
+ downloadService.stop();
+ break;
+ case RemoteControlClient.FLAG_KEY_MEDIA_PLAY:
+ case KeyEvent.KEYCODE_MEDIA_PLAY:
+ if(downloadService.getPlayerState() != PlayerState.STARTED) {
+ downloadService.start();
+ }
+ break;
+ case RemoteControlClient.FLAG_KEY_MEDIA_PAUSE:
+ case KeyEvent.KEYCODE_MEDIA_PAUSE:
+ downloadService.pause();
+ default:
+ break;
+ }
+ }
+ }
+
+ /**
+ * Logic taken from packages/apps/Music. Will pause when an incoming
+ * call rings or if a call (incoming or outgoing) is connected.
+ */
+ private class MyPhoneStateListener extends PhoneStateListener {
+ private boolean resumeAfterCall;
+
+ @Override
+ public void onCallStateChanged(final int state, String incomingNumber) {
+ eventHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ switch (state) {
+ case TelephonyManager.CALL_STATE_RINGING:
+ case TelephonyManager.CALL_STATE_OFFHOOK:
+ if (downloadService.getPlayerState() == PlayerState.STARTED) {
+ resumeAfterCall = true;
+ downloadService.pause(true);
+ }
+ break;
+ case TelephonyManager.CALL_STATE_IDLE:
+ if (resumeAfterCall) {
+ resumeAfterCall = false;
+ if(downloadService.getPlayerState() == PlayerState.PAUSED_TEMP) {
+ downloadService.start();
+ }
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ });
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/HeadphoneListenerService.java b/app/src/main/java/github/daneren2005/dsub/service/HeadphoneListenerService.java
new file mode 100644
index 00000000..f4375c50
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/HeadphoneListenerService.java
@@ -0,0 +1,66 @@
+/*
+ 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.service;
+
+import android.app.Service;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.IBinder;
+
+import github.daneren2005.dsub.receiver.HeadphonePlugReceiver;
+import github.daneren2005.dsub.util.Util;
+
+/**
+ * Created by Scott on 4/6/2015.
+ */
+public class HeadphoneListenerService extends Service {
+ private HeadphonePlugReceiver receiver;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ receiver = new HeadphonePlugReceiver();
+ registerReceiver(receiver, new IntentFilter(Intent.ACTION_HEADSET_PLUG));
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if(!Util.shouldStartOnHeadphones(this)) {
+ stopSelf();
+ }
+
+ return Service.START_STICKY;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+
+ try {
+ if(receiver != null) {
+ unregisterReceiver(receiver);
+ }
+ } catch(Exception e) {
+ // Don't care
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/JukeboxController.java b/app/src/main/java/github/daneren2005/dsub/service/JukeboxController.java
new file mode 100644
index 00000000..e9d7cbc8
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/JukeboxController.java
@@ -0,0 +1,307 @@
+/*
+ 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.service;
+
+import android.os.Handler;
+import android.util.Log;
+
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.RemoteStatus;
+import github.daneren2005.dsub.domain.PlayerState;
+import github.daneren2005.dsub.domain.RemoteControlState;
+import github.daneren2005.dsub.service.parser.SubsonicRESTException;
+import github.daneren2005.dsub.util.Util;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+public class JukeboxController extends RemoteController {
+ private static final String TAG = JukeboxController.class.getSimpleName();
+ private static final long STATUS_UPDATE_INTERVAL_SECONDS = 5L;
+
+ private final Handler handler;
+ private boolean running = false;
+ private final TaskQueue tasks = new TaskQueue();
+ private final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
+ private ScheduledFuture<?> statusUpdateFuture;
+ private final AtomicLong timeOfLastUpdate = new AtomicLong();
+ private RemoteStatus jukeboxStatus;
+ private float gain = 0.5f;
+
+ public JukeboxController(DownloadService downloadService, Handler handler) {
+ this.downloadService = downloadService;
+ this.handler = handler;
+ }
+
+ @Override
+ public void create(boolean playing, int seconds) {
+ new Thread("JukeboxController") {
+ @Override
+ public void run() {
+ running = true;
+ processTasks();
+ }
+ }.start();
+ updatePlaylist();
+ // Best I can do since API doesn't support seeking without starting playback
+ if(seconds != 0 && playing) {
+ changePosition(seconds);
+ }
+ }
+
+ @Override
+ public void start() {
+ tasks.remove(Stop.class);
+ tasks.remove(Start.class);
+
+ startStatusUpdate();
+ tasks.add(new Start());
+ }
+ @Override
+ public void stop() {
+ tasks.remove(Stop.class);
+ tasks.remove(Start.class);
+
+ stopStatusUpdate();
+ tasks.add(new Stop());
+ }
+ @Override
+ public void shutdown() {
+ running = false;
+ }
+
+ @Override
+ public void updatePlaylist() {
+ tasks.remove(Skip.class);
+ tasks.remove(Stop.class);
+ tasks.remove(Start.class);
+
+ List<String> ids = new ArrayList<String>();
+ for (DownloadFile file : downloadService.getDownloads()) {
+ ids.add(file.getSong().getId());
+ }
+ tasks.add(new SetPlaylist(ids));
+ }
+ @Override
+ public void changePosition(int seconds) {
+ tasks.remove(Skip.class);
+ tasks.remove(Stop.class);
+ tasks.remove(Start.class);
+
+ startStatusUpdate();
+ if (jukeboxStatus != null) {
+ jukeboxStatus.setPositionSeconds(seconds);
+ }
+ tasks.add(new Skip(downloadService.getCurrentPlayingIndex(), seconds));
+ downloadService.setPlayerState(PlayerState.STARTED);
+ }
+ @Override
+ public void changeTrack(int index, DownloadFile song) {
+ tasks.remove(Skip.class);
+ tasks.remove(Stop.class);
+ tasks.remove(Start.class);
+
+ startStatusUpdate();
+ tasks.add(new Skip(index, 0));
+ downloadService.setPlayerState(PlayerState.STARTED);
+ }
+ @Override
+ public void setVolume(int volume) {
+ gain = volume / 10.0f;
+
+ tasks.remove(SetGain.class);
+ tasks.add(new SetGain(gain));
+ }
+ @Override
+ public void updateVolume(boolean up) {
+ float delta = up ? 0.1f : -0.1f;
+ gain += delta;
+ gain = Math.max(gain, 0.0f);
+ gain = Math.min(gain, 1.0f);
+
+ tasks.remove(SetGain.class);
+ tasks.add(new SetGain(gain));
+ }
+ @Override
+ public double getVolume() {
+ return gain;
+ }
+
+ @Override
+ public int getRemotePosition() {
+ if (jukeboxStatus == null || jukeboxStatus.getPositionSeconds() == null || timeOfLastUpdate.get() == 0) {
+ return 0;
+ }
+
+ if (jukeboxStatus.isPlaying()) {
+ int secondsSinceLastUpdate = (int) ((System.currentTimeMillis() - timeOfLastUpdate.get()) / 1000L);
+ return jukeboxStatus.getPositionSeconds() + secondsSinceLastUpdate;
+ }
+
+ return jukeboxStatus.getPositionSeconds();
+ }
+
+ private void processTasks() {
+ while (running) {
+ RemoteTask task = null;
+ try {
+ task = tasks.take();
+ RemoteStatus status = task.execute();
+ if(status != null && running) {
+ onStatusUpdate(status);
+ }
+ } catch (Throwable x) {
+ onError(task, x);
+ }
+ }
+ }
+
+ private synchronized void startStatusUpdate() {
+ stopStatusUpdate();
+ Runnable updateTask = new Runnable() {
+ @Override
+ public void run() {
+ tasks.remove(GetStatus.class);
+ tasks.add(new GetStatus());
+ }
+ };
+ statusUpdateFuture = executorService.scheduleWithFixedDelay(updateTask, STATUS_UPDATE_INTERVAL_SECONDS,
+ STATUS_UPDATE_INTERVAL_SECONDS, TimeUnit.SECONDS);
+ }
+
+ private synchronized void stopStatusUpdate() {
+ if (statusUpdateFuture != null) {
+ statusUpdateFuture.cancel(false);
+ statusUpdateFuture = null;
+ }
+ }
+
+ private void onStatusUpdate(RemoteStatus jukeboxStatus) {
+ timeOfLastUpdate.set(System.currentTimeMillis());
+ this.jukeboxStatus = jukeboxStatus;
+
+ // Track change?
+ Integer index = jukeboxStatus.getCurrentPlayingIndex();
+ if (index != null && index != -1 && index != downloadService.getCurrentPlayingIndex()) {
+ downloadService.setPlayerState(PlayerState.COMPLETED);
+ downloadService.setCurrentPlaying(index, true);
+ if(jukeboxStatus.isPlaying()) {
+ downloadService.setPlayerState(PlayerState.STARTED);
+ }
+ }
+ }
+
+ private void onError(RemoteTask task, Throwable x) {
+ if (x instanceof ServerTooOldException && !(task instanceof Stop)) {
+ disableJukeboxOnError(x, R.string.download_jukebox_server_too_old);
+ } else if (x instanceof OfflineException && !(task instanceof Stop)) {
+ disableJukeboxOnError(x, R.string.download_jukebox_offline);
+ } else if (x instanceof SubsonicRESTException && ((SubsonicRESTException) x).getCode() == 50 && !(task instanceof Stop)) {
+ disableJukeboxOnError(x, R.string.download_jukebox_not_authorized);
+ } else {
+ Log.e(TAG, "Failed to process jukebox task: " + x, x);
+ }
+ }
+
+ private void disableJukeboxOnError(Throwable x, final int resourceId) {
+ Log.w(TAG, x.toString());
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ Util.toast(downloadService, resourceId, false);
+ downloadService.setRemoteEnabled(RemoteControlState.LOCAL);
+ }
+ });
+ }
+
+ private MusicService getMusicService() {
+ return MusicServiceFactory.getMusicService(downloadService);
+ }
+
+ private class GetStatus extends RemoteTask {
+ @Override
+ RemoteStatus execute() throws Exception {
+ return getMusicService().getJukeboxStatus(downloadService, null);
+ }
+ }
+
+ private class SetPlaylist extends RemoteTask {
+ private final List<String> ids;
+
+ SetPlaylist(List<String> ids) {
+ this.ids = ids;
+ }
+
+ @Override
+ RemoteStatus execute() throws Exception {
+ return getMusicService().updateJukeboxPlaylist(ids, downloadService, null);
+ }
+ }
+
+ private class Skip extends RemoteTask {
+ private final int index;
+ private final int offsetSeconds;
+
+ Skip(int index, int offsetSeconds) {
+ this.index = index;
+ this.offsetSeconds = offsetSeconds;
+ }
+
+ @Override
+ RemoteStatus execute() throws Exception {
+ return getMusicService().skipJukebox(index, offsetSeconds, downloadService, null);
+ }
+ }
+
+ private class Stop extends RemoteTask {
+ @Override
+ RemoteStatus execute() throws Exception {
+ return getMusicService().stopJukebox(downloadService, null);
+ }
+ }
+
+ private class Start extends RemoteTask {
+ @Override
+ RemoteStatus execute() throws Exception {
+ return getMusicService().startJukebox(downloadService, null);
+ }
+ }
+
+ private class SetGain extends RemoteTask {
+ private final float gain;
+
+ private SetGain(float gain) {
+ this.gain = gain;
+ }
+
+ @Override
+ RemoteStatus execute() throws Exception {
+ return getMusicService().setJukeboxGain(gain, downloadService, null);
+ }
+ }
+
+ private class ShutdownTask extends RemoteTask {
+ @Override
+ RemoteStatus execute() throws Exception {
+ return null;
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/MediaStoreService.java b/app/src/main/java/github/daneren2005/dsub/service/MediaStoreService.java
new file mode 100644
index 00000000..0aa3269f
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/MediaStoreService.java
@@ -0,0 +1,187 @@
+/*
+ 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.service;
+
+import java.io.File;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.MediaStore;
+import android.util.Log;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.Util;
+
+/**
+ * @author Sindre Mehus
+ */
+public class MediaStoreService {
+
+ private static final String TAG = MediaStoreService.class.getSimpleName();
+ private static final Uri ALBUM_ART_URI = Uri.parse("content://media/external/audio/albumart");
+
+ private final Context context;
+
+ public MediaStoreService(Context context) {
+ this.context = context;
+ }
+
+ public void saveInMediaStore(DownloadFile downloadFile) {
+ MusicDirectory.Entry song = downloadFile.getSong();
+ File songFile = downloadFile.getCompleteFile();
+
+ // Delete existing row in case the song has been downloaded before.
+ deleteFromMediaStore(downloadFile);
+
+ ContentResolver contentResolver = context.getContentResolver();
+ ContentValues values = new ContentValues();
+ if(!song.isVideo()) {
+ values.put(MediaStore.MediaColumns.TITLE, song.getTitle());
+ values.put(MediaStore.MediaColumns.DATA, songFile.getAbsolutePath());
+ values.put(MediaStore.Audio.AudioColumns.ARTIST, song.getArtist());
+ values.put(MediaStore.Audio.AudioColumns.ALBUM, song.getAlbum());
+ if (song.getDuration() != null) {
+ values.put(MediaStore.Audio.AudioColumns.DURATION, song.getDuration() * 1000L);
+ }
+ if (song.getTrack() != null) {
+ values.put(MediaStore.Audio.AudioColumns.TRACK, song.getTrack());
+ }
+ if (song.getYear() != null) {
+ values.put(MediaStore.Audio.AudioColumns.YEAR, song.getYear());
+ }
+ if(song.getTranscodedContentType() != null) {
+ values.put(MediaStore.MediaColumns.MIME_TYPE, song.getTranscodedContentType());
+ } else {
+ values.put(MediaStore.MediaColumns.MIME_TYPE, song.getContentType());
+ }
+ values.put(MediaStore.Audio.AudioColumns.IS_MUSIC, 1);
+
+ Uri uri = contentResolver.insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, values);
+
+ // Look up album, and add cover art if found.
+ Cursor cursor = contentResolver.query(uri, new String[]{MediaStore.Audio.AudioColumns.ALBUM_ID}, null, null, null);
+ if (cursor.moveToFirst()) {
+ int albumId = cursor.getInt(0);
+ insertAlbumArt(albumId, downloadFile);
+ }
+
+ cursor.close();
+ } else {
+ values.put(MediaStore.Video.VideoColumns.TITLE, song.getTitle());
+ values.put(MediaStore.Video.VideoColumns.DISPLAY_NAME, song.getTitle());
+ values.put(MediaStore.Video.VideoColumns.ARTIST, song.getArtist());
+ values.put(MediaStore.Video.VideoColumns.DATA, songFile.getAbsolutePath());
+ if (song.getDuration() != null) {
+ values.put(MediaStore.Video.VideoColumns.DURATION, song.getDuration() * 1000L);
+ }
+
+ String videoPlayerType = Util.getVideoPlayerType(context);
+ if("hls".equals(videoPlayerType)) {
+ // HLS should be able to transcode to mp4 automatically
+ values.put(MediaStore.MediaColumns.MIME_TYPE, "video/mpeg");
+ } else if("raw".equals(videoPlayerType) || song.getTranscodedContentType() == null) {
+ // Download the original video without any transcoding
+ values.put(MediaStore.MediaColumns.MIME_TYPE, song.getContentType());
+ } else {
+ values.put(MediaStore.MediaColumns.MIME_TYPE, song.getTranscodedContentType());
+ }
+
+ Uri uri = contentResolver.insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values);
+ if(uri == null) {
+ Log.e(TAG, "Failed to insert");
+ }
+ }
+ }
+
+ public void deleteFromMediaStore(DownloadFile downloadFile) {
+ ContentResolver contentResolver = context.getContentResolver();
+ MusicDirectory.Entry song = downloadFile.getSong();
+ File file = downloadFile.getCompleteFile();
+
+ Uri uri;
+ if(song.isVideo()) {
+ uri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
+ } else {
+ uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
+ }
+
+ int n = contentResolver.delete(uri,
+ MediaStore.MediaColumns.DATA + "=?",
+ new String[]{file.getAbsolutePath()});
+ if (n > 0) {
+ Log.i(TAG, "Deleting media store row for " + song);
+ }
+ }
+
+ public void deleteFromMediaStore(File file) {
+ ContentResolver contentResolver = context.getContentResolver();
+
+ Uri uri;
+ if(FileUtil.isVideoFile(file)) {
+ uri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
+ } else {
+ uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
+ }
+
+ int n = contentResolver.delete(uri,
+ MediaStore.MediaColumns.DATA + "=?",
+ new String[]{file.getAbsolutePath()});
+ if (n > 0) {
+ Log.i(TAG, "Deleting media store row for " + file);
+ }
+ }
+
+ public void renameInMediaStore(File start, File end) {
+ ContentResolver contentResolver = context.getContentResolver();
+
+ ContentValues values = new ContentValues();
+ values.put(MediaStore.MediaColumns.DATA, end.getAbsolutePath());
+
+ int n = contentResolver.update(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+ values,
+ MediaStore.MediaColumns.DATA + "=?",
+ new String[]{start.getAbsolutePath()});
+ if (n > 0) {
+ Log.i(TAG, "Rename media store row for " + start + " to " + end);
+ }
+ }
+
+ private void insertAlbumArt(int albumId, DownloadFile downloadFile) {
+ ContentResolver contentResolver = context.getContentResolver();
+
+ Cursor cursor = contentResolver.query(Uri.withAppendedPath(ALBUM_ART_URI, String.valueOf(albumId)), null, null, null, null);
+ if (!cursor.moveToFirst()) {
+
+ // No album art found, add it.
+ File albumArtFile = FileUtil.getAlbumArtFile(context, downloadFile.getSong());
+ if (albumArtFile.exists()) {
+ ContentValues values = new ContentValues();
+ values.put(MediaStore.Audio.AlbumColumns.ALBUM_ID, albumId);
+ values.put(MediaStore.MediaColumns.DATA, albumArtFile.getPath());
+ contentResolver.insert(ALBUM_ART_URI, values);
+ Log.i(TAG, "Added album art: " + albumArtFile);
+ }
+ }
+ cursor.close();
+ }
+
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/MusicService.java b/app/src/main/java/github/daneren2005/dsub/service/MusicService.java
new file mode 100644
index 00000000..4d014462
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/MusicService.java
@@ -0,0 +1,197 @@
+/*
+ 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.service;
+
+import java.util.List;
+
+import org.apache.http.HttpResponse;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+
+import github.daneren2005.dsub.domain.ArtistInfo;
+import github.daneren2005.dsub.domain.Bookmark;
+import github.daneren2005.dsub.domain.ChatMessage;
+import github.daneren2005.dsub.domain.Genre;
+import github.daneren2005.dsub.domain.Indexes;
+import github.daneren2005.dsub.domain.PlayerQueue;
+import github.daneren2005.dsub.domain.RemoteStatus;
+import github.daneren2005.dsub.domain.Lyrics;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.MusicFolder;
+import github.daneren2005.dsub.domain.Playlist;
+import github.daneren2005.dsub.domain.PodcastChannel;
+import github.daneren2005.dsub.domain.SearchCritera;
+import github.daneren2005.dsub.domain.SearchResult;
+import github.daneren2005.dsub.domain.Share;
+import github.daneren2005.dsub.domain.User;
+import github.daneren2005.dsub.domain.Version;
+import github.daneren2005.dsub.util.SilentBackgroundTask;
+import github.daneren2005.dsub.util.ProgressListener;
+
+/**
+ * @author Sindre Mehus
+ */
+public interface MusicService {
+
+ void ping(Context context, ProgressListener progressListener) throws Exception;
+
+ boolean isLicenseValid(Context context, ProgressListener progressListener) throws Exception;
+
+ List<MusicFolder> getMusicFolders(boolean refresh, Context context, ProgressListener progressListener) throws Exception;
+
+ void startRescan(Context context, ProgressListener listener) throws Exception;
+
+ Indexes getIndexes(String musicFolderId, boolean refresh, Context context, ProgressListener progressListener) throws Exception;
+
+ MusicDirectory getMusicDirectory(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception;
+
+ MusicDirectory getArtist(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception;
+
+ MusicDirectory getAlbum(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception;
+
+ SearchResult search(SearchCritera criteria, Context context, ProgressListener progressListener) throws Exception;
+
+ MusicDirectory getStarredList(Context context, ProgressListener progressListener) throws Exception;
+
+ MusicDirectory getPlaylist(boolean refresh, String id, String name, Context context, ProgressListener progressListener) throws Exception;
+
+ List<Playlist> getPlaylists(boolean refresh, Context context, ProgressListener progressListener) throws Exception;
+
+ void createPlaylist(String id, String name, List<MusicDirectory.Entry> entries, Context context, ProgressListener progressListener) throws Exception;
+
+ void deletePlaylist(String id, Context context, ProgressListener progressListener) throws Exception;
+
+ void addToPlaylist(String id, List<MusicDirectory.Entry> toAdd, Context context, ProgressListener progressListener) throws Exception;
+
+ void removeFromPlaylist(String id, List<Integer> toRemove, Context context, ProgressListener progressListener) throws Exception;
+
+ void overwritePlaylist(String id, String name, int toRemove, List<MusicDirectory.Entry> toAdd, Context context, ProgressListener progressListener) throws Exception;
+
+ void updatePlaylist(String id, String name, String comment, boolean pub, Context context, ProgressListener progressListener) throws Exception;
+
+ Lyrics getLyrics(String artist, String title, Context context, ProgressListener progressListener) throws Exception;
+
+ void scrobble(String id, boolean submission, Context context, ProgressListener progressListener) throws Exception;
+
+ MusicDirectory getAlbumList(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception;
+
+ MusicDirectory getAlbumList(String type, String extra, int size, int offset, Context context, ProgressListener progressListener) throws Exception;
+
+ MusicDirectory getRandomSongs(int size, String artistId, Context context, ProgressListener progressListener) throws Exception;
+ MusicDirectory getRandomSongs(int size, String folder, String genre, String startYear, String endYear, Context context, ProgressListener progressListener) throws Exception;
+
+ String getCoverArtUrl(Context context, MusicDirectory.Entry entry) throws Exception;
+
+ Bitmap getCoverArt(Context context, MusicDirectory.Entry entry, int size, ProgressListener progressListener, SilentBackgroundTask task) throws Exception;
+
+ HttpResponse getDownloadInputStream(Context context, MusicDirectory.Entry song, long offset, int maxBitrate, SilentBackgroundTask task) throws Exception;
+
+ String getMusicUrl(Context context, MusicDirectory.Entry song, int maxBitrate) throws Exception;
+
+ String getVideoUrl(int maxBitrate, Context context, String id);
+
+ String getVideoStreamUrl(String format, int Bitrate, Context context, String id) throws Exception;
+
+ String getHlsUrl(String id, int bitRate, Context context) throws Exception;
+
+ RemoteStatus updateJukeboxPlaylist(List<String> ids, Context context, ProgressListener progressListener) throws Exception;
+
+ RemoteStatus skipJukebox(int index, int offsetSeconds, Context context, ProgressListener progressListener) throws Exception;
+
+ RemoteStatus stopJukebox(Context context, ProgressListener progressListener) throws Exception;
+
+ RemoteStatus startJukebox(Context context, ProgressListener progressListener) throws Exception;
+
+ RemoteStatus getJukeboxStatus(Context context, ProgressListener progressListener) throws Exception;
+
+ RemoteStatus setJukeboxGain(float gain, Context context, ProgressListener progressListener) throws Exception;
+
+ void setStarred(List<MusicDirectory.Entry> entries, List<MusicDirectory.Entry> artists, List<MusicDirectory.Entry> albums, boolean starred, ProgressListener progressListener, Context context) throws Exception;
+
+ List<Share> getShares(Context context, ProgressListener progressListener) throws Exception;
+
+ List<Share> createShare(List<String> ids, String description, Long expires, Context context, ProgressListener progressListener) throws Exception;
+
+ void deleteShare(String id, Context context, ProgressListener progressListener) throws Exception;
+
+ void updateShare(String id, String description, Long expires, Context context, ProgressListener progressListener) throws Exception;
+
+ List<ChatMessage> getChatMessages(Long since, Context context, ProgressListener progressListener) throws Exception;
+
+ void addChatMessage(String message, Context context, ProgressListener progressListener) throws Exception;
+
+ List<Genre> getGenres(boolean refresh, Context context, ProgressListener progressListener) throws Exception;
+
+ MusicDirectory getSongsByGenre(String genre, int count, int offset, Context context, ProgressListener progressListener) throws Exception;
+
+ MusicDirectory getTopTrackSongs(String artist, int size, Context context, ProgressListener progressListener) throws Exception;
+
+ List<PodcastChannel> getPodcastChannels(boolean refresh, Context context, ProgressListener progressListener) throws Exception;
+
+ MusicDirectory getPodcastEpisodes(boolean refresh, String id, Context context, ProgressListener progressListener) throws Exception;
+
+ void refreshPodcasts(Context context, ProgressListener progressListener) throws Exception;
+
+ void createPodcastChannel(String url, Context context, ProgressListener progressListener) throws Exception;
+
+ void deletePodcastChannel(String id, Context context, ProgressListener progressListener) throws Exception;
+
+ void downloadPodcastEpisode(String id, Context context, ProgressListener progressListener) throws Exception;
+
+ void deletePodcastEpisode(String id, String parent, ProgressListener progressListener, Context context) throws Exception;
+
+ void setRating(MusicDirectory.Entry entry, int rating, Context context, ProgressListener progressListener) throws Exception;
+
+ MusicDirectory getBookmarks(boolean refresh, Context context, ProgressListener progressListener) throws Exception;
+
+ void createBookmark(MusicDirectory.Entry entry, int position, String comment, Context context, ProgressListener progressListener) throws Exception;
+
+ void deleteBookmark(MusicDirectory.Entry entry, Context context, ProgressListener progressListener) throws Exception;
+
+ User getUser(boolean refresh, String username, Context context, ProgressListener progressListener) throws Exception;
+
+ List<User> getUsers(boolean refresh, Context context, ProgressListener progressListener) throws Exception;
+
+ void createUser(User user, Context context, ProgressListener progressListener) throws Exception;
+
+ void updateUser(User user, Context context, ProgressListener progressListener) throws Exception;
+
+ void deleteUser(String username, Context context, ProgressListener progressListener) throws Exception;
+
+ void changeEmail(String username, String email, Context context, ProgressListener progressListener) throws Exception;
+
+ void changePassword(String username, String password, Context context, ProgressListener progressListener) throws Exception;
+
+ Bitmap getAvatar(String username, int size, Context context, ProgressListener progressListener, SilentBackgroundTask task) throws Exception;
+
+ ArtistInfo getArtistInfo(String id, boolean refresh, boolean allowNetwork, Context context, ProgressListener progressListener) throws Exception;
+
+ Bitmap getBitmap(String url, int size, Context context, ProgressListener progressListener, SilentBackgroundTask task) throws Exception;
+
+ MusicDirectory getVideos(boolean refresh, Context context, ProgressListener progressListener) throws Exception;
+
+ void savePlayQueue(List<MusicDirectory.Entry> songs, MusicDirectory.Entry currentPlaying, int position, Context context, ProgressListener progressListener) throws Exception;
+
+ PlayerQueue getPlayQueue(Context context, ProgressListener progressListener) throws Exception;
+
+ int processOfflineSyncs(final Context context, final ProgressListener progressListener) throws Exception;
+
+ void setInstance(Integer instance) throws Exception;
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/MusicServiceFactory.java b/app/src/main/java/github/daneren2005/dsub/service/MusicServiceFactory.java
new file mode 100644
index 00000000..e04522ff
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/MusicServiceFactory.java
@@ -0,0 +1,36 @@
+/*
+ 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.service;
+
+import android.content.Context;
+import github.daneren2005.dsub.util.Util;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class MusicServiceFactory {
+
+ private static final MusicService REST_MUSIC_SERVICE = new CachedMusicService(new RESTMusicService());
+ private static final MusicService OFFLINE_MUSIC_SERVICE = new OfflineMusicService();
+
+ public static MusicService getMusicService(Context context) {
+ return Util.isOffline(context) ? OFFLINE_MUSIC_SERVICE : REST_MUSIC_SERVICE;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/OfflineException.java b/app/src/main/java/github/daneren2005/dsub/service/OfflineException.java
new file mode 100644
index 00000000..e3a8d460
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/OfflineException.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 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.service;
+
+/**
+ * Thrown by service methods that are not available in offline mode.
+ *
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class OfflineException extends Exception {
+
+ public OfflineException(String message) {
+ super(message);
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/OfflineMusicService.java b/app/src/main/java/github/daneren2005/dsub/service/OfflineMusicService.java
new file mode 100644
index 00000000..b4105d07
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/OfflineMusicService.java
@@ -0,0 +1,836 @@
+/*
+ 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.service;
+
+import java.io.File;
+import java.io.Reader;
+import java.io.FileReader;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Random;
+import java.util.Set;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.graphics.Bitmap;
+import android.util.Log;
+
+import org.apache.http.HttpResponse;
+
+import github.daneren2005.dsub.domain.Artist;
+import github.daneren2005.dsub.domain.ArtistInfo;
+import github.daneren2005.dsub.domain.ChatMessage;
+import github.daneren2005.dsub.domain.Genre;
+import github.daneren2005.dsub.domain.Indexes;
+import github.daneren2005.dsub.domain.PlayerQueue;
+import github.daneren2005.dsub.domain.PodcastEpisode;
+import github.daneren2005.dsub.domain.RemoteStatus;
+import github.daneren2005.dsub.domain.Lyrics;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.MusicFolder;
+import github.daneren2005.dsub.domain.Playlist;
+import github.daneren2005.dsub.domain.PodcastChannel;
+import github.daneren2005.dsub.domain.SearchCritera;
+import github.daneren2005.dsub.domain.SearchResult;
+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.ProgressListener;
+import github.daneren2005.dsub.util.SilentBackgroundTask;
+import github.daneren2005.dsub.util.Util;
+import java.io.*;
+import java.util.Comparator;
+import java.util.SortedSet;
+
+/**
+ * @author Sindre Mehus
+ */
+public class OfflineMusicService implements MusicService {
+ private static final String TAG = OfflineMusicService.class.getSimpleName();
+ private static final String ERRORMSG = "Not available in offline mode";
+ private static final Random random = new Random();
+
+ @Override
+ public void ping(Context context, ProgressListener progressListener) throws Exception {
+
+ }
+
+ @Override
+ public boolean isLicenseValid(Context context, ProgressListener progressListener) throws Exception {
+ return true;
+ }
+
+ @Override
+ public Indexes getIndexes(String musicFolderId, boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ List<Artist> artists = new ArrayList<Artist>();
+ File root = FileUtil.getMusicDirectory(context);
+ for (File file : FileUtil.listFiles(root)) {
+ if (file.isDirectory()) {
+ Artist artist = new Artist();
+ artist.setId(file.getPath());
+ artist.setIndex(file.getName().substring(0, 1));
+ artist.setName(file.getName());
+ artists.add(artist);
+ }
+ }
+
+ Indexes indexes = new Indexes(0L, Collections.<Artist>emptyList(), artists);
+ indexes.sortChildren(context);
+ return indexes;
+ }
+
+ @Override
+ public MusicDirectory getMusicDirectory(String id, String artistName, boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ return getMusicDirectory(id, artistName, refresh, context, progressListener, false);
+ }
+ private MusicDirectory getMusicDirectory(String id, String artistName, boolean refresh, Context context, ProgressListener progressListener, boolean isPodcast) throws Exception {
+ File dir = new File(id);
+ MusicDirectory result = new MusicDirectory();
+ result.setName(dir.getName());
+
+ Set<String> names = new HashSet<String>();
+
+ for (File file : FileUtil.listMediaFiles(dir)) {
+ String name = getName(file);
+ if (name != null & !names.contains(name)) {
+ names.add(name);
+ result.addChild(createEntry(context, file, name, true, isPodcast));
+ }
+ }
+ result.sortChildren(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_CUSTOM_SORT_ENABLED, true));
+ return result;
+ }
+
+ @Override
+ public MusicDirectory getArtist(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public MusicDirectory getAlbum(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ private String getName(File file) {
+ String name = file.getName();
+ if (file.isDirectory()) {
+ return name;
+ }
+
+ if (name.endsWith(".partial") || name.contains(".partial.") || name.equals(Constants.ALBUM_ART_FILE)) {
+ return null;
+ }
+
+ name = name.replace(".complete", "");
+ return FileUtil.getBaseName(name);
+ }
+
+ private MusicDirectory.Entry createEntry(Context context, File file, String name) {
+ return createEntry(context, file, name, true);
+ }
+ private MusicDirectory.Entry createEntry(Context context, File file, String name, boolean load) {
+ return createEntry(context, file, name, load, false);
+ }
+ private MusicDirectory.Entry createEntry(Context context, File file, String name, boolean load, boolean isPodcast) {
+ MusicDirectory.Entry entry;
+ if(isPodcast) {
+ PodcastEpisode episode = new PodcastEpisode();
+ episode.setStatus("completed");
+ entry = episode;
+ } else {
+ entry = new MusicDirectory.Entry();
+ }
+ entry.setDirectory(file.isDirectory());
+ entry.setId(file.getPath());
+ entry.setParent(file.getParent());
+ entry.setSize(file.length());
+ String root = FileUtil.getMusicDirectory(context).getPath();
+ if(!file.getParentFile().getParentFile().getPath().equals(root)) {
+ entry.setGrandParent(file.getParentFile().getParent());
+ }
+ entry.setPath(file.getPath().replaceFirst("^" + root + "/" , ""));
+ String title = name;
+ if (file.isFile()) {
+ File artistFolder = file.getParentFile().getParentFile();
+ File albumFolder = file.getParentFile();
+ if(artistFolder.getPath().equals(root)) {
+ entry.setArtist(albumFolder.getName());
+ } else {
+ entry.setArtist(artistFolder.getName());
+ }
+ entry.setAlbum(albumFolder.getName());
+
+ int index = name.indexOf('-');
+ if(index != -1) {
+ try {
+ entry.setTrack(Integer.parseInt(name.substring(0, index)));
+ title = title.substring(index + 1);
+ } catch(Exception e) {
+ // Failed parseInt, just means track filled out
+ }
+ }
+
+ if(load) {
+ entry.loadMetadata(file);
+ }
+ }
+
+ entry.setTitle(title);
+ entry.setSuffix(FileUtil.getExtension(file.getName().replace(".complete", "")));
+
+ File albumArt = FileUtil.getAlbumArtFile(context, entry);
+ if (albumArt.exists()) {
+ entry.setCoverArt(albumArt.getPath());
+ }
+ if(FileUtil.isVideoFile(file)) {
+ entry.setVideo(true);
+ }
+ return entry;
+ }
+
+ @Override
+ public Bitmap getCoverArt(Context context, MusicDirectory.Entry entry, int size, ProgressListener progressListener, SilentBackgroundTask task) throws Exception {
+ try {
+ return FileUtil.getAlbumArtBitmap(context, entry, size);
+ } catch(Exception e) {
+ return null;
+ }
+ }
+
+ @Override
+ public HttpResponse getDownloadInputStream(Context context, MusicDirectory.Entry song, long offset, int maxBitrate, SilentBackgroundTask task) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public String getMusicUrl(Context context, MusicDirectory.Entry song, int maxBitrate) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public List<MusicFolder> getMusicFolders(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public void startRescan(Context context, ProgressListener listener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public SearchResult search(SearchCritera criteria, Context context, ProgressListener progressListener) throws Exception {
+ List<Artist> artists = new ArrayList<Artist>();
+ List<MusicDirectory.Entry> albums = new ArrayList<MusicDirectory.Entry>();
+ List<MusicDirectory.Entry> songs = new ArrayList<MusicDirectory.Entry>();
+ File root = FileUtil.getMusicDirectory(context);
+ int closeness = 0;
+ for (File artistFile : FileUtil.listFiles(root)) {
+ String artistName = artistFile.getName();
+ if (artistFile.isDirectory()) {
+ if((closeness = matchCriteria(criteria, artistName)) > 0) {
+ Artist artist = new Artist();
+ artist.setId(artistFile.getPath());
+ artist.setIndex(artistFile.getName().substring(0, 1));
+ artist.setName(artistName);
+ artist.setCloseness(closeness);
+ artists.add(artist);
+ }
+
+ recursiveAlbumSearch(artistName, artistFile, criteria, context, albums, songs);
+ }
+ }
+
+ Collections.sort(artists, new Comparator<Artist>() {
+ public int compare(Artist lhs, Artist rhs) {
+ if(lhs.getCloseness() == rhs.getCloseness()) {
+ return 0;
+ }
+ else if(lhs.getCloseness() > rhs.getCloseness()) {
+ return -1;
+ }
+ else {
+ return 1;
+ }
+ }
+ });
+ Collections.sort(albums, new Comparator<MusicDirectory.Entry>() {
+ public int compare(MusicDirectory.Entry lhs, MusicDirectory.Entry rhs) {
+ if(lhs.getCloseness() == rhs.getCloseness()) {
+ return 0;
+ }
+ else if(lhs.getCloseness() > rhs.getCloseness()) {
+ return -1;
+ }
+ else {
+ return 1;
+ }
+ }
+ });
+ Collections.sort(songs, new Comparator<MusicDirectory.Entry>() {
+ public int compare(MusicDirectory.Entry lhs, MusicDirectory.Entry rhs) {
+ if(lhs.getCloseness() == rhs.getCloseness()) {
+ return 0;
+ }
+ else if(lhs.getCloseness() > rhs.getCloseness()) {
+ return -1;
+ }
+ else {
+ return 1;
+ }
+ }
+ });
+
+ return new SearchResult(artists, albums, songs);
+ }
+
+ @Override
+ public MusicDirectory getStarredList(Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ private void recursiveAlbumSearch(String artistName, File file, SearchCritera criteria, Context context, List<MusicDirectory.Entry> albums, List<MusicDirectory.Entry> songs) {
+ int closeness;
+ for(File albumFile : FileUtil.listMediaFiles(file)) {
+ if(albumFile.isDirectory()) {
+ String albumName = getName(albumFile);
+ if((closeness = matchCriteria(criteria, albumName)) > 0) {
+ MusicDirectory.Entry album = createEntry(context, albumFile, albumName);
+ album.setArtist(artistName);
+ album.setCloseness(closeness);
+ albums.add(album);
+ }
+
+ for(File songFile : FileUtil.listMediaFiles(albumFile)) {
+ String songName = getName(songFile);
+ if(songName == null) {
+ continue;
+ }
+
+ if(songFile.isDirectory()) {
+ recursiveAlbumSearch(artistName, songFile, criteria, context, albums, songs);
+ }
+ else if((closeness = matchCriteria(criteria, songName)) > 0){
+ MusicDirectory.Entry song = createEntry(context, albumFile, songName);
+ song.setArtist(artistName);
+ song.setAlbum(albumName);
+ song.setCloseness(closeness);
+ songs.add(song);
+ }
+ }
+ }
+ else {
+ String songName = getName(albumFile);
+ if((closeness = matchCriteria(criteria, songName)) > 0) {
+ MusicDirectory.Entry song = createEntry(context, albumFile, songName);
+ song.setArtist(artistName);
+ song.setAlbum(songName);
+ song.setCloseness(closeness);
+ songs.add(song);
+ }
+ }
+ }
+ }
+ private int matchCriteria(SearchCritera criteria, String name) {
+ String query = criteria.getQuery().toLowerCase();
+ String[] queryParts = query.split(" ");
+ String[] nameParts = name.toLowerCase().split(" ");
+
+ int closeness = 0;
+ for(String queryPart : queryParts) {
+ for(String namePart : nameParts) {
+ if(namePart.equals(queryPart)) {
+ closeness++;
+ }
+ }
+ }
+
+ return closeness;
+ }
+
+ @Override
+ public List<Playlist> getPlaylists(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ List<Playlist> playlists = new ArrayList<Playlist>();
+ File root = FileUtil.getPlaylistDirectory(context);
+ String lastServer = null;
+ boolean removeServer = true;
+ for (File folder : FileUtil.listFiles(root)) {
+ if(folder.isDirectory()) {
+ String server = folder.getName();
+ SortedSet<File> fileList = FileUtil.listFiles(folder);
+ for(File file: fileList) {
+ if(FileUtil.isPlaylistFile(file)) {
+ String id = file.getName();
+ String filename = server + ": " + FileUtil.getBaseName(id);
+ Playlist playlist = new Playlist(server, filename);
+ playlists.add(playlist);
+ }
+ }
+
+ if(!server.equals(lastServer) && fileList.size() > 0) {
+ if(lastServer != null) {
+ removeServer = false;
+ }
+ lastServer = server;
+ }
+ } else {
+ // Delete legacy playlist files
+ try {
+ folder.delete();
+ } catch(Exception e) {
+ Log.w(TAG, "Failed to delete old playlist file: " + folder.getName());
+ }
+ }
+ }
+
+ if(removeServer) {
+ for(Playlist playlist: playlists) {
+ playlist.setName(playlist.getName().substring(playlist.getId().length() + 2));
+ }
+ }
+ return playlists;
+ }
+
+ @Override
+ public MusicDirectory getPlaylist(boolean refresh, String id, String name, Context context, ProgressListener progressListener) throws Exception {
+ DownloadService downloadService = DownloadService.getInstance();
+ if (downloadService == null) {
+ return new MusicDirectory();
+ }
+
+ Reader reader = null;
+ BufferedReader buffer = null;
+ try {
+ int firstIndex = name.indexOf(id);
+ if(firstIndex != -1) {
+ name = name.substring(id.length() + 2);
+ }
+
+ File playlistFile = FileUtil.getPlaylistFile(context, id, name);
+ reader = new FileReader(playlistFile);
+ buffer = new BufferedReader(reader);
+
+ MusicDirectory playlist = new MusicDirectory();
+ String line = buffer.readLine();
+ if(!"#EXTM3U".equals(line)) return playlist;
+
+ while( (line = buffer.readLine()) != null ){
+ // No matter what, end file can't have .complete in it
+ line = line.replace(".complete", "");
+ File entryFile = new File(line);
+
+ // Don't add file to playlist if it doesn't exist as cached or pinned!
+ File checkFile = entryFile;
+ if(!checkFile.exists()) {
+ // If normal file doens't exist, check if .complete version does
+ checkFile = new File(entryFile.getParent(), FileUtil.getBaseName(entryFile.getName())
+ + ".complete." + FileUtil.getExtension(entryFile.getName()));
+ }
+
+ String entryName = getName(entryFile);
+ if(checkFile.exists() && entryName != null){
+ playlist.addChild(createEntry(context, entryFile, entryName, false));
+ }
+ }
+
+ return playlist;
+ } finally {
+ Util.close(buffer);
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public void createPlaylist(String id, String name, List<MusicDirectory.Entry> entries, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public void deletePlaylist(String id, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public void addToPlaylist(String id, List<MusicDirectory.Entry> toAdd, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public void removeFromPlaylist(String id, List<Integer> toRemove, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public void overwritePlaylist(String id, String name, int toRemove, List<MusicDirectory.Entry> toAdd, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public void updatePlaylist(String id, String name, String comment, boolean pub, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public Lyrics getLyrics(String artist, String title, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public void scrobble(String id, boolean submission, Context context, ProgressListener progressListener) throws Exception {
+ if(!submission) {
+ return;
+ }
+
+ SharedPreferences prefs = Util.getPreferences(context);
+ String cacheLocn = prefs.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, null);
+
+ SharedPreferences offline = Util.getOfflineSync(context);
+ int scrobbles = offline.getInt(Constants.OFFLINE_SCROBBLE_COUNT, 0);
+ scrobbles++;
+ 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);
+ } else {
+ offlineEditor.putString(Constants.OFFLINE_SCROBBLE_ID + scrobbles, id);
+ offlineEditor.remove(Constants.OFFLINE_SCROBBLE_SEARCH + scrobbles);
+ }
+
+ offlineEditor.putLong(Constants.OFFLINE_SCROBBLE_TIME + scrobbles, System.currentTimeMillis());
+ offlineEditor.putInt(Constants.OFFLINE_SCROBBLE_COUNT, scrobbles);
+ offlineEditor.commit();
+ }
+
+ @Override
+ public MusicDirectory getAlbumList(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public MusicDirectory getAlbumList(String type, String extra, int size, int offset, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public MusicDirectory getRandomSongs(int size, String artistId, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public String getVideoUrl(int maxBitrate, Context context, String id) {
+ return null;
+ }
+
+ @Override
+ public String getVideoStreamUrl(String format, int maxBitrate, Context context, String id) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public String getHlsUrl(String id, int bitRate, Context context) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public RemoteStatus updateJukeboxPlaylist(List<String> ids, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public RemoteStatus skipJukebox(int index, int offsetSeconds, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public RemoteStatus stopJukebox(Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public RemoteStatus startJukebox(Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public RemoteStatus getJukeboxStatus(Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public RemoteStatus setJukeboxGain(float gain, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public void setStarred(List<MusicDirectory.Entry> entries, List<MusicDirectory.Entry> artists, List<MusicDirectory.Entry> albums, boolean starred, ProgressListener progressListener, Context context) throws Exception {
+ SharedPreferences prefs = Util.getPreferences(context);
+ String cacheLocn = prefs.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, null);
+
+ SharedPreferences offline = Util.getOfflineSync(context);
+ int stars = offline.getInt(Constants.OFFLINE_STAR_COUNT, 0);
+ stars++;
+ SharedPreferences.Editor offlineEditor = offline.edit();
+
+ String id = entries.get(0).getId();
+ if(id.indexOf(cacheLocn) != -1) {
+ String searchCriteria = Util.parseOfflineIDSearch(context, id, cacheLocn);
+ offlineEditor.putString(Constants.OFFLINE_STAR_SEARCH + stars, searchCriteria);
+ offlineEditor.remove(Constants.OFFLINE_STAR_ID + stars);
+ } else {
+ offlineEditor.putString(Constants.OFFLINE_STAR_ID + stars, id);
+ offlineEditor.remove(Constants.OFFLINE_STAR_SEARCH + stars);
+ }
+
+ offlineEditor.putBoolean(Constants.OFFLINE_STAR_SETTING + stars, starred);
+ offlineEditor.putInt(Constants.OFFLINE_STAR_COUNT, stars);
+ offlineEditor.commit();
+ }
+
+ @Override
+ public List<Share> getShares(Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public List<Share> createShare(List<String> ids, String description, Long expires, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public void deleteShare(String id, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public void updateShare(String id, String description, Long expires, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public List<ChatMessage> getChatMessages(Long since, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public void addChatMessage(String message, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public List<Genre> getGenres(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public MusicDirectory getSongsByGenre(String genre, int count, int offset, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public MusicDirectory getTopTrackSongs(String artist, int size, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public MusicDirectory getRandomSongs(int size, String folder, String genre, String startYear, String endYear, Context context, ProgressListener progressListener) throws Exception {
+ File root = FileUtil.getMusicDirectory(context);
+ List<File> children = new LinkedList<File>();
+ listFilesRecursively(root, children);
+ MusicDirectory result = new MusicDirectory();
+
+ if (children.isEmpty()) {
+ return result;
+ }
+ for (int i = 0; i < size; i++) {
+ File file = children.get(random.nextInt(children.size()));
+ result.addChild(createEntry(context, file, getName(file)));
+ }
+
+ return result;
+ }
+
+ @Override
+ public String getCoverArtUrl(Context context, MusicDirectory.Entry entry) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public List<PodcastChannel> getPodcastChannels(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ List<PodcastChannel> channels = new ArrayList<PodcastChannel>();
+
+ File dir = FileUtil.getPodcastDirectory(context);
+ String line;
+ for(File file: dir.listFiles()) {
+ BufferedReader br = new BufferedReader(new FileReader(file));
+ while ((line = br.readLine()) != null && !"".equals(line)) {
+ PodcastChannel channel = new PodcastChannel();
+ channel.setId(line);
+ channel.setName(line);
+ channel.setStatus("completed");
+
+ if(FileUtil.getPodcastDirectory(context, channel).exists() && !channels.contains(channel)) {
+ channels.add(channel);
+ }
+ }
+ br.close();
+ }
+
+ return channels;
+ }
+
+ @Override
+ public MusicDirectory getPodcastEpisodes(boolean refresh, String id, Context context, ProgressListener progressListener) throws Exception {
+ return getMusicDirectory(FileUtil.getPodcastDirectory(context, id).getPath(), null, false, context, progressListener, true);
+ }
+
+ @Override
+ public void refreshPodcasts(Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public void createPodcastChannel(String url, Context context, ProgressListener progressListener) throws Exception{
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public void deletePodcastChannel(String id, Context context, ProgressListener progressListener) throws Exception{
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public void downloadPodcastEpisode(String id, Context context, ProgressListener progressListener) throws Exception{
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public void deletePodcastEpisode(String id, String parent, ProgressListener progressListener, Context context) throws Exception{
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public void setRating(MusicDirectory.Entry entry, int rating, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public MusicDirectory getBookmarks(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public void createBookmark(MusicDirectory.Entry entry, int position, String comment, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public void deleteBookmark(MusicDirectory.Entry entry, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public User getUser(boolean refresh, String username, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public List<User> getUsers(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public void createUser(User user, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public void updateUser(User user, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public void deleteUser(String username, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public void changeEmail(String username, String email, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public void changePassword(String username, String password, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public Bitmap getAvatar(String username, int size, Context context, ProgressListener progressListener, SilentBackgroundTask task) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public ArtistInfo getArtistInfo(String id, boolean refresh, boolean allowNetwork, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public Bitmap getBitmap(String url, int size, Context context, ProgressListener progressListener, SilentBackgroundTask task) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public MusicDirectory getVideos(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public void savePlayQueue(List<MusicDirectory.Entry> songs, MusicDirectory.Entry currentPlaying, int position, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public PlayerQueue getPlayQueue(Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public int processOfflineSyncs(final Context context, final ProgressListener progressListener) throws Exception{
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
+ public void setInstance(Integer instance) throws Exception{
+ throw new OfflineException(ERRORMSG);
+ }
+
+ private void listFilesRecursively(File parent, List<File> children) {
+ for (File file : FileUtil.listMediaFiles(parent)) {
+ if (file.isFile()) {
+ children.add(file);
+ } else {
+ listFilesRecursively(file, children);
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/RESTMusicService.java b/app/src/main/java/github/daneren2005/dsub/service/RESTMusicService.java
new file mode 100644
index 00000000..459c8c9e
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/RESTMusicService.java
@@ -0,0 +1,1991 @@
+/*
+ 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.service;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpHost;
+import org.apache.http.HttpResponse;
+import org.apache.http.NameValuePair;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.conn.params.ConnManagerParams;
+import org.apache.http.conn.params.ConnPerRouteBean;
+import org.apache.http.conn.scheme.PlainSocketFactory;
+import org.apache.http.conn.scheme.Scheme;
+import org.apache.http.conn.scheme.SchemeRegistry;
+import org.apache.http.conn.scheme.SocketFactory;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
+import org.apache.http.message.BasicHeader;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.params.BasicHttpParams;
+import org.apache.http.params.HttpConnectionParams;
+import org.apache.http.params.HttpParams;
+import org.apache.http.protocol.BasicHttpContext;
+import org.apache.http.protocol.ExecutionContext;
+import org.apache.http.protocol.HttpContext;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.graphics.Bitmap;
+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;
+import github.daneren2005.dsub.service.parser.ArtistInfoParser;
+import github.daneren2005.dsub.service.parser.BookmarkParser;
+import github.daneren2005.dsub.service.parser.ChatMessageParser;
+import github.daneren2005.dsub.service.parser.ErrorParser;
+import github.daneren2005.dsub.service.parser.GenreParser;
+import github.daneren2005.dsub.service.parser.IndexesParser;
+import github.daneren2005.dsub.service.parser.JukeboxStatusParser;
+import github.daneren2005.dsub.service.parser.LicenseParser;
+import github.daneren2005.dsub.service.parser.LyricsParser;
+import github.daneren2005.dsub.service.parser.MusicDirectoryParser;
+import github.daneren2005.dsub.service.parser.MusicFoldersParser;
+import github.daneren2005.dsub.service.parser.PlayQueueParser;
+import github.daneren2005.dsub.service.parser.PlaylistParser;
+import github.daneren2005.dsub.service.parser.PlaylistsParser;
+import github.daneren2005.dsub.service.parser.PodcastChannelParser;
+import github.daneren2005.dsub.service.parser.PodcastEntryParser;
+import github.daneren2005.dsub.service.parser.RandomSongsParser;
+import github.daneren2005.dsub.service.parser.ScanStatusParser;
+import github.daneren2005.dsub.service.parser.SearchResult2Parser;
+import github.daneren2005.dsub.service.parser.SearchResultParser;
+import github.daneren2005.dsub.service.parser.ShareParser;
+import github.daneren2005.dsub.service.parser.StarredListParser;
+import github.daneren2005.dsub.service.parser.UserParser;
+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.SilentBackgroundTask;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.ProgressListener;
+import github.daneren2005.dsub.util.Util;
+import java.io.*;
+import java.util.zip.GZIPInputStream;
+
+/**
+ * @author Sindre Mehus
+ */
+public class RESTMusicService implements MusicService {
+
+ private static final String TAG = RESTMusicService.class.getSimpleName();
+
+ private static final int SOCKET_CONNECT_TIMEOUT = 10 * 1000;
+ private static final int SOCKET_READ_TIMEOUT_DEFAULT = 10 * 1000;
+ private static final int SOCKET_READ_TIMEOUT_DOWNLOAD = 30 * 1000;
+ private static final int SOCKET_READ_TIMEOUT_GET_RANDOM_SONGS = 60 * 1000;
+ private static final int SOCKET_READ_TIMEOUT_GET_PLAYLIST = 60 * 1000;
+
+ // Allow 20 seconds extra timeout per MB offset.
+ private static final double TIMEOUT_MILLIS_PER_OFFSET_BYTE = 20000.0 / 1000000.0;
+
+ private static final int HTTP_REQUEST_MAX_ATTEMPTS = 5;
+ private static final long REDIRECTION_CHECK_INTERVAL_MILLIS = 60L * 60L * 1000L;
+
+ private final DefaultHttpClient httpClient;
+ private long redirectionLastChecked;
+ private int redirectionNetworkType = -1;
+ private String redirectFrom;
+ private String redirectTo;
+ private final ThreadSafeClientConnManager connManager;
+ private Integer instance;
+
+ public RESTMusicService() {
+
+ // Create and initialize default HTTP parameters
+ HttpParams params = new BasicHttpParams();
+ ConnManagerParams.setMaxTotalConnections(params, 20);
+ ConnManagerParams.setMaxConnectionsPerRoute(params, new ConnPerRouteBean(20));
+ HttpConnectionParams.setConnectionTimeout(params, SOCKET_CONNECT_TIMEOUT);
+ HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_DEFAULT);
+
+ // Turn off stale checking. Our connections break all the time anyway,
+ // and it's not worth it to pay the penalty of checking every time.
+ HttpConnectionParams.setStaleCheckingEnabled(params, false);
+
+ // Create and initialize scheme registry
+ SchemeRegistry schemeRegistry = new SchemeRegistry();
+ schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
+ schemeRegistry.register(new Scheme("https", createSSLSocketFactory(), 443));
+
+ // Create an HttpClient with the ThreadSafeClientConnManager.
+ // This connection manager must be used if more than one thread will
+ // be using the HttpClient.
+ connManager = new ThreadSafeClientConnManager(params, schemeRegistry);
+ httpClient = new DefaultHttpClient(connManager, params);
+ }
+
+ private SocketFactory createSSLSocketFactory() {
+ try {
+ return new SSLSocketFactory(new TrustSelfSignedStrategy(), SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
+ } catch (Throwable x) {
+ Log.e(TAG, "Failed to create custom SSL socket factory, using default.", x);
+ return org.apache.http.conn.ssl.SSLSocketFactory.getSocketFactory();
+ }
+ }
+
+ @Override
+ public void ping(Context context, ProgressListener progressListener) throws Exception {
+ Reader reader = getReader(context, progressListener, "ping", null);
+ try {
+ new ErrorParser(context, getInstance(context)).parse(reader);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public boolean isLicenseValid(Context context, ProgressListener progressListener) throws Exception {
+
+ Reader reader = getReader(context, progressListener, "getLicense", null);
+ try {
+ ServerInfo serverInfo = new LicenseParser(context, getInstance(context)).parse(reader);
+ return serverInfo.isLicenseValid();
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ public List<MusicFolder> getMusicFolders(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ Reader reader = getReader(context, progressListener, "getMusicFolders", null);
+ try {
+ return new MusicFoldersParser(context, getInstance(context)).parse(reader, progressListener);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public void startRescan(Context context, ProgressListener listener) throws Exception {
+ Reader reader = getReader(context, listener, "startRescan", null);
+ try {
+ new ErrorParser(context, getInstance(context)).parse(reader);
+ } finally {
+ Util.close(reader);
+ }
+
+ // Now check if still running
+ boolean done = false;
+ while(!done) {
+ reader = getReader(context, null, "scanstatus", null);
+ try {
+ boolean running = new ScanStatusParser(context, getInstance(context)).parse(reader, listener);
+ if(running) {
+ // Don't run system ragged trying to query too much
+ Thread.sleep(100L);
+ } else {
+ done = true;
+ }
+ } catch(Exception e) {
+ done = true;
+ } finally {
+ Util.close(reader);
+ }
+ }
+ }
+
+ @Override
+ public Indexes getIndexes(String musicFolderId, boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ List<String> parameterNames = new ArrayList<String>();
+ List<Object> parameterValues = new ArrayList<Object>();
+
+ if (musicFolderId != null) {
+ parameterNames.add("musicFolderId");
+ parameterValues.add(musicFolderId);
+ }
+
+ Reader reader = getReader(context, progressListener, Util.isTagBrowsing(context, getInstance(context)) ? "getArtists" : "getIndexes", null, parameterNames, parameterValues);
+ try {
+ return new IndexesParser(context, getInstance(context)).parse(reader, progressListener);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public MusicDirectory getMusicDirectory(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ SharedPreferences prefs = Util.getPreferences(context);
+ String cacheLocn = prefs.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, null);
+ if(cacheLocn != null && id.indexOf(cacheLocn) != -1) {
+ String search = Util.parseOfflineIDSearch(context, id, cacheLocn);
+ SearchCritera critera = new SearchCritera(search, 1, 1, 0);
+ SearchResult result = searchNew(critera, context, progressListener);
+ if(result.getArtists().size() == 1) {
+ id = result.getArtists().get(0).getId();
+ } else if(result.getAlbums().size() == 1) {
+ id = result.getAlbums().get(0).getId();
+ }
+ }
+
+ MusicDirectory dir = null;
+ int index, start = 0;
+ while((index = id.indexOf(';', start)) != -1) {
+ MusicDirectory extra = getMusicDirectoryImpl(id.substring(start, index), name, refresh, context, progressListener);
+ if(dir == null) {
+ dir = extra;
+ } else {
+ dir.addChildren(extra.getChildren());
+ }
+
+ start = index + 1;
+ }
+ MusicDirectory extra = getMusicDirectoryImpl(id.substring(start), name, refresh, context, progressListener);
+ if(dir == null) {
+ dir = extra;
+ } else {
+ dir.addChildren(extra.getChildren());
+ }
+
+ // Apply another sort if we are chaining several together
+ if(dir != extra) {
+ dir.sortChildren(context, getInstance(context));
+ }
+
+ return dir;
+ }
+
+ private MusicDirectory getMusicDirectoryImpl(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ Reader reader = getReader(context, progressListener, "getMusicDirectory", null, "id", id);
+ try {
+ return new MusicDirectoryParser(context, getInstance(context)).parse(name, reader, progressListener);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public MusicDirectory getArtist(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ Reader reader = getReader(context, progressListener, "getArtist", null, "id", id);
+ try {
+ return new MusicDirectoryParser(context, getInstance(context)).parse(name, reader, progressListener);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public MusicDirectory getAlbum(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ Reader reader = getReader(context, progressListener, "getAlbum", null, "id", id);
+ try {
+ return new MusicDirectoryParser(context, getInstance(context)).parse(name, reader, progressListener);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public SearchResult search(SearchCritera critera, Context context, ProgressListener progressListener) throws Exception {
+ try {
+ return searchNew(critera, context, progressListener);
+ } catch (ServerTooOldException x) {
+ // Ensure backward compatibility with REST 1.3.
+ return searchOld(critera, context, progressListener);
+ }
+ }
+
+ /**
+ * Search using the "search" REST method.
+ */
+ private SearchResult searchOld(SearchCritera critera, Context context, ProgressListener progressListener) throws Exception {
+ List<String> parameterNames = Arrays.asList("any", "songCount");
+ List<Object> parameterValues = Arrays.<Object>asList(critera.getQuery(), critera.getSongCount());
+ Reader reader = getReader(context, progressListener, "search", null, parameterNames, parameterValues);
+ try {
+ return new SearchResultParser(context, getInstance(context)).parse(reader, progressListener);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ /**
+ * Search using the "search2" REST method, available in 1.4.0 and later.
+ */
+ private SearchResult searchNew(SearchCritera critera, Context context, ProgressListener progressListener) throws Exception {
+ checkServerVersion(context, "1.4", null);
+
+ List<String> parameterNames = Arrays.asList("query", "artistCount", "albumCount", "songCount");
+ List<Object> parameterValues = Arrays.<Object>asList(critera.getQuery(), critera.getArtistCount(),
+ critera.getAlbumCount(), critera.getSongCount());
+ Reader reader = getReader(context, progressListener, Util.isTagBrowsing(context, getInstance(context)) ? "search3" : "search2", null, parameterNames, parameterValues);
+ try {
+ return new SearchResult2Parser(context, getInstance(context)).parse(reader, progressListener);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public MusicDirectory getPlaylist(boolean refresh, String id, String name, Context context, ProgressListener progressListener) throws Exception {
+ HttpParams params = new BasicHttpParams();
+ HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_GET_PLAYLIST);
+
+ Reader reader = getReader(context, progressListener, "getPlaylist", params, "id", id);
+ try {
+ return new PlaylistParser(context, getInstance(context)).parse(reader, progressListener);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public List<Playlist> getPlaylists(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ Reader reader = getReader(context, progressListener, "getPlaylists", null);
+ try {
+ return new PlaylistsParser(context, getInstance(context)).parse(reader, progressListener);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public void createPlaylist(String id, String name, List<MusicDirectory.Entry> entries, Context context, ProgressListener progressListener) throws Exception {
+ List<String> parameterNames = new LinkedList<String>();
+ List<Object> parameterValues = new LinkedList<Object>();
+
+ if (id != null) {
+ parameterNames.add("playlistId");
+ parameterValues.add(id);
+ }
+ if (name != null) {
+ parameterNames.add("name");
+ parameterValues.add(name);
+ }
+ for (MusicDirectory.Entry entry : entries) {
+ parameterNames.add("songId");
+ parameterValues.add(getOfflineSongId(entry.getId(), context, progressListener));
+ }
+
+ Reader reader = getReader(context, progressListener, "createPlaylist", null, parameterNames, parameterValues);
+ try {
+ new ErrorParser(context, getInstance(context)).parse(reader);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public void deletePlaylist(String id, Context context, ProgressListener progressListener) throws Exception {
+ Reader reader = getReader(context, progressListener, "deletePlaylist", null, "id", id);
+ try {
+ new ErrorParser(context, getInstance(context)).parse(reader);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public void addToPlaylist(String id, List<MusicDirectory.Entry> toAdd, Context context, ProgressListener progressListener) throws Exception {
+ checkServerVersion(context, "1.8", "Updating playlists is not supported.");
+ List<String> names = new ArrayList<String>();
+ List<Object> values = new ArrayList<Object>();
+ names.add("playlistId");
+ values.add(id);
+ for(MusicDirectory.Entry song: toAdd) {
+ names.add("songIdToAdd");
+ values.add(getOfflineSongId(song.getId(), context, progressListener));
+ }
+ Reader reader = getReader(context, progressListener, "updatePlaylist", null, names, values);
+ try {
+ new ErrorParser(context, getInstance(context)).parse(reader);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public void removeFromPlaylist(String id, List<Integer> toRemove, Context context, ProgressListener progressListener) throws Exception {
+ checkServerVersion(context, "1.8", "Updating playlists is not supported.");
+ List<String> names = new ArrayList<String>();
+ List<Object> values = new ArrayList<Object>();
+ names.add("playlistId");
+ values.add(id);
+ for(Integer song: toRemove) {
+ names.add("songIndexToRemove");
+ values.add(song);
+ }
+ Reader reader = getReader(context, progressListener, "updatePlaylist", null, names, values);
+ try {
+ new ErrorParser(context, getInstance(context)).parse(reader);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public void overwritePlaylist(String id, String name, int toRemove, List<MusicDirectory.Entry> toAdd, Context context, ProgressListener progressListener) throws Exception {
+ checkServerVersion(context, "1.8", "Updating playlists is not supported.");
+ List<String> names = new ArrayList<String>();
+ List<Object> values = new ArrayList<Object>();
+ names.add("playlistId");
+ values.add(id);
+ names.add("name");
+ values.add(name);
+ for(MusicDirectory.Entry song: toAdd) {
+ names.add("songIdToAdd");
+ values.add(song.getId());
+ }
+ for(int i = 0; i < toRemove; i++) {
+ names.add("songIndexToRemove");
+ values.add(i);
+ }
+ Reader reader = getReader(context, progressListener, "updatePlaylist", null, names, values);
+ try {
+ new ErrorParser(context, getInstance(context)).parse(reader);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public void updatePlaylist(String id, String name, String comment, boolean pub, Context context, ProgressListener progressListener) throws Exception {
+ checkServerVersion(context, "1.8", "Updating playlists is not supported.");
+ Reader reader = getReader(context, progressListener, "updatePlaylist", null, Arrays.asList("playlistId", "name", "comment", "public"), Arrays.<Object>asList(id, name, comment, pub));
+ try {
+ new ErrorParser(context, getInstance(context)).parse(reader);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public Lyrics getLyrics(String artist, String title, Context context, ProgressListener progressListener) throws Exception {
+ Reader reader = getReader(context, progressListener, "getLyrics", null, Arrays.asList("artist", "title"), Arrays.<Object>asList(artist, title));
+ try {
+ return new LyricsParser(context, getInstance(context)).parse(reader, progressListener);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public void scrobble(String id, boolean submission, Context context, ProgressListener progressListener) throws Exception {
+ id = getOfflineSongId(id, context, progressListener);
+ scrobble(id, submission, 0, context, progressListener);
+ }
+
+ public void scrobble(String id, boolean submission, long time, Context context, ProgressListener progressListener) throws Exception {
+ checkServerVersion(context, "1.5", "Scrobbling not supported.");
+ Reader reader;
+ if(time > 0){
+ checkServerVersion(context, "1.8", "Scrobbling with a time not supported.");
+ reader = getReader(context, progressListener, "scrobble", null, Arrays.asList("id", "submission", "time"), Arrays.<Object>asList(id, submission, time));
+ }
+ else
+ reader = getReader(context, progressListener, "scrobble", null, Arrays.asList("id", "submission"), Arrays.<Object>asList(id, submission));
+ try {
+ new ErrorParser(context, getInstance(context)).parse(reader);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public MusicDirectory getAlbumList(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception {
+ List<String> names = new ArrayList<String>();
+ List<Object> values = new ArrayList<Object>();
+
+ names.add("type");
+ values.add(type);
+ names.add("size");
+ values.add(size);
+ names.add("offset");
+ values.add(offset);
+
+ // Add folder if it was set and is non null
+ int instance = getInstance(context);
+ if(Util.getAlbumListsPerFolder(context, instance)) {
+ String folderId = Util.getSelectedMusicFolderId(context, instance);
+ if(folderId != null) {
+ names.add("musicFolderId");
+ values.add(folderId);
+ }
+ }
+
+ Reader reader = getReader(context, progressListener, Util.isTagBrowsing(context, getInstance(context)) ? "getAlbumList2" : "getAlbumList",
+ null, names, values, true);
+ try {
+ return new AlbumListParser(context, getInstance(context)).parse(reader, progressListener);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public MusicDirectory getAlbumList(String type, String extra, int size, int offset, Context context, ProgressListener progressListener) throws Exception {
+ checkServerVersion(context, "1.10.1", "This type of album list is not supported");
+
+ List<String> names = new ArrayList<String>();
+ List<Object> values = new ArrayList<Object>();
+
+ names.add("size");
+ names.add("offset");
+
+ values.add(size);
+ values.add(offset);
+
+ if("genres".equals(type)) {
+ names.add("type");
+ values.add("byGenre");
+
+ names.add("genre");
+ values.add(extra);
+ } else if("years".equals(type)) {
+ names.add("type");
+ values.add("byYear");
+
+ names.add("fromYear");
+ names.add("toYear");
+
+ int decade = Integer.parseInt(extra);
+ values.add(decade);
+ values.add(decade + 10);
+ }
+
+ // Add folder if it was set and is non null
+ int instance = getInstance(context);
+ if(Util.getAlbumListsPerFolder(context, instance)) {
+ String folderId = Util.getSelectedMusicFolderId(context, instance);
+ if(folderId != null) {
+ names.add("musicFolderId");
+ values.add(folderId);
+ }
+ }
+
+ Reader reader = getReader(context, progressListener, Util.isTagBrowsing(context, instance) ? "getAlbumList2" : "getAlbumList", null, names, values, true);
+ try {
+ return new AlbumListParser(context, instance).parse(reader, progressListener);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public MusicDirectory getRandomSongs(int size, String artistId, Context context, ProgressListener progressListener) throws Exception {
+ checkServerVersion(context, "1.11", "Artist radio is not supported");
+
+ List<String> names = new ArrayList<String>();
+ List<Object> values = new ArrayList<Object>();
+
+ names.add("id");
+ names.add("count");
+
+ values.add(artistId);
+ values.add(size);
+
+ int instance = getInstance(context);
+ String method;
+ if(ServerInfo.isMadsonic(context, instance)) {
+ method = "getPandoraSongs";
+ } else {
+ if (Util.isTagBrowsing(context, instance)) {
+ method = "getSimilarSongs2";
+ } else {
+ method = "getSimilarSongs";
+ }
+ }
+
+ Reader reader = getReader(context, progressListener, method, null, names, values);
+ try {
+ return new RandomSongsParser(context, instance).parse(reader, progressListener);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public MusicDirectory getStarredList(Context context, ProgressListener progressListener) throws Exception {
+ List<String> names = new ArrayList<String>();
+ List<Object> values = new ArrayList<Object>();
+
+ // Add folder if it was set and is non null
+ int instance = getInstance(context);
+ if(Util.getAlbumListsPerFolder(context, instance)) {
+ String folderId = Util.getSelectedMusicFolderId(context, instance);
+ if(folderId != null) {
+ names.add("musicFolderId");
+ values.add(folderId);
+ }
+ }
+
+ Reader reader = getReader(context, progressListener, Util.isTagBrowsing(context, instance) ? "getStarred2" : "getStarred", null, names, values, true);
+ try {
+ return new StarredListParser(context, instance).parse(reader, progressListener);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public MusicDirectory getRandomSongs(int size, String musicFolderId, String genre, String startYear, String endYear, Context context, ProgressListener progressListener) throws Exception {
+ HttpParams params = new BasicHttpParams();
+ HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_GET_RANDOM_SONGS);
+
+ List<String> names = new ArrayList<String>();
+ List<Object> values = new ArrayList<Object>();
+
+ names.add("size");
+ values.add(size);
+
+ if (musicFolderId != null && !"".equals(musicFolderId) && !Util.isTagBrowsing(context, getInstance(context))) {
+ names.add("musicFolderId");
+ values.add(musicFolderId);
+ }
+ if(genre != null && !"".equals(genre)) {
+ names.add("genre");
+ values.add(genre);
+ }
+ if(startYear != null && !"".equals(startYear)) {
+ names.add("fromYear");
+ values.add(startYear);
+ }
+ if(endYear != null && !"".equals(endYear)) {
+ names.add("toYear");
+ values.add(endYear);
+ }
+
+ Reader reader = getReader(context, progressListener, "getRandomSongs", params, names, values);
+ try {
+ return new RandomSongsParser(context, getInstance(context)).parse(reader, progressListener);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ private void checkServerVersion(Context context, String version, String text) throws ServerTooOldException {
+ Version serverVersion = ServerInfo.getServerVersion(context);
+ Version requiredVersion = new Version(version);
+ boolean ok = serverVersion == null || serverVersion.compareTo(requiredVersion) >= 0;
+
+ if (!ok) {
+ throw new ServerTooOldException(text, serverVersion, requiredVersion);
+ }
+ }
+
+ @Override
+ public String getCoverArtUrl(Context context, MusicDirectory.Entry entry) throws Exception {
+ StringBuilder builder = new StringBuilder(getRestUrl(context, "getCoverArt"));
+ builder.append("&id=").append(entry.getCoverArt());
+ String url = builder.toString();
+ url = Util.replaceInternalUrl(context, url);
+ url = rewriteUrlWithRedirect(context, url);
+ return url;
+ }
+
+ @Override
+ public Bitmap getCoverArt(Context context, MusicDirectory.Entry entry, int size, ProgressListener progressListener, SilentBackgroundTask task) throws Exception {
+
+ // Synchronize on the entry so that we don't download concurrently for the same song.
+ synchronized (entry) {
+
+ // Use cached file, if existing.
+ Bitmap bitmap = FileUtil.getAlbumArtBitmap(context, entry, size);
+ if (bitmap != null) {
+ return bitmap;
+ }
+
+ String url = getRestUrl(context, "getCoverArt");
+
+ InputStream in = null;
+ try {
+ List<String> parameterNames = Arrays.asList("id");
+ List<Object> parameterValues = Arrays.<Object>asList(entry.getCoverArt());
+ HttpEntity entity = getEntityForURL(context, url, null, parameterNames, parameterValues, progressListener, task);
+
+ in = entity.getContent();
+ Header contentEncoding = entity.getContentEncoding();
+ if (contentEncoding != null && contentEncoding.getValue().equalsIgnoreCase("gzip")) {
+ in = new GZIPInputStream(in);
+ }
+
+ // If content type is XML, an error occured. Get it.
+ String contentType = Util.getContentType(entity);
+ if (contentType != null && (contentType.startsWith("text/xml") || contentType.startsWith("text/html"))) {
+ new ErrorParser(context, getInstance(context)).parse(new InputStreamReader(in, Constants.UTF_8));
+ return null; // Never reached.
+ }
+
+ byte[] bytes = Util.toByteArray(in);
+
+ // Handle case where partial was downloaded before being cancelled
+ if(task != null && task.isCancelled()) {
+ return null;
+ }
+
+ OutputStream out = null;
+ try {
+ out = new FileOutputStream(FileUtil.getAlbumArtFile(context, entry));
+ out.write(bytes);
+ } finally {
+ Util.close(out);
+ }
+
+ // Size == 0 -> only want to download
+ if(size == 0) {
+ return null;
+ } else {
+ return FileUtil.getSampledBitmap(bytes, size);
+ }
+ } finally {
+ Util.close(in);
+ }
+ }
+ }
+
+ @Override
+ public HttpResponse getDownloadInputStream(Context context, MusicDirectory.Entry song, long offset, int maxBitrate, SilentBackgroundTask task) throws Exception {
+
+ String url = getRestUrl(context, "stream");
+
+ // Set socket read timeout. Note: The timeout increases as the offset gets larger. This is
+ // to avoid the thrashing effect seen when offset is combined with transcoding/downsampling on the server.
+ // In that case, the server uses a long time before sending any data, causing the client to time out.
+ HttpParams params = new BasicHttpParams();
+ int timeout = (int) (SOCKET_READ_TIMEOUT_DOWNLOAD + offset * TIMEOUT_MILLIS_PER_OFFSET_BYTE);
+ HttpConnectionParams.setSoTimeout(params, timeout);
+
+ // Add "Range" header if offset is given.
+ List<Header> headers = new ArrayList<Header>();
+ if (offset > 0) {
+ headers.add(new BasicHeader("Range", "bytes=" + offset + "-"));
+ }
+
+ List<String> parameterNames = new ArrayList<String>();
+ parameterNames.add("id");
+ parameterNames.add("maxBitRate");
+
+ List<Object> parameterValues = new ArrayList<Object>();
+ parameterValues.add(song.getId());
+ parameterValues.add(maxBitrate);
+
+ // If video specify what format to download
+ if(song.isVideo()) {
+ String videoPlayerType = Util.getVideoPlayerType(context);
+ if("hls".equals(videoPlayerType)) {
+ // HLS should be able to transcode to mp4 automatically
+ parameterNames.add("format");
+ parameterValues.add("mp4");
+
+ parameterNames.add("hls");
+ parameterValues.add("true");
+ } else if("raw".equals(videoPlayerType)) {
+ // Download the original video without any transcoding
+ parameterNames.add("format");
+ parameterValues.add("raw");
+ }
+ }
+ HttpResponse response = getResponseForURL(context, url, params, parameterNames, parameterValues, headers, null, task, false);
+
+ // If content type is XML, an error occurred. Get it.
+ String contentType = Util.getContentType(response.getEntity());
+ if (contentType != null && (contentType.startsWith("text/xml") || contentType.startsWith("text/html"))) {
+ InputStream in = response.getEntity().getContent();
+ Header contentEncoding = response.getEntity().getContentEncoding();
+ if (contentEncoding != null && contentEncoding.getValue().equalsIgnoreCase("gzip")) {
+ in = new GZIPInputStream(in);
+ }
+ try {
+ new ErrorParser(context, getInstance(context)).parse(new InputStreamReader(in, Constants.UTF_8));
+ } finally {
+ Util.close(in);
+ }
+ }
+
+ return response;
+ }
+
+ @Override
+ public String getMusicUrl(Context context, MusicDirectory.Entry song, int maxBitrate) throws Exception {
+ StringBuilder builder = new StringBuilder(getRestUrl(context, "stream"));
+ builder.append("&id=").append(song.getId());
+
+ // If we are doing mp3 to mp3, just specify raw so that stuff works better
+ if("mp3".equals(song.getSuffix()) && (song.getTranscodedSuffix() == null || "mp3".equals(song.getTranscodedSuffix())) && ServerInfo.checkServerVersion(context, "1.9", getInstance(context))) {
+ builder.append("&format=raw");
+ builder.append("&estimateContentLength=true");
+ } else {
+ builder.append("&maxBitRate=").append(maxBitrate);
+ }
+
+ String url = builder.toString();
+ url = Util.replaceInternalUrl(context, url);
+ url = rewriteUrlWithRedirect(context, url);
+ Log.i(TAG, "Using music URL: " + stripUrlInfo(url));
+ return url;
+ }
+
+ @Override
+ public String getVideoUrl(int maxBitrate, Context context, String id) {
+ StringBuilder builder = new StringBuilder(getRestUrl(context, "videoPlayer"));
+ builder.append("&id=").append(id);
+ builder.append("&maxBitRate=").append(maxBitrate);
+ builder.append("&autoplay=true");
+
+ String url = rewriteUrlWithRedirect(context, builder.toString());
+ Log.i(TAG, "Using video URL: " + stripUrlInfo(url));
+ return url;
+ }
+
+ @Override
+ public String getVideoStreamUrl(String format, int maxBitrate, Context context, String id) throws Exception {
+ StringBuilder builder = new StringBuilder(getRestUrl(context, "stream"));
+ builder.append("&id=").append(id);
+ if(!"raw".equals(format)) {
+ checkServerVersion(context, "1.9", "Video streaming not supported.");
+ builder.append("&maxBitRate=").append(maxBitrate);
+ }
+ builder.append("&format=").append(format);
+
+ String url = rewriteUrlWithRedirect(context, builder.toString());
+ Log.i(TAG, "Using video URL: " + stripUrlInfo(url));
+ return url;
+ }
+
+ @Override
+ public String getHlsUrl(String id, int bitRate, Context context) throws Exception {
+ checkServerVersion(context, "1.9", "HLS video streaming not supported.");
+
+ StringBuilder builder = new StringBuilder(getRestUrl(context, "hls"));
+ builder.append("&id=").append(id);
+ if(bitRate > 0) {
+ builder.append("&bitRate=").append(bitRate);
+ }
+
+ String url = rewriteUrlWithRedirect(context, builder.toString());
+ Log.i(TAG, "Using hls URL: " + stripUrlInfo(url));
+ return url;
+ }
+
+ @Override
+ public RemoteStatus updateJukeboxPlaylist(List<String> ids, Context context, ProgressListener progressListener) throws Exception {
+ int n = ids.size();
+ List<String> parameterNames = new ArrayList<String>(n + 1);
+ parameterNames.add("action");
+ for (int i = 0; i < n; i++) {
+ parameterNames.add("id");
+ }
+ List<Object> parameterValues = new ArrayList<Object>();
+ parameterValues.add("set");
+ parameterValues.addAll(ids);
+
+ return executeJukeboxCommand(context, progressListener, parameterNames, parameterValues);
+ }
+
+ @Override
+ public RemoteStatus skipJukebox(int index, int offsetSeconds, Context context, ProgressListener progressListener) throws Exception {
+ List<String> parameterNames = Arrays.asList("action", "index", "offset");
+ List<Object> parameterValues = Arrays.<Object>asList("skip", index, offsetSeconds);
+ return executeJukeboxCommand(context, progressListener, parameterNames, parameterValues);
+ }
+
+ @Override
+ public RemoteStatus stopJukebox(Context context, ProgressListener progressListener) throws Exception {
+ return executeJukeboxCommand(context, progressListener, Arrays.asList("action"), Arrays.<Object>asList("stop"));
+ }
+
+ @Override
+ public RemoteStatus startJukebox(Context context, ProgressListener progressListener) throws Exception {
+ return executeJukeboxCommand(context, progressListener, Arrays.asList("action"), Arrays.<Object>asList("start"));
+ }
+
+ @Override
+ public RemoteStatus getJukeboxStatus(Context context, ProgressListener progressListener) throws Exception {
+ return executeJukeboxCommand(context, progressListener, Arrays.asList("action"), Arrays.<Object>asList("status"));
+ }
+
+ @Override
+ public RemoteStatus setJukeboxGain(float gain, Context context, ProgressListener progressListener) throws Exception {
+ List<String> parameterNames = Arrays.asList("action", "gain");
+ List<Object> parameterValues = Arrays.<Object>asList("setGain", gain);
+ return executeJukeboxCommand(context, progressListener, parameterNames, parameterValues);
+
+ }
+
+ private RemoteStatus executeJukeboxCommand(Context context, ProgressListener progressListener, List<String> parameterNames, List<Object> parameterValues) throws Exception {
+ checkServerVersion(context, "1.7", "Jukebox not supported.");
+ Reader reader = getReader(context, progressListener, "jukeboxControl", null, parameterNames, parameterValues);
+ try {
+ return new JukeboxStatusParser(context, getInstance(context)).parse(reader);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public void setStarred(List<MusicDirectory.Entry> entries, List<MusicDirectory.Entry> artists, List<MusicDirectory.Entry> albums, boolean starred, ProgressListener progressListener, Context context) throws Exception {
+ checkServerVersion(context, "1.8", "Starring is not supported.");
+
+ List<String> names = new ArrayList<String>();
+ List<Object> values = new ArrayList<Object>();
+
+ if(entries != null && entries.size() > 0) {
+ if(entries.size() > 1) {
+ for (MusicDirectory.Entry entry : entries) {
+ names.add("id");
+ values.add(entry.getId());
+ }
+ } else {
+ names.add("id");
+ values.add(getOfflineSongId(entries.get(0).getId(), context, progressListener));
+ }
+ }
+ if(artists != null && artists.size() > 0) {
+ for (MusicDirectory.Entry artist : artists) {
+ names.add("artistId");
+ values.add(artist.getId());
+ }
+ }
+ if(albums != null && albums.size() > 0) {
+ for (MusicDirectory.Entry album : albums) {
+ names.add("albumId");
+ values.add(album.getId());
+ }
+ }
+
+ Reader reader = getReader(context, progressListener, starred ? "star" : "unstar", null, names, values);
+ try {
+ new ErrorParser(context, getInstance(context)).parse(reader);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public List<Share> getShares(Context context, ProgressListener progressListener) throws Exception {
+ checkServerVersion(context, "1.6", "Shares not supported.");
+
+ Reader reader = getReader(context, progressListener, "getShares", null);
+ try {
+ return new ShareParser(context, getInstance(context)).parse(reader, progressListener);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public List<Share> createShare(List<String> ids, String description, Long expires, Context context, ProgressListener progressListener) throws Exception {
+ List<String> parameterNames = new LinkedList<String>();
+ List<Object> parameterValues = new LinkedList<Object>();
+
+ for (String id : ids) {
+ parameterNames.add("id");
+ parameterValues.add(id);
+ }
+
+ if (description != null) {
+ parameterNames.add("description");
+ parameterValues.add(description);
+ }
+
+ if (expires > 0) {
+ parameterNames.add("expires");
+ parameterValues.add(expires);
+ }
+
+ Reader reader = getReader(context, progressListener, "createShare", null, parameterNames, parameterValues);
+ try {
+ return new ShareParser(context, getInstance(context)).parse(reader, progressListener);
+ }
+ finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public void deleteShare(String id, Context context, ProgressListener progressListener) throws Exception {
+ checkServerVersion(context, "1.6", "Shares not supported.");
+
+ HttpParams params = new BasicHttpParams();
+ HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_GET_RANDOM_SONGS);
+
+ List<String> parameterNames = new ArrayList<String>();
+ List<Object> parameterValues = new ArrayList<Object>();
+
+ parameterNames.add("id");
+ parameterValues.add(id);
+
+ Reader reader = getReader(context, progressListener, "deleteShare", params, parameterNames, parameterValues);
+
+ try {
+ new ErrorParser(context, getInstance(context)).parse(reader);
+ }
+ finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public void updateShare(String id, String description, Long expires, Context context, ProgressListener progressListener) throws Exception {
+ checkServerVersion(context, "1.6", "Updating share not supported.");
+
+ HttpParams params = new BasicHttpParams();
+ HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_GET_RANDOM_SONGS);
+
+ List<String> parameterNames = new ArrayList<String>();
+ List<Object> parameterValues = new ArrayList<Object>();
+
+ parameterNames.add("id");
+ parameterValues.add(id);
+
+ if (description != null) {
+ parameterNames.add("description");
+ parameterValues.add(description);
+ }
+
+ parameterNames.add("expires");
+ parameterValues.add(expires);
+
+ Reader reader = getReader(context, progressListener, "updateShare", params, parameterNames, parameterValues);
+ try {
+ new ErrorParser(context, getInstance(context)).parse(reader);
+ }
+ finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public List<ChatMessage> getChatMessages(Long since, Context context, ProgressListener progressListener) throws Exception {
+ checkServerVersion(context, "1.2", "Chat not supported.");
+
+ HttpParams params = new BasicHttpParams();
+ HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_GET_RANDOM_SONGS);
+
+ List<String> parameterNames = new ArrayList<String>();
+ List<Object> parameterValues = new ArrayList<Object>();
+
+ parameterNames.add("since");
+ parameterValues.add(since);
+
+ Reader reader = getReader(context, progressListener, "getChatMessages", params, parameterNames, parameterValues);
+
+ try {
+ return new ChatMessageParser(context, getInstance(context)).parse(reader, progressListener);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public void addChatMessage(String message, Context context, ProgressListener progressListener) throws Exception {
+ checkServerVersion(context, "1.2", "Chat not supported.");
+
+ HttpParams params = new BasicHttpParams();
+ HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_GET_RANDOM_SONGS);
+
+ List<String> parameterNames = new ArrayList<String>();
+ List<Object> parameterValues = new ArrayList<Object>();
+
+ parameterNames.add("message");
+ parameterValues.add(message);
+
+ Reader reader = getReader(context, progressListener, "addChatMessage", params, parameterNames, parameterValues);
+
+ try {
+ new ErrorParser(context, getInstance(context)).parse(reader);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public List<Genre> getGenres(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ checkServerVersion(context, "1.9", "Genres not supported.");
+
+ Reader reader = getReader(context, progressListener, "getGenres", null);
+ try {
+ return new GenreParser(context, getInstance(context)).parse(reader, progressListener);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public MusicDirectory getSongsByGenre(String genre, int count, int offset, Context context, ProgressListener progressListener) throws Exception {
+ checkServerVersion(context, "1.9", "Genres not supported.");
+
+ HttpParams params = new BasicHttpParams();
+ HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_GET_RANDOM_SONGS);
+
+ List<String> parameterNames = new ArrayList<String>();
+ List<Object> parameterValues = new ArrayList<Object>();
+
+ parameterNames.add("genre");
+ parameterValues.add(genre);
+ parameterNames.add("count");
+ parameterValues.add(count);
+ parameterNames.add("offset");
+ parameterValues.add(offset);
+
+ // Add folder if it was set and is non null
+ int instance = getInstance(context);
+ if(Util.getAlbumListsPerFolder(context, instance)) {
+ String folderId = Util.getSelectedMusicFolderId(context, instance);
+ if(folderId != null) {
+ parameterNames.add("musicFolderId");
+ parameterValues.add(folderId);
+ }
+ }
+
+ Reader reader = getReader(context, progressListener, "getSongsByGenre", params, parameterNames, parameterValues, true);
+ try {
+ return new RandomSongsParser(context, instance).parse(reader, progressListener);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public MusicDirectory getTopTrackSongs(String artist, int size, Context context, ProgressListener progressListener) throws Exception {
+ List<String> parameterNames = new ArrayList<String>();
+ List<Object> parameterValues = new ArrayList<Object>();
+
+ parameterNames.add("artist");
+ parameterValues.add(artist);
+ parameterNames.add("size");
+ parameterValues.add(size);
+
+ Reader reader = getReader(context, progressListener, "getTopTrackSongs", null, parameterNames, parameterValues);
+ try {
+ return new RandomSongsParser(context, getInstance(context)).parse(reader, progressListener);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public List<PodcastChannel> getPodcastChannels(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ checkServerVersion(context, "1.6", "Podcasts not supported.");
+
+ Reader reader = getReader(context, progressListener, "getPodcasts", null, Arrays.asList("includeEpisodes"), Arrays.<Object>asList("false"));
+ try {
+ List<PodcastChannel> channels = new PodcastChannelParser(context, getInstance(context)).parse(reader, progressListener);
+
+ String content = "";
+ for(PodcastChannel channel: channels) {
+ content += channel.getName() + "\n";
+ }
+
+ File file = FileUtil.getPodcastFile(context, Util.getServerName(context, getInstance(context)));
+ BufferedWriter bw = new BufferedWriter(new FileWriter(file));
+ bw.write(content);
+ bw.close();
+
+ return channels;
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public MusicDirectory getPodcastEpisodes(boolean refresh, String id, Context context, ProgressListener progressListener) throws Exception {
+ Reader reader = getReader(context, progressListener, "getPodcasts", null, Arrays.asList("id"), Arrays.<Object>asList(id));
+ try {
+ return new PodcastEntryParser(context, getInstance(context)).parse(id, reader, progressListener);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public void refreshPodcasts(Context context, ProgressListener progressListener) throws Exception {
+ checkServerVersion(context, "1.9", "Refresh podcasts not supported.");
+
+ Reader reader = getReader(context, progressListener, "refreshPodcasts", null);
+ try {
+ new ErrorParser(context, getInstance(context)).parse(reader);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public void createPodcastChannel(String url, Context context, ProgressListener progressListener) throws Exception{
+ checkServerVersion(context, "1.9", "Creating podcasts not supported.");
+
+ Reader reader = getReader(context, progressListener, "createPodcastChannel", null, "url", url);
+ try {
+ new ErrorParser(context, getInstance(context)).parse(reader);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public void deletePodcastChannel(String id, Context context, ProgressListener progressListener) throws Exception {
+ checkServerVersion(context, "1.9", "Deleting podcasts not supported.");
+
+ Reader reader = getReader(context, progressListener, "deletePodcastChannel", null, "id", id);
+ try {
+ new ErrorParser(context, getInstance(context)).parse(reader);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public void downloadPodcastEpisode(String id, Context context, ProgressListener progressListener) throws Exception{
+ checkServerVersion(context, "1.9", "Downloading podcasts not supported.");
+
+ Reader reader = getReader(context, progressListener, "downloadPodcastEpisode", null, "id", id);
+ try {
+ new ErrorParser(context, getInstance(context)).parse(reader);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public void deletePodcastEpisode(String id, String parent, ProgressListener progressListener, Context context) throws Exception{
+ checkServerVersion(context, "1.9", "Deleting podcasts not supported.");
+
+ Reader reader = getReader(context, progressListener, "deletePodcastEpisode", null, "id", id);
+ try {
+ new ErrorParser(context, getInstance(context)).parse(reader);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public void setRating(MusicDirectory.Entry entry, int rating, Context context, ProgressListener progressListener) throws Exception {
+ checkServerVersion(context, "1.6", "Setting ratings not supported.");
+
+ Reader reader = getReader(context, progressListener, "setRating", null, Arrays.asList("id", "rating"), Arrays.<Object>asList(entry.getId(), rating));
+ try {
+ new ErrorParser(context, getInstance(context)).parse(reader);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public MusicDirectory getBookmarks(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ checkServerVersion(context, "1.9", "Bookmarks not supported.");
+
+ Reader reader = getReader(context, progressListener, "getBookmarks", null);
+ try {
+ return new BookmarkParser(context, getInstance(context)).parse(reader, progressListener);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public void createBookmark(MusicDirectory.Entry entry, int position, String comment, Context context, ProgressListener progressListener) throws Exception {
+ checkServerVersion(context, "1.9", "Creating bookmarks not supported.");
+
+ Reader reader = getReader(context, progressListener, "createBookmark", null, Arrays.asList("id", "position", "comment"), Arrays.<Object>asList(entry.getId(), position, comment));
+ try {
+ new ErrorParser(context, getInstance(context)).parse(reader);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public void deleteBookmark(MusicDirectory.Entry entry, Context context, ProgressListener progressListener) throws Exception {
+ checkServerVersion(context, "1.9", "Deleting bookmarks not supported.");
+
+ Reader reader = getReader(context, progressListener, "deleteBookmark", null, Arrays.asList("id"), Arrays.<Object>asList(entry.getId()));
+ try {
+ new ErrorParser(context, getInstance(context)).parse(reader);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public User getUser(boolean refresh, String username, Context context, ProgressListener progressListener) throws Exception {
+ Reader reader = getReader(context, progressListener, "getUser", null, Arrays.asList("username"), Arrays.<Object>asList(username));
+ try {
+ List<User> users = new UserParser(context, getInstance(context)).parse(reader, progressListener);
+ if(users.size() > 0) {
+ // Should only have returned one anyways
+ return users.get(0);
+ } else {
+ return null;
+ }
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public List<User> getUsers(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ checkServerVersion(context, "1.8", "Getting user list is not supported");
+
+ Reader reader = getReader(context, progressListener, "getUsers", null);
+ try {
+ return new UserParser(context, getInstance(context)).parse(reader, progressListener);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public void createUser(User user, Context context, ProgressListener progressListener) throws Exception {
+ List<String> names = new ArrayList<String>();
+ List<Object> values = new ArrayList<Object>();
+
+ names.add("username");
+ values.add(user.getUsername());
+ names.add("email");
+ values.add(user.getEmail());
+ names.add("password");
+ values.add(user.getPassword());
+
+ for(User.Setting setting: user.getSettings()) {
+ names.add(setting.getName());
+ values.add(setting.getValue());
+ }
+
+ Reader reader = getReader(context, progressListener, "createUser", null, names, values);
+ try {
+ new ErrorParser(context, getInstance(context)).parse(reader);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public void updateUser(User user, Context context, ProgressListener progressListener) throws Exception {
+ checkServerVersion(context, "1.10", "Updating user is not supported");
+
+ List<String> names = new ArrayList<String>();
+ List<Object> values = new ArrayList<Object>();
+
+ names.add("username");
+ values.add(user.getUsername());
+
+ for(User.Setting setting: user.getSettings()) {
+ if(setting.getName().indexOf("Role") != -1) {
+ names.add(setting.getName());
+ values.add(setting.getValue());
+ }
+ }
+
+ Reader reader = getReader(context, progressListener, "updateUser", null, names, values);
+ try {
+ new ErrorParser(context, getInstance(context)).parse(reader);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public void deleteUser(String username, Context context, ProgressListener progressListener) throws Exception {
+ Reader reader = getReader(context, progressListener, "deleteUser", null, Arrays.asList("username"), Arrays.<Object>asList(username));
+ try {
+ new ErrorParser(context, getInstance(context)).parse(reader);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public void changeEmail(String username, String email, Context context, ProgressListener progressListener) throws Exception {
+ Reader reader = getReader(context, progressListener, "updateUser", null, Arrays.asList("username", "email"), Arrays.<Object>asList(username, email));
+ try {
+ new ErrorParser(context, getInstance(context)).parse(reader);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public void changePassword(String username, String password, Context context, ProgressListener progressListener) throws Exception {
+ Reader reader = getReader(context, progressListener, "changePassword", null, Arrays.asList("username", "password"), Arrays.<Object>asList(username, password));
+ try {
+ new ErrorParser(context, getInstance(context)).parse(reader);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public Bitmap getAvatar(String username, int size, Context context, ProgressListener progressListener, SilentBackgroundTask task) throws Exception {
+ // Return silently if server is too old
+ if (!ServerInfo.checkServerVersion(context, "1.8")) {
+ return null;
+ }
+
+ // Synchronize on the username so that we don't download concurrently for
+ // the same user.
+ synchronized (username) {
+ // Use cached file, if existing.
+ Bitmap bitmap = FileUtil.getAvatarBitmap(context, username, size);
+ if(bitmap != null) {
+ return bitmap;
+ }
+
+ String url = Util.getRestUrl(context, "getAvatar");
+ InputStream in = null;
+ try
+ {
+ List<String> parameterNames;
+ List<Object> parameterValues;
+
+ parameterNames = Collections.singletonList("username");
+ parameterValues = Arrays.<Object>asList(username);
+
+ HttpEntity entity = getEntityForURL(context, url, null, parameterNames, parameterValues, progressListener, task);
+ in = entity.getContent();
+ Header contentEncoding = entity.getContentEncoding();
+ if (contentEncoding != null && contentEncoding.getValue().equalsIgnoreCase("gzip")) {
+ in = new GZIPInputStream(in);
+ }
+
+ // If content type is XML, an error occurred. Get it.
+ String contentType = Util.getContentType(entity);
+ if (contentType != null && (contentType.startsWith("text/xml") || contentType.startsWith("text/html"))) {
+ new ErrorParser(context, getInstance(context)).parse(new InputStreamReader(in, Constants.UTF_8));
+ return null; // Never reached.
+ }
+
+ byte[] bytes = Util.toByteArray(in);
+ if(task != null && task.isCancelled()) {
+ // Handle case where partial is downloaded and cancelled
+ return null;
+ }
+
+ OutputStream out = null;
+ try {
+ out = new FileOutputStream(FileUtil.getAvatarFile(context, username));
+ out.write(bytes);
+ } finally {
+ Util.close(out);
+ }
+
+ return FileUtil.getSampledBitmap(bytes, size, false);
+ }
+ finally {
+ Util.close(in);
+ }
+ }
+ }
+
+ @Override
+ public ArtistInfo getArtistInfo(String id, boolean refresh, boolean allowNetwork, Context context, ProgressListener progressListener) throws Exception {
+ checkServerVersion(context, "1.11", "Getting artist info is not supported");
+
+ Reader reader = getReader(context, progressListener, Util.isTagBrowsing(context, getInstance(context)) ? "getArtistInfo2" : "getArtistInfo", null, Arrays.asList("id", "includeNotPresent"), Arrays.<Object>asList(id, "true"));
+ try {
+ return new ArtistInfoParser(context, getInstance(context)).parse(reader, progressListener);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public Bitmap getBitmap(String url, int size, Context context, ProgressListener progressListener, SilentBackgroundTask task) throws Exception {
+ // Synchronize on the url so that we don't download concurrently
+ synchronized (url) {
+ // Use cached file, if existing.
+ Bitmap bitmap = FileUtil.getMiscBitmap(context, url, size);
+ if(bitmap != null) {
+ return bitmap;
+ }
+
+ InputStream in = null;
+ try {
+ HttpEntity entity = getEntityForURL(context, url, null, null, null, progressListener, task);
+ in = entity.getContent();
+ Header contentEncoding = entity.getContentEncoding();
+ if (contentEncoding != null && contentEncoding.getValue().equalsIgnoreCase("gzip")) {
+ in = new GZIPInputStream(in);
+ }
+
+ // If content type is XML, an error occurred. Get it.
+ String contentType = Util.getContentType(entity);
+ if (contentType != null && (contentType.startsWith("text/xml") || contentType.startsWith("text/html"))) {
+ new ErrorParser(context, getInstance(context)).parse(new InputStreamReader(in, Constants.UTF_8));
+ return null; // Never reached.
+ }
+
+ byte[] bytes = Util.toByteArray(in);
+ if(task != null && task.isCancelled()) {
+ // Handle case where partial is downloaded and cancelled
+ return null;
+ }
+
+ OutputStream out = null;
+ try {
+ out = new FileOutputStream(FileUtil.getMiscFile(context, url));
+ out.write(bytes);
+ } finally {
+ Util.close(out);
+ }
+
+ return FileUtil.getSampledBitmap(bytes, size, false);
+ }
+ finally {
+ Util.close(in);
+ }
+ }
+ }
+
+ @Override
+ public MusicDirectory getVideos(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ Reader reader = getReader(context, progressListener, "getVideos", null, true);
+ try {
+ return new VideosParser(context, getInstance(context)).parse(reader, progressListener);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public void savePlayQueue(List<MusicDirectory.Entry> songs, MusicDirectory.Entry currentPlaying, int position, Context context, ProgressListener progressListener) throws Exception {
+ List<String> parameterNames = new LinkedList<String>();
+ List<Object> parameterValues = new LinkedList<Object>();
+
+ for(MusicDirectory.Entry song: songs) {
+ parameterNames.add("id");
+ parameterValues.add(song.getId());
+ }
+
+ parameterNames.add("current");
+ parameterValues.add(currentPlaying.getId());
+
+ parameterNames.add("position");
+ parameterValues.add(position);
+
+ Reader reader = getReader(context, progressListener, "savePlayQueue", null, parameterNames, parameterValues);
+ try {
+ new ErrorParser(context, getInstance(context)).parse(reader);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public PlayerQueue getPlayQueue(Context context, ProgressListener progressListener) throws Exception {
+ Reader reader = getReader(context, progressListener, "getPlayQueue", null);
+ try {
+ return new PlayQueueParser(context, getInstance(context)).parse(reader, progressListener);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public int processOfflineSyncs(final Context context, final ProgressListener progressListener) throws Exception{
+ return processOfflineScrobbles(context, progressListener) + processOfflineStars(context, progressListener);
+ }
+
+ public int processOfflineScrobbles(final Context context, final ProgressListener progressListener) throws Exception {
+ SharedPreferences offline = Util.getOfflineSync(context);
+ SharedPreferences.Editor offlineEditor = offline.edit();
+ int count = offline.getInt(Constants.OFFLINE_SCROBBLE_COUNT, 0);
+ int retry = 0;
+ for(int i = 1; i <= count; i++) {
+ String id = offline.getString(Constants.OFFLINE_SCROBBLE_ID + i, null);
+ long time = offline.getLong(Constants.OFFLINE_SCROBBLE_TIME + i, 0);
+ if(id != null) {
+ scrobble(id, true, time, context, progressListener);
+ } else {
+ String search = offline.getString(Constants.OFFLINE_SCROBBLE_SEARCH + i, "");
+ try{
+ SearchCritera critera = new SearchCritera(search, 0, 0, 1);
+ SearchResult result = searchNew(critera, context, progressListener);
+ if(result.getSongs().size() == 1){
+ Log.i(TAG, "Query '" + search + "' returned song " + result.getSongs().get(0).getTitle() + " by " + result.getSongs().get(0).getArtist() + " with id " + result.getSongs().get(0).getId());
+ Log.i(TAG, "Scrobbling " + result.getSongs().get(0).getId() + " with time " + time);
+ scrobble(result.getSongs().get(0).getId(), true, time, context, progressListener);
+ }
+ else{
+ throw new Exception("Song not found on server");
+ }
+ }
+ catch(Exception e){
+ Log.e(TAG, e.toString());
+ retry++;
+ }
+ }
+ }
+
+ offlineEditor.putInt(Constants.OFFLINE_SCROBBLE_COUNT, 0);
+ offlineEditor.commit();
+
+ return count - retry;
+ }
+
+ public int processOfflineStars(final Context context, final ProgressListener progressListener) throws Exception {
+ SharedPreferences offline = Util.getOfflineSync(context);
+ SharedPreferences.Editor offlineEditor = offline.edit();
+ int count = offline.getInt(Constants.OFFLINE_STAR_COUNT, 0);
+ int retry = 0;
+ for(int i = 1; i <= count; i++) {
+ String id = offline.getString(Constants.OFFLINE_STAR_ID + i, null);
+ boolean starred = offline.getBoolean(Constants.OFFLINE_STAR_SETTING + i, false);
+ if(id != null) {
+ setStarred(Arrays.asList(new MusicDirectory.Entry(id)), null, null, starred, progressListener, context);
+ } else {
+ String search = offline.getString(Constants.OFFLINE_STAR_SEARCH + i, "");
+ try{
+ SearchCritera critera = new SearchCritera(search, 0, 1, 1);
+ SearchResult result = searchNew(critera, context, progressListener);
+ if(result.getSongs().size() == 1) {
+ MusicDirectory.Entry song = result.getSongs().get(0);
+ Log.i(TAG, "Query '" + search + "' returned song " + song.getTitle() + " by " + song.getArtist() + " with id " + song.getId());
+ setStarred(Arrays.asList(song), null, null, starred, progressListener, context);
+ } else if(result.getAlbums().size() == 1) {
+ MusicDirectory.Entry album = result.getAlbums().get(0);
+ Log.i(TAG, "Query '" + search + "' returned album " + album.getTitle() + " by " + album.getArtist() + " with id " + album.getId());
+ if(Util.isTagBrowsing(context, getInstance(context))) {
+ setStarred(null, null, Arrays.asList(album), starred, progressListener, context);
+ } else {
+ setStarred(Arrays.asList(album), null, null, starred, progressListener, context);
+ }
+ }
+ else {
+ throw new Exception("Song not found on server");
+ }
+ }
+ catch(Exception e) {
+ Log.e(TAG, e.toString());
+ retry++;
+ }
+ }
+ }
+
+ offlineEditor.putInt(Constants.OFFLINE_STAR_COUNT, 0);
+ offlineEditor.commit();
+
+ return count - retry;
+ }
+
+ private String getOfflineSongId(String id, Context context, ProgressListener progressListener) throws Exception {
+ 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();
+ }
+ }
+
+ return id;
+ }
+
+ @Override
+ public void setInstance(Integer instance) throws Exception {
+ this.instance = instance;
+ }
+
+ private Reader getReader(Context context, ProgressListener progressListener, String method, HttpParams requestParams) throws Exception {
+ return getReader(context, progressListener, method, requestParams, false);
+ }
+ private Reader getReader(Context context, ProgressListener progressListener, String method, HttpParams requestParams, boolean throwsError) throws Exception {
+ return getReader(context, progressListener, method, requestParams, Collections.<String>emptyList(), Collections.emptyList(), throwsError);
+ }
+
+ private Reader getReader(Context context, ProgressListener progressListener, String method,
+ HttpParams requestParams, String parameterName, Object parameterValue) throws Exception {
+ return getReader(context, progressListener, method, requestParams, Arrays.asList(parameterName), Arrays.<Object>asList(parameterValue));
+ }
+
+ private Reader getReader(Context context, ProgressListener progressListener, String method,
+ HttpParams requestParams, List<String> parameterNames, List<Object> parameterValues) throws Exception {
+ return getReader(context, progressListener, method, requestParams, parameterNames, parameterValues, false);
+ }
+ private Reader getReader(Context context, ProgressListener progressListener, String method,
+ HttpParams requestParams, List<String> parameterNames, List<Object> parameterValues, boolean throwErrors) throws Exception {
+
+ if (progressListener != null) {
+ progressListener.updateProgress(R.string.service_connecting);
+ }
+
+ String url = getRestUrl(context, method);
+ return getReaderForURL(context, url, requestParams, parameterNames, parameterValues, progressListener, throwErrors);
+ }
+
+ private Reader getReaderForURL(Context context, String url, HttpParams requestParams, List<String> parameterNames,
+ List<Object> parameterValues, ProgressListener progressListener) throws Exception {
+ return getReaderForURL(context, url, requestParams, parameterNames, parameterValues, progressListener, true);
+ }
+ private Reader getReaderForURL(Context context, String url, HttpParams requestParams, List<String> parameterNames,
+ List<Object> parameterValues, ProgressListener progressListener, boolean throwErrors) throws Exception {
+ HttpEntity entity = getEntityForURL(context, url, requestParams, parameterNames, parameterValues, progressListener, throwErrors);
+ if (entity == null) {
+ throw new RuntimeException("No entity received for URL " + url);
+ }
+
+ InputStream in = entity.getContent();
+ Header contentEncoding = entity.getContentEncoding();
+ if (contentEncoding != null && contentEncoding.getValue().equalsIgnoreCase("gzip")) {
+ in = new GZIPInputStream(in);
+ }
+ return new InputStreamReader(in, Constants.UTF_8);
+ }
+
+ private HttpEntity getEntityForURL(Context context, String url, HttpParams requestParams, List<String> parameterNames,
+ List<Object> parameterValues, ProgressListener progressListener, boolean throwErrors) throws Exception {
+
+ return getEntityForURL(context, url, requestParams, parameterNames, parameterValues, progressListener, null, throwErrors);
+ }
+
+ private HttpEntity getEntityForURL(Context context, String url, HttpParams requestParams, List<String> parameterNames,
+ List<Object> parameterValues, ProgressListener progressListener, SilentBackgroundTask task) throws Exception {
+ return getResponseForURL(context, url, requestParams, parameterNames, parameterValues, null, progressListener, task, false).getEntity();
+ }
+ private HttpEntity getEntityForURL(Context context, String url, HttpParams requestParams, List<String> parameterNames,
+ List<Object> parameterValues, ProgressListener progressListener, SilentBackgroundTask task, boolean throwsError) throws Exception {
+ return getResponseForURL(context, url, requestParams, parameterNames, parameterValues, null, progressListener, task, throwsError).getEntity();
+ }
+
+ private HttpResponse getResponseForURL(Context context, String url, HttpParams requestParams,
+ List<String> parameterNames, List<Object> parameterValues,
+ List<Header> headers, ProgressListener progressListener, SilentBackgroundTask task, boolean throwsErrors) throws Exception {
+ // If not too many parameters, extract them to the URL rather than relying on the HTTP POST request being
+ // received intact. Remember, HTTP POST requests are converted to GET requests during HTTP redirects, thus
+ // loosing its entity.
+ if (parameterNames != null && parameterNames.size() < 10) {
+ StringBuilder builder = new StringBuilder(url);
+ for (int i = 0; i < parameterNames.size(); i++) {
+ builder.append("&").append(parameterNames.get(i)).append("=");
+ String part = URLEncoder.encode(String.valueOf(parameterValues.get(i)), "UTF-8");
+ part = part.replaceAll("\\%27", "&#39;");
+ builder.append(part);
+ }
+ url = builder.toString();
+ parameterNames = null;
+ parameterValues = null;
+ }
+
+ String rewrittenUrl = rewriteUrlWithRedirect(context, url);
+ return executeWithRetry(context, rewrittenUrl, url, requestParams, parameterNames, parameterValues, headers, progressListener, task, throwsErrors);
+ }
+
+ private HttpResponse executeWithRetry(final Context context, String url, String originalUrl, HttpParams requestParams,
+ List<String> parameterNames, List<Object> parameterValues,
+ List<Header> headers, ProgressListener progressListener, SilentBackgroundTask task, boolean throwErrors) throws Exception {
+ // Strip out sensitive information from log
+ if(url.indexOf("scanstatus") == -1) {
+ Log.i(TAG, stripUrlInfo(url));
+ }
+
+ SharedPreferences prefs = Util.getPreferences(context);
+ int networkTimeout = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_NETWORK_TIMEOUT, "15000"));
+ HttpParams newParams = httpClient.getParams();
+ HttpConnectionParams.setSoTimeout(newParams, networkTimeout);
+ httpClient.setParams(newParams);
+
+ final AtomicReference<Boolean> isCancelled = new AtomicReference<Boolean>(false);
+ int attempts = 0;
+ while (true) {
+ attempts++;
+ HttpContext httpContext = new BasicHttpContext();
+ final HttpPost request = new HttpPost(url);
+
+ if (task != null) {
+ // Attempt to abort the HTTP request if the task is cancelled.
+ task.setOnCancelListener(new BackgroundTask.OnCancelListener() {
+ @Override
+ public void onCancel() {
+ try {
+ isCancelled.set(true);
+ if(Thread.currentThread() == Looper.getMainLooper().getThread()) {
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ request.abort();
+ return null;
+ }
+ }.execute();
+ } else {
+ request.abort();
+ }
+ } catch(Exception e) {
+ Log.e(TAG, "Failed to stop http task", e);
+ }
+ }
+ });
+ }
+
+ if (parameterNames != null) {
+ List<NameValuePair> params = new ArrayList<NameValuePair>();
+ for (int i = 0; i < parameterNames.size(); i++) {
+ params.add(new BasicNameValuePair(parameterNames.get(i), String.valueOf(parameterValues.get(i))));
+ }
+ request.setEntity(new UrlEncodedFormEntity(params, Constants.UTF_8));
+ }
+
+ if (requestParams != null) {
+ request.setParams(requestParams);
+ }
+
+ if (headers != null) {
+ for (Header header : headers) {
+ request.addHeader(header);
+ }
+ }
+ if(url.indexOf("getCoverArt") == -1 && url.indexOf("stream") == -1 && url.indexOf("getAvatar") == -1) {
+ request.addHeader("Accept-Encoding", "gzip");
+ }
+
+ // Set credentials to get through apache proxies that require authentication.
+ int instance = prefs.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1);
+ String username = prefs.getString(Constants.PREFERENCES_KEY_USERNAME + instance, null);
+ String password = prefs.getString(Constants.PREFERENCES_KEY_PASSWORD + instance, null);
+ httpClient.getCredentialsProvider().setCredentials(new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT),
+ new UsernamePasswordCredentials(username, password));
+
+ try {
+ HttpResponse response = httpClient.execute(request, httpContext);
+ detectRedirect(originalUrl, context, httpContext);
+ return response;
+ } catch (IOException x) {
+ request.abort();
+ if (attempts >= HTTP_REQUEST_MAX_ATTEMPTS || isCancelled.get() || throwErrors) {
+ throw x;
+ }
+ if (progressListener != null) {
+ String msg = context.getResources().getString(R.string.music_service_retry, attempts, HTTP_REQUEST_MAX_ATTEMPTS - 1);
+ progressListener.updateProgress(msg);
+ }
+ Log.w(TAG, "Got IOException " + x + " (" + attempts + "), will retry");
+ increaseTimeouts(requestParams);
+ Thread.sleep(2000L);
+ }
+ }
+ }
+
+ private void increaseTimeouts(HttpParams requestParams) {
+ if (requestParams != null) {
+ int connectTimeout = HttpConnectionParams.getConnectionTimeout(requestParams);
+ if (connectTimeout != 0) {
+ HttpConnectionParams.setConnectionTimeout(requestParams, (int) (connectTimeout * 1.3F));
+ }
+ int readTimeout = HttpConnectionParams.getSoTimeout(requestParams);
+ if (readTimeout != 0) {
+ HttpConnectionParams.setSoTimeout(requestParams, (int) (readTimeout * 1.5F));
+ }
+ }
+ }
+
+ private void detectRedirect(String originalUrl, Context context, HttpContext httpContext) throws Exception {
+ HttpUriRequest request = (HttpUriRequest) httpContext.getAttribute(ExecutionContext.HTTP_REQUEST);
+ HttpHost host = (HttpHost) httpContext.getAttribute(ExecutionContext.HTTP_TARGET_HOST);
+
+ // Sometimes the request doesn't contain the "http://host" part
+ String redirectedUrl;
+ if (request.getURI().getScheme() == null) {
+ redirectedUrl = host.toURI() + request.getURI();
+ } else {
+ redirectedUrl = request.getURI().toString();
+ }
+
+ if(redirectedUrl != null && "http://subsonic.org/pages/".equals(redirectedUrl)) {
+ throw new Exception("Invalid url, redirects to http://subsonic.org/pages/");
+ }
+
+ int fromIndex = originalUrl.indexOf("/rest/");
+ int toIndex = redirectedUrl.indexOf("/rest/");
+ if(fromIndex != -1 && toIndex != -1 && !Util.equals(originalUrl, redirectedUrl)) {
+ redirectFrom = originalUrl.substring(0, fromIndex);
+ redirectTo = redirectedUrl.substring(0, toIndex);
+
+ if (redirectFrom.compareTo(redirectTo) != 0) {
+ Log.i(TAG, redirectFrom + " redirects to " + redirectTo);
+ }
+ redirectionLastChecked = System.currentTimeMillis();
+ redirectionNetworkType = getCurrentNetworkType(context);
+ }
+ }
+
+ private String rewriteUrlWithRedirect(Context context, String url) {
+
+ // Only cache for a certain time.
+ if (System.currentTimeMillis() - redirectionLastChecked > REDIRECTION_CHECK_INTERVAL_MILLIS) {
+ return url;
+ }
+
+ // Ignore cache if network type has changed.
+ if (redirectionNetworkType != getCurrentNetworkType(context)) {
+ return url;
+ }
+
+ if (redirectFrom == null || redirectTo == null) {
+ return url;
+ }
+
+ return url.replace(redirectFrom, redirectTo);
+ }
+
+ private String stripUrlInfo(String url) {
+ return url.substring(0, url.indexOf("?u=") + 1) + url.substring(url.indexOf("&v=") + 1);
+ }
+
+ private int getCurrentNetworkType(Context context) {
+ ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo networkInfo = manager.getActiveNetworkInfo();
+ return networkInfo == null ? -1 : networkInfo.getType();
+ }
+
+ public int getInstance(Context context) {
+ if(instance == null) {
+ return Util.getActiveServer(context);
+ } else {
+ return instance;
+ }
+ }
+ public String getRestUrl(Context context, String method) {
+ return getRestUrl(context, method, true);
+ }
+ public String getRestUrl(Context context, String method, boolean allowAltAddress) {
+ if(instance == null) {
+ return Util.getRestUrl(context, method, allowAltAddress);
+ } else {
+ return Util.getRestUrl(context, method, instance, allowAltAddress);
+ }
+ }
+
+ public HttpClient getHttpClient() {
+ return httpClient;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/RemoteController.java b/app/src/main/java/github/daneren2005/dsub/service/RemoteController.java
new file mode 100644
index 00000000..cac28c09
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/RemoteController.java
@@ -0,0 +1,116 @@
+/*
+ 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.service;
+
+import android.content.Context;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ProgressBar;
+import android.widget.Toast;
+
+import java.util.Iterator;
+import java.util.concurrent.LinkedBlockingQueue;
+
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.RemoteStatus;
+import github.daneren2005.serverproxy.WebProxy;
+
+public abstract class RemoteController {
+ private static final String TAG = RemoteController.class.getSimpleName();
+ protected DownloadService downloadService;
+ protected boolean nextSupported = false;
+
+ public abstract void create(boolean playing, int seconds);
+ public abstract void start();
+ public abstract void stop();
+ public abstract void shutdown();
+
+ public abstract void updatePlaylist();
+ public abstract void changePosition(int seconds);
+ public abstract void changeTrack(int index, DownloadFile song);
+ // Really is abstract, just don't want to require RemoteController's support it
+ public void changeNextTrack(DownloadFile song) {
+
+ };
+ public boolean isNextSupported() {
+ return this.nextSupported;
+ }
+ public abstract void setVolume(int volume);
+ public abstract void updateVolume(boolean up);
+ public abstract double getVolume();
+ public boolean isSeekable() {
+ return true;
+ }
+
+ public abstract int getRemotePosition();
+ public int getRemoteDuration() {
+ return 0;
+ }
+
+ protected abstract class RemoteTask {
+ abstract RemoteStatus execute() throws Exception;
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName();
+ }
+ }
+
+ protected static class TaskQueue {
+ private final LinkedBlockingQueue<RemoteTask> queue = new LinkedBlockingQueue<RemoteTask>();
+
+ void add(RemoteTask jukeboxTask) {
+ queue.add(jukeboxTask);
+ }
+
+ RemoteTask take() throws InterruptedException {
+ return queue.take();
+ }
+
+ void remove(Class<? extends RemoteTask> clazz) {
+ try {
+ Iterator<RemoteTask> iterator = queue.iterator();
+ while (iterator.hasNext()) {
+ RemoteTask task = iterator.next();
+ if (clazz.equals(task.getClass())) {
+ iterator.remove();
+ }
+ }
+ } catch (Throwable x) {
+ Log.w(TAG, "Failed to clean-up task queue.", x);
+ }
+ }
+
+ void clear() {
+ queue.clear();
+ }
+ }
+
+ protected WebProxy createWebProxy() {
+ MusicService musicService = MusicServiceFactory.getMusicService(downloadService);
+ if(musicService instanceof CachedMusicService) {
+ return new WebProxy(downloadService, ((CachedMusicService)musicService).getMusicService().getHttpClient());
+ } else {
+ return new WebProxy(downloadService);
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/Scrobbler.java b/app/src/main/java/github/daneren2005/dsub/service/Scrobbler.java
new file mode 100644
index 00000000..1f8538c9
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/Scrobbler.java
@@ -0,0 +1,85 @@
+package github.daneren2005.dsub.service;
+
+import android.content.Context;
+import android.util.Log;
+
+import github.daneren2005.dsub.domain.PodcastEpisode;
+import github.daneren2005.dsub.util.SilentBackgroundTask;
+import github.daneren2005.dsub.util.Util;
+
+/**
+ * Scrobbles played songs to Last.fm.
+ *
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class Scrobbler {
+ private static final String TAG = Scrobbler.class.getSimpleName();
+ private static final int FOUR_MINUTES = 4 * 60 * 1000;
+
+ private String lastSubmission;
+ private String lastNowPlaying;
+
+ public void conditionalScrobble(Context context, DownloadFile song, int playerPosition, int duration) {
+ // More than 4 minutes
+ if(playerPosition > FOUR_MINUTES) {
+ scrobble(context, song, true);
+ }
+ // More than 50% played
+ else if(duration > 0 && playerPosition > (duration / 2)) {
+ scrobble(context, song, true);
+ }
+ }
+
+ 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) {
+ return;
+ }
+
+ final String id = song.getSong().getId();
+
+ // Avoid duplicate registrations.
+ if (submission && id.equals(lastSubmission)) {
+ return;
+ }
+ if (!submission && id.equals(lastNowPlaying)) {
+ return;
+ }
+
+ if (submission) {
+ lastSubmission = id;
+ } else {
+ lastNowPlaying = id;
+ }
+
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() {
+ MusicService service = MusicServiceFactory.getMusicService(context);
+ try {
+ service.scrobble(id, submission, context, null);
+ Log.i(TAG, "Scrobbled '" + (submission ? "submission" : "now playing") + "' for " + song);
+ } catch (Exception x) {
+ Log.i(TAG, "Failed to scrobble'" + (submission ? "submission" : "now playing") + "' for " + song, x);
+ }
+ return null;
+ }
+ }.execute();
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/ServerTooOldException.java b/app/src/main/java/github/daneren2005/dsub/service/ServerTooOldException.java
new file mode 100644
index 00000000..e4a951de
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/ServerTooOldException.java
@@ -0,0 +1,60 @@
+/*
+ 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.service;
+
+import github.daneren2005.dsub.domain.Version;
+
+/**
+ * Thrown if the REST API version implemented by the server is too old.
+ *
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class ServerTooOldException extends Exception {
+
+ private final String text;
+ private final Version serverVersion;
+ private final Version requiredVersion;
+
+ public ServerTooOldException(String text, Version serverVersion, Version requiredVersion) {
+ this.text = text;
+ this.serverVersion = serverVersion;
+ this.requiredVersion = requiredVersion;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ if (text != null) {
+ builder.append(text).append(" ");
+ }
+ builder.append("Server API version too old. ");
+ builder.append("Requires server version ")
+ .append(requiredVersion.getVersion())
+ .append(", but it is version ")
+ .append(serverVersion.getVersion())
+ .append(".");
+ return builder.toString();
+ }
+
+ @Override
+ public String getMessage() {
+ return this.toString();
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/AbstractParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/AbstractParser.java
new file mode 100644
index 00000000..bc5d2199
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/parser/AbstractParser.java
@@ -0,0 +1,150 @@
+/*
+ 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.service.parser;
+
+import java.io.Reader;
+
+import org.xmlpull.v1.XmlPullParser;
+
+import android.content.Context;
+import android.util.Xml;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.ServerInfo;
+import github.daneren2005.dsub.domain.Version;
+import github.daneren2005.dsub.util.ProgressListener;
+import github.daneren2005.dsub.util.Util;
+
+/**
+ * @author Sindre Mehus
+ */
+public abstract class AbstractParser {
+
+ protected final Context context;
+ protected final int instance;
+ private XmlPullParser parser;
+ private boolean rootElementFound;
+
+ public AbstractParser(Context context, int instance) {
+ this.context = context;
+ this.instance = instance;
+ }
+
+ protected Context getContext() {
+ return context;
+ }
+
+ protected void handleError() throws Exception {
+ int code = getInteger("code");
+ String message;
+ switch (code) {
+ case 0:
+ message = context.getResources().getString(R.string.parser_server_error, get("message"));
+ break;
+ case 20:
+ message = context.getResources().getString(R.string.parser_upgrade_client);
+ break;
+ case 30:
+ message = context.getResources().getString(R.string.parser_upgrade_server);
+ break;
+ case 40:
+ message = context.getResources().getString(R.string.parser_not_authenticated);
+ break;
+ case 50:
+ message = context.getResources().getString(R.string.parser_not_authorized);
+ break;
+ default:
+ message = get("message");
+ break;
+ }
+ throw new SubsonicRESTException(code, message);
+ }
+
+ protected void updateProgress(ProgressListener progressListener, int messageId) {
+ if (progressListener != null) {
+ progressListener.updateProgress(messageId);
+ }
+ }
+
+ protected void updateProgress(ProgressListener progressListener, String message) {
+ if (progressListener != null) {
+ progressListener.updateProgress(message);
+ }
+ }
+
+ protected String getText() {
+ return parser.getText();
+ }
+
+ protected String get(String name) {
+ return parser.getAttributeValue(null, name);
+ }
+
+ protected boolean getBoolean(String name) {
+ return "true".equals(get(name));
+ }
+
+ protected Integer getInteger(String name) {
+ String s = get(name);
+ return s == null ? null : Integer.valueOf(s);
+ }
+
+ protected Long getLong(String name) {
+ String s = get(name);
+ return s == null ? null : Long.valueOf(s);
+ }
+
+ protected Float getFloat(String name) {
+ String s = get(name);
+ return s == null ? null : Float.valueOf(s);
+ }
+
+ protected void init(Reader reader) throws Exception {
+ parser = Xml.newPullParser();
+ parser.setInput(reader);
+ rootElementFound = false;
+ }
+
+ protected int nextParseEvent() throws Exception {
+ return parser.next();
+ }
+
+ protected String getElementName() {
+ String name = parser.getName();
+ if ("subsonic-response".equals(name)) {
+ rootElementFound = true;
+ String version = get("version");
+ if (version != null) {
+ ServerInfo server = new ServerInfo();
+ server.setRestVersion(new Version(version));
+
+ if("madsonic".equals(get("type"))) {
+ server.setRestType(ServerInfo.TYPE_MADSONIC);
+ }
+ server.saveServerInfo(context, instance);
+ }
+ }
+ return name;
+ }
+
+ protected void validate() throws Exception {
+ if (!rootElementFound) {
+ throw new Exception(context.getResources().getString(R.string.background_task_parse_error));
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/AlbumListParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/AlbumListParser.java
new file mode 100644
index 00000000..773c241b
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/parser/AlbumListParser.java
@@ -0,0 +1,61 @@
+/*
+ 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.service.parser;
+
+import android.content.Context;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.util.ProgressListener;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.Reader;
+
+/**
+ * @author Sindre Mehus
+ */
+public class AlbumListParser extends MusicDirectoryEntryParser {
+
+ public AlbumListParser(Context context, int instance) {
+ super(context, instance);
+ }
+
+ public MusicDirectory parse(Reader reader, ProgressListener progressListener) throws Exception {
+ init(reader);
+
+ MusicDirectory dir = new MusicDirectory();
+ int eventType;
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = getElementName();
+ if ("album".equals(name)) {
+ MusicDirectory.Entry entry = parseEntry("");
+ entry.setDirectory(true);
+ dir.addChild(entry);
+ } else if ("error".equals(name)) {
+ handleError();
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+
+ return dir;
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/ArtistInfoParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/ArtistInfoParser.java
new file mode 100644
index 00000000..5c3d2412
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/parser/ArtistInfoParser.java
@@ -0,0 +1,82 @@
+/*
+ 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.service.parser;
+
+import android.content.Context;
+import android.util.Log;
+
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.List;
+
+import github.daneren2005.dsub.domain.Artist;
+import github.daneren2005.dsub.domain.ArtistInfo;
+import github.daneren2005.dsub.util.ProgressListener;
+
+public class ArtistInfoParser extends AbstractParser {
+ private static final String TAG = ArtistInfo.class.getSimpleName();
+
+ public ArtistInfoParser(Context context, int instance) {
+ super(context, instance);
+ }
+
+ public ArtistInfo parse(Reader reader, ProgressListener progressListener) throws Exception {
+ init(reader);
+
+ ArtistInfo info = new ArtistInfo();
+ List<Artist> artists = new ArrayList<Artist>();
+ List<String> missingArtists = new ArrayList<String>();
+
+ String tagName = null;
+ int eventType;
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG) {
+ tagName = getElementName();
+ if ("similarArtist".equals(tagName)) {
+ String id = get("id");
+ if(id.equals("-1")) {
+ missingArtists.add(get("name"));
+ } else {
+ Artist artist = new Artist();
+ artist.setId(id);
+ artist.setName(get("name"));
+ artist.setStarred(get("starred") != null);
+ artists.add(artist);
+ }
+ } else if ("error".equals(tagName)) {
+ handleError();
+ }
+ } else if(eventType == XmlPullParser.TEXT) {
+ if ("biography".equals(tagName) && info.getBiography() == null) {
+ info.setBiography(getText());
+ } else if ("musicBrainzId".equals(tagName) && info.getMusicBrainzId() == null) {
+ info.setMusicBrainzId(getText());
+ } else if ("lastFmUrl".equals(tagName) && info.getLastFMUrl() == null) {
+ info.setLastFMUrl(getText());
+ } else if ("largeImageUrl".equals(tagName) && info.getImageUrl() == null) {
+ info.setImageUrl(getText());
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ info.setSimilarArtists(artists);
+ info.setMissingArtists(missingArtists);
+ return info;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/BookmarkParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/BookmarkParser.java
new file mode 100644
index 00000000..8e04749c
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/parser/BookmarkParser.java
@@ -0,0 +1,100 @@
+/*
+ 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 2013 (C) Scott Jackson
+*/
+package github.daneren2005.dsub.service.parser;
+
+import android.content.Context;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.Bookmark;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.ServerInfo;
+import github.daneren2005.dsub.util.ProgressListener;
+import org.xmlpull.v1.XmlPullParser;
+import java.io.Reader;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.TimeZone;
+
+/**
+ * @author Scott Jackson
+ */
+public class BookmarkParser extends MusicDirectoryEntryParser {
+ public BookmarkParser(Context context, int instance) {
+ super(context, instance);
+ }
+
+ public MusicDirectory parse(Reader reader, ProgressListener progressListener) throws Exception {
+ init(reader);
+
+ List<MusicDirectory.Entry> bookmarks = new ArrayList<MusicDirectory.Entry>();
+ Bookmark bookmark = null;
+ int eventType;
+
+ boolean isDateNormalized = ServerInfo.checkServerVersion(context, "1.11");
+ SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH);
+ if(isDateNormalized) {
+ dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+ }
+
+ do {
+ eventType = nextParseEvent();
+
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = getElementName();
+
+ if ("bookmark".equals(name)) {
+ bookmark = new Bookmark();
+
+ try {
+ bookmark.setCreated(dateFormat.parse(get("created")));
+ } catch (Exception e) {
+ bookmark.setCreated((Date) null);
+ }
+
+ try {
+ bookmark.setChanged(dateFormat.parse(get("changed")));
+ } catch (Exception e) {
+ bookmark.setChanged((Date) null);
+ }
+
+ bookmark.setComment(get("comment"));
+ bookmark.setPosition(getInteger("position"));
+ bookmark.setUsername(get("username"));
+ } else if ("entry".equals(name)) {
+ MusicDirectory.Entry entry = parseEntry(null);
+ // Work around for bookmarks showing entry with a track when podcast listings don't
+ if("podcast".equals(get("type"))) {
+ entry.setTrack(null);
+ }
+ entry.setBookmark(bookmark);
+ bookmarks.add(entry);
+ } else if ("error".equals(name)) {
+ handleError();
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+
+ return new MusicDirectory(bookmarks);
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/ChatMessageParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/ChatMessageParser.java
new file mode 100644
index 00000000..36835fce
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/parser/ChatMessageParser.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.service.parser;
+
+import android.content.Context;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.ChatMessage;
+import github.daneren2005.dsub.util.ProgressListener;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author Joshua Bahnsen
+ */
+public class ChatMessageParser extends AbstractParser {
+
+ public ChatMessageParser(Context context, int instance) {
+ super(context, instance);
+ }
+
+ public List<ChatMessage> parse(Reader reader, ProgressListener progressListener) throws Exception {
+ init(reader);
+ List<ChatMessage> result = new ArrayList<ChatMessage>();
+ int eventType;
+
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = getElementName();
+ if ("chatMessage".equals(name)) {
+ ChatMessage chatMessage = new ChatMessage();
+ chatMessage.setUsername(get("username"));
+ chatMessage.setTime(getLong("time"));
+ chatMessage.setMessage(get("message"));
+ result.add(chatMessage);
+ } else if ("error".equals(name)) {
+ handleError();
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+
+ return result;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/ErrorParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/ErrorParser.java
new file mode 100644
index 00000000..afb05928
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/parser/ErrorParser.java
@@ -0,0 +1,49 @@
+/*
+ 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.service.parser;
+
+import android.content.Context;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.Reader;
+
+/**
+ * @author Sindre Mehus
+ */
+public class ErrorParser extends AbstractParser {
+
+ public ErrorParser(Context context, int instance) {
+ super(context, instance);
+ }
+
+ public void parse(Reader reader) throws Exception {
+
+ init(reader);
+
+ int eventType;
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG && "error".equals(getElementName())) {
+ handleError();
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/GenreParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/GenreParser.java
new file mode 100644
index 00000000..ddb03544
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/parser/GenreParser.java
@@ -0,0 +1,122 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.service.parser;
+
+import android.content.Context;
+import android.text.Html;
+import android.util.Log;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.Genre;
+import github.daneren2005.dsub.util.ProgressListener;
+
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author Joshua Bahnsen
+ */
+public class GenreParser extends AbstractParser {
+ private static final String TAG = GenreParser.class.getSimpleName();
+
+ public GenreParser(Context context, int instance) {
+ super(context, instance);
+ }
+
+ public List<Genre> parse(Reader reader, ProgressListener progressListener) throws Exception {
+ List<Genre> result = new ArrayList<Genre>();
+ StringReader sr = null;
+
+ try {
+ BufferedReader br = new BufferedReader(reader);
+ String xml = null;
+ String line = null;
+
+ while ((line = br.readLine()) != null) {
+ if (xml == null) {
+ xml = line;
+ } else {
+ xml += line;
+ }
+ }
+ br.close();
+
+ // Replace double escaped ampersand (&amp;apos;)
+ xml = xml.replaceAll("(?:&amp;)(amp;|lt;|gt;|#37;|apos;)", "&$1");
+
+ // Replace unescaped ampersand
+ xml = xml.replaceAll("&(?!amp;|lt;|gt;|#37;|apos;)", "&amp;");
+
+ // Replace unescaped percent symbol
+ // No replacements for <> at this time
+ xml = xml.replaceAll("%", "&#37;");
+
+ xml = xml.replaceAll("'", "&apos;");
+
+ sr = new StringReader(xml);
+ } catch (IOException ioe) {
+ Log.e(TAG, "Error parsing Genre XML", ioe);
+ }
+
+ if (sr == null) {
+ Log.w(TAG, "Unable to parse Genre XML, returning empty list");
+ return result;
+ }
+
+ init(sr);
+
+ Genre genre = null;
+
+ int eventType;
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = getElementName();
+ if ("genre".equals(name)) {
+ genre = new Genre();
+ genre.setSongCount(getInteger("songCount"));
+ genre.setAlbumCount(getInteger("albumCount"));
+ } else if ("error".equals(name)) {
+ handleError();
+ } else {
+ genre = null;
+ }
+ } else if (eventType == XmlPullParser.TEXT) {
+ if (genre != null) {
+ String value = getText();
+ if (genre != null) {
+ genre.setName(Html.fromHtml(value).toString());
+ genre.setIndex(value.substring(0, 1));
+ result.add(genre);
+ genre = null;
+ }
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+
+ return Genre.GenreComparator.sort(result);
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/IndexesParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/IndexesParser.java
new file mode 100644
index 00000000..0ac86476
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/parser/IndexesParser.java
@@ -0,0 +1,134 @@
+/*
+ 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.service.parser;
+
+import java.io.Reader;
+import java.util.HashMap;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Map;
+
+import org.xmlpull.v1.XmlPullParser;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.Artist;
+import github.daneren2005.dsub.domain.Indexes;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.util.ProgressListener;
+import android.util.Log;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.Util;
+
+/**
+ * @author Sindre Mehus
+ */
+public class IndexesParser extends MusicDirectoryEntryParser {
+ private static final String TAG = IndexesParser.class.getSimpleName();
+
+ public IndexesParser(Context context, int instance) {
+ super(context, instance);
+ }
+
+ public Indexes parse(Reader reader, ProgressListener progressListener) throws Exception {
+ long t0 = System.currentTimeMillis();
+ init(reader);
+
+ List<Artist> artists = new ArrayList<Artist>();
+ List<Artist> shortcuts = new ArrayList<Artist>();
+ List<MusicDirectory.Entry> entries = new ArrayList<MusicDirectory.Entry>();
+ Long lastModified = null;
+ int eventType;
+ String index = "#";
+ String ignoredArticles = null;
+ boolean changed = false;
+ Map<String, Artist> artistList = new HashMap<String, Artist>();
+
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = getElementName();
+ if ("indexes".equals(name) || "artists".equals(name)) {
+ changed = true;
+ lastModified = getLong("lastModified");
+ ignoredArticles = get("ignoredArticles");
+ } else if ("index".equals(name)) {
+ index = get("name");
+
+ } else if ("artist".equals(name)) {
+ Artist artist = new Artist();
+ artist.setId(get("id"));
+ artist.setName(get("name"));
+ artist.setIndex(index);
+ artist.setStarred(get("starred") != null);
+
+ // Combine the id's for the two artists
+ if(artistList.containsKey(artist.getName())) {
+ Artist originalArtist = artistList.get(artist.getName());
+ if(originalArtist.isStarred()) {
+ artist.setStarred(true);
+ }
+ originalArtist.setId(originalArtist.getId() + ";" + artist.getId());
+ } else {
+ artistList.put(artist.getName(), artist);
+ artists.add(artist);
+ }
+
+ if (artists.size() % 10 == 0) {
+ String msg = getContext().getResources().getString(R.string.parser_artist_count, artists.size());
+ updateProgress(progressListener, msg);
+ }
+ } else if ("shortcut".equals(name)) {
+ Artist shortcut = new Artist();
+ shortcut.setId(get("id"));
+ shortcut.setName(get("name"));
+ shortcut.setIndex("*");
+ shortcut.setStarred(get("starred") != null);
+ shortcuts.add(shortcut);
+ } else if("child".equals(name)) {
+ MusicDirectory.Entry entry = parseEntry("");
+ entries.add(entry);
+ } else if ("error".equals(name)) {
+ handleError();
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+
+ if(ignoredArticles != null) {
+ SharedPreferences.Editor prefs = Util.getPreferences(context).edit();
+ prefs.putString(Constants.CACHE_KEY_IGNORE, ignoredArticles);
+ prefs.commit();
+ }
+
+ if (!changed) {
+ return null;
+ }
+
+ long t1 = System.currentTimeMillis();
+ Log.d(TAG, "Got " + artists.size() + " artist(s) in " + (t1 - t0) + "ms.");
+
+ String msg = getContext().getResources().getString(R.string.parser_artist_count, artists.size());
+ updateProgress(progressListener, msg);
+
+ return new Indexes(lastModified == null ? 0L : lastModified, shortcuts, artists, entries);
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/JukeboxStatusParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/JukeboxStatusParser.java
new file mode 100644
index 00000000..95529635
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/parser/JukeboxStatusParser.java
@@ -0,0 +1,62 @@
+/*
+ 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.service.parser;
+
+import java.io.Reader;
+
+import org.xmlpull.v1.XmlPullParser;
+
+import android.content.Context;
+import github.daneren2005.dsub.domain.RemoteStatus;
+
+/**
+ * @author Sindre Mehus
+ */
+public class JukeboxStatusParser extends AbstractParser {
+
+ public JukeboxStatusParser(Context context, int instance) {
+ super(context, instance);
+ }
+
+ public RemoteStatus parse(Reader reader) throws Exception {
+
+ init(reader);
+
+ RemoteStatus jukeboxStatus = new RemoteStatus();
+ int eventType;
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = getElementName();
+ if ("jukeboxPlaylist".equals(name) || "jukeboxStatus".equals(name)) {
+ jukeboxStatus.setPositionSeconds(getInteger("position"));
+ jukeboxStatus.setCurrentIndex(getInteger("currentIndex"));
+ jukeboxStatus.setPlaying(getBoolean("playing"));
+ jukeboxStatus.setGain(getFloat("gain"));
+ } else if ("error".equals(name)) {
+ handleError();
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+
+ return jukeboxStatus;
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/LicenseParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/LicenseParser.java
new file mode 100644
index 00000000..78790062
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/parser/LicenseParser.java
@@ -0,0 +1,62 @@
+/*
+ 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.service.parser;
+
+import android.content.Context;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.Reader;
+
+import github.daneren2005.dsub.domain.ServerInfo;
+import github.daneren2005.dsub.domain.Version;
+
+/**
+ * @author Sindre Mehus
+ */
+public class LicenseParser extends AbstractParser {
+
+ public LicenseParser(Context context, int instance) {
+ super(context, instance);
+ }
+
+ public ServerInfo parse(Reader reader) throws Exception {
+
+ init(reader);
+
+ ServerInfo serverInfo = new ServerInfo();
+ int eventType;
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = getElementName();
+ if ("subsonic-response".equals(name)) {
+ serverInfo.setRestVersion(new Version(get("version")));
+ } else if ("license".equals(name)) {
+ serverInfo.setLicenseValid(getBoolean("valid"));
+ } else if ("error".equals(name)) {
+ handleError();
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+
+ return serverInfo;
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/LyricsParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/LyricsParser.java
new file mode 100644
index 00000000..e7ce7a4b
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/parser/LyricsParser.java
@@ -0,0 +1,64 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.service.parser;
+
+import android.content.Context;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.Lyrics;
+import github.daneren2005.dsub.util.ProgressListener;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.Reader;
+
+/**
+ * @author Sindre Mehus
+ */
+public class LyricsParser extends AbstractParser {
+
+ public LyricsParser(Context context, int instance) {
+ super(context, instance);
+ }
+
+ public Lyrics parse(Reader reader, ProgressListener progressListener) throws Exception {
+ init(reader);
+
+ Lyrics lyrics = null;
+ int eventType;
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = getElementName();
+ if ("lyrics".equals(name)) {
+ lyrics = new Lyrics();
+ lyrics.setArtist(get("artist"));
+ lyrics.setTitle(get("title"));
+ } else if ("error".equals(name)) {
+ handleError();
+ }
+ } else if (eventType == XmlPullParser.TEXT) {
+ if (lyrics != null && lyrics.getText() == null) {
+ lyrics.setText(getText());
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+ return lyrics;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/MusicDirectoryEntryParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/MusicDirectoryEntryParser.java
new file mode 100644
index 00000000..9542324e
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/parser/MusicDirectoryEntryParser.java
@@ -0,0 +1,94 @@
+/*
+ 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.service.parser;
+
+import android.content.Context;
+
+import github.daneren2005.dsub.domain.Bookmark;
+import github.daneren2005.dsub.domain.MusicDirectory;
+
+/**
+ * @author Sindre Mehus
+ */
+public class MusicDirectoryEntryParser extends AbstractParser {
+ public MusicDirectoryEntryParser(Context context, int instance) {
+ super(context, instance);
+ }
+
+ protected MusicDirectory.Entry parseEntry(String artist) {
+ MusicDirectory.Entry entry = new MusicDirectory.Entry();
+ entry.setId(get("id"));
+ entry.setParent(get("parent"));
+ entry.setArtistId(get("artistId"));
+ entry.setTitle(get("title"));
+ if(entry.getTitle() == null) {
+ entry.setTitle(get("name"));
+ }
+ entry.setDirectory(getBoolean("isDir"));
+ entry.setCoverArt(get("coverArt"));
+ entry.setArtist(get("artist"));
+ entry.setStarred(get("starred") != null);
+ entry.setYear(getInteger("year"));
+ entry.setGenre(get("genre"));
+ entry.setAlbum(get("album"));
+ entry.setRating(getInteger("userRating"));
+
+ if (!entry.isDirectory()) {
+ entry.setAlbumId(get("albumId"));
+ entry.setTrack(getInteger("track"));
+ entry.setContentType(get("contentType"));
+ entry.setSuffix(get("suffix"));
+ entry.setTranscodedContentType(get("transcodedContentType"));
+ entry.setTranscodedSuffix(get("transcodedSuffix"));
+ entry.setSize(getLong("size"));
+ entry.setDuration(getInteger("duration"));
+ entry.setBitRate(getInteger("bitRate"));
+ entry.setPath(get("path"));
+ entry.setVideo(getBoolean("isVideo"));
+ entry.setDiscNumber(getInteger("discNumber"));
+
+ Integer bookmark = getInteger("bookmarkPosition");
+ if(bookmark != null) {
+ entry.setBookmark(new Bookmark(bookmark));
+ }
+
+ String type = get("type");
+ if("podcast".equals(type)) {
+ entry.setType(MusicDirectory.Entry.TYPE_PODCAST);
+ } else if("audiobook".equals(type) || (entry.getGenre() != null && "audiobook".equals(entry.getGenre().toLowerCase()))) {
+ entry.setType(MusicDirectory.Entry.TYPE_AUDIO_BOOK);
+ }
+ } else if(!"".equals(artist)) {
+ entry.setPath(artist + "/" + entry.getTitle());
+ }
+ return entry;
+ }
+
+ protected MusicDirectory.Entry parseArtist() {
+ MusicDirectory.Entry entry = new MusicDirectory.Entry();
+
+ entry.setId(get("id"));
+ entry.setTitle(get("name"));
+ entry.setPath(entry.getTitle());
+ entry.setStarred(true);
+ entry.setDirectory(true);
+
+ return entry;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/MusicDirectoryParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/MusicDirectoryParser.java
new file mode 100644
index 00000000..a786bceb
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/parser/MusicDirectoryParser.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 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.service.parser;
+
+import android.content.Context;
+import android.util.Log;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.ServerInfo;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.ProgressListener;
+import github.daneren2005.dsub.util.Util;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.Reader;
+import java.util.HashMap;
+import java.util.Map;
+
+import static github.daneren2005.dsub.domain.MusicDirectory.*;
+
+/**
+ * @author Sindre Mehus
+ */
+public class MusicDirectoryParser extends MusicDirectoryEntryParser {
+
+ private static final String TAG = MusicDirectoryParser.class.getSimpleName();
+
+ public MusicDirectoryParser(Context context, int instance) {
+ super(context, instance);
+ }
+
+ public MusicDirectory parse(String artist, Reader reader, ProgressListener progressListener) throws Exception {
+ init(reader);
+
+ MusicDirectory dir = new MusicDirectory();
+ int eventType;
+ boolean isArtist = false;
+ boolean checkForDuplicates = Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_RENAME_DUPLICATES, true);
+ Map<String, Entry> titleMap = new HashMap<String, Entry>();
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = getElementName();
+ if ("child".equals(name) || "song".equals(name) || "video".equals(name)) {
+ Entry entry = parseEntry(artist);
+ entry.setGrandParent(dir.getParent());
+
+ // Only check for songs
+ if(checkForDuplicates && !entry.isDirectory()) {
+ // Check if duplicates
+ Entry duplicate = titleMap.get(entry.getTitle());
+ if (duplicate != null) {
+ // Check if the first already has been rebased or not
+ if (duplicate.getTitle().equals(entry.getTitle())) {
+ duplicate.rebaseTitleOffPath();
+ }
+
+ // Rebase if this is the second instance of this title found
+ entry.rebaseTitleOffPath();
+ } else {
+ titleMap.put(entry.getTitle(), entry);
+ }
+ }
+
+ dir.addChild(entry);
+ } else if ("directory".equals(name) || "artist".equals(name) || ("album".equals(name) && !isArtist)) {
+ dir.setName(get("name"));
+ dir.setId(get("id"));
+ if(Util.isTagBrowsing(context, instance)) {
+ dir.setParent(get("artistId"));
+ } else {
+ dir.setParent(get("parent"));
+ }
+ isArtist = true;
+ } else if("album".equals(name)) {
+ Entry entry = parseEntry(artist);
+ entry.setDirectory(true);
+ dir.addChild(entry);
+ } else if ("error".equals(name)) {
+ handleError();
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+
+ // Only apply sorting on server version 4.7 and greater, where disc is supported
+ dir.sortChildren(context, instance);
+
+ return dir;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/MusicFoldersParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/MusicFoldersParser.java
new file mode 100644
index 00000000..a525084e
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/parser/MusicFoldersParser.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.service.parser;
+
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.xmlpull.v1.XmlPullParser;
+
+import android.content.Context;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicFolder;
+import github.daneren2005.dsub.util.ProgressListener;
+
+/**
+ * @author Sindre Mehus
+ */
+public class MusicFoldersParser extends AbstractParser {
+
+ public MusicFoldersParser(Context context, int instance) {
+ super(context, instance);
+ }
+
+ public List<MusicFolder> parse(Reader reader, ProgressListener progressListener) throws Exception {
+ init(reader);
+
+ List<MusicFolder> result = new ArrayList<MusicFolder>();
+ int eventType;
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG) {
+ String tag = getElementName();
+ if ("musicFolder".equals(tag)) {
+ String id = get("id");
+ String name = get("name");
+ result.add(new MusicFolder(id, name));
+ } else if ("error".equals(tag)) {
+ handleError();
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+
+ return result;
+ }
+
+} \ No newline at end of file
diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/PlayQueueParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/PlayQueueParser.java
new file mode 100644
index 00000000..ec161d2b
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/parser/PlayQueueParser.java
@@ -0,0 +1,85 @@
+/*
+ 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.service.parser;
+
+import android.content.Context;
+
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.Reader;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Locale;
+import java.util.TimeZone;
+
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.PlayerQueue;
+import github.daneren2005.dsub.util.ProgressListener;
+
+public class PlayQueueParser extends MusicDirectoryEntryParser {
+ private static final String TAG = PlayQueueParser.class.getSimpleName();
+
+ public PlayQueueParser(Context context, int instance) {
+ super(context, instance);
+ }
+
+ public PlayerQueue parse(Reader reader, ProgressListener progressListener) throws Exception {
+ init(reader);
+
+ PlayerQueue state = new PlayerQueue();
+ String currentId = null;
+ int eventType;
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = getElementName();
+ if("playQueue".equals(name)) {
+ currentId = get("current");
+ state.currentPlayingPosition = getInteger("position");
+ try {
+ SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH);
+ dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+ state.changed = dateFormat.parse(get("changed"));
+ } catch (ParseException e) {
+ state.changed = null;
+ }
+ } else if ("entry".equals(name)) {
+ MusicDirectory.Entry entry = parseEntry("");
+ // Only add songs
+ if(!entry.isVideo()) {
+ state.songs.add(entry);
+ }
+ } else if ("error".equals(name)) {
+ handleError();
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ if(currentId != null) {
+ for (MusicDirectory.Entry entry : state.songs) {
+ if (entry.getId().equals(currentId)) {
+ state.currentPlayingIndex = state.songs.indexOf(entry);
+ }
+ }
+ } else {
+ state.currentPlayingIndex = 0;
+ state.currentPlayingPosition = 0;
+ }
+
+ validate();
+ return state;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/PlaylistParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/PlaylistParser.java
new file mode 100644
index 00000000..5bb07dfd
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/parser/PlaylistParser.java
@@ -0,0 +1,63 @@
+/*
+ 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.service.parser;
+
+import android.content.Context;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.util.ProgressListener;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.Reader;
+
+/**
+ * @author Sindre Mehus
+ */
+public class PlaylistParser extends MusicDirectoryEntryParser {
+
+ public PlaylistParser(Context context, int instance) {
+ super(context, instance);
+ }
+
+ public MusicDirectory parse(Reader reader, ProgressListener progressListener) throws Exception {
+ init(reader);
+
+ MusicDirectory dir = new MusicDirectory();
+ int eventType;
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = getElementName();
+ if ("entry".equals(name)) {
+ dir.addChild(parseEntry(""));
+ } else if ("error".equals(name)) {
+ handleError();
+ } else if ("playlist".equals(name)) {
+ dir.setName(get("name"));
+ dir.setId(get("id"));
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+
+ return dir;
+ }
+
+} \ No newline at end of file
diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/PlaylistsParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/PlaylistsParser.java
new file mode 100644
index 00000000..6f01d510
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/parser/PlaylistsParser.java
@@ -0,0 +1,70 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.service.parser;
+
+import android.content.Context;
+
+import github.daneren2005.dsub.domain.Playlist;
+import github.daneren2005.dsub.util.ProgressListener;
+import github.daneren2005.dsub.adapter.PlaylistAdapter;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author Sindre Mehus
+ */
+public class PlaylistsParser extends AbstractParser {
+
+ public PlaylistsParser(Context context, int instance) {
+ super(context, instance);
+ }
+
+ public List<Playlist> parse(Reader reader, ProgressListener progressListener) throws Exception {
+ init(reader);
+
+ List<Playlist> result = new ArrayList<Playlist>();
+ int eventType;
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG) {
+ String tag = getElementName();
+ if ("playlist".equals(tag)) {
+ String id = get("id");
+ String name = get("name");
+ String owner = get("owner");
+ String comment = get("comment");
+ String songCount = get("songCount");
+ String created = get("created");
+ String pub = get("public");
+ result.add(new Playlist(id, name, owner, comment, songCount, created, pub));
+ } else if ("error".equals(tag)) {
+ handleError();
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+
+ return PlaylistAdapter.PlaylistComparator.sort(result);
+ }
+
+} \ No newline at end of file
diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/PodcastChannelParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/PodcastChannelParser.java
new file mode 100644
index 00000000..36ed17de
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/parser/PodcastChannelParser.java
@@ -0,0 +1,66 @@
+/*
+ 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.service.parser;
+
+import android.content.Context;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.PodcastChannel;
+import github.daneren2005.dsub.util.ProgressListener;
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.List;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ *
+ * @author Scott
+ */
+public class PodcastChannelParser extends AbstractParser {
+ public PodcastChannelParser(Context context, int instance) {
+ super(context, instance);
+ }
+
+ public List<PodcastChannel> parse(Reader reader, ProgressListener progressListener) throws Exception {
+ init(reader);
+
+ List<PodcastChannel> channels = new ArrayList<PodcastChannel>();
+ int eventType;
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = getElementName();
+ if ("channel".equals(name)) {
+ PodcastChannel channel = new PodcastChannel();
+ channel.setId(get("id"));
+ channel.setUrl(get("url"));
+ channel.setName(get("title"));
+ channel.setDescription(get("description"));
+ channel.setStatus(get("status"));
+ channel.setErrorMessage(get("errorMessage"));
+ channels.add(channel);
+ } else if ("error".equals(name)) {
+ handleError();
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+ return PodcastChannel.PodcastComparator.sort(channels, context);
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/PodcastEntryParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/PodcastEntryParser.java
new file mode 100644
index 00000000..00089363
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/parser/PodcastEntryParser.java
@@ -0,0 +1,112 @@
+/*
+ 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.service.parser;
+
+import android.content.Context;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.Bookmark;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.PodcastEpisode;
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.ProgressListener;
+import java.io.Reader;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ *
+ * @author Scott
+ */
+public class PodcastEntryParser extends AbstractParser {
+ private static int bogusId = -1;
+
+ public PodcastEntryParser(Context context, int instance) {
+ super(context, instance);
+ }
+
+ public MusicDirectory parse(String channel, Reader reader, ProgressListener progressListener) throws Exception {
+ init(reader);
+
+ MusicDirectory episodes = new MusicDirectory();
+ int eventType;
+ boolean valid = false;
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = getElementName();
+ if ("channel".equals(name)) {
+ String id = get("id");
+ if(id.equals(channel)) {
+ episodes.setId(id);
+ episodes.setName(get("title"));
+ valid = true;
+ } else {
+ valid = false;
+ }
+ }
+ else if ("episode".equals(name) && valid) {
+ PodcastEpisode episode = new PodcastEpisode();
+ episode.setEpisodeId(get("id"));
+ episode.setId(get("streamId"));
+ episode.setTitle(get("title"));
+ episode.setParent(episodes.getId());
+ episode.setArtist(episodes.getName());
+ episode.setAlbum(get("description"));
+ episode.setDate(get("publishDate"));
+ if(episode.getDate() == null) {
+ episode.setDate(get("created"));
+ }
+ if(episode.getDate() != null && episode.getDate().indexOf("T") != -1) {
+ episode.setDate(episode.getDate().replace("T", " "));
+ }
+ episode.setStatus(get("status"));
+ episode.setCoverArt(get("coverArt"));
+ episode.setSize(getLong("size"));
+ episode.setContentType(get("contentType"));
+ episode.setSuffix(get("suffix"));
+ episode.setDuration(getInteger("duration"));
+ episode.setBitRate(getInteger("bitRate"));
+ episode.setVideo(getBoolean("isVideo"));
+ episode.setPath(get("path"));
+ if(episode.getPath() == null) {
+ episode.setPath(FileUtil.getPodcastPath(context, episode));
+ } else if(episode.getPath().indexOf("Podcasts/") == 0) {
+ episode.setPath(episode.getPath().substring("Podcasts/".length()));
+ }
+
+ Integer bookmark = getInteger("bookmarkPosition");
+ if(bookmark != null) {
+ episode.setBookmark(new Bookmark(bookmark));
+ }
+ episode.setType(MusicDirectory.Entry.TYPE_PODCAST);
+
+ if(episode.getId() == null) {
+ episode.setId(String.valueOf(bogusId));
+ bogusId--;
+ }
+ episodes.addChild(episode);
+ } else if ("error".equals(name)) {
+ handleError();
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+ return episodes;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/RandomSongsParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/RandomSongsParser.java
new file mode 100644
index 00000000..37057723
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/parser/RandomSongsParser.java
@@ -0,0 +1,60 @@
+/*
+ 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.service.parser;
+
+import android.content.Context;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.util.ProgressListener;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.Reader;
+
+/**
+ * @author Sindre Mehus
+ */
+public class RandomSongsParser extends MusicDirectoryEntryParser {
+
+ public RandomSongsParser(Context context, int instance) {
+ super(context, instance);
+ }
+
+ public MusicDirectory parse(Reader reader, ProgressListener progressListener) throws Exception {
+ init(reader);
+
+ MusicDirectory dir = new MusicDirectory();
+ int eventType;
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = getElementName();
+ if ("song".equals(name)) {
+ dir.addChild(parseEntry(""));
+ } else if ("error".equals(name)) {
+ handleError();
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+
+ return dir;
+ }
+
+} \ No newline at end of file
diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/ScanStatusParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/ScanStatusParser.java
new file mode 100644
index 00000000..ffb3ba05
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/parser/ScanStatusParser.java
@@ -0,0 +1,56 @@
+/*
+ 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.service.parser;
+
+import android.content.Context;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.Reader;
+
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.util.ProgressListener;
+
+public class ScanStatusParser extends AbstractParser {
+
+ public ScanStatusParser(Context context, int instance) {
+ super(context, instance);
+ }
+
+ public boolean parse(Reader reader, ProgressListener progressListener) throws Exception {
+ init(reader);
+
+ Boolean started = null;
+ int eventType;
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = getElementName();
+ if("status".equals(name)) {
+ started = getBoolean("started");
+
+ String msg = context.getResources().getString(R.string.parser_scan_count, getInteger("count"));
+ progressListener.updateProgress(msg);
+ } else if ("error".equals(name)) {
+ handleError();
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+
+ return started != null && started;
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/SearchResult2Parser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/SearchResult2Parser.java
new file mode 100644
index 00000000..8cc0c50d
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/parser/SearchResult2Parser.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.service.parser;
+
+import android.content.Context;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.SearchResult;
+import github.daneren2005.dsub.domain.Artist;
+import github.daneren2005.dsub.util.ProgressListener;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.Reader;
+import java.util.List;
+import java.util.ArrayList;
+
+/**
+ * @author Sindre Mehus
+ */
+public class SearchResult2Parser extends MusicDirectoryEntryParser {
+
+ public SearchResult2Parser(Context context, int instance) {
+ super(context, instance);
+ }
+
+ public SearchResult parse(Reader reader, ProgressListener progressListener) throws Exception {
+ init(reader);
+
+ List<Artist> artists = new ArrayList<Artist>();
+ List<MusicDirectory.Entry> albums = new ArrayList<MusicDirectory.Entry>();
+ List<MusicDirectory.Entry> songs = new ArrayList<MusicDirectory.Entry>();
+ int eventType;
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = getElementName();
+ if ("artist".equals(name)) {
+ Artist artist = new Artist();
+ artist.setId(get("id"));
+ artist.setName(get("name"));
+ artists.add(artist);
+ } else if ("album".equals(name)) {
+ MusicDirectory.Entry entry = parseEntry("");
+ entry.setDirectory(true);
+ albums.add(entry);
+ } else if ("song".equals(name)) {
+ songs.add(parseEntry(""));
+ } else if ("error".equals(name)) {
+ handleError();
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+
+ return new SearchResult(artists, albums, songs);
+ }
+
+} \ No newline at end of file
diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/SearchResultParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/SearchResultParser.java
new file mode 100644
index 00000000..252a7b20
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/parser/SearchResultParser.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.service.parser;
+
+import android.content.Context;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.SearchResult;
+import github.daneren2005.dsub.domain.Artist;
+import github.daneren2005.dsub.util.ProgressListener;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.Reader;
+import java.util.Collections;
+import java.util.List;
+import java.util.ArrayList;
+
+/**
+ * @author Sindre Mehus
+ */
+public class SearchResultParser extends MusicDirectoryEntryParser {
+
+ public SearchResultParser(Context context, int instance) {
+ super(context, instance);
+ }
+
+ public SearchResult parse(Reader reader, ProgressListener progressListener) throws Exception {
+ init(reader);
+
+ List<MusicDirectory.Entry> songs = new ArrayList<MusicDirectory.Entry>();
+ int eventType;
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = getElementName();
+ if ("match".equals(name)) {
+ songs.add(parseEntry(""));
+ } else if ("error".equals(name)) {
+ handleError();
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+
+ return new SearchResult(Collections.<Artist>emptyList(), Collections.<MusicDirectory.Entry>emptyList(), songs);
+ }
+
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/ShareParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/ShareParser.java
new file mode 100644
index 00000000..418393d1
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/parser/ShareParser.java
@@ -0,0 +1,126 @@
+/*
+ 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.service.parser;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.Log;
+
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.ServerInfo;
+import github.daneren2005.dsub.domain.Share;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.ProgressListener;
+import github.daneren2005.dsub.util.Util;
+
+import org.xmlpull.v1.XmlPullParser;
+import java.io.Reader;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.TimeZone;
+
+/**
+ * @author Joshua Bahnsen
+ */
+public class ShareParser extends MusicDirectoryEntryParser {
+ private static final String TAG = ShareParser.class.getSimpleName();
+
+ public ShareParser(Context context, int instance) {
+ super(context, instance);
+ }
+
+ public List<Share> parse(Reader reader, ProgressListener progressListener) throws Exception {
+ init(reader);
+
+ List<Share> dir = new ArrayList<Share>();
+ Share share = null;
+ int eventType;
+
+ SharedPreferences prefs = Util.getPreferences(context);
+ int instance = prefs.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1);
+ String serverUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_URL + instance, null);
+ if(serverUrl.charAt(serverUrl.length() - 1) != '/') {
+ serverUrl += '/';
+ }
+ serverUrl += "share/";
+
+ boolean isDateNormalized = ServerInfo.checkServerVersion(context, "1.11");
+ SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH);
+ if(isDateNormalized) {
+ dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+ }
+
+ do {
+ eventType = nextParseEvent();
+
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = getElementName();
+
+ if ("share".equals(name)) {
+ share = new Share();
+
+ try {
+ share.setCreated(dateFormat.parse(get("created")));
+ } catch (Exception e) {
+ share.setCreated((Date) null);
+ }
+
+ String url = get("url");
+ if(url != null && url.indexOf(".php") == -1) {
+ url = url.replaceFirst(".*/([^/?]+).*", serverUrl + "$1");
+ }
+ share.setUrl(url);
+
+ share.setDescription(get("description"));
+
+ try {
+ share.setExpires(dateFormat.parse(get("expires")));
+ } catch (Exception e) {
+ share.setExpires((Date) null);
+ }
+ share.setId(get("id"));
+
+ try {
+ share.setLastVisited(dateFormat.parse(get("lastVisited")));
+ } catch (Exception e) {
+ share.setLastVisited((Date) null);
+ }
+
+ share.setUsername(get("username"));
+ share.setVisitCount(getLong("visitCount"));
+ dir.add(share);
+ } else if ("entry".equals(name)) {
+ if(share != null) {
+ share.addEntry(parseEntry(null));
+ }
+ } else if ("error".equals(name)) {
+ handleError();
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+
+ return dir;
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/StarredListParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/StarredListParser.java
new file mode 100644
index 00000000..59652e29
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/parser/StarredListParser.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.service.parser;
+
+import android.content.Context;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.util.ProgressListener;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.Reader;
+
+/**
+ * @author Kurt Hardin
+ */
+public class StarredListParser extends MusicDirectoryEntryParser {
+
+ public StarredListParser(Context context, int instance) {
+ super(context, instance);
+ }
+
+ public MusicDirectory parse(Reader reader, ProgressListener progressListener) throws Exception {
+ init(reader);
+
+ MusicDirectory dir = new MusicDirectory();
+ int eventType;
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = getElementName();
+ if ("album".equals(name) || "song".equals(name)) {
+ MusicDirectory.Entry entry = parseEntry("");
+ if("album".equals(name)) {
+ entry.setDirectory(true);
+ }
+ dir.addChild(entry);
+ } else if("artist".equals(name)) {
+ MusicDirectory.Entry entry = parseArtist();
+ entry.setDirectory(true);
+ entry.setArtist(null);
+ entry.setParent(null);
+ dir.addChild(entry);
+ } else if ("error".equals(name)) {
+ handleError();
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+
+ return dir;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/SubsonicRESTException.java b/app/src/main/java/github/daneren2005/dsub/service/parser/SubsonicRESTException.java
new file mode 100644
index 00000000..096597a1
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/parser/SubsonicRESTException.java
@@ -0,0 +1,19 @@
+package github.daneren2005.dsub.service.parser;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class SubsonicRESTException extends Exception {
+
+ private final int code;
+
+ public SubsonicRESTException(int code, String message) {
+ super(message);
+ this.code = code;
+ }
+
+ public int getCode() {
+ return code;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/UserParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/UserParser.java
new file mode 100644
index 00000000..e20556c0
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/parser/UserParser.java
@@ -0,0 +1,73 @@
+/*
+ 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.service.parser;
+
+import android.content.Context;
+
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.List;
+
+import github.daneren2005.dsub.domain.User;
+import github.daneren2005.dsub.util.ProgressListener;
+
+public class UserParser extends AbstractParser {
+
+ public UserParser(Context context, int instance) {
+ super(context, instance);
+ }
+
+ public List<User> parse(Reader reader, ProgressListener progressListener) throws Exception {
+ init(reader);
+ List<User> result = new ArrayList<User>();
+ int eventType;
+
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = getElementName();
+ if ("user".equals(name)) {
+ User user = new User();
+
+ user.setUsername(get("username"));
+ user.setEmail(get("email"));
+ parseSetting(user, User.SCROBBLING);
+ for(String role: User.ROLES) {
+ parseSetting(user, role);
+ }
+ parseSetting(user, User.LASTFM);
+
+ result.add(user);
+ } else if ("error".equals(name)) {
+ handleError();
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+
+ return result;
+ }
+
+ private void parseSetting(User user, String name) {
+ String value = get(name);
+ if(value != null) {
+ user.addSetting(name, "true".equals(value));
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/VideosParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/VideosParser.java
new file mode 100644
index 00000000..f22c4a4a
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/parser/VideosParser.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 2015 (C) Scott Jackson
+*/
+
+package github.daneren2005.dsub.service.parser;
+
+import android.content.Context;
+
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.Reader;
+
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.util.ProgressListener;
+
+public class VideosParser extends MusicDirectoryEntryParser {
+ public VideosParser(Context context, int instance) {
+ super(context, instance);
+ }
+
+ public MusicDirectory parse(Reader reader, ProgressListener progressListener) throws Exception {
+ init(reader);
+
+ MusicDirectory dir = new MusicDirectory();
+ int eventType;
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = getElementName();
+ if ("video".equals(name)) {
+ MusicDirectory.Entry entry = parseEntry("");
+ dir.addChild(entry);
+ } else if ("error".equals(name)) {
+ handleError();
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+ return dir;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/ssl/SSLSocketFactory.java b/app/src/main/java/github/daneren2005/dsub/service/ssl/SSLSocketFactory.java
new file mode 100644
index 00000000..3b1203c7
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/ssl/SSLSocketFactory.java
@@ -0,0 +1,549 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package github.daneren2005.dsub.service.ssl;
+
+import android.util.Log;
+
+import org.apache.http.conn.ConnectTimeoutException;
+import org.apache.http.conn.scheme.HostNameResolver;
+import org.apache.http.conn.scheme.LayeredSocketFactory;
+import org.apache.http.conn.ssl.AllowAllHostnameVerifier;
+import org.apache.http.conn.ssl.BrowserCompatHostnameVerifier;
+import org.apache.http.conn.ssl.StrictHostnameVerifier;
+import org.apache.http.conn.ssl.X509HostnameVerifier;
+import org.apache.http.params.HttpConnectionParams;
+import org.apache.http.params.HttpParams;
+
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509TrustManager;
+
+import java.io.IOException;
+import java.lang.reflect.Array;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.SocketTimeoutException;
+import java.net.UnknownHostException;
+import java.security.KeyManagementException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.Provider;
+import java.security.SecureRandom;
+import java.security.Security;
+import java.security.UnrecoverableKeyException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Layered socket factory for TLS/SSL connections.
+ * <p>
+ * SSLSocketFactory can be used to validate the identity of the HTTPS server against a list of
+ * trusted certificates and to authenticate to the HTTPS server using a private key.
+ * <p>
+ * SSLSocketFactory will enable server authentication when supplied with
+ * a {@link KeyStore trust-store} file containing one or several trusted certificates. The client
+ * secure socket will reject the connection during the SSL session handshake if the target HTTPS
+ * server attempts to authenticate itself with a non-trusted certificate.
+ * <p>
+ * Use JDK keytool utility to import a trusted certificate and generate a trust-store file:
+ * <pre>
+ * keytool -import -alias "my server cert" -file server.crt -keystore my.truststore
+ * </pre>
+ * <p>
+ * In special cases the standard trust verification process can be bypassed by using a custom
+ * {@link TrustStrategy}. This interface is primarily intended for allowing self-signed
+ * certificates to be accepted as trusted without having to add them to the trust-store file.
+ * <p>
+ * The following parameters can be used to customize the behavior of this
+ * class:
+ * <ul>
+ * <li>{@link org.apache.http.params.CoreConnectionPNames#CONNECTION_TIMEOUT}</li>
+ * <li>{@link org.apache.http.params.CoreConnectionPNames#SO_TIMEOUT}</li>
+ * </ul>
+ * <p>
+ * SSLSocketFactory will enable client authentication when supplied with
+ * a {@link KeyStore key-store} file containing a private key/public certificate
+ * pair. The client secure socket will use the private key to authenticate
+ * itself to the target HTTPS server during the SSL session handshake if
+ * requested to do so by the server.
+ * The target HTTPS server will in its turn verify the certificate presented
+ * by the client in order to establish client's authenticity
+ * <p>
+ * Use the following sequence of actions to generate a key-store file
+ * </p>
+ * <ul>
+ * <li>
+ * <p>
+ * Use JDK keytool utility to generate a new key
+ * <pre>keytool -genkey -v -alias "my client key" -validity 365 -keystore my.keystore</pre>
+ * For simplicity use the same password for the key as that of the key-store
+ * </p>
+ * </li>
+ * <li>
+ * <p>
+ * Issue a certificate signing request (CSR)
+ * <pre>keytool -certreq -alias "my client key" -file mycertreq.csr -keystore my.keystore</pre>
+ * </p>
+ * </li>
+ * <li>
+ * <p>
+ * Send the certificate request to the trusted Certificate Authority for signature.
+ * One may choose to act as her own CA and sign the certificate request using a PKI
+ * tool, such as OpenSSL.
+ * </p>
+ * </li>
+ * <li>
+ * <p>
+ * Import the trusted CA root certificate
+ * <pre>keytool -import -alias "my trusted ca" -file caroot.crt -keystore my.keystore</pre>
+ * </p>
+ * </li>
+ * <li>
+ * <p>
+ * Import the PKCS#7 file containg the complete certificate chain
+ * <pre>keytool -import -alias "my client key" -file mycert.p7 -keystore my.keystore</pre>
+ * </p>
+ * </li>
+ * <li>
+ * <p>
+ * Verify the content the resultant keystore file
+ * <pre>keytool -list -v -keystore my.keystore</pre>
+ * </p>
+ * </li>
+ * </ul>
+ *
+ * @since 4.0
+ */
+public class SSLSocketFactory implements LayeredSocketFactory {
+ private static final String TAG = SSLSocketFactory.class.getSimpleName();
+ public static final String TLS = "TLS";
+
+ public static final X509HostnameVerifier ALLOW_ALL_HOSTNAME_VERIFIER
+ = new AllowAllHostnameVerifier();
+
+ public static final X509HostnameVerifier BROWSER_COMPATIBLE_HOSTNAME_VERIFIER
+ = new BrowserCompatHostnameVerifier();
+
+ public static final X509HostnameVerifier STRICT_HOSTNAME_VERIFIER
+ = new StrictHostnameVerifier();
+
+ /**
+ * The default factory using the default JVM settings for secure connections.
+ */
+ private static final SSLSocketFactory DEFAULT_FACTORY = new SSLSocketFactory();
+
+ /**
+ * Gets the default factory, which uses the default JVM settings for secure
+ * connections.
+ *
+ * @return the default factory
+ */
+ public static SSLSocketFactory getSocketFactory() {
+ return DEFAULT_FACTORY;
+ }
+
+ private final javax.net.ssl.SSLSocketFactory socketfactory;
+ private final HostNameResolver nameResolver;
+ // TODO: make final
+ private volatile X509HostnameVerifier hostnameVerifier;
+
+ private static SSLContext createSSLContext(
+ String algorithm,
+ final KeyStore keystore,
+ final String keystorePassword,
+ final KeyStore truststore,
+ final SecureRandom random,
+ final TrustStrategy trustStrategy)
+ throws NoSuchAlgorithmException, KeyStoreException, UnrecoverableKeyException, KeyManagementException {
+ if (algorithm == null) {
+ algorithm = TLS;
+ }
+ KeyManagerFactory kmfactory = KeyManagerFactory.getInstance(
+ KeyManagerFactory.getDefaultAlgorithm());
+ kmfactory.init(keystore, keystorePassword != null ? keystorePassword.toCharArray(): null);
+ KeyManager[] keymanagers = kmfactory.getKeyManagers();
+ TrustManagerFactory tmfactory = TrustManagerFactory.getInstance(
+ TrustManagerFactory.getDefaultAlgorithm());
+ tmfactory.init(keystore);
+ TrustManager[] trustmanagers = tmfactory.getTrustManagers();
+ if (trustmanagers != null && trustStrategy != null) {
+ for (int i = 0; i < trustmanagers.length; i++) {
+ TrustManager tm = trustmanagers[i];
+ if (tm instanceof X509TrustManager) {
+ trustmanagers[i] = new TrustManagerDecorator(
+ (X509TrustManager) tm, trustStrategy);
+ }
+ }
+ }
+
+ SSLContext sslcontext = SSLContext.getInstance(algorithm);
+ sslcontext.init(keymanagers, trustmanagers, random);
+ return sslcontext;
+ }
+
+ /**
+ * @deprecated Use {@link #SSLSocketFactory(String, KeyStore, String, KeyStore, SecureRandom, X509HostnameVerifier)}
+ */
+ @Deprecated
+ public SSLSocketFactory(
+ final String algorithm,
+ final KeyStore keystore,
+ final String keystorePassword,
+ final KeyStore truststore,
+ final SecureRandom random,
+ final HostNameResolver nameResolver)
+ throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
+ this(createSSLContext(
+ algorithm, keystore, keystorePassword, truststore, random, null),
+ nameResolver);
+ }
+
+ /**
+ * @since 4.1
+ */
+ public SSLSocketFactory(
+ String algorithm,
+ final KeyStore keystore,
+ final String keystorePassword,
+ final KeyStore truststore,
+ final SecureRandom random,
+ final X509HostnameVerifier hostnameVerifier)
+ throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
+ this(createSSLContext(
+ algorithm, keystore, keystorePassword, truststore, random, null),
+ hostnameVerifier);
+ }
+
+ /**
+ * @since 4.1
+ */
+ public SSLSocketFactory(
+ String algorithm,
+ final KeyStore keystore,
+ final String keystorePassword,
+ final KeyStore truststore,
+ final SecureRandom random,
+ final TrustStrategy trustStrategy,
+ final X509HostnameVerifier hostnameVerifier)
+ throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
+ this(createSSLContext(
+ algorithm, keystore, keystorePassword, truststore, random, trustStrategy),
+ hostnameVerifier);
+ }
+
+ public SSLSocketFactory(
+ final KeyStore keystore,
+ final String keystorePassword,
+ final KeyStore truststore)
+ throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
+ this(TLS, keystore, keystorePassword, truststore, null, null, BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
+ }
+
+ public SSLSocketFactory(
+ final KeyStore keystore,
+ final String keystorePassword)
+ throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException{
+ this(TLS, keystore, keystorePassword, null, null, null, BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
+ }
+
+ public SSLSocketFactory(
+ final KeyStore truststore)
+ throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
+ this(TLS, null, null, truststore, null, null, BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
+ }
+
+ /**
+ * @since 4.1
+ */
+ public SSLSocketFactory(
+ final TrustStrategy trustStrategy,
+ final X509HostnameVerifier hostnameVerifier)
+ throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
+ this(TLS, null, null, null, null, trustStrategy, hostnameVerifier);
+ }
+
+ /**
+ * @since 4.1
+ */
+ public SSLSocketFactory(
+ final TrustStrategy trustStrategy)
+ throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
+ this(TLS, null, null, null, null, trustStrategy, BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
+ }
+
+ public SSLSocketFactory(final SSLContext sslContext) {
+ this(sslContext, BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
+ }
+
+ /**
+ * @deprecated Use {@link #SSLSocketFactory(SSLContext)}
+ */
+ @Deprecated
+ public SSLSocketFactory(
+ final SSLContext sslContext, final HostNameResolver nameResolver) {
+ super();
+ this.socketfactory = sslContext.getSocketFactory();
+ this.hostnameVerifier = BROWSER_COMPATIBLE_HOSTNAME_VERIFIER;
+ this.nameResolver = nameResolver;
+ }
+
+ /**
+ * @since 4.1
+ */
+ public SSLSocketFactory(
+ final SSLContext sslContext, final X509HostnameVerifier hostnameVerifier) {
+ super();
+ this.socketfactory = sslContext.getSocketFactory();
+ this.hostnameVerifier = hostnameVerifier;
+ this.nameResolver = null;
+ }
+
+ private SSLSocketFactory() {
+ super();
+ this.socketfactory = HttpsURLConnection.getDefaultSSLSocketFactory();
+ this.hostnameVerifier = null;
+ this.nameResolver = null;
+ }
+
+ /**
+ * @param params Optional parameters. Parameters passed to this method will have no effect.
+ * This method will create a unconnected instance of {@link Socket} class
+ * using {@link javax.net.ssl.SSLSocketFactory#createSocket()} method.
+ * @since 4.1
+ */
+ @SuppressWarnings("cast")
+ public Socket createSocket(final HttpParams params) throws IOException {
+ // the cast makes sure that the factory is working as expected
+ SSLSocket sslSocket = (SSLSocket) this.socketfactory.createSocket();
+ sslSocket.setEnabledProtocols(getProtocols(sslSocket));
+ sslSocket.setEnabledCipherSuites(getCiphers(sslSocket));
+ return sslSocket;
+ }
+
+ @SuppressWarnings("cast")
+ public Socket createSocket() throws IOException {
+ // the cast makes sure that the factory is working as expected
+ SSLSocket sslSocket = (SSLSocket) this.socketfactory.createSocket();
+ sslSocket.setEnabledProtocols(getProtocols(sslSocket));
+ sslSocket.setEnabledCipherSuites(getCiphers(sslSocket));
+ return sslSocket;
+ }
+
+ /**
+ * @since 4.1
+ */
+ public Socket connectSocket(
+ final Socket sock,
+ final InetSocketAddress remoteAddress,
+ final InetSocketAddress localAddress,
+ final HttpParams params) throws IOException, UnknownHostException, ConnectTimeoutException {
+ if (remoteAddress == null) {
+ throw new IllegalArgumentException("Remote address may not be null");
+ }
+ if (params == null) {
+ throw new IllegalArgumentException("HTTP parameters may not be null");
+ }
+ SSLSocket sslsock = (SSLSocket) (sock != null ? sock : createSocket());
+ if (localAddress != null) {
+// sslsock.setReuseAddress(HttpConnectionParams.getSoReuseaddr(params));
+ sslsock.bind(localAddress);
+ }
+
+ setHostName(sslsock, remoteAddress.getHostName());
+ int connTimeout = HttpConnectionParams.getConnectionTimeout(params);
+ int soTimeout = HttpConnectionParams.getSoTimeout(params);
+
+ try {
+ sslsock.connect(remoteAddress, connTimeout);
+ } catch (SocketTimeoutException ex) {
+ throw new ConnectTimeoutException("Connect to " + remoteAddress.getHostName() + "/"
+ + remoteAddress.getAddress() + " timed out");
+ }
+ sslsock.setSoTimeout(soTimeout);
+ if (this.hostnameVerifier != null) {
+ try {
+ this.hostnameVerifier.verify(remoteAddress.getHostName(), sslsock);
+ // verifyHostName() didn't blowup - good!
+ } catch (IOException iox) {
+ // close the socket before re-throwing the exception
+ try { sslsock.close(); } catch (Exception x) { /*ignore*/ }
+ throw iox;
+ }
+ }
+ return sslsock;
+ }
+
+
+ /**
+ * Checks whether a socket connection is secure.
+ * This factory creates TLS/SSL socket connections
+ * which, by default, are considered secure.
+ * <br/>
+ * Derived classes may override this method to perform
+ * runtime checks, for example based on the cypher suite.
+ *
+ * @param sock the connected socket
+ *
+ * @return <code>true</code>
+ *
+ * @throws IllegalArgumentException if the argument is invalid
+ */
+ public boolean isSecure(final Socket sock) throws IllegalArgumentException {
+ if (sock == null) {
+ throw new IllegalArgumentException("Socket may not be null");
+ }
+ // This instanceof check is in line with createSocket() above.
+ if (!(sock instanceof SSLSocket)) {
+ throw new IllegalArgumentException("Socket not created by this factory");
+ }
+ // This check is performed last since it calls the argument object.
+ if (sock.isClosed()) {
+ throw new IllegalArgumentException("Socket is closed");
+ }
+ return true;
+ }
+
+ /**
+ * @since 4.1
+ */
+ public Socket createLayeredSocket(
+ final Socket socket,
+ final String host,
+ final int port,
+ final boolean autoClose) throws IOException, UnknownHostException {
+ SSLSocket sslSocket = (SSLSocket) this.socketfactory.createSocket(
+ socket,
+ host,
+ port,
+ autoClose
+ );
+ sslSocket.setEnabledProtocols(getProtocols(sslSocket));
+ sslSocket.setEnabledCipherSuites(getCiphers(sslSocket));
+ if (this.hostnameVerifier != null) {
+ this.hostnameVerifier.verify(host, sslSocket);
+ }
+ // verifyHostName() didn't blowup - good!
+ return sslSocket;
+ }
+
+ @Deprecated
+ public void setHostnameVerifier(X509HostnameVerifier hostnameVerifier) {
+ if ( hostnameVerifier == null ) {
+ throw new IllegalArgumentException("Hostname verifier may not be null");
+ }
+ this.hostnameVerifier = hostnameVerifier;
+ }
+
+ public X509HostnameVerifier getHostnameVerifier() {
+ return this.hostnameVerifier;
+ }
+
+ /**
+ * @deprecated Use {@link #connectSocket(Socket, InetSocketAddress, InetSocketAddress, HttpParams)}
+ */
+ @Deprecated
+ public Socket connectSocket(
+ final Socket socket,
+ final String host, int port,
+ final InetAddress localAddress, int localPort,
+ final HttpParams params) throws IOException, UnknownHostException, ConnectTimeoutException {
+ InetSocketAddress local = null;
+ if (localAddress != null || localPort > 0) {
+ // we need to bind explicitly
+ if (localPort < 0) {
+ localPort = 0; // indicates "any"
+ }
+ local = new InetSocketAddress(localAddress, localPort);
+ }
+ InetAddress remoteAddress;
+ if (this.nameResolver != null) {
+ remoteAddress = this.nameResolver.resolve(host);
+ } else {
+ remoteAddress = InetAddress.getByName(host);
+ }
+ InetSocketAddress remote = new InetSocketAddress(remoteAddress, port);
+ return connectSocket(socket, remote, local, params);
+ }
+
+ /**
+ * @deprecated Use {@link #createLayeredSocket(Socket, String, int, boolean)}
+ */
+ @Deprecated
+ public Socket createSocket(
+ final Socket socket,
+ final String host, int port,
+ boolean autoClose) throws IOException, UnknownHostException {
+ SSLSocket sslSocket = (SSLSocket) this.socketfactory.createSocket(socket, host, port, autoClose);
+ sslSocket.setEnabledProtocols(getProtocols(sslSocket));
+ sslSocket.setEnabledCipherSuites(getCiphers(sslSocket));
+ setHostName(sslSocket, host);
+ return sslSocket;
+ }
+
+ private void setHostName(SSLSocket sslsock, String hostname){
+ try {
+ java.lang.reflect.Method setHostnameMethod = sslsock.getClass().getMethod("setHostname", String.class);
+ setHostnameMethod.invoke(sslsock, hostname);
+ } catch (Exception e) {
+ Log.w(TAG, "SNI not useable", e);
+ }
+ }
+
+ private String[] getProtocols(SSLSocket sslSocket) {
+ String[] protocols = sslSocket.getEnabledProtocols();
+
+ // Remove SSLv3 if it is not the only option
+ if(protocols.length > 1) {
+ List<String> protocolList = new ArrayList(Arrays.asList(protocols));
+ protocolList.remove("SSLv3");
+ protocols = protocolList.toArray(new String[protocolList.size()]);
+ }
+
+ return protocols;
+ }
+
+ private String[] getCiphers(SSLSocket sslSocket) {
+ String[] ciphers = sslSocket.getEnabledCipherSuites();
+
+ List<String> enabledCiphers = new ArrayList(Arrays.asList(ciphers));
+ // On Android 5.0 release, Jetty doesn't seem to play nice with these ciphers
+ enabledCiphers.remove("TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA");
+ enabledCiphers.remove("TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA");
+
+ ciphers = enabledCiphers.toArray(new String[enabledCiphers.size()]);
+ return ciphers;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/ssl/TrustManagerDecorator.java b/app/src/main/java/github/daneren2005/dsub/service/ssl/TrustManagerDecorator.java
new file mode 100644
index 00000000..f2364368
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/ssl/TrustManagerDecorator.java
@@ -0,0 +1,65 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package github.daneren2005.dsub.service.ssl;
+
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+
+import javax.net.ssl.X509TrustManager;
+
+
+/**
+ * @since 4.1
+ */
+class TrustManagerDecorator implements X509TrustManager {
+
+ private final X509TrustManager trustManager;
+ private final TrustStrategy trustStrategy;
+
+ TrustManagerDecorator(final X509TrustManager trustManager, final TrustStrategy trustStrategy) {
+ super();
+ this.trustManager = trustManager;
+ this.trustStrategy = trustStrategy;
+ }
+
+ public void checkClientTrusted(
+ final X509Certificate[] chain, final String authType) throws CertificateException {
+ this.trustManager.checkClientTrusted(chain, authType);
+ }
+
+ public void checkServerTrusted(
+ final X509Certificate[] chain, final String authType) throws CertificateException {
+ if (!this.trustStrategy.isTrusted(chain, authType)) {
+ this.trustManager.checkServerTrusted(chain, authType);
+ }
+ }
+
+ public X509Certificate[] getAcceptedIssuers() {
+ return this.trustManager.getAcceptedIssuers();
+ }
+
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/ssl/TrustSelfSignedStrategy.java b/app/src/main/java/github/daneren2005/dsub/service/ssl/TrustSelfSignedStrategy.java
new file mode 100644
index 00000000..637a8931
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/ssl/TrustSelfSignedStrategy.java
@@ -0,0 +1,44 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package github.daneren2005.dsub.service.ssl;
+
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+
+/**
+ * A trust strategy that accepts self-signed certificates as trusted. Verification of all other
+ * certificates is done by the trust manager configured in the SSL context.
+ *
+ * @since 4.1
+ */
+public class TrustSelfSignedStrategy implements TrustStrategy {
+
+ public boolean isTrusted(final X509Certificate[] chain, final String authType) throws CertificateException {
+ return true;
+ }
+
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/ssl/TrustStrategy.java b/app/src/main/java/github/daneren2005/dsub/service/ssl/TrustStrategy.java
new file mode 100644
index 00000000..334a97c5
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/ssl/TrustStrategy.java
@@ -0,0 +1,57 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package github.daneren2005.dsub.service.ssl;
+
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+
+/**
+ * A strategy to establish trustworthiness of certificates without consulting the trust manager
+ * configured in the actual SSL context. This interface can be used to override the standard
+ * JSSE certificate verification process.
+ *
+ * @since 4.1
+ */
+public interface TrustStrategy {
+
+ /**
+ * Determines whether the certificate chain can be trusted without consulting the trust manager
+ * configured in the actual SSL context. This method can be used to override the standard JSSE
+ * certificate verification process.
+ * <p>
+ * Please note that, if this method returns <code>false</code>, the trust manager configured
+ * in the actual SSL context can still clear the certificate as trusted.
+ *
+ * @param chain the peer certificate chain
+ * @param authType the authentication type based on the client certificate
+ * @return <code>true</code> if the certificate can be trusted without verification by
+ * the trust manager, <code>false</code> otherwise.
+ * @throws CertificateException thrown if the certificate is not trusted or invalid.
+ */
+ boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException;
+
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/sync/AuthenticatorService.java b/app/src/main/java/github/daneren2005/dsub/service/sync/AuthenticatorService.java
new file mode 100644
index 00000000..89fccb91
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/sync/AuthenticatorService.java
@@ -0,0 +1,90 @@
+/*
+ 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.service.sync;
+
+import android.accounts.AbstractAccountAuthenticator;
+import android.accounts.Account;
+import android.accounts.AccountAuthenticatorResponse;
+import android.accounts.NetworkErrorException;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+
+/**
+ * Created by Scott on 8/28/13.
+ */
+
+public class AuthenticatorService extends Service {
+ private SubsonicAuthenticator authenticator;
+
+ @Override
+ public void onCreate() {
+ authenticator = new SubsonicAuthenticator(this);
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return authenticator.getIBinder();
+
+ }
+
+ private class SubsonicAuthenticator extends AbstractAccountAuthenticator {
+ public SubsonicAuthenticator(Context context) {
+ super(context);
+ }
+
+ @Override
+ public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
+ return null;
+ }
+
+ @Override
+ public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options) throws NetworkErrorException {
+ return null;
+ }
+
+ @Override
+ public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options) throws NetworkErrorException {
+ return null;
+ }
+
+ @Override
+ public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException {
+ return null;
+ }
+
+ @Override
+ public String getAuthTokenLabel(String authTokenType) {
+ return null;
+ }
+
+ @Override
+ public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException {
+ return null;
+ }
+
+ @Override
+ public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account, String[] features) throws NetworkErrorException {
+ return null;
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/sync/MostRecentSyncAdapter.java b/app/src/main/java/github/daneren2005/dsub/service/sync/MostRecentSyncAdapter.java
new file mode 100644
index 00000000..f7a8634e
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/sync/MostRecentSyncAdapter.java
@@ -0,0 +1,105 @@
+/*
+ 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.service.sync;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.util.Log;
+
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.List;
+
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.PodcastEpisode;
+import github.daneren2005.dsub.service.DownloadFile;
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.Notifications;
+import github.daneren2005.dsub.util.SyncUtil;
+import github.daneren2005.dsub.util.SyncUtil.SyncSet;
+import github.daneren2005.dsub.util.Util;
+
+/**
+ * Created by Scott on 8/28/13.
+ */
+
+public class MostRecentSyncAdapter extends SubsonicSyncAdapter {
+ private static String TAG = MostRecentSyncAdapter.class.getSimpleName();
+
+ public MostRecentSyncAdapter(Context context, boolean autoInitialize) {
+ super(context, autoInitialize);
+ }
+ @TargetApi(14)
+ public MostRecentSyncAdapter(Context context, boolean autoInitialize, boolean allowParallelSyncs) {
+ super(context, autoInitialize, allowParallelSyncs);
+ }
+
+ @Override
+ public void onExecuteSync(Context context, int instance) {
+ try {
+ ArrayList<String> syncedList = SyncUtil.getSyncedMostRecent(context, instance);
+ MusicDirectory albumList = musicService.getAlbumList("newest", 20, 0, context, null);
+ List<String> updated = new ArrayList<String>();
+ boolean firstRun = false;
+ if(syncedList.size() == 0) {
+ // Get the initial set of albums on first run, don't sync any of these!
+ for(MusicDirectory.Entry album: albumList.getChildren()) {
+ syncedList.add(album.getId());
+ }
+ firstRun = true;
+ } else {
+ for(MusicDirectory.Entry album: albumList.getChildren()) {
+ if(!syncedList.contains(album.getId())) {
+ if(!"Podcast".equals(album.getGenre())) {
+ try {
+ if(downloadRecursively(null, getMusicDirectory(album), context, false)) {
+ updated.add(album.getTitle());
+ }
+ } catch(Exception e) {
+ Log.w(TAG, "Failed to get songs for " + album.getId() + " on " + Util.getServerName(context, instance));
+ }
+ }
+ syncedList.add(album.getId());
+ }
+ }
+ }
+
+ if(updated.size() > 0) {
+ while(syncedList.size() > 40) {
+ syncedList.remove(0);
+ }
+
+ FileUtil.serialize(context, syncedList, SyncUtil.getMostRecentSyncFile(context, instance));
+
+ // If there is a new album on the active server, chances are artists need to be refreshed
+ if(Util.getActiveServer(context) == instance) {
+ musicService.getIndexes(Util.getSelectedMusicFolderId(context), true, context, null);
+ }
+
+ Notifications.showSyncNotification(context, R.string.sync_new_albums, SyncUtil.joinNames(updated));
+ } else if(firstRun) {
+ FileUtil.serialize(context, syncedList, SyncUtil.getMostRecentSyncFile(context, instance));
+ }
+ } catch(Exception e) {
+ Log.e(TAG, "Failed to get most recent list for " + Util.getServerName(context, instance));
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/sync/MostRecentSyncService.java b/app/src/main/java/github/daneren2005/dsub/service/sync/MostRecentSyncService.java
new file mode 100644
index 00000000..49ea4a0e
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/sync/MostRecentSyncService.java
@@ -0,0 +1,48 @@
+/*
+ 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.service.sync;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+
+/**
+ * Created by Scott on 8/28/13.
+ */
+
+public class MostRecentSyncService extends Service {
+ private static MostRecentSyncAdapter mostRecentSyncAdapter;
+ private static final Object syncLock = new Object();
+
+ @Override
+ public void onCreate() {
+ synchronized (syncLock) {
+ if(mostRecentSyncAdapter == null) {
+ mostRecentSyncAdapter = new MostRecentSyncAdapter(getApplicationContext(), true);
+ }
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mostRecentSyncAdapter.getSyncAdapterBinder();
+
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/sync/PlaylistSyncAdapter.java b/app/src/main/java/github/daneren2005/dsub/service/sync/PlaylistSyncAdapter.java
new file mode 100644
index 00000000..a0996628
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/sync/PlaylistSyncAdapter.java
@@ -0,0 +1,153 @@
+/*
+ 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.service.sync;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.util.Log;
+
+import java.io.File;
+import java.util.ArrayList;
+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.service.DownloadFile;
+import github.daneren2005.dsub.service.parser.SubsonicRESTException;
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.Notifications;
+import github.daneren2005.dsub.util.SyncUtil;
+import github.daneren2005.dsub.util.SyncUtil.SyncSet;
+import github.daneren2005.dsub.util.Util;
+
+/**
+ * Created by Scott on 8/28/13.
+*/
+
+public class PlaylistSyncAdapter extends SubsonicSyncAdapter {
+ private static String TAG = PlaylistSyncAdapter.class.getSimpleName();
+ // Update playlists every day to make sure they are still valid
+ private static int MAX_PLAYLIST_AGE = 24;
+
+ public PlaylistSyncAdapter(Context context, boolean autoInitialize) {
+ super(context, autoInitialize);
+ }
+ @TargetApi(14)
+ public PlaylistSyncAdapter(Context context, boolean autoInitialize, boolean allowParallelSyncs) {
+ super(context, autoInitialize, allowParallelSyncs);
+ }
+
+ @Override
+ public void onExecuteSync(Context context, int instance) {
+ String serverName = Util.getServerName(context, instance);
+
+ List<Playlist> remainder = null;
+ try {
+ // Just update playlist listings so user doesn't have to
+ remainder = musicService.getPlaylists(true, context, null);
+ } catch(Exception e) {
+ Log.e(TAG, "Failed to refresh playlist list for " + serverName);
+ }
+
+ ArrayList<SyncSet> playlistList = SyncUtil.getSyncedPlaylists(context, instance);
+ List<String> updated = new ArrayList<String>();
+ boolean removed = false;
+ for(int i = 0; i < playlistList.size(); i++) {
+ SyncSet cachedPlaylist = playlistList.get(i);
+ String id = cachedPlaylist.id;
+
+ // Remove playlist from remainder list
+ if(remainder != null) {
+ remainder.remove(new Playlist(id, ""));
+ }
+
+ try {
+ MusicDirectory playlist = musicService.getPlaylist(true, id, serverName, context, null);
+
+ // Get list of original paths
+ List<String> origPathList = new ArrayList<String>();
+ if(cachedPlaylist.synced != null) {
+ origPathList.addAll(cachedPlaylist.synced);
+ } else {
+ cachedPlaylist.synced = new ArrayList<String>();
+ }
+
+ for(MusicDirectory.Entry entry: playlist.getChildren()) {
+ DownloadFile file = new DownloadFile(context, entry, true);
+ String path = file.getCompleteFile().getPath();
+ while(!file.isSaved() && !file.isFailedMax()) {
+ file.downloadNow(musicService);
+ if(file.isSaved() && !updated.contains(playlist.getName())) {
+ updated.add(playlist.getName());
+ }
+ }
+
+ // Add to cached path set if saved
+ if(file.isSaved() && !cachedPlaylist.synced.contains(path)) {
+ cachedPlaylist.synced.add(path);
+ }
+
+ origPathList.remove(path);
+ }
+
+ // Check to unpin all paths which are no longer in playlist
+ if(origPathList.size() > 0) {
+ for(String path: origPathList) {
+ File saveFile = new File(path);
+ FileUtil.unpinSong(context, saveFile);
+ cachedPlaylist.synced.remove(path);
+ }
+
+ removed = true;
+ }
+ } catch(SubsonicRESTException e) {
+ if(e.getCode() == 70) {
+ SyncUtil.removeSyncedPlaylist(context, id, instance);
+ Log.i(TAG, "Unsync deleted playlist " + id + " for " + serverName);
+ }
+ } catch(Exception e) {
+ Log.e(TAG, "Failed to get playlist " + id + " for " + serverName, e);
+ }
+
+ if(updated.size() > 0 || removed) {
+ SyncUtil.setSyncedPlaylists(context, instance, playlistList);
+ }
+ }
+
+ // For remaining playlists, check to make sure they have been updated recently
+ if(remainder != null) {
+ for (Playlist playlist : remainder) {
+ MusicDirectory dir = FileUtil.deserialize(context, Util.getCacheName(context, instance, "playlist", playlist.getId()), MusicDirectory.class, MAX_PLAYLIST_AGE);
+ if (dir == null) {
+ try {
+ musicService.getPlaylist(true, playlist.getId(), serverName, context, null);
+ } catch(Exception e) {
+ Log.w(TAG, "Failed to update playlist for " + playlist.getName());
+ }
+ }
+ }
+ }
+
+ if(updated.size() > 0) {
+ Notifications.showSyncNotification(context, R.string.sync_new_playlists, SyncUtil.joinNames(updated));
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/sync/PlaylistSyncService.java b/app/src/main/java/github/daneren2005/dsub/service/sync/PlaylistSyncService.java
new file mode 100644
index 00000000..dd1f3859
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/sync/PlaylistSyncService.java
@@ -0,0 +1,48 @@
+/*
+ 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.service.sync;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+
+/**
+ * Created by Scott on 8/28/13.
+ */
+
+public class PlaylistSyncService extends Service {
+ private static PlaylistSyncAdapter playlistSyncAdapter;
+ private static final Object syncLock = new Object();
+
+ @Override
+ public void onCreate() {
+ synchronized (syncLock) {
+ if(playlistSyncAdapter == null) {
+ playlistSyncAdapter = new PlaylistSyncAdapter(getApplicationContext(), true);
+ }
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return playlistSyncAdapter.getSyncAdapterBinder();
+
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/sync/PodcastSyncAdapter.java b/app/src/main/java/github/daneren2005/dsub/service/sync/PodcastSyncAdapter.java
new file mode 100644
index 00000000..985a7267
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/sync/PodcastSyncAdapter.java
@@ -0,0 +1,113 @@
+/*
+ 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.service.sync;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.util.Log;
+
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.List;
+
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.PodcastEpisode;
+import github.daneren2005.dsub.service.DownloadFile;
+import github.daneren2005.dsub.service.parser.SubsonicRESTException;
+import github.daneren2005.dsub.util.Notifications;
+import github.daneren2005.dsub.util.SyncUtil;
+import github.daneren2005.dsub.util.SyncUtil.SyncSet;
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.Util;
+
+/**
+ * Created by Scott on 8/28/13.
+ */
+
+public class PodcastSyncAdapter extends SubsonicSyncAdapter {
+ private static String TAG = PodcastSyncAdapter.class.getSimpleName();
+
+ public PodcastSyncAdapter(Context context, boolean autoInitialize) {
+ super(context, autoInitialize);
+ }
+ @TargetApi(14)
+ public PodcastSyncAdapter(Context context, boolean autoInitialize, boolean allowParallelSyncs) {
+ super(context, autoInitialize, allowParallelSyncs);
+ }
+
+ @Override
+ public void onExecuteSync(Context context, int instance) {
+ ArrayList<SyncSet> podcastList = SyncUtil.getSyncedPodcasts(context, instance);
+
+ try {
+ // Only refresh if syncs exist (implies a server where supported)
+ if(podcastList.size() > 0) {
+ // Just update podcast listings so user doesn't have to
+ musicService.getPodcastChannels(true, context, null);
+
+ // Refresh podcast listings before syncing
+ musicService.refreshPodcasts(context, null);
+ }
+
+ List<String> updated = new ArrayList<String>();
+ for(int i = 0; i < podcastList.size(); i++) {
+ SyncSet set = podcastList.get(i);
+ String id = set.id;
+ List<String> existingEpisodes = set.synced;
+ try {
+ MusicDirectory podcasts = musicService.getPodcastEpisodes(true, id, context, null);
+
+ for(MusicDirectory.Entry entry: podcasts.getChildren()) {
+ // Make sure podcast is valid and not already synced
+ if(entry.getId() != null && "completed".equals(((PodcastEpisode)entry).getStatus()) && !existingEpisodes.contains(entry.getId())) {
+ DownloadFile file = new DownloadFile(context, entry, false);
+ while(!file.isCompleteFileAvailable() && !file.isFailedMax()) {
+ file.downloadNow(musicService);
+ }
+ // Only add if actualy downloaded correctly
+ if(file.isCompleteFileAvailable()) {
+ existingEpisodes.add(entry.getId());
+ if(!updated.contains(podcasts.getName())) {
+ updated.add(podcasts.getName());
+ }
+ }
+ }
+ }
+ } catch(SubsonicRESTException e) {
+ if(e.getCode() == 70) {
+ SyncUtil.removeSyncedPodcast(context, id, instance);
+ Log.i(TAG, "Unsync deleted podcasts for " + id + " on " + Util.getServerName(context, instance));
+ }
+ } catch (Exception e) {
+ Log.w(TAG, "Failed to get podcasts for " + id + " on " + Util.getServerName(context, instance));
+ }
+ }
+
+ // Make sure there are is at least one change before re-syncing
+ if(updated.size() > 0) {
+ FileUtil.serialize(context, podcastList, SyncUtil.getPodcastSyncFile(context, instance));
+ Notifications.showSyncNotification(context, R.string.sync_new_podcasts, SyncUtil.joinNames(updated));
+ }
+ } catch(Exception e) {
+ Log.w(TAG, "Failed to get podcasts for " + Util.getServerName(context, instance));
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/sync/PodcastSyncService.java b/app/src/main/java/github/daneren2005/dsub/service/sync/PodcastSyncService.java
new file mode 100644
index 00000000..e4936581
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/sync/PodcastSyncService.java
@@ -0,0 +1,48 @@
+/*
+ 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.service.sync;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+
+/**
+ * Created by Scott on 8/28/13.
+ */
+
+public class PodcastSyncService extends Service {
+ private static PodcastSyncAdapter podcastSyncAdapter;
+ private static final Object syncLock = new Object();
+
+ @Override
+ public void onCreate() {
+ synchronized (syncLock) {
+ if(podcastSyncAdapter == null) {
+ podcastSyncAdapter = new PodcastSyncAdapter(getApplicationContext(), true);
+ }
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return podcastSyncAdapter.getSyncAdapterBinder();
+
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/sync/StarredSyncAdapter.java b/app/src/main/java/github/daneren2005/dsub/service/sync/StarredSyncAdapter.java
new file mode 100644
index 00000000..cf985227
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/sync/StarredSyncAdapter.java
@@ -0,0 +1,80 @@
+/*
+ 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.service.sync;
+
+import android.annotation.TargetApi;
+import android.app.Notification;
+import android.content.Context;
+import android.util.Log;
+
+import java.io.File;
+import java.util.ArrayList;
+
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.Notifications;
+import github.daneren2005.dsub.util.SyncUtil;
+import github.daneren2005.dsub.util.Util;
+
+/**
+ * Created by Scott on 8/28/13.
+ */
+
+public class StarredSyncAdapter extends SubsonicSyncAdapter {
+ private static String TAG = StarredSyncAdapter.class.getSimpleName();
+
+ public StarredSyncAdapter(Context context, boolean autoInitialize) {
+ super(context, autoInitialize);
+ }
+ @TargetApi(14)
+ public StarredSyncAdapter(Context context, boolean autoInitialize, boolean allowParallelSyncs) {
+ super(context, autoInitialize, allowParallelSyncs);
+ }
+
+ @Override
+ public void onExecuteSync(Context context, int instance) {
+ try {
+ ArrayList<String> syncedList = new ArrayList<String>();
+ MusicDirectory starredList = musicService.getStarredList(context, null);
+
+ // Pin all the starred stuff
+ boolean updated = downloadRecursively(syncedList, starredList, context, true);
+
+ // Get old starred list
+ ArrayList<String> oldSyncedList = SyncUtil.getSyncedStarred(context, instance);
+
+ // Check to make sure there aren't any old starred songs that now need to be removed
+ oldSyncedList.removeAll(syncedList);
+
+ for(String path: oldSyncedList) {
+ File saveFile = new File(path);
+ FileUtil.unpinSong(context, saveFile);
+ }
+
+ SyncUtil.setSyncedStarred(syncedList, context, instance);
+ if(updated) {
+ Notifications.showSyncNotification(context, R.string.sync_new_starred, null);
+ }
+ } catch(Exception e) {
+ Log.e(TAG, "Failed to get starred list for " + Util.getServerName(context, instance));
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/sync/StarredSyncService.java b/app/src/main/java/github/daneren2005/dsub/service/sync/StarredSyncService.java
new file mode 100644
index 00000000..9806d09b
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/sync/StarredSyncService.java
@@ -0,0 +1,48 @@
+/*
+ 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.service.sync;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+
+/**
+ * Created by Scott on 8/28/13.
+ */
+
+public class StarredSyncService extends Service {
+ private static StarredSyncAdapter starredSyncAdapter;
+ private static final Object syncLock = new Object();
+
+ @Override
+ public void onCreate() {
+ synchronized (syncLock) {
+ if(starredSyncAdapter == null) {
+ starredSyncAdapter = new StarredSyncAdapter(getApplicationContext(), true);
+ }
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return starredSyncAdapter.getSyncAdapterBinder();
+
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/sync/SubsonicSyncAdapter.java b/app/src/main/java/github/daneren2005/dsub/service/sync/SubsonicSyncAdapter.java
new file mode 100644
index 00000000..661f126d
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/sync/SubsonicSyncAdapter.java
@@ -0,0 +1,174 @@
+/*
+ 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.service.sync;
+
+import android.accounts.Account;
+import android.annotation.TargetApi;
+import android.content.AbstractThreadedSyncAdapter;
+import android.content.ContentProviderClient;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.content.SyncResult;
+import android.os.BatteryManager;
+import android.os.Bundle;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.util.Log;
+
+import java.util.List;
+
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.service.CachedMusicService;
+import github.daneren2005.dsub.service.DownloadFile;
+import github.daneren2005.dsub.service.RESTMusicService;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.Util;
+
+/**
+ * Created by Scott on 9/6/13.
+ */
+
+public class SubsonicSyncAdapter extends AbstractThreadedSyncAdapter {
+ private static final String TAG = SubsonicSyncAdapter.class.getSimpleName();
+ protected CachedMusicService musicService = new CachedMusicService(new RESTMusicService());
+ protected boolean tagBrowsing;
+ private Context context;
+
+ public SubsonicSyncAdapter(Context context, boolean autoInitialize) {
+ super(context, autoInitialize);
+ this.context = context;
+ }
+ @TargetApi(14)
+ public SubsonicSyncAdapter(Context context, boolean autoInitialize, boolean allowParallelSyncs) {
+ super(context, autoInitialize, allowParallelSyncs);
+ this.context = context;
+ }
+
+ @Override
+ public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
+ ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo networkInfo = manager.getActiveNetworkInfo();
+
+ // Don't try to sync if no network!
+ if(networkInfo == null || !networkInfo.isConnected() || Util.isOffline(context)) {
+ Log.w(TAG, "Not running sync, not connected to network");
+ return;
+ }
+
+ // Make sure battery > x% or is charging
+ IntentFilter intentFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
+ Intent batteryStatus = context.registerReceiver(null, intentFilter);
+ int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
+ if(status != BatteryManager.BATTERY_STATUS_CHARGING && status != BatteryManager.BATTERY_STATUS_FULL) {
+ int level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
+ int scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
+
+ if((level / (float)scale) < 0.15) {
+ Log.w(TAG, "Not running sync, battery too low");
+ return;
+ }
+ }
+
+ // Check if user wants to only sync on wifi
+ SharedPreferences prefs = Util.getPreferences(context);
+ if(prefs.getBoolean(Constants.PREFERENCES_KEY_SYNC_WIFI, true)) {
+ if(networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
+ executeSync(context);
+ } else {
+ Log.w(TAG, "Not running sync, not connected to wifi");
+ }
+ } else {
+ executeSync(context);
+ }
+ }
+
+ private void executeSync(Context context) {
+ String className = this.getClass().getSimpleName();
+ Log.i(TAG, "Running sync for " + className);
+ long start = System.currentTimeMillis();
+ int servers = Util.getServerCount(context);
+ for(int i = 1; i <= servers; i++) {
+ try {
+ if(isValidServer(context, i) && Util.isSyncEnabled(context, i)) {
+ tagBrowsing = Util.isTagBrowsing(context, i);
+ musicService.setInstance(i);
+ onExecuteSync(context, i);
+ } else {
+ Log.i(TAG, "Skipped sync for " + i);
+ }
+ } catch(Exception e) {
+ Log.e(TAG, "Failed sync for " + className + "(" + i + ")", e);
+ }
+ }
+
+ Log.i(TAG, className + " executed in " + (System.currentTimeMillis() - start) + " ms");
+ }
+ public void onExecuteSync(Context context, int instance) {
+
+ }
+
+ protected boolean downloadRecursively(List<String> paths, MusicDirectory parent, Context context, boolean save) throws Exception {
+ boolean downloaded = false;
+ for (MusicDirectory.Entry song: parent.getChildren(false, true)) {
+ if (!song.isVideo()) {
+ DownloadFile file = new DownloadFile(context, song, save);
+ while(!(save && file.isSaved() || !save && file.isCompleteFileAvailable()) && !file.isFailedMax()) {
+ file.downloadNow(musicService);
+ if(!file.isFailed()) {
+ downloaded = true;
+ }
+ }
+
+ if(paths != null && file.isCompleteFileAvailable()) {
+ paths.add(file.getCompleteFile().getPath());
+ }
+ }
+ }
+
+ for (MusicDirectory.Entry dir: parent.getChildren(true, false)) {
+ if(downloadRecursively(paths, getMusicDirectory(dir), context, save)) {
+ downloaded = true;
+ }
+ }
+
+ return downloaded;
+ }
+ protected MusicDirectory getMusicDirectory(MusicDirectory.Entry dir) throws Exception{
+ String id = dir.getId();
+ String name = dir.getTitle();
+
+ if(tagBrowsing) {
+ if(dir.getArtist() == null) {
+ return musicService.getArtist(id, name, true, context, null);
+ } else {
+ return musicService.getAlbum(id, name, true, context, null);
+ }
+ } else {
+ return musicService.getMusicDirectory(id, name, true, context, null);
+ }
+ }
+
+ private boolean isValidServer(Context context, int instance) {
+ String url = Util.getRestUrl(context, "null", instance, false);
+ return !(url.contains("demo.subsonic.org") || url.contains("yourhost"));
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/updates/Updater.java b/app/src/main/java/github/daneren2005/dsub/updates/Updater.java
new file mode 100644
index 00000000..a2870941
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/updates/Updater.java
@@ -0,0 +1,98 @@
+/*
+ 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.updates;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.Log;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.SilentBackgroundTask;
+import github.daneren2005.dsub.util.Util;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ *
+ * @author Scott
+ */
+public class Updater {
+ protected String TAG = Updater.class.getSimpleName();
+ protected int version;
+ protected Context context;
+
+ public Updater(int version) {
+ this.version = version;
+ }
+
+ public void checkUpdates(Context context) {
+ this.context = context;
+ List<Updater> updaters = new ArrayList<Updater>();
+ updaters.add(new Updater403());
+
+ SharedPreferences prefs = Util.getPreferences(context);
+ int lastVersion = prefs.getInt(Constants.LAST_VERSION, 0);
+ if(lastVersion == 0) {
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putInt(Constants.LAST_VERSION, version);
+ editor.commit();
+ }
+ else if(version > lastVersion) {
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putInt(Constants.LAST_VERSION, version);
+ editor.commit();
+
+ Log.i(TAG, "Updating from version " + lastVersion + " to " + version);
+ for(Updater updater: updaters) {
+ if(updater.shouldUpdate(lastVersion)) {
+ new BackgroundUpdate(context, updater).execute();
+ }
+ }
+ }
+ }
+
+ public String getName() {
+ return this.TAG;
+ }
+
+ private class BackgroundUpdate extends SilentBackgroundTask<Void> {
+ private final Updater updater;
+
+ public BackgroundUpdate(Context context, Updater updater) {
+ super(context);
+ this.updater = updater;
+ }
+
+ @Override
+ protected Void doInBackground() {
+ try {
+ updater.update(context);
+ } catch(Exception e) {
+ Log.w(TAG, "Failed to run update for " + updater.getName());
+ }
+ return null;
+ }
+ }
+
+ public boolean shouldUpdate(int version) {
+ return this.version > version;
+ }
+ public void update(Context context) {
+
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/updates/Updater403.java b/app/src/main/java/github/daneren2005/dsub/updates/Updater403.java
new file mode 100644
index 00000000..4f2cbf43
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/updates/Updater403.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.updates;
+
+import android.content.Context;
+import android.util.Log;
+import github.daneren2005.dsub.updates.Updater;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.FileUtil;
+import java.io.File;
+
+/**
+ *
+ * @author Scott
+ */
+public class Updater403 extends Updater {
+ public Updater403() {
+ super(403);
+ TAG = Updater403.class.getSimpleName();
+ }
+
+ @Override
+ public void update(Context context) {
+ // Rename cover.jpeg to cover.jpg
+ Log.i(TAG, "Running Updater403: updating cover.jpg to albumart.jpg");
+ File dir = FileUtil.getMusicDirectory(context);
+ if(dir != null) {
+ moveArt(dir);
+ }
+ }
+
+ private void moveArt(File dir) {
+ for(File file: dir.listFiles()) {
+ if(file.isDirectory()) {
+ moveArt(file);
+ } else if("cover.jpg".equals(file.getName()) || "cover.jpeg".equals(file.getName())) {
+ File renamed = new File(dir, Constants.ALBUM_ART_FILE);
+ file.renameTo(renamed);
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/ArtistRadioBuffer.java b/app/src/main/java/github/daneren2005/dsub/util/ArtistRadioBuffer.java
new file mode 100644
index 00000000..6e9b8309
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/ArtistRadioBuffer.java
@@ -0,0 +1,148 @@
+/*
+ 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.util;
+
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+
+public class ArtistRadioBuffer {
+ private static final String TAG = ArtistRadioBuffer.class.getSimpleName();
+
+ private ScheduledExecutorService executorService;
+ private Runnable runnable;
+ private final ArrayList<MusicDirectory.Entry> buffer = new ArrayList<MusicDirectory.Entry>();
+ private int lastCount = -1;
+ private DownloadService context;
+ private boolean awaitingResults = false;
+ private int capacity;
+ private int refillThreshold;
+
+ private String artistId;
+
+ public ArtistRadioBuffer(DownloadService context) {
+ this.context = context;
+ runnable = new Runnable() {
+ @Override
+ public void run() {
+ refill();
+ }
+ };
+
+ // Calculate out the capacity and refill threshold based on the user's random size preference
+ int shuffleListSize = Integer.parseInt(Util.getPreferences(context).getString(Constants.PREFERENCES_KEY_RANDOM_SIZE, "20"));
+ // ex: default 20 -> 50
+ capacity = shuffleListSize * 5 / 2;
+ capacity = Math.min(500, capacity);
+
+ // ex: default 20 -> 40
+ refillThreshold = capacity * 4 / 5;
+ }
+
+ public void setArtist(String artistId) {
+ if(!Util.equals(this.artistId, artistId)) {
+ buffer.clear();
+ }
+
+ context.clear();
+ this.artistId = artistId;
+ awaitingResults = true;
+ refill();
+ }
+ public void restoreArtist(String artistId) {
+ this.artistId = artistId;
+ awaitingResults = false;
+ restart();
+ }
+
+ public List<MusicDirectory.Entry> get(int size) {
+ // Make sure fetcher is running if needed
+ restart();
+
+ List<MusicDirectory.Entry> result = new ArrayList<MusicDirectory.Entry>(size);
+ synchronized (buffer) {
+ while (!buffer.isEmpty() && result.size() < size) {
+ result.add(buffer.remove(buffer.size() - 1));
+ }
+ }
+ Log.i(TAG, "Taking " + result.size() + " songs from artist radio buffer. " + buffer.size() + " remaining.");
+ if(result.isEmpty()) {
+ awaitingResults = true;
+ }
+ return result;
+ }
+
+ public void shutdown() {
+ executorService.shutdown();
+ }
+
+ private void restart() {
+ synchronized(buffer) {
+ if(buffer.size() <= refillThreshold && lastCount != 0 && (executorService == null || executorService.isShutdown())) {
+ executorService = Executors.newSingleThreadScheduledExecutor();
+ executorService.scheduleWithFixedDelay(runnable, 0, 10, TimeUnit.SECONDS);
+ }
+ }
+ }
+
+ private void refill() {
+ if (buffer != null && (buffer.size() > refillThreshold || (!Util.isNetworkConnected(context) && !Util.isOffline(context)) || lastCount == 0)) {
+ executorService.shutdown();
+ return;
+ }
+
+ try {
+ MusicService service = MusicServiceFactory.getMusicService(context);
+
+ // Get capacity based
+ int n = capacity - buffer.size();
+ MusicDirectory songs = service.getRandomSongs(n, artistId, context, null);
+
+ synchronized (buffer) {
+ lastCount = 0;
+ for(MusicDirectory.Entry entry: songs.getChildren()) {
+ if(!buffer.contains(entry) && entry.getRating() != 1) {
+ buffer.add(entry);
+ lastCount++;
+ }
+ }
+ Log.i(TAG, "Refilled artist radio buffer with " + lastCount + " songs.");
+ }
+ } catch (Exception x) {
+ // Give it one more try before quitting
+ if(lastCount != -2) {
+ lastCount = -2;
+ } else if(lastCount == -2) {
+ lastCount = 0;
+ }
+ Log.w(TAG, "Failed to refill artist radio buffer.", x);
+ }
+
+ if(awaitingResults) {
+ awaitingResults = false;
+ context.checkDownloads();
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/BackgroundTask.java b/app/src/main/java/github/daneren2005/dsub/util/BackgroundTask.java
new file mode 100644
index 00000000..9b39ac82
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/BackgroundTask.java
@@ -0,0 +1,307 @@
+/*
+ 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.util;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.view.ErrorDialog;
+
+/**
+ * @author Sindre Mehus
+ */
+public abstract class BackgroundTask<T> implements ProgressListener {
+ private static final String TAG = BackgroundTask.class.getSimpleName();
+
+ private final Context context;
+ protected AtomicBoolean cancelled = new AtomicBoolean(false);
+ protected OnCancelListener cancelListener;
+ protected Runnable onCompletionListener = null;
+ protected Task task;
+
+ private static final int DEFAULT_CONCURRENCY = 8;
+ private static final Collection<Thread> threads = Collections.synchronizedCollection(new ArrayList<Thread>());
+ protected static final BlockingQueue<BackgroundTask.Task> queue = new LinkedBlockingQueue<BackgroundTask.Task>(10);
+ private static Handler handler = null;
+ static {
+ try {
+ handler = new Handler(Looper.getMainLooper());
+ } catch(Exception e) {
+ // Not called from main thread
+ }
+ }
+
+ public BackgroundTask(Context context) {
+ this.context = context;
+
+ if(threads.size() < DEFAULT_CONCURRENCY) {
+ for(int i = threads.size(); i < DEFAULT_CONCURRENCY; i++) {
+ Thread thread = new Thread(new TaskRunnable(), String.format("BackgroundTask_%d", i));
+ threads.add(thread);
+ thread.start();
+ }
+ }
+ if(handler == null) {
+ try {
+ handler = new Handler(Looper.getMainLooper());
+ } catch(Exception e) {
+ // Not called from main thread
+ }
+ }
+ }
+
+ public static void stopThreads() {
+ for(Thread thread: threads) {
+ thread.interrupt();
+ }
+ threads.clear();
+ queue.clear();
+ }
+
+ protected Activity getActivity() {
+ return (context instanceof Activity) ? ((Activity) context) : null;
+ }
+
+ protected Handler getHandler() {
+ return handler;
+ }
+
+ public abstract void execute();
+
+ protected abstract T doInBackground() throws Throwable;
+
+ protected abstract void done(T result);
+
+ protected void error(Throwable error) {
+ Log.w(TAG, "Got exception: " + error, error);
+ Activity activity = getActivity();
+ if(activity != null) {
+ new ErrorDialog(activity, getErrorMessage(error), true);
+ }
+ }
+
+ protected String getErrorMessage(Throwable error) {
+
+ if (error instanceof IOException && !Util.isNetworkConnected(context)) {
+ return context.getResources().getString(R.string.background_task_no_network);
+ }
+
+ if (error instanceof FileNotFoundException) {
+ return context.getResources().getString(R.string.background_task_not_found);
+ }
+
+ if (error instanceof IOException) {
+ return context.getResources().getString(R.string.background_task_network_error);
+ }
+
+ if (error instanceof XmlPullParserException) {
+ return context.getResources().getString(R.string.background_task_parse_error);
+ }
+
+ String message = error.getMessage();
+ if (message != null) {
+ return message;
+ }
+ return error.getClass().getSimpleName();
+ }
+
+ public void cancel() {
+ if(cancelled.compareAndSet(false, true)) {
+ if(isRunning()) {
+ if(cancelListener != null) {
+ cancelListener.onCancel();
+ } else {
+ task.cancel();
+ }
+ }
+
+ task = null;
+ }
+ }
+ public boolean isCancelled() {
+ return cancelled.get();
+ }
+ public void setOnCancelListener(OnCancelListener listener) {
+ cancelListener = listener;
+ }
+
+ public boolean isRunning() {
+ if(task == null) {
+ return false;
+ } else {
+ return task.isRunning();
+ }
+ }
+
+ @Override
+ public abstract void updateProgress(final String message);
+
+ @Override
+ public void updateProgress(int messageId) {
+ updateProgress(context.getResources().getString(messageId));
+ }
+
+ public void setOnCompletionListener(Runnable onCompletionListener) {
+ this.onCompletionListener = onCompletionListener;
+ }
+
+ protected class Task {
+ private Thread thread;
+ private AtomicBoolean taskStart = new AtomicBoolean(false);
+
+ private void execute() throws Exception {
+ // Don't run if cancelled already
+ if(isCancelled()) {
+ return;
+ }
+
+ try {
+ thread = Thread.currentThread();
+ taskStart.set(true);
+
+ final T result = doInBackground();
+ if(isCancelled()) {
+ taskStart.set(false);
+ return;
+ }
+
+ if(handler != null) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ if(!isCancelled()) {
+ onDone(result);
+ }
+
+ taskStart.set(false);
+ }
+ });
+ } else {
+ taskStart.set(false);
+ }
+ } catch(InterruptedException interrupt) {
+ if(taskStart.get()) {
+ // Don't exit root thread if task cancelled
+ throw interrupt;
+ }
+ } catch(final Throwable t) {
+ if(isCancelled()) {
+ taskStart.set(false);
+ return;
+ }
+
+ if(handler != null) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ if(!isCancelled()) {
+ try {
+ onError(t);
+ } catch(Exception e) {
+ // Don't care
+ }
+ }
+
+ taskStart.set(false);
+ }
+ });
+ } else {
+ taskStart.set(false);
+ }
+ } finally {
+ thread = null;
+ }
+ }
+
+ public void cancel() {
+ if(taskStart.compareAndSet(true, false)) {
+ if (thread != null) {
+ thread.interrupt();
+ }
+ }
+ }
+ public boolean isCancelled() {
+ if(Thread.interrupted()) {
+ return true;
+ } else if(BackgroundTask.this.isCancelled()) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+ public void onDone(T result) {
+ done(result);
+
+ if(onCompletionListener != null) {
+ onCompletionListener.run();
+ }
+ }
+ public void onError(Throwable t) {
+ error(t);
+ }
+
+ public boolean isRunning() {
+ return taskStart.get();
+ }
+ }
+
+ private class TaskRunnable implements Runnable {
+ private boolean running = true;
+
+ public TaskRunnable() {
+
+ }
+
+ @Override
+ public void run() {
+ Looper.prepare();
+ while(running) {
+ try {
+ Task task = queue.take();
+ task.execute();
+ } catch(InterruptedException stop) {
+ Log.e(TAG, "Thread died");
+ running = false;
+ threads.remove(Thread.currentThread());
+ } catch(Throwable t) {
+ Log.e(TAG, "Unexpected crash in BackgroundTask thread", t);
+ }
+ }
+ }
+ }
+
+ public static interface OnCancelListener {
+ void onCancel();
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/CacheCleaner.java b/app/src/main/java/github/daneren2005/dsub/util/CacheCleaner.java
new file mode 100644
index 00000000..ac8fa72a
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/CacheCleaner.java
@@ -0,0 +1,292 @@
+package github.daneren2005.dsub.util;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import android.content.Context;
+import android.util.Log;
+import android.os.StatFs;
+import github.daneren2005.dsub.domain.Playlist;
+import github.daneren2005.dsub.service.DownloadFile;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.service.MediaStoreService;
+
+import java.util.*;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class CacheCleaner {
+
+ private static final String TAG = CacheCleaner.class.getSimpleName();
+ private static final long MIN_FREE_SPACE = 500 * 1024L * 1024L;
+ private static final long MAX_COVER_ART_SPACE = 100 * 1024L * 1024L;
+
+ private final Context context;
+ private final DownloadService downloadService;
+ private final MediaStoreService mediaStore;
+
+ public CacheCleaner(Context context, DownloadService downloadService) {
+ this.context = context;
+ this.downloadService = downloadService;
+ this.mediaStore = new MediaStoreService(context);
+ }
+
+ public void clean() {
+ new BackgroundCleanup(context).execute();
+ }
+ public void cleanSpace() {
+ new BackgroundSpaceCleanup(context).execute();
+ }
+ public void cleanPlaylists(List<Playlist> playlists) {
+ new BackgroundPlaylistsCleanup(context, playlists).execute();
+ }
+
+ private void deleteEmptyDirs(List<File> dirs, Set<File> undeletable) {
+ for (File dir : dirs) {
+ if (undeletable.contains(dir)) {
+ continue;
+ }
+
+ FileUtil.deleteEmptyDir(dir);
+ }
+ }
+
+ private long getMinimumDelete(List<File> files, List<File> pinned) {
+ if(files.size() == 0) {
+ return 0L;
+ }
+
+ long cacheSizeBytes = Util.getCacheSizeMB(context) * 1024L * 1024L;
+
+ long bytesUsedBySubsonic = 0L;
+ for (File file : files) {
+ bytesUsedBySubsonic += file.length();
+ }
+ for (File file : pinned) {
+ bytesUsedBySubsonic += file.length();
+ }
+
+ // Ensure that file system is not more than 95% full.
+ StatFs stat = new StatFs(files.get(0).getPath());
+ long bytesTotalFs = (long) stat.getBlockCount() * (long) stat.getBlockSize();
+ long bytesAvailableFs = (long) stat.getAvailableBlocks() * (long) stat.getBlockSize();
+ long bytesUsedFs = bytesTotalFs - bytesAvailableFs;
+ long minFsAvailability = bytesTotalFs - MIN_FREE_SPACE;
+
+ long bytesToDeleteCacheLimit = Math.max(bytesUsedBySubsonic - cacheSizeBytes, 0L);
+ long bytesToDeleteFsLimit = Math.max(bytesUsedFs - minFsAvailability, 0L);
+ long bytesToDelete = Math.max(bytesToDeleteCacheLimit, bytesToDeleteFsLimit);
+
+ Log.i(TAG, "File system : " + Util.formatBytes(bytesAvailableFs) + " of " + Util.formatBytes(bytesTotalFs) + " available");
+ Log.i(TAG, "Cache limit : " + Util.formatBytes(cacheSizeBytes));
+ Log.i(TAG, "Cache size before : " + Util.formatBytes(bytesUsedBySubsonic));
+ Log.i(TAG, "Minimum to delete : " + Util.formatBytes(bytesToDelete));
+
+ return bytesToDelete;
+ }
+
+ private void deleteFiles(List<File> files, Set<File> undeletable, long bytesToDelete, boolean deletePartials) {
+ if (files.isEmpty()) {
+ return;
+ }
+
+ long bytesDeleted = 0L;
+ for (File file : files) {
+ if(!deletePartials && bytesDeleted > bytesToDelete) break;
+
+ if (bytesToDelete > bytesDeleted || (deletePartials && (file.getName().endsWith(".partial") || file.getName().contains(".partial.")))) {
+ if (!undeletable.contains(file) && !file.getName().equals(Constants.ALBUM_ART_FILE)) {
+ long size = file.length();
+ if (Util.delete(file)) {
+ bytesDeleted += size;
+ mediaStore.deleteFromMediaStore(file);
+ }
+ }
+ }
+ }
+
+ Log.i(TAG, "Deleted : " + Util.formatBytes(bytesDeleted));
+ }
+
+ private void findCandidatesForDeletion(File file, List<File> files, List<File> pinned, List<File> dirs) {
+ if (file.isFile()) {
+ String name = file.getName();
+ boolean isCacheFile = name.endsWith(".partial") || name.contains(".partial.") || name.endsWith(".complete") || name.contains(".complete.");
+ if (isCacheFile) {
+ files.add(file);
+ } else {
+ pinned.add(file);
+ }
+ } else {
+ // Depth-first
+ for (File child : FileUtil.listFiles(file)) {
+ findCandidatesForDeletion(child, files, pinned, dirs);
+ }
+ dirs.add(file);
+ }
+ }
+
+ private void sortByAscendingModificationTime(List<File> files) {
+ Collections.sort(files, new Comparator<File>() {
+ @Override
+ public int compare(File a, File b) {
+ if (a.lastModified() < b.lastModified()) {
+ return -1;
+ }
+ if (a.lastModified() > b.lastModified()) {
+ return 1;
+ }
+ return 0;
+ }
+ });
+ }
+
+ private Set<File> findUndeletableFiles() {
+ Set<File> undeletable = new HashSet<File>(5);
+
+ for (DownloadFile downloadFile : downloadService.getDownloads()) {
+ undeletable.add(downloadFile.getPartialFile());
+ undeletable.add(downloadFile.getCompleteFile());
+ }
+
+ undeletable.add(FileUtil.getMusicDirectory(context));
+ return undeletable;
+ }
+
+ private void cleanupCoverArt(Context context) {
+ File dir = FileUtil.getAlbumArtDirectory(context);
+
+ List<File> files = new ArrayList<File>();
+ long bytesUsed = 0L;
+ for(File file: dir.listFiles()) {
+ if(file.isFile()) {
+ files.add(file);
+ bytesUsed += file.length();
+ }
+ }
+
+ // Don't waste time sorting if under limit already
+ if(bytesUsed < MAX_COVER_ART_SPACE) {
+ return;
+ }
+
+ sortByAscendingModificationTime(files);
+ long bytesDeleted = 0L;
+ for(File file: files) {
+ // End as soon as the space used is below the threshold
+ if(bytesUsed < MAX_COVER_ART_SPACE) {
+ break;
+ }
+
+ long bytes = file.length();
+ if(file.delete()) {
+ bytesUsed -= bytes;
+ bytesDeleted += bytes;
+ }
+ }
+
+ Log.i(TAG, "Deleted " + Util.formatBytes(bytesDeleted) + " worth of cover art");
+ }
+
+ private class BackgroundCleanup extends SilentBackgroundTask<Void> {
+ public BackgroundCleanup(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected Void doInBackground() {
+ if (downloadService == null) {
+ Log.e(TAG, "DownloadService not set. Aborting cache cleaning.");
+ return null;
+ }
+
+ try {
+ List<File> files = new ArrayList<File>();
+ List<File> pinned = new ArrayList<File>();
+ List<File> dirs = new ArrayList<File>();
+
+ findCandidatesForDeletion(FileUtil.getMusicDirectory(context), files, pinned, dirs);
+ sortByAscendingModificationTime(files);
+
+ Set<File> undeletable = findUndeletableFiles();
+
+ deleteFiles(files, undeletable, getMinimumDelete(files, pinned), true);
+ deleteEmptyDirs(dirs, undeletable);
+
+ // Make sure cover art directory does not grow too large
+ cleanupCoverArt(context);
+ } catch (RuntimeException x) {
+ Log.e(TAG, "Error in cache cleaning.", x);
+ }
+
+ return null;
+ }
+ }
+
+ private class BackgroundSpaceCleanup extends SilentBackgroundTask<Void> {
+ public BackgroundSpaceCleanup(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected Void doInBackground() {
+ if (downloadService == null) {
+ Log.e(TAG, "DownloadService not set. Aborting cache cleaning.");
+ return null;
+ }
+
+ try {
+ List<File> files = new ArrayList<File>();
+ List<File> pinned = new ArrayList<File>();
+ List<File> dirs = new ArrayList<File>();
+ findCandidatesForDeletion(FileUtil.getMusicDirectory(context), files, pinned, dirs);
+
+ long bytesToDelete = getMinimumDelete(files, pinned);
+ if(bytesToDelete > 0L) {
+ sortByAscendingModificationTime(files);
+ Set<File> undeletable = findUndeletableFiles();
+ deleteFiles(files, undeletable, bytesToDelete, false);
+ }
+ } catch (RuntimeException x) {
+ Log.e(TAG, "Error in cache cleaning.", x);
+ }
+
+ return null;
+ }
+ }
+
+ private class BackgroundPlaylistsCleanup extends SilentBackgroundTask<Void> {
+ private final List<Playlist> playlists;
+
+ public BackgroundPlaylistsCleanup(Context context, List<Playlist> playlists) {
+ super(context);
+ this.playlists = playlists;
+ }
+
+ @Override
+ protected Void doInBackground() {
+ try {
+ String server = Util.getServerName(context);
+ SortedSet<File> playlistFiles = FileUtil.listFiles(FileUtil.getPlaylistDirectory(context, server));
+ for (Playlist playlist : playlists) {
+ playlistFiles.remove(FileUtil.getPlaylistFile(context, server, playlist.getName()));
+ }
+
+ for(File playlist : playlistFiles) {
+ playlist.delete();
+ }
+ } catch (RuntimeException x) {
+ Log.e(TAG, "Error in playlist cache cleaning.", x);
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/Constants.java b/app/src/main/java/github/daneren2005/dsub/util/Constants.java
new file mode 100644
index 00000000..31c5bef2
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/Constants.java
@@ -0,0 +1,206 @@
+/*
+ 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.util;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public final class Constants {
+
+ // Character encoding used throughout.
+ public static final String UTF_8 = "UTF-8";
+
+ // REST protocol version and client ID.
+ // Note: Keep it as low as possible to maintain compatibility with older servers.
+ public static final String REST_PROTOCOL_VERSION = "1.2.0";
+ public static final String REST_CLIENT_ID = "DSub";
+ public static final String CHROMECAST_CLIENT_ID = "DSubCC";
+ public static final String LAST_VERSION = "subsonic.version";
+
+ // Names for intent extras.
+ public static final String INTENT_EXTRA_NAME_ID = "subsonic.id";
+ public static final String INTENT_EXTRA_NAME_NAME = "subsonic.name";
+ public static final String INTENT_EXTRA_NAME_DIRECTORY = "subsonic.directory";
+ public static final String INTENT_EXTRA_NAME_CHILD_ID = "subsonic.child.id";
+ public static final String INTENT_EXTRA_NAME_ARTIST = "subsonic.artist";
+ public static final String INTENT_EXTRA_NAME_TITLE = "subsonic.title";
+ public static final String INTENT_EXTRA_NAME_AUTOPLAY = "subsonic.playall";
+ public static final String INTENT_EXTRA_NAME_QUERY = "subsonic.query";
+ public static final String INTENT_EXTRA_NAME_PLAYLIST_ID = "subsonic.playlist.id";
+ public static final String INTENT_EXTRA_NAME_PLAYLIST_NAME = "subsonic.playlist.name";
+ public static final String INTENT_EXTRA_NAME_PLAYLIST_OWNER = "subsonic.playlist.isOwner";
+ public static final String INTENT_EXTRA_NAME_ALBUM_LIST_TYPE = "subsonic.albumlisttype";
+ public static final String INTENT_EXTRA_NAME_ALBUM_LIST_EXTRA = "subsonic.albumlistextra";
+ public static final String INTENT_EXTRA_NAME_ALBUM_LIST_SIZE = "subsonic.albumlistsize";
+ public static final String INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET = "subsonic.albumlistoffset";
+ public static final String INTENT_EXTRA_NAME_SHUFFLE = "subsonic.shuffle";
+ public static final String INTENT_EXTRA_REQUEST_SEARCH = "subsonic.requestsearch";
+ public static final String INTENT_EXTRA_NAME_EXIT = "subsonic.exit" ;
+ public static final String INTENT_EXTRA_NAME_DOWNLOAD = "subsonic.download";
+ public static final String INTENT_EXTRA_NAME_DOWNLOAD_VIEW = "subsonic.download_view";
+ public static final String INTENT_EXTRA_VIEW_ALBUM = "subsonic.view_album";
+ public static final String INTENT_EXTRA_NAME_PODCAST_ID = "subsonic.podcast.id";
+ public static final String INTENT_EXTRA_NAME_PODCAST_NAME = "subsonic.podcast.name";
+ public static final String INTENT_EXTRA_NAME_PODCAST_DESCRIPTION = "subsonic.podcast.description";
+ public static final String INTENT_EXTRA_NAME_SHARE = "subsonic.share";
+ public static final String INTENT_EXTRA_FRAGMENT_TYPE = "fragmentType";
+ public static final String INTENT_EXTRA_REFRESH_LISTINGS = "refreshListings";
+ public static final String INTENT_EXTRA_SEARCH_SONG = "searchSong";
+ public static final String INTENT_EXTRA_TOP_TRACKS = "topTracks";
+ public static final String INTENT_EXTRA_SHOW_ALL = "showAll";
+
+ // Preferences keys.
+ public static final String PREFERENCES_KEY_SERVER_KEY = "server";
+ public static final String PREFERENCES_KEY_SERVER_COUNT = "serverCount";
+ public static final String PREFERENCES_KEY_SERVER_ADD = "serverAdd";
+ public static final String PREFERENCES_KEY_SERVER_REMOVE = "serverRemove";
+ public static final String PREFERENCES_KEY_SERVER_INSTANCE = "serverInstanceId";
+ public static final String PREFERENCES_KEY_SERVER_NAME = "serverName";
+ public static final String PREFERENCES_KEY_SERVER_URL = "serverUrl";
+ public static final String PREFERENCES_KEY_SERVER_INTERNAL_URL = "serverInternalUrl";
+ public static final String PREFERENCES_KEY_SERVER_LOCAL_NETWORK_SSID = "serverLocalNetworkSSID";
+ public static final String PREFERENCES_KEY_TEST_CONNECTION = "serverTestConnection";
+ public static final String PREFERENCES_KEY_OPEN_BROWSER = "openBrowser";
+ public static final String PREFERENCES_KEY_MUSIC_FOLDER_ID = "musicFolderId";
+ public static final String PREFERENCES_KEY_USERNAME = "username";
+ public static final String PREFERENCES_KEY_PASSWORD = "password";
+ public static final String PREFERENCES_KEY_INSTALL_TIME = "installTime";
+ public static final String PREFERENCES_KEY_THEME = "theme";
+ public static final String PREFERENCES_KEY_FULL_SCREEN = "fullScreen";
+ public static final String PREFERENCES_KEY_DISPLAY_TRACK = "displayTrack";
+ public static final String PREFERENCES_KEY_MAX_BITRATE_WIFI = "maxBitrateWifi";
+ public static final String PREFERENCES_KEY_MAX_BITRATE_MOBILE = "maxBitrateMobile";
+ public static final String PREFERENCES_KEY_MAX_VIDEO_BITRATE_WIFI = "maxVideoBitrateWifi";
+ public static final String PREFERENCES_KEY_MAX_VIDEO_BITRATE_MOBILE = "maxVideoBitrateMobile";
+ public static final String PREFERENCES_KEY_NETWORK_TIMEOUT = "networkTimeout";
+ public static final String PREFERENCES_KEY_CACHE_SIZE = "cacheSize";
+ public static final String PREFERENCES_KEY_CACHE_LOCATION = "cacheLocation";
+ public static final String PREFERENCES_KEY_PRELOAD_COUNT_WIFI = "preloadCountWifi";
+ public static final String PREFERENCES_KEY_PRELOAD_COUNT_MOBILE = "preloadCountMobile";
+ public static final String PREFERENCES_KEY_HIDE_MEDIA = "hideMedia";
+ public static final String PREFERENCES_KEY_MEDIA_BUTTONS = "mediaButtons";
+ public static final String PREFERENCES_KEY_SCREEN_LIT_ON_DOWNLOAD = "screenLitOnDownload";
+ public static final String PREFERENCES_KEY_SCROBBLE = "scrobble";
+ public static final String PREFERENCES_KEY_REPEAT_MODE = "repeatMode";
+ public static final String PREFERENCES_KEY_WIFI_REQUIRED_FOR_DOWNLOAD = "wifiRequiredForDownload";
+ public static final String PREFERENCES_KEY_RANDOM_SIZE = "randomSize";
+ public static final String PREFERENCES_KEY_SLEEP_TIMER_DURATION = "sleepTimerDuration";
+ public static final String PREFERENCES_KEY_OFFLINE = "offline";
+ public static final String PREFERENCES_KEY_TEMP_LOSS = "tempLoss";
+ public static final String PREFERENCES_KEY_SHUFFLE_START_YEAR = "startYear";
+ public static final String PREFERENCES_KEY_SHUFFLE_END_YEAR = "endYear";
+ public static final String PREFERENCES_KEY_SHUFFLE_GENRE = "genre";
+ public static final String PREFERENCES_KEY_KEEP_SCREEN_ON = "keepScreenOn";
+ public static final String PREFERENCES_EQUALIZER_ON = "equalizerOn";
+ public static final String PREFERENCES_EQUALIZER_SETTINGS = "equalizerSettings";
+ public static final String PREFERENCES_KEY_PERSISTENT_NOTIFICATION = "persistentNotification";
+ public static final String PREFERENCES_KEY_GAPLESS_PLAYBACK = "gaplessPlayback";
+ public static final String PREFERENCES_KEY_REMOVE_PLAYED = "removePlayed";
+ public static final String PREFERENCES_KEY_SHUFFLE_MODE = "shuffleMode2";
+ public static final String PREFERENCES_KEY_SHUFFLE_MODE_EXTRA = "shuffleModeExtra";
+ public static final String PREFERENCES_KEY_CHAT_REFRESH = "chatRefreshRate";
+ public static final String PREFERENCES_KEY_CHAT_ENABLED = "chatEnabled";
+ public static final String PREFERENCES_KEY_VIDEO_PLAYER = "videoPlayer";
+ public static final String PREFERENCES_KEY_CONTROL_MODE = "remoteControlMode";
+ public static final String PREFERENCES_KEY_CONTROL_ID = "remoteControlId";
+ public static final String PREFERENCES_KEY_SYNC_ENABLED = "syncEnabled";
+ public static final String PREFERENCES_KEY_SYNC_INTERVAL = "syncInterval";
+ public static final String PREFERENCES_KEY_SYNC_WIFI = "syncWifi";
+ public static final String PREFERENCES_KEY_SYNC_NOTIFICATION = "syncNotification";
+ public static final String PREFERENCES_KEY_SYNC_STARRED = "syncStarred";
+ public static final String PREFERENCES_KEY_SYNC_MOST_RECENT = "syncMostRecent";
+ public static final String PREFERENCES_KEY_PAUSE_DISCONNECT = "pauseOnDisconnect";
+ public static final String PREFERENCES_KEY_HIDE_WIDGET = "hideWidget";
+ public static final String PREFERENCES_KEY_PODCASTS_ENABLED = "podcastsEnabled";
+ public static final String PREFERENCES_KEY_BOOKMARKS_ENABLED = "bookmarksEnabled";
+ public static final String PREFERENCES_KEY_CUSTOM_SORT_ENABLED = "customSortEnabled";
+ public static final String PREFERENCES_KEY_MENU_PLAY_NEXT = "showPlayNext";
+ public static final String PREFERENCES_KEY_MENU_PLAY_LAST = "showPlayLast";
+ public static final String PREFERENCES_KEY_MENU_STAR = "showStar";
+ public static final String PREFERENCES_KEY_MENU_SHARED = "showShared";
+ public static final String PREFERENCES_KEY_SHARED_ENABLED = "sharedEnabled";
+ public static final String PREFERENCES_KEY_BROWSE_TAGS = "browseTags";
+ public static final String PREFERENCES_KEY_OPEN_TO_TAB = "openToTab";
+ public static final String PREFERENCES_KEY_OVERRIDE_SYSTEM_LANGUAGE = "overrideSystemLanguage";
+ public static final String PREFERENCES_KEY_PLAY_NOW_AFTER = "playNowAfter";
+ public static final String PREFERENCES_KEY_LARGE_ALBUM_ART = "largeAlbumArt";
+ public static final String PREFERENCES_KEY_ADMIN_ENABLED = "adminEnabled";
+ public static final String PREFERENCES_KEY_PLAYLIST_NAME = "suggestedPlaylistName";
+ public static final String PREFERENCES_KEY_PLAYLIST_ID = "suggestedPlaylistId";
+ public static final String PREFERENCES_KEY_SERVER_SYNC = "serverSync";
+ public static final String PREFERENCES_KEY_RECENT_COUNT = "mostRecentCount";
+ public static final String PREFERENCES_KEY_MENU_RATING = "showRating";
+ public static final String PREFERENCES_KEY_REPLAY_GAIN = "replayGain";
+ public static final String PREFERENCES_KEY_REPLAY_GAIN_BUMP = "replayGainBump2";
+ public static final String PREFERENCES_KEY_REPLAY_GAIN_UNTAGGED = "replayGainUntagged2";
+ public static final String PREFERENCES_KEY_REPLAY_GAIN_TYPE= "replayGainType";
+ public static final String PREFERENCES_KEY_ALBUMS_PER_FOLDER = "albumsPerFolder";
+ public static final String PREFERENCES_KEY_CAST_PROXY = "castProxy";
+ public static final String PREFERENCES_KEY_DISABLE_EXIT_PROMPT = "disableExitPrompt";
+ public static final String PREFERENCES_KEY_RENAME_DUPLICATES = "renameDuplicates";
+ public static final String PREFERENCES_KEY_FIRST_LEVEL_ARTIST = "firstLevelArtist";
+ public static final String PREFERENCES_KEY_START_ON_HEADPHONES = "startOnHeadphones";
+
+ public static final String OFFLINE_SCROBBLE_COUNT = "scrobbleCount";
+ public static final String OFFLINE_SCROBBLE_ID = "scrobbleID";
+ public static final String OFFLINE_SCROBBLE_SEARCH = "scrobbleTitle";
+ public static final String OFFLINE_SCROBBLE_TIME = "scrobbleTime";
+ public static final String OFFLINE_STAR_COUNT = "starCount";
+ public static final String OFFLINE_STAR_ID = "starID";
+ public static final String OFFLINE_STAR_SEARCH = "starTitle";
+ public static final String OFFLINE_STAR_SETTING = "starSetting";
+
+ public static final String CACHE_KEY_IGNORE = "ignoreArticles";
+
+ public static final String MAIN_BACK_STACK = "backStackIds";
+ public static final String MAIN_BACK_STACK_SIZE = "backStackIdsSize";
+ public static final String FRAGMENT_LIST = "fragmentList";
+ public static final String FRAGMENT_LIST2 = "fragmentList2";
+ public static final String FRAGMENT_EXTRA = "fragmentExtra";
+ public static final String FRAGMENT_DOWNLOAD_FLIPPER = "fragmentDownloadFlipper";
+ public static final String FRAGMENT_NAME = "fragmentName";
+ public static final String FRAGMENT_POSITION = "fragmentPosition";
+
+ // Name of the preferences file.
+ public static final String PREFERENCES_FILE_NAME = "github.daneren2005.dsub_preferences";
+ public static final String OFFLINE_SYNC_NAME = "github.daneren2005.dsub.offline";
+ public static final String OFFLINE_SYNC_DEFAULT = "syncDefaults";
+
+ // Account prefs
+ public static final String SYNC_ACCOUNT_NAME = "Subsonic Account";
+ public static final String SYNC_ACCOUNT_TYPE = "subsonic.org";
+ public static final String SYNC_ACCOUNT_PLAYLIST_AUTHORITY = "github.daneren2005.dsub.playlists.provider";
+ public static final String SYNC_ACCOUNT_PODCAST_AUTHORITY = "github.daneren2005.dsub.podcasts.provider";
+ public static final String SYNC_ACCOUNT_STARRED_AUTHORITY = "github.daneren2005.dsub.starred.provider";
+ public static final String SYNC_ACCOUNT_MOST_RECENT_AUTHORITY = "github.daneren2005.dsub.mostrecent.provider";
+
+ public static final String TASKER_EXTRA_BUNDLE = "com.twofortyfouram.locale.intent.extra.BUNDLE";
+
+ // Number of free trial days for non-licensed servers.
+ public static final int FREE_TRIAL_DAYS = 30;
+
+ // URL for project donations.
+ public static final String DONATION_URL = "http://subsonic.org/pages/android-donation.jsp";
+
+ public static final String ALBUM_ART_FILE = "albumart.jpg";
+
+ private Constants() {
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/FileUtil.java b/app/src/main/java/github/daneren2005/dsub/util/FileUtil.java
new file mode 100644
index 00000000..990eae06
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/FileUtil.java
@@ -0,0 +1,860 @@
+/*
+ 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.util;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.zip.DeflaterOutputStream;
+import java.util.zip.InflaterInputStream;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.os.Build;
+import android.os.Environment;
+import android.support.v4.content.ContextCompat;
+import android.util.Log;
+import github.daneren2005.dsub.domain.Artist;
+import github.daneren2005.dsub.domain.Genre;
+import github.daneren2005.dsub.domain.Indexes;
+import github.daneren2005.dsub.domain.Playlist;
+import github.daneren2005.dsub.domain.PodcastChannel;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.MusicFolder;
+import github.daneren2005.dsub.domain.PodcastEpisode;
+import github.daneren2005.dsub.service.MediaStoreService;
+
+import com.esotericsoftware.kryo.Kryo;
+import com.esotericsoftware.kryo.io.Input;
+import com.esotericsoftware.kryo.io.Output;
+
+/**
+ * @author Sindre Mehus
+ */
+public class FileUtil {
+
+ private static final String TAG = FileUtil.class.getSimpleName();
+ private static final String[] FILE_SYSTEM_UNSAFE = {"/", "\\", "..", ":", "\"", "?", "*", "<", ">", "|"};
+ private static final String[] FILE_SYSTEM_UNSAFE_DIR = {"\\", "..", ":", "\"", "?", "*", "<", ">", "|"};
+ private static final List<String> MUSIC_FILE_EXTENSIONS = Arrays.asList("mp3", "ogg", "aac", "flac", "m4a", "wav", "wma");
+ private static final List<String> VIDEO_FILE_EXTENSIONS = Arrays.asList("flv", "mp4", "m4v", "wmv", "avi", "mov", "mpg", "mkv");
+ private static final List<String> PLAYLIST_FILE_EXTENSIONS = Arrays.asList("m3u");
+ private static File DEFAULT_MUSIC_DIR;
+ private static final Kryo kryo = new Kryo();
+ private static HashMap<String, MusicDirectory.Entry> entryLookup;
+
+ static {
+ kryo.register(MusicDirectory.Entry.class);
+ kryo.register(Indexes.class);
+ kryo.register(Artist.class);
+ kryo.register(MusicFolder.class);
+ kryo.register(PodcastChannel.class);
+ kryo.register(Playlist.class);
+ kryo.register(Genre.class);
+ }
+
+ public static File getAnySong(Context context) {
+ File dir = getMusicDirectory(context);
+ return getAnySong(context, dir);
+ }
+ private static File getAnySong(Context context, File dir) {
+ for(File file: dir.listFiles()) {
+ if(file.isDirectory()) {
+ return getAnySong(context, file);
+ }
+
+ String extension = getExtension(file.getName());
+ if(MUSIC_FILE_EXTENSIONS.contains(extension)) {
+ return file;
+ }
+ }
+
+ return null;
+ }
+
+ public static File getEntryFile(Context context, MusicDirectory.Entry entry) {
+ if(entry.isDirectory()) {
+ return getAlbumDirectory(context, entry);
+ } else {
+ return getSongFile(context, entry);
+ }
+ }
+
+ public static File getSongFile(Context context, MusicDirectory.Entry song) {
+ File dir = getAlbumDirectory(context, song);
+
+ StringBuilder fileName = new StringBuilder();
+ Integer track = song.getTrack();
+ if (track != null) {
+ if (track < 10) {
+ fileName.append("0");
+ }
+ fileName.append(track).append("-");
+ }
+
+ fileName.append(fileSystemSafe(song.getTitle())).append(".");
+
+ if(song.isVideo()) {
+ String videoPlayerType = Util.getVideoPlayerType(context);
+ if("hls".equals(videoPlayerType)) {
+ // HLS should be able to transcode to mp4 automatically
+ fileName.append("mp4");
+ } else if("raw".equals(videoPlayerType)) {
+ // Download the original video without any transcoding
+ fileName.append(song.getSuffix());
+ }
+ } else {
+ if (song.getTranscodedSuffix() != null) {
+ fileName.append(song.getTranscodedSuffix());
+ } else {
+ fileName.append(song.getSuffix());
+ }
+ }
+
+ return new File(dir, fileName.toString());
+ }
+
+ public static File getPlaylistFile(Context context, String server, String name) {
+ File playlistDir = getPlaylistDirectory(context, server);
+ return new File(playlistDir, fileSystemSafe(name) + ".m3u");
+ }
+ public static void writePlaylistFile(Context context, File file, MusicDirectory playlist) throws IOException {
+ FileWriter fw = new FileWriter(file);
+ BufferedWriter bw = new BufferedWriter(fw);
+ try {
+ fw.write("#EXTM3U\n");
+ for (MusicDirectory.Entry e : playlist.getChildren()) {
+ String filePath = FileUtil.getSongFile(context, e).getAbsolutePath();
+ if(! new File(filePath).exists()){
+ String ext = FileUtil.getExtension(filePath);
+ String base = FileUtil.getBaseName(filePath);
+ filePath = base + ".complete." + ext;
+ }
+ fw.write(filePath + "\n");
+ }
+ } catch(Exception e) {
+ Log.w(TAG, "Failed to save playlist: " + playlist.getName());
+ } finally {
+ bw.close();
+ fw.close();
+ }
+ }
+ public static File getPlaylistDirectory(Context context) {
+ File playlistDir = new File(getSubsonicDirectory(context), "playlists");
+ ensureDirectoryExistsAndIsReadWritable(playlistDir);
+ return playlistDir;
+ }
+ public static File getPlaylistDirectory(Context context, String server) {
+ File playlistDir = new File(getPlaylistDirectory(context), server);
+ ensureDirectoryExistsAndIsReadWritable(playlistDir);
+ return playlistDir;
+ }
+
+ public static File getAlbumArtFile(Context context, MusicDirectory.Entry entry) {
+ File albumDir = getAlbumDirectory(context, entry);
+ File artFile;
+ File albumFile = getAlbumArtFile(albumDir);
+ File hexFile = getHexAlbumArtFile(context, albumDir);
+ if(albumDir.exists()) {
+ if(hexFile.exists()) {
+ hexFile.renameTo(albumFile);
+ }
+ artFile = albumFile;
+ } else {
+ artFile = hexFile;
+ }
+ return artFile;
+ }
+
+ public static File getAlbumArtFile(File albumDir) {
+ return new File(albumDir, Constants.ALBUM_ART_FILE);
+ }
+ public static File getHexAlbumArtFile(Context context, File albumDir) {
+ return new File(getAlbumArtDirectory(context), Util.md5Hex(albumDir.getPath()) + ".jpeg");
+ }
+
+ public static Bitmap getAlbumArtBitmap(Context context, MusicDirectory.Entry entry, int size) {
+ File albumArtFile = getAlbumArtFile(context, entry);
+ if (albumArtFile.exists()) {
+ final BitmapFactory.Options opt = new BitmapFactory.Options();
+ opt.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(albumArtFile.getPath(), opt);
+ opt.inPurgeable = true;
+ opt.inSampleSize = Util.calculateInSampleSize(opt, size, Util.getScaledHeight(opt.outHeight, opt.outWidth, size));
+ opt.inJustDecodeBounds = false;
+
+ Bitmap bitmap = BitmapFactory.decodeFile(albumArtFile.getPath(), opt);
+ return bitmap == null ? null : getScaledBitmap(bitmap, size);
+ }
+ return null;
+ }
+
+ public static File getAvatarDirectory(Context context) {
+ File avatarDir = new File(getSubsonicDirectory(context), "avatars");
+ ensureDirectoryExistsAndIsReadWritable(avatarDir);
+ ensureDirectoryExistsAndIsReadWritable(new File(avatarDir, ".nomedia"));
+ return avatarDir;
+ }
+
+ public static File getAvatarFile(Context context, String username) {
+ return new File(getAvatarDirectory(context), Util.md5Hex(username) + ".jpeg");
+ }
+
+ public static Bitmap getAvatarBitmap(Context context, String username, int size) {
+ File avatarFile = getAvatarFile(context, username);
+ if (avatarFile.exists()) {
+ final BitmapFactory.Options opt = new BitmapFactory.Options();
+ opt.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(avatarFile.getPath(), opt);
+ opt.inPurgeable = true;
+ opt.inSampleSize = Util.calculateInSampleSize(opt, size, Util.getScaledHeight(opt.outHeight, opt.outWidth, size));
+ opt.inJustDecodeBounds = false;
+
+ Bitmap bitmap = BitmapFactory.decodeFile(avatarFile.getPath(), opt);
+ return bitmap == null ? null : getScaledBitmap(bitmap, size, false);
+ }
+ return null;
+ }
+
+ public static File getMiscDirectory(Context context) {
+ File dir = new File(getSubsonicDirectory(context), "misc");
+ ensureDirectoryExistsAndIsReadWritable(dir);
+ ensureDirectoryExistsAndIsReadWritable(new File(dir, ".nomedia"));
+ return dir;
+ }
+
+ public static File getMiscFile(Context context, String url) {
+ return new File(getMiscDirectory(context), Util.md5Hex(url) + ".jpeg");
+ }
+
+ public static Bitmap getMiscBitmap(Context context, String url, int size) {
+ File avatarFile = getMiscFile(context, url);
+ if (avatarFile.exists()) {
+ final BitmapFactory.Options opt = new BitmapFactory.Options();
+ opt.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(avatarFile.getPath(), opt);
+ opt.inPurgeable = true;
+ opt.inSampleSize = Util.calculateInSampleSize(opt, size, Util.getScaledHeight(opt.outHeight, opt.outWidth, size));
+ opt.inJustDecodeBounds = false;
+
+ Bitmap bitmap = BitmapFactory.decodeFile(avatarFile.getPath(), opt);
+ return bitmap == null ? null : getScaledBitmap(bitmap, size, false);
+ }
+ return null;
+ }
+
+ public static Bitmap getSampledBitmap(byte[] bytes, int size) {
+ return getSampledBitmap(bytes, size, true);
+ }
+ public static Bitmap getSampledBitmap(byte[] bytes, int size, boolean allowUnscaled) {
+ final BitmapFactory.Options opt = new BitmapFactory.Options();
+ opt.inJustDecodeBounds = true;
+ BitmapFactory.decodeByteArray(bytes, 0, bytes.length, opt);
+ opt.inPurgeable = true;
+ opt.inSampleSize = Util.calculateInSampleSize(opt, size, Util.getScaledHeight(opt.outHeight, opt.outWidth, size));
+ opt.inJustDecodeBounds = false;
+ Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, opt);
+ if(bitmap == null) {
+ return null;
+ } else {
+ return getScaledBitmap(bitmap, size, allowUnscaled);
+ }
+ }
+ public static Bitmap getScaledBitmap(Bitmap bitmap, int size) {
+ return getScaledBitmap(bitmap, size, true);
+ }
+ public static Bitmap getScaledBitmap(Bitmap bitmap, int size, boolean allowUnscaled) {
+ // Don't waste time scaling if the difference is minor
+ // Large album arts still need to be scaled since displayed as is on now playing!
+ if(allowUnscaled && size < 400 && bitmap.getWidth() < (size * 1.1)) {
+ return bitmap;
+ } else {
+ return Bitmap.createScaledBitmap(bitmap, size, Util.getScaledHeight(bitmap, size), true);
+ }
+ }
+
+ public static File getAlbumArtDirectory(Context context) {
+ File albumArtDir = new File(getSubsonicDirectory(context), "artwork");
+ ensureDirectoryExistsAndIsReadWritable(albumArtDir);
+ ensureDirectoryExistsAndIsReadWritable(new File(albumArtDir, ".nomedia"));
+ return albumArtDir;
+ }
+
+ public static File getArtistDirectory(Context context, Artist artist) {
+ File dir = new File(getMusicDirectory(context).getPath() + "/" + fileSystemSafe(artist.getName()));
+ return dir;
+ }
+ public static File getArtistDirectory(Context context, MusicDirectory.Entry artist) {
+ File dir = new File(getMusicDirectory(context).getPath() + "/" + fileSystemSafe(artist.getTitle()));
+ return dir;
+ }
+
+ public static File getAlbumDirectory(Context context, MusicDirectory.Entry entry) {
+ File dir = null;
+ if (entry.getPath() != null) {
+ File f = new File(fileSystemSafeDir(entry.getPath()));
+ dir = new File(getMusicDirectory(context).getPath() + "/" + (entry.isDirectory() ? f.getPath() : f.getParent()));
+ } else {
+ MusicDirectory.Entry firstSong;
+ if(!Util.isOffline(context)) {
+ firstSong = lookupChild(context, entry, false);
+ if(firstSong != null) {
+ File songFile = FileUtil.getSongFile(context, firstSong);
+ dir = songFile.getParentFile();
+ }
+ }
+
+ if(dir == null) {
+ String artist = fileSystemSafe(entry.getArtist());
+ String album = fileSystemSafe(entry.getAlbum());
+ if("unnamed".equals(album)) {
+ album = fileSystemSafe(entry.getTitle());
+ }
+ dir = new File(getMusicDirectory(context).getPath() + "/" + artist + "/" + album);
+ }
+ }
+ return dir;
+ }
+
+ public static MusicDirectory.Entry lookupChild(Context context, MusicDirectory.Entry entry, boolean allowDir) {
+ // Initialize lookupMap if first time called
+ String lookupName = Util.getCacheName(context, "entryLookup");
+ if(entryLookup == null) {
+ entryLookup = deserialize(context, lookupName, HashMap.class);
+
+ // Create it if
+ if(entryLookup == null) {
+ entryLookup = new HashMap<String, MusicDirectory.Entry>();
+ }
+ }
+
+ // Check if this lookup has already been done before
+ MusicDirectory.Entry child = entryLookup.get(entry.getId());
+ if(child != null) {
+ return child;
+ }
+
+ // Do a special lookup since 4.7+ doesn't match artist/album to entry.getPath
+ String s = Util.getRestUrl(context, null, false) + entry.getId();
+ String cacheName = (Util.isTagBrowsing(context) ? "album-" : "directory-") + s.hashCode() + ".ser";
+ MusicDirectory entryDir = FileUtil.deserialize(context, cacheName, MusicDirectory.class);
+
+ if(entryDir != null) {
+ List<MusicDirectory.Entry> songs = entryDir.getChildren(allowDir, true);
+ if(songs.size() > 0) {
+ child = songs.get(0);
+ entryLookup.put(entry.getId(), child);
+ serialize(context, entryLookup, lookupName);
+ return child;
+ }
+ }
+
+ return null;
+ }
+
+ public static String getPodcastPath(Context context, PodcastEpisode episode) {
+ return fileSystemSafe(episode.getArtist()) + "/" + fileSystemSafe(episode.getTitle());
+ }
+ public static File getPodcastFile(Context context, String server) {
+ File dir = getPodcastDirectory(context);
+ return new File(dir.getPath() + "/" + fileSystemSafe(server));
+ }
+ public static File getPodcastDirectory(Context context) {
+ File dir = new File(context.getCacheDir(), "podcasts");
+ ensureDirectoryExistsAndIsReadWritable(dir);
+ return dir;
+ }
+ public static File getPodcastDirectory(Context context, PodcastChannel channel) {
+ File dir = new File(getMusicDirectory(context).getPath() + "/" + fileSystemSafe(channel.getName()));
+ return dir;
+ }
+ public static File getPodcastDirectory(Context context, String channel) {
+ File dir = new File(getMusicDirectory(context).getPath() + "/" + fileSystemSafe(channel));
+ return dir;
+ }
+
+ public static void createDirectoryForParent(File file) {
+ File dir = file.getParentFile();
+ if (!dir.exists()) {
+ if (!dir.mkdirs()) {
+ Log.e(TAG, "Failed to create directory " + dir);
+ }
+ }
+ }
+
+ private static File createDirectory(Context context, String name) {
+ File dir = new File(getSubsonicDirectory(context), name);
+ if (!dir.exists() && !dir.mkdirs()) {
+ Log.e(TAG, "Failed to create " + name);
+ }
+ return dir;
+ }
+
+ public static File getSubsonicDirectory(Context context) {
+ return context.getExternalFilesDir(null);
+ }
+
+ public static File getDefaultMusicDirectory(Context context) {
+ if(DEFAULT_MUSIC_DIR == null) {
+ File[] dirs;
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ dirs = context.getExternalMediaDirs();
+ } else {
+ dirs = ContextCompat.getExternalFilesDirs(context, null);
+ }
+
+ DEFAULT_MUSIC_DIR = new File(getBestDir(dirs), "music");
+ Log.d(TAG, "Default: " + DEFAULT_MUSIC_DIR.getAbsolutePath());
+
+ if (!DEFAULT_MUSIC_DIR.exists() && !DEFAULT_MUSIC_DIR.mkdirs()) {
+ Log.e(TAG, "Failed to create default dir " + DEFAULT_MUSIC_DIR);
+
+ // Some devices seem to have screwed up the new media directory API. Go figure. Try again with standard locations
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ dirs = ContextCompat.getExternalFilesDirs(context, null);
+
+ DEFAULT_MUSIC_DIR = new File(getBestDir(dirs), "music");
+ if (!DEFAULT_MUSIC_DIR.exists() && !DEFAULT_MUSIC_DIR.mkdirs()) {
+ Log.e(TAG, "Failed to create default dir " + DEFAULT_MUSIC_DIR);
+ } else {
+ Log.w(TAG, "Stupid OEM's messed up media dir API added in 5.0");
+ }
+ }
+ }
+ }
+
+ return DEFAULT_MUSIC_DIR;
+ }
+ private static File getBestDir(File[] dirs) {
+ // Past 5.0 we can query directly for SD Card
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ for(int i = 0; i < dirs.length; i++) {
+ if(dirs[i] != null && Environment.isExternalStorageRemovable(dirs[i])) {
+ return dirs[i];
+ }
+ }
+ }
+
+ // Before 5.0, we have to guess. Most of the time the SD card is last
+ for(int i = dirs.length - 1; i >= 0; i--) {
+ if(dirs[i] != null) {
+ return dirs[i];
+ }
+ }
+
+ // Should be impossible to be reached
+ return dirs[0];
+ }
+
+ public static File getMusicDirectory(Context context) {
+ String path = Util.getPreferences(context).getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, getDefaultMusicDirectory(context).getPath());
+ File dir = new File(path);
+ return ensureDirectoryExistsAndIsReadWritable(dir) ? dir : getDefaultMusicDirectory(context);
+ }
+ public static boolean deleteMusicDirectory(Context context) {
+ File musicDirectory = FileUtil.getMusicDirectory(context);
+ MediaStoreService mediaStore = new MediaStoreService(context);
+ return recursiveDelete(musicDirectory, mediaStore);
+ }
+ public static void deleteSerializedCache(Context context) {
+ for(File file: context.getCacheDir().listFiles()) {
+ if(file.getName().indexOf(".ser") != -1) {
+ file.delete();
+ }
+ }
+ }
+ public static boolean deleteArtworkCache(Context context) {
+ File artDirectory = FileUtil.getAlbumArtDirectory(context);
+ return recursiveDelete(artDirectory);
+ }
+ public static boolean deleteAvatarCache(Context context) {
+ File artDirectory = FileUtil.getAvatarDirectory(context);
+ return recursiveDelete(artDirectory);
+ }
+
+ public static boolean recursiveDelete(File dir) {
+ return recursiveDelete(dir, null);
+ }
+ public static boolean recursiveDelete(File dir, MediaStoreService mediaStore) {
+ if (dir != null && dir.exists()) {
+ File[] list = dir.listFiles();
+ if(list != null) {
+ for(File file: list) {
+ if(file.isDirectory()) {
+ if(!recursiveDelete(file, mediaStore)) {
+ return false;
+ }
+ } else if(file.exists()) {
+ if(!file.delete()) {
+ return false;
+ } else if(mediaStore != null) {
+ mediaStore.deleteFromMediaStore(file);
+ }
+ }
+ }
+ }
+ return dir.delete();
+ }
+ return false;
+ }
+
+ public static void deleteEmptyDir(File dir) {
+ try {
+ File[] children = dir.listFiles();
+ if(children == null) {
+ return;
+ }
+
+ // No songs left in the folder
+ if (children.length == 1 && children[0].getPath().equals(FileUtil.getAlbumArtFile(dir).getPath())) {
+ Util.delete(children[0]);
+ children = dir.listFiles();
+ }
+
+ // Delete empty directory
+ if (children.length == 0) {
+ Util.delete(dir);
+ }
+ } catch(Exception e) {
+ Log.w(TAG, "Error while trying to delete empty dir", e);
+ }
+ }
+
+ public static void unpinSong(Context context, File saveFile) {
+ // Don't try to unpin a song which isn't actually pinned
+ if(saveFile.getName().contains(".complete")) {
+ return;
+ }
+
+ // Unpin file, rename to .complete
+ File completeFile = new File(saveFile.getParent(), FileUtil.getBaseName(saveFile.getName()) +
+ ".complete." + FileUtil.getExtension(saveFile.getName()));
+
+ if(!saveFile.renameTo(completeFile)) {
+ Log.w(TAG, "Failed to upin " + saveFile + " to " + completeFile);
+ } else {
+ try {
+ new MediaStoreService(context).renameInMediaStore(completeFile, saveFile);
+ } catch(Exception e) {
+ Log.w(TAG, "Failed to write to media store");
+ }
+ }
+ }
+
+ public static boolean ensureDirectoryExistsAndIsReadWritable(File dir) {
+ if (dir == null) {
+ return false;
+ }
+
+ if (dir.exists()) {
+ if (!dir.isDirectory()) {
+ Log.w(TAG, dir + " exists but is not a directory.");
+ return false;
+ }
+ } else {
+ if (dir.mkdirs()) {
+ Log.i(TAG, "Created directory " + dir);
+ } else {
+ Log.w(TAG, "Failed to create directory " + dir);
+ return false;
+ }
+ }
+
+ if (!dir.canRead()) {
+ Log.w(TAG, "No read permission for directory " + dir);
+ return false;
+ }
+
+ if (!dir.canWrite()) {
+ Log.w(TAG, "No write permission for directory " + dir);
+ return false;
+ }
+ return true;
+ }
+ public static boolean verifyCanWrite(File dir) {
+ if(ensureDirectoryExistsAndIsReadWritable(dir)) {
+ try {
+ File tmp = new File(dir, "checkWrite");
+ tmp.createNewFile();
+ if(tmp.exists()) {
+ if(tmp.delete()) {
+ return true;
+ } else {
+ Log.w(TAG, "Failed to delete temp file, retrying");
+
+ // This should never be reached since this is a file DSub created!
+ Thread.sleep(100L);
+ tmp = new File(dir, "checkWrite");
+ if(tmp.delete()) {
+ return true;
+ } else {
+ Log.w(TAG, "Failed retry to delete temp file");
+ return false;
+ }
+ }
+ } else {
+ Log.w(TAG, "Temp file does not actually exist");
+ return false;
+ }
+ } catch(Exception e) {
+ Log.w(TAG, "Failed to create tmp file", e);
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Makes a given filename safe by replacing special characters like slashes ("/" and "\")
+ * with dashes ("-").
+ *
+ * @param filename The filename in question.
+ * @return The filename with special characters replaced by hyphens.
+ */
+ private static String fileSystemSafe(String filename) {
+ if (filename == null || filename.trim().length() == 0) {
+ return "unnamed";
+ }
+
+ for (String s : FILE_SYSTEM_UNSAFE) {
+ filename = filename.replace(s, "-");
+ }
+ return filename;
+ }
+
+ /**
+ * Makes a given filename safe by replacing special characters like colons (":")
+ * with dashes ("-").
+ *
+ * @param path The path of the directory in question.
+ * @return The the directory name with special characters replaced by hyphens.
+ */
+ private static String fileSystemSafeDir(String path) {
+ if (path == null || path.trim().length() == 0) {
+ return "";
+ }
+
+ for (String s : FILE_SYSTEM_UNSAFE_DIR) {
+ path = path.replace(s, "-");
+ }
+ return path;
+ }
+
+ /**
+ * Similar to {@link File#listFiles()}, but returns a sorted set.
+ * Never returns {@code null}, instead a warning is logged, and an empty set is returned.
+ */
+ public static SortedSet<File> listFiles(File dir) {
+ File[] files = dir.listFiles();
+ if (files == null) {
+ Log.w(TAG, "Failed to list children for " + dir.getPath());
+ return new TreeSet<File>();
+ }
+
+ return new TreeSet<File>(Arrays.asList(files));
+ }
+
+ public static SortedSet<File> listMediaFiles(File dir) {
+ SortedSet<File> files = listFiles(dir);
+ Iterator<File> iterator = files.iterator();
+ while (iterator.hasNext()) {
+ File file = iterator.next();
+ if (!file.isDirectory() && !isMediaFile(file)) {
+ iterator.remove();
+ }
+ }
+ return files;
+ }
+
+ private static boolean isMediaFile(File file) {
+ String extension = getExtension(file.getName());
+ return MUSIC_FILE_EXTENSIONS.contains(extension) || VIDEO_FILE_EXTENSIONS.contains(extension);
+ }
+
+ public static boolean isMusicFile(File file) {
+ String extension = getExtension(file.getName());
+ return MUSIC_FILE_EXTENSIONS.contains(extension);
+ }
+ public static boolean isVideoFile(File file) {
+ String extension = getExtension(file.getName());
+ return VIDEO_FILE_EXTENSIONS.contains(extension);
+ }
+
+ public static boolean isPlaylistFile(File file) {
+ String extension = getExtension(file.getName());
+ return PLAYLIST_FILE_EXTENSIONS.contains(extension);
+ }
+
+ /**
+ * Returns the extension (the substring after the last dot) of the given file. The dot
+ * is not included in the returned extension.
+ *
+ * @param name The filename in question.
+ * @return The extension, or an empty string if no extension is found.
+ */
+ public static String getExtension(String name) {
+ int index = name.lastIndexOf('.');
+ return index == -1 ? "" : name.substring(index + 1).toLowerCase();
+ }
+
+ /**
+ * Returns the base name (the substring before the last dot) of the given file. The dot
+ * is not included in the returned basename.
+ *
+ * @param name The filename in question.
+ * @return The base name, or an empty string if no basename is found.
+ */
+ public static String getBaseName(String name) {
+ int index = name.lastIndexOf('.');
+ return index == -1 ? name : name.substring(0, index);
+ }
+
+ public static Pair<Long, Long> getUsedSize(Context context, File file) {
+ long number = 0L;
+ long size = 0L;
+
+ if(file.isFile()) {
+ if(isMediaFile(file)) {
+ return new Pair<Long, Long>(1L, file.length());
+ } else {
+ return new Pair<Long, Long>(0L, 0L);
+ }
+ } else {
+ for (File child : FileUtil.listFiles(file)) {
+ Pair<Long, Long> pair = getUsedSize(context, child);
+ number += pair.getFirst();
+ size += pair.getSecond();
+ }
+ return new Pair<Long, Long>(number, size);
+ }
+ }
+
+ public static <T extends Serializable> boolean serialize(Context context, T obj, String fileName) {
+ Output out = null;
+ try {
+ RandomAccessFile file = new RandomAccessFile(context.getCacheDir() + "/" + fileName, "rw");
+ out = new Output(new FileOutputStream(file.getFD()));
+ synchronized (kryo) {
+ kryo.writeObject(out, obj);
+ }
+ return true;
+ } catch (Throwable x) {
+ Log.w(TAG, "Failed to serialize object to " + fileName);
+ return false;
+ } finally {
+ Util.close(out);
+ }
+ }
+
+ public static <T extends Serializable> T deserialize(Context context, String fileName, Class<T> tClass) {
+ return deserialize(context, fileName, tClass, 0);
+ }
+
+ public static <T extends Serializable> T deserialize(Context context, String fileName, Class<T> tClass, int hoursOld) {
+ Input in = null;
+ try {
+ File file = new File(context.getCacheDir(), fileName);
+ if(!file.exists()) {
+ return null;
+ }
+
+ if(hoursOld != 0) {
+ Date fileDate = new Date(file.lastModified());
+ // Convert into hours
+ long age = (new Date().getTime() - fileDate.getTime()) / 1000 / 3600;
+ if(age > hoursOld) {
+ return null;
+ }
+ }
+
+ RandomAccessFile randomFile = new RandomAccessFile(file, "r");
+
+ in = new Input(new FileInputStream(randomFile.getFD()));
+ synchronized (kryo) {
+ T result = kryo.readObject(in, tClass);
+ return result;
+ }
+ } catch(FileNotFoundException e) {
+ // Different error message
+ Log.w(TAG, "No serialization for object from " + fileName);
+ return null;
+ } catch (Throwable x) {
+ Log.w(TAG, "Failed to deserialize object from " + fileName);
+ return null;
+ } finally {
+ Util.close(in);
+ }
+ }
+
+ public static <T extends Serializable> boolean serializeCompressed(Context context, T obj, String fileName) {
+ Output out = null;
+ try {
+ RandomAccessFile file = new RandomAccessFile(context.getCacheDir() + "/" + fileName, "rw");
+ out = new Output(new DeflaterOutputStream(new FileOutputStream(file.getFD())));
+ synchronized (kryo) {
+ kryo.writeObject(out, obj);
+ }
+ return true;
+ } catch (Throwable x) {
+ Log.w(TAG, "Failed to serialize compressed object to " + fileName);
+ return false;
+ } finally {
+ Util.close(out);
+ }
+ }
+
+ @TargetApi(Build.VERSION_CODES.GINGERBREAD)
+ public static <T extends Serializable> T deserializeCompressed(Context context, String fileName, Class<T> tClass) {
+ Input in = null;
+ try {
+ RandomAccessFile file = new RandomAccessFile(context.getCacheDir() + "/" + fileName, "r");
+
+ in = new Input(new InflaterInputStream(new FileInputStream(file.getFD())));
+ synchronized (kryo) {
+ T result = kryo.readObject(in, tClass);
+ return result;
+ }
+ } catch(FileNotFoundException e) {
+ // Different error message
+ Log.w(TAG, "No serialization compressed for object from " + fileName);
+ return null;
+ } catch (Throwable x) {
+ Log.w(TAG, "Failed to deserialize compressed object from " + fileName);
+ return null;
+ } finally {
+ Util.close(in);
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/ImageLoader.java b/app/src/main/java/github/daneren2005/dsub/util/ImageLoader.java
new file mode 100644
index 00000000..1a0e8242
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/ImageLoader.java
@@ -0,0 +1,600 @@
+/*
+ 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.util;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.LinearGradient;
+import android.graphics.Paint;
+import android.graphics.Shader;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.TransitionDrawable;
+import android.media.RemoteControlClient;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.support.v4.util.LruCache;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.ArtistInfo;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.ServerInfo;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+
+/**
+ * Asynchronous loading of images, with caching.
+ * <p/>
+ * There should normally be only one instance of this class.
+ *
+ * @author Sindre Mehus
+ */
+public class ImageLoader {
+ private static final String TAG = ImageLoader.class.getSimpleName();
+
+ private Context context;
+ private LruCache<String, Bitmap> cache;
+ private Handler handler;
+ private Bitmap nowPlaying;
+ private final int imageSizeDefault;
+ private final int imageSizeLarge;
+ private final int avatarSizeDefault;
+ private boolean clearingCache = false;
+
+ private final static int[] COLORS = {0xFF33B5E5, 0xFFAA66CC, 0xFF99CC00, 0xFFFFBB33, 0xFFFF4444};
+
+ public ImageLoader(Context context) {
+ this.context = context;
+ handler = new Handler(Looper.getMainLooper());
+ final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
+ final int cacheSize = maxMemory / 4;
+
+ // Determine the density-dependent image sizes.
+ imageSizeDefault = context.getResources().getDrawable(R.drawable.unknown_album).getIntrinsicHeight();
+ DisplayMetrics metrics = context.getResources().getDisplayMetrics();
+ imageSizeLarge = Math.round(Math.min(metrics.widthPixels, metrics.heightPixels));
+ avatarSizeDefault = context.getResources().getDrawable(R.drawable.ic_social_person).getIntrinsicHeight();
+
+ cache = new LruCache<String, Bitmap>(cacheSize) {
+ @Override
+ protected int sizeOf(String key, Bitmap bitmap) {
+ return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
+ }
+
+ @Override
+ protected void entryRemoved(boolean evicted, String key, Bitmap oldBitmap, Bitmap newBitmap) {
+ if(evicted) {
+ if(oldBitmap != nowPlaying && key.indexOf("unknown") != 0 || clearingCache) {
+ if(sizeOf("", oldBitmap) > 500) {
+ oldBitmap.recycle();
+ }
+ } else {
+ cache.put(key, oldBitmap);
+ }
+ }
+ }
+ };
+ }
+
+ public void clearCache() {
+ nowPlaying = null;
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ clearingCache = true;
+ cache.evictAll();
+ clearingCache = false;
+ return null;
+ }
+ }.execute();
+ }
+
+ private Bitmap getUnknownImage(MusicDirectory.Entry entry, int size) {
+ String key;
+ int color;
+ if(entry == null) {
+ key = getKey("unknown", size);
+ color = COLORS[0];
+
+ return getUnknownImage(key, size, color, null, null);
+ } else {
+ key = getKey(entry.getId() + "unknown", size);
+ String hash;
+ if(entry.getAlbum() != null) {
+ hash = entry.getAlbum();
+ } else if(entry.getArtist() != null) {
+ hash = entry.getArtist();
+ } else {
+ hash = entry.getId();
+ }
+ color = COLORS[Math.abs(hash.hashCode()) % COLORS.length];
+
+ return getUnknownImage(key, size, color, entry.getAlbum(), entry.getArtist());
+ }
+ }
+ private Bitmap getUnknownImage(String key, int size, int color, String topText, String bottomText) {
+ Bitmap bitmap = cache.get(key);
+ if(bitmap == null) {
+ bitmap = createUnknownImage(size, color, topText, bottomText);
+ cache.put(key, bitmap);
+ }
+
+ return bitmap;
+ }
+ private Bitmap createUnknownImage(int size, int primaryColor, String topText, String bottomText) {
+ Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+
+ Paint color = new Paint();
+ color.setColor(primaryColor);
+ canvas.drawRect(0, 0, size, size * 2.0f / 3.0f, color);
+
+ color.setShader(new LinearGradient(0, 0, 0, size / 3.0f, Color.rgb(82, 82, 82), Color.BLACK, Shader.TileMode.MIRROR));
+ canvas.drawRect(0, size * 2.0f / 3.0f, size, size, color);
+
+ if(topText != null || bottomText != null) {
+ Paint font = new Paint();
+ font.setFlags(Paint.ANTI_ALIAS_FLAG);
+ font.setColor(Color.WHITE);
+ font.setTextSize(3.0f + size * 0.07f);
+
+ if(topText != null) {
+ canvas.drawText(topText, size * 0.05f, size * 0.6f, font);
+ }
+
+ if(bottomText != null) {
+ canvas.drawText(bottomText, size * 0.05f, size * 0.8f, font);
+ }
+ }
+
+ return bitmap;
+ }
+
+ public Bitmap getCachedImage(Context context, MusicDirectory.Entry entry, boolean large) {
+ int size = large ? imageSizeLarge : imageSizeDefault;
+ if(entry == null || entry.getCoverArt() == null) {
+ return getUnknownImage(entry, size);
+ }
+
+ Bitmap bitmap = cache.get(getKey(entry.getCoverArt(), size));
+ if(bitmap == null || bitmap.isRecycled()) {
+ bitmap = FileUtil.getAlbumArtBitmap(context, entry, size);
+ String key = getKey(entry.getCoverArt(), size);
+ cache.put(key, bitmap);
+ cache.get(key);
+ }
+
+ return bitmap;
+ }
+
+ public SilentBackgroundTask loadImage(View view, MusicDirectory.Entry entry, boolean large, boolean crossfade) {
+ // TODO: If we know this a artist, try to load artist info instead
+ int size = large ? imageSizeLarge : imageSizeDefault;
+ if(entry != null && !entry.isAlbum() && ServerInfo.checkServerVersion(context, "1.11") && !Util.isOffline(context)) {
+ SilentBackgroundTask task = new ArtistImageTask(view.getContext(), entry, size, imageSizeLarge, large, view, crossfade);
+ task.execute();
+ return task;
+ } else if(entry != null && entry.getCoverArt() == null && entry.isDirectory() && !Util.isOffline(context)) {
+ // Try to lookup child cover art
+ MusicDirectory.Entry firstChild = FileUtil.lookupChild(context, entry, true);
+ if(firstChild != null) {
+ entry.setCoverArt(firstChild.getCoverArt());
+ }
+ }
+
+ Bitmap bitmap;
+ if (entry == null || entry.getCoverArt() == null) {
+ bitmap = getUnknownImage(entry, size);
+ setImage(view, Util.createDrawableFromBitmap(context, bitmap), crossfade);
+ return null;
+ }
+
+ bitmap = cache.get(getKey(entry.getCoverArt(), size));
+ if (bitmap != null && !bitmap.isRecycled()) {
+ final Drawable drawable = Util.createDrawableFromBitmap(this.context, bitmap);
+ setImage(view, drawable, crossfade);
+ if(large) {
+ nowPlaying = bitmap;
+ }
+ return null;
+ }
+
+ if (!large) {
+ setImage(view, Util.createDrawableFromBitmap(context, null), false);
+ }
+ ImageTask task = new ViewImageTask(view.getContext(), entry, size, imageSizeLarge, large, view, crossfade);
+ task.execute();
+ return task;
+ }
+
+ public SilentBackgroundTask<Void> loadImage(View view, String url, boolean large) {
+ Bitmap bitmap;
+ int size = large ? imageSizeLarge : imageSizeDefault;
+ if (url == null) {
+ String key = getKey(url + "unknown", size);
+ int color = COLORS[Math.abs(key.hashCode()) % COLORS.length];
+ bitmap = getUnknownImage(key, size, color, null, null);
+ setImage(view, Util.createDrawableFromBitmap(context, bitmap), true);
+ return null;
+ }
+
+ bitmap = cache.get(getKey(url, size));
+ if (bitmap != null && !bitmap.isRecycled()) {
+ final Drawable drawable = Util.createDrawableFromBitmap(this.context, bitmap);
+ setImage(view, drawable, true);
+ return null;
+ }
+
+ SilentBackgroundTask<Void> task = new ViewUrlTask(view.getContext(), view, url, size);
+ task.execute();
+ return task;
+ }
+
+ public SilentBackgroundTask<Void> loadImage(Context context, RemoteControlClient remoteControl, MusicDirectory.Entry entry) {
+ Bitmap bitmap;
+ if (entry == null || entry.getCoverArt() == null) {
+ bitmap = getUnknownImage(entry, imageSizeLarge);
+ setImage(remoteControl, Util.createDrawableFromBitmap(context, bitmap));
+ return null;
+ }
+
+ bitmap = cache.get(getKey(entry.getCoverArt(), imageSizeLarge));
+ if (bitmap != null && !bitmap.isRecycled()) {
+ Drawable drawable = Util.createDrawableFromBitmap(this.context, bitmap);
+ setImage(remoteControl, drawable);
+ return null;
+ }
+
+ setImage(remoteControl, Util.createDrawableFromBitmap(context, null));
+ ImageTask task = new RemoteControlClientImageTask(context, entry, imageSizeLarge, imageSizeLarge, false, remoteControl);
+ task.execute();
+ return task;
+ }
+
+ public SilentBackgroundTask<Void> loadAvatar(Context context, ImageView view, String username) {
+ Bitmap bitmap = cache.get(username);
+ if (bitmap != null && !bitmap.isRecycled()) {
+ Drawable drawable = Util.createDrawableFromBitmap(this.context, bitmap);
+ view.setImageDrawable(drawable);
+ return null;
+ }
+
+ SilentBackgroundTask<Void> task = new AvatarTask(context, view, username);
+ task.execute();
+ return task;
+ }
+
+ private String getKey(String coverArtId, int size) {
+ return coverArtId + size;
+ }
+
+ private void setImage(View view, final Drawable drawable, boolean crossfade) {
+ if (view instanceof TextView) {
+ // Cross-fading is not implemented for TextView since it's not in use. It would be easy to add it, though.
+ TextView textView = (TextView) view;
+ textView.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null);
+ } else if (view instanceof ImageView) {
+ final ImageView imageView = (ImageView) view;
+ if (crossfade && drawable != null) {
+ Drawable existingDrawable = imageView.getDrawable();
+ if (existingDrawable == null) {
+ Bitmap emptyImage;
+ if(drawable.getIntrinsicWidth() > 0 && drawable.getIntrinsicHeight() > 0) {
+ emptyImage = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
+ } else {
+ emptyImage = Bitmap.createBitmap(imageSizeDefault, imageSizeDefault, Bitmap.Config.ARGB_8888);
+ }
+ existingDrawable = new BitmapDrawable(context.getResources(), emptyImage);
+ } else if(existingDrawable instanceof TransitionDrawable) {
+ // This should only ever be used if user is skipping through many songs quickly
+ TransitionDrawable tmp = (TransitionDrawable) existingDrawable;
+ existingDrawable = tmp.getDrawable(tmp.getNumberOfLayers() - 1);
+ }
+ if(existingDrawable != null && drawable != null) {
+ Drawable[] layers = new Drawable[]{existingDrawable, drawable};
+ final TransitionDrawable transitionDrawable = new TransitionDrawable(layers);
+ imageView.setImageDrawable(transitionDrawable);
+ transitionDrawable.startTransition(250);
+
+ // Get rid of transition drawable after transition occurs
+ handler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ // Only execute if still on same transition drawable
+ if (imageView.getDrawable() == transitionDrawable) {
+ imageView.setImageDrawable(drawable);
+ }
+ }
+ }, 500L);
+ } else {
+ imageView.setImageDrawable(drawable);
+ }
+ } else {
+ imageView.setImageDrawable(drawable);
+ }
+ }
+ }
+
+ @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
+ private void setImage(RemoteControlClient remoteControl, Drawable drawable) {
+ if(remoteControl != null && drawable != null) {
+ Bitmap origBitmap = ((BitmapDrawable)drawable).getBitmap();
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && origBitmap != null) {
+ origBitmap = origBitmap.copy(origBitmap.getConfig(), false);
+ }
+ if ( origBitmap != null && !origBitmap.isRecycled()) {
+ remoteControl.editMetadata(false).putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, origBitmap).apply();
+ } else {
+ if(origBitmap != null) {
+ Log.e(TAG, "Tried to load a recycled bitmap.");
+ }
+ remoteControl.editMetadata(false)
+ .putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, null)
+ .apply();
+ }
+ }
+ }
+
+ public abstract class ImageTask extends SilentBackgroundTask<Void> {
+ private final Context mContext;
+ private final MusicDirectory.Entry mEntry;
+ private final int mSize;
+ private final int mSaveSize;
+ private final boolean mIsNowPlaying;
+ protected Drawable mDrawable;
+
+ public ImageTask(Context context, MusicDirectory.Entry entry, int size, int saveSize, boolean isNowPlaying) {
+ super(context);
+ mContext = context;
+ mEntry = entry;
+ mSize = size;
+ mSaveSize = saveSize;
+ mIsNowPlaying = isNowPlaying;
+ }
+
+ @Override
+ protected Void doInBackground() throws Throwable {
+ try {
+ MusicService musicService = MusicServiceFactory.getMusicService(mContext);
+ Bitmap bitmap = musicService.getCoverArt(mContext, mEntry, mSize, null, this);
+ String key = getKey(mEntry.getCoverArt(), mSize);
+ cache.put(key, bitmap);
+ // Make sure key is the most recently "used"
+ cache.get(key);
+ if(mIsNowPlaying) {
+ nowPlaying = bitmap;
+ }
+
+ mDrawable = Util.createDrawableFromBitmap(mContext, bitmap);
+ } catch (Throwable x) {
+ Log.e(TAG, "Failed to download album art.", x);
+ cancelled.set(true);
+ }
+
+ return null;
+ }
+ }
+
+ private class ViewImageTask extends ImageTask {
+ protected boolean mCrossfade;
+ private View mView;
+
+ public ViewImageTask(Context context, MusicDirectory.Entry entry, int size, int saveSize, boolean isNowPlaying, View view, boolean crossfade) {
+ super(context, entry, size, saveSize, isNowPlaying);
+
+ mView = view;
+ mCrossfade = crossfade;
+ }
+
+ @Override
+ protected void done(Void result) {
+ setImage(mView, mDrawable, mCrossfade);
+ }
+ }
+
+ private class RemoteControlClientImageTask extends ImageTask {
+ private RemoteControlClient mRemoteControl;
+
+ public RemoteControlClientImageTask(Context context, MusicDirectory.Entry entry, int size, int saveSize, boolean isNowPlaying, RemoteControlClient remoteControl) {
+ super(context, entry, size, saveSize, isNowPlaying);
+
+ mRemoteControl = remoteControl;
+ }
+
+ @Override
+ protected void done(Void result) {
+ setImage(mRemoteControl, mDrawable);
+ }
+ }
+
+ private class ArtistImageTask extends SilentBackgroundTask<Void> {
+ private final Context mContext;
+ private final MusicDirectory.Entry mEntry;
+ private final int mSize;
+ private final int mSaveSize;
+ private final boolean mIsNowPlaying;
+ private Drawable mDrawable;
+ private boolean mCrossfade;
+ private View mView;
+
+ private SilentBackgroundTask subTask;
+
+ public ArtistImageTask(Context context, MusicDirectory.Entry entry, int size, int saveSize, boolean isNowPlaying, View view, boolean crossfade) {
+ super(context);
+ mContext = context;
+ mEntry = entry;
+ mSize = size;
+ mSaveSize = saveSize;
+ mIsNowPlaying = isNowPlaying;
+ mView = view;
+ mCrossfade = crossfade;
+ }
+
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(mContext);
+ ArtistInfo artistInfo = musicService.getArtistInfo(mEntry.getId(), false, true, mContext, null);
+ String url = artistInfo.getImageUrl();
+
+ // Figure out whether we are going to get a artist image or the standard image
+ if(url != null && !"".equals(url.trim())) {
+ // If getting the artist image fails for any reason, retry for the standard version
+ subTask = new ViewUrlTask(mContext, mView, url, mSize) {
+ @Override
+ protected void failedToDownload() {
+ // Call loadImage so we can take advantage of all of it's logic checks
+ loadImage(mView, mEntry, mSize == imageSizeLarge, mCrossfade);
+
+ // Delete subTask so it doesn't get called in done
+ subTask = null;
+ }
+ };
+ } else {
+ if (mEntry != null && mEntry.getCoverArt() == null && mEntry.isDirectory() && !Util.isOffline(context)) {
+ // Try to lookup child cover art
+ MusicDirectory.Entry firstChild = FileUtil.lookupChild(context, mEntry, true);
+ if (firstChild != null) {
+ mEntry.setCoverArt(firstChild.getCoverArt());
+ }
+ }
+
+ if (mEntry != null && mEntry.getCoverArt() != null) {
+ subTask = new ViewImageTask(mContext, mEntry, mSize, mSaveSize, mIsNowPlaying, mView, mCrossfade);
+ } else {
+ // If entry is null as well, we need to just set as a blank image
+ Bitmap bitmap = getUnknownImage(mEntry, mSize);
+ mDrawable = Util.createDrawableFromBitmap(mContext, bitmap);
+ return null;
+ }
+ }
+
+ // Execute whichever way we decided to go
+ subTask.doInBackground();
+ return null;
+ }
+
+ @Override
+ public void done(Void result) {
+ if(subTask != null) {
+ subTask.done(result);
+ } else if(mDrawable != null) {
+ setImage(mView, mDrawable, mCrossfade);
+ }
+ }
+ }
+
+ private class ViewUrlTask extends SilentBackgroundTask<Void> {
+ private final Context mContext;
+ private final String mUrl;
+ private final ImageView mView;
+ private Drawable mDrawable;
+ private int mSize;
+
+ public ViewUrlTask(Context context, View view, String url, int size) {
+ super(context);
+ mContext = context;
+ mView = (ImageView) view;
+ mUrl = url;
+ mSize = size;
+ }
+
+ @Override
+ protected Void doInBackground() throws Throwable {
+ try {
+ MusicService musicService = MusicServiceFactory.getMusicService(mContext);
+ Bitmap bitmap = musicService.getBitmap(mUrl, mSize, mContext, null, this);
+ if(bitmap != null) {
+ String key = getKey(mUrl, mSize);
+ cache.put(key, bitmap);
+ // Make sure key is the most recently "used"
+ cache.get(key);
+
+ mDrawable = Util.createDrawableFromBitmap(mContext, bitmap);
+ }
+ } catch (Throwable x) {
+ Log.e(TAG, "Failed to download from url " + mUrl, x);
+ cancelled.set(true);
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ if(mDrawable != null) {
+ mView.setImageDrawable(mDrawable);
+ } else {
+ failedToDownload();
+ }
+ }
+
+ protected void failedToDownload() {
+
+ }
+ }
+
+ private class AvatarTask extends SilentBackgroundTask<Void> {
+ private final Context mContext;
+ private final String mUsername;
+ private final ImageView mView;
+ private Drawable mDrawable;
+
+ public AvatarTask(Context context, ImageView view, String username) {
+ super(context);
+ mContext = context;
+ mView = view;
+ mUsername = username;
+ }
+
+ @Override
+ protected Void doInBackground() throws Throwable {
+ try {
+ MusicService musicService = MusicServiceFactory.getMusicService(mContext);
+ Bitmap bitmap = musicService.getAvatar(mUsername, avatarSizeDefault, mContext, null, this);
+ if(bitmap != null) {
+ cache.put(mUsername, bitmap);
+ // Make sure key is the most recently "used"
+ cache.get(mUsername);
+
+ mDrawable = Util.createDrawableFromBitmap(mContext, bitmap);
+ }
+ } catch (Throwable x) {
+ Log.e(TAG, "Failed to download album art.", x);
+ cancelled.set(true);
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ if(mDrawable != null) {
+ mView.setImageDrawable(mDrawable);
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/LoadingTask.java b/app/src/main/java/github/daneren2005/dsub/util/LoadingTask.java
new file mode 100644
index 00000000..116da816
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/LoadingTask.java
@@ -0,0 +1,73 @@
+package github.daneren2005.dsub.util;
+
+import android.app.Activity;
+import android.app.ProgressDialog;
+import android.content.DialogInterface;
+
+import github.daneren2005.dsub.activity.SubsonicActivity;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public abstract class LoadingTask<T> extends BackgroundTask<T> {
+
+ private final Activity tabActivity;
+ private ProgressDialog loading;
+ private final boolean cancellable;
+
+ public LoadingTask(Activity activity) {
+ super(activity);
+ tabActivity = activity;
+ this.cancellable = true;
+ }
+ public LoadingTask(Activity activity, final boolean cancellable) {
+ super(activity);
+ tabActivity = activity;
+ this.cancellable = cancellable;
+ }
+
+ @Override
+ public void execute() {
+ loading = ProgressDialog.show(tabActivity, "", "Loading. Please Wait...", true, cancellable, new DialogInterface.OnCancelListener() {
+ public void onCancel(DialogInterface dialog) {
+ cancel();
+ }
+ });
+
+ queue.offer(task = new Task() {
+ @Override
+ public void onDone(T result) {
+ if(loading.isShowing()) {
+ loading.dismiss();
+ }
+ done(result);
+ }
+
+ @Override
+ public void onError(Throwable t) {
+ if(loading.isShowing()) {
+ loading.dismiss();
+ }
+ error(t);
+ }
+ });
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return (tabActivity instanceof SubsonicActivity && ((SubsonicActivity) tabActivity).isDestroyedCompat()) || cancelled.get();
+ }
+
+ @Override
+ public void updateProgress(final String message) {
+ if(!cancelled.get()) {
+ getHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ loading.setMessage(message);
+ }
+ });
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/MediaRouteManager.java b/app/src/main/java/github/daneren2005/dsub/util/MediaRouteManager.java
new file mode 100644
index 00000000..9aa54c4b
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/MediaRouteManager.java
@@ -0,0 +1,181 @@
+/*
+ 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.util;
+
+import android.os.Build;
+import android.support.v7.media.MediaRouteProvider;
+import android.support.v7.media.MediaRouteSelector;
+import android.support.v7.media.MediaRouter;
+import android.util.Log;
+
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.GooglePlayServicesUtil;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import github.daneren2005.dsub.domain.RemoteControlState;
+import github.daneren2005.dsub.provider.DLNARouteProvider;
+import github.daneren2005.dsub.provider.JukeboxRouteProvider;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.service.RemoteController;
+import github.daneren2005.dsub.util.compat.CastCompat;
+
+import static android.support.v7.media.MediaRouter.RouteInfo;
+
+/**
+ * Created by owner on 2/8/14.
+ */
+public class MediaRouteManager extends MediaRouter.Callback {
+ private static final String TAG = MediaRouteManager.class.getSimpleName();
+ private static boolean castAvailable = false;
+
+ private DownloadService downloadService;
+ private MediaRouter router;
+ private MediaRouteSelector selector;
+ private List<MediaRouteProvider> providers = new ArrayList<MediaRouteProvider>();
+ private List<MediaRouteProvider> onlineProviders = new ArrayList<MediaRouteProvider>();
+
+ static {
+ try {
+ CastCompat.checkAvailable();
+ castAvailable = true;
+ } catch(Throwable t) {
+ castAvailable = false;
+ }
+ }
+
+ public MediaRouteManager(DownloadService downloadService) {
+ this.downloadService = downloadService;
+ router = MediaRouter.getInstance(downloadService);
+
+ // Check if play services is available
+ int result = GooglePlayServicesUtil.isGooglePlayServicesAvailable(downloadService);
+ if(result != ConnectionResult.SUCCESS){
+ Log.w(TAG, "No play services, failed with result: " + result);
+ castAvailable = false;
+ }
+
+ addProviders();
+ buildSelector();
+ }
+
+ public void destroy() {
+ for(MediaRouteProvider provider: providers) {
+ router.removeProvider(provider);
+ }
+ }
+
+ @Override
+ public void onRouteSelected(MediaRouter router, RouteInfo info) {
+ if(castAvailable) {
+ RemoteController controller = CastCompat.getController(downloadService, info);
+ if(controller != null) {
+ downloadService.setRemoteEnabled(RemoteControlState.CHROMECAST, controller);
+ }
+ }
+
+ if(downloadService.isRemoteEnabled()) {
+ downloadService.registerRoute(router);
+ }
+ }
+ @Override
+ public void onRouteUnselected(MediaRouter router, RouteInfo info) {
+ if(downloadService.isRemoteEnabled()) {
+ downloadService.unregisterRoute(router);
+ }
+
+ downloadService.setRemoteEnabled(RemoteControlState.LOCAL);
+ }
+
+ public void setDefaultRoute() {
+ router.selectRoute(router.getDefaultRoute());
+ }
+
+ public void startScan() {
+ router.addCallback(selector, this, MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);
+ }
+ public void stopScan() {
+ router.removeCallback(this);
+ }
+
+ public MediaRouteSelector getSelector() {
+ return selector;
+ }
+
+ public RouteInfo getSelectedRoute() {
+ return router.getSelectedRoute();
+ }
+ public RouteInfo getRouteForId(String id) {
+ if(id == null) {
+ return null;
+ }
+
+ // Try to find matching id
+ for(RouteInfo info: router.getRoutes()) {
+ if(id.equals(info.getId())) {
+ router.selectRoute(info);
+ return info;
+ }
+ }
+
+ return null;
+ }
+ public RemoteController getRemoteController(RouteInfo info) {
+ if(castAvailable) {
+ return CastCompat.getController(downloadService, info);
+ } else {
+ return null;
+ }
+ }
+
+ public void addOnlineProviders() {
+ JukeboxRouteProvider jukeboxProvider = new JukeboxRouteProvider(downloadService);
+ router.addProvider(jukeboxProvider);
+ providers.add(jukeboxProvider);
+ onlineProviders.add(jukeboxProvider);
+ }
+ public void removeOnlineProviders() {
+ for(MediaRouteProvider provider: onlineProviders) {
+ router.removeProvider(provider);
+ }
+ }
+
+ private void addProviders() {
+ if(!Util.isOffline(downloadService)) {
+ addOnlineProviders();
+ }
+
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ DLNARouteProvider dlnaProvider = new DLNARouteProvider(downloadService);
+ router.addProvider(dlnaProvider);
+ providers.add(dlnaProvider);
+ }
+ }
+ public void buildSelector() {
+ MediaRouteSelector.Builder builder = new MediaRouteSelector.Builder();
+ if(UserUtil.canJukebox()) {
+ builder.addControlCategory(JukeboxRouteProvider.CATEGORY_JUKEBOX_ROUTE);
+ }
+ if(castAvailable) {
+ builder.addControlCategory(CastCompat.getCastControlCategory());
+ }
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ builder.addControlCategory(DLNARouteProvider.CATEGORY_DLNA);
+ }
+ selector = builder.build();
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/Notifications.java b/app/src/main/java/github/daneren2005/dsub/util/Notifications.java
new file mode 100644
index 00000000..d078d77e
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/Notifications.java
@@ -0,0 +1,348 @@
+/*
+ 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.util;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.os.Build;
+import android.os.Handler;
+import android.support.v4.app.NotificationCompat;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.RemoteViews;
+import android.widget.TextView;
+
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.activity.SubsonicActivity;
+import github.daneren2005.dsub.activity.SubsonicFragmentActivity;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.PlayerState;
+import github.daneren2005.dsub.provider.DSubWidgetProvider;
+import github.daneren2005.dsub.service.DownloadFile;
+import github.daneren2005.dsub.service.DownloadService;
+
+public final class Notifications {
+ private static final String TAG = Notifications.class.getSimpleName();
+
+ // Notification IDs.
+ public static final int NOTIFICATION_ID_PLAYING = 100;
+ public static final int NOTIFICATION_ID_DOWNLOADING = 102;
+ public static final String NOTIFICATION_SYNC_GROUP = "github.daneren2005.dsub.sync";
+
+ private static boolean playShowing = false;
+ private static boolean downloadShowing = false;
+ private static boolean downloadForeground = false;
+
+ private final static Pair<Integer, Integer> NOTIFICATION_TEXT_COLORS = new Pair<Integer, Integer>();
+
+ public static void showPlayingNotification(final Context context, final DownloadService downloadService, final Handler handler, MusicDirectory.Entry song) {
+ // Set the icon, scrolling text and timestamp
+ final Notification notification = new Notification(R.drawable.stat_notify_playing, song.getTitle(), System.currentTimeMillis());
+
+ final boolean playing = downloadService.getPlayerState() == PlayerState.STARTED;
+ if(playing) {
+ notification.flags |= Notification.FLAG_NO_CLEAR | Notification.FLAG_ONGOING_EVENT;
+ }
+ boolean remote = downloadService.isRemoteEnabled();
+ if (Build.VERSION.SDK_INT>= Build.VERSION_CODES.JELLY_BEAN){
+ RemoteViews expandedContentView = new RemoteViews(context.getPackageName(), R.layout.notification_expanded);
+ setupViews(expandedContentView ,context, song, true, playing, remote);
+ notification.bigContentView = expandedContentView;
+ notification.priority = Notification.PRIORITY_HIGH;
+ }
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ notification.visibility = Notification.VISIBILITY_PUBLIC;
+ }
+
+ RemoteViews smallContentView = new RemoteViews(context.getPackageName(), R.layout.notification);
+ setupViews(smallContentView, context, song, false, playing, remote);
+ notification.contentView = smallContentView;
+
+ Intent notificationIntent = new Intent(context, SubsonicFragmentActivity.class);
+ notificationIntent.putExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD, true);
+ notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ notification.contentIntent = PendingIntent.getActivity(context, 0, notificationIntent, 0);
+
+ playShowing = true;
+ if(downloadForeground && downloadShowing) {
+ downloadForeground = false;
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ downloadService.stopForeground(true);
+ showDownloadingNotification(context, downloadService, handler, downloadService.getCurrentDownloading(), downloadService.getBackgroundDownloads().size());
+ downloadService.startForeground(NOTIFICATION_ID_PLAYING, notification);
+ }
+ });
+ } else {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ if(playing) {
+ downloadService.startForeground(NOTIFICATION_ID_PLAYING, notification);
+ } else {
+ playShowing = false;
+ NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ downloadService.stopForeground(true);
+ notificationManager.notify(NOTIFICATION_ID_PLAYING, notification);
+ }
+ }
+ });
+ }
+
+ // Update widget
+ DSubWidgetProvider.notifyInstances(context, downloadService, playing);
+ }
+
+ private static void setupViews(RemoteViews rv, Context context, MusicDirectory.Entry song, boolean expanded, boolean playing, boolean remote){
+
+ // Use the same text for the ticker and the expanded notification
+ String title = song.getTitle();
+ String arist = song.getArtist();
+ String album = song.getAlbum();
+
+ // Set the album art.
+ try {
+ ImageLoader imageLoader = SubsonicActivity.getStaticImageLoader(context);
+ Bitmap bitmap = null;
+ if(imageLoader != null) {
+ bitmap = imageLoader.getCachedImage(context, song, false);
+ }
+ if (bitmap == null) {
+ // set default album art
+ rv.setImageViewResource(R.id.notification_image, R.drawable.unknown_album);
+ } else {
+ rv.setImageViewBitmap(R.id.notification_image, bitmap);
+ }
+ } catch (Exception x) {
+ Log.w(TAG, "Failed to get notification cover art", x);
+ rv.setImageViewResource(R.id.notification_image, R.drawable.unknown_album);
+ }
+
+ // set the text for the notifications
+ rv.setTextViewText(R.id.notification_title, title);
+ rv.setTextViewText(R.id.notification_artist, arist);
+ rv.setTextViewText(R.id.notification_album, album);
+
+ boolean persistent = Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_PERSISTENT_NOTIFICATION, false);
+ if(persistent) {
+ if(expanded) {
+ rv.setImageViewResource(R.id.control_pause, playing ? R.drawable.notification_pause : R.drawable.notification_play);
+ } else {
+ rv.setImageViewResource(R.id.control_previous, playing ? R.drawable.notification_pause : R.drawable.notification_play);
+ rv.setImageViewResource(R.id.control_pause, R.drawable.notification_next);
+ rv.setImageViewResource(R.id.control_next, R.drawable.notification_close);
+ }
+ }
+
+ // Create actions for media buttons
+ PendingIntent pendingIntent;
+ int previous = 0, pause = 0, next = 0, close = 0;
+ if(persistent && !expanded) {
+ pause = R.id.control_previous;
+ next = R.id.control_pause;
+ close = R.id.control_next;
+ } else {
+ previous = R.id.control_previous;
+ pause = R.id.control_pause;
+ next = R.id.control_next;
+ }
+
+ if((remote || persistent) && close == 0 && expanded) {
+ close = R.id.notification_close;
+ rv.setViewVisibility(close, View.VISIBLE);
+ }
+
+ if(previous > 0) {
+ Intent prevIntent = new Intent("KEYCODE_MEDIA_PREVIOUS");
+ prevIntent.setComponent(new ComponentName(context, DownloadService.class));
+ prevIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PREVIOUS));
+ pendingIntent = PendingIntent.getService(context, 0, prevIntent, 0);
+ rv.setOnClickPendingIntent(previous, pendingIntent);
+ }
+ if(pause > 0) {
+ if(playing) {
+ Intent pauseIntent = new Intent("KEYCODE_MEDIA_PLAY_PAUSE");
+ pauseIntent.setComponent(new ComponentName(context, DownloadService.class));
+ pauseIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE));
+ pendingIntent = PendingIntent.getService(context, 0, pauseIntent, 0);
+ rv.setOnClickPendingIntent(pause, pendingIntent);
+ } else {
+ Intent prevIntent = new Intent("KEYCODE_MEDIA_START");
+ prevIntent.setComponent(new ComponentName(context, DownloadService.class));
+ prevIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY));
+ pendingIntent = PendingIntent.getService(context, 0, prevIntent, 0);
+ rv.setOnClickPendingIntent(pause, pendingIntent);
+ }
+ }
+ if(next > 0) {
+ Intent nextIntent = new Intent("KEYCODE_MEDIA_NEXT");
+ nextIntent.setComponent(new ComponentName(context, DownloadService.class));
+ nextIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT));
+ pendingIntent = PendingIntent.getService(context, 0, nextIntent, 0);
+ rv.setOnClickPendingIntent(next, pendingIntent);
+ }
+ if(close > 0) {
+ Intent prevIntent = new Intent("KEYCODE_MEDIA_STOP");
+ prevIntent.setComponent(new ComponentName(context, DownloadService.class));
+ prevIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_STOP));
+ pendingIntent = PendingIntent.getService(context, 0, prevIntent, 0);
+ rv.setOnClickPendingIntent(close, pendingIntent);
+ }
+ }
+
+ public static void hidePlayingNotification(final Context context, final DownloadService downloadService, Handler handler) {
+ playShowing = false;
+
+ // Remove notification and remove the service from the foreground
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ downloadService.stopForeground(true);
+ }
+ });
+
+ // Get downloadNotification in foreground if playing
+ if(downloadShowing) {
+ showDownloadingNotification(context, downloadService, handler, downloadService.getCurrentDownloading(), downloadService.getBackgroundDownloads().size());
+ }
+
+ // Update widget
+ DSubWidgetProvider.notifyInstances(context, downloadService, false);
+ }
+
+ public static void showDownloadingNotification(final Context context, final DownloadService downloadService, Handler handler, DownloadFile file, int size) {
+ Intent cancelIntent = new Intent(context, DownloadService.class);
+ cancelIntent.setAction(DownloadService.CANCEL_DOWNLOADS);
+ PendingIntent cancelPI = PendingIntent.getService(context, 0, cancelIntent, 0);
+
+ String currentDownloading, currentSize;
+ if(file != null) {
+ currentDownloading = file.getSong().getTitle();
+ currentSize = Util.formatLocalizedBytes(file.getEstimatedSize(), context);
+ } else {
+ currentDownloading = "none";
+ currentSize = "0";
+ }
+
+ NotificationCompat.Builder builder;
+ builder = new NotificationCompat.Builder(context)
+ .setSmallIcon(android.R.drawable.stat_sys_download)
+ .setContentTitle(context.getResources().getString(R.string.download_downloading_title, size))
+ .setContentText(context.getResources().getString(R.string.download_downloading_summary, currentDownloading))
+ .setStyle(new NotificationCompat.BigTextStyle()
+ .bigText(context.getResources().getString(R.string.download_downloading_summary_expanded, currentDownloading, currentSize)))
+ .setProgress(10, 5, true)
+ .setOngoing(true)
+ .addAction(R.drawable.notification_close,
+ context.getResources().getString(R.string.common_cancel),
+ cancelPI);
+
+ Intent notificationIntent = new Intent(context, SubsonicFragmentActivity.class);
+ notificationIntent.putExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD, true);
+ notificationIntent.putExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD_VIEW, true);
+ notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ builder.setContentIntent(PendingIntent.getActivity(context, 1, notificationIntent, 0));
+
+ final Notification notification = builder.build();
+ downloadShowing = true;
+ if(playShowing) {
+ NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ notificationManager.notify(NOTIFICATION_ID_DOWNLOADING, notification);
+ } else {
+ downloadForeground = true;
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ downloadService.startForeground(NOTIFICATION_ID_DOWNLOADING, notification);
+ }
+ });
+ }
+
+ }
+ public static void hideDownloadingNotification(final Context context, final DownloadService downloadService, Handler handler) {
+ downloadShowing = false;
+ if(playShowing) {
+ NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ notificationManager.cancel(NOTIFICATION_ID_DOWNLOADING);
+ } else {
+ downloadForeground = false;
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ downloadService.stopForeground(true);
+ }
+ });
+ }
+ }
+
+ public static void showSyncNotification(final Context context, int stringId, String extra) {
+ if(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_SYNC_NOTIFICATION, true)) {
+ if(extra == null) {
+ extra = "";
+ }
+
+ NotificationCompat.Builder builder;
+ builder = new NotificationCompat.Builder(context)
+ .setSmallIcon(R.drawable.stat_notify_sync)
+ .setContentTitle(context.getResources().getString(stringId))
+ .setContentText(extra)
+ .setStyle(new NotificationCompat.BigTextStyle().bigText(extra.replace(", ", "\n")))
+ .setOngoing(false)
+ .setGroup(NOTIFICATION_SYNC_GROUP)
+ .setPriority(NotificationCompat.PRIORITY_LOW)
+ .setAutoCancel(true);
+
+ Intent notificationIntent = new Intent(context, SubsonicFragmentActivity.class);
+ notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+
+ String tab = null, type = null;
+ switch(stringId) {
+ case R.string.sync_new_albums:
+ type = "newest";
+ break;
+ case R.string.sync_new_playlists:
+ tab = "Playlist";
+ break;
+ case R.string.sync_new_podcasts:
+ tab = "Podcast";
+ break;
+ case R.string.sync_new_starred:
+ type = "starred";
+ break;
+ }
+ if(tab != null) {
+ notificationIntent.putExtra(Constants.INTENT_EXTRA_FRAGMENT_TYPE, tab);
+ }
+ if(type != null) {
+ notificationIntent.putExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, type);
+ }
+
+ builder.setContentIntent(PendingIntent.getActivity(context, stringId, notificationIntent, 0));
+
+ NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ notificationManager.notify(stringId, builder.build());
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/Pair.java b/app/src/main/java/github/daneren2005/dsub/util/Pair.java
new file mode 100644
index 00000000..54386a62
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/Pair.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 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.util;
+
+import java.io.Serializable;
+
+/**
+ * @author Sindre Mehus
+ */
+public class Pair<S, T> implements Serializable {
+
+ private S first;
+ private T second;
+
+ public Pair() {
+ }
+
+ public Pair(S first, T second) {
+ this.first = first;
+ this.second = second;
+ }
+
+ public S getFirst() {
+ return first;
+ }
+
+ public void setFirst(S first) {
+ this.first = first;
+ }
+
+ public T getSecond() {
+ return second;
+ }
+
+ public void setSecond(T second) {
+ this.second = second;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/ProgressListener.java b/app/src/main/java/github/daneren2005/dsub/util/ProgressListener.java
new file mode 100644
index 00000000..c6d58f42
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/ProgressListener.java
@@ -0,0 +1,27 @@
+/*
+ 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.util;
+
+/**
+ * @author Sindre Mehus
+ */
+public interface ProgressListener {
+ void updateProgress(String message);
+ void updateProgress(int messageId);
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/SettingsBackupAgent.java b/app/src/main/java/github/daneren2005/dsub/util/SettingsBackupAgent.java
new file mode 100644
index 00000000..7eb6d137
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/SettingsBackupAgent.java
@@ -0,0 +1,31 @@
+/*
+ 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.util;
+
+import android.app.backup.BackupAgentHelper;
+import android.app.backup.SharedPreferencesBackupHelper;
+import github.daneren2005.dsub.util.Constants;
+
+public class SettingsBackupAgent extends BackupAgentHelper {
+ public void onCreate() {
+ super.onCreate();
+ SharedPreferencesBackupHelper helper = new SharedPreferencesBackupHelper(this, Constants.PREFERENCES_FILE_NAME);
+ addHelper("mypreferences", helper);
+ }
+ }
diff --git a/app/src/main/java/github/daneren2005/dsub/util/ShufflePlayBuffer.java b/app/src/main/java/github/daneren2005/dsub/util/ShufflePlayBuffer.java
new file mode 100644
index 00000000..7da35f77
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/ShufflePlayBuffer.java
@@ -0,0 +1,212 @@
+/*
+ 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.util;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.Log;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import github.daneren2005.dsub.util.FileUtil;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class ShufflePlayBuffer {
+
+ private static final String TAG = ShufflePlayBuffer.class.getSimpleName();
+ private static final String CACHE_FILENAME = "shuffleBuffer.ser";
+
+ private ScheduledExecutorService executorService;
+ private Runnable runnable;
+ private boolean firstRun = true;
+ private final ArrayList<MusicDirectory.Entry> buffer = new ArrayList<MusicDirectory.Entry>();
+ private int lastCount = -1;
+ private DownloadService context;
+ private boolean awaitingResults = false;
+ private int capacity;
+ private int refillThreshold;
+
+ private SharedPreferences.OnSharedPreferenceChangeListener listener;
+ private int currentServer;
+ private String currentFolder = "";
+ private String genre = "";
+ private String startYear = "";
+ private String endYear = "";
+
+ public ShufflePlayBuffer(DownloadService context) {
+ this.context = context;
+
+ executorService = Executors.newSingleThreadScheduledExecutor();
+ runnable = new Runnable() {
+ @Override
+ public void run() {
+ refill();
+ }
+ };
+ executorService.scheduleWithFixedDelay(runnable, 1, 10, TimeUnit.SECONDS);
+
+ // Calculate out the capacity and refill threshold based on the user's random size preference
+ int shuffleListSize = Integer.parseInt(Util.getPreferences(context).getString(Constants.PREFERENCES_KEY_RANDOM_SIZE, "20"));
+ // ex: default 20 -> 50
+ capacity = shuffleListSize * 5 / 2;
+ capacity = Math.min(500, capacity);
+
+ // ex: default 20 -> 40
+ refillThreshold = capacity * 4 / 5;
+ }
+
+ public List<MusicDirectory.Entry> get(int size) {
+ clearBufferIfnecessary();
+ // Make sure fetcher is running if needed
+ restart();
+
+ List<MusicDirectory.Entry> result = new ArrayList<MusicDirectory.Entry>(size);
+ synchronized (buffer) {
+ boolean removed = false;
+ while (!buffer.isEmpty() && result.size() < size) {
+ result.add(buffer.remove(buffer.size() - 1));
+ removed = true;
+ }
+
+ // Re-cache if anything is taken out
+ if(removed) {
+ FileUtil.serialize(context, buffer, CACHE_FILENAME);
+ }
+ }
+ Log.i(TAG, "Taking " + result.size() + " songs from shuffle play buffer. " + buffer.size() + " remaining.");
+ if(result.isEmpty()) {
+ awaitingResults = true;
+ }
+ return result;
+ }
+
+ public void shutdown() {
+ executorService.shutdown();
+ Util.getPreferences(context).unregisterOnSharedPreferenceChangeListener(listener);
+ }
+
+ private void restart() {
+ synchronized(buffer) {
+ if(buffer.size() <= refillThreshold && lastCount != 0 && executorService.isShutdown()) {
+ executorService = Executors.newSingleThreadScheduledExecutor();
+ executorService.scheduleWithFixedDelay(runnable, 0, 10, TimeUnit.SECONDS);
+ }
+ }
+ }
+
+ private void refill() {
+ // Check if active server has changed.
+ clearBufferIfnecessary();
+
+ if (buffer != null && (buffer.size() > refillThreshold || (!Util.isNetworkConnected(context) && !Util.isOffline(context)) || lastCount == 0)) {
+ executorService.shutdown();
+ return;
+ }
+
+ try {
+ MusicService service = MusicServiceFactory.getMusicService(context);
+
+ // Get capacity based
+ int n = capacity - buffer.size();
+ String folder = null;
+ if(!Util.isTagBrowsing(context)) {
+ folder = Util.getSelectedMusicFolderId(context);
+ }
+ MusicDirectory songs = service.getRandomSongs(n, folder, genre, startYear, endYear, context, null);
+
+ synchronized (buffer) {
+ lastCount = 0;
+ for(MusicDirectory.Entry entry: songs.getChildren()) {
+ if(!buffer.contains(entry) && entry.getRating() != 1) {
+ buffer.add(entry);
+ lastCount++;
+ }
+ }
+ Log.i(TAG, "Refilled shuffle play buffer with " + lastCount + " songs.");
+
+ // Cache buffer
+ FileUtil.serialize(context, buffer, CACHE_FILENAME);
+ }
+ } catch (Exception x) {
+ // Give it one more try before quitting
+ if(lastCount != -2) {
+ lastCount = -2;
+ } else if(lastCount == -2) {
+ lastCount = 0;
+ }
+ Log.w(TAG, "Failed to refill shuffle play buffer.", x);
+ }
+
+ if(awaitingResults) {
+ awaitingResults = false;
+ context.checkDownloads();
+ }
+ }
+
+ private void clearBufferIfnecessary() {
+ synchronized (buffer) {
+ final SharedPreferences prefs = Util.getPreferences(context);
+ if (currentServer != Util.getActiveServer(context)
+ || !Util.equals(currentFolder, Util.getSelectedMusicFolderId(context))
+ || (genre != null && !genre.equals(prefs.getString(Constants.PREFERENCES_KEY_SHUFFLE_GENRE, "")))
+ || (startYear != null && !startYear.equals(prefs.getString(Constants.PREFERENCES_KEY_SHUFFLE_START_YEAR, "")))
+ || (endYear != null && !endYear.equals(prefs.getString(Constants.PREFERENCES_KEY_SHUFFLE_END_YEAR, "")))) {
+ lastCount = -1;
+ currentServer = Util.getActiveServer(context);
+ currentFolder = Util.getSelectedMusicFolderId(context);
+ genre = prefs.getString(Constants.PREFERENCES_KEY_SHUFFLE_GENRE, "");
+ startYear = prefs.getString(Constants.PREFERENCES_KEY_SHUFFLE_START_YEAR, "");
+ endYear = prefs.getString(Constants.PREFERENCES_KEY_SHUFFLE_END_YEAR, "");
+ buffer.clear();
+
+ if(firstRun) {
+ ArrayList cacheList = FileUtil.deserialize(context, CACHE_FILENAME, ArrayList.class);
+ if(cacheList != null) {
+ buffer.addAll(cacheList);
+ }
+
+ listener = new SharedPreferences.OnSharedPreferenceChangeListener() {
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
+ clearBufferIfnecessary();
+ restart();
+ }
+ };
+ prefs.registerOnSharedPreferenceChangeListener(listener);
+ firstRun = false;
+ } else {
+ // Clear cache
+ File file = new File(context.getCacheDir(), CACHE_FILENAME);
+ file.delete();
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/SilentBackgroundTask.java b/app/src/main/java/github/daneren2005/dsub/util/SilentBackgroundTask.java
new file mode 100644
index 00000000..b99b7e0e
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/SilentBackgroundTask.java
@@ -0,0 +1,48 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.util;
+
+import android.content.Context;
+
+/**
+ * @author Sindre Mehus
+ */
+public abstract class SilentBackgroundTask<T> extends BackgroundTask<T> {
+ public SilentBackgroundTask(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void execute() {
+ queue.offer(task = new Task());
+ }
+
+ @Override
+ protected void done(T result) {
+ // Don't do anything unless overriden
+ }
+
+ @Override
+ public void updateProgress(int messageId) {
+ }
+
+ @Override
+ public void updateProgress(String message) {
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/SimpleServiceBinder.java b/app/src/main/java/github/daneren2005/dsub/util/SimpleServiceBinder.java
new file mode 100644
index 00000000..9c0b36a9
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/SimpleServiceBinder.java
@@ -0,0 +1,37 @@
+/*
+ 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.util;
+
+import android.os.Binder;
+
+/**
+ * @author Sindre Mehus
+ */
+public class SimpleServiceBinder<S> extends Binder {
+
+ private final S service;
+
+ public SimpleServiceBinder(S service) {
+ this.service = service;
+ }
+
+ public S getService() {
+ return service;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/SyncUtil.java b/app/src/main/java/github/daneren2005/dsub/util/SyncUtil.java
new file mode 100644
index 00000000..a369715f
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/SyncUtil.java
@@ -0,0 +1,222 @@
+package github.daneren2005.dsub.util;
+
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.support.v4.app.NotificationCompat;
+
+import java.io.File;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.activity.SubsonicFragmentActivity;
+
+/**
+ * Created by Scott on 11/24/13.
+ */
+public final class SyncUtil {
+ private static String TAG = SyncUtil.class.getSimpleName();
+ private static ArrayList<SyncSet> syncedPlaylists;
+ private static ArrayList<SyncSet> syncedPodcasts;
+ private static String url;
+
+ private static void checkRestURL(Context context) {
+ int instance = Util.getActiveServer(context);
+ String newURL = Util.getRestUrl(context, null, instance, false);
+ if(url == null || !url.equals(newURL)) {
+ syncedPlaylists = null;
+ syncedPodcasts = null;
+ url = newURL;
+ }
+ }
+
+ // Playlist sync
+ public static boolean isSyncedPlaylist(Context context, String playlistId) {
+ checkRestURL(context);
+ if(syncedPlaylists == null) {
+ syncedPlaylists = getSyncedPlaylists(context);
+ }
+ return syncedPlaylists.contains(new SyncSet(playlistId));
+ }
+ public static ArrayList<SyncSet> getSyncedPlaylists(Context context) {
+ return getSyncedPlaylists(context, Util.getActiveServer(context));
+ }
+ public static ArrayList<SyncSet> getSyncedPlaylists(Context context, int instance) {
+ String syncFile = getPlaylistSyncFile(context, instance);
+ ArrayList<SyncSet> playlists = FileUtil.deserializeCompressed(context, syncFile, ArrayList.class);
+ if(playlists == null) {
+ playlists = new ArrayList<SyncSet>();
+
+ // Try to convert old style into new style
+ ArrayList<String> oldPlaylists = FileUtil.deserialize(context, syncFile, ArrayList.class);
+ // If exists, time to convert!
+ if(oldPlaylists != null) {
+ for(String id: oldPlaylists) {
+ playlists.add(new SyncSet(id));
+ }
+
+ FileUtil.serializeCompressed(context, playlists, syncFile);
+ }
+ }
+ return playlists;
+ }
+ public static void setSyncedPlaylists(Context context, int instance, ArrayList<SyncSet> playlists) {
+ FileUtil.serializeCompressed(context, playlists, getPlaylistSyncFile(context, instance));
+ }
+ public static void addSyncedPlaylist(Context context, String playlistId) {
+ String playlistFile = getPlaylistSyncFile(context);
+ ArrayList<SyncSet> playlists = getSyncedPlaylists(context);
+ SyncSet set = new SyncSet(playlistId);
+ if(!playlists.contains(set)) {
+ playlists.add(set);
+ }
+ FileUtil.serializeCompressed(context, playlists, playlistFile);
+ syncedPlaylists = playlists;
+ }
+ public static void removeSyncedPlaylist(Context context, String playlistId) {
+ int instance = Util.getActiveServer(context);
+ removeSyncedPlaylist(context, playlistId, instance);
+ }
+ public static void removeSyncedPlaylist(Context context, String playlistId, int instance) {
+ String playlistFile = getPlaylistSyncFile(context, instance);
+ ArrayList<SyncSet> playlists = getSyncedPlaylists(context, instance);
+ SyncSet set = new SyncSet(playlistId);
+ if(playlists.contains(set)) {
+ playlists.remove(set);
+ FileUtil.serializeCompressed(context, playlists, playlistFile);
+ syncedPlaylists = playlists;
+ }
+ }
+ public static String getPlaylistSyncFile(Context context) {
+ int instance = Util.getActiveServer(context);
+ return getPlaylistSyncFile(context, instance);
+ }
+ public static String getPlaylistSyncFile(Context context, int instance) {
+ return "sync-playlist-" + (Util.getRestUrl(context, null, instance, false)).hashCode() + ".ser";
+ }
+
+ // Podcast sync
+ public static boolean isSyncedPodcast(Context context, String podcastId) {
+ checkRestURL(context);
+ if(syncedPodcasts == null) {
+ syncedPodcasts = getSyncedPodcasts(context);
+ }
+ return syncedPodcasts.contains(new SyncSet(podcastId));
+ }
+ public static ArrayList<SyncSet> getSyncedPodcasts(Context context) {
+ return getSyncedPodcasts(context, Util.getActiveServer(context));
+ }
+ public static ArrayList<SyncSet> getSyncedPodcasts(Context context, int instance) {
+ ArrayList<SyncSet> podcasts = FileUtil.deserialize(context, getPodcastSyncFile(context, instance), ArrayList.class);
+ if(podcasts == null) {
+ podcasts = new ArrayList<SyncSet>();
+ }
+ return podcasts;
+ }
+ public static void addSyncedPodcast(Context context, String podcastId, List<String> synced) {
+ String podcastFile = getPodcastSyncFile(context);
+ ArrayList<SyncSet> podcasts = getSyncedPodcasts(context);
+ SyncSet set = new SyncSet(podcastId, synced);
+ if(!podcasts.contains(set)) {
+ podcasts.add(set);
+ }
+ FileUtil.serialize(context, podcasts, podcastFile);
+ syncedPodcasts = podcasts;
+ }
+ public static void removeSyncedPodcast(Context context, String podcastId) {
+ removeSyncedPodcast(context, podcastId, Util.getActiveServer(context));
+ }
+ public static void removeSyncedPodcast(Context context, String podcastId, int instance) {
+ String podcastFile = getPodcastSyncFile(context, instance);
+ ArrayList<SyncSet> podcasts = getSyncedPodcasts(context, instance);
+ SyncSet set = new SyncSet(podcastId);
+ if(podcasts.contains(set)) {
+ podcasts.remove(set);
+ FileUtil.serialize(context, podcasts, podcastFile);
+ syncedPodcasts = podcasts;
+ }
+ }
+ public static String getPodcastSyncFile(Context context) {
+ int instance = Util.getActiveServer(context);
+ return getPodcastSyncFile(context, instance);
+ }
+ public static String getPodcastSyncFile(Context context, int instance) {
+ return "sync-podcast-" + (Util.getRestUrl(context, null, instance, false)).hashCode() + ".ser";
+ }
+
+ // Starred
+ public static ArrayList<String> getSyncedStarred(Context context, int instance) {
+ ArrayList<String> list = FileUtil.deserializeCompressed(context, getStarredSyncFile(context, instance), ArrayList.class);
+ if(list == null) {
+ list = new ArrayList<String>();
+ }
+ return list;
+ }
+ public static void setSyncedStarred(ArrayList<String> syncedList, Context context, int instance) {
+ FileUtil.serializeCompressed(context, syncedList, SyncUtil.getStarredSyncFile(context, instance));
+ }
+ public static String getStarredSyncFile(Context context, int instance) {
+ return "sync-starred-" + (Util.getRestUrl(context, null, instance, false)).hashCode() + ".ser";
+ }
+
+ // Most Recently Added
+ public static ArrayList<String> getSyncedMostRecent(Context context, int instance) {
+ ArrayList<String> list = FileUtil.deserialize(context, getMostRecentSyncFile(context, instance), ArrayList.class);
+ if(list == null) {
+ list = new ArrayList<String>();
+ }
+ return list;
+ }
+ public static void removeMostRecentSyncFiles(Context context) {
+ int total = Util.getServerCount(context);
+ for(int i = 0; i < total; i++) {
+ File file = new File(context.getCacheDir(), getMostRecentSyncFile(context, i));
+ file.delete();
+ }
+ }
+ public static String getMostRecentSyncFile(Context context, int instance) {
+ return "sync-most_recent-" + (Util.getRestUrl(context, null, instance, false)).hashCode() + ".ser";
+ }
+
+ public static String joinNames(List<String> names) {
+ StringBuilder builder = new StringBuilder();
+ for (String val : names) {
+ builder.append(val).append(", ");
+ }
+ builder.setLength(builder.length() - 2);
+ return builder.toString();
+ }
+
+ public static class SyncSet implements Serializable {
+ public String id;
+ public List<String> synced;
+
+ protected SyncSet() {
+
+ }
+ public SyncSet(String id) {
+ this.id = id;
+ }
+ public SyncSet(String id, List<String> synced) {
+ this.id = id;
+ this.synced = synced;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if(obj instanceof SyncSet) {
+ return this.id.equals(((SyncSet)obj).id);
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return id.hashCode();
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/TabBackgroundTask.java b/app/src/main/java/github/daneren2005/dsub/util/TabBackgroundTask.java
new file mode 100644
index 00000000..759e893a
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/TabBackgroundTask.java
@@ -0,0 +1,51 @@
+package github.daneren2005.dsub.util;
+
+import github.daneren2005.dsub.fragments.SubsonicFragment;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public abstract class TabBackgroundTask<T> extends BackgroundTask<T> {
+
+ private final SubsonicFragment tabFragment;
+
+ public TabBackgroundTask(SubsonicFragment fragment) {
+ super(fragment.getActivity());
+ tabFragment = fragment;
+ }
+
+ @Override
+ public void execute() {
+ tabFragment.setProgressVisible(true);
+
+ queue.offer(task = new Task() {
+ @Override
+ public void onDone(T result) {
+ tabFragment.setProgressVisible(false);
+ done(result);
+ }
+
+ @Override
+ public void onError(Throwable t) {
+ tabFragment.setProgressVisible(false);
+ error(t);
+ }
+ });
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return !tabFragment.isAdded() || cancelled.get();
+ }
+
+ @Override
+ public void updateProgress(final String message) {
+ getHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ tabFragment.updateProgress(message);
+ }
+ });
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/TimeLimitedCache.java b/app/src/main/java/github/daneren2005/dsub/util/TimeLimitedCache.java
new file mode 100644
index 00000000..8b7df783
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/TimeLimitedCache.java
@@ -0,0 +1,55 @@
+/*
+ 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.util;
+
+import java.lang.ref.SoftReference;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class TimeLimitedCache<T> {
+
+ private SoftReference<T> value;
+ private final long ttlMillis;
+ private long expires;
+
+ public TimeLimitedCache(long ttl, TimeUnit timeUnit) {
+ this.ttlMillis = TimeUnit.MILLISECONDS.convert(ttl, timeUnit);
+ }
+
+ public T get() {
+ return System.currentTimeMillis() < expires ? value.get() : null;
+ }
+
+ public void set(T value) {
+ set(value, ttlMillis, TimeUnit.MILLISECONDS);
+ }
+
+ public void set(T value, long ttl, TimeUnit timeUnit) {
+ this.value = new SoftReference<T>(value);
+ expires = System.currentTimeMillis() + timeUnit.toMillis(ttl);
+ }
+
+ public void clear() {
+ expires = 0L;
+ value = null;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/UserUtil.java b/app/src/main/java/github/daneren2005/dsub/util/UserUtil.java
new file mode 100644
index 00000000..29618424
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/UserUtil.java
@@ -0,0 +1,452 @@
+/*
+ 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.util;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.SharedPreferences;
+import android.support.v7.app.ActionBarActivity;
+import android.util.Log;
+import android.view.View;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.User;
+import github.daneren2005.dsub.fragments.SubsonicFragment;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import github.daneren2005.dsub.service.OfflineException;
+import github.daneren2005.dsub.service.ServerTooOldException;
+import github.daneren2005.dsub.adapter.SettingsAdapter;
+
+public final class UserUtil {
+ private static final String TAG = UserUtil.class.getSimpleName();
+ private static final long MIN_VERIFY_DURATION = 1000L * 60L * 60L;
+
+ private static int instance = -1;
+ private static User currentUser;
+ private static long lastVerifiedTime = 0;
+
+
+ public static void refreshCurrentUser(Context context, boolean forceRefresh) {
+ refreshCurrentUser(context, forceRefresh, false);
+ }
+ public static void refreshCurrentUser(Context context, boolean forceRefresh, boolean unAuth) {
+ currentUser = null;
+ if(unAuth) {
+ lastVerifiedTime = 0;
+ }
+ seedCurrentUser(context, forceRefresh);
+ }
+
+ public static void seedCurrentUser(Context context) {
+ seedCurrentUser(context, false);
+ }
+ public static void seedCurrentUser(final Context context, final boolean refresh) {
+ // Only try to seed if online
+ if(Util.isOffline(context)) {
+ currentUser = null;
+ return;
+ }
+
+ final int instance = Util.getActiveServer(context);
+ if(UserUtil.instance == instance && currentUser != null) {
+ return;
+ } else {
+ UserUtil.instance = instance;
+ }
+
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ currentUser = MusicServiceFactory.getMusicService(context).getUser(refresh, getCurrentUsername(context, instance), context, null);
+
+ // If running, redo cast selector
+ DownloadService downloadService = DownloadService.getInstance();
+ if(downloadService != null) {
+ downloadService.userSettingsChanged();
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ if(context instanceof ActionBarActivity) {
+ ((ActionBarActivity) context).supportInvalidateOptionsMenu();
+ }
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ // Don't do anything, supposed to be background pull
+ Log.e(TAG, "Failed to seed user information");
+ }
+ }.execute();
+ }
+
+ public static User getCurrentUser() {
+ return currentUser;
+ }
+
+ public static String getCurrentUsername(Context context, int instance) {
+ SharedPreferences prefs = Util.getPreferences(context);
+ return prefs.getString(Constants.PREFERENCES_KEY_USERNAME + instance, null);
+ }
+
+ public static String getCurrentUsername(Context context) {
+ return getCurrentUsername(context, Util.getActiveServer(context));
+ }
+
+ public static boolean isCurrentAdmin() {
+ return isCurrentRole(User.ADMIN);
+ }
+
+ public static boolean canPodcast() {
+ return isCurrentRole(User.PODCAST);
+ }
+ public static boolean canShare() {
+ return isCurrentRole(User.SHARE);
+ }
+ public static boolean canJukebox() {
+ return isCurrentRole(User.JUKEBOX);
+ }
+ public static boolean canScrobble() {
+ return isCurrentRole(User.SCROBBLING, true);
+ }
+
+ public static boolean isCurrentRole(String role) {
+ return isCurrentRole(role, false);
+ }
+ public static boolean isCurrentRole(String role, boolean defaultValue) {
+ if(currentUser == null) {
+ return defaultValue;
+ }
+
+ for(User.Setting setting: currentUser.getSettings()) {
+ if(setting.getName().equals(role)) {
+ return setting.getValue() == true;
+ }
+ }
+
+ return defaultValue;
+ }
+
+ public static void confirmCredentials(final Activity context, final Runnable onSuccess) {
+ final long currentTime = System.currentTimeMillis();
+ // If already ran this check within last x time, just go ahead and auth
+ if((currentTime - lastVerifiedTime) < MIN_VERIFY_DURATION) {
+ onSuccess.run();
+ } else {
+ View layout = context.getLayoutInflater().inflate(R.layout.confirm_password, null);
+ final TextView passwordView = (TextView) layout.findViewById(R.id.password);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.admin_confirm_password)
+ .setView(layout)
+ .setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ String password = passwordView.getText().toString();
+
+ SharedPreferences prefs = Util.getPreferences(context);
+ String correctPassword = prefs.getString(Constants.PREFERENCES_KEY_PASSWORD + Util.getActiveServer(context), null);
+
+ if(password != null && password.equals(correctPassword)) {
+ lastVerifiedTime = currentTime;
+ onSuccess.run();
+ } else {
+ Util.toast(context, R.string.admin_confirm_password_bad);
+ }
+ }
+ })
+ .setNegativeButton(R.string.common_cancel, null)
+ .setCancelable(true);
+
+ AlertDialog dialog = builder.create();
+ dialog.show();
+ }
+ }
+
+ public static void changePassword(final Activity context, final User user) {
+ View layout = context.getLayoutInflater().inflate(R.layout.change_password, null);
+ final TextView passwordView = (TextView) layout.findViewById(R.id.new_password);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.admin_change_password)
+ .setView(layout)
+ .setPositiveButton(R.string.common_save, null)
+ .setNegativeButton(R.string.common_cancel, null)
+ .setCancelable(true);
+
+ final AlertDialog dialog = builder.create();
+ dialog.show();
+
+ dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ final String password = passwordView.getText().toString();
+ // Don't allow blank passwords
+ if ("".equals(password)) {
+ Util.toast(context, R.string.admin_change_password_invalid);
+ return;
+ }
+
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.changePassword(user.getUsername(), password, context, null);
+ return null;
+ }
+
+ @Override
+ protected void done(Void v) {
+ Util.toast(context, context.getResources().getString(R.string.admin_change_password_success, user.getUsername()));
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.admin_change_password_error, user.getUsername());
+ }
+
+ Util.toast(context, msg);
+ }
+ }.execute();
+
+ dialog.dismiss();
+ }
+ });
+ }
+
+ public static void updateSettings(final Context context, final User user) {
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.updateUser(user, context, null);
+ user.setSettings(user.getSettings());
+ return null;
+ }
+
+ @Override
+ protected void done(Void v) {
+ Util.toast(context, context.getResources().getString(R.string.admin_update_permissions_success, user.getUsername()));
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.admin_update_permissions_error, user.getUsername());
+ }
+
+ Util.toast(context, msg);
+ }
+ }.execute();
+ }
+
+ public static void changeEmail(final Activity context, final User user) {
+ View layout = context.getLayoutInflater().inflate(R.layout.change_email, null);
+ final TextView emailView = (TextView) layout.findViewById(R.id.new_email);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.admin_change_email)
+ .setView(layout)
+ .setPositiveButton(R.string.common_save, null)
+ .setNegativeButton(R.string.common_cancel, null)
+ .setCancelable(true);
+
+ final AlertDialog dialog = builder.create();
+ dialog.show();
+
+ dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ final String email = emailView.getText().toString();
+ // Don't allow blank emails
+ if ("".equals(email)) {
+ Util.toast(context, R.string.admin_change_email_invalid);
+ return;
+ }
+
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.changeEmail(user.getUsername(), email, context, null);
+ user.setEmail(email);
+ return null;
+ }
+
+ @Override
+ protected void done(Void v) {
+ Util.toast(context, context.getResources().getString(R.string.admin_change_email_success, user.getUsername()));
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.admin_change_email_error, user.getUsername());
+ }
+
+ Util.toast(context, msg);
+ }
+ }.execute();
+
+ dialog.dismiss();
+ }
+ });
+ }
+
+ public static void deleteUser(final Context context, final User user, final ArrayAdapter adapter) {
+ Util.confirmDialog(context, R.string.common_delete, user.getUsername(), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.deleteUser(user.getUsername(), context, null);
+ return null;
+ }
+
+ @Override
+ protected void done(Void v) {
+ if(adapter != null) {
+ adapter.remove(user);
+ adapter.notifyDataSetChanged();
+ }
+
+ Util.toast(context, context.getResources().getString(R.string.admin_delete_user_success, user.getUsername()));
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.admin_delete_user_error, user.getUsername());
+ }
+
+ Util.toast(context, msg);
+ }
+ }.execute();
+ }
+ });
+ }
+
+ public static void addNewUser(final Activity context, final SubsonicFragment fragment) {
+ final User user = new User();
+ for(String role: User.ROLES) {
+ if(role.equals(User.SETTINGS) || role.equals(User.STREAM)) {
+ user.addSetting(role, true);
+ } else {
+ user.addSetting(role, false);
+ }
+ }
+
+ View layout = context.getLayoutInflater().inflate(R.layout.create_user, null);
+ final TextView usernameView = (TextView) layout.findViewById(R.id.username);
+ final TextView emailView = (TextView) layout.findViewById(R.id.email);
+ final TextView passwordView = (TextView) layout.findViewById(R.id.password);
+ final ListView listView = (ListView) layout.findViewById(R.id.settings_list);
+ listView.setAdapter(new SettingsAdapter(context, user, true));
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.menu_add_user)
+ .setView(layout)
+ .setPositiveButton(R.string.common_save, null)
+ .setNegativeButton(R.string.common_cancel, null)
+ .setCancelable(true);
+
+ final AlertDialog dialog = builder.create();
+ dialog.show();
+
+ dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ final String username = usernameView.getText().toString();
+ // Don't allow blank emails
+ if ("".equals(username)) {
+ Util.toast(context, R.string.admin_change_username_invalid);
+ return;
+ }
+
+ final String email = emailView.getText().toString();
+ // Don't allow blank emails
+ if ("".equals(email)) {
+ Util.toast(context, R.string.admin_change_email_invalid);
+ return;
+ }
+
+ final String password = passwordView.getText().toString();
+ if ("".equals(password)) {
+ Util.toast(context, R.string.admin_change_password_invalid);
+ return;
+ }
+
+ user.setUsername(username);
+ user.setEmail(email);
+ user.setPassword(password);
+
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.createUser(user, context, null);
+ return null;
+ }
+
+ @Override
+ protected void done(Void v) {
+ fragment.onRefresh();
+ Util.toast(context, context.getResources().getString(R.string.admin_create_user_success));
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.admin_create_user_error);
+ }
+
+ Util.toast(context, msg);
+ }
+ }.execute();
+
+ dialog.dismiss();
+ }
+ });
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/Util.java b/app/src/main/java/github/daneren2005/dsub/util/Util.java
new file mode 100644
index 00000000..75d8d5dd
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/Util.java
@@ -0,0 +1,1339 @@
+/*
+ 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
+ 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.util;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.media.AudioManager;
+import android.media.AudioManager.OnAudioFocusChangeListener;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.wifi.WifiManager;
+import android.os.Build;
+import android.os.Environment;
+import android.text.Html;
+import android.text.SpannableString;
+import android.text.method.LinkMovementMethod;
+import android.text.util.Linkify;
+import android.util.Log;
+import android.view.Gravity;
+import android.widget.TextView;
+import android.widget.Toast;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.PlayerState;
+import github.daneren2005.dsub.domain.RepeatMode;
+import github.daneren2005.dsub.receiver.MediaButtonIntentReceiver;
+import github.daneren2005.dsub.service.DownloadService;
+
+import org.apache.http.HttpEntity;
+
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.security.MessageDigest;
+import java.text.DecimalFormat;
+import java.text.NumberFormat;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.Locale;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public final class Util {
+ private static final String TAG = Util.class.getSimpleName();
+
+ private static final DecimalFormat GIGA_BYTE_FORMAT = new DecimalFormat("0.00 GB");
+ private static final DecimalFormat MEGA_BYTE_FORMAT = new DecimalFormat("0.00 MB");
+ private static final DecimalFormat KILO_BYTE_FORMAT = new DecimalFormat("0 KB");
+
+ private static DecimalFormat GIGA_BYTE_LOCALIZED_FORMAT = null;
+ private static DecimalFormat MEGA_BYTE_LOCALIZED_FORMAT = null;
+ private static DecimalFormat KILO_BYTE_LOCALIZED_FORMAT = null;
+ private static DecimalFormat BYTE_LOCALIZED_FORMAT = null;
+ private static SimpleDateFormat DATE_FORMAT_SHORT = new SimpleDateFormat("MMM d h:mm a");
+ private static SimpleDateFormat DATE_FORMAT_LONG = new SimpleDateFormat("MMM d, yyyy h:mm a");
+ private static int CURRENT_YEAR = new Date().getYear();
+
+ public static final String EVENT_META_CHANGED = "github.daneren2005.dsub.EVENT_META_CHANGED";
+ public static final String EVENT_PLAYSTATE_CHANGED = "github.daneren2005.dsub.EVENT_PLAYSTATE_CHANGED";
+
+ public static final String AVRCP_PLAYSTATE_CHANGED = "com.android.music.playstatechanged";
+ public static final String AVRCP_METADATA_CHANGED = "com.android.music.metachanged";
+
+ private static OnAudioFocusChangeListener focusListener;
+ private static boolean pauseFocus = false;
+ private static boolean lowerFocus = false;
+
+ // Used by hexEncode()
+ private static final char[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
+
+ private static Toast toast;
+
+ private Util() {
+ }
+
+ public static boolean isOffline(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getBoolean(Constants.PREFERENCES_KEY_OFFLINE, false);
+ }
+
+ public static void setOffline(Context context, boolean offline) {
+ SharedPreferences prefs = getPreferences(context);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putBoolean(Constants.PREFERENCES_KEY_OFFLINE, offline);
+ editor.commit();
+ }
+
+ public static boolean isScreenLitOnDownload(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getBoolean(Constants.PREFERENCES_KEY_SCREEN_LIT_ON_DOWNLOAD, false);
+ }
+
+ public static RepeatMode getRepeatMode(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return RepeatMode.valueOf(prefs.getString(Constants.PREFERENCES_KEY_REPEAT_MODE, RepeatMode.OFF.name()));
+ }
+
+ public static void setRepeatMode(Context context, RepeatMode repeatMode) {
+ SharedPreferences prefs = getPreferences(context);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putString(Constants.PREFERENCES_KEY_REPEAT_MODE, repeatMode.name());
+ editor.commit();
+ }
+
+ public static boolean isScrobblingEnabled(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getBoolean(Constants.PREFERENCES_KEY_SCROBBLE, true) && (isOffline(context) || UserUtil.canScrobble());
+ }
+
+ public static void setActiveServer(Context context, int instance) {
+ SharedPreferences prefs = getPreferences(context);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, instance);
+ editor.commit();
+ }
+
+ public static int getActiveServer(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getBoolean(Constants.PREFERENCES_KEY_OFFLINE, false) ? 0 : prefs.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1);
+ }
+
+ public static int getServerCount(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getInt(Constants.PREFERENCES_KEY_SERVER_COUNT, 1);
+ }
+
+ public static void removeInstanceName(Context context, int instance, int activeInstance) {
+ SharedPreferences prefs = getPreferences(context);
+ SharedPreferences.Editor editor = prefs.edit();
+
+ int newInstance = instance + 1;
+
+ // Get what the +1 server details are
+ String server = prefs.getString(Constants.PREFERENCES_KEY_SERVER_KEY + newInstance, null);
+ String serverName = prefs.getString(Constants.PREFERENCES_KEY_SERVER_NAME + newInstance, null);
+ String serverUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_URL + newInstance, null);
+ String userName = prefs.getString(Constants.PREFERENCES_KEY_USERNAME + newInstance, null);
+ String password = prefs.getString(Constants.PREFERENCES_KEY_PASSWORD + newInstance, null);
+ String musicFolderId = prefs.getString(Constants.PREFERENCES_KEY_MUSIC_FOLDER_ID + newInstance, null);
+
+ // Store the +1 server details in the to be deleted instance
+ editor.putString(Constants.PREFERENCES_KEY_SERVER_KEY + instance, server);
+ editor.putString(Constants.PREFERENCES_KEY_SERVER_NAME + instance, serverName);
+ editor.putString(Constants.PREFERENCES_KEY_SERVER_URL + instance, serverUrl);
+ editor.putString(Constants.PREFERENCES_KEY_USERNAME + instance, userName);
+ editor.putString(Constants.PREFERENCES_KEY_PASSWORD + instance, password);
+ editor.putString(Constants.PREFERENCES_KEY_MUSIC_FOLDER_ID + instance, musicFolderId);
+
+ // Delete the +1 server instance
+ // Calling method will loop up to fill this in if +2 server exists
+ editor.putString(Constants.PREFERENCES_KEY_SERVER_KEY + newInstance, null);
+ editor.putString(Constants.PREFERENCES_KEY_SERVER_NAME + newInstance, null);
+ editor.putString(Constants.PREFERENCES_KEY_SERVER_URL + newInstance, null);
+ editor.putString(Constants.PREFERENCES_KEY_USERNAME + newInstance, null);
+ editor.putString(Constants.PREFERENCES_KEY_PASSWORD + newInstance, null);
+ editor.putString(Constants.PREFERENCES_KEY_MUSIC_FOLDER_ID + newInstance, null);
+ editor.commit();
+
+ if (instance == activeInstance) {
+ if(instance != 1) {
+ Util.setActiveServer(context, 1);
+ } else {
+ Util.setOffline(context, true);
+ }
+ } else if (newInstance == activeInstance) {
+ Util.setActiveServer(context, instance);
+ }
+ }
+
+ public static String getServerName(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ int instance = prefs.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1);
+ return prefs.getString(Constants.PREFERENCES_KEY_SERVER_NAME + instance, null);
+ }
+ public static String getServerName(Context context, int instance) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getString(Constants.PREFERENCES_KEY_SERVER_NAME + instance, null);
+ }
+
+ public static void setSelectedMusicFolderId(Context context, String musicFolderId) {
+ int instance = getActiveServer(context);
+ SharedPreferences prefs = getPreferences(context);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putString(Constants.PREFERENCES_KEY_MUSIC_FOLDER_ID + instance, musicFolderId);
+ editor.commit();
+ }
+
+ public static String getSelectedMusicFolderId(Context context) {
+ return getSelectedMusicFolderId(context, getActiveServer(context));
+ }
+ public static String getSelectedMusicFolderId(Context context, int instance) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getString(Constants.PREFERENCES_KEY_MUSIC_FOLDER_ID + instance, null);
+ }
+
+ public static boolean getAlbumListsPerFolder(Context context) {
+ return getAlbumListsPerFolder(context, getActiveServer(context));
+ }
+ public static boolean getAlbumListsPerFolder(Context context, int instance) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getBoolean(Constants.PREFERENCES_KEY_ALBUMS_PER_FOLDER + instance, false);
+ }
+ public static void setAlbumListsPerFolder(Context context, boolean perFolder) {
+ int instance = getActiveServer(context);
+ SharedPreferences prefs = getPreferences(context);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putBoolean(Constants.PREFERENCES_KEY_ALBUMS_PER_FOLDER + instance, perFolder);
+ editor.commit();
+ }
+
+ public static String getTheme(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getString(Constants.PREFERENCES_KEY_THEME, null);
+ }
+ public static void setTheme(Context context, String theme) {
+ SharedPreferences.Editor editor = getPreferences(context).edit();
+ editor.putString(Constants.PREFERENCES_KEY_THEME, theme);
+ editor.commit();
+ }
+
+ public static void applyTheme(Context context, String theme) {
+ if ("dark".equals(theme)) {
+ context.setTheme(R.style.Theme_DSub_Dark);
+ } else if ("black".equals(theme)) {
+ context.setTheme(R.style.Theme_DSub_Black);
+ } else if ("holo".equals(theme)) {
+ context.setTheme(R.style.Theme_DSub_Holo);
+ } else {
+ context.setTheme(R.style.Theme_DSub_Light);
+ }
+
+ SharedPreferences prefs = Util.getPreferences(context);
+ if(prefs.getBoolean(Constants.PREFERENCES_KEY_OVERRIDE_SYSTEM_LANGUAGE, false)) {
+ Configuration config = new Configuration();
+ config.locale = Locale.ENGLISH;
+ context.getResources().updateConfiguration(config, context.getResources().getDisplayMetrics());
+ }
+ }
+
+ public static boolean getDisplayTrack(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getBoolean(Constants.PREFERENCES_KEY_DISPLAY_TRACK, false);
+ }
+
+ public static int getMaxBitrate(Context context) {
+ ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo networkInfo = manager.getActiveNetworkInfo();
+ if (networkInfo == null) {
+ return 0;
+ }
+
+ boolean wifi = networkInfo.getType() == ConnectivityManager.TYPE_WIFI;
+ SharedPreferences prefs = getPreferences(context);
+ return Integer.parseInt(prefs.getString(wifi ? Constants.PREFERENCES_KEY_MAX_BITRATE_WIFI : Constants.PREFERENCES_KEY_MAX_BITRATE_MOBILE, "0"));
+ }
+
+ public static int getMaxVideoBitrate(Context context) {
+ ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo networkInfo = manager.getActiveNetworkInfo();
+ if (networkInfo == null) {
+ return 0;
+ }
+
+ boolean wifi = networkInfo.getType() == ConnectivityManager.TYPE_WIFI;
+ SharedPreferences prefs = getPreferences(context);
+ return Integer.parseInt(prefs.getString(wifi ? Constants.PREFERENCES_KEY_MAX_VIDEO_BITRATE_WIFI : Constants.PREFERENCES_KEY_MAX_VIDEO_BITRATE_MOBILE, "0"));
+ }
+
+ public static int getPreloadCount(Context context) {
+ ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo networkInfo = manager.getActiveNetworkInfo();
+ if (networkInfo == null) {
+ return 3;
+ }
+
+ SharedPreferences prefs = getPreferences(context);
+ boolean wifi = networkInfo.getType() == ConnectivityManager.TYPE_WIFI;
+ int preloadCount = Integer.parseInt(prefs.getString(wifi ? Constants.PREFERENCES_KEY_PRELOAD_COUNT_WIFI : Constants.PREFERENCES_KEY_PRELOAD_COUNT_MOBILE, "-1"));
+ return preloadCount == -1 ? Integer.MAX_VALUE : preloadCount;
+ }
+
+ public static int getCacheSizeMB(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ int cacheSize = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_CACHE_SIZE, "-1"));
+ return cacheSize == -1 ? Integer.MAX_VALUE : cacheSize;
+ }
+
+ public static String getRestUrl(Context context, String method) {
+ return getRestUrl(context, method, true);
+ }
+ public static String getRestUrl(Context context, String method, boolean allowAltAddress) {
+ SharedPreferences prefs = getPreferences(context);
+ int instance = prefs.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1);
+ return getRestUrl(context, method, prefs, instance, allowAltAddress);
+ }
+ public static String getRestUrl(Context context, String method, int instance) {
+ return getRestUrl(context, method, instance, true);
+ }
+ public static String getRestUrl(Context context, String method, int instance, boolean allowAltAddress) {
+ SharedPreferences prefs = getPreferences(context);
+ return getRestUrl(context, method, prefs, instance, allowAltAddress);
+ }
+ public static String getRestUrl(Context context, String method, SharedPreferences prefs, int instance) {
+ return getRestUrl(context, method, prefs, instance, true);
+ }
+ public static String getRestUrl(Context context, String method, SharedPreferences prefs, int instance, boolean allowAltAddress) {
+ StringBuilder builder = new StringBuilder();
+
+ String serverUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_URL + instance, null);
+ if(allowAltAddress && Util.isWifiConnected(context)) {
+ String SSID = prefs.getString(Constants.PREFERENCES_KEY_SERVER_LOCAL_NETWORK_SSID + instance, "");
+ String currentSSID = Util.getSSID(context);
+
+ String[] ssidParts = SSID.split(",");
+ if("".equals(SSID) || SSID.equals(currentSSID) || Arrays.asList(ssidParts).contains(currentSSID)) {
+ String internalUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_INTERNAL_URL + instance, null);
+ if(internalUrl != null && !"".equals(internalUrl) && !"http://".equals(internalUrl)) {
+ serverUrl = internalUrl;
+ }
+ }
+ }
+
+ String username = prefs.getString(Constants.PREFERENCES_KEY_USERNAME + instance, null);
+ String password = prefs.getString(Constants.PREFERENCES_KEY_PASSWORD + instance, null);
+
+ // Slightly obfuscate password
+ password = "enc:" + Util.utf8HexEncode(password);
+
+ builder.append(serverUrl);
+ if (builder.charAt(builder.length() - 1) != '/') {
+ builder.append("/");
+ }
+ builder.append("rest/").append(method).append(".view");
+ builder.append("?u=").append(username);
+ builder.append("&p=").append(password);
+ builder.append("&v=").append(Constants.REST_PROTOCOL_VERSION);
+ builder.append("&c=").append(Constants.REST_CLIENT_ID);
+
+ return builder.toString();
+ }
+
+ public static String replaceInternalUrl(Context context, String url) {
+ // Only change to internal when using https
+ if(url.indexOf("https") != -1) {
+ SharedPreferences prefs = Util.getPreferences(context);
+ int instance = prefs.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1);
+ String internalUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_INTERNAL_URL + instance, null);
+ if(internalUrl != null && !"".equals(internalUrl)) {
+ String externalUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_URL + instance, null);
+ url = url.replace(internalUrl, externalUrl);
+ }
+ }
+
+ // Use separate profile for Chromecast so users can do ogg on phone, mp3 for CC
+ return url.replace("c=" + Constants.REST_CLIENT_ID, "c=" + Constants.CHROMECAST_CLIENT_ID);
+ }
+
+ public static boolean isTagBrowsing(Context context) {
+ return isTagBrowsing(context, Util.getActiveServer(context));
+ }
+ public static boolean isTagBrowsing(Context context, int instance) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getBoolean(Constants.PREFERENCES_KEY_BROWSE_TAGS + instance, false);
+ }
+
+ public static boolean isSyncEnabled(Context context, int instance) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getBoolean(Constants.PREFERENCES_KEY_SERVER_SYNC + instance, true);
+ }
+
+ public static String getParentFromEntry(Context context, MusicDirectory.Entry entry) {
+ if(Util.isTagBrowsing(context)) {
+ if(!entry.isDirectory()) {
+ return entry.getAlbumId();
+ } else if(entry.isAlbum()) {
+ return entry.getArtistId();
+ } else {
+ return null;
+ }
+ } else {
+ return entry.getParent();
+ }
+ }
+
+ public static String openToTab(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getString(Constants.PREFERENCES_KEY_OPEN_TO_TAB, null);
+ }
+
+ public static boolean disableExitPrompt(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getBoolean(Constants.PREFERENCES_KEY_DISABLE_EXIT_PROMPT, false);
+ }
+
+ public static String getVideoPlayerType(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getString(Constants.PREFERENCES_KEY_VIDEO_PLAYER, "raw");
+ }
+
+ public static SharedPreferences getPreferences(Context context) {
+ return context.getSharedPreferences(Constants.PREFERENCES_FILE_NAME, 0);
+ }
+ public static SharedPreferences getOfflineSync(Context context) {
+ return context.getSharedPreferences(Constants.OFFLINE_SYNC_NAME, 0);
+ }
+
+ public static String getSyncDefault(Context context) {
+ SharedPreferences prefs = Util.getOfflineSync(context);
+ return prefs.getString(Constants.OFFLINE_SYNC_DEFAULT, null);
+ }
+ public static void setSyncDefault(Context context, String defaultValue) {
+ SharedPreferences.Editor editor = Util.getOfflineSync(context).edit();
+ editor.putString(Constants.OFFLINE_SYNC_DEFAULT, defaultValue);
+ editor.commit();
+ }
+
+ public static String getCacheName(Context context, String name, String id) {
+ return getCacheName(context, getActiveServer(context), name, id);
+ }
+ public static String getCacheName(Context context, int instance, String name, String id) {
+ String s = getRestUrl(context, null, instance, false) + id;
+ return name + "-" + s.hashCode() + ".ser";
+ }
+ public static String getCacheName(Context context, String name) {
+ return getCacheName(context, getActiveServer(context), name);
+ }
+ public static String getCacheName(Context context, int instance, String name) {
+ String s = getRestUrl(context, null, instance, false);
+ return name + "-" + s.hashCode() + ".ser";
+ }
+
+ public static int offlineScrobblesCount(Context context) {
+ SharedPreferences offline = getOfflineSync(context);
+ return offline.getInt(Constants.OFFLINE_SCROBBLE_COUNT, 0);
+ }
+ public static int offlineStarsCount(Context context) {
+ SharedPreferences offline = getOfflineSync(context);
+ return offline.getInt(Constants.OFFLINE_STAR_COUNT, 0);
+ }
+
+ public static String parseOfflineIDSearch(Context context, String id, String cacheLocation) {
+ // Try to get this info based off of tags first
+ String name = parseOfflineIDSearch(id);
+ if(name != null) {
+ return name;
+ }
+
+ // Otherwise go nuts trying to parse from file structure
+ name = id.replace(cacheLocation, "");
+ if(name.startsWith("/")) {
+ name = name.substring(1);
+ }
+ name = name.replace(".complete", "").replace(".partial", "");
+ int index = name.lastIndexOf(".");
+ name = index == -1 ? name : name.substring(0, index);
+ String[] details = name.split("/");
+
+ String title = details[details.length - 1];
+ if(index == -1) {
+ if(details.length > 1) {
+ String artist = "artist:\"" + details[details.length - 2] + "\"";
+ String simpleArtist = "artist:\"" + title + "\"";
+ title = "album:\"" + title + "\"";
+ if(details[details.length - 1].equals(details[details.length - 2])) {
+ name = title;
+ } else {
+ name = "(" + artist + " AND " + title + ")" + " OR " + simpleArtist;
+ }
+ } else {
+ name = "artist:\"" + title + "\" OR album:\"" + title + "\"";
+ }
+ } else {
+ String artist;
+ if(details.length > 2) {
+ artist = "artist:\"" + details[details.length - 3] + "\"";
+ } else {
+ artist = "(artist:\"" + details[0] + "\" OR album:\"" + details[0] + "\")";
+ }
+ title = "title:\"" + title.substring(title.indexOf('-') + 1) + "\"";
+ name = artist + " AND " + title;
+ }
+
+ return name;
+ }
+
+ public static String parseOfflineIDSearch(String id) {
+ MusicDirectory.Entry entry = new MusicDirectory.Entry();
+ File file = new File(id);
+
+ if(file.exists()) {
+ entry.loadMetadata(file);
+
+ if(entry.getArtist() != null) {
+ String title = file.getName();
+ title = title.replace(".complete", "").replace(".partial", "");
+ int index = title.lastIndexOf(".");
+ title = index == -1 ? title : title.substring(0, index);
+ title = title.substring(title.indexOf('-') + 1);
+
+ String query = "artist:\"" + entry.getArtist() + "\"" +
+ " AND title:\"" + title + "\"";
+
+ return query;
+ } else {
+ return null;
+ }
+ } else {
+ return null;
+ }
+ }
+
+ public static String getContentType(HttpEntity entity) {
+ if (entity == null || entity.getContentType() == null) {
+ return null;
+ }
+ return entity.getContentType().getValue();
+ }
+
+ public static int getRemainingTrialDays(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ long installTime = prefs.getLong(Constants.PREFERENCES_KEY_INSTALL_TIME, 0L);
+
+ if (installTime == 0L) {
+ installTime = System.currentTimeMillis();
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putLong(Constants.PREFERENCES_KEY_INSTALL_TIME, installTime);
+ editor.commit();
+ }
+
+ long now = System.currentTimeMillis();
+ long millisPerDay = 24L * 60L * 60L * 1000L;
+ int daysSinceInstall = (int) ((now - installTime) / millisPerDay);
+ return Math.max(0, Constants.FREE_TRIAL_DAYS - daysSinceInstall);
+ }
+
+ public static boolean isCastProxy(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getBoolean(Constants.PREFERENCES_KEY_CAST_PROXY, false);
+ }
+
+ public static boolean isFirstLevelArtist(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getBoolean(Constants.PREFERENCES_KEY_FIRST_LEVEL_ARTIST + getActiveServer(context), true);
+ }
+ public static void toggleFirstLevelArtist(Context context) {
+ SharedPreferences prefs = Util.getPreferences(context);
+ SharedPreferences.Editor editor = prefs.edit();
+
+ if(prefs.getBoolean(Constants.PREFERENCES_KEY_FIRST_LEVEL_ARTIST + getActiveServer(context), true)) {
+ editor.putBoolean(Constants.PREFERENCES_KEY_FIRST_LEVEL_ARTIST + getActiveServer(context), false);
+ } else {
+ editor.putBoolean(Constants.PREFERENCES_KEY_FIRST_LEVEL_ARTIST + getActiveServer(context), true);
+ }
+
+ editor.commit();
+ }
+
+ public static boolean shouldStartOnHeadphones(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getBoolean(Constants.PREFERENCES_KEY_START_ON_HEADPHONES, false);
+ }
+
+ /**
+ * Get the contents of an <code>InputStream</code> as a <code>byte[]</code>.
+ * <p/>
+ * This method buffers the input internally, so there is no need to use a
+ * <code>BufferedInputStream</code>.
+ *
+ * @param input the <code>InputStream</code> to read from
+ * @return the requested byte array
+ * @throws NullPointerException if the input is null
+ * @throws IOException if an I/O error occurs
+ */
+ public static byte[] toByteArray(InputStream input) throws IOException {
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ copy(input, output);
+ return output.toByteArray();
+ }
+
+ public static long copy(InputStream input, OutputStream output)
+ throws IOException {
+ byte[] buffer = new byte[1024 * 4];
+ long count = 0;
+ int n;
+ while (-1 != (n = input.read(buffer))) {
+ output.write(buffer, 0, n);
+ count += n;
+ }
+ return count;
+ }
+
+ public static void renameFile(File from, File to) throws IOException {
+ if(!from.renameTo(to)) {
+ Log.i(TAG, "Failed to rename " + from + " to " + to);
+ }
+ }
+
+ public static void close(Closeable closeable) {
+ try {
+ if (closeable != null) {
+ closeable.close();
+ }
+ } catch (Throwable x) {
+ // Ignored
+ }
+ }
+
+ public static boolean delete(File file) {
+ if (file != null && file.exists()) {
+ if (!file.delete()) {
+ Log.w(TAG, "Failed to delete file " + file);
+ return false;
+ }
+ Log.i(TAG, "Deleted file " + file);
+ }
+ return true;
+ }
+
+ public static void toast(Context context, int messageId) {
+ toast(context, messageId, true);
+ }
+
+ public static void toast(Context context, int messageId, boolean shortDuration) {
+ toast(context, context.getString(messageId), shortDuration);
+ }
+
+ public static void toast(Context context, String message) {
+ toast(context, message, true);
+ }
+
+ public static void toast(Context context, String message, boolean shortDuration) {
+ if (toast == null) {
+ toast = Toast.makeText(context, message, shortDuration ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG);
+ toast.setGravity(Gravity.CENTER, 0, 0);
+ } else {
+ toast.setText(message);
+ toast.setDuration(shortDuration ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG);
+ }
+ toast.show();
+ }
+
+ public static void confirmDialog(Context context, int action, int subject, DialogInterface.OnClickListener onClick) {
+ Util.confirmDialog(context, context.getResources().getString(action).toLowerCase(), context.getResources().getString(subject), onClick, null);
+ }
+ public static void confirmDialog(Context context, int action, int subject, DialogInterface.OnClickListener onClick, DialogInterface.OnClickListener onCancel) {
+ Util.confirmDialog(context, context.getResources().getString(action).toLowerCase(), context.getResources().getString(subject), onClick, onCancel);
+ }
+ public static void confirmDialog(Context context, int action, String subject, DialogInterface.OnClickListener onClick) {
+ Util.confirmDialog(context, context.getResources().getString(action).toLowerCase(), subject, onClick, null);
+ }
+ public static void confirmDialog(Context context, int action, String subject, DialogInterface.OnClickListener onClick, DialogInterface.OnClickListener onCancel) {
+ Util.confirmDialog(context, context.getResources().getString(action).toLowerCase(), subject, onClick, onCancel);
+ }
+ public static void confirmDialog(Context context, String action, String subject, DialogInterface.OnClickListener onClick, DialogInterface.OnClickListener onCancel) {
+ new AlertDialog.Builder(context)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setTitle(R.string.common_confirm)
+ .setMessage(context.getResources().getString(R.string.common_confirm_message, action, subject))
+ .setPositiveButton(R.string.common_ok, onClick)
+ .setNegativeButton(R.string.common_cancel, onCancel)
+ .show();
+ }
+
+ /**
+ * Converts a byte-count to a formatted string suitable for display to the user.
+ * For instance:
+ * <ul>
+ * <li><code>format(918)</code> returns <em>"918 B"</em>.</li>
+ * <li><code>format(98765)</code> returns <em>"96 KB"</em>.</li>
+ * <li><code>format(1238476)</code> returns <em>"1.2 MB"</em>.</li>
+ * </ul>
+ * This method assumes that 1 KB is 1024 bytes.
+ * To get a localized string, please use formatLocalizedBytes instead.
+ *
+ * @param byteCount The number of bytes.
+ * @return The formatted string.
+ */
+ public static synchronized String formatBytes(long byteCount) {
+
+ // More than 1 GB?
+ if (byteCount >= 1024 * 1024 * 1024) {
+ NumberFormat gigaByteFormat = GIGA_BYTE_FORMAT;
+ return gigaByteFormat.format((double) byteCount / (1024 * 1024 * 1024));
+ }
+
+ // More than 1 MB?
+ if (byteCount >= 1024 * 1024) {
+ NumberFormat megaByteFormat = MEGA_BYTE_FORMAT;
+ return megaByteFormat.format((double) byteCount / (1024 * 1024));
+ }
+
+ // More than 1 KB?
+ if (byteCount >= 1024) {
+ NumberFormat kiloByteFormat = KILO_BYTE_FORMAT;
+ return kiloByteFormat.format((double) byteCount / 1024);
+ }
+
+ return byteCount + " B";
+ }
+
+ /**
+ * Converts a byte-count to a formatted string suitable for display to the user.
+ * For instance:
+ * <ul>
+ * <li><code>format(918)</code> returns <em>"918 B"</em>.</li>
+ * <li><code>format(98765)</code> returns <em>"96 KB"</em>.</li>
+ * <li><code>format(1238476)</code> returns <em>"1.2 MB"</em>.</li>
+ * </ul>
+ * This method assumes that 1 KB is 1024 bytes.
+ * This version of the method returns a localized string.
+ *
+ * @param byteCount The number of bytes.
+ * @return The formatted string.
+ */
+ public static synchronized String formatLocalizedBytes(long byteCount, Context context) {
+
+ // More than 1 GB?
+ if (byteCount >= 1024 * 1024 * 1024) {
+ if (GIGA_BYTE_LOCALIZED_FORMAT == null) {
+ GIGA_BYTE_LOCALIZED_FORMAT = new DecimalFormat(context.getResources().getString(R.string.util_bytes_format_gigabyte));
+ }
+
+ return GIGA_BYTE_LOCALIZED_FORMAT.format((double) byteCount / (1024 * 1024 * 1024));
+ }
+
+ // More than 1 MB?
+ if (byteCount >= 1024 * 1024) {
+ if (MEGA_BYTE_LOCALIZED_FORMAT == null) {
+ MEGA_BYTE_LOCALIZED_FORMAT = new DecimalFormat(context.getResources().getString(R.string.util_bytes_format_megabyte));
+ }
+
+ return MEGA_BYTE_LOCALIZED_FORMAT.format((double) byteCount / (1024 * 1024));
+ }
+
+ // More than 1 KB?
+ if (byteCount >= 1024) {
+ if (KILO_BYTE_LOCALIZED_FORMAT == null) {
+ KILO_BYTE_LOCALIZED_FORMAT = new DecimalFormat(context.getResources().getString(R.string.util_bytes_format_kilobyte));
+ }
+
+ return KILO_BYTE_LOCALIZED_FORMAT.format((double) byteCount / 1024);
+ }
+
+ if (BYTE_LOCALIZED_FORMAT == null) {
+ BYTE_LOCALIZED_FORMAT = new DecimalFormat(context.getResources().getString(R.string.util_bytes_format_byte));
+ }
+
+ return BYTE_LOCALIZED_FORMAT.format((double) byteCount);
+ }
+
+ public static String formatDuration(Integer seconds) {
+ if (seconds == null) {
+ return null;
+ }
+
+ int hours = seconds / 3600;
+ int minutes = (seconds / 60) % 60;
+ int secs = seconds % 60;
+
+ StringBuilder builder = new StringBuilder(7);
+ if(hours > 0) {
+ builder.append(hours).append(":");
+ if(minutes < 10) {
+ builder.append("0");
+ }
+ }
+ builder.append(minutes).append(":");
+ if (secs < 10) {
+ builder.append("0");
+ }
+ builder.append(secs);
+ return builder.toString();
+ }
+
+ public static String formatDate(Date date) {
+ if(date == null) {
+ return "Never";
+ } else {
+ if(date.getYear() != CURRENT_YEAR) {
+ return DATE_FORMAT_LONG.format(date);
+ } else {
+ return DATE_FORMAT_SHORT.format(date);
+ }
+ }
+ }
+
+ public static boolean equals(Object object1, Object object2) {
+ if (object1 == object2) {
+ return true;
+ }
+ if (object1 == null || object2 == null) {
+ return false;
+ }
+ return object1.equals(object2);
+
+ }
+
+ /**
+ * Encodes the given string by using the hexadecimal representation of its UTF-8 bytes.
+ *
+ * @param s The string to encode.
+ * @return The encoded string.
+ */
+ public static String utf8HexEncode(String s) {
+ if (s == null) {
+ return null;
+ }
+ byte[] utf8;
+ try {
+ utf8 = s.getBytes(Constants.UTF_8);
+ } catch (UnsupportedEncodingException x) {
+ throw new RuntimeException(x);
+ }
+ return hexEncode(utf8);
+ }
+
+ /**
+ * Converts an array of bytes into an array of characters representing the hexadecimal values of each byte in order.
+ * The returned array will be double the length of the passed array, as it takes two characters to represent any
+ * given byte.
+ *
+ * @param data Bytes to convert to hexadecimal characters.
+ * @return A string containing hexadecimal characters.
+ */
+ public static String hexEncode(byte[] data) {
+ int length = data.length;
+ char[] out = new char[length << 1];
+ // two characters form the hex value.
+ for (int i = 0, j = 0; i < length; i++) {
+ out[j++] = HEX_DIGITS[(0xF0 & data[i]) >>> 4];
+ out[j++] = HEX_DIGITS[0x0F & data[i]];
+ }
+ return new String(out);
+ }
+
+ /**
+ * Calculates the MD5 digest and returns the value as a 32 character hex string.
+ *
+ * @param s Data to digest.
+ * @return MD5 digest as a hex string.
+ */
+ public static String md5Hex(String s) {
+ if (s == null) {
+ return null;
+ }
+
+ try {
+ MessageDigest md5 = MessageDigest.getInstance("MD5");
+ return hexEncode(md5.digest(s.getBytes(Constants.UTF_8)));
+ } catch (Exception x) {
+ throw new RuntimeException(x.getMessage(), x);
+ }
+ }
+
+ public static boolean isNullOrWhiteSpace(String string) {
+ return string == null || "".equals(string) || "".equals(string.trim());
+ }
+
+ public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
+ // Raw height and width of image
+ final int height = options.outHeight;
+ final int width = options.outWidth;
+ int inSampleSize = 1;
+
+ if (height > reqHeight || width > reqWidth) {
+
+ // Calculate ratios of height and width to requested height and
+ // width
+ final int heightRatio = Math.round((float) height / (float) reqHeight);
+ final int widthRatio = Math.round((float) width / (float) reqWidth);
+
+ // Choose the smallest ratio as inSampleSize value, this will
+ // guarantee
+ // a final image with both dimensions larger than or equal to the
+ // requested height and width.
+ inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
+ }
+
+ return inSampleSize;
+ }
+
+ public static int getScaledHeight(double height, double width, int newWidth) {
+ // Try to keep correct aspect ratio of the original image, do not force a square
+ double aspectRatio = height / width;
+
+ // Assume the size given refers to the width of the image, so calculate the new height using
+ // the previously determined aspect ratio
+ return (int) Math.round(newWidth * aspectRatio);
+ }
+
+ public static int getScaledHeight(Bitmap bitmap, int width) {
+ return Util.getScaledHeight((double) bitmap.getHeight(), (double) bitmap.getWidth(), width);
+ }
+
+ public static int getStringDistance(CharSequence s, CharSequence t) {
+ if (s == null || t == null) {
+ throw new IllegalArgumentException("Strings must not be null");
+ }
+
+ if(t.toString().toLowerCase().indexOf(s.toString().toLowerCase()) != -1) {
+ return 1;
+ }
+
+ int n = s.length();
+ int m = t.length();
+
+ if (n == 0) {
+ return m;
+ } else if (m == 0) {
+ return n;
+ }
+
+ if (n > m) {
+ final CharSequence tmp = s;
+ s = t;
+ t = tmp;
+ n = m;
+ m = t.length();
+ }
+
+ int p[] = new int[n + 1];
+ int d[] = new int[n + 1];
+ int _d[];
+
+ int i;
+ int j;
+ char t_j;
+ int cost;
+
+ for (i = 0; i <= n; i++) {
+ p[i] = i;
+ }
+
+ for (j = 1; j <= m; j++) {
+ t_j = t.charAt(j - 1);
+ d[0] = j;
+
+ for (i = 1; i <= n; i++) {
+ cost = s.charAt(i - 1) == t_j ? 0 : 1;
+ d[i] = Math.min(Math.min(d[i - 1] + 1, p[i] + 1), p[i - 1] + cost);
+ }
+
+ _d = p;
+ p = d;
+ d = _d;
+ }
+
+ return p[n];
+ }
+
+ public static boolean isNetworkConnected(Context context) {
+ return isNetworkConnected(context, false);
+ }
+ public static boolean isNetworkConnected(Context context, boolean streaming) {
+ ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo networkInfo = manager.getActiveNetworkInfo();
+ boolean connected = networkInfo != null && networkInfo.isConnected();
+
+ if(streaming) {
+ boolean wifiConnected = connected && networkInfo.getType() == ConnectivityManager.TYPE_WIFI;
+ boolean wifiRequired = isWifiRequiredForDownload(context);
+
+ return connected && (!wifiRequired || wifiConnected);
+ } else {
+ return connected;
+ }
+ }
+ public static boolean isWifiConnected(Context context) {
+ ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo networkInfo = manager.getActiveNetworkInfo();
+ boolean connected = networkInfo != null && networkInfo.isConnected();
+ return connected && (networkInfo.getType() == ConnectivityManager.TYPE_WIFI);
+ }
+ public static String getSSID(Context context) {
+ if (isWifiConnected(context)) {
+ WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+ if (wifiManager.getConnectionInfo() != null && wifiManager.getConnectionInfo().getSSID() != null) {
+ return wifiManager.getConnectionInfo().getSSID().replace("\"", "");
+ }
+ return null;
+ }
+ return null;
+ }
+
+ public static boolean isExternalStoragePresent() {
+ return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState());
+ }
+
+ private static boolean isWifiRequiredForDownload(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getBoolean(Constants.PREFERENCES_KEY_WIFI_REQUIRED_FOR_DOWNLOAD, false);
+ }
+
+ public static void info(Context context, int titleId, int messageId) {
+ info(context, titleId, messageId, true);
+ }
+ public static void info(Context context, int titleId, String message) {
+ info(context, titleId, message, true);
+ }
+ public static void info(Context context, String title, String message) {
+ info(context, title, message, true);
+ }
+ public static void info(Context context, int titleId, int messageId, boolean linkify) {
+ showDialog(context, android.R.drawable.ic_dialog_info, titleId, messageId, linkify);
+ }
+ public static void info(Context context, int titleId, String message, boolean linkify) {
+ showDialog(context, android.R.drawable.ic_dialog_info, titleId, message, linkify);
+ }
+ public static void info(Context context, String title, String message, boolean linkify) {
+ showDialog(context, android.R.drawable.ic_dialog_info, title, message, linkify);
+ }
+
+ private static void showDialog(Context context, int icon, int titleId, int messageId) {
+ showDialog(context, icon, titleId, messageId, true);
+ }
+ private static void showDialog(Context context, int icon, int titleId, String message) {
+ showDialog(context, icon, titleId, message, true);
+ }
+ private static void showDialog(Context context, int icon, String title, String message) {
+ showDialog(context, icon, title, message, true);
+ }
+ private static void showDialog(Context context, int icon, int titleId, int messageId, boolean linkify) {
+ showDialog(context, icon, context.getResources().getString(titleId), context.getResources().getString(messageId), linkify);
+ }
+ private static void showDialog(Context context, int icon, int titleId, String message, boolean linkify) {
+ showDialog(context, icon, context.getResources().getString(titleId), message, linkify);
+ }
+ private static void showDialog(Context context, int icon, String title, String message, boolean linkify) {
+ SpannableString ss = new SpannableString(message);
+ if(linkify) {
+ Linkify.addLinks(ss, Linkify.ALL);
+ }
+
+ AlertDialog dialog = new AlertDialog.Builder(context)
+ .setIcon(icon)
+ .setTitle(title)
+ .setMessage(ss)
+ .setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int i) {
+ dialog.dismiss();
+ }
+ })
+ .show();
+
+ ((TextView)dialog.findViewById(android.R.id.message)).setMovementMethod(LinkMovementMethod.getInstance());
+ }
+ public static void showHTMLDialog(Context context, int title, int message) {
+ showHTMLDialog(context, title, context.getResources().getString(message));
+ }
+ public static void showHTMLDialog(Context context, int title, String message) {
+ AlertDialog dialog = new AlertDialog.Builder(context)
+ .setIcon(android.R.drawable.ic_dialog_info)
+ .setTitle(title)
+ .setMessage(Html.fromHtml(message))
+ .setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int i) {
+ dialog.dismiss();
+ }
+ })
+ .show();
+
+ ((TextView)dialog.findViewById(android.R.id.message)).setMovementMethod(LinkMovementMethod.getInstance());
+ }
+
+ public static void sleepQuietly(long millis) {
+ try {
+ Thread.sleep(millis);
+ } catch (InterruptedException x) {
+ Log.w(TAG, "Interrupted from sleep.", x);
+ }
+ }
+
+ public static void startActivityWithoutTransition(Activity currentActivity, Class<? extends Activity> newActivitiy) {
+ startActivityWithoutTransition(currentActivity, new Intent(currentActivity, newActivitiy));
+ }
+
+ public static void startActivityWithoutTransition(Activity currentActivity, Intent intent) {
+ currentActivity.startActivity(intent);
+ disablePendingTransition(currentActivity);
+ }
+
+ public static void disablePendingTransition(Activity activity) {
+
+ // Activity.overridePendingTransition() was introduced in Android 2.0. Use reflection to maintain
+ // compatibility with 1.5.
+ try {
+ Method method = Activity.class.getMethod("overridePendingTransition", int.class, int.class);
+ method.invoke(activity, 0, 0);
+ } catch (Throwable x) {
+ // Ignored
+ }
+ }
+
+ public static Drawable createDrawableFromBitmap(Context context, Bitmap bitmap) {
+ // BitmapDrawable(Resources, Bitmap) was introduced in Android 1.6. Use reflection to maintain
+ // compatibility with 1.5.
+ try {
+ Constructor<BitmapDrawable> constructor = BitmapDrawable.class.getConstructor(Resources.class, Bitmap.class);
+ return constructor.newInstance(context.getResources(), bitmap);
+ } catch (Throwable x) {
+ return new BitmapDrawable(bitmap);
+ }
+ }
+
+ public static int getAttribute(Context context, int attr) {
+ int res;
+ int[] attrs = new int[] {attr};
+ TypedArray typedArray = context.obtainStyledAttributes(attrs);
+ res = typedArray.getResourceId(0, 0);
+ typedArray.recycle();
+ return res;
+ }
+
+ public static void registerMediaButtonEventReceiver(Context context) {
+
+ // Only do it if enabled in the settings.
+ SharedPreferences prefs = getPreferences(context);
+ boolean enabled = prefs.getBoolean(Constants.PREFERENCES_KEY_MEDIA_BUTTONS, true);
+
+ if (enabled) {
+
+ // AudioManager.registerMediaButtonEventReceiver() was introduced in Android 2.2.
+ // Use reflection to maintain compatibility with 1.5.
+ try {
+ AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ ComponentName componentName = new ComponentName(context.getPackageName(), MediaButtonIntentReceiver.class.getName());
+ Method method = AudioManager.class.getMethod("registerMediaButtonEventReceiver", ComponentName.class);
+ method.invoke(audioManager, componentName);
+ } catch (Throwable x) {
+ // Ignored.
+ }
+ }
+ }
+
+ public static void unregisterMediaButtonEventReceiver(Context context) {
+ // AudioManager.unregisterMediaButtonEventReceiver() was introduced in Android 2.2.
+ // Use reflection to maintain compatibility with 1.5.
+ try {
+ AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ ComponentName componentName = new ComponentName(context.getPackageName(), MediaButtonIntentReceiver.class.getName());
+ Method method = AudioManager.class.getMethod("unregisterMediaButtonEventReceiver", ComponentName.class);
+ method.invoke(audioManager, componentName);
+ } catch (Throwable x) {
+ // Ignored.
+ }
+ }
+
+ @TargetApi(8)
+ public static void requestAudioFocus(final Context context) {
+ if (Build.VERSION.SDK_INT >= 8 && focusListener == null) {
+ final AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ audioManager.requestAudioFocus(focusListener = new OnAudioFocusChangeListener() {
+ public void onAudioFocusChange(int focusChange) {
+ DownloadService downloadService = (DownloadService)context;
+ if((focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT || focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) && !downloadService.isRemoteEnabled()) {
+ if(downloadService.getPlayerState() == PlayerState.STARTED) {
+ Log.i(TAG, "Temporary loss of focus");
+ SharedPreferences prefs = getPreferences(context);
+ int lossPref = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_TEMP_LOSS, "1"));
+ if(lossPref == 2 || (lossPref == 1 && focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK)) {
+ lowerFocus = true;
+ downloadService.setVolume(0.1f);
+ } else if(lossPref == 0 || (lossPref == 1 && focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT)) {
+ pauseFocus = true;
+ downloadService.pause(true);
+ }
+ }
+ } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
+ if(pauseFocus) {
+ pauseFocus = false;
+ downloadService.start();
+ } else if(lowerFocus) {
+ lowerFocus = false;
+ downloadService.setVolume(1.0f);
+ }
+ } else if(focusChange == AudioManager.AUDIOFOCUS_LOSS && !downloadService.isRemoteEnabled()) {
+ Log.i(TAG, "Permanently lost focus");
+ focusListener = null;
+ downloadService.pause();
+ audioManager.abandonAudioFocus(this);
+ }
+ }
+ }, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
+ }
+ }
+
+ public static void abandonAudioFocus(Context context) {
+ if(focusListener != null) {
+ final AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ audioManager.abandonAudioFocus(focusListener);
+ focusListener = null;
+ }
+ }
+
+ /**
+ * <p>Broadcasts the given song info as the new song being played.</p>
+ */
+ public static void broadcastNewTrackInfo(Context context, MusicDirectory.Entry song) {
+ DownloadService downloadService = (DownloadService)context;
+ Intent intent = new Intent(EVENT_META_CHANGED);
+ Intent avrcpIntent = new Intent(AVRCP_METADATA_CHANGED);
+
+ if (song != null) {
+ intent.putExtra("title", song.getTitle());
+ intent.putExtra("artist", song.getArtist());
+ intent.putExtra("album", song.getAlbum());
+
+ File albumArtFile = FileUtil.getAlbumArtFile(context, song);
+ intent.putExtra("coverart", albumArtFile.getAbsolutePath());
+ avrcpIntent.putExtra("playing", true);
+ } else {
+ intent.putExtra("title", "");
+ intent.putExtra("artist", "");
+ intent.putExtra("album", "");
+ intent.putExtra("coverart", "");
+ avrcpIntent.putExtra("playing", false);
+ }
+ addTrackInfo(context, song, avrcpIntent);
+
+ context.sendBroadcast(intent);
+ context.sendBroadcast(avrcpIntent);
+ }
+
+ /**
+ * <p>Broadcasts the given player state as the one being set.</p>
+ */
+ public static void broadcastPlaybackStatusChange(Context context, MusicDirectory.Entry song, PlayerState state) {
+ Intent intent = new Intent(EVENT_PLAYSTATE_CHANGED);
+ Intent avrcpIntent = new Intent(AVRCP_PLAYSTATE_CHANGED);
+
+ switch (state) {
+ case STARTED:
+ intent.putExtra("state", "play");
+ avrcpIntent.putExtra("playing", true);
+ break;
+ case STOPPED:
+ intent.putExtra("state", "stop");
+ avrcpIntent.putExtra("playing", false);
+ break;
+ case PAUSED:
+ intent.putExtra("state", "pause");
+ avrcpIntent.putExtra("playing", false);
+ break;
+ case PREPARED:
+ // Only send quick pause event for samsung devices, causes issues for others
+ if(Build.MANUFACTURER.toLowerCase().indexOf("samsung") != -1) {
+ avrcpIntent.putExtra("playing", false);
+ } else {
+ return; // Don't broadcast anything
+ }
+ break;
+ case COMPLETED:
+ intent.putExtra("state", "complete");
+ avrcpIntent.putExtra("playing", false);
+ break;
+ default:
+ return; // No need to broadcast.
+ }
+ addTrackInfo(context, song, avrcpIntent);
+
+ if(state != PlayerState.PREPARED) {
+ context.sendBroadcast(intent);
+ }
+ context.sendBroadcast(avrcpIntent);
+ }
+
+ private static void addTrackInfo(Context context, MusicDirectory.Entry song, Intent intent) {
+ if (song != null) {
+ DownloadService downloadService = (DownloadService)context;
+ File albumArtFile = FileUtil.getAlbumArtFile(context, song);
+
+ intent.putExtra("track", song.getTitle());
+ intent.putExtra("artist", song.getArtist());
+ intent.putExtra("album", song.getAlbum());
+ intent.putExtra("ListSize", (long) downloadService.getSongs().size());
+ intent.putExtra("id", (long) downloadService.getCurrentPlayingIndex() + 1);
+ intent.putExtra("duration", (long) downloadService.getPlayerDuration());
+ intent.putExtra("position", (long) downloadService.getPlayerPosition());
+ intent.putExtra("coverart", albumArtFile.getAbsolutePath());
+ } else {
+ intent.putExtra("track", "");
+ intent.putExtra("artist", "");
+ intent.putExtra("album", "");
+ intent.putExtra("ListSize", (long) 0);
+ intent.putExtra("id", (long) 0);
+ intent.putExtra("duration", (long) 0);
+ intent.putExtra("position", (long) 0);
+ intent.putExtra("coverart", "");
+ }
+ }
+
+ public static WifiManager.WifiLock createWifiLock(Context context, String tag) {
+ WifiManager wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+ int lockType = WifiManager.WIFI_MODE_FULL;
+ if (Build.VERSION.SDK_INT >= 12) {
+ lockType = 3;
+ }
+ return wm.createWifiLock(lockType, tag);
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/compat/CastCompat.java b/app/src/main/java/github/daneren2005/dsub/util/compat/CastCompat.java
new file mode 100644
index 00000000..ab64bca9
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/compat/CastCompat.java
@@ -0,0 +1,57 @@
+/*
+ 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.util.compat;
+
+import android.support.v7.media.MediaRouter;
+
+import com.google.android.gms.cast.CastDevice;
+import com.google.android.gms.cast.CastMediaControlIntent;
+
+import github.daneren2005.dsub.service.ChromeCastController;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.service.RemoteController;
+
+/**
+ * Created by owner on 2/9/14.
+ */
+public final class CastCompat {
+ public static final String APPLICATION_ID = "5F85EBEB";
+
+ static {
+ try {
+ Class.forName("com.google.android.gms.cast.CastDevice");
+ } catch (Exception ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ public static void checkAvailable() throws Throwable {
+ // Calling here forces class initialization.
+ }
+
+ public static RemoteController getController(DownloadService downloadService, MediaRouter.RouteInfo info) {
+ CastDevice device = CastDevice.getFromBundle(info.getExtras());
+ if(device != null) {
+ return new ChromeCastController(downloadService, device);
+ } else {
+ return null;
+ }
+ }
+
+ public static String getCastControlCategory() {
+ return CastMediaControlIntent.categoryForCast(APPLICATION_ID);
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientBase.java b/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientBase.java
new file mode 100644
index 00000000..320092e9
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientBase.java
@@ -0,0 +1,43 @@
+package github.daneren2005.dsub.util.compat;
+
+import github.daneren2005.dsub.domain.MusicDirectory.Entry;
+import android.content.ComponentName;
+import android.content.Context;
+import android.support.v7.media.MediaRouter;
+import android.util.Log;
+
+public class RemoteControlClientBase extends RemoteControlClientHelper {
+
+ private static final String TAG = RemoteControlClientBase.class.getSimpleName();
+
+ @Override
+ public void register(Context context, ComponentName mediaButtonReceiverComponent) {
+
+ }
+
+ @Override
+ public void unregister(Context context) {
+
+ }
+
+ @Override
+ public void setPlaybackState(int state) {
+
+ }
+
+ @Override
+ public void updateMetadata(Context context, Entry currentSong) {
+
+ }
+
+ @Override
+ public void registerRoute(MediaRouter router) {
+
+ }
+
+ @Override
+ public void unregisterRoute(MediaRouter router) {
+
+ }
+
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientHelper.java b/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientHelper.java
new file mode 100644
index 00000000..93075a28
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientHelper.java
@@ -0,0 +1,32 @@
+package github.daneren2005.dsub.util.compat;
+
+import github.daneren2005.dsub.domain.MusicDirectory;
+import android.content.ComponentName;
+import android.content.Context;
+import android.support.v7.media.MediaRouter;
+import android.os.Build;
+
+public abstract class RemoteControlClientHelper {
+
+ public static RemoteControlClientHelper createInstance() {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ return new RemoteControlClientBase();
+ } else if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+ return new RemoteControlClientJB();
+ } else {
+ return new RemoteControlClientICS();
+ }
+ }
+
+ protected RemoteControlClientHelper() {
+ // Avoid instantiation
+ }
+
+ public abstract void register(final Context context, final ComponentName mediaButtonReceiverComponent);
+ public abstract void unregister(final Context context);
+ public abstract void setPlaybackState(final int state);
+ public abstract void updateMetadata(final Context context, final MusicDirectory.Entry currentSong);
+ public abstract void registerRoute(MediaRouter router);
+ public abstract void unregisterRoute(MediaRouter router);
+
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientICS.java b/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientICS.java
new file mode 100644
index 00000000..50283da6
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientICS.java
@@ -0,0 +1,104 @@
+package github.daneren2005.dsub.util.compat;
+
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.util.ImageLoader;
+import android.annotation.TargetApi;
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.media.AudioManager;
+import android.media.MediaMetadataRetriever;
+import android.media.RemoteControlClient;
+import android.support.v7.media.MediaRouter;
+
+import github.daneren2005.dsub.activity.SubsonicActivity;
+
+@TargetApi(14)
+public class RemoteControlClientICS extends RemoteControlClientHelper {
+ private static String TAG = RemoteControlClientICS.class.getSimpleName();
+
+ protected RemoteControlClient mRemoteControl;
+ protected ImageLoader imageLoader;
+ protected DownloadService downloadService;
+
+ public void register(final Context context, final ComponentName mediaButtonReceiverComponent) {
+ downloadService = (DownloadService) context;
+ AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+
+ // build the PendingIntent for the remote control client
+ Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
+ mediaButtonIntent.setComponent(mediaButtonReceiverComponent);
+ PendingIntent mediaPendingIntent = PendingIntent.getBroadcast(context.getApplicationContext(), 0, mediaButtonIntent, 0);
+
+ // create and register the remote control client
+ mRemoteControl = new RemoteControlClient(mediaPendingIntent);
+ audioManager.registerRemoteControlClient(mRemoteControl);
+
+ mRemoteControl.setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED);
+ mRemoteControl.setTransportControlFlags(getTransportFlags());
+ imageLoader = SubsonicActivity.getStaticImageLoader(context);
+ }
+
+ public void unregister(final Context context) {
+ if (mRemoteControl != null) {
+ AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ audioManager.unregisterRemoteControlClient(mRemoteControl);
+ }
+ }
+
+ public void setPlaybackState(final int state) {
+ mRemoteControl.setPlaybackState(state);
+ }
+
+ public void updateMetadata(final Context context, final MusicDirectory.Entry currentSong) {
+ if(imageLoader == null) {
+ imageLoader = SubsonicActivity.getStaticImageLoader(context);
+ }
+
+ // Update the remote controls
+ RemoteControlClient.MetadataEditor editor = mRemoteControl.editMetadata(true);
+ updateMetadata(currentSong, editor);
+ editor.apply();
+ if (currentSong == null || imageLoader == null) {
+ mRemoteControl.editMetadata(true)
+ .putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, null)
+ .apply();
+ } else {
+ imageLoader.loadImage(context, mRemoteControl, currentSong);
+ }
+ }
+
+ @Override
+ public void registerRoute(MediaRouter router) {
+ router.addRemoteControlClient(mRemoteControl);
+ }
+
+ @Override
+ public void unregisterRoute(MediaRouter router) {
+ router.removeRemoteControlClient(mRemoteControl);
+ }
+
+ protected void updateMetadata(final MusicDirectory.Entry currentSong, final RemoteControlClient.MetadataEditor editor) {
+ editor.putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, (currentSong == null) ? null : currentSong.getArtist())
+ .putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, (currentSong == null) ? null : currentSong.getAlbum())
+ .putString(MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST, (currentSong == null) ? null : currentSong.getArtist())
+ .putString(MediaMetadataRetriever.METADATA_KEY_TITLE, (currentSong) == null ? null : currentSong.getTitle())
+ .putString(MediaMetadataRetriever.METADATA_KEY_GENRE, (currentSong) == null ? null : currentSong.getGenre())
+ .putLong(MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER, (currentSong == null) ?
+ 0 : ((currentSong.getTrack() == null) ? 0 : currentSong.getTrack()))
+ .putLong(MediaMetadataRetriever.METADATA_KEY_DURATION, (currentSong == null) ?
+ 0 : ((currentSong.getDuration() == null) ? 0 : (currentSong.getDuration() * 1000)));
+ }
+
+ protected int getTransportFlags() {
+ return RemoteControlClient.FLAG_KEY_MEDIA_PLAY |
+ RemoteControlClient.FLAG_KEY_MEDIA_PAUSE |
+ RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE |
+ RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS |
+ RemoteControlClient.FLAG_KEY_MEDIA_NEXT |
+ RemoteControlClient.FLAG_KEY_MEDIA_STOP;
+ }
+
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientJB.java b/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientJB.java
new file mode 100644
index 00000000..c27df2ba
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientJB.java
@@ -0,0 +1,58 @@
+package github.daneren2005.dsub.util.compat;
+
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.util.ImageLoader;
+import android.annotation.TargetApi;
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.media.AudioManager;
+import android.media.MediaMetadataRetriever;
+import android.media.RemoteControlClient;
+import github.daneren2005.dsub.activity.SubsonicActivity;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.util.SilentBackgroundTask;
+
+@TargetApi(18)
+public class RemoteControlClientJB extends RemoteControlClientICS {
+ @Override
+ public void register(final Context context, final ComponentName mediaButtonReceiverComponent) {
+ super.register(context, mediaButtonReceiverComponent);
+
+ mRemoteControl.setOnGetPlaybackPositionListener(new RemoteControlClient.OnGetPlaybackPositionListener() {
+ @Override
+ public long onGetPlaybackPosition() {
+ return downloadService.getPlayerPosition();
+ }
+ });
+ mRemoteControl.setPlaybackPositionUpdateListener(new RemoteControlClient.OnPlaybackPositionUpdateListener() {
+ @Override
+ public void onPlaybackPositionUpdate(final long newPosition) {
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ downloadService.seekTo((int) newPosition);
+ return null;
+ }
+ }.execute();
+ setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING);
+ }
+ });
+ }
+
+ @Override
+ public void setPlaybackState(final int state) {
+ long position = -1;
+ if(state == RemoteControlClient.PLAYSTATE_PLAYING || state == RemoteControlClient.PLAYSTATE_PAUSED) {
+ position = downloadService.getPlayerPosition();
+ }
+ mRemoteControl.setPlaybackState(state, position, 1.0f);
+ }
+
+ @Override
+ protected int getTransportFlags() {
+ return super.getTransportFlags() | RemoteControlClient.FLAG_KEY_MEDIA_POSITION_UPDATE;
+ }
+
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/tags/Bastp.java b/app/src/main/java/github/daneren2005/dsub/util/tags/Bastp.java
new file mode 100644
index 00000000..aa0a2e25
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/tags/Bastp.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2013 Adrian Ulrich <adrian@blinkenlights.ch>
+ *
+ * This program 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.
+ *
+ * This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+package github.daneren2005.dsub.util.tags;
+
+import java.io.RandomAccessFile;
+import java.io.IOException;
+import java.util.HashMap;
+
+
+public class Bastp {
+
+ public Bastp() {
+ }
+
+ public HashMap getTags(String fname) {
+ HashMap tags = new HashMap();
+ try {
+ RandomAccessFile ra = new RandomAccessFile(fname, "r");
+ tags = getTags(ra);
+ ra.close();
+ }
+ catch(Exception e) {
+ /* we dont' care much: SOMETHING went wrong. d'oh! */
+ }
+
+ return tags;
+ }
+
+ public HashMap getTags(RandomAccessFile s) {
+ HashMap tags = new HashMap();
+ byte[] file_ff = new byte[4];
+
+ try {
+ s.read(file_ff);
+ String magic = new String(file_ff);
+ if(magic.equals("fLaC")) {
+ tags = (new FlacFile()).getTags(s);
+ }
+ else if(magic.equals("OggS")) {
+ tags = (new OggFile()).getTags(s);
+ }
+ else if(file_ff[0] == -1 && file_ff[1] == -5) { /* aka 0xfffb in real languages */
+ tags = (new LameHeader()).getTags(s);
+ }
+ else if(magic.substring(0,3).equals("ID3")) {
+ tags = (new ID3v2File()).getTags(s);
+ if(tags.containsKey("_hdrlen")) {
+ Long hlen = Long.parseLong( tags.get("_hdrlen").toString(), 10 );
+ HashMap lameInfo = (new LameHeader()).parseLameHeader(s, hlen);
+ /* add gain tags if not already present */
+ inheritTag("REPLAYGAIN_TRACK_GAIN", lameInfo, tags);
+ inheritTag("REPLAYGAIN_ALBUM_GAIN", lameInfo, tags);
+ }
+ }
+ tags.put("_magic", magic);
+ }
+ catch (IOException e) {
+ }
+ return tags;
+ }
+
+ private void inheritTag(String key, HashMap from, HashMap to) {
+ if(!to.containsKey(key) && from.containsKey(key)) {
+ to.put(key, from.get(key));
+ }
+ }
+
+}
+
diff --git a/app/src/main/java/github/daneren2005/dsub/util/tags/BastpUtil.java b/app/src/main/java/github/daneren2005/dsub/util/tags/BastpUtil.java
new file mode 100644
index 00000000..7ff517fd
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/tags/BastpUtil.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2013 Adrian Ulrich <adrian@blinkenlights.ch>
+ *
+ * This program 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.
+ *
+ * This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package github.daneren2005.dsub.util.tags;
+
+import android.support.v4.util.LruCache;
+import java.util.HashMap;
+import java.util.Vector;
+
+public final class BastpUtil {
+ private static final RGLruCache rgCache = new RGLruCache(16);
+
+ /** Returns the ReplayGain values of 'path' as <track,album>
+ */
+ public static float[] getReplayGainValues(String path) {
+ float[] cached = rgCache.get(path);
+
+ if(cached == null) {
+ cached = getReplayGainValuesFromFile(path);
+ rgCache.put(path, cached);
+ }
+ return cached;
+ }
+
+
+
+ /** Parse given file and return track,album replay gain values
+ */
+ private static float[] getReplayGainValuesFromFile(String path) {
+ String[] keys = { "REPLAYGAIN_TRACK_GAIN", "REPLAYGAIN_ALBUM_GAIN" };
+ float[] adjust= { 0f , 0f };
+ HashMap tags = (new Bastp()).getTags(path);
+
+ for (int i=0; i<keys.length; i++) {
+ String curKey = keys[i];
+ if(tags.containsKey(curKey)) {
+ String rg_raw = (String)((Vector)tags.get(curKey)).get(0);
+ String rg_numonly = "";
+ float rg_float = 0f;
+ try {
+ String nums = rg_raw.replaceAll("[^0-9.-]","");
+ rg_float = Float.parseFloat(nums);
+ } catch(Exception e) {}
+ adjust[i] = rg_float;
+ }
+ }
+ return adjust;
+ }
+
+ /** LRU cache for ReplayGain values
+ */
+ private static class RGLruCache extends LruCache<String, float[]> {
+ public RGLruCache(int size) {
+ super(size);
+ }
+ }
+
+}
+
diff --git a/app/src/main/java/github/daneren2005/dsub/util/tags/Common.java b/app/src/main/java/github/daneren2005/dsub/util/tags/Common.java
new file mode 100644
index 00000000..51344d90
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/tags/Common.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2013 Adrian Ulrich <adrian@blinkenlights.ch>
+ *
+ * This program 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.
+ *
+ * This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+package github.daneren2005.dsub.util.tags;
+
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.util.HashMap;
+import java.util.Vector;
+
+public class Common {
+ private static final long MAX_PKT_SIZE = 524288;
+
+ public void xdie(String reason) throws IOException {
+ throw new IOException(reason);
+ }
+
+ /*
+ ** Returns a 32bit int from given byte offset in LE
+ */
+ public int b2le32(byte[] b, int off) {
+ int r = 0;
+ for(int i=0; i<4; i++) {
+ r |= ( b2u(b[off+i]) << (8*i) );
+ }
+ return r;
+ }
+
+ public int b2be32(byte[] b, int off) {
+ return swap32(b2le32(b, off));
+ }
+
+ public int swap32(int i) {
+ return((i&0xff)<<24)+((i&0xff00)<<8)+((i&0xff0000)>>8)+((i>>24)&0xff);
+ }
+
+ /*
+ ** convert 'byte' value into unsigned int
+ */
+ public int b2u(byte x) {
+ return (x & 0xFF);
+ }
+
+ /*
+ ** Printout debug message to STDOUT
+ */
+ public void debug(String s) {
+ System.out.println("DBUG "+s);
+ }
+
+ public HashMap parse_vorbis_comment(RandomAccessFile s, long offset, long payload_len) throws IOException {
+ HashMap tags = new HashMap();
+ int comments = 0; // number of found comments
+ int xoff = 0; // offset within 'scratch'
+ int can_read = (int)(payload_len > MAX_PKT_SIZE ? MAX_PKT_SIZE : payload_len);
+ byte[] scratch = new byte[can_read];
+
+ // seek to given position and slurp in the payload
+ s.seek(offset);
+ s.read(scratch);
+
+ // skip vendor string in format: [LEN][VENDOR_STRING]
+ xoff += 4 + b2le32(scratch, xoff); // 4 = LEN = 32bit int
+ comments = b2le32(scratch, xoff);
+ xoff += 4;
+
+ // debug("comments count = "+comments);
+ for(int i=0; i<comments; i++) {
+
+ int clen = (int)b2le32(scratch, xoff);
+ xoff += 4+clen;
+
+ if(xoff > scratch.length)
+ xdie("string out of bounds");
+
+ String tag_raw = new String(scratch, xoff-clen, clen);
+ String[] tag_vec = tag_raw.split("=",2);
+ String tag_key = tag_vec[0].toUpperCase();
+
+ addTagEntry(tags, tag_key, tag_vec[1]);
+ }
+ return tags;
+ }
+
+ public void addTagEntry(HashMap tags, String key, String value) {
+ if(tags.containsKey(key)) {
+ ((Vector)tags.get(key)).add(value); // just add to existing vector
+ }
+ else {
+ Vector vx = new Vector();
+ vx.add(value);
+ tags.put(key, vx);
+ }
+ }
+
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/tags/FlacFile.java b/app/src/main/java/github/daneren2005/dsub/util/tags/FlacFile.java
new file mode 100644
index 00000000..de3584d1
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/tags/FlacFile.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2013 Adrian Ulrich <adrian@blinkenlights.ch>
+ *
+ * This program 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.
+ *
+ * This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package github.daneren2005.dsub.util.tags;
+
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.util.HashMap;
+import java.util.Enumeration;
+
+
+public class FlacFile extends Common {
+ private static final int FLAC_TYPE_COMMENT = 4; // ID of 'VorbisComment's
+
+ public FlacFile() {
+ }
+
+ public HashMap getTags(RandomAccessFile s) throws IOException {
+ int xoff = 4; // skip file magic
+ int retry = 64;
+ int r[];
+ HashMap tags = new HashMap();
+
+ for(; retry > 0; retry--) {
+ r = parse_metadata_block(s, xoff);
+
+ if(r[2] == FLAC_TYPE_COMMENT) {
+ tags = parse_vorbis_comment(s, xoff+r[0], r[1]);
+ break;
+ }
+
+ if(r[3] != 0)
+ break; // eof reached
+
+ // else: calculate next offset
+ xoff += r[0] + r[1];
+ }
+ return tags;
+ }
+
+ /* Parses the metadata block at 'offset' and returns
+ ** [header_size, payload_size, type, stop_after]
+ */
+ private int[] parse_metadata_block(RandomAccessFile s, long offset) throws IOException {
+ int[] result = new int[4];
+ byte[] mb_head = new byte[4];
+ int stop_after = 0;
+ int block_type = 0;
+ int block_size = 0;
+
+ s.seek(offset);
+
+ if( s.read(mb_head) != 4 )
+ xdie("failed to read metadata block header");
+
+ block_size = b2be32(mb_head,0); // read whole header as 32 big endian
+ block_type = (block_size >> 24) & 127; // BIT 1-7 are the type
+ stop_after = (((block_size >> 24) & 128) > 0 ? 1 : 0 ); // BIT 0 indicates the last-block flag
+ block_size = (block_size & 0x00FFFFFF); // byte 1-7 are the size
+
+ // debug("size="+block_size+", type="+block_type+", is_last="+stop_after);
+
+ result[0] = 4; // hardcoded - only returned to be consistent with OGG parser
+ result[1] = block_size;
+ result[2] = block_type;
+ result[3] = stop_after;
+
+ return result;
+ }
+
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/tags/ID3v2File.java b/app/src/main/java/github/daneren2005/dsub/util/tags/ID3v2File.java
new file mode 100644
index 00000000..ea61f36c
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/tags/ID3v2File.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2013 Adrian Ulrich <adrian@blinkenlights.ch>
+ *
+ * This program 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.
+ *
+ * This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package github.daneren2005.dsub.util.tags;
+
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Locale;
+
+
+public class ID3v2File extends Common {
+ private static int ID3_ENC_LATIN = 0x00;
+ private static int ID3_ENC_UTF16LE = 0x01;
+ private static int ID3_ENC_UTF16BE = 0x02;
+ private static int ID3_ENC_UTF8 = 0x03;
+
+ public ID3v2File() {
+ }
+
+ public HashMap getTags(RandomAccessFile s) throws IOException {
+ HashMap tags = new HashMap();
+
+ final int v2hdr_len = 10;
+ byte[] v2hdr = new byte[v2hdr_len];
+
+ // read the whole 10 byte header into memory
+ s.seek(0);
+ s.read(v2hdr);
+
+ int id3v = ((b2be32(v2hdr,0))) & 0xFF; // swapped ID3\04 -> ver. ist the first byte
+ int v3len = ((b2be32(v2hdr,6))); // total size EXCLUDING the this 10 byte header
+ v3len = ((v3len & 0x7f000000) >> 3) | // for some funky reason, this is encoded as 7*4 bits
+ ((v3len & 0x007f0000) >> 2) |
+ ((v3len & 0x00007f00) >> 1) |
+ ((v3len & 0x0000007f) >> 0) ;
+
+ // debug(">> tag version ID3v2."+id3v);
+ // debug(">> LEN= "+v3len+" // "+v3len);
+
+ // we should already be at the first frame
+ // so we can start the parsing right now
+ tags = parse_v3_frames(s, v3len);
+ tags.put("_hdrlen", v3len+v2hdr_len);
+ return tags;
+ }
+
+ /* Parses all ID3v2 frames at the current position up until payload_len
+ ** bytes were read
+ */
+ public HashMap parse_v3_frames(RandomAccessFile s, long payload_len) throws IOException {
+ HashMap tags = new HashMap();
+ byte[] frame = new byte[10]; // a frame header is always 10 bytes
+ long bread = 0; // total amount of read bytes
+
+ while(bread < payload_len) {
+ bread += s.read(frame);
+ String framename = new String(frame, 0, 4);
+ int slen = b2be32(frame, 4);
+
+ /* Abort on silly sizes */
+ if(slen < 1 || slen > 524288)
+ break;
+
+ byte[] xpl = new byte[slen];
+ bread += s.read(xpl);
+
+ if(framename.substring(0,1).equals("T")) {
+ String[] nmzInfo = normalizeTaginfo(framename, xpl);
+
+ for(int i = 0; i < nmzInfo.length; i += 2) {
+ String oggKey = nmzInfo[i];
+ String decPld = nmzInfo[i + 1];
+
+ if (oggKey.length() > 0 && !tags.containsKey(oggKey)) {
+ addTagEntry(tags, oggKey, decPld);
+ }
+ }
+ }
+ else if(framename.equals("RVA2")) {
+ //
+ }
+
+ }
+ return tags;
+ }
+
+ /* Converts ID3v2 sillyframes to OggNames */
+ private String[] normalizeTaginfo(String k, byte[] v) {
+ String[] rv = new String[] {"",""};
+ HashMap lu = new HashMap<String, String>();
+ lu.put("TIT2", "TITLE");
+ lu.put("TALB", "ALBUM");
+ lu.put("TPE1", "ARTIST");
+
+ if(lu.containsKey(k)) {
+ /* A normal, known key: translate into Ogg-Frame name */
+ rv[0] = (String)lu.get(k);
+ rv[1] = getDecodedString(v);
+ }
+ else if(k.equals("TXXX")) {
+ /* A freestyle field, ieks! */
+ String txData[] = getDecodedString(v).split(Character.toString('\0'), 2);
+ /* Check if we got replaygain info in key\0value style */
+ if(txData.length == 2) {
+ if(txData[0].matches("^(?i)REPLAYGAIN_(ALBUM|TRACK)_GAIN$")) {
+ rv[0] = txData[0].toUpperCase(); /* some tagwriters use lowercase for this */
+ rv[1] = txData[1];
+ } else {
+ // Check for replaygain tags just thrown randomly in field
+ int nextStartIndex = 1;
+ int startName = txData[1].toLowerCase(Locale.US).indexOf("replaygain_");
+ ArrayList<String> parts = new ArrayList<String>();
+ while(startName != -1) {
+ int endName = txData[1].indexOf((char) 0, startName);
+ if(endName != -1) {
+ parts.add(txData[1].substring(startName, endName).toUpperCase());
+ int endValue = txData[1].indexOf((char) 0, endName + 1);
+ if(endValue != -1) {
+ parts.add(txData[1].substring(endName + 1, endValue));
+ nextStartIndex = endValue + 1;
+ }
+ }
+
+ startName = txData[1].toLowerCase(Locale.US).indexOf("replaygain_", nextStartIndex);
+ }
+
+ if(parts.size() > 0) {
+ rv = new String[parts.size()];
+ rv = parts.toArray(rv);
+ }
+ }
+ }
+ }
+
+ return rv;
+ }
+
+ /* Converts a raw byte-stream text into a java String */
+ private String getDecodedString(byte[] raw) {
+ int encid = raw[0] & 0xFF;
+ int len = raw.length;
+ String v = "";
+ try {
+ if(encid == ID3_ENC_LATIN) {
+ v = new String(raw, 1, len-1, "ISO-8859-1");
+ }
+ else if (encid == ID3_ENC_UTF8) {
+ v = new String(raw, 1, len-1, "UTF-8");
+ }
+ else if (encid == ID3_ENC_UTF16LE) {
+ v = new String(raw, 3, len-3, "UTF-16LE");
+ }
+ else if (encid == ID3_ENC_UTF16BE) {
+ v = new String(raw, 3, len-3, "UTF-16BE");
+ }
+ } catch(Exception e) {}
+ return v;
+ }
+
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/tags/LameHeader.java b/app/src/main/java/github/daneren2005/dsub/util/tags/LameHeader.java
new file mode 100644
index 00000000..720ee87f
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/tags/LameHeader.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2013 Adrian Ulrich <adrian@blinkenlights.ch>
+ *
+ * This program 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.
+ *
+ * This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package github.daneren2005.dsub.util.tags;
+
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.util.HashMap;
+import java.util.Enumeration;
+
+
+public class LameHeader extends Common {
+
+ public LameHeader() {
+ }
+
+ public HashMap getTags(RandomAccessFile s) throws IOException {
+ return parseLameHeader(s, 0);
+ }
+
+ public HashMap parseLameHeader(RandomAccessFile s, long offset) throws IOException {
+ HashMap tags = new HashMap();
+ byte[] chunk = new byte[4];
+
+ s.seek(offset + 0x24);
+ s.read(chunk);
+
+ String lameMark = new String(chunk, 0, chunk.length, "ISO-8859-1");
+
+ if(lameMark.equals("Info") || lameMark.equals("Xing")) {
+ s.seek(offset+0xAB);
+ s.read(chunk);
+
+ int raw = b2be32(chunk, 0);
+ int gtrk_raw = raw >> 16; /* first 16 bits are the raw track gain value */
+ int galb_raw = raw & 0xFFFF; /* the rest is for the album gain value */
+
+ float gtrk_val = (float)(gtrk_raw & 0x01FF)/10;
+ float galb_val = (float)(galb_raw & 0x01FF)/10;
+
+ gtrk_val = ((gtrk_raw&0x0200)!=0 ? -1*gtrk_val : gtrk_val);
+ galb_val = ((galb_raw&0x0200)!=0 ? -1*galb_val : galb_val);
+
+ if( (gtrk_raw&0xE000) == 0x2000 ) {
+ addTagEntry(tags, "REPLAYGAIN_TRACK_GAIN", gtrk_val+" dB");
+ }
+ if( (gtrk_raw&0xE000) == 0x4000 ) {
+ addTagEntry(tags, "REPLAYGAIN_ALBUM_GAIN", galb_val+" dB");
+ }
+
+ }
+
+ return tags;
+ }
+
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/tags/OggFile.java b/app/src/main/java/github/daneren2005/dsub/util/tags/OggFile.java
new file mode 100644
index 00000000..d0b31671
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/tags/OggFile.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2013 Adrian Ulrich <adrian@blinkenlights.ch>
+ *
+ * This program 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.
+ *
+ * This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package github.daneren2005.dsub.util.tags;
+
+
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.util.HashMap;
+
+
+public class OggFile extends Common {
+
+ private static final int OGG_PAGE_SIZE = 27; // Static size of an OGG Page
+ private static final int OGG_TYPE_COMMENT = 3; // ID of 'VorbisComment's
+
+ public OggFile() {
+ }
+
+ public HashMap getTags(RandomAccessFile s) throws IOException {
+ long offset = 0;
+ int retry = 64;
+ HashMap tags = new HashMap();
+
+ for( ; retry > 0 ; retry-- ) {
+ long res[] = parse_ogg_page(s, offset);
+ if(res[2] == OGG_TYPE_COMMENT) {
+ tags = parse_ogg_vorbis_comment(s, offset+res[0], res[1]);
+ break;
+ }
+ offset += res[0] + res[1];
+ }
+ return tags;
+ }
+
+
+ /* Parses the ogg page at offset 'offset' and returns
+ ** [header_size, payload_size, type]
+ */
+ private long[] parse_ogg_page(RandomAccessFile s, long offset) throws IOException {
+ long[] result = new long[3]; // [header_size, payload_size]
+ byte[] p_header = new byte[OGG_PAGE_SIZE]; // buffer for the page header
+ byte[] scratch;
+ int bread = 0; // number of bytes read
+ int psize = 0; // payload-size
+ int nsegs = 0; // Number of segments
+
+ s.seek(offset);
+ bread = s.read(p_header);
+ if(bread != OGG_PAGE_SIZE)
+ xdie("Unable to read() OGG_PAGE_HEADER");
+ if((new String(p_header, 0, 5)).equals("OggS\0") != true)
+ xdie("Invalid magic - not an ogg file?");
+
+ nsegs = b2u(p_header[26]);
+ // debug("> file seg: "+nsegs);
+ if(nsegs > 0) {
+ scratch = new byte[nsegs];
+ bread = s.read(scratch);
+ if(bread != nsegs)
+ xdie("Failed to read segtable");
+
+ for(int i=0; i<nsegs; i++) {
+ psize += b2u(scratch[i]);
+ }
+ }
+
+ // populate result array
+ result[0] = (s.getFilePointer() - offset);
+ result[1] = psize;
+ result[2] = -1;
+
+ /* next byte is most likely the type -> pre-read */
+ if(psize >= 1 && s.read(p_header, 0, 1) == 1) {
+ result[2] = b2u(p_header[0]);
+ }
+
+ return result;
+ }
+
+ /* In 'vorbiscomment' field is prefixed with \3vorbis in OGG files
+ ** we check that this marker is present and call the generic comment
+ ** parset with the correct offset (+7) */
+ private HashMap parse_ogg_vorbis_comment(RandomAccessFile s, long offset, long pl_len) throws IOException {
+ final int pfx_len = 7;
+ byte[] pfx = new byte[pfx_len];
+
+ if(pl_len < pfx_len)
+ xdie("ogg vorbis comment field is too short!");
+
+ s.seek(offset);
+ s.read(pfx);
+
+ if( (new String(pfx, 0, pfx_len)).equals("\3vorbis") == false )
+ xdie("Damaged packet found!");
+
+ return parse_vorbis_comment(s, offset+pfx_len, pl_len-pfx_len);
+ }
+
+};
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());
+ }
+}