aboutsummaryrefslogtreecommitdiff
path: root/app
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
parent8a332a20ec272d59fe74520825b18017a8f0cac3 (diff)
downloaddsub-cfd014d38cba03ba05f571597b361ab253bff578.tar.gz
dsub-cfd014d38cba03ba05f571597b361ab253bff578.tar.bz2
dsub-cfd014d38cba03ba05f571597b361ab253bff578.zip
Update to gradle
Diffstat (limited to 'app')
-rw-r--r--app/.gitignore1
-rw-r--r--app/app.iml108
-rw-r--r--app/build.gradle32
-rw-r--r--app/libs/CWAC-AdapterWrapper.jarbin0 -> 4841 bytes
-rw-r--r--app/libs/CWAC-EndlessAdapter.jarbin0 -> 5317 bytes
-rw-r--r--app/libs/cling-core-2.0.1.jarbin0 -> 686501 bytes
-rw-r--r--app/libs/cling-support-2.0.1.jarbin0 -> 490043 bytes
-rw-r--r--app/libs/javax.servlet-3.0.0.v201112011016.jarbin0 -> 200387 bytes
-rw-r--r--app/libs/jetty-all-8.1.16.v20140903.jarbin0 -> 1880786 bytes
-rw-r--r--app/libs/kryo-2.21-all.jarbin0 -> 236628 bytes
-rw-r--r--app/libs/seamless-http-1.1.0.jarbin0 -> 21646 bytes
-rw-r--r--app/libs/seamless-util-1.1.0.jarbin0 -> 94456 bytes
-rw-r--r--app/libs/seamless-xml-1.1.0.jarbin0 -> 63142 bytes
-rw-r--r--app/src/androidTest/java/github/daneren2005/dsub/ApplicationTest.java13
-rw-r--r--app/src/androidTest/java/github/daneren2005/dsub/activity/DownloadActivityTest.java32
-rw-r--r--app/src/androidTest/java/github/daneren2005/dsub/activity/SubsonicFragmentActivityTest.java34
-rw-r--r--app/src/androidTest/java/github/daneren2005/dsub/domain/BookmarkTest.java40
-rw-r--r--app/src/androidTest/java/github/daneren2005/dsub/domain/GenreComparatorTest.java68
-rw-r--r--app/src/androidTest/java/github/daneren2005/dsub/service/DownloadServiceTest.java296
-rw-r--r--app/src/main/AndroidManifest.xml250
-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
-rw-r--r--app/src/main/res/anim/enter_from_left.xml12
-rw-r--r--app/src/main/res/anim/enter_from_right.xml12
-rw-r--r--app/src/main/res/anim/exit_to_left.xml12
-rw-r--r--app/src/main/res/anim/exit_to_right.xml12
-rw-r--r--app/src/main/res/anim/fade_in.xml5
-rw-r--r--app/src/main/res/anim/fade_out.xml5
-rw-r--r--app/src/main/res/anim/push_down_in.xml22
-rw-r--r--app/src/main/res/anim/push_down_out.xml22
-rw-r--r--app/src/main/res/anim/push_up_in.xml22
-rw-r--r--app/src/main/res/anim/push_up_out.xml22
-rw-r--r--app/src/main/res/drawable-hdpi-v11/notification_close.pngbin0 -> 384 bytes
-rw-r--r--app/src/main/res/drawable-hdpi-v11/notification_next.pngbin0 -> 525 bytes
-rw-r--r--app/src/main/res/drawable-hdpi-v11/notification_pause.pngbin0 -> 210 bytes
-rw-r--r--app/src/main/res/drawable-hdpi-v11/notification_play.pngbin0 -> 385 bytes
-rw-r--r--app/src/main/res/drawable-hdpi-v11/notification_previous.pngbin0 -> 541 bytes
-rw-r--r--app/src/main/res/drawable-hdpi-v11/stat_notify_download.pngbin0 -> 300 bytes
-rw-r--r--app/src/main/res/drawable-hdpi-v11/stat_notify_playing.pngbin0 -> 385 bytes
-rw-r--r--app/src/main/res/drawable-hdpi-v11/stat_notify_sync.pngbin0 -> 819 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/action_toggle_list_dark.pngbin0 -> 290 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/action_toggle_list_light.pngbin0 -> 309 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/actionbar_button_normal.9.pngbin0 -> 208 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/appwidget_art_default.pngbin0 -> 3711 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/appwidget_art_unknown.pngbin0 -> 3711 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/appwidget_bg.9.pngbin0 -> 489 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/background.pngbin0 -> 1701 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/download_cached.pngbin0 -> 982 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/download_none_dark.pngbin0 -> 342 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/download_none_light.pngbin0 -> 374 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/download_pinned.pngbin0 -> 992 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/downloading_dark.pngbin0 -> 618 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/downloading_light.pngbin0 -> 743 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_action_add_dark.pngbin0 -> 289 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_action_add_light.pngbin0 -> 308 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_action_album.pngbin0 -> 716 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_action_artist.pngbin0 -> 685 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_action_rating_bad_dark.pngbin0 -> 754 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_action_rating_bad_light.pngbin0 -> 892 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_action_rating_bad_selected.pngbin0 -> 965 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_action_rating_good_dark.pngbin0 -> 744 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_action_rating_good_light.pngbin0 -> 873 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_action_rating_good_selected.pngbin0 -> 921 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_action_song.pngbin0 -> 568 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_action_volume_dark.pngbin0 -> 1365 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_action_volume_light.pngbin0 -> 1550 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_appwidget_music_next.pngbin0 -> 489 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_appwidget_music_pause.pngbin0 -> 232 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_appwidget_music_play.pngbin0 -> 344 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_appwidget_music_previous.pngbin0 -> 666 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_menu_add_person_dark.pngbin0 -> 990 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_menu_add_person_light.pngbin0 -> 1191 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_menu_admin_dark.pngbin0 -> 1263 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_menu_admin_light.pngbin0 -> 1524 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_menu_bookmark_dark.pngbin0 -> 1087 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_menu_bookmark_light.pngbin0 -> 1292 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_menu_bookmark_selected.pngbin0 -> 1374 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_menu_chat_dark.pngbin0 -> 421 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_menu_chat_light.pngbin0 -> 453 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_menu_chat_send_dark.pngbin0 -> 602 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_menu_chat_send_light.pngbin0 -> 677 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_menu_download_dark.pngbin0 -> 540 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_menu_download_light.pngbin0 -> 615 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_menu_library_dark.pngbin0 -> 617 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_menu_library_light.pngbin0 -> 696 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_menu_password_dark.pngbin0 -> 843 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_menu_password_light.pngbin0 -> 958 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_menu_playlist_dark.pngbin0 -> 457 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_menu_playlist_light.pngbin0 -> 496 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_menu_podcast_dark.pngbin0 -> 1167 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_menu_podcast_light.pngbin0 -> 1410 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_menu_radio_dark.pngbin0 -> 768 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_menu_radio_light.pngbin0 -> 878 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_menu_refresh_dark.pngbin0 -> 1139 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_menu_refresh_light.pngbin0 -> 1351 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_menu_remove_dark.pngbin0 -> 898 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_menu_remove_light.pngbin0 -> 1090 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_menu_save_dark.pngbin0 -> 553 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_menu_save_light.pngbin0 -> 631 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_menu_search_dark.pngbin0 -> 1071 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_menu_search_light.pngbin0 -> 1271 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_menu_settings_dark.pngbin0 -> 557 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_menu_settings_light.pngbin0 -> 586 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_menu_share_dark.pngbin0 -> 737 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_menu_share_light.pngbin0 -> 825 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_menu_shuffle_dark.pngbin0 -> 985 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_menu_shuffle_light.pngbin0 -> 1132 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_number_border.pngbin0 -> 2058 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_social_person.pngbin0 -> 4518 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_stat_star.pngbin0 -> 826 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/launch.pngbin0 -> 7496 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/main_offline_dark.pngbin0 -> 631 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/main_offline_light.pngbin0 -> 746 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/main_select_server_dark.pngbin0 -> 720 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/main_select_server_light.pngbin0 -> 799 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/media_backward_dark.pngbin0 -> 579 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/media_backward_light.pngbin0 -> 627 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/media_forward_dark.pngbin0 -> 559 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/media_forward_light.pngbin0 -> 631 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/media_pause_dark.pngbin0 -> 276 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/media_pause_light.pngbin0 -> 301 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/media_repeat_all.pngbin0 -> 5090 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/media_repeat_off.pngbin0 -> 1079 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/media_repeat_off_light.pngbin0 -> 1512 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/media_repeat_single.pngbin0 -> 5564 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/media_start_dark.pngbin0 -> 449 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/media_start_light.pngbin0 -> 511 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/media_stop_dark.pngbin0 -> 265 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/media_stop_light.pngbin0 -> 274 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/notification_close.pngbin0 -> 501 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/notification_next.pngbin0 -> 651 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/notification_pause.pngbin0 -> 459 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/notification_play.pngbin0 -> 599 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/notification_previous.pngbin0 -> 633 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/now_playing.pngbin0 -> 599 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/stat_notify_download.pngbin0 -> 350 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/stat_notify_playing.pngbin0 -> 599 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/stat_notify_sync.pngbin0 -> 894 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/toast_frame.9.pngbin0 -> 2461 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/unknown_album.pngbin0 -> 7127 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/unknown_album_large.pngbin0 -> 41908 bytes
-rw-r--r--app/src/main/res/drawable-large/unknown_album.pngbin0 -> 14939 bytes
-rw-r--r--app/src/main/res/drawable-mdpi-v11/notification_close.pngbin0 -> 241 bytes
-rw-r--r--app/src/main/res/drawable-mdpi-v11/notification_next.pngbin0 -> 341 bytes
-rw-r--r--app/src/main/res/drawable-mdpi-v11/notification_pause.pngbin0 -> 156 bytes
-rw-r--r--app/src/main/res/drawable-mdpi-v11/notification_play.pngbin0 -> 280 bytes
-rw-r--r--app/src/main/res/drawable-mdpi-v11/notification_previous.pngbin0 -> 355 bytes
-rw-r--r--app/src/main/res/drawable-mdpi-v11/stat_notify_download.pngbin0 -> 234 bytes
-rw-r--r--app/src/main/res/drawable-mdpi-v11/stat_notify_playing.pngbin0 -> 280 bytes
-rw-r--r--app/src/main/res/drawable-mdpi-v11/stat_notify_sync.pngbin0 -> 623 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/action_toggle_list_dark.pngbin0 -> 204 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/action_toggle_list_light.pngbin0 -> 225 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/download_cached.pngbin0 -> 704 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/download_none_dark.pngbin0 -> 216 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/download_none_light.pngbin0 -> 239 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/download_pinned.pngbin0 -> 673 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/downloading_dark.pngbin0 -> 447 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/downloading_light.pngbin0 -> 527 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_action_add_dark.pngbin0 -> 171 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_action_add_light.pngbin0 -> 183 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_action_album.pngbin0 -> 474 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_action_artist.pngbin0 -> 505 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_action_rating_bad_dark.pngbin0 -> 460 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_action_rating_bad_light.pngbin0 -> 543 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_action_rating_bad_selected.pngbin0 -> 584 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_action_rating_good_dark.pngbin0 -> 456 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_action_rating_good_light.pngbin0 -> 541 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_action_rating_good_selected.pngbin0 -> 581 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_action_song.pngbin0 -> 431 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_action_volume_dark.pngbin0 -> 820 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_action_volume_light.pngbin0 -> 974 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_menu_add_person_dark.pngbin0 -> 652 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_menu_add_person_light.pngbin0 -> 811 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_menu_admin_dark.pngbin0 -> 781 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_menu_admin_light.pngbin0 -> 966 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_menu_bookmark_dark.pngbin0 -> 658 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_menu_bookmark_light.pngbin0 -> 782 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_menu_bookmark_selected.pngbin0 -> 849 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_menu_chat_dark.pngbin0 -> 277 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_menu_chat_light.pngbin0 -> 311 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_menu_chat_send_dark.pngbin0 -> 366 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_menu_chat_send_light.pngbin0 -> 394 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_menu_download_dark.pngbin0 -> 379 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_menu_download_light.pngbin0 -> 444 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_menu_library_dark.pngbin0 -> 420 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_menu_library_light.pngbin0 -> 492 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_menu_password_dark.pngbin0 -> 554 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_menu_password_light.pngbin0 -> 676 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_menu_playlist_dark.pngbin0 -> 315 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_menu_playlist_light.pngbin0 -> 364 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_menu_podcast_dark.pngbin0 -> 750 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_menu_podcast_light.pngbin0 -> 862 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_menu_radio_dark.pngbin0 -> 578 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_menu_radio_light.pngbin0 -> 675 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_menu_refresh_dark.pngbin0 -> 748 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_menu_refresh_light.pngbin0 -> 914 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_menu_remove_dark.pngbin0 -> 576 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_menu_remove_light.pngbin0 -> 689 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_menu_save_dark.pngbin0 -> 406 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_menu_save_light.pngbin0 -> 481 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_menu_search_dark.pngbin0 -> 655 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_menu_search_light.pngbin0 -> 794 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_menu_settings_dark.pngbin0 -> 365 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_menu_settings_light.pngbin0 -> 365 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_menu_share_dark.pngbin0 -> 455 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_menu_share_light.pngbin0 -> 534 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_menu_shuffle_dark.pngbin0 -> 653 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_menu_shuffle_light.pngbin0 -> 725 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_number_border.pngbin0 -> 1206 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_social_person.pngbin0 -> 2834 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/launch.pngbin0 -> 4077 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/main_offline_dark.pngbin0 -> 408 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/main_offline_light.pngbin0 -> 456 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/main_select_server_dark.pngbin0 -> 434 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/main_select_server_light.pngbin0 -> 502 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/media_backward_dark.pngbin0 -> 378 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/media_backward_light.pngbin0 -> 412 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/media_forward_dark.pngbin0 -> 372 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/media_forward_light.pngbin0 -> 417 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/media_pause_dark.pngbin0 -> 169 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/media_pause_light.pngbin0 -> 192 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/media_start_dark.pngbin0 -> 301 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/media_start_light.pngbin0 -> 335 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/media_stop_dark.pngbin0 -> 154 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/media_stop_light.pngbin0 -> 162 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/notification_close.pngbin0 -> 337 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/notification_next.pngbin0 -> 460 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/notification_pause.pngbin0 -> 361 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/notification_play.pngbin0 -> 417 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/notification_previous.pngbin0 -> 476 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/now_playing.pngbin0 -> 417 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/stat_notify_download.pngbin0 -> 272 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/stat_notify_playing.pngbin0 -> 417 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/stat_notify_sync.pngbin0 -> 575 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi-v11/notification_close.pngbin0 -> 491 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi-v11/notification_next.pngbin0 -> 731 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi-v11/notification_pause.pngbin0 -> 257 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi-v11/notification_play.pngbin0 -> 493 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi-v11/notification_previous.pngbin0 -> 750 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi-v11/stat_notify_download.pngbin0 -> 379 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi-v11/stat_notify_playing.pngbin0 -> 493 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi-v11/stat_notify_sync.pngbin0 -> 1205 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/action_toggle_list_dark.pngbin0 -> 312 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/action_toggle_list_light.pngbin0 -> 320 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/download_cached.pngbin0 -> 1300 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/download_none_dark.pngbin0 -> 355 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/download_none_light.pngbin0 -> 375 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/download_pinned.pngbin0 -> 1278 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/downloading_dark.pngbin0 -> 869 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/downloading_light.pngbin0 -> 1017 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_action_add_dark.pngbin0 -> 336 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_action_add_light.pngbin0 -> 349 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_action_album.pngbin0 -> 1023 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_action_artist.pngbin0 -> 820 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_action_rating_bad_dark.pngbin0 -> 961 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_action_rating_bad_light.pngbin0 -> 1141 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_action_rating_bad_selected.pngbin0 -> 1197 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_action_rating_good_dark.pngbin0 -> 946 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_action_rating_good_light.pngbin0 -> 1129 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_action_rating_good_selected.pngbin0 -> 1176 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_action_song.pngbin0 -> 705 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_action_volume_dark.pngbin0 -> 1916 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_action_volume_light.pngbin0 -> 2180 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_menu_add_person_dark.pngbin0 -> 1284 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_menu_add_person_light.pngbin0 -> 1534 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_menu_admin_dark.pngbin0 -> 1807 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_menu_admin_light.pngbin0 -> 2119 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_menu_bookmark_dark.pngbin0 -> 1442 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_menu_bookmark_light.pngbin0 -> 1665 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_menu_bookmark_selected.pngbin0 -> 1780 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_menu_chat_dark.pngbin0 -> 472 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_menu_chat_light.pngbin0 -> 517 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_menu_chat_send_dark.pngbin0 -> 743 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_menu_chat_send_light.pngbin0 -> 799 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_menu_download_dark.pngbin0 -> 695 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_menu_download_light.pngbin0 -> 797 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_menu_library_dark.pngbin0 -> 820 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_menu_library_light.pngbin0 -> 980 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_menu_password_dark.pngbin0 -> 1067 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_menu_password_light.pngbin0 -> 1234 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_menu_playlist_dark.pngbin0 -> 508 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_menu_playlist_light.pngbin0 -> 555 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_menu_podcast_dark.pngbin0 -> 1553 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_menu_podcast_light.pngbin0 -> 1787 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_menu_radio_dark.pngbin0 -> 1131 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_menu_radio_light.pngbin0 -> 1376 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_menu_refresh_dark.pngbin0 -> 1520 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_menu_refresh_light.pngbin0 -> 1802 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_menu_remove_dark.pngbin0 -> 1146 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_menu_remove_light.pngbin0 -> 1394 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_menu_save_dark.pngbin0 -> 644 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_menu_save_light.pngbin0 -> 735 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_menu_search_dark.pngbin0 -> 1445 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_menu_search_light.pngbin0 -> 1701 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_menu_settings_dark.pngbin0 -> 708 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_menu_settings_light.pngbin0 -> 748 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_menu_share_dark.pngbin0 -> 947 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_menu_share_light.pngbin0 -> 1101 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_menu_shuffle_dark.pngbin0 -> 1400 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_menu_shuffle_light.pngbin0 -> 1637 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_number_border.pngbin0 -> 2798 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_social_person.pngbin0 -> 5960 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/launch.pngbin0 -> 10916 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/main_offline_dark.pngbin0 -> 818 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/main_offline_light.pngbin0 -> 976 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/main_select_server_dark.pngbin0 -> 939 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/main_select_server_light.pngbin0 -> 1079 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/media_backward_dark.pngbin0 -> 778 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/media_backward_light.pngbin0 -> 860 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/media_forward_dark.pngbin0 -> 716 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/media_forward_light.pngbin0 -> 834 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/media_pause_dark.pngbin0 -> 314 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/media_pause_light.pngbin0 -> 333 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/media_start_dark.pngbin0 -> 580 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/media_start_light.pngbin0 -> 649 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/media_stop_dark.pngbin0 -> 298 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/media_stop_light.pngbin0 -> 307 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/notification_close.pngbin0 -> 538 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/notification_next.pngbin0 -> 886 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/notification_pause.pngbin0 -> 529 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/notification_play.pngbin0 -> 753 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/notification_previous.pngbin0 -> 891 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/now_playing.pngbin0 -> 753 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/stat_notify_download.pngbin0 -> 404 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/stat_notify_playing.pngbin0 -> 753 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/stat_notify_sync.pngbin0 -> 1058 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi-v11/notification_close.pngbin0 -> 712 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi-v11/notification_next.pngbin0 -> 1105 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi-v11/notification_pause.pngbin0 -> 358 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi-v11/notification_play.pngbin0 -> 781 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi-v11/notification_previous.pngbin0 -> 1143 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi-v11/stat_notify_download.pngbin0 -> 531 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi-v11/stat_notify_playing.pngbin0 -> 781 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi-v11/stat_notify_sync.pngbin0 -> 2198 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/action_toggle_list_dark.pngbin0 -> 608 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/action_toggle_list_light.pngbin0 -> 630 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/download_cached.pngbin0 -> 1906 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/download_none_dark.pngbin0 -> 617 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/download_none_light.pngbin0 -> 639 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/download_pinned.pngbin0 -> 1899 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/downloading_dark.pngbin0 -> 1353 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/downloading_light.pngbin0 -> 1542 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_action_add_dark.pngbin0 -> 645 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_action_add_light.pngbin0 -> 636 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_action_rating_bad_dark.pngbin0 -> 1540 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_action_rating_bad_light.pngbin0 -> 1822 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_action_rating_bad_selected.pngbin0 -> 1953 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_action_rating_good_dark.pngbin0 -> 1582 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_action_rating_good_light.pngbin0 -> 1835 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_action_rating_good_selected.pngbin0 -> 1915 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_action_volume_dark.pngbin0 -> 3148 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_action_volume_light.pngbin0 -> 3473 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_menu_add_person_dark.pngbin0 -> 2036 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_menu_add_person_light.pngbin0 -> 2350 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_menu_admin_dark.pngbin0 -> 2992 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_menu_admin_light.pngbin0 -> 3467 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_menu_bookmark_dark.pngbin0 -> 2194 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_menu_bookmark_light.pngbin0 -> 2474 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_menu_bookmark_selected.pngbin0 -> 2635 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_menu_chat_dark.pngbin0 -> 723 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_menu_chat_light.pngbin0 -> 771 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_menu_chat_send_dark.pngbin0 -> 1326 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_menu_chat_send_light.pngbin0 -> 1608 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_menu_download_dark.pngbin0 -> 1072 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_menu_download_light.pngbin0 -> 1230 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_menu_library_dark.pngbin0 -> 1357 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_menu_library_light.pngbin0 -> 1579 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_menu_password_dark.pngbin0 -> 1610 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_menu_password_light.pngbin0 -> 1852 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_menu_playlist_dark.pngbin0 -> 783 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_menu_playlist_light.pngbin0 -> 840 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_menu_podcast_dark.pngbin0 -> 2439 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_menu_podcast_light.pngbin0 -> 2798 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_menu_radio_dark.pngbin0 -> 1992 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_menu_radio_light.pngbin0 -> 2310 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_menu_refresh_dark.pngbin0 -> 2453 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_menu_refresh_light.pngbin0 -> 2952 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_menu_remove_dark.pngbin0 -> 1843 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_menu_remove_light.pngbin0 -> 2164 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_menu_save_dark.pngbin0 -> 977 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_menu_save_light.pngbin0 -> 1076 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_menu_search_dark.pngbin0 -> 2258 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_menu_search_light.pngbin0 -> 2571 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_menu_settings_dark.pngbin0 -> 1221 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_menu_settings_light.pngbin0 -> 1194 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_menu_share_dark.pngbin0 -> 1592 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_menu_share_light.pngbin0 -> 1790 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_menu_shuffle_dark.pngbin0 -> 2268 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_menu_shuffle_light.pngbin0 -> 2529 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_number_border.pngbin0 -> 5066 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_social_person.pngbin0 -> 9169 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/launch.pngbin0 -> 20218 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/main_offline_dark.pngbin0 -> 1265 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/main_offline_light.pngbin0 -> 1466 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/main_select_server_dark.pngbin0 -> 1396 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/main_select_server_light.pngbin0 -> 1622 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/media_backward_dark.pngbin0 -> 1282 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/media_backward_light.pngbin0 -> 1443 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/media_forward_dark.pngbin0 -> 1258 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/media_forward_light.pngbin0 -> 1388 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/media_pause_dark.pngbin0 -> 612 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/media_pause_light.pngbin0 -> 631 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/media_start_dark.pngbin0 -> 996 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/media_start_light.pngbin0 -> 1069 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/media_stop_dark.pngbin0 -> 545 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/media_stop_light.pngbin0 -> 554 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/notification_close.pngbin0 -> 1081 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/notification_next.pngbin0 -> 1292 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/notification_pause.pngbin0 -> 724 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/notification_play.pngbin0 -> 1125 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/notification_previous.pngbin0 -> 1261 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/now_playing.pngbin0 -> 1125 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/stat_notify_download.pngbin0 -> 558 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/stat_notify_playing.pngbin0 -> 1125 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/stat_notify_sync.pngbin0 -> 1932 bytes
-rw-r--r--app/src/main/res/drawable/appwidget4x1_preview.pngbin0 -> 3365 bytes
-rw-r--r--app/src/main/res/drawable/appwidget4x2_preview.pngbin0 -> 5856 bytes
-rw-r--r--app/src/main/res/drawable/appwidget4x3_preview.pngbin0 -> 6216 bytes
-rw-r--r--app/src/main/res/drawable/appwidget4x4_preview.pngbin0 -> 9186 bytes
-rw-r--r--app/src/main/res/layout-land/download.xml129
-rw-r--r--app/src/main/res/layout-large-land/abstract_fragment_container.xml21
-rw-r--r--app/src/main/res/layout-large-land/download.xml130
-rw-r--r--app/src/main/res/layout-port/download.xml120
-rw-r--r--app/src/main/res/layout/abstract_activity.xml21
-rw-r--r--app/src/main/res/layout/abstract_fragment_activity.xml84
-rw-r--r--app/src/main/res/layout/abstract_fragment_container.xml6
-rw-r--r--app/src/main/res/layout/abstract_list_fragment.xml27
-rw-r--r--app/src/main/res/layout/actionbar_spinner.xml13
-rw-r--r--app/src/main/res/layout/album_cell_item.xml89
-rw-r--r--app/src/main/res/layout/album_list_item.xml74
-rw-r--r--app/src/main/res/layout/appwidget4x1.xml107
-rw-r--r--app/src/main/res/layout/appwidget4x2.xml130
-rw-r--r--app/src/main/res/layout/appwidget4x3.xml113
-rw-r--r--app/src/main/res/layout/appwidget4x4.xml115
-rw-r--r--app/src/main/res/layout/basic_count_item.xml35
-rw-r--r--app/src/main/res/layout/basic_list_item.xml37
-rw-r--r--app/src/main/res/layout/change_email.xml28
-rw-r--r--app/src/main/res/layout/change_password.xml28
-rw-r--r--app/src/main/res/layout/chat.xml53
-rw-r--r--app/src/main/res/layout/chat_item.xml61
-rw-r--r--app/src/main/res/layout/chat_item_reverse.xml61
-rw-r--r--app/src/main/res/layout/complex_list_item.xml49
-rw-r--r--app/src/main/res/layout/confirm_password.xml28
-rw-r--r--app/src/main/res/layout/create_bookmark.xml26
-rw-r--r--app/src/main/res/layout/create_podcast.xml27
-rw-r--r--app/src/main/res/layout/create_user.xml77
-rw-r--r--app/src/main/res/layout/download_activity.xml4
-rw-r--r--app/src/main/res/layout/download_media_buttons.xml63
-rw-r--r--app/src/main/res/layout/download_playlist.xml31
-rw-r--r--app/src/main/res/layout/download_slider.xml43
-rw-r--r--app/src/main/res/layout/drawer_list_item.xml26
-rw-r--r--app/src/main/res/layout/edit_play_action.xml119
-rw-r--r--app/src/main/res/layout/equalizer.xml51
-rw-r--r--app/src/main/res/layout/equalizer_bar.xml36
-rw-r--r--app/src/main/res/layout/genre_list_item.xml42
-rw-r--r--app/src/main/res/layout/grid_view.xml14
-rw-r--r--app/src/main/res/layout/home.xml23
-rw-r--r--app/src/main/res/layout/jukebox_volume.xml46
-rw-r--r--app/src/main/res/layout/lyrics.xml55
-rw-r--r--app/src/main/res/layout/main_buttons.xml157
-rw-r--r--app/src/main/res/layout/notification.xml83
-rw-r--r--app/src/main/res/layout/notification_expanded.xml123
-rw-r--r--app/src/main/res/layout/preferences.xml10
-rw-r--r--app/src/main/res/layout/progress.xml20
-rw-r--r--app/src/main/res/layout/rating.xml15
-rw-r--r--app/src/main/res/layout/save_playlist.xml26
-rw-r--r--app/src/main/res/layout/search_buttons.xml73
-rw-r--r--app/src/main/res/layout/seekbar_preference.xml18
-rw-r--r--app/src/main/res/layout/select_album.xml28
-rw-r--r--app/src/main/res/layout/select_album_header.xml153
-rw-r--r--app/src/main/res/layout/select_artist_header.xml43
-rw-r--r--app/src/main/res/layout/shuffle_dialog.xml80
-rw-r--r--app/src/main/res/layout/song_list_item.xml126
-rw-r--r--app/src/main/res/layout/start_timer.xml21
-rw-r--r--app/src/main/res/layout/static_drawer_activity.xml23
-rw-r--r--app/src/main/res/layout/sync_dialog.xml12
-rw-r--r--app/src/main/res/layout/tab_progress.xml31
-rw-r--r--app/src/main/res/layout/unscrollable_grid_view.xml11
-rw-r--r--app/src/main/res/layout/update_playlist.xml70
-rw-r--r--app/src/main/res/layout/update_share.xml69
-rw-r--r--app/src/main/res/layout/user_header.xml57
-rw-r--r--app/src/main/res/layout/user_list_item.xml44
-rw-r--r--app/src/main/res/menu/abstract_top_menu.xml21
-rw-r--r--app/src/main/res/menu/admin.xml12
-rw-r--r--app/src/main/res/menu/admin_context.xml16
-rw-r--r--app/src/main/res/menu/admin_context_user.xml8
-rw-r--r--app/src/main/res/menu/downloading.xml13
-rw-r--r--app/src/main/res/menu/drawer_menu.xml14
-rw-r--r--app/src/main/res/menu/empty.xml16
-rw-r--r--app/src/main/res/menu/main.xml41
-rw-r--r--app/src/main/res/menu/nowplaying.xml53
-rw-r--r--app/src/main/res/menu/nowplaying_context.xml52
-rw-r--r--app/src/main/res/menu/nowplaying_context_offline.xml30
-rw-r--r--app/src/main/res/menu/nowplaying_offline.xml46
-rw-r--r--app/src/main/res/menu/search.xml14
-rw-r--r--app/src/main/res/menu/select_album.xml53
-rw-r--r--app/src/main/res/menu/select_album_context.xml67
-rw-r--r--app/src/main/res/menu/select_album_context_offline.xml33
-rw-r--r--app/src/main/res/menu/select_album_list.xml27
-rw-r--r--app/src/main/res/menu/select_artist.xml32
-rw-r--r--app/src/main/res/menu/select_artist_context.xml48
-rw-r--r--app/src/main/res/menu/select_artist_context_offline.xml30
-rw-r--r--app/src/main/res/menu/select_bookmark_context.xml32
-rw-r--r--app/src/main/res/menu/select_playlist_context.xml43
-rw-r--r--app/src/main/res/menu/select_playlist_context_offline.xml13
-rw-r--r--app/src/main/res/menu/select_podcast_episode.xml20
-rw-r--r--app/src/main/res/menu/select_podcast_episode_context.xml49
-rw-r--r--app/src/main/res/menu/select_podcast_episode_context_offline.xml30
-rw-r--r--app/src/main/res/menu/select_podcast_episode_offline.xml7
-rw-r--r--app/src/main/res/menu/select_podcasts.xml31
-rw-r--r--app/src/main/res/menu/select_podcasts_context.xml18
-rw-r--r--app/src/main/res/menu/select_podcasts_context_offline.xml7
-rw-r--r--app/src/main/res/menu/select_share_context.xml21
-rw-r--r--app/src/main/res/menu/select_song.xml55
-rw-r--r--app/src/main/res/menu/select_song_context.xml74
-rw-r--r--app/src/main/res/menu/select_song_context_offline.xml34
-rw-r--r--app/src/main/res/menu/select_song_offline.xml31
-rw-r--r--app/src/main/res/menu/select_video_context.xml24
-rw-r--r--app/src/main/res/menu/select_video_context_offline.xml15
-rw-r--r--app/src/main/res/menu/similar_artists.xml20
-rw-r--r--app/src/main/res/menu/tasker_configuration.xml16
-rw-r--r--app/src/main/res/menu/unstar.xml7
-rw-r--r--app/src/main/res/menu/user.xml32
-rw-r--r--app/src/main/res/menu/user_user.xml14
-rw-r--r--app/src/main/res/values-de/strings.xml557
-rw-r--r--app/src/main/res/values-es/strings.xml580
-rw-r--r--app/src/main/res/values-fr/strings.xml570
-rw-r--r--app/src/main/res/values-hu/strings.xml602
-rw-r--r--app/src/main/res/values-land/integers.xml4
-rw-r--r--app/src/main/res/values-large/dimens.xml7
-rw-r--r--app/src/main/res/values-large/integers.xml5
-rw-r--r--app/src/main/res/values-ru/strings.xml312
-rw-r--r--app/src/main/res/values-v11/colors.xml5
-rw-r--r--app/src/main/res/values-v11/styles.xml6
-rw-r--r--app/src/main/res/values-v16/themes.xml15
-rw-r--r--app/src/main/res/values/arrays.xml264
-rw-r--r--app/src/main/res/values/attrs.xml37
-rw-r--r--app/src/main/res/values/colors.xml17
-rw-r--r--app/src/main/res/values/dimens.xml7
-rw-r--r--app/src/main/res/values/ids.xml4
-rw-r--r--app/src/main/res/values/integers.xml5
-rw-r--r--app/src/main/res/values/strings.xml607
-rw-r--r--app/src/main/res/values/styles.xml95
-rw-r--r--app/src/main/res/values/themes.xml109
-rw-r--r--app/src/main/res/xml/appwidget4x1.xml8
-rw-r--r--app/src/main/res/xml/appwidget4x2.xml8
-rw-r--r--app/src/main/res/xml/appwidget4x3.xml8
-rw-r--r--app/src/main/res/xml/appwidget4x4.xml10
-rw-r--r--app/src/main/res/xml/authenticator.xml7
-rw-r--r--app/src/main/res/xml/changelog.xml214
-rw-r--r--app/src/main/res/xml/mostrecent_syncadapter.xml8
-rw-r--r--app/src/main/res/xml/playlists_syncadapter.xml8
-rw-r--r--app/src/main/res/xml/podcasts_syncadapter.xml8
-rw-r--r--app/src/main/res/xml/searchable.xml10
-rw-r--r--app/src/main/res/xml/settings.xml450
-rw-r--r--app/src/main/res/xml/starred_syncadapter.xml8
762 files changed, 52889 insertions, 0 deletions
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 00000000..796b96d1
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/app/app.iml b/app/app.iml
new file mode 100644
index 00000000..e50d2481
--- /dev/null
+++ b/app/app.iml
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" external.system.module.group="DSub" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
+ <component name="FacetManager">
+ <facet type="android-gradle" name="Android-Gradle">
+ <configuration>
+ <option name="GRADLE_PROJECT_PATH" value=":app" />
+ </configuration>
+ </facet>
+ <facet type="android" name="Android">
+ <configuration>
+ <option name="SELECTED_BUILD_VARIANT" value="debug" />
+ <option name="SELECTED_TEST_ARTIFACT" value="_android_test_" />
+ <option name="ASSEMBLE_TASK_NAME" value="assembleDebug" />
+ <option name="COMPILE_JAVA_TASK_NAME" value="compileDebugSources" />
+ <option name="ASSEMBLE_TEST_TASK_NAME" value="assembleDebugAndroidTest" />
+ <option name="SOURCE_GEN_TASK_NAME" value="generateDebugSources" />
+ <option name="TEST_SOURCE_GEN_TASK_NAME" value="generateDebugAndroidTestSources" />
+ <option name="ALLOW_USER_CONFIGURATION" value="false" />
+ <option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
+ <option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
+ <option name="RES_FOLDERS_RELATIVE_PATH" value="file://$MODULE_DIR$/src/main/res" />
+ <option name="ASSETS_FOLDER_RELATIVE_PATH" value="/src/main/assets" />
+ </configuration>
+ </facet>
+ </component>
+ <component name="NewModuleRootManager" inherit-compiler-output="false">
+ <output url="file://$MODULE_DIR$/build/intermediates/classes/debug" />
+ <output-test url="file://$MODULE_DIR$/build/intermediates/classes/androidTest/debug" />
+ <exclude-output />
+ <content url="file://$MODULE_DIR$">
+ <sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/debug" isTestSource="false" generated="true" />
+ <sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/debug" isTestSource="false" generated="true" />
+ <sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/debug" isTestSource="false" generated="true" />
+ <sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/debug" isTestSource="false" generated="true" />
+ <sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/debug" type="java-resource" />
+ <sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/debug" type="java-resource" />
+ <sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/androidTest/debug" isTestSource="true" generated="true" />
+ <sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/androidTest/debug" isTestSource="true" generated="true" />
+ <sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/androidTest/debug" isTestSource="true" generated="true" />
+ <sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/androidTest/debug" isTestSource="true" generated="true" />
+ <sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/androidTest/debug" type="java-test-resource" />
+ <sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/androidTest/debug" type="java-test-resource" />
+ <sourceFolder url="file://$MODULE_DIR$/src/debug/res" type="java-resource" />
+ <sourceFolder url="file://$MODULE_DIR$/src/debug/resources" type="java-resource" />
+ <sourceFolder url="file://$MODULE_DIR$/src/debug/assets" type="java-resource" />
+ <sourceFolder url="file://$MODULE_DIR$/src/debug/aidl" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/src/debug/java" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/src/debug/jni" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/src/debug/rs" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/src/main/res" type="java-resource" />
+ <sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
+ <sourceFolder url="file://$MODULE_DIR$/src/main/assets" type="java-resource" />
+ <sourceFolder url="file://$MODULE_DIR$/src/main/aidl" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/src/main/jni" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/src/main/rs" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/src/androidTest/res" type="java-test-resource" />
+ <sourceFolder url="file://$MODULE_DIR$/src/androidTest/resources" type="java-test-resource" />
+ <sourceFolder url="file://$MODULE_DIR$/src/androidTest/assets" type="java-test-resource" />
+ <sourceFolder url="file://$MODULE_DIR$/src/androidTest/aidl" isTestSource="true" />
+ <sourceFolder url="file://$MODULE_DIR$/src/androidTest/java" isTestSource="true" />
+ <sourceFolder url="file://$MODULE_DIR$/src/androidTest/jni" isTestSource="true" />
+ <sourceFolder url="file://$MODULE_DIR$/src/androidTest/rs" isTestSource="true" />
+ <excludeFolder url="file://$MODULE_DIR$/build/intermediates/assets" />
+ <excludeFolder url="file://$MODULE_DIR$/build/intermediates/bundles" />
+ <excludeFolder url="file://$MODULE_DIR$/build/intermediates/classes" />
+ <excludeFolder url="file://$MODULE_DIR$/build/intermediates/coverage-instrumented-classes" />
+ <excludeFolder url="file://$MODULE_DIR$/build/intermediates/dependency-cache" />
+ <excludeFolder url="file://$MODULE_DIR$/build/intermediates/dex" />
+ <excludeFolder url="file://$MODULE_DIR$/build/intermediates/dex-cache" />
+ <excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental" />
+ <excludeFolder url="file://$MODULE_DIR$/build/intermediates/jacoco" />
+ <excludeFolder url="file://$MODULE_DIR$/build/intermediates/javaResources" />
+ <excludeFolder url="file://$MODULE_DIR$/build/intermediates/libs" />
+ <excludeFolder url="file://$MODULE_DIR$/build/intermediates/lint" />
+ <excludeFolder url="file://$MODULE_DIR$/build/intermediates/manifests" />
+ <excludeFolder url="file://$MODULE_DIR$/build/intermediates/ndk" />
+ <excludeFolder url="file://$MODULE_DIR$/build/intermediates/pre-dexed" />
+ <excludeFolder url="file://$MODULE_DIR$/build/intermediates/proguard" />
+ <excludeFolder url="file://$MODULE_DIR$/build/intermediates/res" />
+ <excludeFolder url="file://$MODULE_DIR$/build/intermediates/rs" />
+ <excludeFolder url="file://$MODULE_DIR$/build/intermediates/symbols" />
+ <excludeFolder url="file://$MODULE_DIR$/build/outputs" />
+ <excludeFolder url="file://$MODULE_DIR$/build/tmp" />
+ </content>
+ <orderEntry type="jdk" jdkName="Android API 22 Platform" jdkType="Android SDK" />
+ <orderEntry type="sourceFolder" forTests="false" />
+ <orderEntry type="library" exported="" name="mediarouter-v7-22.1.1" level="project" />
+ <orderEntry type="library" exported="" name="seamless-util-1.1.0" level="project" />
+ <orderEntry type="library" exported="" name="cling-core-2.0.1" level="project" />
+ <orderEntry type="library" exported="" name="javax.servlet-3.0.0.v201112011016" level="project" />
+ <orderEntry type="library" exported="" name="support-annotations-22.1.1" level="project" />
+ <orderEntry type="library" exported="" name="support-v4-22.1.1" level="project" />
+ <orderEntry type="library" exported="" name="play-services-cast-7.0.0" level="project" />
+ <orderEntry type="library" exported="" name="cling-support-2.0.1" level="project" />
+ <orderEntry type="library" exported="" name="seamless-http-1.1.0" level="project" />
+ <orderEntry type="library" exported="" name="appcompat-v7-22.1.1" level="project" />
+ <orderEntry type="library" exported="" name="seamless-xml-1.1.0" level="project" />
+ <orderEntry type="library" exported="" name="CWAC-EndlessAdapter" level="project" />
+ <orderEntry type="library" exported="" name="kryo-2.21-all" level="project" />
+ <orderEntry type="library" exported="" name="play-services-base-7.0.0" level="project" />
+ <orderEntry type="library" exported="" name="jetty-all-8.1.16.v20140903" level="project" />
+ <orderEntry type="library" exported="" name="CWAC-AdapterWrapper" level="project" />
+ <orderEntry type="module" module-name="DragSort ListView" exported="" />
+ <orderEntry type="module" module-name="Server Proxy" exported="" />
+ </component>
+</module>
+
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 00000000..cb3119bd
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,32 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 22
+ buildToolsVersion "22.0.0"
+
+ defaultConfig {
+ applicationId "github.daneren2005.dsub"
+ minSdkVersion 9
+ targetSdkVersion 22
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard.cfg')
+ }
+ }
+
+ packagingOptions {
+ exclude 'META-INF/beans.xml'
+ }
+}
+
+dependencies {
+ compile project(':DragSort ListView')
+ compile project(':Server Proxy')
+ compile fileTree(include: ['*.jar'], dir: 'libs')
+ compile 'com.android.support:support-v4:22.1.1'
+ compile 'com.android.support:appcompat-v7:22.1.1'
+ compile 'com.android.support:mediarouter-v7:22.1.1'
+ compile 'com.google.android.gms:play-services-cast:7.0.0'
+}
diff --git a/app/libs/CWAC-AdapterWrapper.jar b/app/libs/CWAC-AdapterWrapper.jar
new file mode 100644
index 00000000..692fe4d3
--- /dev/null
+++ b/app/libs/CWAC-AdapterWrapper.jar
Binary files differ
diff --git a/app/libs/CWAC-EndlessAdapter.jar b/app/libs/CWAC-EndlessAdapter.jar
new file mode 100644
index 00000000..ec20d936
--- /dev/null
+++ b/app/libs/CWAC-EndlessAdapter.jar
Binary files differ
diff --git a/app/libs/cling-core-2.0.1.jar b/app/libs/cling-core-2.0.1.jar
new file mode 100644
index 00000000..632d3038
--- /dev/null
+++ b/app/libs/cling-core-2.0.1.jar
Binary files differ
diff --git a/app/libs/cling-support-2.0.1.jar b/app/libs/cling-support-2.0.1.jar
new file mode 100644
index 00000000..7fa28604
--- /dev/null
+++ b/app/libs/cling-support-2.0.1.jar
Binary files differ
diff --git a/app/libs/javax.servlet-3.0.0.v201112011016.jar b/app/libs/javax.servlet-3.0.0.v201112011016.jar
new file mode 100644
index 00000000..b1354096
--- /dev/null
+++ b/app/libs/javax.servlet-3.0.0.v201112011016.jar
Binary files differ
diff --git a/app/libs/jetty-all-8.1.16.v20140903.jar b/app/libs/jetty-all-8.1.16.v20140903.jar
new file mode 100644
index 00000000..25b1d324
--- /dev/null
+++ b/app/libs/jetty-all-8.1.16.v20140903.jar
Binary files differ
diff --git a/app/libs/kryo-2.21-all.jar b/app/libs/kryo-2.21-all.jar
new file mode 100644
index 00000000..83f8b0f0
--- /dev/null
+++ b/app/libs/kryo-2.21-all.jar
Binary files differ
diff --git a/app/libs/seamless-http-1.1.0.jar b/app/libs/seamless-http-1.1.0.jar
new file mode 100644
index 00000000..98ec884a
--- /dev/null
+++ b/app/libs/seamless-http-1.1.0.jar
Binary files differ
diff --git a/app/libs/seamless-util-1.1.0.jar b/app/libs/seamless-util-1.1.0.jar
new file mode 100644
index 00000000..12026b7f
--- /dev/null
+++ b/app/libs/seamless-util-1.1.0.jar
Binary files differ
diff --git a/app/libs/seamless-xml-1.1.0.jar b/app/libs/seamless-xml-1.1.0.jar
new file mode 100644
index 00000000..1e740877
--- /dev/null
+++ b/app/libs/seamless-xml-1.1.0.jar
Binary files differ
diff --git a/app/src/androidTest/java/github/daneren2005/dsub/ApplicationTest.java b/app/src/androidTest/java/github/daneren2005/dsub/ApplicationTest.java
new file mode 100644
index 00000000..b7d12df1
--- /dev/null
+++ b/app/src/androidTest/java/github/daneren2005/dsub/ApplicationTest.java
@@ -0,0 +1,13 @@
+package github.daneren2005.dsub;
+
+import android.app.Application;
+import android.test.ApplicationTestCase;
+
+/**
+ * <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
+ */
+public class ApplicationTest extends ApplicationTestCase<Application> {
+ public ApplicationTest() {
+ super(Application.class);
+ }
+} \ No newline at end of file
diff --git a/app/src/androidTest/java/github/daneren2005/dsub/activity/DownloadActivityTest.java b/app/src/androidTest/java/github/daneren2005/dsub/activity/DownloadActivityTest.java
new file mode 100644
index 00000000..ce859181
--- /dev/null
+++ b/app/src/androidTest/java/github/daneren2005/dsub/activity/DownloadActivityTest.java
@@ -0,0 +1,32 @@
+package github.daneren2005.dsub.activity;
+
+import github.daneren2005.dsub.R;
+import android.test.*;
+import android.view.View;
+
+public class DownloadActivityTest extends
+ ActivityInstrumentationTestCase2<DownloadActivity> {
+
+ private DownloadActivity activity;
+
+ public DownloadActivityTest() {
+ super(DownloadActivity.class);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ activity = getActivity();
+ }
+
+ /**
+ * Test the main layout.
+ */
+ public void testLayout() {
+ View view = activity.findViewById(R.layout.download_activity);
+ assertNotNull(view);
+ assertNotNull(view.findViewById(R.layout.download_activity));
+ assertNotNull(activity.findViewById(R.id.fragment_container));
+ }
+
+}
diff --git a/app/src/androidTest/java/github/daneren2005/dsub/activity/SubsonicFragmentActivityTest.java b/app/src/androidTest/java/github/daneren2005/dsub/activity/SubsonicFragmentActivityTest.java
new file mode 100644
index 00000000..553938c8
--- /dev/null
+++ b/app/src/androidTest/java/github/daneren2005/dsub/activity/SubsonicFragmentActivityTest.java
@@ -0,0 +1,34 @@
+package github.daneren2005.dsub.activity;
+
+import github.daneren2005.dsub.R;
+import android.test.ActivityInstrumentationTestCase2;
+
+public class SubsonicFragmentActivityTest extends
+ ActivityInstrumentationTestCase2<SubsonicFragmentActivity> {
+
+ private SubsonicFragmentActivity activity;
+
+ public SubsonicFragmentActivityTest() {
+ super(SubsonicFragmentActivity.class);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ activity = getActivity();
+ }
+
+ /**
+ * Test the main layout.
+ */
+ public void testLayout() {
+ assertNotNull(activity.findViewById(R.id.content_frame));
+ }
+
+ /**
+ * Test the bottom bar.
+ */
+ public void testBottomBar() {
+ assertNotNull(activity.findViewById(R.id.bottom_bar));
+ }
+}
diff --git a/app/src/androidTest/java/github/daneren2005/dsub/domain/BookmarkTest.java b/app/src/androidTest/java/github/daneren2005/dsub/domain/BookmarkTest.java
new file mode 100644
index 00000000..814f658a
--- /dev/null
+++ b/app/src/androidTest/java/github/daneren2005/dsub/domain/BookmarkTest.java
@@ -0,0 +1,40 @@
+package github.daneren2005.dsub.domain;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+import junit.framework.TestCase;
+
+public class BookmarkTest extends TestCase {
+
+ /**
+ * tests the set created date
+ * @throws ParseException
+ */
+ public void testSetCreated() throws ParseException {
+ Bookmark bookmark = new Bookmark();
+ bookmark.setCreated(null);
+ assertEquals(null, bookmark.getCreated());
+
+ bookmark.setCreated("");
+ assertEquals(null, bookmark.getCreated());
+
+ bookmark.setCreated("2014-04-04");
+ assertEquals(null, bookmark.getCreated());
+
+ bookmark.setCreated("2014/04/04");
+ assertEquals(null, bookmark.getCreated());
+
+ bookmark.setCreated("18/03/1988");
+ assertEquals(null, bookmark.getCreated());
+
+ bookmark.setCreated("18/03/88");
+ assertEquals(null, bookmark.getCreated());
+
+ Date date = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH).parse("2013-10-20T00:00:00");
+ bookmark.setCreated("2013-10-20T00:00:00");
+ assertEquals(date, bookmark.getCreated());
+ }
+} \ No newline at end of file
diff --git a/app/src/androidTest/java/github/daneren2005/dsub/domain/GenreComparatorTest.java b/app/src/androidTest/java/github/daneren2005/dsub/domain/GenreComparatorTest.java
new file mode 100644
index 00000000..9ffa518e
--- /dev/null
+++ b/app/src/androidTest/java/github/daneren2005/dsub/domain/GenreComparatorTest.java
@@ -0,0 +1,68 @@
+package github.daneren2005.dsub.domain;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import junit.framework.TestCase;
+
+public class GenreComparatorTest extends TestCase {
+
+ /**
+ * Sort genres which doesn't have name
+ */
+ public void testSortGenreWithoutNameComparator() {
+ Genre g1 = new Genre();
+ g1.setName("Genre");
+
+ Genre g2 = new Genre();
+
+ List<Genre> genres = new ArrayList<Genre>();
+ genres.add(g1);
+ genres.add(g2);
+
+ List<Genre> sortedGenre = Genre.GenreComparator.sort(genres);
+ assertEquals(sortedGenre.get(0), g2);
+ }
+
+ /**
+ * Sort genre with same name
+ */
+ public void testSortGenreWithSameName() {
+ Genre g1 = new Genre();
+ g1.setName("Genre");
+
+ Genre g2 = new Genre();
+ g2.setName("genre");
+
+ List<Genre> genres = new ArrayList<Genre>();
+ genres.add(g1);
+ genres.add(g2);
+
+ List<Genre> sortedGenre = Genre.GenreComparator.sort(genres);
+ assertEquals(sortedGenre.get(0), g1);
+ }
+
+ /**
+ * test nominal genre sort
+ */
+ public void testSortGenre() {
+ Genre g1 = new Genre();
+ g1.setName("Rock");
+
+ Genre g2 = new Genre();
+ g2.setName("Pop");
+
+ Genre g3 = new Genre();
+ g2.setName("Rap");
+
+ List<Genre> genres = new ArrayList<Genre>();
+ genres.add(g1);
+ genres.add(g2);
+ genres.add(g3);
+
+ List<Genre> sortedGenre = Genre.GenreComparator.sort(genres);
+ assertEquals(sortedGenre.get(0), g2);
+ assertEquals(sortedGenre.get(1), g3);
+ assertEquals(sortedGenre.get(2), g1);
+ }
+} \ No newline at end of file
diff --git a/app/src/androidTest/java/github/daneren2005/dsub/service/DownloadServiceTest.java b/app/src/androidTest/java/github/daneren2005/dsub/service/DownloadServiceTest.java
new file mode 100644
index 00000000..44b77b84
--- /dev/null
+++ b/app/src/androidTest/java/github/daneren2005/dsub/service/DownloadServiceTest.java
@@ -0,0 +1,296 @@
+package github.daneren2005.dsub.service;
+
+import static github.daneren2005.dsub.domain.PlayerState.COMPLETED;
+import static github.daneren2005.dsub.domain.PlayerState.IDLE;
+import static github.daneren2005.dsub.domain.PlayerState.PAUSED;
+import static github.daneren2005.dsub.domain.PlayerState.STARTED;
+import static github.daneren2005.dsub.domain.PlayerState.STOPPED;
+import java.util.List;
+
+import github.daneren2005.dsub.activity.SubsonicFragmentActivity;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.PlayerState;
+
+import java.util.LinkedList;
+import android.test.ActivityInstrumentationTestCase2;
+import android.util.Log;
+
+public class DownloadServiceTest extends
+ ActivityInstrumentationTestCase2<SubsonicFragmentActivity> {
+
+ private SubsonicFragmentActivity activity;
+ private DownloadService downloadService;
+
+ public DownloadServiceTest() {
+ super(SubsonicFragmentActivity.class);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ activity = getActivity();
+ downloadService = activity.getDownloadService();
+ downloadService.clear();
+ }
+
+ /**
+ * Test the get player duration without playlist.
+ */
+ public void testGetPlayerDurationWithoutPlayList() {
+ int duration = downloadService.getPlayerDuration();
+ assertEquals(0, duration);
+ }
+
+ /**
+ * Test the get player position without playlist.
+ */
+ public void testGetPlayerPositionWithoutPlayList() {
+ int position = downloadService.getPlayerPosition();
+ assertEquals(0, position);
+ }
+
+ public void testGetCurrentPlayingIndexWithoutPlayList() {
+ int currentPlayingIndex = activity.getDownloadService()
+ .getCurrentPlayingIndex();
+ assertEquals(currentPlayingIndex, -1);
+ }
+
+ /**
+ * Test next action without playlist.
+ */
+ public void testNextWithoutPlayList() {
+ int oldCurrentPlayingIndex = downloadService.getCurrentPlayingIndex();
+ downloadService.next();
+ int newCurrentPlayingIndex = downloadService.getCurrentPlayingIndex();
+ assertTrue(oldCurrentPlayingIndex == newCurrentPlayingIndex);
+ }
+
+ /**
+ * Test previous action without playlist.
+ */
+ public void testPreviousWithoutPlayList() {
+ int oldCurrentPlayingIndex = downloadService.getCurrentPlayingIndex();
+ downloadService.previous();
+ int newCurrentPlayingIndex = downloadService.getCurrentPlayingIndex();
+ assertTrue(oldCurrentPlayingIndex == newCurrentPlayingIndex);
+ }
+
+ /**
+ * Test next action with playlist.
+ */
+ public void testNextWithPlayList() throws InterruptedException {
+ // Download two songs
+ downloadService.getDownloads().clear();
+ downloadService.download(this.createMusicSongs(2), false, false, false,
+ false, 0, 0);
+
+ Log.w("testPreviousWithPlayList", "Start waiting to downloads");
+ Thread.sleep(5000);
+ Log.w("testPreviousWithPlayList", "Stop waiting downloads");
+
+ // Get the current index
+ int oldCurrentPlayingIndex = downloadService.getCurrentPlayingIndex();
+
+ // Do the next
+ downloadService.next();
+
+ // Check that the new current index is incremented
+ int newCurrentPlayingIndex = downloadService.getCurrentPlayingIndex();
+ assertEquals(oldCurrentPlayingIndex + 1, newCurrentPlayingIndex);
+ }
+
+ /**
+ * Test previous action with playlist.
+ */
+ public void testPreviousWithPlayList() throws InterruptedException {
+ // Download two songs
+ downloadService.getDownloads().clear();
+ downloadService.download(this.createMusicSongs(2), false, false, false,
+ false, 0, 0);
+
+ Log.w("testPreviousWithPlayList", "Start waiting downloads");
+ Thread.sleep(5000);
+ Log.w("testPreviousWithPlayList", "Stop waiting downloads");
+
+ // Get the current index
+ int oldCurrentPlayingIndex = downloadService.getCurrentPlayingIndex();
+
+ // Do a next before the previous
+ downloadService.next();
+
+ // Do the previous
+ downloadService.previous();
+
+ // Check that the new current index is incremented
+ int newCurrentPlayingIndex = downloadService.getCurrentPlayingIndex();
+ assertEquals(oldCurrentPlayingIndex, newCurrentPlayingIndex);
+ }
+
+ /**
+ * Test seek feature.
+ */
+ public void testSeekTo() {
+ // seek with negative
+ downloadService.seekTo(Integer.MIN_VALUE);
+
+ // seek with null
+ downloadService.seekTo(0);
+
+ // seek with big value
+ downloadService.seekTo(Integer.MAX_VALUE);
+ }
+
+ /**
+ * Test toggle play pause.
+ */
+ public void testTogglePlayPause() {
+ PlayerState oldPlayState = downloadService.getPlayerState();
+ downloadService.togglePlayPause();
+ PlayerState newPlayState = downloadService.getPlayerState();
+ if (oldPlayState == PAUSED || oldPlayState == COMPLETED
+ || oldPlayState == STOPPED) {
+ assertEquals(STARTED, newPlayState);
+ } else if (oldPlayState == STOPPED || oldPlayState == IDLE) {
+ if (downloadService.size() == 0) {
+ assertEquals(IDLE, newPlayState);
+ } else {
+ assertEquals(STARTED, newPlayState);
+ }
+ } else if (oldPlayState == STARTED) {
+ assertEquals(PAUSED, newPlayState);
+ }
+ downloadService.togglePlayPause();
+ newPlayState = downloadService.getPlayerState();
+ assertEquals(oldPlayState, newPlayState);
+ }
+
+ /**
+ * Test toggle play pause without playlist.
+ */
+ public void testTogglePlayPauseWithoutPlayList() {
+ PlayerState oldPlayState = downloadService.getPlayerState();
+ downloadService.togglePlayPause();
+ PlayerState newPlayState = downloadService.getPlayerState();
+
+ assertEquals(IDLE, oldPlayState);
+ assertEquals(IDLE, newPlayState);
+ }
+
+ /**
+ * Test toggle play pause without playlist.
+ *
+ * @throws InterruptedException
+ */
+ public void testTogglePlayPauseWithPlayList() throws InterruptedException {
+ // Download two songs
+ downloadService.getDownloads().clear();
+ downloadService.download(this.createMusicSongs(2), false, false, false,
+ false, 0, 0);
+
+ Log.w("testPreviousWithPlayList", "Start waiting downloads");
+ Thread.sleep(5000);
+ Log.w("testPreviousWithPlayList", "Stop waiting downloads");
+
+ PlayerState oldPlayState = downloadService.getPlayerState();
+ downloadService.togglePlayPause();
+ Thread.sleep(500);
+ assertEquals(STARTED, downloadService.getPlayerState());
+ downloadService.togglePlayPause();
+ PlayerState newPlayState = downloadService.getPlayerState();
+ assertEquals(PAUSED, newPlayState);
+ }
+
+ /**
+ * Test the autoplay.
+ *
+ * @throws InterruptedException
+ */
+ public void testAutoplay() throws InterruptedException {
+ // Download one songs
+ downloadService.getDownloads().clear();
+ downloadService.download(this.createMusicSongs(1), false, true, false,
+ false, 0, 0);
+
+ Log.w("testPreviousWithPlayList", "Start waiting downloads");
+ Thread.sleep(5000);
+ Log.w("testPreviousWithPlayList", "Stop waiting downloads");
+
+ PlayerState playerState = downloadService.getPlayerState();
+ assertEquals(STARTED, playerState);
+ }
+
+ /**
+ * Test if the download list is empty.
+ */
+ public void testGetDownloadsEmptyList() {
+ List<DownloadFile> list = downloadService.getDownloads();
+ assertEquals(0, list.size());
+ }
+
+ /**
+ * Test if the download service add the given song to its queue.
+ */
+ public void testAddMusicToDownload() {
+ assertNotNull(downloadService);
+
+ // Download list before
+ List<DownloadFile> downloadList = downloadService.getDownloads();
+ int beforeDownloadAction = 0;
+ if (downloadList != null) {
+ beforeDownloadAction = downloadList.size();
+ }
+
+ // Launch download
+ downloadService.download(this.createMusicSongs(1), false, false, false,
+ false, 0, 0);
+
+ // Check number of download after
+ int afterDownloadAction = 0;
+ downloadList = downloadService.getDownloads();
+ if (downloadList != null && !downloadList.isEmpty()) {
+ afterDownloadAction = downloadList.size();
+ }
+ assertEquals(beforeDownloadAction + 1, afterDownloadAction);
+ }
+
+ /**
+ * Generate a list containing some music directory entries.
+ *
+ * @return list containing some music directory entries.
+ */
+ private List<MusicDirectory.Entry> createMusicSongs(int size) {
+ MusicDirectory.Entry musicEntry = new MusicDirectory.Entry();
+ musicEntry.setAlbum("Itchy Hitchhiker");
+ musicEntry.setBitRate(198);
+ musicEntry.setAlbumId("49");
+ musicEntry.setDuration(247);
+ musicEntry.setSize(Long.valueOf(6162717));
+ musicEntry.setArtistId("23");
+ musicEntry.setArtist("The Dada Weatherman");
+ musicEntry.setCloseness(0);
+ musicEntry.setContentType("audio/mpeg");
+ musicEntry.setCoverArt("433");
+ musicEntry.setDirectory(false);
+ musicEntry.setGenre("Easy Listening/New Age");
+ musicEntry.setGrandParent("306");
+ musicEntry.setId("466");
+ musicEntry.setParent("433");
+ musicEntry
+ .setPath("The Dada Weatherman/Itchy Hitchhiker/08 - The Dada Weatherman - Harmonies.mp3");
+ musicEntry.setStarred(true);
+ musicEntry.setSuffix("mp3");
+ musicEntry.setTitle("Harmonies");
+ musicEntry.setType(0);
+ musicEntry.setVideo(false);
+
+ List<MusicDirectory.Entry> musicEntries = new LinkedList<MusicDirectory.Entry>();
+
+ for (int i = 0; i < size; i++) {
+ musicEntries.add(musicEntry);
+ }
+
+ return musicEntries;
+
+ }
+
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..30a6eb3c
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,250 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="github.daneren2005.dsub"
+ android:installLocation="internalOnly"
+ android:versionCode="151"
+ android:versionName="4.9.6">
+
+ <instrumentation android:name="android.test.InstrumentationTestRunner"
+ android:targetPackage="github.daneren2005.dsub"
+ android:label="Tests" />
+
+ <uses-permission android:name="android.permission.INTERNET"/>
+ <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+ <uses-permission android:name="android.permission.WAKE_LOCK"/>
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+ <uses-permission android:name="android.permission.RECORD_AUDIO"/>
+ <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
+ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+ <uses-permission android:name="android.permission.BLUETOOTH"/>
+ <uses-permission android:name="android.permission.READ_LOGS"/>
+ <uses-permission android:name="android.permission.READ_SYNC_SETTINGS"/>
+ <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS"/>
+ <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS"/>
+ <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
+ <uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"/>
+ <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
+
+ <uses-feature android:name="android.hardware.touchscreen" android:required="false" />
+ <uses-feature android:name="android.hardware.bluetooth" android:required="false" />
+ <uses-feature android:name="android.hardware.microphone" android:required="false" />
+ <uses-feature android:name="android.hardware.wifi" android:required="false" />
+
+ <uses-sdk android:minSdkVersion="9" android:targetSdkVersion="19"/>
+
+ <supports-screens android:anyDensity="true" android:xlargeScreens="true" android:largeScreens="true" android:normalScreens="true" android:smallScreens="true"/>
+
+ <application android:label="@string/common.appname"
+ android:backupAgent="github.daneren2005.dsub.util.SettingsBackupAgent"
+ android:icon="@drawable/launch"
+ android:theme="@style/Theme.DSub.Light">
+
+ <uses-library android:name="android.test.runner" />
+
+ <activity android:name="github.daneren2005.dsub.activity.SubsonicFragmentActivity"
+ android:configChanges="orientation|keyboardHidden"
+ android:launchMode="standard">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ <category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
+ </intent-filter>
+ </activity>
+
+ <activity android:name="github.daneren2005.dsub.activity.DownloadActivity"
+ android:configChanges="keyboardHidden"
+ android:launchMode="singleTask"/>
+
+ <activity android:name="github.daneren2005.dsub.activity.SettingsActivity"
+ android:configChanges="orientation|keyboardHidden"
+ android:launchMode="singleTask"/>
+
+ <activity android:name="github.daneren2005.dsub.activity.VoiceQueryReceiverActivity"
+ android:launchMode="singleTask">
+ <intent-filter>
+ <action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+
+ <intent-filter>
+ <action android:name="com.google.android.gms.actions.SEARCH_ACTION"/>
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ </intent-filter>
+ </activity>
+
+ <activity android:name="github.daneren2005.dsub.activity.QueryReceiverActivity"
+ android:launchMode="singleTask">
+ <intent-filter>
+ <action android:name="android.intent.action.SEARCH"/>
+ </intent-filter>
+ <meta-data android:name="android.app.searchable" android:resource="@xml/searchable"/>
+ </activity>
+
+ <activity
+ android:name="github.daneren2005.dsub.activity.EditPlayActionActivity"
+ android:label="@string/tasker.start_playing"
+ android:icon="@drawable/launch">
+
+ <intent-filter>
+ <action android:name="com.twofortyfouram.locale.intent.action.EDIT_SETTING" />
+ </intent-filter>
+ </activity>
+
+ <service android:name=".service.DownloadService"
+ android:label="DSub Playback Service"/>
+ <service android:name="org.fourthline.cling.android.AndroidUpnpServiceImpl"/>
+ <service android:name="github.daneren2005.dsub.service.sync.AuthenticatorService">
+ <intent-filter>
+ <action android:name="android.accounts.AccountAuthenticator"/>
+ </intent-filter>
+
+ <meta-data android:name="android.accounts.AccountAuthenticator"
+ android:resource="@xml/authenticator" />
+ </service>
+ <service android:name="github.daneren2005.dsub.service.sync.PlaylistSyncService"
+ android:exported="true"
+ android:process=":sync">
+
+ <intent-filter>
+ <action android:name="android.content.SyncAdapter"/>
+ </intent-filter>
+ <meta-data android:name="android.content.SyncAdapter"
+ android:resource="@xml/playlists_syncadapter" />
+ </service>
+ <service android:name="github.daneren2005.dsub.service.sync.PodcastSyncService"
+ android:exported="true"
+ android:process=":sync">
+
+ <intent-filter>
+ <action android:name="android.content.SyncAdapter"/>
+ </intent-filter>
+ <meta-data android:name="android.content.SyncAdapter"
+ android:resource="@xml/podcasts_syncadapter" />
+ </service>
+ <service android:name="github.daneren2005.dsub.service.sync.StarredSyncService"
+ android:exported="true"
+ android:process=":sync">
+
+ <intent-filter>
+ <action android:name="android.content.SyncAdapter"/>
+ </intent-filter>
+ <meta-data android:name="android.content.SyncAdapter"
+ android:resource="@xml/starred_syncadapter" />
+ </service>
+ <service android:name="github.daneren2005.dsub.service.sync.MostRecentSyncService"
+ android:exported="true"
+ android:process=":sync">
+
+ <intent-filter>
+ <action android:name="android.content.SyncAdapter"/>
+ </intent-filter>
+ <meta-data android:name="android.content.SyncAdapter"
+ android:resource="@xml/mostrecent_syncadapter" />
+ </service>
+
+ <service android:name="github.daneren2005.dsub.service.HeadphoneListenerService"
+ android:label="DSub Headphone Listener"/>
+ <receiver
+ android:name="github.daneren2005.dsub.receiver.BootReceiver">
+ <intent-filter>
+ <action
+ android:name="android.intent.action.BOOT_COMPLETED" />
+ </intent-filter>
+ </receiver>
+
+ <receiver android:name="github.daneren2005.dsub.receiver.MediaButtonIntentReceiver">
+ <intent-filter>
+ <action android:name="android.intent.action.MEDIA_BUTTON" />
+ </intent-filter>
+ </receiver>
+
+ <receiver android:name="github.daneren2005.dsub.receiver.AudioNoisyReceiver">
+ <intent-filter android:priority="999">
+ <action android:name="android.media.AUDIO_BECOMING_NOISY" />
+ </intent-filter>
+ </receiver>
+
+ <receiver android:name="github.daneren2005.dsub.receiver.A2dpIntentReceiver">
+ <intent-filter>
+ <action android:name="android.music.playstatusrequest"/>
+ </intent-filter>
+ </receiver>
+
+ <receiver
+ android:name="github.daneren2005.dsub.provider.DSubWidget4x1"
+ android:label="@string/widget.4x1">
+ <intent-filter>
+ <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
+ </intent-filter>
+ <meta-data android:name="android.appwidget.provider" android:resource="@xml/appwidget4x1"/>
+ </receiver>
+ <receiver
+ android:name="github.daneren2005.dsub.provider.DSubWidget4x2"
+ android:label="@string/widget.4x2">
+ <intent-filter>
+ <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
+ </intent-filter>
+ <meta-data android:name="android.appwidget.provider" android:resource="@xml/appwidget4x2"/>
+ </receiver>
+ <receiver
+ android:name="github.daneren2005.dsub.provider.DSubWidget4x3"
+ android:label="@string/widget.4x3">
+ <intent-filter>
+ <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
+ </intent-filter>
+ <meta-data android:name="android.appwidget.provider" android:resource="@xml/appwidget4x3"/>
+ </receiver>
+ <receiver
+ android:name="github.daneren2005.dsub.provider.DSubWidget4x4"
+ android:label="@string/widget.4x4">
+ <intent-filter>
+ <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
+ </intent-filter>
+ <meta-data android:name="android.appwidget.provider" android:resource="@xml/appwidget4x4"/>
+ </receiver>
+
+ <receiver
+ android:name="github.daneren2005.dsub.receiver.PlayActionReceiver">
+
+ <intent-filter>
+ <action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING" />
+ </intent-filter>
+ </receiver>
+
+ <provider android:name="github.daneren2005.dsub.provider.DSubSearchProvider"
+ android:authorities="github.daneren2005.dsub.provider.DSubSearchProvider"/>
+ <provider android:name="github.daneren2005.dsub.provider.PlaylistStubProvider"
+ android:authorities="github.daneren2005.dsub.playlists.provider"
+ android:label="@string/button_bar.playlists"
+ android:exported="false"
+ android:syncable="true"/>
+ <provider android:name="github.daneren2005.dsub.provider.PodcastStubProvider"
+ android:authorities="github.daneren2005.dsub.podcasts.provider"
+ android:label="@string/button_bar.podcasts"
+ android:exported="false"
+ android:syncable="true"/>
+ <provider android:name="github.daneren2005.dsub.provider.StarredStubProvider"
+ android:authorities="github.daneren2005.dsub.starred.provider"
+ android:label="@string/main.albums_starred"
+ android:exported="false"
+ android:syncable="true"/>
+ <provider android:name="github.daneren2005.dsub.provider.MostRecentStubProvider"
+ android:authorities="github.daneren2005.dsub.mostrecent.provider"
+ android:label="@string/main.albums_newest"
+ android:exported="false"
+ android:syncable="true"/>
+
+ <meta-data android:name="android.app.default_searchable"
+ android:value="github.daneren2005.dsub.activity.QueryReceiverActivity"/>
+
+ <meta-data android:name="com.google.android.backup.api_key"
+ android:value="AEdPqrEAAAAIUhOMtwa_eG-f0oYUHnetl_Cz7cO9zae8ZXOK5w"/>
+
+ <meta-data
+ android:name="com.google.android.gms.version"
+ android:value="@integer/google_play_services_version" />
+ </application>
+
+</manifest>
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());
+ }
+}
diff --git a/app/src/main/res/anim/enter_from_left.xml b/app/src/main/res/anim/enter_from_left.xml
new file mode 100644
index 00000000..3c11332c
--- /dev/null
+++ b/app/src/main/res/anim/enter_from_left.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shareInterpolator="false">
+
+ <translate
+ android:fromXDelta="-100%" android:toXDelta="0%"
+ android:duration="@android:integer/config_shortAnimTime"
+ android:interpolator="@android:anim/accelerate_interpolator"/>
+
+ <alpha android:fromAlpha="0.0" android:toAlpha="1.0"
+ android:duration="@android:integer/config_shortAnimTime" />
+</set> \ No newline at end of file
diff --git a/app/src/main/res/anim/enter_from_right.xml b/app/src/main/res/anim/enter_from_right.xml
new file mode 100644
index 00000000..568a0c07
--- /dev/null
+++ b/app/src/main/res/anim/enter_from_right.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shareInterpolator="false">
+
+ <translate
+ android:fromXDelta="100%" android:toXDelta="0%"
+ android:duration="@android:integer/config_shortAnimTime"
+ android:interpolator="@android:anim/accelerate_interpolator"/>
+
+ <alpha android:fromAlpha="0.0" android:toAlpha="1.0"
+ android:duration="@android:integer/config_shortAnimTime" />
+</set> \ No newline at end of file
diff --git a/app/src/main/res/anim/exit_to_left.xml b/app/src/main/res/anim/exit_to_left.xml
new file mode 100644
index 00000000..2cb8febf
--- /dev/null
+++ b/app/src/main/res/anim/exit_to_left.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shareInterpolator="false">
+
+ <translate
+ android:fromXDelta="0%" android:toXDelta="-100%"
+ android:duration="@android:integer/config_shortAnimTime"
+ android:interpolator="@android:anim/accelerate_interpolator"/>
+
+ <alpha android:fromAlpha="1.0" android:toAlpha="0.0"
+ android:duration="@android:integer/config_shortAnimTime" />
+</set> \ No newline at end of file
diff --git a/app/src/main/res/anim/exit_to_right.xml b/app/src/main/res/anim/exit_to_right.xml
new file mode 100644
index 00000000..a3fa5bad
--- /dev/null
+++ b/app/src/main/res/anim/exit_to_right.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shareInterpolator="false">
+
+ <translate
+ android:fromXDelta="0%" android:toXDelta="100%"
+ android:duration="@android:integer/config_shortAnimTime"
+ android:interpolator="@android:anim/accelerate_interpolator"/>
+
+ <alpha android:fromAlpha="1.0" android:toAlpha="0.0"
+ android:duration="@android:integer/config_shortAnimTime" />
+</set> \ No newline at end of file
diff --git a/app/src/main/res/anim/fade_in.xml b/app/src/main/res/anim/fade_in.xml
new file mode 100644
index 00000000..c41db065
--- /dev/null
+++ b/app/src/main/res/anim/fade_in.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<alpha xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="500"
+ android:fromAlpha="0.0"
+ android:toAlpha="1.0" /> \ No newline at end of file
diff --git a/app/src/main/res/anim/fade_out.xml b/app/src/main/res/anim/fade_out.xml
new file mode 100644
index 00000000..d615f2a1
--- /dev/null
+++ b/app/src/main/res/anim/fade_out.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<alpha xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="500"
+ android:fromAlpha="1.0"
+ android:toAlpha="0.0" /> \ No newline at end of file
diff --git a/app/src/main/res/anim/push_down_in.xml b/app/src/main/res/anim/push_down_in.xml
new file mode 100644
index 00000000..6ab9a047
--- /dev/null
+++ b/app/src/main/res/anim/push_down_in.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 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.
+-->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <translate android:fromYDelta="-100%p" android:toYDelta="0"
+ android:duration="@android:integer/config_longAnimTime"/>
+ <alpha android:fromAlpha="0.0" android:toAlpha="1.0"
+ android:duration="@android:integer/config_longAnimTime" />
+</set>
diff --git a/app/src/main/res/anim/push_down_out.xml b/app/src/main/res/anim/push_down_out.xml
new file mode 100644
index 00000000..ce36458a
--- /dev/null
+++ b/app/src/main/res/anim/push_down_out.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 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.
+-->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <translate android:fromYDelta="0" android:toYDelta="100%p"
+ android:duration="@android:integer/config_longAnimTime"/>
+ <alpha android:fromAlpha="1.0" android:toAlpha="0.0"
+ android:duration="@android:integer/config_longAnimTime" />
+</set>
diff --git a/app/src/main/res/anim/push_up_in.xml b/app/src/main/res/anim/push_up_in.xml
new file mode 100644
index 00000000..6ef582c4
--- /dev/null
+++ b/app/src/main/res/anim/push_up_in.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 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.
+-->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <translate android:fromYDelta="100%p" android:toYDelta="0"
+ android:duration="@android:integer/config_longAnimTime"/>
+ <alpha android:fromAlpha="0.0" android:toAlpha="1.0"
+ android:duration="@android:integer/config_longAnimTime" />
+</set>
diff --git a/app/src/main/res/anim/push_up_out.xml b/app/src/main/res/anim/push_up_out.xml
new file mode 100644
index 00000000..2b267d59
--- /dev/null
+++ b/app/src/main/res/anim/push_up_out.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 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.
+-->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <translate android:fromYDelta="0" android:toYDelta="-100%p"
+ android:duration="@android:integer/config_longAnimTime"/>
+ <alpha android:fromAlpha="1.0" android:toAlpha="0.0"
+ android:duration="@android:integer/config_longAnimTime" />
+</set>
diff --git a/app/src/main/res/drawable-hdpi-v11/notification_close.png b/app/src/main/res/drawable-hdpi-v11/notification_close.png
new file mode 100644
index 00000000..254e130f
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi-v11/notification_close.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi-v11/notification_next.png b/app/src/main/res/drawable-hdpi-v11/notification_next.png
new file mode 100644
index 00000000..59239305
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi-v11/notification_next.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi-v11/notification_pause.png b/app/src/main/res/drawable-hdpi-v11/notification_pause.png
new file mode 100644
index 00000000..cbd61795
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi-v11/notification_pause.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi-v11/notification_play.png b/app/src/main/res/drawable-hdpi-v11/notification_play.png
new file mode 100644
index 00000000..78b4d5bf
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi-v11/notification_play.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi-v11/notification_previous.png b/app/src/main/res/drawable-hdpi-v11/notification_previous.png
new file mode 100644
index 00000000..556eaec3
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi-v11/notification_previous.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi-v11/stat_notify_download.png b/app/src/main/res/drawable-hdpi-v11/stat_notify_download.png
new file mode 100644
index 00000000..48ca6924
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi-v11/stat_notify_download.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi-v11/stat_notify_playing.png b/app/src/main/res/drawable-hdpi-v11/stat_notify_playing.png
new file mode 100644
index 00000000..78b4d5bf
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi-v11/stat_notify_playing.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi-v11/stat_notify_sync.png b/app/src/main/res/drawable-hdpi-v11/stat_notify_sync.png
new file mode 100644
index 00000000..f1ff1eb2
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi-v11/stat_notify_sync.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/action_toggle_list_dark.png b/app/src/main/res/drawable-hdpi/action_toggle_list_dark.png
new file mode 100644
index 00000000..d0ec1a5d
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/action_toggle_list_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/action_toggle_list_light.png b/app/src/main/res/drawable-hdpi/action_toggle_list_light.png
new file mode 100644
index 00000000..60ec88be
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/action_toggle_list_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/actionbar_button_normal.9.png b/app/src/main/res/drawable-hdpi/actionbar_button_normal.9.png
new file mode 100644
index 00000000..385f751c
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/actionbar_button_normal.9.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/appwidget_art_default.png b/app/src/main/res/drawable-hdpi/appwidget_art_default.png
new file mode 100644
index 00000000..5bd39cc2
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/appwidget_art_default.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/appwidget_art_unknown.png b/app/src/main/res/drawable-hdpi/appwidget_art_unknown.png
new file mode 100644
index 00000000..5bd39cc2
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/appwidget_art_unknown.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/appwidget_bg.9.png b/app/src/main/res/drawable-hdpi/appwidget_bg.9.png
new file mode 100644
index 00000000..6bacc7fe
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/appwidget_bg.9.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/background.png b/app/src/main/res/drawable-hdpi/background.png
new file mode 100644
index 00000000..07d6a9cc
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/background.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/download_cached.png b/app/src/main/res/drawable-hdpi/download_cached.png
new file mode 100644
index 00000000..56bfc0e1
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/download_cached.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/download_none_dark.png b/app/src/main/res/drawable-hdpi/download_none_dark.png
new file mode 100644
index 00000000..a074c10d
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/download_none_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/download_none_light.png b/app/src/main/res/drawable-hdpi/download_none_light.png
new file mode 100644
index 00000000..21544e5f
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/download_none_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/download_pinned.png b/app/src/main/res/drawable-hdpi/download_pinned.png
new file mode 100644
index 00000000..711c7704
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/download_pinned.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/downloading_dark.png b/app/src/main/res/drawable-hdpi/downloading_dark.png
new file mode 100644
index 00000000..3ccb1837
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/downloading_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/downloading_light.png b/app/src/main/res/drawable-hdpi/downloading_light.png
new file mode 100644
index 00000000..07be3016
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/downloading_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_action_add_dark.png b/app/src/main/res/drawable-hdpi/ic_action_add_dark.png
new file mode 100644
index 00000000..81d535d5
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_action_add_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_action_add_light.png b/app/src/main/res/drawable-hdpi/ic_action_add_light.png
new file mode 100644
index 00000000..0e4f3347
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_action_add_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_action_album.png b/app/src/main/res/drawable-hdpi/ic_action_album.png
new file mode 100644
index 00000000..6058c483
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_action_album.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_action_artist.png b/app/src/main/res/drawable-hdpi/ic_action_artist.png
new file mode 100644
index 00000000..40c9a7fb
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_action_artist.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_action_rating_bad_dark.png b/app/src/main/res/drawable-hdpi/ic_action_rating_bad_dark.png
new file mode 100644
index 00000000..855709e9
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_action_rating_bad_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_action_rating_bad_light.png b/app/src/main/res/drawable-hdpi/ic_action_rating_bad_light.png
new file mode 100644
index 00000000..34199d3a
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_action_rating_bad_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_action_rating_bad_selected.png b/app/src/main/res/drawable-hdpi/ic_action_rating_bad_selected.png
new file mode 100644
index 00000000..c57aba50
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_action_rating_bad_selected.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_action_rating_good_dark.png b/app/src/main/res/drawable-hdpi/ic_action_rating_good_dark.png
new file mode 100644
index 00000000..fa91e699
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_action_rating_good_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_action_rating_good_light.png b/app/src/main/res/drawable-hdpi/ic_action_rating_good_light.png
new file mode 100644
index 00000000..3427d770
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_action_rating_good_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_action_rating_good_selected.png b/app/src/main/res/drawable-hdpi/ic_action_rating_good_selected.png
new file mode 100644
index 00000000..34d53153
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_action_rating_good_selected.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_action_song.png b/app/src/main/res/drawable-hdpi/ic_action_song.png
new file mode 100644
index 00000000..95342f76
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_action_song.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_action_volume_dark.png b/app/src/main/res/drawable-hdpi/ic_action_volume_dark.png
new file mode 100644
index 00000000..62550655
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_action_volume_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_action_volume_light.png b/app/src/main/res/drawable-hdpi/ic_action_volume_light.png
new file mode 100644
index 00000000..1b1d182c
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_action_volume_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_appwidget_music_next.png b/app/src/main/res/drawable-hdpi/ic_appwidget_music_next.png
new file mode 100644
index 00000000..99d28766
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_appwidget_music_next.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_appwidget_music_pause.png b/app/src/main/res/drawable-hdpi/ic_appwidget_music_pause.png
new file mode 100644
index 00000000..a05a8c50
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_appwidget_music_pause.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_appwidget_music_play.png b/app/src/main/res/drawable-hdpi/ic_appwidget_music_play.png
new file mode 100644
index 00000000..a754b920
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_appwidget_music_play.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_appwidget_music_previous.png b/app/src/main/res/drawable-hdpi/ic_appwidget_music_previous.png
new file mode 100644
index 00000000..7fb3921b
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_appwidget_music_previous.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_menu_add_person_dark.png b/app/src/main/res/drawable-hdpi/ic_menu_add_person_dark.png
new file mode 100644
index 00000000..971048d5
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_menu_add_person_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_menu_add_person_light.png b/app/src/main/res/drawable-hdpi/ic_menu_add_person_light.png
new file mode 100644
index 00000000..f94446d0
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_menu_add_person_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_menu_admin_dark.png b/app/src/main/res/drawable-hdpi/ic_menu_admin_dark.png
new file mode 100644
index 00000000..76da5ade
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_menu_admin_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_menu_admin_light.png b/app/src/main/res/drawable-hdpi/ic_menu_admin_light.png
new file mode 100644
index 00000000..5431889c
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_menu_admin_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_menu_bookmark_dark.png b/app/src/main/res/drawable-hdpi/ic_menu_bookmark_dark.png
new file mode 100644
index 00000000..e7cd08e4
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_menu_bookmark_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_menu_bookmark_light.png b/app/src/main/res/drawable-hdpi/ic_menu_bookmark_light.png
new file mode 100644
index 00000000..fdb46da3
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_menu_bookmark_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_menu_bookmark_selected.png b/app/src/main/res/drawable-hdpi/ic_menu_bookmark_selected.png
new file mode 100644
index 00000000..5a33d60c
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_menu_bookmark_selected.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_menu_chat_dark.png b/app/src/main/res/drawable-hdpi/ic_menu_chat_dark.png
new file mode 100644
index 00000000..75363fce
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_menu_chat_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_menu_chat_light.png b/app/src/main/res/drawable-hdpi/ic_menu_chat_light.png
new file mode 100644
index 00000000..e28933e4
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_menu_chat_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_menu_chat_send_dark.png b/app/src/main/res/drawable-hdpi/ic_menu_chat_send_dark.png
new file mode 100644
index 00000000..c0e9b372
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_menu_chat_send_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_menu_chat_send_light.png b/app/src/main/res/drawable-hdpi/ic_menu_chat_send_light.png
new file mode 100644
index 00000000..ebcfe9e8
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_menu_chat_send_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_menu_download_dark.png b/app/src/main/res/drawable-hdpi/ic_menu_download_dark.png
new file mode 100644
index 00000000..872b73c0
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_menu_download_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_menu_download_light.png b/app/src/main/res/drawable-hdpi/ic_menu_download_light.png
new file mode 100644
index 00000000..f8818490
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_menu_download_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_menu_library_dark.png b/app/src/main/res/drawable-hdpi/ic_menu_library_dark.png
new file mode 100644
index 00000000..717cb3e1
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_menu_library_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_menu_library_light.png b/app/src/main/res/drawable-hdpi/ic_menu_library_light.png
new file mode 100644
index 00000000..17a45d77
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_menu_library_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_menu_password_dark.png b/app/src/main/res/drawable-hdpi/ic_menu_password_dark.png
new file mode 100644
index 00000000..67fa3e84
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_menu_password_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_menu_password_light.png b/app/src/main/res/drawable-hdpi/ic_menu_password_light.png
new file mode 100644
index 00000000..bd99c01f
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_menu_password_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_menu_playlist_dark.png b/app/src/main/res/drawable-hdpi/ic_menu_playlist_dark.png
new file mode 100644
index 00000000..8e3babc7
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_menu_playlist_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_menu_playlist_light.png b/app/src/main/res/drawable-hdpi/ic_menu_playlist_light.png
new file mode 100644
index 00000000..4131dba4
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_menu_playlist_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_menu_podcast_dark.png b/app/src/main/res/drawable-hdpi/ic_menu_podcast_dark.png
new file mode 100644
index 00000000..d1d62d03
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_menu_podcast_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_menu_podcast_light.png b/app/src/main/res/drawable-hdpi/ic_menu_podcast_light.png
new file mode 100644
index 00000000..4ce1b787
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_menu_podcast_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_menu_radio_dark.png b/app/src/main/res/drawable-hdpi/ic_menu_radio_dark.png
new file mode 100644
index 00000000..a801dce0
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_menu_radio_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_menu_radio_light.png b/app/src/main/res/drawable-hdpi/ic_menu_radio_light.png
new file mode 100644
index 00000000..b723d574
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_menu_radio_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_menu_refresh_dark.png b/app/src/main/res/drawable-hdpi/ic_menu_refresh_dark.png
new file mode 100644
index 00000000..2795cfa9
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_menu_refresh_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_menu_refresh_light.png b/app/src/main/res/drawable-hdpi/ic_menu_refresh_light.png
new file mode 100644
index 00000000..86d1b042
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_menu_refresh_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_menu_remove_dark.png b/app/src/main/res/drawable-hdpi/ic_menu_remove_dark.png
new file mode 100644
index 00000000..878b378a
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_menu_remove_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_menu_remove_light.png b/app/src/main/res/drawable-hdpi/ic_menu_remove_light.png
new file mode 100644
index 00000000..ece5ad8d
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_menu_remove_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_menu_save_dark.png b/app/src/main/res/drawable-hdpi/ic_menu_save_dark.png
new file mode 100644
index 00000000..b80828bf
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_menu_save_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_menu_save_light.png b/app/src/main/res/drawable-hdpi/ic_menu_save_light.png
new file mode 100644
index 00000000..a3a5e23a
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_menu_save_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_menu_search_dark.png b/app/src/main/res/drawable-hdpi/ic_menu_search_dark.png
new file mode 100644
index 00000000..ef2b3013
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_menu_search_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_menu_search_light.png b/app/src/main/res/drawable-hdpi/ic_menu_search_light.png
new file mode 100644
index 00000000..756937df
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_menu_search_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_menu_settings_dark.png b/app/src/main/res/drawable-hdpi/ic_menu_settings_dark.png
new file mode 100644
index 00000000..d6dd17ec
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_menu_settings_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_menu_settings_light.png b/app/src/main/res/drawable-hdpi/ic_menu_settings_light.png
new file mode 100644
index 00000000..70c29951
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_menu_settings_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_menu_share_dark.png b/app/src/main/res/drawable-hdpi/ic_menu_share_dark.png
new file mode 100644
index 00000000..218aa864
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_menu_share_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_menu_share_light.png b/app/src/main/res/drawable-hdpi/ic_menu_share_light.png
new file mode 100644
index 00000000..cfd19d43
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_menu_share_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_menu_shuffle_dark.png b/app/src/main/res/drawable-hdpi/ic_menu_shuffle_dark.png
new file mode 100644
index 00000000..f77cfed2
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_menu_shuffle_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_menu_shuffle_light.png b/app/src/main/res/drawable-hdpi/ic_menu_shuffle_light.png
new file mode 100644
index 00000000..ded93939
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_menu_shuffle_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_number_border.png b/app/src/main/res/drawable-hdpi/ic_number_border.png
new file mode 100644
index 00000000..d05aa7c2
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_number_border.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_social_person.png b/app/src/main/res/drawable-hdpi/ic_social_person.png
new file mode 100644
index 00000000..0a0a5ff2
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_social_person.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_stat_star.png b/app/src/main/res/drawable-hdpi/ic_stat_star.png
new file mode 100644
index 00000000..67ad40f5
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_stat_star.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/launch.png b/app/src/main/res/drawable-hdpi/launch.png
new file mode 100644
index 00000000..0c77b9b4
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/launch.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/main_offline_dark.png b/app/src/main/res/drawable-hdpi/main_offline_dark.png
new file mode 100644
index 00000000..a594530d
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/main_offline_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/main_offline_light.png b/app/src/main/res/drawable-hdpi/main_offline_light.png
new file mode 100644
index 00000000..cabca581
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/main_offline_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/main_select_server_dark.png b/app/src/main/res/drawable-hdpi/main_select_server_dark.png
new file mode 100644
index 00000000..e3a9dd5d
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/main_select_server_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/main_select_server_light.png b/app/src/main/res/drawable-hdpi/main_select_server_light.png
new file mode 100644
index 00000000..4606410d
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/main_select_server_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/media_backward_dark.png b/app/src/main/res/drawable-hdpi/media_backward_dark.png
new file mode 100644
index 00000000..b1dde4f5
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/media_backward_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/media_backward_light.png b/app/src/main/res/drawable-hdpi/media_backward_light.png
new file mode 100644
index 00000000..3e277267
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/media_backward_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/media_forward_dark.png b/app/src/main/res/drawable-hdpi/media_forward_dark.png
new file mode 100644
index 00000000..eb2546c7
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/media_forward_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/media_forward_light.png b/app/src/main/res/drawable-hdpi/media_forward_light.png
new file mode 100644
index 00000000..185e3c39
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/media_forward_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/media_pause_dark.png b/app/src/main/res/drawable-hdpi/media_pause_dark.png
new file mode 100644
index 00000000..b057588e
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/media_pause_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/media_pause_light.png b/app/src/main/res/drawable-hdpi/media_pause_light.png
new file mode 100644
index 00000000..e01815e3
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/media_pause_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/media_repeat_all.png b/app/src/main/res/drawable-hdpi/media_repeat_all.png
new file mode 100644
index 00000000..c2255058
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/media_repeat_all.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/media_repeat_off.png b/app/src/main/res/drawable-hdpi/media_repeat_off.png
new file mode 100644
index 00000000..10315ab3
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/media_repeat_off.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/media_repeat_off_light.png b/app/src/main/res/drawable-hdpi/media_repeat_off_light.png
new file mode 100644
index 00000000..39408bec
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/media_repeat_off_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/media_repeat_single.png b/app/src/main/res/drawable-hdpi/media_repeat_single.png
new file mode 100644
index 00000000..6d280e7a
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/media_repeat_single.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/media_start_dark.png b/app/src/main/res/drawable-hdpi/media_start_dark.png
new file mode 100644
index 00000000..dbfd337a
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/media_start_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/media_start_light.png b/app/src/main/res/drawable-hdpi/media_start_light.png
new file mode 100644
index 00000000..e4310efc
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/media_start_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/media_stop_dark.png b/app/src/main/res/drawable-hdpi/media_stop_dark.png
new file mode 100644
index 00000000..5ceb39f3
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/media_stop_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/media_stop_light.png b/app/src/main/res/drawable-hdpi/media_stop_light.png
new file mode 100644
index 00000000..8deca73e
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/media_stop_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/notification_close.png b/app/src/main/res/drawable-hdpi/notification_close.png
new file mode 100644
index 00000000..916c9a0f
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/notification_close.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/notification_next.png b/app/src/main/res/drawable-hdpi/notification_next.png
new file mode 100644
index 00000000..078c310f
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/notification_next.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/notification_pause.png b/app/src/main/res/drawable-hdpi/notification_pause.png
new file mode 100644
index 00000000..16627e44
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/notification_pause.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/notification_play.png b/app/src/main/res/drawable-hdpi/notification_play.png
new file mode 100644
index 00000000..02f38944
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/notification_play.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/notification_previous.png b/app/src/main/res/drawable-hdpi/notification_previous.png
new file mode 100644
index 00000000..9d10abd9
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/notification_previous.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/now_playing.png b/app/src/main/res/drawable-hdpi/now_playing.png
new file mode 100644
index 00000000..02f38944
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/now_playing.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/stat_notify_download.png b/app/src/main/res/drawable-hdpi/stat_notify_download.png
new file mode 100644
index 00000000..aa1b6c92
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/stat_notify_download.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/stat_notify_playing.png b/app/src/main/res/drawable-hdpi/stat_notify_playing.png
new file mode 100644
index 00000000..02f38944
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/stat_notify_playing.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/stat_notify_sync.png b/app/src/main/res/drawable-hdpi/stat_notify_sync.png
new file mode 100644
index 00000000..7dbf0e95
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/stat_notify_sync.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/toast_frame.9.png b/app/src/main/res/drawable-hdpi/toast_frame.9.png
new file mode 100644
index 00000000..8f5d8119
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/toast_frame.9.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/unknown_album.png b/app/src/main/res/drawable-hdpi/unknown_album.png
new file mode 100644
index 00000000..9b7844f4
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/unknown_album.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/unknown_album_large.png b/app/src/main/res/drawable-hdpi/unknown_album_large.png
new file mode 100644
index 00000000..42c28c7d
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/unknown_album_large.png
Binary files differ
diff --git a/app/src/main/res/drawable-large/unknown_album.png b/app/src/main/res/drawable-large/unknown_album.png
new file mode 100644
index 00000000..c4e32c61
--- /dev/null
+++ b/app/src/main/res/drawable-large/unknown_album.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi-v11/notification_close.png b/app/src/main/res/drawable-mdpi-v11/notification_close.png
new file mode 100644
index 00000000..a056fe61
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi-v11/notification_close.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi-v11/notification_next.png b/app/src/main/res/drawable-mdpi-v11/notification_next.png
new file mode 100644
index 00000000..7297577f
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi-v11/notification_next.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi-v11/notification_pause.png b/app/src/main/res/drawable-mdpi-v11/notification_pause.png
new file mode 100644
index 00000000..5d3ca3f2
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi-v11/notification_pause.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi-v11/notification_play.png b/app/src/main/res/drawable-mdpi-v11/notification_play.png
new file mode 100644
index 00000000..999ce798
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi-v11/notification_play.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi-v11/notification_previous.png b/app/src/main/res/drawable-mdpi-v11/notification_previous.png
new file mode 100644
index 00000000..55a1f326
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi-v11/notification_previous.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi-v11/stat_notify_download.png b/app/src/main/res/drawable-mdpi-v11/stat_notify_download.png
new file mode 100644
index 00000000..4164e0fa
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi-v11/stat_notify_download.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi-v11/stat_notify_playing.png b/app/src/main/res/drawable-mdpi-v11/stat_notify_playing.png
new file mode 100644
index 00000000..999ce798
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi-v11/stat_notify_playing.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi-v11/stat_notify_sync.png b/app/src/main/res/drawable-mdpi-v11/stat_notify_sync.png
new file mode 100644
index 00000000..3e3c64c0
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi-v11/stat_notify_sync.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/action_toggle_list_dark.png b/app/src/main/res/drawable-mdpi/action_toggle_list_dark.png
new file mode 100644
index 00000000..ace7fcee
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/action_toggle_list_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/action_toggle_list_light.png b/app/src/main/res/drawable-mdpi/action_toggle_list_light.png
new file mode 100644
index 00000000..fa6432da
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/action_toggle_list_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/download_cached.png b/app/src/main/res/drawable-mdpi/download_cached.png
new file mode 100644
index 00000000..2b5d33d1
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/download_cached.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/download_none_dark.png b/app/src/main/res/drawable-mdpi/download_none_dark.png
new file mode 100644
index 00000000..b6d614fc
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/download_none_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/download_none_light.png b/app/src/main/res/drawable-mdpi/download_none_light.png
new file mode 100644
index 00000000..2485c570
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/download_none_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/download_pinned.png b/app/src/main/res/drawable-mdpi/download_pinned.png
new file mode 100644
index 00000000..ce3fe064
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/download_pinned.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/downloading_dark.png b/app/src/main/res/drawable-mdpi/downloading_dark.png
new file mode 100644
index 00000000..ae6c5c9c
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/downloading_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/downloading_light.png b/app/src/main/res/drawable-mdpi/downloading_light.png
new file mode 100644
index 00000000..abd5b748
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/downloading_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_action_add_dark.png b/app/src/main/res/drawable-mdpi/ic_action_add_dark.png
new file mode 100644
index 00000000..a4c84f0f
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_action_add_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_action_add_light.png b/app/src/main/res/drawable-mdpi/ic_action_add_light.png
new file mode 100644
index 00000000..86097d84
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_action_add_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_action_album.png b/app/src/main/res/drawable-mdpi/ic_action_album.png
new file mode 100644
index 00000000..02674347
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_action_album.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_action_artist.png b/app/src/main/res/drawable-mdpi/ic_action_artist.png
new file mode 100644
index 00000000..c113cf78
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_action_artist.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_action_rating_bad_dark.png b/app/src/main/res/drawable-mdpi/ic_action_rating_bad_dark.png
new file mode 100644
index 00000000..64f3cd1f
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_action_rating_bad_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_action_rating_bad_light.png b/app/src/main/res/drawable-mdpi/ic_action_rating_bad_light.png
new file mode 100644
index 00000000..d6c8d42a
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_action_rating_bad_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_action_rating_bad_selected.png b/app/src/main/res/drawable-mdpi/ic_action_rating_bad_selected.png
new file mode 100644
index 00000000..34f5a9de
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_action_rating_bad_selected.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_action_rating_good_dark.png b/app/src/main/res/drawable-mdpi/ic_action_rating_good_dark.png
new file mode 100644
index 00000000..cadfbe1e
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_action_rating_good_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_action_rating_good_light.png b/app/src/main/res/drawable-mdpi/ic_action_rating_good_light.png
new file mode 100644
index 00000000..75711920
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_action_rating_good_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_action_rating_good_selected.png b/app/src/main/res/drawable-mdpi/ic_action_rating_good_selected.png
new file mode 100644
index 00000000..97d279be
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_action_rating_good_selected.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_action_song.png b/app/src/main/res/drawable-mdpi/ic_action_song.png
new file mode 100644
index 00000000..fa9acbde
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_action_song.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_action_volume_dark.png b/app/src/main/res/drawable-mdpi/ic_action_volume_dark.png
new file mode 100644
index 00000000..2b5f1d11
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_action_volume_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_action_volume_light.png b/app/src/main/res/drawable-mdpi/ic_action_volume_light.png
new file mode 100644
index 00000000..47071ccf
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_action_volume_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_menu_add_person_dark.png b/app/src/main/res/drawable-mdpi/ic_menu_add_person_dark.png
new file mode 100644
index 00000000..7d64f5d3
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_menu_add_person_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_menu_add_person_light.png b/app/src/main/res/drawable-mdpi/ic_menu_add_person_light.png
new file mode 100644
index 00000000..55c38c26
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_menu_add_person_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_menu_admin_dark.png b/app/src/main/res/drawable-mdpi/ic_menu_admin_dark.png
new file mode 100644
index 00000000..f88f5f15
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_menu_admin_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_menu_admin_light.png b/app/src/main/res/drawable-mdpi/ic_menu_admin_light.png
new file mode 100644
index 00000000..35cd14f4
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_menu_admin_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_menu_bookmark_dark.png b/app/src/main/res/drawable-mdpi/ic_menu_bookmark_dark.png
new file mode 100644
index 00000000..3360f37e
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_menu_bookmark_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_menu_bookmark_light.png b/app/src/main/res/drawable-mdpi/ic_menu_bookmark_light.png
new file mode 100644
index 00000000..b4d916fb
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_menu_bookmark_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_menu_bookmark_selected.png b/app/src/main/res/drawable-mdpi/ic_menu_bookmark_selected.png
new file mode 100644
index 00000000..efcc1afa
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_menu_bookmark_selected.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_menu_chat_dark.png b/app/src/main/res/drawable-mdpi/ic_menu_chat_dark.png
new file mode 100644
index 00000000..74d98888
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_menu_chat_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_menu_chat_light.png b/app/src/main/res/drawable-mdpi/ic_menu_chat_light.png
new file mode 100644
index 00000000..468c1220
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_menu_chat_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_menu_chat_send_dark.png b/app/src/main/res/drawable-mdpi/ic_menu_chat_send_dark.png
new file mode 100644
index 00000000..91db4a4a
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_menu_chat_send_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_menu_chat_send_light.png b/app/src/main/res/drawable-mdpi/ic_menu_chat_send_light.png
new file mode 100644
index 00000000..f2a3e724
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_menu_chat_send_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_menu_download_dark.png b/app/src/main/res/drawable-mdpi/ic_menu_download_dark.png
new file mode 100644
index 00000000..935bbd45
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_menu_download_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_menu_download_light.png b/app/src/main/res/drawable-mdpi/ic_menu_download_light.png
new file mode 100644
index 00000000..cc13d444
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_menu_download_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_menu_library_dark.png b/app/src/main/res/drawable-mdpi/ic_menu_library_dark.png
new file mode 100644
index 00000000..0102d7ad
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_menu_library_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_menu_library_light.png b/app/src/main/res/drawable-mdpi/ic_menu_library_light.png
new file mode 100644
index 00000000..a30b4d39
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_menu_library_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_menu_password_dark.png b/app/src/main/res/drawable-mdpi/ic_menu_password_dark.png
new file mode 100644
index 00000000..74d0095a
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_menu_password_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_menu_password_light.png b/app/src/main/res/drawable-mdpi/ic_menu_password_light.png
new file mode 100644
index 00000000..159f7889
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_menu_password_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_menu_playlist_dark.png b/app/src/main/res/drawable-mdpi/ic_menu_playlist_dark.png
new file mode 100644
index 00000000..ebf00427
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_menu_playlist_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_menu_playlist_light.png b/app/src/main/res/drawable-mdpi/ic_menu_playlist_light.png
new file mode 100644
index 00000000..e248a488
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_menu_playlist_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_menu_podcast_dark.png b/app/src/main/res/drawable-mdpi/ic_menu_podcast_dark.png
new file mode 100644
index 00000000..ad69156a
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_menu_podcast_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_menu_podcast_light.png b/app/src/main/res/drawable-mdpi/ic_menu_podcast_light.png
new file mode 100644
index 00000000..c15cb03f
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_menu_podcast_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_menu_radio_dark.png b/app/src/main/res/drawable-mdpi/ic_menu_radio_dark.png
new file mode 100644
index 00000000..bab20118
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_menu_radio_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_menu_radio_light.png b/app/src/main/res/drawable-mdpi/ic_menu_radio_light.png
new file mode 100644
index 00000000..72578d54
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_menu_radio_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_menu_refresh_dark.png b/app/src/main/res/drawable-mdpi/ic_menu_refresh_dark.png
new file mode 100644
index 00000000..554c07dc
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_menu_refresh_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_menu_refresh_light.png b/app/src/main/res/drawable-mdpi/ic_menu_refresh_light.png
new file mode 100644
index 00000000..a2d90c16
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_menu_refresh_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_menu_remove_dark.png b/app/src/main/res/drawable-mdpi/ic_menu_remove_dark.png
new file mode 100644
index 00000000..5ba24546
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_menu_remove_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_menu_remove_light.png b/app/src/main/res/drawable-mdpi/ic_menu_remove_light.png
new file mode 100644
index 00000000..93483b6c
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_menu_remove_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_menu_save_dark.png b/app/src/main/res/drawable-mdpi/ic_menu_save_dark.png
new file mode 100644
index 00000000..89aa17cc
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_menu_save_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_menu_save_light.png b/app/src/main/res/drawable-mdpi/ic_menu_save_light.png
new file mode 100644
index 00000000..dcb3a2f6
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_menu_save_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_menu_search_dark.png b/app/src/main/res/drawable-mdpi/ic_menu_search_dark.png
new file mode 100644
index 00000000..076085c5
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_menu_search_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_menu_search_light.png b/app/src/main/res/drawable-mdpi/ic_menu_search_light.png
new file mode 100644
index 00000000..026c8498
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_menu_search_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_menu_settings_dark.png b/app/src/main/res/drawable-mdpi/ic_menu_settings_dark.png
new file mode 100644
index 00000000..fc2bf8c3
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_menu_settings_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_menu_settings_light.png b/app/src/main/res/drawable-mdpi/ic_menu_settings_light.png
new file mode 100644
index 00000000..0e65c682
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_menu_settings_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_menu_share_dark.png b/app/src/main/res/drawable-mdpi/ic_menu_share_dark.png
new file mode 100644
index 00000000..c37aadba
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_menu_share_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_menu_share_light.png b/app/src/main/res/drawable-mdpi/ic_menu_share_light.png
new file mode 100644
index 00000000..72eeb598
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_menu_share_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_menu_shuffle_dark.png b/app/src/main/res/drawable-mdpi/ic_menu_shuffle_dark.png
new file mode 100644
index 00000000..7007fde5
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_menu_shuffle_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_menu_shuffle_light.png b/app/src/main/res/drawable-mdpi/ic_menu_shuffle_light.png
new file mode 100644
index 00000000..4d07c3b4
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_menu_shuffle_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_number_border.png b/app/src/main/res/drawable-mdpi/ic_number_border.png
new file mode 100644
index 00000000..212fabce
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_number_border.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_social_person.png b/app/src/main/res/drawable-mdpi/ic_social_person.png
new file mode 100644
index 00000000..c09313d8
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_social_person.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/launch.png b/app/src/main/res/drawable-mdpi/launch.png
new file mode 100644
index 00000000..88887e94
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/launch.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/main_offline_dark.png b/app/src/main/res/drawable-mdpi/main_offline_dark.png
new file mode 100644
index 00000000..4990fb8e
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/main_offline_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/main_offline_light.png b/app/src/main/res/drawable-mdpi/main_offline_light.png
new file mode 100644
index 00000000..e70ec1c2
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/main_offline_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/main_select_server_dark.png b/app/src/main/res/drawable-mdpi/main_select_server_dark.png
new file mode 100644
index 00000000..119b1573
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/main_select_server_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/main_select_server_light.png b/app/src/main/res/drawable-mdpi/main_select_server_light.png
new file mode 100644
index 00000000..7d8dad34
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/main_select_server_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/media_backward_dark.png b/app/src/main/res/drawable-mdpi/media_backward_dark.png
new file mode 100644
index 00000000..4f2233a1
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/media_backward_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/media_backward_light.png b/app/src/main/res/drawable-mdpi/media_backward_light.png
new file mode 100644
index 00000000..425f2df7
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/media_backward_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/media_forward_dark.png b/app/src/main/res/drawable-mdpi/media_forward_dark.png
new file mode 100644
index 00000000..1641c0fa
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/media_forward_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/media_forward_light.png b/app/src/main/res/drawable-mdpi/media_forward_light.png
new file mode 100644
index 00000000..2e66868f
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/media_forward_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/media_pause_dark.png b/app/src/main/res/drawable-mdpi/media_pause_dark.png
new file mode 100644
index 00000000..3580dab4
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/media_pause_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/media_pause_light.png b/app/src/main/res/drawable-mdpi/media_pause_light.png
new file mode 100644
index 00000000..7e9ade73
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/media_pause_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/media_start_dark.png b/app/src/main/res/drawable-mdpi/media_start_dark.png
new file mode 100644
index 00000000..a2f198ae
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/media_start_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/media_start_light.png b/app/src/main/res/drawable-mdpi/media_start_light.png
new file mode 100644
index 00000000..d69107ba
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/media_start_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/media_stop_dark.png b/app/src/main/res/drawable-mdpi/media_stop_dark.png
new file mode 100644
index 00000000..944482e6
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/media_stop_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/media_stop_light.png b/app/src/main/res/drawable-mdpi/media_stop_light.png
new file mode 100644
index 00000000..ff1932a1
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/media_stop_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/notification_close.png b/app/src/main/res/drawable-mdpi/notification_close.png
new file mode 100644
index 00000000..2a8f9a36
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/notification_close.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/notification_next.png b/app/src/main/res/drawable-mdpi/notification_next.png
new file mode 100644
index 00000000..f85d45a5
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/notification_next.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/notification_pause.png b/app/src/main/res/drawable-mdpi/notification_pause.png
new file mode 100644
index 00000000..06c3cf9d
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/notification_pause.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/notification_play.png b/app/src/main/res/drawable-mdpi/notification_play.png
new file mode 100644
index 00000000..0248c1cc
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/notification_play.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/notification_previous.png b/app/src/main/res/drawable-mdpi/notification_previous.png
new file mode 100644
index 00000000..167d7d05
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/notification_previous.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/now_playing.png b/app/src/main/res/drawable-mdpi/now_playing.png
new file mode 100644
index 00000000..0248c1cc
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/now_playing.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/stat_notify_download.png b/app/src/main/res/drawable-mdpi/stat_notify_download.png
new file mode 100644
index 00000000..4c2a22de
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/stat_notify_download.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/stat_notify_playing.png b/app/src/main/res/drawable-mdpi/stat_notify_playing.png
new file mode 100644
index 00000000..0248c1cc
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/stat_notify_playing.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/stat_notify_sync.png b/app/src/main/res/drawable-mdpi/stat_notify_sync.png
new file mode 100644
index 00000000..35a06857
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/stat_notify_sync.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi-v11/notification_close.png b/app/src/main/res/drawable-xhdpi-v11/notification_close.png
new file mode 100644
index 00000000..f1013578
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi-v11/notification_close.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi-v11/notification_next.png b/app/src/main/res/drawable-xhdpi-v11/notification_next.png
new file mode 100644
index 00000000..ad070680
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi-v11/notification_next.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi-v11/notification_pause.png b/app/src/main/res/drawable-xhdpi-v11/notification_pause.png
new file mode 100644
index 00000000..709602aa
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi-v11/notification_pause.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi-v11/notification_play.png b/app/src/main/res/drawable-xhdpi-v11/notification_play.png
new file mode 100644
index 00000000..e2bafa6a
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi-v11/notification_play.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi-v11/notification_previous.png b/app/src/main/res/drawable-xhdpi-v11/notification_previous.png
new file mode 100644
index 00000000..d22488cb
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi-v11/notification_previous.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi-v11/stat_notify_download.png b/app/src/main/res/drawable-xhdpi-v11/stat_notify_download.png
new file mode 100644
index 00000000..96ceb383
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi-v11/stat_notify_download.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi-v11/stat_notify_playing.png b/app/src/main/res/drawable-xhdpi-v11/stat_notify_playing.png
new file mode 100644
index 00000000..e2bafa6a
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi-v11/stat_notify_playing.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi-v11/stat_notify_sync.png b/app/src/main/res/drawable-xhdpi-v11/stat_notify_sync.png
new file mode 100644
index 00000000..b723bf54
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi-v11/stat_notify_sync.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/action_toggle_list_dark.png b/app/src/main/res/drawable-xhdpi/action_toggle_list_dark.png
new file mode 100644
index 00000000..92003c6b
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/action_toggle_list_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/action_toggle_list_light.png b/app/src/main/res/drawable-xhdpi/action_toggle_list_light.png
new file mode 100644
index 00000000..a4007ea5
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/action_toggle_list_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/download_cached.png b/app/src/main/res/drawable-xhdpi/download_cached.png
new file mode 100644
index 00000000..70de6f04
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/download_cached.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/download_none_dark.png b/app/src/main/res/drawable-xhdpi/download_none_dark.png
new file mode 100644
index 00000000..7be3c2a4
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/download_none_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/download_none_light.png b/app/src/main/res/drawable-xhdpi/download_none_light.png
new file mode 100644
index 00000000..817651d7
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/download_none_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/download_pinned.png b/app/src/main/res/drawable-xhdpi/download_pinned.png
new file mode 100644
index 00000000..79bf92e3
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/download_pinned.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/downloading_dark.png b/app/src/main/res/drawable-xhdpi/downloading_dark.png
new file mode 100644
index 00000000..3f14bdf4
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/downloading_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/downloading_light.png b/app/src/main/res/drawable-xhdpi/downloading_light.png
new file mode 100644
index 00000000..643c15d0
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/downloading_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_action_add_dark.png b/app/src/main/res/drawable-xhdpi/ic_action_add_dark.png
new file mode 100644
index 00000000..93eae7c6
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_action_add_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_action_add_light.png b/app/src/main/res/drawable-xhdpi/ic_action_add_light.png
new file mode 100644
index 00000000..1ebdb432
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_action_add_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_action_album.png b/app/src/main/res/drawable-xhdpi/ic_action_album.png
new file mode 100644
index 00000000..e4b12908
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_action_album.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_action_artist.png b/app/src/main/res/drawable-xhdpi/ic_action_artist.png
new file mode 100644
index 00000000..2dff43ea
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_action_artist.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_action_rating_bad_dark.png b/app/src/main/res/drawable-xhdpi/ic_action_rating_bad_dark.png
new file mode 100644
index 00000000..1393be0c
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_action_rating_bad_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_action_rating_bad_light.png b/app/src/main/res/drawable-xhdpi/ic_action_rating_bad_light.png
new file mode 100644
index 00000000..fc1959b6
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_action_rating_bad_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_action_rating_bad_selected.png b/app/src/main/res/drawable-xhdpi/ic_action_rating_bad_selected.png
new file mode 100644
index 00000000..cf7802d5
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_action_rating_bad_selected.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_action_rating_good_dark.png b/app/src/main/res/drawable-xhdpi/ic_action_rating_good_dark.png
new file mode 100644
index 00000000..249ea9ec
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_action_rating_good_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_action_rating_good_light.png b/app/src/main/res/drawable-xhdpi/ic_action_rating_good_light.png
new file mode 100644
index 00000000..c8a776b0
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_action_rating_good_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_action_rating_good_selected.png b/app/src/main/res/drawable-xhdpi/ic_action_rating_good_selected.png
new file mode 100644
index 00000000..c6770221
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_action_rating_good_selected.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_action_song.png b/app/src/main/res/drawable-xhdpi/ic_action_song.png
new file mode 100644
index 00000000..29fd3a2d
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_action_song.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_action_volume_dark.png b/app/src/main/res/drawable-xhdpi/ic_action_volume_dark.png
new file mode 100644
index 00000000..400de38b
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_action_volume_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_action_volume_light.png b/app/src/main/res/drawable-xhdpi/ic_action_volume_light.png
new file mode 100644
index 00000000..9a1128c1
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_action_volume_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_add_person_dark.png b/app/src/main/res/drawable-xhdpi/ic_menu_add_person_dark.png
new file mode 100644
index 00000000..30c78e5a
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_menu_add_person_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_add_person_light.png b/app/src/main/res/drawable-xhdpi/ic_menu_add_person_light.png
new file mode 100644
index 00000000..b3fb3808
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_menu_add_person_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_admin_dark.png b/app/src/main/res/drawable-xhdpi/ic_menu_admin_dark.png
new file mode 100644
index 00000000..09f90c15
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_menu_admin_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_admin_light.png b/app/src/main/res/drawable-xhdpi/ic_menu_admin_light.png
new file mode 100644
index 00000000..4bd3beaf
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_menu_admin_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_bookmark_dark.png b/app/src/main/res/drawable-xhdpi/ic_menu_bookmark_dark.png
new file mode 100644
index 00000000..18f71365
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_menu_bookmark_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_bookmark_light.png b/app/src/main/res/drawable-xhdpi/ic_menu_bookmark_light.png
new file mode 100644
index 00000000..d5776317
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_menu_bookmark_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_bookmark_selected.png b/app/src/main/res/drawable-xhdpi/ic_menu_bookmark_selected.png
new file mode 100644
index 00000000..353b7b79
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_menu_bookmark_selected.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_chat_dark.png b/app/src/main/res/drawable-xhdpi/ic_menu_chat_dark.png
new file mode 100644
index 00000000..28318219
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_menu_chat_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_chat_light.png b/app/src/main/res/drawable-xhdpi/ic_menu_chat_light.png
new file mode 100644
index 00000000..dcc95dcb
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_menu_chat_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_chat_send_dark.png b/app/src/main/res/drawable-xhdpi/ic_menu_chat_send_dark.png
new file mode 100644
index 00000000..c0a5a3eb
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_menu_chat_send_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_chat_send_light.png b/app/src/main/res/drawable-xhdpi/ic_menu_chat_send_light.png
new file mode 100644
index 00000000..f9c3b9bb
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_menu_chat_send_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_download_dark.png b/app/src/main/res/drawable-xhdpi/ic_menu_download_dark.png
new file mode 100644
index 00000000..6b6c65df
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_menu_download_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_download_light.png b/app/src/main/res/drawable-xhdpi/ic_menu_download_light.png
new file mode 100644
index 00000000..c8caf90b
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_menu_download_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_library_dark.png b/app/src/main/res/drawable-xhdpi/ic_menu_library_dark.png
new file mode 100644
index 00000000..b1612f65
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_menu_library_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_library_light.png b/app/src/main/res/drawable-xhdpi/ic_menu_library_light.png
new file mode 100644
index 00000000..1f93c8f2
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_menu_library_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_password_dark.png b/app/src/main/res/drawable-xhdpi/ic_menu_password_dark.png
new file mode 100644
index 00000000..d1fc0a97
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_menu_password_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_password_light.png b/app/src/main/res/drawable-xhdpi/ic_menu_password_light.png
new file mode 100644
index 00000000..1cbf085c
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_menu_password_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_playlist_dark.png b/app/src/main/res/drawable-xhdpi/ic_menu_playlist_dark.png
new file mode 100644
index 00000000..fd6cd498
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_menu_playlist_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_playlist_light.png b/app/src/main/res/drawable-xhdpi/ic_menu_playlist_light.png
new file mode 100644
index 00000000..e7e510d0
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_menu_playlist_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_podcast_dark.png b/app/src/main/res/drawable-xhdpi/ic_menu_podcast_dark.png
new file mode 100644
index 00000000..40469b46
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_menu_podcast_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_podcast_light.png b/app/src/main/res/drawable-xhdpi/ic_menu_podcast_light.png
new file mode 100644
index 00000000..3748526a
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_menu_podcast_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_radio_dark.png b/app/src/main/res/drawable-xhdpi/ic_menu_radio_dark.png
new file mode 100644
index 00000000..3a4114a3
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_menu_radio_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_radio_light.png b/app/src/main/res/drawable-xhdpi/ic_menu_radio_light.png
new file mode 100644
index 00000000..5bcc9261
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_menu_radio_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_refresh_dark.png b/app/src/main/res/drawable-xhdpi/ic_menu_refresh_dark.png
new file mode 100644
index 00000000..b6801006
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_menu_refresh_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_refresh_light.png b/app/src/main/res/drawable-xhdpi/ic_menu_refresh_light.png
new file mode 100644
index 00000000..38943f82
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_menu_refresh_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_remove_dark.png b/app/src/main/res/drawable-xhdpi/ic_menu_remove_dark.png
new file mode 100644
index 00000000..09ce75e2
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_menu_remove_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_remove_light.png b/app/src/main/res/drawable-xhdpi/ic_menu_remove_light.png
new file mode 100644
index 00000000..94f7c8c1
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_menu_remove_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_save_dark.png b/app/src/main/res/drawable-xhdpi/ic_menu_save_dark.png
new file mode 100644
index 00000000..1612fd0a
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_menu_save_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_save_light.png b/app/src/main/res/drawable-xhdpi/ic_menu_save_light.png
new file mode 100644
index 00000000..5dcd75d7
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_menu_save_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_search_dark.png b/app/src/main/res/drawable-xhdpi/ic_menu_search_dark.png
new file mode 100644
index 00000000..1ae3dff0
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_menu_search_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_search_light.png b/app/src/main/res/drawable-xhdpi/ic_menu_search_light.png
new file mode 100644
index 00000000..705074bd
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_menu_search_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_settings_dark.png b/app/src/main/res/drawable-xhdpi/ic_menu_settings_dark.png
new file mode 100644
index 00000000..ae917587
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_menu_settings_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_settings_light.png b/app/src/main/res/drawable-xhdpi/ic_menu_settings_light.png
new file mode 100644
index 00000000..29f961b2
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_menu_settings_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_share_dark.png b/app/src/main/res/drawable-xhdpi/ic_menu_share_dark.png
new file mode 100644
index 00000000..41073d1f
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_menu_share_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_share_light.png b/app/src/main/res/drawable-xhdpi/ic_menu_share_light.png
new file mode 100644
index 00000000..36f9f55f
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_menu_share_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_shuffle_dark.png b/app/src/main/res/drawable-xhdpi/ic_menu_shuffle_dark.png
new file mode 100644
index 00000000..e3a31a84
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_menu_shuffle_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_shuffle_light.png b/app/src/main/res/drawable-xhdpi/ic_menu_shuffle_light.png
new file mode 100644
index 00000000..14eb942c
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_menu_shuffle_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_number_border.png b/app/src/main/res/drawable-xhdpi/ic_number_border.png
new file mode 100644
index 00000000..1b370fbd
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_number_border.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_social_person.png b/app/src/main/res/drawable-xhdpi/ic_social_person.png
new file mode 100644
index 00000000..ed333afe
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_social_person.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/launch.png b/app/src/main/res/drawable-xhdpi/launch.png
new file mode 100644
index 00000000..0f647dfa
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/launch.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/main_offline_dark.png b/app/src/main/res/drawable-xhdpi/main_offline_dark.png
new file mode 100644
index 00000000..231e4715
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/main_offline_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/main_offline_light.png b/app/src/main/res/drawable-xhdpi/main_offline_light.png
new file mode 100644
index 00000000..87937fcb
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/main_offline_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/main_select_server_dark.png b/app/src/main/res/drawable-xhdpi/main_select_server_dark.png
new file mode 100644
index 00000000..b84f1851
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/main_select_server_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/main_select_server_light.png b/app/src/main/res/drawable-xhdpi/main_select_server_light.png
new file mode 100644
index 00000000..ee154cc7
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/main_select_server_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/media_backward_dark.png b/app/src/main/res/drawable-xhdpi/media_backward_dark.png
new file mode 100644
index 00000000..3c9921a8
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/media_backward_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/media_backward_light.png b/app/src/main/res/drawable-xhdpi/media_backward_light.png
new file mode 100644
index 00000000..aafd76fa
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/media_backward_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/media_forward_dark.png b/app/src/main/res/drawable-xhdpi/media_forward_dark.png
new file mode 100644
index 00000000..b082b3a6
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/media_forward_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/media_forward_light.png b/app/src/main/res/drawable-xhdpi/media_forward_light.png
new file mode 100644
index 00000000..20772843
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/media_forward_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/media_pause_dark.png b/app/src/main/res/drawable-xhdpi/media_pause_dark.png
new file mode 100644
index 00000000..aafdd4aa
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/media_pause_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/media_pause_light.png b/app/src/main/res/drawable-xhdpi/media_pause_light.png
new file mode 100644
index 00000000..2639777d
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/media_pause_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/media_start_dark.png b/app/src/main/res/drawable-xhdpi/media_start_dark.png
new file mode 100644
index 00000000..9e63c90b
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/media_start_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/media_start_light.png b/app/src/main/res/drawable-xhdpi/media_start_light.png
new file mode 100644
index 00000000..2ff8c399
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/media_start_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/media_stop_dark.png b/app/src/main/res/drawable-xhdpi/media_stop_dark.png
new file mode 100644
index 00000000..9cb32909
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/media_stop_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/media_stop_light.png b/app/src/main/res/drawable-xhdpi/media_stop_light.png
new file mode 100644
index 00000000..edf13ccf
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/media_stop_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/notification_close.png b/app/src/main/res/drawable-xhdpi/notification_close.png
new file mode 100644
index 00000000..4230842e
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/notification_close.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/notification_next.png b/app/src/main/res/drawable-xhdpi/notification_next.png
new file mode 100644
index 00000000..44dbbd12
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/notification_next.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/notification_pause.png b/app/src/main/res/drawable-xhdpi/notification_pause.png
new file mode 100644
index 00000000..e8d8c535
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/notification_pause.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/notification_play.png b/app/src/main/res/drawable-xhdpi/notification_play.png
new file mode 100644
index 00000000..532041fa
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/notification_play.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/notification_previous.png b/app/src/main/res/drawable-xhdpi/notification_previous.png
new file mode 100644
index 00000000..87ee8d2f
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/notification_previous.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/now_playing.png b/app/src/main/res/drawable-xhdpi/now_playing.png
new file mode 100644
index 00000000..532041fa
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/now_playing.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/stat_notify_download.png b/app/src/main/res/drawable-xhdpi/stat_notify_download.png
new file mode 100644
index 00000000..bd4cb567
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/stat_notify_download.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/stat_notify_playing.png b/app/src/main/res/drawable-xhdpi/stat_notify_playing.png
new file mode 100644
index 00000000..532041fa
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/stat_notify_playing.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/stat_notify_sync.png b/app/src/main/res/drawable-xhdpi/stat_notify_sync.png
new file mode 100644
index 00000000..6da882a2
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/stat_notify_sync.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi-v11/notification_close.png b/app/src/main/res/drawable-xxhdpi-v11/notification_close.png
new file mode 100644
index 00000000..c3ac026a
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi-v11/notification_close.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi-v11/notification_next.png b/app/src/main/res/drawable-xxhdpi-v11/notification_next.png
new file mode 100644
index 00000000..06911082
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi-v11/notification_next.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi-v11/notification_pause.png b/app/src/main/res/drawable-xxhdpi-v11/notification_pause.png
new file mode 100644
index 00000000..1513f9d9
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi-v11/notification_pause.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi-v11/notification_play.png b/app/src/main/res/drawable-xxhdpi-v11/notification_play.png
new file mode 100644
index 00000000..9138a760
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi-v11/notification_play.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi-v11/notification_previous.png b/app/src/main/res/drawable-xxhdpi-v11/notification_previous.png
new file mode 100644
index 00000000..b4456c16
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi-v11/notification_previous.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi-v11/stat_notify_download.png b/app/src/main/res/drawable-xxhdpi-v11/stat_notify_download.png
new file mode 100644
index 00000000..b2dc5651
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi-v11/stat_notify_download.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi-v11/stat_notify_playing.png b/app/src/main/res/drawable-xxhdpi-v11/stat_notify_playing.png
new file mode 100644
index 00000000..9138a760
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi-v11/stat_notify_playing.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi-v11/stat_notify_sync.png b/app/src/main/res/drawable-xxhdpi-v11/stat_notify_sync.png
new file mode 100644
index 00000000..61f6a331
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi-v11/stat_notify_sync.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/action_toggle_list_dark.png b/app/src/main/res/drawable-xxhdpi/action_toggle_list_dark.png
new file mode 100644
index 00000000..598fc312
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/action_toggle_list_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/action_toggle_list_light.png b/app/src/main/res/drawable-xxhdpi/action_toggle_list_light.png
new file mode 100644
index 00000000..ceb3fade
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/action_toggle_list_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/download_cached.png b/app/src/main/res/drawable-xxhdpi/download_cached.png
new file mode 100644
index 00000000..243c570e
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/download_cached.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/download_none_dark.png b/app/src/main/res/drawable-xxhdpi/download_none_dark.png
new file mode 100644
index 00000000..a0cb8a41
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/download_none_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/download_none_light.png b/app/src/main/res/drawable-xxhdpi/download_none_light.png
new file mode 100644
index 00000000..7a1639ef
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/download_none_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/download_pinned.png b/app/src/main/res/drawable-xxhdpi/download_pinned.png
new file mode 100644
index 00000000..354ac23d
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/download_pinned.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/downloading_dark.png b/app/src/main/res/drawable-xxhdpi/downloading_dark.png
new file mode 100644
index 00000000..afc4bf84
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/downloading_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/downloading_light.png b/app/src/main/res/drawable-xxhdpi/downloading_light.png
new file mode 100644
index 00000000..ba31a979
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/downloading_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_add_dark.png b/app/src/main/res/drawable-xxhdpi/ic_action_add_dark.png
new file mode 100644
index 00000000..70495672
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_action_add_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_add_light.png b/app/src/main/res/drawable-xxhdpi/ic_action_add_light.png
new file mode 100644
index 00000000..9322b136
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_action_add_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_rating_bad_dark.png b/app/src/main/res/drawable-xxhdpi/ic_action_rating_bad_dark.png
new file mode 100644
index 00000000..d784b239
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_action_rating_bad_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_rating_bad_light.png b/app/src/main/res/drawable-xxhdpi/ic_action_rating_bad_light.png
new file mode 100644
index 00000000..a1484d25
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_action_rating_bad_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_rating_bad_selected.png b/app/src/main/res/drawable-xxhdpi/ic_action_rating_bad_selected.png
new file mode 100644
index 00000000..13218a08
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_action_rating_bad_selected.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_rating_good_dark.png b/app/src/main/res/drawable-xxhdpi/ic_action_rating_good_dark.png
new file mode 100644
index 00000000..a332a632
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_action_rating_good_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_rating_good_light.png b/app/src/main/res/drawable-xxhdpi/ic_action_rating_good_light.png
new file mode 100644
index 00000000..2ef75765
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_action_rating_good_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_rating_good_selected.png b/app/src/main/res/drawable-xxhdpi/ic_action_rating_good_selected.png
new file mode 100644
index 00000000..bb444806
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_action_rating_good_selected.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_volume_dark.png b/app/src/main/res/drawable-xxhdpi/ic_action_volume_dark.png
new file mode 100644
index 00000000..7991a65d
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_action_volume_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_volume_light.png b/app/src/main/res/drawable-xxhdpi/ic_action_volume_light.png
new file mode 100644
index 00000000..8dfbf3f5
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_action_volume_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_add_person_dark.png b/app/src/main/res/drawable-xxhdpi/ic_menu_add_person_dark.png
new file mode 100644
index 00000000..446985ea
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_menu_add_person_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_add_person_light.png b/app/src/main/res/drawable-xxhdpi/ic_menu_add_person_light.png
new file mode 100644
index 00000000..0f1d36bc
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_menu_add_person_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_admin_dark.png b/app/src/main/res/drawable-xxhdpi/ic_menu_admin_dark.png
new file mode 100644
index 00000000..0e57c9ed
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_menu_admin_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_admin_light.png b/app/src/main/res/drawable-xxhdpi/ic_menu_admin_light.png
new file mode 100644
index 00000000..63ab2f83
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_menu_admin_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_bookmark_dark.png b/app/src/main/res/drawable-xxhdpi/ic_menu_bookmark_dark.png
new file mode 100644
index 00000000..2523e14c
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_menu_bookmark_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_bookmark_light.png b/app/src/main/res/drawable-xxhdpi/ic_menu_bookmark_light.png
new file mode 100644
index 00000000..9e8c4591
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_menu_bookmark_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_bookmark_selected.png b/app/src/main/res/drawable-xxhdpi/ic_menu_bookmark_selected.png
new file mode 100644
index 00000000..a1890fbb
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_menu_bookmark_selected.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_chat_dark.png b/app/src/main/res/drawable-xxhdpi/ic_menu_chat_dark.png
new file mode 100644
index 00000000..60efb47d
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_menu_chat_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_chat_light.png b/app/src/main/res/drawable-xxhdpi/ic_menu_chat_light.png
new file mode 100644
index 00000000..02c89560
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_menu_chat_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_chat_send_dark.png b/app/src/main/res/drawable-xxhdpi/ic_menu_chat_send_dark.png
new file mode 100644
index 00000000..b86ca3d3
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_menu_chat_send_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_chat_send_light.png b/app/src/main/res/drawable-xxhdpi/ic_menu_chat_send_light.png
new file mode 100644
index 00000000..048b8aac
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_menu_chat_send_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_download_dark.png b/app/src/main/res/drawable-xxhdpi/ic_menu_download_dark.png
new file mode 100644
index 00000000..03ddef9f
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_menu_download_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_download_light.png b/app/src/main/res/drawable-xxhdpi/ic_menu_download_light.png
new file mode 100644
index 00000000..c487580c
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_menu_download_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_library_dark.png b/app/src/main/res/drawable-xxhdpi/ic_menu_library_dark.png
new file mode 100644
index 00000000..02a4f3f2
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_menu_library_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_library_light.png b/app/src/main/res/drawable-xxhdpi/ic_menu_library_light.png
new file mode 100644
index 00000000..52ce8203
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_menu_library_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_password_dark.png b/app/src/main/res/drawable-xxhdpi/ic_menu_password_dark.png
new file mode 100644
index 00000000..a7cd1a6d
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_menu_password_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_password_light.png b/app/src/main/res/drawable-xxhdpi/ic_menu_password_light.png
new file mode 100644
index 00000000..5670a209
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_menu_password_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_playlist_dark.png b/app/src/main/res/drawable-xxhdpi/ic_menu_playlist_dark.png
new file mode 100644
index 00000000..2c955eee
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_menu_playlist_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_playlist_light.png b/app/src/main/res/drawable-xxhdpi/ic_menu_playlist_light.png
new file mode 100644
index 00000000..d1877328
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_menu_playlist_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_podcast_dark.png b/app/src/main/res/drawable-xxhdpi/ic_menu_podcast_dark.png
new file mode 100644
index 00000000..a748dc60
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_menu_podcast_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_podcast_light.png b/app/src/main/res/drawable-xxhdpi/ic_menu_podcast_light.png
new file mode 100644
index 00000000..efa7b037
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_menu_podcast_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_radio_dark.png b/app/src/main/res/drawable-xxhdpi/ic_menu_radio_dark.png
new file mode 100644
index 00000000..0c63afbe
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_menu_radio_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_radio_light.png b/app/src/main/res/drawable-xxhdpi/ic_menu_radio_light.png
new file mode 100644
index 00000000..133772f8
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_menu_radio_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_refresh_dark.png b/app/src/main/res/drawable-xxhdpi/ic_menu_refresh_dark.png
new file mode 100644
index 00000000..0e5616bd
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_menu_refresh_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_refresh_light.png b/app/src/main/res/drawable-xxhdpi/ic_menu_refresh_light.png
new file mode 100644
index 00000000..7dea70df
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_menu_refresh_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_remove_dark.png b/app/src/main/res/drawable-xxhdpi/ic_menu_remove_dark.png
new file mode 100644
index 00000000..d5952ea0
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_menu_remove_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_remove_light.png b/app/src/main/res/drawable-xxhdpi/ic_menu_remove_light.png
new file mode 100644
index 00000000..c814869e
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_menu_remove_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_save_dark.png b/app/src/main/res/drawable-xxhdpi/ic_menu_save_dark.png
new file mode 100644
index 00000000..acb264ec
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_menu_save_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_save_light.png b/app/src/main/res/drawable-xxhdpi/ic_menu_save_light.png
new file mode 100644
index 00000000..fcd18ccd
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_menu_save_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_search_dark.png b/app/src/main/res/drawable-xxhdpi/ic_menu_search_dark.png
new file mode 100644
index 00000000..500ac03a
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_menu_search_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_search_light.png b/app/src/main/res/drawable-xxhdpi/ic_menu_search_light.png
new file mode 100644
index 00000000..fa64f9e8
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_menu_search_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_settings_dark.png b/app/src/main/res/drawable-xxhdpi/ic_menu_settings_dark.png
new file mode 100644
index 00000000..ded5dbb5
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_menu_settings_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_settings_light.png b/app/src/main/res/drawable-xxhdpi/ic_menu_settings_light.png
new file mode 100644
index 00000000..cd242306
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_menu_settings_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_share_dark.png b/app/src/main/res/drawable-xxhdpi/ic_menu_share_dark.png
new file mode 100644
index 00000000..1fa12609
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_menu_share_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_share_light.png b/app/src/main/res/drawable-xxhdpi/ic_menu_share_light.png
new file mode 100644
index 00000000..7511340b
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_menu_share_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_shuffle_dark.png b/app/src/main/res/drawable-xxhdpi/ic_menu_shuffle_dark.png
new file mode 100644
index 00000000..b53733df
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_menu_shuffle_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_shuffle_light.png b/app/src/main/res/drawable-xxhdpi/ic_menu_shuffle_light.png
new file mode 100644
index 00000000..4d5dff32
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_menu_shuffle_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_number_border.png b/app/src/main/res/drawable-xxhdpi/ic_number_border.png
new file mode 100644
index 00000000..caf4ca23
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_number_border.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_social_person.png b/app/src/main/res/drawable-xxhdpi/ic_social_person.png
new file mode 100644
index 00000000..f81dc6a4
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_social_person.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/launch.png b/app/src/main/res/drawable-xxhdpi/launch.png
new file mode 100644
index 00000000..cae4f99a
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/launch.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/main_offline_dark.png b/app/src/main/res/drawable-xxhdpi/main_offline_dark.png
new file mode 100644
index 00000000..c415e0a5
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/main_offline_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/main_offline_light.png b/app/src/main/res/drawable-xxhdpi/main_offline_light.png
new file mode 100644
index 00000000..b7e1c380
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/main_offline_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/main_select_server_dark.png b/app/src/main/res/drawable-xxhdpi/main_select_server_dark.png
new file mode 100644
index 00000000..b85e3a1e
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/main_select_server_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/main_select_server_light.png b/app/src/main/res/drawable-xxhdpi/main_select_server_light.png
new file mode 100644
index 00000000..8fc39eff
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/main_select_server_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/media_backward_dark.png b/app/src/main/res/drawable-xxhdpi/media_backward_dark.png
new file mode 100644
index 00000000..5b6c6148
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/media_backward_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/media_backward_light.png b/app/src/main/res/drawable-xxhdpi/media_backward_light.png
new file mode 100644
index 00000000..32f7d3bc
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/media_backward_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/media_forward_dark.png b/app/src/main/res/drawable-xxhdpi/media_forward_dark.png
new file mode 100644
index 00000000..ca4ee295
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/media_forward_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/media_forward_light.png b/app/src/main/res/drawable-xxhdpi/media_forward_light.png
new file mode 100644
index 00000000..208e46e9
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/media_forward_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/media_pause_dark.png b/app/src/main/res/drawable-xxhdpi/media_pause_dark.png
new file mode 100644
index 00000000..4b5aacbc
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/media_pause_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/media_pause_light.png b/app/src/main/res/drawable-xxhdpi/media_pause_light.png
new file mode 100644
index 00000000..111f6d00
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/media_pause_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/media_start_dark.png b/app/src/main/res/drawable-xxhdpi/media_start_dark.png
new file mode 100644
index 00000000..641ad544
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/media_start_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/media_start_light.png b/app/src/main/res/drawable-xxhdpi/media_start_light.png
new file mode 100644
index 00000000..a6286203
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/media_start_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/media_stop_dark.png b/app/src/main/res/drawable-xxhdpi/media_stop_dark.png
new file mode 100644
index 00000000..9a9c432a
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/media_stop_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/media_stop_light.png b/app/src/main/res/drawable-xxhdpi/media_stop_light.png
new file mode 100644
index 00000000..79eb8d95
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/media_stop_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/notification_close.png b/app/src/main/res/drawable-xxhdpi/notification_close.png
new file mode 100644
index 00000000..022a6780
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/notification_close.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/notification_next.png b/app/src/main/res/drawable-xxhdpi/notification_next.png
new file mode 100644
index 00000000..dfe129db
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/notification_next.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/notification_pause.png b/app/src/main/res/drawable-xxhdpi/notification_pause.png
new file mode 100644
index 00000000..9c952207
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/notification_pause.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/notification_play.png b/app/src/main/res/drawable-xxhdpi/notification_play.png
new file mode 100644
index 00000000..4ee0a5eb
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/notification_play.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/notification_previous.png b/app/src/main/res/drawable-xxhdpi/notification_previous.png
new file mode 100644
index 00000000..e6908126
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/notification_previous.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/now_playing.png b/app/src/main/res/drawable-xxhdpi/now_playing.png
new file mode 100644
index 00000000..4ee0a5eb
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/now_playing.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/stat_notify_download.png b/app/src/main/res/drawable-xxhdpi/stat_notify_download.png
new file mode 100644
index 00000000..9d9a7f3e
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/stat_notify_download.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/stat_notify_playing.png b/app/src/main/res/drawable-xxhdpi/stat_notify_playing.png
new file mode 100644
index 00000000..4ee0a5eb
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/stat_notify_playing.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/stat_notify_sync.png b/app/src/main/res/drawable-xxhdpi/stat_notify_sync.png
new file mode 100644
index 00000000..89fe6525
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/stat_notify_sync.png
Binary files differ
diff --git a/app/src/main/res/drawable/appwidget4x1_preview.png b/app/src/main/res/drawable/appwidget4x1_preview.png
new file mode 100644
index 00000000..eb1fabc6
--- /dev/null
+++ b/app/src/main/res/drawable/appwidget4x1_preview.png
Binary files differ
diff --git a/app/src/main/res/drawable/appwidget4x2_preview.png b/app/src/main/res/drawable/appwidget4x2_preview.png
new file mode 100644
index 00000000..704dc09e
--- /dev/null
+++ b/app/src/main/res/drawable/appwidget4x2_preview.png
Binary files differ
diff --git a/app/src/main/res/drawable/appwidget4x3_preview.png b/app/src/main/res/drawable/appwidget4x3_preview.png
new file mode 100644
index 00000000..3437b857
--- /dev/null
+++ b/app/src/main/res/drawable/appwidget4x3_preview.png
Binary files differ
diff --git a/app/src/main/res/drawable/appwidget4x4_preview.png b/app/src/main/res/drawable/appwidget4x4_preview.png
new file mode 100644
index 00000000..ebc2b7a1
--- /dev/null
+++ b/app/src/main/res/drawable/appwidget4x4_preview.png
Binary files differ
diff --git a/app/src/main/res/layout-land/download.xml b/app/src/main/res/layout-land/download.xml
new file mode 100644
index 00000000..f3e39a5f
--- /dev/null
+++ b/app/src/main/res/layout-land/download.xml
@@ -0,0 +1,129 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/download_layout_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/download_layout"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+
+ <LinearLayout android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1">
+
+ <github.daneren2005.dsub.view.MyViewFlipper
+ android:id="@+id/download_playlist_flipper"
+ android:layout_width="0dp"
+ android:layout_height="fill_parent"
+ android:layout_weight="1">
+
+ <github.daneren2005.dsub.view.RecyclingImageView
+ android:id="@+id/download_album_art_image"
+ android:src="@drawable/unknown_album_large"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:layout_weight="1"
+ android:scaleType="fitStart"/>
+
+ <include layout="@layout/download_playlist"/>
+
+ </github.daneren2005.dsub.view.MyViewFlipper>
+
+ <RelativeLayout android:orientation="vertical"
+ android:id="@+id/download_control_layout"
+ android:layout_width="0dp"
+ android:layout_height="fill_parent"
+ android:layout_weight="1"
+ android:background="@android:color/transparent">
+
+ <LinearLayout
+ android:id="@+id/download_other_controls_wrapper"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerHorizontal="true"
+ android:layout_above="@+id/download_song_title">
+
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/download_other_controls_layout"
+ android:orientation="horizontal"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal">
+
+ <ImageButton
+ android:id="@+id/download_rating_bad"
+ style="@style/DownloadActionImageButton"
+ android:src="?attr/rating_bad"/>
+
+ <ImageButton
+ android:id="@+id/download_star"
+ style="@style/DownloadActionImageButton"
+ android:src="@android:drawable/star_big_off"/>
+
+ <ImageButton
+ android:id="@+id/download_bookmark"
+ style="@style/DownloadActionImageButton"
+ android:src="?attr/bookmark"/>
+
+ <ImageButton
+ android:id="@+id/download_rating_good"
+ style="@style/DownloadActionImageButton"
+ android:src="?attr/rating_good"/>
+ </LinearLayout>
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/download_song_title"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_centerHorizontal="true"
+ android:layout_marginLeft="12dip"
+ android:layout_marginRight="12dip"
+ android:singleLine="true"
+ android:ellipsize="end"
+ android:gravity="center_horizontal"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textColor="?android:textColorPrimary"
+ android:layout_above="@+id/download_status"/>
+
+ <TextView
+ android:id="@+id/download_status"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:layout_marginBottom="8dip"
+ android:layout_marginLeft="12dip"
+ android:layout_marginRight="12dip"
+ android:singleLine="true"
+ android:ellipsize="end"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="?android:textColorSecondary"
+ android:layout_above="@+id/download_media_buttons_wrapper"/>
+
+ <LinearLayout
+ android:id="@+id/download_media_buttons_wrapper"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_above="@+id/download_slider_wrapper">
+
+ <include layout="@layout/download_media_buttons"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/download_slider_wrapper"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true">
+
+ <include layout="@layout/download_slider"/>
+ </LinearLayout>
+
+ </RelativeLayout>
+
+ </LinearLayout>
+ </LinearLayout>
+</FrameLayout>
diff --git a/app/src/main/res/layout-large-land/abstract_fragment_container.xml b/app/src/main/res/layout-large-land/abstract_fragment_container.xml
new file mode 100644
index 00000000..5e3b1561
--- /dev/null
+++ b/app/src/main/res/layout-large-land/abstract_fragment_container.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="0px"
+ android:layout_weight="1">
+
+ <FrameLayout
+ android:id="@+id/fragment_container"
+ android:layout_width="0px"
+ android:layout_height="match_parent"
+ android:layout_weight="4"/>
+
+ <FrameLayout
+ android:id="@+id/fragment_second_container"
+ android:layout_width="0px"
+ android:layout_height="match_parent"
+ android:layout_weight="6"
+ android:visibility="gone"/>
+
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout-large-land/download.xml b/app/src/main/res/layout-large-land/download.xml
new file mode 100644
index 00000000..8b252190
--- /dev/null
+++ b/app/src/main/res/layout-large-land/download.xml
@@ -0,0 +1,130 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/download_layout_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/download_layout"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+
+ <LinearLayout android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1">
+
+ <github.daneren2005.dsub.view.RecyclingImageView
+ android:id="@+id/download_album_art_image"
+ android:src="@drawable/unknown_album_large"
+ android:layout_width="0dp"
+ android:layout_height="fill_parent"
+ android:layout_weight="1"
+ android:scaleType="fitStart"/>
+
+ <RelativeLayout android:orientation="vertical"
+ android:id="@+id/download_control_layout"
+ android:layout_width="0dp"
+ android:layout_height="fill_parent"
+ android:layout_weight="1"
+ android:background="@android:color/transparent">
+
+ <github.daneren2005.dsub.view.MyViewFlipper
+ android:id="@+id/download_playlist_flipper"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:layout_above="@+id/download_song_title">
+
+ <RelativeLayout
+ android:id="@+id/download_other_controls_wrapper"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:id="@+id/download_other_controls_layout"
+ android:orientation="horizontal"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerHorizontal="true"
+ android:layout_alignParentBottom="true">
+
+ <ImageButton
+ android:id="@+id/download_rating_bad"
+ style="@style/DownloadActionImageButton"
+ android:src="?attr/rating_bad"/>
+
+ <ImageButton
+ android:id="@+id/download_star"
+ style="@style/DownloadActionImageButton"
+ android:src="@android:drawable/star_big_off"/>
+
+ <ImageButton
+ android:id="@+id/download_bookmark"
+ style="@style/DownloadActionImageButton"
+ android:src="?attr/bookmark"/>
+
+ <ImageButton
+ android:id="@+id/download_rating_good"
+ style="@style/DownloadActionImageButton"
+ android:src="?attr/rating_good"/>
+ </LinearLayout>
+ </RelativeLayout>
+
+ <include layout="@layout/download_playlist"/>
+
+ </github.daneren2005.dsub.view.MyViewFlipper>
+
+ <TextView
+ android:id="@+id/download_song_title"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_centerHorizontal="true"
+ android:layout_marginLeft="12dip"
+ android:layout_marginRight="12dip"
+ android:singleLine="true"
+ android:ellipsize="end"
+ android:gravity="center_horizontal"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textColor="?android:textColorPrimary"
+ android:layout_above="@+id/download_status"/>
+
+ <TextView
+ android:id="@+id/download_status"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:layout_marginBottom="8dip"
+ android:layout_marginLeft="12dip"
+ android:layout_marginRight="12dip"
+ android:singleLine="true"
+ android:ellipsize="end"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="?android:textColorSecondary"
+ android:layout_above="@+id/download_media_buttons_wrapper"/>
+
+ <LinearLayout
+ android:id="@+id/download_media_buttons_wrapper"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_above="@+id/download_slider_wrapper">
+
+ <include layout="@layout/download_media_buttons"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/download_slider_wrapper"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true">
+
+ <include layout="@layout/download_slider"/>
+ </LinearLayout>
+
+ </RelativeLayout>
+
+ </LinearLayout>
+ </LinearLayout>
+</FrameLayout>
diff --git a/app/src/main/res/layout-port/download.xml b/app/src/main/res/layout-port/download.xml
new file mode 100644
index 00000000..96e2c864
--- /dev/null
+++ b/app/src/main/res/layout-port/download.xml
@@ -0,0 +1,120 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/download_layout_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/download_layout"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+
+ <github.daneren2005.dsub.view.MyViewFlipper
+ android:id="@+id/download_playlist_flipper"
+ android:layout_width="fill_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1">
+
+ <RelativeLayout
+ android:id="@+id/download_album_art_layout"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:layout_weight="1"
+ android:background="@android:color/transparent">
+
+ <FrameLayout android:orientation="vertical"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true">
+
+ <github.daneren2005.dsub.view.RecyclingImageView
+ android:id="@+id/download_album_art_image"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:scaleType="fitCenter"
+ android:layout_gravity="center_horizontal|top"/>
+
+ <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/download_overlay_buttons"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/overlayColor"
+ android:layout_gravity="center_horizontal|bottom">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerHorizontal="true">
+
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/download_other_controls_layout"
+ android:orientation="horizontal"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal">
+
+ <ImageButton
+ android:id="@+id/download_rating_bad"
+ style="@style/DownloadActionImageButton"
+ android:src="@drawable/ic_action_rating_bad_dark"/>
+
+ <ImageButton
+ android:id="@+id/download_star"
+ style="@style/DownloadActionImageButton"
+ android:src="@android:drawable/star_big_off"/>
+
+ <ImageButton
+ android:id="@+id/download_bookmark"
+ style="@style/DownloadActionImageButton"
+ android:src="@drawable/ic_menu_bookmark_dark"/>
+
+ <ImageButton
+ android:id="@+id/download_rating_good"
+ style="@style/DownloadActionImageButton"
+ android:src="@drawable/ic_action_rating_good_dark"/>
+ </LinearLayout>
+ </LinearLayout>
+ </RelativeLayout>
+ </FrameLayout>
+
+ <TextView
+ android:id="@+id/download_status"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_centerHorizontal="true"
+ android:layout_marginLeft="16dip"
+ android:layout_marginRight="16dip"
+ android:singleLine="true"
+ android:ellipsize="end"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="?android:textColorSecondary"/>
+
+ <TextView
+ android:id="@+id/download_song_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:layout_above="@+id/download_status"
+ android:layout_centerHorizontal="true"
+ android:layout_marginLeft="16dip"
+ android:layout_marginRight="16dip"
+ android:singleLine="true"
+ android:textColor="?android:textColorPrimary"
+ android:textStyle="bold"
+ android:textSize="18sp"
+ android:ellipsize="end"/>
+
+ </RelativeLayout>
+
+ <include layout="@layout/download_playlist"/>
+
+ </github.daneren2005.dsub.view.MyViewFlipper>
+
+ <include layout="@layout/download_media_buttons"/>
+
+ <include layout="@layout/download_slider"/>
+ </LinearLayout>
+</FrameLayout>
diff --git a/app/src/main/res/layout/abstract_activity.xml b/app/src/main/res/layout/abstract_activity.xml
new file mode 100644
index 00000000..be65e437
--- /dev/null
+++ b/app/src/main/res/layout/abstract_activity.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.v4.widget.DrawerLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/drawer_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <!-- The main content view -->
+ <FrameLayout
+ android:id="@+id/content_frame"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+ <!-- The navigation drawer -->
+ <ListView android:id="@+id/left_drawer"
+ android:layout_width="240dp"
+ android:layout_height="match_parent"
+ android:layout_gravity="start"
+ android:choiceMode="singleChoice"
+ android:divider="@android:color/transparent"
+ android:dividerHeight="0dp"
+ android:background="?android:windowBackground"/>
+</android.support.v4.widget.DrawerLayout>
diff --git a/app/src/main/res/layout/abstract_fragment_activity.xml b/app/src/main/res/layout/abstract_fragment_activity.xml
new file mode 100644
index 00000000..d9c99f2f
--- /dev/null
+++ b/app/src/main/res/layout/abstract_fragment_activity.xml
@@ -0,0 +1,84 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center_horizontal"
+ android:orientation="vertical" >
+
+ <include layout="@layout/abstract_fragment_container" />
+
+ <View
+ android:layout_width="fill_parent"
+ android:layout_height="1px"
+ android:background="@color/dividerColor"/>
+
+ <LinearLayout
+ android:id="@+id/bottom_bar"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ style="@style/BasicButton"
+ android:orientation="horizontal">
+
+ <github.daneren2005.dsub.view.RecyclingImageView
+ android:id="@+id/album_art"
+ android:layout_width="50dip"
+ android:layout_height="50dip"
+ android:layout_gravity="left|center"
+ android:scaleType="fitStart"/>
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:paddingLeft="8dip">
+
+ <TextView
+ android:id="@+id/track_name"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:textColor="?android:textColorPrimary"
+ android:singleLine="true"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textSize="13sp"
+ android:text="@string/search.artists"/>
+
+ <TextView
+ android:id="@+id/artist_name"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:textColor="?android:textColorSecondary"
+ android:singleLine="true"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textSize="12sp"
+ android:text="@string/search.albums"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_height="wrap_content"
+ android:layout_width="0dp"
+ android:layout_weight="1">
+
+ <ImageButton
+ style="@style/PlaybackControl.Small"
+ android:id="@+id/download_previous"
+ android:src="?attr/media_button_backward"
+ android:layout_width="0dp"
+ android:layout_weight="1"/>
+
+ <ImageButton
+ style="@style/PlaybackControl.Small"
+ android:id="@+id/download_start"
+ android:src="?attr/media_button_start"
+ android:layout_width="0dp"
+ android:layout_weight="1"/>
+
+ <ImageButton
+ style="@style/PlaybackControl.Small"
+ android:id="@+id/download_next"
+ android:src="?attr/media_button_forward"
+ android:layout_width="0dp"
+ android:layout_weight="1"/>
+ </LinearLayout>
+ </LinearLayout>
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/abstract_fragment_container.xml b/app/src/main/res/layout/abstract_fragment_container.xml
new file mode 100644
index 00000000..61e17d1d
--- /dev/null
+++ b/app/src/main/res/layout/abstract_fragment_container.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/fragment_container"
+ android:layout_width="match_parent"
+ android:layout_height="0px"
+ android:layout_weight="1"/> \ No newline at end of file
diff --git a/app/src/main/res/layout/abstract_list_fragment.xml b/app/src/main/res/layout/abstract_list_fragment.xml
new file mode 100644
index 00000000..618a7341
--- /dev/null
+++ b/app/src/main/res/layout/abstract_list_fragment.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.v4.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/refresh_layout"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+
+ <LinearLayout
+ android:id="@+id/fragment_list_layout"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:orientation="vertical" >
+
+ <View
+ android:layout_width="fill_parent"
+ android:layout_height="1px"
+ android:background="@color/dividerColor"/>
+
+ <ListView
+ android:id="@+id/fragment_list"
+ android:layout_width="fill_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1.0"
+ android:fastScrollEnabled="true"/>
+
+ <include layout="@layout/tab_progress" />
+ </LinearLayout>
+</android.support.v4.widget.SwipeRefreshLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/actionbar_spinner.xml b/app/src/main/res/layout/actionbar_spinner.xml
new file mode 100644
index 00000000..22055901
--- /dev/null
+++ b/app/src/main/res/layout/actionbar_spinner.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:gravity="fill_horizontal" >
+ <Spinner
+ android:id="@+id/spinner"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:prompt="@string/common.appname"
+ />
+</RelativeLayout>
diff --git a/app/src/main/res/layout/album_cell_item.xml b/app/src/main/res/layout/album_cell_item.xml
new file mode 100644
index 00000000..3f708e63
--- /dev/null
+++ b/app/src/main/res/layout/album_cell_item.xml
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1">
+
+ <github.daneren2005.dsub.view.SquareImageView
+ android:id="@+id/album_coverart"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
+ <RatingBar
+ android:id="@+id/album_rating"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:isIndicator="true"
+ android:layout_centerHorizontal="true"
+ android:numStars="5"
+ style="@android:style/Widget.Holo.RatingBar.Small"
+ android:layout_alignParentBottom="true"
+ android:visibility="gone"/>
+ </RelativeLayout>
+
+ <LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:paddingTop="4dp"
+ android:paddingLeft="2dp">
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:gravity="center_vertical"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/album_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:singleLine="true"
+ android:ellipsize="marquee"
+ android:text="@string/search.albums"
+ android:textColor="?android:textColorPrimary"/>
+
+ <LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/album_artist"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"
+ android:textSize="12sp"
+ android:textColor="?android:textColorSecondary"
+ android:singleLine="true"
+ android:text="@string/search.artists"/>
+
+ <ImageButton
+ android:id="@+id/album_star"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="right|center_vertical"
+ android:src="@drawable/ic_stat_star"
+ android:background="@android:color/transparent"
+ android:focusable="false"
+ android:visibility="gone"/>
+ </LinearLayout>
+ </LinearLayout>
+
+ <ImageView
+ android:id="@+id/album_more"
+ android:src="?attr/download_none"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="right|center_vertical"
+ android:paddingRight="2dp"
+ style="@style/BasicButton"/>
+ </LinearLayout>
+
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/album_list_item.xml b/app/src/main/res/layout/album_list_item.xml
new file mode 100644
index 00000000..0ee92edd
--- /dev/null
+++ b/app/src/main/res/layout/album_list_item.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@id/drag_handle"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <RelativeLayout
+ android:layout_width="@dimen/AlbumArt.Small"
+ android:layout_height="@dimen/AlbumArt.Small">
+
+ <github.daneren2005.dsub.view.RecyclingImageView
+ android:id="@+id/album_coverart"
+ android:layout_width="@dimen/AlbumArt.Small"
+ android:layout_height="@dimen/AlbumArt.Small"
+ android:layout_gravity="left|center_vertical"/>
+
+ <RatingBar
+ android:id="@+id/album_rating"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:isIndicator="true"
+ android:layout_centerHorizontal="true"
+ android:numStars="5"
+ style="@android:style/Widget.Holo.RatingBar.Small"
+ android:layout_alignParentBottom="true"
+ android:visibility="gone"/>
+ </RelativeLayout>
+
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_gravity="left|center_vertical"
+ android:paddingLeft="10dip"
+ android:paddingRight="3dip">
+
+ <TextView
+ android:id="@+id/album_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:singleLine="true"
+ android:ellipsize="marquee"
+ android:paddingBottom="6dip"/>
+
+ <TextView
+ android:id="@+id/album_artist"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:singleLine="true"/>
+
+ </LinearLayout>
+
+ <ImageButton
+ android:id="@+id/album_star"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="right|center_vertical"
+ android:src="@drawable/ic_stat_star"
+ android:background="@android:color/transparent"
+ android:focusable="false"
+ android:visibility="gone"/>
+
+ <ImageView
+ android:id="@+id/album_more"
+ android:src="?attr/download_none"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:layout_gravity="right|center_vertical"
+ style="@style/MoreButton"/>
+</LinearLayout>
diff --git a/app/src/main/res/layout/appwidget4x1.xml b/app/src/main/res/layout/appwidget4x1.xml
new file mode 100644
index 00000000..8f52c872
--- /dev/null
+++ b/app/src/main/res/layout/appwidget4x1.xml
@@ -0,0 +1,107 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:minWidth="250dp"
+ android:minHeight="40dp"
+ android:background="@drawable/appwidget_bg"
+ android:orientation="horizontal"
+ android:id="@+id/widget_root">
+
+ <ImageView
+ android:id="@+id/appwidget_coverart"
+ android:layout_width="80dp"
+ android:layout_height="80dp"
+ android:layout_gravity="center_vertical"
+ android:clickable="true"
+ android:focusable="true"
+ android:src="@drawable/appwidget_art_default" />
+
+ <LinearLayout
+ android:id="@+id/linearLayout1"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:id="@+id/appwidget_top"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:clickable="true"
+ android:focusable="true"
+ android:orientation="vertical"
+ style="@style/BasicButton">
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="marquee"
+ android:fadingEdge="horizontal"
+ android:fadingEdgeLength="20dip"
+ android:minHeight="16sp"
+ android:paddingLeft="4dip"
+ android:paddingRight="4dip"
+ android:paddingTop="4dip"
+ android:singleLine="true"
+ android:gravity="center_horizontal"
+ android:text="Title"
+ android:textColor="@color/appwidget_text"
+ android:textSize="16sp"
+ android:textStyle="bold"/>
+
+ <TextView
+ android:id="@+id/artist"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="marquee"
+ android:fadingEdge="horizontal"
+ android:fadingEdgeLength="10dip"
+ android:minHeight="12sp"
+ android:paddingBottom="4dip"
+ android:paddingLeft="4dip"
+ android:paddingRight="4dip"
+ android:singleLine="true"
+ android:gravity="center_horizontal"
+ android:text="Artist"
+ android:textColor="@color/appwidget_text"
+ android:textSize="12sp" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:orientation="horizontal"
+ android:paddingBottom="4dip"
+ android:paddingTop="4dip" >
+
+ <ImageButton
+ android:id="@+id/control_previous"
+ android:layout_width="0dip"
+ android:layout_height="fill_parent"
+ android:layout_weight="1"
+ android:scaleType="center"
+ style="@style/BasicButton"
+ android:src="@drawable/ic_appwidget_music_previous" />
+
+ <ImageButton
+ android:id="@+id/control_play"
+ android:layout_width="0dip"
+ android:layout_height="fill_parent"
+ android:layout_weight="1"
+ android:scaleType="center"
+ android:src="@drawable/ic_appwidget_music_play"
+ style="@style/BasicButton" />
+
+ <ImageButton
+ android:id="@+id/control_next"
+ android:layout_width="0dip"
+ android:layout_height="fill_parent"
+ android:layout_weight="1"
+ android:scaleType="center"
+ android:src="@drawable/ic_appwidget_music_next"
+ style="@style/BasicButton" />
+ </LinearLayout>
+ </LinearLayout>
+
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/appwidget4x2.xml b/app/src/main/res/layout/appwidget4x2.xml
new file mode 100644
index 00000000..5763fb01
--- /dev/null
+++ b/app/src/main/res/layout/appwidget4x2.xml
@@ -0,0 +1,130 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:minWidth="250dp"
+ android:minHeight="110dp"
+ android:background="@drawable/appwidget_bg"
+ android:orientation="horizontal"
+ android:id="@+id/widget_root">
+
+ <ImageView
+ android:id="@+id/appwidget_coverart"
+ android:layout_width="120dp"
+ android:layout_height="120dp"
+ android:layout_gravity="center_vertical"
+ android:clickable="true"
+ android:focusable="true"
+ android:src="@drawable/appwidget_art_default" />
+
+ <LinearLayout
+ android:id="@+id/linearLayout1"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:id="@+id/appwidget_top"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:clickable="true"
+ android:focusable="true"
+ android:orientation="vertical"
+ android:paddingTop="4dip"
+ android:paddingBottom="4dip"
+ style="@style/BasicButton">
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="marquee"
+ android:fadingEdge="horizontal"
+ android:fadingEdgeLength="20dip"
+ android:minHeight="16sp"
+ android:paddingLeft="4dip"
+ android:paddingRight="4dip"
+ android:paddingTop="4dip"
+ android:paddingBottom="2dip"
+ android:singleLine="true"
+ android:gravity="center_horizontal"
+ android:text="Title"
+ android:textColor="@color/appwidget_text"
+ android:textSize="16sp"
+ android:textStyle="bold" />
+
+ <TextView
+ android:id="@+id/artist"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="marquee"
+ android:fadingEdge="horizontal"
+ android:fadingEdgeLength="10dip"
+ android:minHeight="12sp"
+ android:paddingLeft="4dip"
+ android:paddingRight="4dip"
+ android:paddingBottom="2dip"
+ android:singleLine="true"
+ android:gravity="center_horizontal"
+ android:text="Artist"
+ android:textColor="@color/appwidget_text"
+ android:textSize="12sp" />
+
+ <TextView
+ android:id="@+id/album"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="marquee"
+ android:fadingEdge="horizontal"
+ android:fadingEdgeLength="10dip"
+ android:minHeight="12sp"
+ android:paddingBottom="2dip"
+ android:paddingLeft="4dip"
+ android:paddingRight="4dip"
+ android:singleLine="true"
+ android:gravity="center_horizontal"
+ android:text="Album"
+ android:textColor="@color/appwidget_text"
+ android:textSize="12sp" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:layout_gravity="bottom"
+ android:gravity="bottom"
+ android:paddingBottom="4dip"
+ android:paddingTop="4dip" >"
+
+ <ImageButton
+ android:id="@+id/control_previous"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:scaleType="center"
+ style="@style/BasicButton"
+ android:src="@drawable/ic_appwidget_music_previous" />
+
+ <ImageButton
+ android:id="@+id/control_play"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:scaleType="center"
+ android:src="@drawable/ic_appwidget_music_play"
+ style="@style/BasicButton" />
+
+ <ImageButton
+ android:id="@+id/control_next"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:scaleType="center"
+ android:src="@drawable/ic_appwidget_music_next"
+ style="@style/BasicButton" />
+ </LinearLayout>
+ </LinearLayout>
+
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/appwidget4x3.xml b/app/src/main/res/layout/appwidget4x3.xml
new file mode 100644
index 00000000..539b9f01
--- /dev/null
+++ b/app/src/main/res/layout/appwidget4x3.xml
@@ -0,0 +1,113 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:background="@drawable/appwidget_bg"
+ android:orientation="vertical"
+ android:id="@+id/widget_root">
+
+ <ImageView
+ android:id="@+id/appwidget_coverart"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:scaleType="fitCenter"
+ android:layout_weight="1"
+ android:layout_gravity="center_horizontal"
+ android:clickable="true"
+ android:focusable="true"
+ android:paddingTop="6dip"
+ android:paddingBottom="6dip"
+ android:src="@drawable/appwidget_art_default" />
+
+ <LinearLayout
+ android:id="@+id/linearLayout1"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:id="@+id/appwidget_top"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:clickable="true"
+ android:focusable="true"
+ android:orientation="vertical"
+ android:paddingBottom="4dip"
+ android:paddingTop="4dip"
+ style="@style/BasicButton">
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="marquee"
+ android:fadingEdge="horizontal"
+ android:fadingEdgeLength="20dip"
+ android:minHeight="16sp"
+ android:paddingLeft="5dip"
+ android:paddingRight="5dip"
+ android:singleLine="true"
+ android:textColor="@color/appwidget_text"
+ android:textSize="16sp"
+ android:text="Title"
+ android:layout_gravity="center_horizontal"
+ android:gravity="center"
+ android:textStyle="bold" />
+
+ <TextView
+ android:id="@+id/artist"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="marquee"
+ android:fadingEdge="horizontal"
+ android:fadingEdgeLength="10dip"
+ android:minHeight="12sp"
+ android:paddingBottom="2dip"
+ android:paddingLeft="5dip"
+ android:singleLine="true"
+ android:text="Artist"
+ android:layout_gravity="center_horizontal"
+ android:gravity="center"
+ android:textColor="@color/appwidget_text"
+ android:textSize="12sp" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom"
+ android:gravity="bottom"
+ android:orientation="horizontal"
+ android:paddingBottom="4dip">
+
+ <ImageButton
+ android:id="@+id/control_previous"
+ android:layout_width="0dip"
+ android:layout_height="56dip"
+ android:layout_weight="1"
+ android:scaleType="center"
+ style="@style/BasicButton"
+ android:src="@drawable/ic_appwidget_music_previous" />
+
+ <ImageButton
+ android:id="@+id/control_play"
+ android:layout_width="0dip"
+ android:layout_height="56dip"
+ android:layout_weight="1"
+ android:scaleType="center"
+ android:src="@drawable/ic_appwidget_music_play"
+ style="@style/BasicButton" />
+
+ <ImageButton
+ android:id="@+id/control_next"
+ android:layout_width="0dip"
+ android:layout_height="56dip"
+ android:layout_weight="1"
+ android:scaleType="center"
+ android:src="@drawable/ic_appwidget_music_next"
+ style="@style/BasicButton" />
+ </LinearLayout>
+
+ </LinearLayout>
+
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/appwidget4x4.xml b/app/src/main/res/layout/appwidget4x4.xml
new file mode 100644
index 00000000..1f2db9e1
--- /dev/null
+++ b/app/src/main/res/layout/appwidget4x4.xml
@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:orientation="vertical"
+ android:background="@drawable/appwidget_bg"
+ android:id="@+id/widget_root">
+
+ <ImageView
+ android:id="@+id/appwidget_coverart"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:scaleType="fitCenter"
+ android:layout_weight="1"
+ android:layout_gravity="center_horizontal"
+ android:clickable="true"
+ android:focusable="true"
+ android:layout_margin="6dip"
+ android:paddingTop="6dip"
+ android:paddingBottom="6dip"
+ android:src="@drawable/appwidget_art_default" />
+
+ <LinearLayout
+ android:id="@+id/linearLayout1"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:id="@+id/appwidget_top"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:clickable="true"
+ android:focusable="true"
+ android:orientation="vertical"
+ android:paddingTop="4dip"
+ android:paddingBottom="4dip"
+ style="@style/BasicButton">
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="marquee"
+ android:fadingEdge="horizontal"
+ android:fadingEdgeLength="20dip"
+ android:minHeight="16sp"
+ android:paddingLeft="5dip"
+ android:paddingRight="5dip"
+ android:singleLine="true"
+ android:textColor="@color/appwidget_text"
+ android:textSize="16sp"
+ android:text="Title"
+ android:layout_gravity="center_horizontal"
+ android:gravity="center"
+ android:textStyle="bold" />
+
+ <TextView
+ android:id="@+id/artist"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="marquee"
+ android:fadingEdge="horizontal"
+ android:fadingEdgeLength="10dip"
+ android:minHeight="12sp"
+ android:paddingBottom="2dip"
+ android:paddingLeft="5dip"
+ android:singleLine="true"
+ android:text="Artist"
+ android:layout_gravity="center_horizontal"
+ android:gravity="center"
+ android:textColor="@color/appwidget_text"
+ android:textSize="12sp" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom"
+ android:gravity="bottom"
+ android:orientation="horizontal"
+ android:paddingBottom="4dip"
+ android:paddingTop="4dip" >
+
+ <ImageButton
+ android:id="@+id/control_previous"
+ android:layout_width="0dip"
+ android:layout_height="56dip"
+ android:layout_weight="1"
+ android:scaleType="center"
+ style="@style/BasicButton"
+ android:src="@drawable/ic_appwidget_music_previous" />
+
+ <ImageButton
+ android:id="@+id/control_play"
+ android:layout_width="0dip"
+ android:layout_height="56dip"
+ android:layout_weight="1"
+ android:scaleType="center"
+ android:src="@drawable/ic_appwidget_music_play"
+ style="@style/BasicButton" />
+
+ <ImageButton
+ android:id="@+id/control_next"
+ android:layout_width="0dip"
+ android:layout_height="56dip"
+ android:layout_weight="1"
+ android:scaleType="center"
+ android:src="@drawable/ic_appwidget_music_next"
+ style="@style/BasicButton" />
+ </LinearLayout>
+
+ </LinearLayout>
+
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/basic_count_item.xml b/app/src/main/res/layout/basic_count_item.xml
new file mode 100644
index 00000000..08d276db
--- /dev/null
+++ b/app/src/main/res/layout/basic_count_item.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:background="@android:color/transparent"
+ android:minHeight="50dip">
+
+ <TextView
+ android:id="@+id/basic_count_name"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:gravity="left|center_vertical"
+ android:paddingLeft="6dip"
+ android:paddingRight="6dip"
+ android:background="@android:color/transparent"
+ android:text="Text"/>
+
+ <TextView
+ android:id="@+id/basic_count_count"
+ android:layout_width="32dp"
+ android:layout_height="32dp"
+ android:layout_marginRight="12dp"
+ android:background="@drawable/ic_number_border"
+ android:focusable="false"
+ android:paddingRight="10dp"
+ android:layout_marginLeft="20px"
+ android:layout_marginBottom="4px"
+ android:text="99"
+ android:textAppearance="?android:attr/textAppearanceSmallPopupMenu"
+ android:textSize="11sp"
+ android:gravity="right|center_vertical"
+ android:layout_gravity="center_vertical"
+ android:visibility="gone"/>
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/basic_list_item.xml b/app/src/main/res/layout/basic_list_item.xml
new file mode 100644
index 00000000..2338f7e0
--- /dev/null
+++ b/app/src/main/res/layout/basic_list_item.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:background="@android:color/transparent">
+
+ <TextView
+ android:id="@+id/item_name"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:gravity="left|center_vertical"
+ android:paddingLeft="6dip"
+ android:paddingRight="6dip"
+ android:minHeight="50dip"
+ android:background="@android:color/transparent"/>
+
+ <ImageButton
+ android:id="@+id/item_star"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="right|center_vertical"
+ android:src="@drawable/ic_stat_star"
+ android:background="@android:color/transparent"
+ android:focusable="false"
+ android:visibility="gone"/>
+
+ <ImageView
+ android:id="@+id/item_more"
+ android:src="?attr/download_none"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:layout_gravity="right|center_vertical"
+ style="@style/MoreButton"/>
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/change_email.xml b/app/src/main/res/layout/change_email.xml
new file mode 100644
index 00000000..18ffc765
--- /dev/null
+++ b/app/src/main/res/layout/change_email.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/new_email_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="4dp"
+ android:textSize="20dp"
+ android:text="@string/admin.change_email_label" />
+ <EditText
+ android:id="@+id/new_email"
+ android:inputType="textEmailAddress"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginLeft="4dp" />
+ </LinearLayout>
+
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/change_password.xml b/app/src/main/res/layout/change_password.xml
new file mode 100644
index 00000000..1a382a6b
--- /dev/null
+++ b/app/src/main/res/layout/change_password.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/new_password_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="4dp"
+ android:textSize="20dp"
+ android:text="@string/admin.change_password_label" />
+ <EditText
+ android:id="@+id/new_password"
+ android:inputType="textPassword"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginLeft="4dp" />
+ </LinearLayout>
+
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/chat.xml b/app/src/main/res/layout/chat.xml
new file mode 100644
index 00000000..89ad77ac
--- /dev/null
+++ b/app/src/main/res/layout/chat.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.v4.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/refresh_layout"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+
+ <LinearLayout
+ android:id="@+id/chat_layout"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:orientation="vertical" >
+
+ <include layout="@layout/tab_progress" />
+
+ <ListView
+ android:id="@+id/chat_entries"
+ android:layout_width="fill_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1.0"
+ android:textFilterEnabled="true" />
+
+ <LinearLayout
+ android:layout_height="4dip"
+ android:layout_width="fill_parent"
+ android:layout_marginTop="4dip"/>
+
+ <LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="bottom" >
+
+ <EditText
+ android:id="@+id/chat_edittext"
+ android:layout_width="0dip"
+ android:layout_height="40dip"
+ android:layout_weight="1"
+ android:autoLink="all"
+ android:hint="@string/chat.send_a_message"
+ android:inputType="textCapSentences|textAutoCorrect|textLongMessage"
+ android:linksClickable="true"
+ android:paddingBottom="10dip"
+ android:paddingTop="10dip" />
+
+ <ImageButton
+ android:id="@+id/chat_send"
+ android:layout_width="60dip"
+ android:layout_height="40dip"
+ android:src="?attr/chat_send" />
+
+ </LinearLayout>
+ </LinearLayout>
+</android.support.v4.widget.SwipeRefreshLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/chat_item.xml b/app/src/main/res/layout/chat_item.xml
new file mode 100644
index 00000000..f31f7988
--- /dev/null
+++ b/app/src/main/res/layout/chat_item.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <github.daneren2005.dsub.view.RecyclingImageView
+ android:id="@+id/chat_avatar"
+ android:src="@drawable/ic_social_person"
+ android:layout_width="@dimen/AlbumArt.Small"
+ android:layout_height="@dimen/AlbumArt.Small"
+ android:layout_gravity="left|center_vertical"/>
+
+ <LinearLayout
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:orientation="vertical" >
+
+ <LinearLayout
+ android:id="@+id/chat_message_layout"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="4dip"
+ android:orientation="horizontal" >
+
+ <TextView
+ android:id="@+id/chat_username"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="6dip"
+ android:layout_marginRight="6dip"
+ android:ellipsize="marquee"
+ android:singleLine="true"
+ android:text="User"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:textColor="?android:textColorSecondary"/>
+
+ <TextView
+ android:id="@+id/chat_time"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="6dip"
+ android:singleLine="true"
+ android:text="00:00"
+ android:textAppearance="?android:attr/textAppearanceSmall" />
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/chat_message"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="6dip"
+ android:layout_marginRight="6dip"
+ android:autoLink="all"
+ android:linksClickable="true"
+ android:singleLine="false"
+ android:text="Message Text Goes Here"
+ android:textAppearance="?android:attr/textAppearanceMedium" />
+ </LinearLayout>
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/chat_item_reverse.xml b/app/src/main/res/layout/chat_item_reverse.xml
new file mode 100644
index 00000000..b8102193
--- /dev/null
+++ b/app/src/main/res/layout/chat_item_reverse.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <LinearLayout
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:orientation="vertical" >
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="4dip"
+ android:orientation="horizontal"
+ android:layout_gravity="right" >
+
+ <TextView
+ android:id="@+id/chat_time"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="6dip"
+ android:singleLine="true"
+ android:text="00:00"
+ android:textAppearance="?android:attr/textAppearanceSmall" />
+
+ <TextView
+ android:id="@+id/chat_username"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="6dip"
+ android:ellipsize="marquee"
+ android:singleLine="true"
+ android:text="User"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:textColor="?android:textColorSecondary"/>
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/chat_message"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="6dip"
+ android:layout_marginRight="6dip"
+ android:autoLink="all"
+ android:linksClickable="true"
+ android:singleLine="false"
+ android:text="Chat message"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:layout_gravity="right"/>
+ </LinearLayout>
+
+ <github.daneren2005.dsub.view.RecyclingImageView
+ android:id="@+id/chat_avatar"
+ android:src="@drawable/ic_social_person"
+ android:layout_width="@dimen/AlbumArt.Small"
+ android:layout_height="@dimen/AlbumArt.Small"
+ android:layout_gravity="right|center_vertical"/>
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/complex_list_item.xml b/app/src/main/res/layout/complex_list_item.xml
new file mode 100644
index 00000000..67851eca
--- /dev/null
+++ b/app/src/main/res/layout/complex_list_item.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="?android:attr/listPreferredItemHeight"
+ android:background="@android:color/transparent">
+
+ <LinearLayout android:orientation="vertical"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_gravity="center_vertical"
+ android:paddingLeft="6dip"
+ android:paddingRight="6dip"
+ android:gravity="left|center_vertical">
+
+ <TextView
+ android:id="@+id/item_name"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:background="@android:color/transparent"/>
+
+ <TextView
+ android:id="@+id/item_description"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:background="@android:color/transparent"/>
+ </LinearLayout>
+
+ <ImageButton
+ android:id="@+id/item_star"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="right|center_vertical"
+ android:src="@drawable/ic_stat_star"
+ android:background="@android:color/transparent"
+ android:focusable="false"
+ android:visibility="gone"/>
+
+ <ImageView
+ android:id="@+id/item_more"
+ android:src="?attr/download_none"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:layout_gravity="right|center_vertical"
+ style="@style/MoreButton"/>
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/confirm_password.xml b/app/src/main/res/layout/confirm_password.xml
new file mode 100644
index 00000000..27ee04ea
--- /dev/null
+++ b/app/src/main/res/layout/confirm_password.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/password_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="4dp"
+ android:textSize="20dp"
+ android:text="@string/admin.add_user_password" />
+ <EditText
+ android:id="@+id/password"
+ android:inputType="textPassword"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginLeft="4dp" />
+ </LinearLayout>
+
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/create_bookmark.xml b/app/src/main/res/layout/create_bookmark.xml
new file mode 100644
index 00000000..f72b39d8
--- /dev/null
+++ b/app/src/main/res/layout/create_bookmark.xml
@@ -0,0 +1,26 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/comment_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="4dp"
+ android:textSize="20dp"
+ android:text="@string/common.comment" />
+ <EditText
+ android:id="@+id/comment_text"
+ android:inputType="text"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginLeft="4dp" />
+ </LinearLayout>
+</LinearLayout>
diff --git a/app/src/main/res/layout/create_podcast.xml b/app/src/main/res/layout/create_podcast.xml
new file mode 100644
index 00000000..5a2ec970
--- /dev/null
+++ b/app/src/main/res/layout/create_podcast.xml
@@ -0,0 +1,27 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/create_podcast_url_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="4dp"
+ android:textSize="20dp"
+ android:text="@string/select_podcasts.add_url"/>
+ <EditText
+ android:id="@+id/create_podcast_url"
+ android:inputType="textUri"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginLeft="4dp"
+ android:text="http://"/>
+ </LinearLayout>
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/create_user.xml b/app/src/main/res/layout/create_user.xml
new file mode 100644
index 00000000..eac6e5cb
--- /dev/null
+++ b/app/src/main/res/layout/create_user.xml
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/username_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="4dp"
+ android:textSize="20dp"
+ android:text="@string/admin.add_user_username" />
+ <EditText
+ android:id="@+id/username"
+ android:inputType="text"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginLeft="4dp" />
+ </LinearLayout>
+
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/email_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="4dp"
+ android:textSize="20dp"
+ android:text="@string/admin.add_user_email" />
+ <EditText
+ android:id="@+id/email"
+ android:inputType="textEmailAddress"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginLeft="4dp" />
+ </LinearLayout>
+
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/password_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="4dp"
+ android:textSize="20dp"
+ android:text="@string/admin.add_user_password" />
+ <EditText
+ android:id="@+id/password"
+ android:inputType="textPassword"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginLeft="4dp" />
+ </LinearLayout>
+
+ <ListView
+ android:id="@+id/settings_list"
+ android:layout_width="fill_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1.0"
+ android:fastScrollEnabled="true"/>
+
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/download_activity.xml b/app/src/main/res/layout/download_activity.xml
new file mode 100644
index 00000000..017e4013
--- /dev/null
+++ b/app/src/main/res/layout/download_activity.xml
@@ -0,0 +1,4 @@
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/fragment_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" /> \ No newline at end of file
diff --git a/app/src/main/res/layout/download_media_buttons.xml b/app/src/main/res/layout/download_media_buttons.xml
new file mode 100644
index 00000000..1affb164
--- /dev/null
+++ b/app/src/main/res/layout/download_media_buttons.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:background="@android:color/transparent">
+
+ <ImageButton
+ style="@style/PlaybackControl.Small"
+ android:id="@+id/download_repeat"
+ android:src="?attr/media_button_repeat_off"
+ android:layout_alignParentLeft="true"
+ android:layout_centerVertical="true"
+ />
+
+ <github.daneren2005.dsub.view.AutoRepeatButton
+ style="@style/PlaybackControl"
+ android:id="@+id/download_previous"
+ android:src="?attr/media_button_backward"
+ android:layout_toLeftOf="@+id/download_pause"
+ android:layout_centerVertical="true"
+ />
+
+ <ImageButton
+ style="@style/PlaybackControl"
+ android:id="@+id/download_pause"
+ android:src="?attr/media_button_pause"
+ android:layout_centerInParent="true"
+ android:visibility="invisible"
+ />
+
+ <ImageButton
+ style="@style/PlaybackControl"
+ android:id="@+id/download_stop"
+ android:src="?attr/media_button_stop"
+ android:layout_centerInParent="true"
+ android:visibility="invisible"
+ />
+
+ <ImageButton
+ style="@style/PlaybackControl"
+ android:id="@+id/download_start"
+ android:src="?attr/media_button_start"
+ android:layout_centerInParent="true"
+ />
+
+ <github.daneren2005.dsub.view.AutoRepeatButton
+ style="@style/PlaybackControl"
+ android:id="@+id/download_next"
+ android:src="?attr/media_button_forward"
+ android:layout_toRightOf="@+id/download_start"
+ android:layout_centerVertical="true"
+ />
+
+ <ImageButton
+ style="@style/PlaybackControl.Small"
+ android:id="@+id/download_toggle_list"
+ android:src="?attr/toggle_list"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ />
+</RelativeLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/download_playlist.xml b/app/src/main/res/layout/download_playlist.xml
new file mode 100644
index 00000000..8a73ef3b
--- /dev/null
+++ b/app/src/main/res/layout/download_playlist.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:layout_weight="1">
+
+ <View
+ android:layout_width="fill_parent"
+ android:layout_height="1px"
+ android:background="@color/dividerColor"/>
+
+ <TextView
+ android:id="@+id/download_empty"
+ android:text="@string/download.empty"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:padding="10dip"/>
+
+ <com.mobeta.android.dslv.DragSortListView
+ style="@style/DragDropListView"
+ android:id="@+id/download_list"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:layout_weight="1"
+ android:cacheColorHint="#00000000"
+ android:fastScrollEnabled="true"/>
+
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/download_slider.xml b/app/src/main/res/layout/download_slider.xml
new file mode 100644
index 00000000..bfd4c120
--- /dev/null
+++ b/app/src/main/res/layout/download_slider.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/download_slider"
+ android:layout_height="wrap_content"
+ android:layout_width="fill_parent"
+ android:background="@android:color/transparent"
+ android:paddingBottom="10dip">
+
+ <TextView
+ android:id="@+id/download_position"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_centerVertical="true"
+ android:paddingLeft="8dip"
+ android:text="0:00"
+ android:textSize="12sp"
+ android:textColor="?android:textColorPrimary"
+ android:paddingBottom="4dip"/>
+
+ <SeekBar
+ android:id="@+id/download_progress_bar"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:indeterminate="false"
+ android:paddingLeft="55dip"
+ android:paddingRight="55dip"
+ android:paddingTop="3dip"
+ android:paddingBottom="7dip" />
+
+ <TextView
+ android:id="@+id/download_duration"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:paddingRight="8dip"
+ android:text="-:--"
+ android:textSize="12sp"
+ android:textColor="?android:textColorPrimary"
+ android:paddingBottom="4dip"/>
+</RelativeLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/drawer_list_item.xml b/app/src/main/res/layout/drawer_list_item.xml
new file mode 100644
index 00000000..5f17c9e9
--- /dev/null
+++ b/app/src/main/res/layout/drawer_list_item.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="8dip"
+ android:paddingBottom="9dip">
+
+ <ImageView
+ android:id="@+id/drawer_icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="left|center_vertical"
+ android:paddingTop="1dip"
+ android:paddingBottom="1dip"
+ android:paddingRight="8dip"
+ android:paddingLeft="10dip"/>
+
+ <TextView
+ android:id="@+id/drawer_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="26sp"
+ android:singleLine="true"
+ android:textColor="?android:textColorPrimary"/>
+</LinearLayout>
diff --git a/app/src/main/res/layout/edit_play_action.xml b/app/src/main/res/layout/edit_play_action.xml
new file mode 100644
index 00000000..a1115da6
--- /dev/null
+++ b/app/src/main/res/layout/edit_play_action.xml
@@ -0,0 +1,119 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="4dp"
+ android:textSize="20dp"
+ android:text="@string/tasker.edit_shuffle_mode" />
+ <CheckBox
+ android:id="@+id/edit_shuffle_checkbox"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:longClickable="true"
+ />
+ </LinearLayout>
+
+ <LinearLayout
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="4dp"
+ android:textSize="20dp"
+ android:text="@string/tasker.edit_shuffle_start_year" />
+
+ <CheckBox
+ android:id="@+id/edit_start_year_checkbox"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:longClickable="true"
+ android:enabled="false"/>
+
+ <EditText
+ android:id="@+id/edit_start_year"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:inputType="number"
+ android:hint="@string/shuffle.startYear"
+ android:enabled="false"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="4dp"
+ android:textSize="20dp"
+ android:text="@string/tasker.edit_shuffle_end_year" />
+
+ <CheckBox
+ android:id="@+id/edit_end_year_checkbox"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:longClickable="true"
+ android:enabled="false"/>
+
+ <EditText
+ android:id="@+id/edit_end_year"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:inputType="number"
+ android:hint="@string/shuffle.endYear"
+ android:enabled="false"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="4dp"
+ android:textSize="20dp"
+ android:text="@string/tasker.edit_shuffle_genre"/>
+
+ <Button
+ android:id="@+id/edit_genre_spinner"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ style="?android:attr/spinnerStyle"
+ android:enabled="false"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="4dp"
+ android:textSize="20dp"
+ android:text="@string/tasker.edit_server_offline"/>
+
+ <Spinner
+ android:id="@+id/edit_offline_spinner"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+ </LinearLayout>
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/equalizer.xml b/app/src/main/res/layout/equalizer.xml
new file mode 100644
index 00000000..1c6cc833
--- /dev/null
+++ b/app/src/main/res/layout/equalizer.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:padding="16dip">
+
+ <CheckBox
+ android:id="@+id/equalizer_enabled"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/equalizer.enabled"
+ android:textAppearance="?android:attr/textAppearanceMedium"/>
+
+ <ScrollView
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <LinearLayout
+ android:id="@+id/special_effects_layout"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"/>
+
+ <LinearLayout
+ android:id="@+id/equalizer_layout"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"/>
+
+ <Button
+ android:id="@+id/equalizer_preset"
+ android:text="@string/equalizer.preset"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:layout_marginTop="20dip"
+ android:paddingLeft="40dip"
+ android:paddingRight="40dip"/>
+
+ </LinearLayout>
+ </ScrollView>
+
+</LinearLayout>
+
diff --git a/app/src/main/res/layout/equalizer_bar.xml b/app/src/main/res/layout/equalizer_bar.xml
new file mode 100644
index 00000000..6dc91565
--- /dev/null
+++ b/app/src/main/res/layout/equalizer_bar.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/equalizer.frequency"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:layout_alignParentLeft="true"
+ />
+
+ <TextView
+ android:id="@+id/equalizer.level"
+ android:text="0 dB"
+ android:textSize="12sp"
+ android:gravity="right"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:layout_alignParentRight="true"
+ android:layout_toRightOf="@+id/equalizer.frequency"
+ />
+
+ <SeekBar
+ android:id="@+id/equalizer.bar"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/equalizer.frequency"
+ />
+
+
+</RelativeLayout>
+
diff --git a/app/src/main/res/layout/genre_list_item.xml b/app/src/main/res/layout/genre_list_item.xml
new file mode 100644
index 00000000..6affa24c
--- /dev/null
+++ b/app/src/main/res/layout/genre_list_item.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:background="@android:color/transparent">
+
+ <TextView
+ android:id="@+id/genre_name"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:gravity="left|center_vertical"
+ android:paddingLeft="6dip"
+ android:paddingRight="6dip"
+ android:minHeight="50dip"
+ android:singleLine="true"
+ android:ellipsize="marquee"
+ android:background="@android:color/transparent"/>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:orientation="vertical"
+ android:gravity="right|center_vertical"
+ android:paddingRight="10dip"
+ android:background="@android:color/transparent">
+
+ <TextView
+ android:id="@+id/genre_songs"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"/>
+
+ <TextView
+ android:id="@+id/genre_albums"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"/>
+ </LinearLayout>
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/grid_view.xml b/app/src/main/res/layout/grid_view.xml
new file mode 100644
index 00000000..599cf92c
--- /dev/null
+++ b/app/src/main/res/layout/grid_view.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<github.daneren2005.dsub.view.HeaderGridView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/gridview"
+ android:layout_width="fill_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1.0"
+ android:numColumns="@integer/Grid.Columns"
+ android:horizontalSpacing="10dp"
+ android:verticalSpacing="10dp"
+ android:gravity="center"
+ android:stretchMode="columnWidth"
+ android:padding="24px"
+ android:fastScrollEnabled="true"
+ android:scrollbarStyle="outsideOverlay"/> \ No newline at end of file
diff --git a/app/src/main/res/layout/home.xml b/app/src/main/res/layout/home.xml
new file mode 100644
index 00000000..e5bf5a70
--- /dev/null
+++ b/app/src/main/res/layout/home.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/home_layout"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+
+ <View
+ android:layout_width="fill_parent"
+ android:layout_height="1px"
+ android:background="@color/dividerColor"/>
+
+ <ListView
+ android:id="@+id/main_list"
+ android:layout_width="fill_parent"
+ android:layout_height="0px"
+ android:layout_weight="1"/>
+
+ <View android:id="@+id/main_dummy"
+ android:layout_width="0px"
+ android:layout_height="0px"/>
+</LinearLayout>
+
diff --git a/app/src/main/res/layout/jukebox_volume.xml b/app/src/main/res/layout/jukebox_volume.xml
new file mode 100644
index 00000000..0c49f634
--- /dev/null
+++ b/app/src/main/res/layout/jukebox_volume.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<RelativeLayout
+ android:background="@drawable/toast_frame"
+ android:padding="20dip"
+ android:layout_height="fill_parent"
+ android:layout_width="fill_parent"
+ android:orientation="vertical"
+ android:id="@+id/toast_layout_root"
+ xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <TextView
+ android:layout_height="wrap_content"
+ android:layout_width="fill_parent"
+ android:id="@+id/jukebox_volume_title"
+ android:paddingBottom="12dp"
+ android:paddingRight="32dp"
+ android:paddingLeft="32dp"
+ android:shadowRadius="2.75"
+ android:shadowColor="#bb000000"
+ android:textColor="#ffffffff"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:text="@string/download.jukebox_volume"
+ android:gravity="center_horizontal"
+ android:layout_alignParentTop="true"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentLeft="true"/>
+
+ <ImageView
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:id="@+id/jukebox_volume_image"
+ android:paddingRight="12dip"
+ android:layout_alignParentLeft="true"
+ android:layout_below="@+id/jukebox_volume_title"
+ android:src="@drawable/ic_action_volume_dark"/>
+
+ <SeekBar
+ android:layout_height="wrap_content"
+ android:layout_width="fill_parent"
+ android:id="@+id/jukebox_volume_progress_bar"
+ android:paddingBottom="3dp"
+ android:layout_alignParentRight="true"
+ android:layout_below="@+id/jukebox_volume_title"
+ android:layout_toRightOf="@+id/jukebox_volume_image"
+ android:indeterminate="false"/>
+</RelativeLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/lyrics.xml b/app/src/main/res/layout/lyrics.xml
new file mode 100644
index 00000000..747727bd
--- /dev/null
+++ b/app/src/main/res/layout/lyrics.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+
+ <include layout="@layout/tab_progress"/>
+
+ <ScrollView
+ android:layout_width="fill_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1.0">
+
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+ <TextView
+ android:id="@+id/lyrics_artist"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:gravity="center_horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:paddingLeft="10dip"
+ android:paddingRight="10dip"
+ android:paddingTop="10dip"
+ android:paddingBottom="4dip"
+ />
+
+ <TextView
+ android:id="@+id/lyrics_title"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:gravity="center_horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:paddingLeft="10dip"
+ android:paddingRight="10dip"
+ />
+
+ <TextView
+ android:id="@+id/lyrics_text"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:gravity="center_horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:paddingLeft="10dip"
+ android:paddingRight="10dip"
+ />
+
+ </LinearLayout>
+
+ </ScrollView>
+
+</LinearLayout>
+
diff --git a/app/src/main/res/layout/main_buttons.xml b/app/src/main/res/layout/main_buttons.xml
new file mode 100644
index 00000000..95a60409
--- /dev/null
+++ b/app/src/main/res/layout/main_buttons.xml
@@ -0,0 +1,157 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <LinearLayout
+ android:id="@+id/main_select_server"
+ android:orientation="horizontal"
+ android:paddingTop="2dip"
+ android:paddingBottom="2dip"
+ android:paddingLeft="6dp"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeight">
+
+ <ImageView
+ android:src="?attr/select_server"
+ android:layout_gravity="center_vertical"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+
+ <TextView android:id="@+id/main.select_server_1"
+ android:text="@string/main.select_server"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="10dip"
+ android:layout_marginTop="6dip"
+ android:textAppearance="?android:attr/textAppearanceLarge"/>
+
+ <TextView android:id="@+id/main.select_server_2"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="10dip"
+ android:textAppearance="?android:attr/textAppearanceSmall"/>
+
+ </LinearLayout>
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/main_offline"
+ android:text="@string/main.offline"
+ android:drawablePadding="12dip"
+ android:drawableLeft="?attr/offline_icon"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:gravity="center_vertical"
+ android:paddingLeft="6dp"
+ android:paddingBottom="4dp"
+ android:minHeight="50dip"/>
+
+ <LinearLayout
+ android:id="@+id/main_albums"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <TextView
+ android:text="@string/main.albums_title"
+ style="@style/MainAlbumButtonLabel"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="fill_parent"/>
+
+ <CheckBox
+ android:id="@+id/main_albums_per_folder"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/main.albums_per_folder"
+ android:layout_marginRight="6dp"
+ android:layout_gravity="right"/>
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/main_video_section"
+ android:text="@string/main.videos"
+ style="@style/MainAlbumButtonLabel"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="fill_parent"/>
+
+ <LinearLayout
+ android:id="@+id/main_albums_newest"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="46dip">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:text="@string/main.albums_newest"
+ style="@style/MainAlbumButton"/>
+
+ <TextView
+ android:id="@+id/main_albums_recent_count"
+ android:layout_width="32dp"
+ android:layout_height="32dp"
+ android:layout_marginRight="12dp"
+ android:background="@drawable/ic_number_border"
+ android:focusable="false"
+ android:paddingRight="10dp"
+ android:layout_marginLeft="20px"
+ android:layout_marginBottom="4px"
+ android:text="99"
+ android:textAppearance="?android:attr/textAppearanceSmallPopupMenu"
+ android:textSize="11sp"
+ android:gravity="right|center_vertical"
+ android:layout_gravity="center_vertical"
+ android:visibility="gone"/>
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/main_albums_recent"
+ android:text="@string/main.albums_recent"
+ style="@style/MainAlbumButton"/>
+ <TextView
+ android:id="@+id/main_albums_frequent"
+ android:text="@string/main.albums_frequent"
+ style="@style/MainAlbumButton"/>
+ <TextView
+ android:id="@+id/main_albums_highest"
+ android:text="@string/main.albums_highest"
+ style="@style/MainAlbumButton"/>
+ <TextView
+ android:id="@+id/main_albums_starred"
+ android:text="@string/main.albums_starred"
+ style="@style/MainAlbumButton"/>
+ <TextView
+ android:id="@+id/main_albums_genres"
+ android:text="@string/main.albums_genres"
+ style="@style/MainAlbumButton"/>
+ <TextView
+ android:id="@+id/main_albums_year"
+ android:text="@string/main.albums_year"
+ style="@style/MainAlbumButton"/>
+ <TextView
+ android:id="@+id/main_albums_random"
+ android:text="@string/main.albums_random"
+ style="@style/MainAlbumButton"/>
+ <TextView
+ android:id="@+id/main_albums_alphabetical"
+ android:text="@string/main.albums_alphabetical"
+ style="@style/MainAlbumButton"/>
+
+ <TextView
+ android:id="@+id/main_videos"
+ android:text="@string/main.videos"
+ style="@style/MainAlbumButton"/>
+
+</LinearLayout>
+
diff --git a/app/src/main/res/layout/notification.xml b/app/src/main/res/layout/notification.xml
new file mode 100644
index 00000000..12efa4ec
--- /dev/null
+++ b/app/src/main/res/layout/notification.xml
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/statusbar"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:orientation="horizontal" >
+
+ <ImageView
+ android:id="@+id/notification_image"
+ android:layout_width="64.0dip"
+ android:layout_height="64.0dip"
+ android:layout_weight="0.0"
+ android:gravity="center" />
+
+ <LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:paddingLeft="11.0dip"
+ android:layout_gravity="center_vertical">
+
+ <TextView
+ android:id="@+id/notification_title"
+ style="@android:style/TextAppearance.StatusBar.EventContent.Title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="left"
+ android:ellipsize="marquee"
+ android:focusable="true"
+ android:singleLine="true" />
+
+ <TextView
+ android:id="@+id/notification_artist"
+ style="@android:style/TextAppearance.StatusBar.EventContent"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="left"
+ android:ellipsize="end"
+ android:scrollHorizontally="true"
+ android:singleLine="true" />
+
+ <TextView
+ android:id="@+id/notification_album"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="gone"/>
+ </LinearLayout>
+
+ <ImageButton
+ android:id="@+id/control_previous"
+ android:src="@drawable/notification_previous"
+ style="@style/BasicButton"
+ android:layout_width="46dip"
+ android:layout_height="fill_parent"
+ android:layout_gravity="center|right"
+ android:padding="8.0dip"
+ android:layout_weight="0.0"
+ android:scaleType="fitCenter"/>
+
+ <ImageButton
+ android:id="@+id/control_pause"
+ android:src="@drawable/notification_pause"
+ style="@style/BasicButton"
+ android:layout_width="46dip"
+ android:layout_height="fill_parent"
+ android:layout_gravity="center|right"
+ android:padding="8.0dip"
+ android:layout_weight="0.0"
+ android:scaleType="fitCenter"/>
+
+ <ImageButton
+ android:id="@+id/control_next"
+ android:src="@drawable/notification_next"
+ style="@style/BasicButton"
+ android:layout_width="46dip"
+ android:layout_height="fill_parent"
+ android:layout_gravity="center|right"
+ android:padding="8.0dip"
+ android:layout_weight="0.0"
+ android:scaleType="fitCenter"/>
+</LinearLayout>
diff --git a/app/src/main/res/layout/notification_expanded.xml b/app/src/main/res/layout/notification_expanded.xml
new file mode 100644
index 00000000..aa9fe759
--- /dev/null
+++ b/app/src/main/res/layout/notification_expanded.xml
@@ -0,0 +1,123 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/statusbar"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:orientation="horizontal" >
+
+ <ImageView
+ android:id="@+id/notification_image"
+ android:layout_width="128dp"
+ android:layout_height="128dp"
+ android:gravity="center" />
+
+ <LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="0.0"
+ android:orientation="vertical"
+ android:paddingLeft="11.0dip" >
+
+ <LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/notification_title"
+ style="@android:style/TextAppearance.StatusBar.EventContent.Title"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_gravity="left"
+ android:ellipsize="marquee"
+ android:focusable="true"
+ android:singleLine="true" />
+
+ <ImageButton
+ android:id="@+id/notification_close"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:src="@drawable/notification_close"
+ style="@style/BasicButton"
+ android:padding="10dip"
+ android:layout_gravity="right"
+ android:visibility="gone"/>
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/notification_artist"
+ style="@android:style/TextAppearance.StatusBar.EventContent"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="left"
+ android:ellipsize="end"
+ android:scrollHorizontally="true"
+ android:singleLine="true" />
+
+ <TextView
+ android:id="@+id/notification_album"
+ style="@android:style/TextAppearance.StatusBar.EventContent"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="left"
+ android:ellipsize="end"
+ android:scrollHorizontally="true"
+ android:singleLine="true" />
+
+ <TextView
+ android:id="@+id/textView1"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent" />
+
+ <ImageView
+ android:background="?android:dividerHorizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="1.0px"/>
+
+ <LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:showDividers="middle"
+ android:divider="?android:listDivider">
+
+ <ImageButton
+ android:id="@+id/control_previous"
+ android:layout_width="0dip"
+ android:layout_height="fill_parent"
+ android:layout_gravity="center"
+ android:layout_weight="1"
+ android:padding="10dip"
+ style="@style/BasicButton"
+ android:scaleType="fitCenter"
+ android:src="@drawable/notification_previous" />
+
+ <ImageButton
+ android:id="@+id/control_pause"
+ android:layout_width="0dip"
+ android:layout_height="fill_parent"
+ android:layout_gravity="center"
+ android:layout_weight="1"
+ android:padding="10dip"
+ style="@style/BasicButton"
+ android:scaleType="fitCenter"
+ android:src="@drawable/notification_pause" />
+
+ <ImageButton
+ android:id="@+id/control_next"
+ android:layout_width="0dip"
+ android:layout_height="fill_parent"
+ android:layout_gravity="center"
+ android:layout_weight="1"
+ android:padding="10dip"
+ style="@style/BasicButton"
+ android:scaleType="fitCenter"
+ android:src="@drawable/notification_next" />
+ </LinearLayout>
+
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/app/src/main/res/layout/preferences.xml b/app/src/main/res/layout/preferences.xml
new file mode 100644
index 00000000..5caaa804
--- /dev/null
+++ b/app/src/main/res/layout/preferences.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ListView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@android:id/list"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:drawSelectorOnTop="false"
+ android:scrollbarAlwaysDrawVerticalTrack="true"
+ android:paddingTop="6dp"
+ android:paddingLeft="12dp"
+ android:paddingRight="12dp"/> \ No newline at end of file
diff --git a/app/src/main/res/layout/progress.xml b/app/src/main/res/layout/progress.xml
new file mode 100644
index 00000000..a1904c11
--- /dev/null
+++ b/app/src/main/res/layout/progress.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_weight="1"
+ android:layout_width="0dip"
+ android:layout_height="fill_parent"
+ android:padding="10dp">
+
+ <ProgressBar
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:layout_marginRight="10dp"/>
+
+ <TextView
+ android:id="@+id/progress_message"
+ android:text="@string/progress.wait"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"/>
+
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/rating.xml b/app/src/main/res/layout/rating.xml
new file mode 100644
index 00000000..2753ef68
--- /dev/null
+++ b/app/src/main/res/layout/rating.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <RatingBar
+ android:id="@+id/rating_bar"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:numStars="5"
+ android:stepSize="1"
+ android:layout_gravity="center_horizontal"/>
+
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/save_playlist.xml b/app/src/main/res/layout/save_playlist.xml
new file mode 100644
index 00000000..8bb21748
--- /dev/null
+++ b/app/src/main/res/layout/save_playlist.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/save_playlist_root"
+ android:padding="10dip"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+
+ <EditText
+ android:id="@+id/save_playlist_name"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:inputType="text"
+ android:singleLine="true"/>
+
+ <CheckBox
+ android:id="@+id/save_playlist_overwrite"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/playlist.overwrite"
+ android:layout_marginLeft="4dp"
+ android:checked="false"
+ android:visibility="gone"/>
+
+</LinearLayout>
+
diff --git a/app/src/main/res/layout/search_buttons.xml b/app/src/main/res/layout/search_buttons.xml
new file mode 100644
index 00000000..699ad341
--- /dev/null
+++ b/app/src/main/res/layout/search_buttons.xml
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/search_artists"
+ android:text="@string/search.artists"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="#EFEFEF"
+ android:textStyle="bold"
+ android:background="#A5A5A5"
+ android:gravity="center_vertical"
+ android:paddingLeft="4dp"/>
+
+ <TextView
+ android:id="@+id/search_albums"
+ android:text="@string/search.albums"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="#EFEFEF"
+ android:textStyle="bold"
+ android:background="#A5A5A5"
+ android:gravity="center_vertical"
+ android:paddingLeft="4dp"/>
+
+ <TextView
+ android:id="@+id/search_songs"
+ android:text="@string/search.songs"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="#EFEFEF"
+ android:textStyle="bold"
+ android:background="#A5A5A5"
+ android:gravity="center_vertical"
+ android:paddingLeft="4dp"/>
+
+ <TextView
+ android:id="@+id/search_more_artists"
+ android:text="@string/search.more"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:gravity="center"
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp"/>
+
+ <TextView
+ android:id="@+id/search_more_albums"
+ android:text="@string/search.more"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:gravity="center"
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp"/>
+
+ <TextView
+ android:id="@+id/search_more_songs"
+ android:text="@string/search.more"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:gravity="center"
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp"/>
+
+</LinearLayout>
diff --git a/app/src/main/res/layout/seekbar_preference.xml b/app/src/main/res/layout/seekbar_preference.xml
new file mode 100644
index 00000000..030b608b
--- /dev/null
+++ b/app/src/main/res/layout/seekbar_preference.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/value"
+ android:padding="5dip"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:gravity="right" />
+ <SeekBar
+ android:id="@+id/seek_bar"
+ android:padding="15dip"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content" />
+</LinearLayout>
diff --git a/app/src/main/res/layout/select_album.xml b/app/src/main/res/layout/select_album.xml
new file mode 100644
index 00000000..bbdf0e54
--- /dev/null
+++ b/app/src/main/res/layout/select_album.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.v4.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/refresh_layout"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+
+ <LinearLayout
+ android:id="@+id/select_album_layout"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+
+ <View
+ android:layout_width="fill_parent"
+ android:layout_height="1px"
+ android:background="@color/dividerColor"/>
+
+ <include layout="@layout/tab_progress"/>
+
+ <ListView
+ android:id="@+id/select_album_entries"
+ android:textFilterEnabled="true"
+ android:layout_width="fill_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1.0"
+ android:fastScrollEnabled="true"/>
+ </LinearLayout>
+</android.support.v4.widget.SwipeRefreshLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/select_album_header.xml b/app/src/main/res/layout/select_album_header.xml
new file mode 100644
index 00000000..d028a476
--- /dev/null
+++ b/app/src/main/res/layout/select_album_header.xml
@@ -0,0 +1,153 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/select_album_header_wrapper"
+ android:layout_height="wrap_content"
+ android:layout_width="fill_parent">
+
+ <RelativeLayout
+ android:id="@+id/select_album_header"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="top">
+
+ <LinearLayout
+ android:id="@+id/select_album_text_layout"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_toRightOf="@+id/select_album_art"
+ android:orientation="vertical"
+ android:layout_centerVertical="true">
+
+ <TextView
+ android:text="This is the album title"
+ android:id="@+id/select_album_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:textStyle="bold"
+ android:singleLine="true"
+ android:ellipsize="marquee"
+ android:marqueeRepeatLimit="marquee_forever"
+ android:scrollHorizontally="true"
+ android:focusable="true"
+ android:focusableInTouchMode="true">
+
+ <requestFocus android:focusable="true"
+ android:focusableInTouchMode="true"
+ android:duplicateParentState="true" />
+ </TextView>
+
+ <TextView
+ android:text="This is the artist name"
+ android:id="@+id/select_album_artist"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:singleLine="true"
+ android:ellipsize="end"/>
+
+ <TextView
+ android:text="XX SONGS"
+ android:id="@+id/select_album_song_count"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingTop="14dip"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:singleLine="true"
+ android:ellipsize="none"/>
+
+ <TextView
+ android:text="0:00"
+ android:id="@+id/select_album_song_length"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:singleLine="true"
+ android:ellipsize="none"/>
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentRight="true"
+ android:padding="10dip">
+
+ <LinearLayout
+ android:id="@+id/select_album_rate_wrapper"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent">
+
+ <RatingBar
+ android:id="@+id/select_album_rate"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:numStars="5"
+ style="@android:style/Widget.Holo.RatingBar.Small"
+ android:layout_gravity="center_vertical"/>
+ </LinearLayout>
+
+ <ImageButton
+ android:id="@+id/select_album_star"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ style="@style/BasicButton"
+ android:src="@android:drawable/star_big_off"
+ android:layout_gravity="center_vertical"/>
+
+ <ImageView
+ android:id="@+id/select_album_share"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:scaleType="fitCenter"
+ android:src="?attr/share"
+ style="@style/BasicButton"
+ android:layout_gravity="center_vertical"/>
+ </LinearLayout>
+
+ <github.daneren2005.dsub.view.RecyclingImageView
+ android:id="@+id/select_album_art"
+ android:layout_width="@dimen/AlbumArt.Header"
+ android:layout_height="@dimen/AlbumArt.Header"
+ android:layout_alignParentTop="true"
+ android:layout_alignParentLeft="true"
+ android:layout_marginRight="10dip"
+ android:scaleType="fitCenter"
+ android:contentDescription="@null"/>
+ </RelativeLayout>
+
+ <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/header_progress"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:clickable="true"
+ android:visibility="gone"
+ android:layout_gravity="top">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:layout_gravity="center">
+
+ <ProgressBar
+ android:id="@+id/tab_progress_spinner"
+ style="?android:attr/progressBarStyleLarge"
+ android:layout_gravity="center_horizontal"
+ android:layout_marginRight="6dp"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+ <TextView
+ android:id="@+id/tab_progress_message"
+ android:text="@string/progress.artist_info"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:textAppearance="?android:attr/textAppearanceMedium"/>
+ </LinearLayout>
+ </FrameLayout>
+</FrameLayout>
+
+
diff --git a/app/src/main/res/layout/select_artist_header.xml b/app/src/main/res/layout/select_artist_header.xml
new file mode 100644
index 00000000..2821ce43
--- /dev/null
+++ b/app/src/main/res/layout/select_artist_header.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+ <LinearLayout
+ android:id="@+id/select_artist_folder"
+ android:orientation="horizontal"
+ android:paddingTop="2dip"
+ android:paddingBottom="2dip"
+ android:paddingLeft="6dp"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeight">
+
+ <ImageView
+ android:src="?attr/select_server"
+ android:layout_gravity="center_vertical"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+
+ <TextView android:id="@+id/select_artist_folder_1"
+ android:text="@string/select_artist.folder"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="10dip"
+ android:layout_marginTop="6dip"
+ android:textAppearance="?android:attr/textAppearanceLarge"/>
+
+ <TextView android:id="@+id/select_artist_folder_2"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="10dip"
+ android:textAppearance="?android:attr/textAppearanceSmall"/>
+
+ </LinearLayout>
+ </LinearLayout>
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/shuffle_dialog.xml b/app/src/main/res/layout/shuffle_dialog.xml
new file mode 100644
index 00000000..295f57cb
--- /dev/null
+++ b/app/src/main/res/layout/shuffle_dialog.xml
@@ -0,0 +1,80 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/start_year_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="4dp"
+ android:textSize="20dp"
+ android:text="@string/shuffle.startYear" />
+ <EditText
+ android:id="@+id/start_year"
+ android:inputType="number"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginLeft="4dp"
+ android:hint="@string/shuffle.startYear" />
+ </LinearLayout>
+
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/end_year_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="4dp"
+ android:textSize="20dp"
+ android:text="@string/shuffle.endYear" />
+ <EditText
+ android:id="@+id/end_year"
+ android:inputType="number"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginLeft="4dp"
+ android:hint="@string/shuffle.endYear" />
+ </LinearLayout>
+
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/genre_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="4dp"
+ android:textSize="20dp"
+ android:text="@string/shuffle.genre" />
+ <EditText
+ android:id="@+id/genre"
+ android:inputType="text"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginLeft="4dp"
+ android:hint="@string/shuffle.genre"/>
+
+ <Button
+ android:id="@+id/genre_combo"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginLeft="4dp"
+ android:text="@string/shuffle.genre"
+ style="?android:attr/spinnerStyle"/>
+ </LinearLayout>
+</LinearLayout>
diff --git a/app/src/main/res/layout/song_list_item.xml b/app/src/main/res/layout/song_list_item.xml
new file mode 100644
index 00000000..86f77869
--- /dev/null
+++ b/app/src/main/res/layout/song_list_item.xml
@@ -0,0 +1,126 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@id/drag_handle"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="?android:attr/listPreferredItemHeight">
+
+ <CheckedTextView
+ android:id="@+id/song_check"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:gravity="center_vertical"
+ android:checkMark="?android:attr/listChoiceIndicatorMultiple"
+ android:paddingLeft="3dip"/>
+
+ <LinearLayout android:orientation="vertical"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_gravity="center_vertical">
+
+ <LinearLayout android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical">
+
+ <TextView
+ android:id="@+id/song_title"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_gravity="left|center_vertical"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:singleLine="true"
+ android:ellipsize="marquee"
+ android:drawablePadding="6dip"
+ android:paddingLeft="6dip"
+ android:paddingRight="6dip"/>
+
+ <ImageButton
+ android:id="@+id/song_bookmark"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_gravity="right|center_vertical"
+ android:src="@drawable/ic_menu_bookmark_selected"
+ android:background="@null"
+ android:focusable="false"
+ android:scaleType="fitCenter"
+ android:visibility="gone"/>
+
+ <ImageButton
+ android:id="@+id/song_star"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="right|center_vertical"
+ android:src="@drawable/ic_stat_star"
+ android:background="@null"
+ android:focusable="false"
+ android:visibility="gone"/>
+
+ <TextView
+ android:id="@+id/song_status"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="right|center_vertical"
+ android:drawablePadding="1dip"
+ android:paddingRight="2dip"/>
+
+ <ImageView
+ android:id="@+id/song_status_icon"
+ android:layout_width="24dip"
+ android:layout_height="24dip"
+ android:layout_gravity="center_vertical"
+ android:src="?attr/downloading"
+ android:visibility="gone"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/song_bottom"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical">
+
+ <TextView
+ android:id="@+id/song_artist"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_gravity="left|center_vertical"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:singleLine="true"
+ android:ellipsize="middle"
+ android:paddingLeft="6dip"/>
+
+ <RatingBar
+ android:id="@+id/song_rating"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:isIndicator="true"
+ android:layout_centerHorizontal="true"
+ android:numStars="5"
+ style="@android:style/Widget.Holo.RatingBar.Small"
+ android:visibility="gone"/>
+
+ <TextView
+ android:id="@+id/song_duration"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="right|center_vertical"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:singleLine="true"
+ android:paddingLeft="3dip"
+ android:paddingRight="4dip"/>
+
+ </LinearLayout>
+ </LinearLayout>
+
+ <ImageView
+ android:id="@+id/artist_more"
+ android:src="?attr/download_none"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:layout_gravity="right|center_vertical"
+ style="@style/MoreButton"/>
+</LinearLayout>
diff --git a/app/src/main/res/layout/start_timer.xml b/app/src/main/res/layout/start_timer.xml
new file mode 100644
index 00000000..9736a31d
--- /dev/null
+++ b/app/src/main/res/layout/start_timer.xml
@@ -0,0 +1,21 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center">
+
+ <TextView
+ android:id="@+id/timer_length_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="8dp"
+ android:textSize="20dp"
+ android:paddingRight="10px"
+ android:layout_gravity="center"/>
+
+ <SeekBar
+ android:id="@+id/timer_length_bar"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:max="97"/>
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/static_drawer_activity.xml b/app/src/main/res/layout/static_drawer_activity.xml
new file mode 100644
index 00000000..db631540
--- /dev/null
+++ b/app/src/main/res/layout/static_drawer_activity.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/drawer_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="horizontal">
+
+ <!-- The navigation drawer -->
+ <ListView android:id="@+id/left_drawer"
+ android:layout_width="240dp"
+ android:layout_height="match_parent"
+ android:layout_gravity="start"
+ android:choiceMode="singleChoice"
+ android:divider="@android:color/transparent"
+ android:dividerHeight="0dp"
+ android:background="?android:windowBackground"/>
+
+ <!-- The main content view -->
+ <FrameLayout
+ android:id="@+id/content_frame"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/sync_dialog.xml b/app/src/main/res/layout/sync_dialog.xml
new file mode 100644
index 00000000..5133b753
--- /dev/null
+++ b/app/src/main/res/layout/sync_dialog.xml
@@ -0,0 +1,12 @@
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content" >
+ <CheckBox
+ android:id="@+id/sync_default"
+ style="?android:attr/textAppearanceMedium"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="5dp"
+ android:checked="false"
+ android:text="@string/offline.sync_dialog_default"/>
+</FrameLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/tab_progress.xml b/app/src/main/res/layout/tab_progress.xml
new file mode 100644
index 00000000..f6d326ca
--- /dev/null
+++ b/app/src/main/res/layout/tab_progress.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/tab_progress"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:clickable="true"
+ android:visibility="gone">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:layout_gravity="center">
+
+ <ProgressBar
+ android:id="@+id/tab_progress_spinner"
+ style="?android:attr/progressBarStyleLarge"
+ android:layout_gravity="center_horizontal"
+ android:layout_marginRight="6dp"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+ <TextView
+ android:id="@+id/tab_progress_message"
+ android:text="@string/progress.wait"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:textAppearance="?android:attr/textAppearanceMedium"/>
+ </LinearLayout>
+</FrameLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/unscrollable_grid_view.xml b/app/src/main/res/layout/unscrollable_grid_view.xml
new file mode 100644
index 00000000..96bea5ce
--- /dev/null
+++ b/app/src/main/res/layout/unscrollable_grid_view.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<github.daneren2005.dsub.view.UnscrollableGridView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/gridview"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:numColumns="@integer/Grid.Columns"
+ android:horizontalSpacing="10dp"
+ android:verticalSpacing="10dp"
+ android:gravity="center"
+ android:padding="20px"
+ android:stretchMode="columnWidth"/> \ No newline at end of file
diff --git a/app/src/main/res/layout/update_playlist.xml b/app/src/main/res/layout/update_playlist.xml
new file mode 100644
index 00000000..7354ef5c
--- /dev/null
+++ b/app/src/main/res/layout/update_playlist.xml
@@ -0,0 +1,70 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/get_playlist_name_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="4dp"
+ android:textSize="20dp"
+ android:text="@string/common.name" />
+ <EditText
+ android:id="@+id/get_playlist_name"
+ android:inputType="text"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginLeft="4dp"
+ android:hint="@string/common.name" />
+ </LinearLayout>
+
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/get_playlist_comment_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="4dp"
+ android:textSize="20dp"
+ android:text="@string/common.comment" />
+ <EditText
+ android:id="@+id/get_playlist_comment"
+ android:inputType="text"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginLeft="4dp"
+ android:hint="@string/common.comment" />
+ </LinearLayout>
+
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/get_playlist_public_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="4dp"
+ android:textSize="20dp"
+ android:text="@string/common.public" />
+ <CheckBox
+ android:id="@+id/get_playlist_public"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginLeft="4dp"
+ android:checked="false"/>
+ </LinearLayout>
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/update_share.xml b/app/src/main/res/layout/update_share.xml
new file mode 100644
index 00000000..92b7137b
--- /dev/null
+++ b/app/src/main/res/layout/update_share.xml
@@ -0,0 +1,69 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/get_share_name_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="4dp"
+ android:textSize="20dp"
+ android:text="@string/common.name" />
+ <EditText
+ android:id="@+id/get_share_name"
+ android:inputType="text"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginLeft="4dp"
+ android:hint="@string/common.name" />
+ </LinearLayout>
+
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/get_share_expire_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="4dp"
+ android:textSize="20dp"
+ android:text="@string/share.expiration" />
+ <DatePicker
+ android:id="@+id/get_share_expire"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginLeft="4dp"
+ android:calendarViewShown="false"/>
+ </LinearLayout>
+
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/get_share_no_expire_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="4dp"
+ android:textSize="20dp"
+ android:text="@string/share.no_expiration" />
+ <CheckBox
+ android:id="@+id/get_share_no_expire"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginLeft="4dp"
+ android:checked="false"/>
+ </LinearLayout>
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/user_header.xml b/app/src/main/res/layout/user_header.xml
new file mode 100644
index 00000000..0b303afe
--- /dev/null
+++ b/app/src/main/res/layout/user_header.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/select_album_header"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <github.daneren2005.dsub.view.RecyclingImageView
+ android:id="@+id/user_avatar"
+ android:src="@drawable/ic_social_person"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ android:layout_alignParentLeft="true"
+ android:layout_marginRight="10dip"
+ android:scaleType="fitCenter"
+ android:contentDescription="@null"/>
+
+ <LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_toRightOf="@+id/user_avatar"
+ android:orientation="vertical"
+ android:layout_centerVertical="true">
+
+ <TextView
+ android:text="Username"
+ android:id="@+id/user_username"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:textStyle="bold"
+ android:singleLine="true"
+ android:ellipsize="marquee"
+ android:marqueeRepeatLimit="marquee_forever"
+ android:scrollHorizontally="true"
+ android:focusable="true"
+ android:focusableInTouchMode="true">
+
+ <requestFocus android:focusable="true"
+ android:focusableInTouchMode="true"
+ android:duplicateParentState="true" />
+ </TextView>
+
+ <TextView
+ android:text="Email"
+ android:id="@+id/user_email"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:singleLine="true"
+ android:ellipsize="end"
+ android:autoLink="email"/>
+
+ </LinearLayout>
+</RelativeLayout>
+
+
diff --git a/app/src/main/res/layout/user_list_item.xml b/app/src/main/res/layout/user_list_item.xml
new file mode 100644
index 00000000..dc2bdab9
--- /dev/null
+++ b/app/src/main/res/layout/user_list_item.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:background="@android:color/transparent">
+
+ <github.daneren2005.dsub.view.RecyclingImageView
+ android:id="@+id/item_avatar"
+ android:src="@drawable/ic_social_person"
+ android:layout_width="@dimen/AlbumArt.Small"
+ android:layout_height="@dimen/AlbumArt.Small"
+ android:layout_gravity="left|center_vertical"/>
+
+ <TextView
+ android:id="@+id/item_name"
+ android:layout_width="0dip"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:gravity="left|center_vertical"
+ android:paddingLeft="12dip"
+ android:paddingRight="6dip"
+ android:minHeight="50dip"
+ android:background="@android:color/transparent"/>
+
+ <ImageButton
+ android:id="@+id/item_star"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="right|center_vertical"
+ android:src="@drawable/ic_stat_star"
+ android:background="@android:color/transparent"
+ android:focusable="false"
+ android:visibility="gone"/>
+
+ <ImageView
+ android:id="@+id/item_more"
+ android:src="?attr/download_none"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:layout_gravity="right|center_vertical"
+ style="@style/MoreButton"/>
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/menu/abstract_top_menu.xml b/app/src/main/res/menu/abstract_top_menu.xml
new file mode 100644
index 00000000..22499ae9
--- /dev/null
+++ b/app/src/main/res/menu/abstract_top_menu.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:compat="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/menu_search"
+ android:icon="?attr/search"
+ android:title="@string/menu.search"
+ compat:showAsAction="always|withText"/>
+
+ <group android:id="@+id/not_touchscreen">
+ <item
+ android:id="@+id/menu_refresh"
+ android:icon="?attr/refresh"
+ android:title="@string/menu.refresh"
+ compat:showAsAction="ifRoom|withText"/>
+ </group>
+
+ <item
+ android:id="@+id/menu_exit"
+ android:title="@string/menu.exit"/>
+</menu>
diff --git a/app/src/main/res/menu/admin.xml b/app/src/main/res/menu/admin.xml
new file mode 100644
index 00000000..28c5134b
--- /dev/null
+++ b/app/src/main/res/menu/admin.xml
@@ -0,0 +1,12 @@
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:compat="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/menu_add_user"
+ android:title="@string/menu.add_user"
+ android:icon="?attr/add_person"
+ compat:showAsAction="always|withText"/>
+
+ <item
+ android:id="@+id/menu_exit"
+ android:title="@string/menu.exit"/>
+</menu> \ No newline at end of file
diff --git a/app/src/main/res/menu/admin_context.xml b/app/src/main/res/menu/admin_context.xml
new file mode 100644
index 00000000..e4c8fdc3
--- /dev/null
+++ b/app/src/main/res/menu/admin_context.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:compat="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@+id/admin_change_email"
+ android:title="@string/admin.change_email"/>
+
+ <item
+ android:id="@+id/admin_change_password"
+ android:title="@string/admin.change_password"/>
+
+ <item
+ android:id="@+id/admin_delete_user"
+ android:title="@string/admin.delete_user"/>
+</menu>
diff --git a/app/src/main/res/menu/admin_context_user.xml b/app/src/main/res/menu/admin_context_user.xml
new file mode 100644
index 00000000..d53eee49
--- /dev/null
+++ b/app/src/main/res/menu/admin_context_user.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:compat="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@+id/admin_change_password"
+ android:title="@string/admin.change_password"/>
+</menu> \ No newline at end of file
diff --git a/app/src/main/res/menu/downloading.xml b/app/src/main/res/menu/downloading.xml
new file mode 100644
index 00000000..670a2803
--- /dev/null
+++ b/app/src/main/res/menu/downloading.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:compat="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/menu_remove_all"
+ android:icon="?attr/remove"
+ android:title="@string/download.menu_remove_all"
+ compat:showAsAction="always|withText"/>
+
+ <item
+ android:id="@+id/menu_exit"
+ android:title="@string/menu.exit"/>
+</menu>
diff --git a/app/src/main/res/menu/drawer_menu.xml b/app/src/main/res/menu/drawer_menu.xml
new file mode 100644
index 00000000..b3e70cfa
--- /dev/null
+++ b/app/src/main/res/menu/drawer_menu.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:compat="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@+id/menu_search"
+ android:icon="?attr/search"
+ android:title="@string/menu.search"
+ compat:showAsAction="always|withText"/>
+
+ <item
+ android:id="@+id/menu_exit"
+ android:title="@string/menu.exit"/>
+</menu>
diff --git a/app/src/main/res/menu/empty.xml b/app/src/main/res/menu/empty.xml
new file mode 100644
index 00000000..eae036cf
--- /dev/null
+++ b/app/src/main/res/menu/empty.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:compat="http://schemas.android.com/apk/res-auto">
+
+ <group android:id="@+id/not_touchscreen">
+ <item
+ android:id="@+id/menu_refresh"
+ android:icon="?attr/refresh"
+ android:title="@string/menu.refresh"
+ compat:showAsAction="ifRoom|withText"/>
+ </group>
+
+ <item
+ android:id="@+id/menu_exit"
+ android:title="@string/menu.exit"/>
+</menu>
diff --git a/app/src/main/res/menu/main.xml b/app/src/main/res/menu/main.xml
new file mode 100644
index 00000000..549c5fb6
--- /dev/null
+++ b/app/src/main/res/menu/main.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:compat="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/menu_search"
+ android:icon="?attr/search"
+ android:title="@string/menu.search"
+ compat:showAsAction="always|withText"/>
+
+ <item
+ android:id="@+id/menu_shuffle"
+ android:icon="?attr/shuffle"
+ android:title="@string/menu.shuffle"
+ compat:showAsAction="always|withText"/>
+
+ <group android:id="@+id/madsonic">
+ <item
+ android:id="@+id/menu_rescan"
+ android:title="@string/menu.rescan"/>
+ </group>
+
+ <item
+ android:id="@+id/menu_about"
+ android:title="@string/menu.about"/>
+
+ <item
+ android:id="@+id/menu_faq"
+ android:title="@string/menu.faq"/>
+
+ <item
+ android:id="@+id/menu_log"
+ android:title="@string/menu.log"/>
+
+ <item
+ android:id="@+id/menu_changelog"
+ android:title="@string/changelog_full_title"/>
+
+ <item
+ android:id="@+id/menu_exit"
+ android:title="@string/menu.exit"/>
+</menu>
diff --git a/app/src/main/res/menu/nowplaying.xml b/app/src/main/res/menu/nowplaying.xml
new file mode 100644
index 00000000..60255692
--- /dev/null
+++ b/app/src/main/res/menu/nowplaying.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:compat="http://schemas.android.com/apk/res-auto"
+ android:checkableBehavior="all">
+
+ <item
+ android:id="@+id/menu_shuffle"
+ android:icon="?attr/shuffle"
+ android:title="@string/download.menu_shuffle"
+ compat:showAsAction="ifRoom|withText"/>
+
+ <item
+ android:id="@+id/menu_mediaroute"
+ compat:actionProviderClass="android.support.v7.app.MediaRouteActionProvider"
+ compat:actionViewClass="android.support.v7.app.MediaRouteButton"
+ compat:showAsAction="always"
+ android:title="@string/menu.cast"/>
+
+ <item
+ android:id="@+id/menu_remove_all"
+ android:icon="?attr/remove"
+ android:title="@string/download.menu_remove_all"
+ compat:showAsAction="ifRoom|withText"/>
+
+ <item
+ android:id="@+id/menu_save_playlist"
+ android:icon="?attr/save"
+ android:title="@string/download.menu_save"
+ compat:showAsAction="ifRoom|withText"/>
+
+ <item
+ android:id="@+id/menu_equalizer"
+ android:title="@string/equalizer.label"
+ android:checkable="true"/>
+
+ <item
+ android:id="@+id/menu_screen_on_off"
+ android:title="@string/download.menu_screen_on"
+ android:checkable="true"/>
+
+ <item
+ android:id="@+id/menu_remove_played"
+ android:title="@string/download.menu_remove_played_songs"
+ android:checkable="true"/>
+
+ <item
+ android:id="@+id/menu_toggle_timer"
+ android:title="@string/download.start_timer"/>
+
+ <item
+ android:id="@+id/menu_exit"
+ android:title="@string/menu.exit"/>
+</menu>
diff --git a/app/src/main/res/menu/nowplaying_context.xml b/app/src/main/res/menu/nowplaying_context.xml
new file mode 100644
index 00000000..c9347353
--- /dev/null
+++ b/app/src/main/res/menu/nowplaying_context.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:compat="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/menu_info"
+ android:title="@string/common.info"
+ />
+
+ <item
+ android:id="@+id/menu_show_artist"
+ android:title="@string/menu.show_artist"/>
+
+ <item
+ android:id="@+id/menu_show_album"
+ android:title="@string/download.menu_show_album"/>
+
+ <item
+ android:id="@+id/menu_lyrics"
+ android:title="@string/download.menu_lyrics"/>
+
+ <item
+ android:id="@+id/menu_remove"
+ android:title="@string/download.menu_remove"/>
+
+ <item
+ android:id="@+id/menu_delete"
+ android:title="@string/menu.delete_cache"/>
+
+ <group android:id="@+id/hide_star">
+ <item
+ android:id="@+id/menu_star"
+ android:title="@string/common.star"/>
+ </group>
+
+ <group android:id="@+id/hide_share">
+ <item
+ android:id="@+id/menu_share"
+ android:title="@string/menu.share"/>
+ </group>
+
+ <group android:id="@+id/hide_rating">
+ <item
+ android:id="@+id/menu_rate"
+ android:title="@string/menu.rate"/>
+ </group>
+
+ <group android:id="@+id/server_1.8">
+ <item
+ android:id="@+id/menu_add_playlist"
+ android:title="@string/menu.add_playlist"/>
+ </group>
+</menu>
diff --git a/app/src/main/res/menu/nowplaying_context_offline.xml b/app/src/main/res/menu/nowplaying_context_offline.xml
new file mode 100644
index 00000000..24b23a8d
--- /dev/null
+++ b/app/src/main/res/menu/nowplaying_context_offline.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:compat="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/menu_info"
+ android:title="@string/common.info"
+ />
+
+ <item
+ android:id="@+id/menu_show_artist"
+ android:title="@string/menu.show_artist"/>
+
+ <item
+ android:id="@+id/menu_show_album"
+ android:title="@string/download.menu_show_album"/>
+
+ <item
+ android:id="@+id/menu_remove"
+ android:title="@string/download.menu_remove"/>
+
+ <item
+ android:id="@+id/menu_delete"
+ android:title="@string/menu.delete_cache"/>
+
+ <group android:id="@+id/hide_star">
+ <item
+ android:id="@+id/menu_star"
+ android:title="@string/common.star"/>
+ </group>
+</menu>
diff --git a/app/src/main/res/menu/nowplaying_offline.xml b/app/src/main/res/menu/nowplaying_offline.xml
new file mode 100644
index 00000000..bba5ba00
--- /dev/null
+++ b/app/src/main/res/menu/nowplaying_offline.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:compat="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@+id/menu_shuffle"
+ android:icon="?attr/shuffle"
+ android:title="@string/download.menu_shuffle"
+ compat:showAsAction="always|withText"/>
+
+ <item
+ android:id="@+id/menu_mediaroute"
+ compat:actionProviderClass="android.support.v7.app.MediaRouteActionProvider"
+ compat:actionViewClass="android.support.v7.app.MediaRouteButton"
+ compat:showAsAction="always"
+ android:title="@string/menu.cast"/>
+
+ <item
+ android:id="@+id/menu_remove_all"
+ android:icon="?attr/remove"
+ android:title="@string/download.menu_remove_all"
+ compat:showAsAction="ifRoom|withText"/>
+
+ <item
+ android:id="@+id/menu_equalizer"
+ android:title="@string/equalizer.label"
+ android:checkable="true"/>
+
+ <item
+ android:id="@+id/menu_screen_on_off"
+ android:title="@string/download.menu_screen_on"
+ android:checkable="true"/>
+
+ <item
+ android:id="@+id/menu_remove_played"
+ android:title="@string/download.menu_remove_played_songs"
+ android:checkable="true"/>
+
+ <item
+ android:id="@+id/menu_toggle_timer"
+ android:title="@string/download.start_timer"/>
+
+ <item
+ android:id="@+id/menu_exit"
+ android:title="@string/menu.exit"/>
+</menu>
diff --git a/app/src/main/res/menu/search.xml b/app/src/main/res/menu/search.xml
new file mode 100644
index 00000000..cab9c4f6
--- /dev/null
+++ b/app/src/main/res/menu/search.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:compat="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/menu_search"
+ android:icon="?attr/search"
+ android:title="@string/menu.search"
+ compat:showAsAction="ifRoom|withText"/>
+
+ <item
+ android:id="@+id/menu_exit"
+ android:title="@string/menu.exit"/>
+
+</menu>
diff --git a/app/src/main/res/menu/select_album.xml b/app/src/main/res/menu/select_album.xml
new file mode 100644
index 00000000..3d2228e8
--- /dev/null
+++ b/app/src/main/res/menu/select_album.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:compat="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/menu_play_now"
+ android:icon="?media_button_start"
+ android:title="@string/menu.play"
+ compat:showAsAction="always|withText"/>
+
+ <item
+ android:id="@+id/menu_radio"
+ android:icon="?attr/radio"
+ android:title="@string/menu.start_radio"
+ compat:showAsAction="ifRoom|withText"/>
+
+ <item
+ android:id="@+id/menu_shuffle"
+ android:icon="?attr/shuffle"
+ android:title="@string/menu.shuffle"
+ compat:showAsAction="ifRoom|withText"/>
+
+ <group android:id="@+id/not_touchscreen">
+ <item
+ android:id="@+id/menu_refresh"
+ android:icon="?attr/refresh"
+ android:title="@string/menu.refresh"
+ compat:showAsAction="ifRoom|withText"/>
+ </group>
+
+ <item
+ android:id="@+id/menu_top_tracks"
+ android:title="@string/menu.top_tracks"/>
+
+ <item
+ android:id="@+id/menu_similar_artists"
+ android:title="@string/menu.similar_artists"/>
+
+ <item
+ android:id="@+id/menu_show_all"
+ android:title="@string/menu.show_all"/>
+
+ <item
+ android:id="@+id/menu_download"
+ android:title="@string/common.download"/>
+
+ <item
+ android:id="@+id/menu_cache"
+ android:title="@string/common.pin"/>
+
+ <item
+ android:id="@+id/menu_delete"
+ android:title="@string/menu.delete_cache"/>
+</menu>
diff --git a/app/src/main/res/menu/select_album_context.xml b/app/src/main/res/menu/select_album_context.xml
new file mode 100644
index 00000000..5b2529e7
--- /dev/null
+++ b/app/src/main/res/menu/select_album_context.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:compat="http://schemas.android.com/apk/res-auto">
+
+
+ <item
+ android:id="@+id/album_menu_info"
+ android:title="@string/common.info"/>
+
+ <item
+ android:id="@+id/album_menu_play_now"
+ android:title="@string/common.play_now"
+ />
+
+ <item
+ android:id="@+id/album_menu_play_shuffled"
+ android:title="@string/common.play_shuffled"
+ />
+
+ <group android:id="@+id/hide_play_next">
+ <item
+ android:id="@+id/album_menu_play_next"
+ android:title="@string/common.play_next"/>
+ </group>
+
+ <group android:id="@+id/hide_play_last">
+ <item
+ android:id="@+id/album_menu_play_last"
+ android:title="@string/common.play_last"/>
+ </group>
+
+ <item
+ android:id="@+id/album_menu_download"
+ android:title="@string/common.download"
+ />
+
+ <item
+ android:id="@+id/album_menu_pin"
+ android:title="@string/common.pin"
+ />
+
+ <item
+ android:id="@+id/album_menu_delete"
+ android:title="@string/menu.delete_cache"/>
+
+ <item
+ android:id="@+id/album_menu_show_artist"
+ android:title="@string/menu.show_artist"/>
+
+ <group android:id="@+id/hide_star">
+ <item
+ android:id="@+id/album_menu_star"
+ android:title="@string/common.star"/>
+ </group>
+
+ <group android:id="@+id/hide_share">
+ <item
+ android:id="@+id/album_menu_share"
+ android:title="@string/menu.share"/>
+ </group>
+
+ <group android:id="@+id/hide_rating">
+ <item
+ android:id="@+id/menu_rate"
+ android:title="@string/menu.rate"/>
+ </group>
+</menu>
diff --git a/app/src/main/res/menu/select_album_context_offline.xml b/app/src/main/res/menu/select_album_context_offline.xml
new file mode 100644
index 00000000..a1805f5b
--- /dev/null
+++ b/app/src/main/res/menu/select_album_context_offline.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:compat="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/album_menu_play_now"
+ android:title="@string/common.play_now"
+ />
+
+ <item
+ android:id="@+id/album_menu_play_shuffled"
+ android:title="@string/common.play_shuffled"
+ />
+
+ <group android:id="@+id/hide_play_next">
+ <item
+ android:id="@+id/album_menu_play_next"
+ android:title="@string/common.play_next"/>
+ </group>
+
+ <group android:id="@+id/hide_play_last">
+ <item
+ android:id="@+id/album_menu_play_last"
+ android:title="@string/common.play_last"/>
+ </group>
+
+ <item
+ android:id="@+id/album_menu_delete"
+ android:title="@string/menu.delete_cache"/>
+
+ <item
+ android:id="@+id/album_menu_star"
+ android:title="@string/common.star"/>
+</menu>
diff --git a/app/src/main/res/menu/select_album_list.xml b/app/src/main/res/menu/select_album_list.xml
new file mode 100644
index 00000000..a9196d1c
--- /dev/null
+++ b/app/src/main/res/menu/select_album_list.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:compat="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/menu_play_now"
+ android:icon="?media_button_start"
+ android:title="@string/menu.play"
+ compat:showAsAction="always|withText"/>
+
+ <item
+ android:id="@+id/menu_shuffle"
+ android:icon="?attr/shuffle"
+ android:title="@string/menu.shuffle"
+ compat:showAsAction="ifRoom|withText"/>
+
+ <group android:id="@+id/not_touchscreen">
+ <item
+ android:id="@+id/menu_refresh"
+ android:icon="?attr/refresh"
+ android:title="@string/menu.refresh"
+ compat:showAsAction="ifRoom|withText"/>
+ </group>
+
+ <item
+ android:id="@+id/menu_exit"
+ android:title="@string/menu.exit"/>
+</menu>
diff --git a/app/src/main/res/menu/select_artist.xml b/app/src/main/res/menu/select_artist.xml
new file mode 100644
index 00000000..603f0a3b
--- /dev/null
+++ b/app/src/main/res/menu/select_artist.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:compat="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/menu_shuffle"
+ android:icon="?attr/shuffle"
+ android:title="@string/menu.shuffle"
+ compat:showAsAction="always|withText"/>
+
+ <item
+ android:id="@+id/menu_search"
+ android:icon="?attr/search"
+ android:title="@string/menu.search"
+ compat:showAsAction="ifRoom|withText"/>
+
+ <group android:id="@+id/not_touchscreen">
+ <item
+ android:id="@+id/menu_refresh"
+ android:icon="?attr/refresh"
+ android:title="@string/menu.refresh"
+ compat:showAsAction="ifRoom|withText"/>
+ </group>
+
+ <item
+ android:id="@+id/menu_first_level_artist"
+ android:title="@string/menu.first_level_artist"
+ android:checkable="true"/>
+
+ <item
+ android:id="@+id/menu_exit"
+ android:title="@string/menu.exit"/>
+</menu>
diff --git a/app/src/main/res/menu/select_artist_context.xml b/app/src/main/res/menu/select_artist_context.xml
new file mode 100644
index 00000000..debc07c6
--- /dev/null
+++ b/app/src/main/res/menu/select_artist_context.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:compat="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@+id/artist_menu_play_now"
+ android:title="@string/common.play_now"
+ />
+
+ <item
+ android:id="@+id/artist_menu_play_shuffled"
+ android:title="@string/common.play_shuffled"
+ />
+
+ <group android:id="@+id/hide_play_next">
+ <item
+ android:id="@+id/artist_menu_play_next"
+ android:title="@string/common.play_next"/>
+ </group>
+
+ <group android:id="@+id/hide_play_last">
+ <item
+ android:id="@+id/artist_menu_play_last"
+ android:title="@string/common.play_last"/>
+ </group>
+
+ <item
+ android:id="@+id/artist_menu_download"
+ android:title="@string/common.download"
+ />
+
+ <item
+ android:id="@+id/artist_menu_pin"
+ android:title="@string/common.pin"
+ />
+
+ <item
+ android:id="@+id/artist_menu_delete"
+ android:title="@string/menu.delete_cache"/>
+
+ <group android:id="@+id/server_1_10">
+ <group android:id="@+id/hide_star">
+ <item
+ android:id="@+id/artist_menu_star"
+ android:title="@string/common.star"/>
+ </group>
+ </group>
+</menu>
diff --git a/app/src/main/res/menu/select_artist_context_offline.xml b/app/src/main/res/menu/select_artist_context_offline.xml
new file mode 100644
index 00000000..17ee97e0
--- /dev/null
+++ b/app/src/main/res/menu/select_artist_context_offline.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:compat="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@+id/artist_menu_play_now"
+ android:title="@string/common.play_now"
+ />
+
+ <item
+ android:id="@+id/artist_menu_play_shuffled"
+ android:title="@string/common.play_shuffled"
+ />
+
+ <group android:id="@+id/hide_play_next">
+ <item
+ android:id="@+id/artist_menu_play_next"
+ android:title="@string/common.play_next"/>
+ </group>
+
+ <group android:id="@+id/hide_play_last">
+ <item
+ android:id="@+id/artist_menu_play_last"
+ android:title="@string/common.play_last"/>
+ </group>
+
+ <item
+ android:id="@+id/artist_menu_delete"
+ android:title="@string/menu.delete_cache"/>
+</menu>
diff --git a/app/src/main/res/menu/select_bookmark_context.xml b/app/src/main/res/menu/select_bookmark_context.xml
new file mode 100644
index 00000000..2b1b83fd
--- /dev/null
+++ b/app/src/main/res/menu/select_bookmark_context.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:compat="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@+id/bookmark_menu_info"
+ android:title="@string/common.info"/>
+
+ <item
+ android:id="@+id/song_menu_show_album"
+ android:title="@string/download.menu_show_album"/>
+
+ <item
+ android:id="@+id/song_menu_show_artist"
+ android:title="@string/menu.show_artist"/>
+
+ <item
+ android:id="@+id/song_menu_download"
+ android:title="@string/common.download"/>
+
+ <item
+ android:id="@+id/song_menu_pin"
+ android:title="@string/common.pin"/>
+
+ <item
+ android:id="@+id/song_menu_delete"
+ android:title="@string/menu.delete_cache"/>
+
+ <item
+ android:id="@+id/bookmark_menu_delete"
+ android:title="@string/bookmark.delete"/>
+</menu>
diff --git a/app/src/main/res/menu/select_playlist_context.xml b/app/src/main/res/menu/select_playlist_context.xml
new file mode 100644
index 00000000..47033d9c
--- /dev/null
+++ b/app/src/main/res/menu/select_playlist_context.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:compat="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@+id/playlist_info"
+ android:title="@string/common.info"
+ />
+
+ <item
+ android:id="@+id/playlist_menu_play_now"
+ android:title="@string/common.play_now"
+ />
+
+ <item
+ android:id="@+id/playlist_menu_play_shuffled"
+ android:title="@string/common.play_shuffled"
+ />
+
+ <item
+ android:id="@+id/playlist_menu_download"
+ android:title="@string/common.download"
+ />
+
+ <item
+ android:id="@+id/playlist_menu_sync"
+ android:title="@string/menu.keep_synced"/>
+
+ <item
+ android:id="@+id/playlist_menu_stop_sync"
+ android:title="@string/menu.stop_sync"/>
+
+ <item
+ android:id="@+id/playlist_update_info"
+ android:title="@string/playlist.update_info"
+ />
+
+ <item
+ android:id="@+id/playlist_menu_delete"
+ android:title="@string/playlist.delete"
+ />
+
+</menu>
diff --git a/app/src/main/res/menu/select_playlist_context_offline.xml b/app/src/main/res/menu/select_playlist_context_offline.xml
new file mode 100644
index 00000000..d63aec17
--- /dev/null
+++ b/app/src/main/res/menu/select_playlist_context_offline.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:compat="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/playlist_menu_play_now"
+ android:title="@string/common.play_now"
+ />
+
+ <item
+ android:id="@+id/playlist_menu_play_shuffled"
+ android:title="@string/common.play_shuffled"
+ />
+</menu> \ No newline at end of file
diff --git a/app/src/main/res/menu/select_podcast_episode.xml b/app/src/main/res/menu/select_podcast_episode.xml
new file mode 100644
index 00000000..65cb0555
--- /dev/null
+++ b/app/src/main/res/menu/select_podcast_episode.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:compat="http://schemas.android.com/apk/res-auto">
+
+ <group android:id="@+id/not_touchscreen">
+ <item
+ android:id="@+id/menu_refresh"
+ android:icon="?attr/refresh"
+ android:title="@string/menu.refresh"
+ compat:showAsAction="ifRoom|withText"/>
+ </group>
+
+ <item
+ android:id="@+id/menu_download_all"
+ android:title="@string/select_podcasts.server_download"/>
+
+ <item
+ android:id="@+id/menu_delete"
+ android:title="@string/menu.delete_cache"/>
+</menu>
diff --git a/app/src/main/res/menu/select_podcast_episode_context.xml b/app/src/main/res/menu/select_podcast_episode_context.xml
new file mode 100644
index 00000000..bacccda3
--- /dev/null
+++ b/app/src/main/res/menu/select_podcast_episode_context.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:compat="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@+id/song_menu_info"
+ android:title="@string/common.info"
+ />
+
+ <item
+ android:id="@+id/song_menu_play_now"
+ android:title="@string/common.play_now"
+ />
+
+ <group android:id="@+id/hide_play_next">
+ <item
+ android:id="@+id/song_menu_play_next"
+ android:title="@string/common.play_next"/>
+ </group>
+
+ <group android:id="@+id/hide_play_last">
+ <item
+ android:id="@+id/song_menu_play_last"
+ android:title="@string/common.play_last"/>
+ </group>
+
+ <item
+ android:id="@+id/song_menu_download"
+ android:title="@string/common.download"
+ />
+
+ <item
+ android:id="@+id/song_menu_delete"
+ android:title="@string/menu.delete_cache"/>
+
+ <group android:id="@+id/server_1.9">
+ <item
+ android:id="@+id/bookmark_menu_delete"
+ android:title="@string/bookmark.delete"/>
+ </group>
+
+ <item
+ android:id="@+id/song_menu_server_download"
+ android:title="@string/select_podcasts.server_download"/>
+
+ <item
+ android:id="@+id/song_menu_server_delete"
+ android:title="@string/select_podcasts.server_delete"/>
+</menu>
diff --git a/app/src/main/res/menu/select_podcast_episode_context_offline.xml b/app/src/main/res/menu/select_podcast_episode_context_offline.xml
new file mode 100644
index 00000000..587d01f7
--- /dev/null
+++ b/app/src/main/res/menu/select_podcast_episode_context_offline.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:compat="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@+id/song_menu_info"
+ android:title="@string/common.info"
+ />
+
+ <item
+ android:id="@+id/song_menu_play_now"
+ android:title="@string/common.play_now"
+ />
+
+ <group android:id="@+id/hide_play_next">
+ <item
+ android:id="@+id/song_menu_play_next"
+ android:title="@string/common.play_next"/>
+ </group>
+
+ <group android:id="@+id/hide_play_last">
+ <item
+ android:id="@+id/song_menu_play_last"
+ android:title="@string/common.play_last"/>
+ </group>
+
+ <item
+ android:id="@+id/song_menu_delete"
+ android:title="@string/menu.delete_cache"/>
+</menu>
diff --git a/app/src/main/res/menu/select_podcast_episode_offline.xml b/app/src/main/res/menu/select_podcast_episode_offline.xml
new file mode 100644
index 00000000..3665d317
--- /dev/null
+++ b/app/src/main/res/menu/select_podcast_episode_offline.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:compat="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/menu_delete"
+ android:title="@string/menu.delete_cache"/>
+</menu>
diff --git a/app/src/main/res/menu/select_podcasts.xml b/app/src/main/res/menu/select_podcasts.xml
new file mode 100644
index 00000000..f30429ce
--- /dev/null
+++ b/app/src/main/res/menu/select_podcasts.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:compat="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/menu_search"
+ android:icon="?attr/search"
+ android:title="@string/menu.search"
+ compat:showAsAction="always|withText"/>
+
+ <item
+ android:id="@+id/menu_add_podcast"
+ android:icon="?attr/add"
+ android:title="@string/menu.add_podcast"
+ compat:showAsAction="always|withText"/>
+
+ <group android:id="@+id/not_touchscreen">
+ <item
+ android:id="@+id/menu_refresh"
+ android:icon="?attr/refresh"
+ android:title="@string/menu.refresh"
+ compat:showAsAction="ifRoom|withText"/>
+ </group>
+
+ <item
+ android:id="@+id/menu_check"
+ android:title="@string/menu.check_podcasts"/>
+
+ <item
+ android:id="@+id/menu_exit"
+ android:title="@string/menu.exit"/>
+</menu> \ No newline at end of file
diff --git a/app/src/main/res/menu/select_podcasts_context.xml b/app/src/main/res/menu/select_podcasts_context.xml
new file mode 100644
index 00000000..5df9d278
--- /dev/null
+++ b/app/src/main/res/menu/select_podcasts_context.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:compat="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/podcast_channel_info"
+ android:title="@string/common.info"/>
+ <item
+ android:id="@+id/podcast_channel_delete"
+ android:title="@string/select_podcasts.delete"/>
+
+ <item
+ android:id="@+id/podcast_menu_sync"
+ android:title="@string/menu.keep_synced"/>
+
+ <item
+ android:id="@+id/podcast_menu_stop_sync"
+ android:title="@string/menu.stop_sync"/>
+</menu>
diff --git a/app/src/main/res/menu/select_podcasts_context_offline.xml b/app/src/main/res/menu/select_podcasts_context_offline.xml
new file mode 100644
index 00000000..cbc76224
--- /dev/null
+++ b/app/src/main/res/menu/select_podcasts_context_offline.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:compat="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/podcast_channel_info"
+ android:title="@string/common.info"/>
+</menu> \ No newline at end of file
diff --git a/app/src/main/res/menu/select_share_context.xml b/app/src/main/res/menu/select_share_context.xml
new file mode 100644
index 00000000..79eb3d55
--- /dev/null
+++ b/app/src/main/res/menu/select_share_context.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:compat="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@+id/share_menu_info"
+ android:title="@string/common.info"/>
+
+ <item
+ android:id="@+id/share_menu_share"
+ android:title="@string/menu.share"/>
+
+ <item
+ android:id="@+id/share_update_info"
+ android:title="@string/playlist.update_info"
+ />
+
+ <item
+ android:id="@+id/share_menu_delete"
+ android:title="@string/share.delete"/>
+</menu>
diff --git a/app/src/main/res/menu/select_song.xml b/app/src/main/res/menu/select_song.xml
new file mode 100644
index 00000000..fc4494cb
--- /dev/null
+++ b/app/src/main/res/menu/select_song.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:compat="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/menu_play_now"
+ android:icon="?attr/media_button_start"
+ android:title="@string/menu.play"
+ compat:showAsAction="always|withText"/>
+
+ <item
+ android:id="@+id/menu_shuffle"
+ android:icon="?attr/shuffle"
+ android:title="@string/menu.shuffle"
+ compat:showAsAction="ifRoom|withText"/>
+
+ <group android:id="@+id/not_touchscreen">
+ <item
+ android:id="@+id/menu_refresh"
+ android:icon="?attr/refresh"
+ android:title="@string/menu.refresh"
+ compat:showAsAction="ifRoom|withText"/>
+ </group>
+
+ <item
+ android:id="@+id/menu_download"
+ android:title="@string/common.download"/>
+
+ <item
+ android:id="@+id/menu_cache"
+ android:title="@string/common.pin"/>
+
+ <item
+ android:id="@+id/menu_delete"
+ android:title="@string/menu.delete_cache"/>
+
+ <item
+ android:id="@+id/menu_add_playlist"
+ android:title="@string/menu.add_playlist"/>
+
+ <item
+ android:id="@+id/menu_remove_playlist"
+ android:title="@string/menu.remove_playlist"/>
+
+ <group android:id="@+id/hide_play_next">
+ <item
+ android:id="@+id/menu_play_next"
+ android:title="@string/common.play_next"/>
+ </group>
+
+ <group android:id="@+id/hide_play_last">
+ <item
+ android:id="@+id/menu_play_last"
+ android:title="@string/menu.play_last"/>
+ </group>
+</menu>
diff --git a/app/src/main/res/menu/select_song_context.xml b/app/src/main/res/menu/select_song_context.xml
new file mode 100644
index 00000000..d8fc211c
--- /dev/null
+++ b/app/src/main/res/menu/select_song_context.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:compat="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@+id/song_menu_info"
+ android:title="@string/common.info"
+ />
+
+ <item
+ android:id="@+id/song_menu_play_now"
+ android:title="@string/common.play_now"
+ />
+
+ <group android:id="@+id/hide_play_next">
+ <item
+ android:id="@+id/song_menu_play_next"
+ android:title="@string/common.play_next"/>
+ </group>
+
+ <group android:id="@+id/hide_play_last">
+ <item
+ android:id="@+id/song_menu_play_last"
+ android:title="@string/common.play_last"/>
+ </group>
+
+ <item
+ android:id="@+id/song_menu_download"
+ android:title="@string/common.download"
+ />
+
+ <item
+ android:id="@+id/song_menu_pin"
+ android:title="@string/common.pin"
+ />
+
+ <item
+ android:id="@+id/song_menu_delete"
+ android:title="@string/menu.delete_cache"/>
+
+ <group android:id="@+id/server_1.8">
+ <item
+ android:id="@+id/song_menu_add_playlist"
+ android:title="@string/menu.add_playlist"/>
+ </group>
+
+ <item
+ android:id="@+id/song_menu_remove_playlist"
+ android:title="@string/menu.remove_playlist"/>
+
+ <group android:id="@+id/hide_star">
+ <item
+ android:id="@+id/song_menu_star"
+ android:title="@string/common.star"/>
+ </group>
+
+ <group android:id="@+id/hide_share">
+ <item
+ android:id="@+id/song_menu_share"
+ android:title="@string/menu.share"/>
+ </group>
+
+ <group android:id="@+id/hide_rating">
+ <item
+ android:id="@+id/menu_rate"
+ android:title="@string/menu.rate"/>
+ </group>
+
+ <group android:id="@+id/server_1.9">
+ <item
+ android:id="@+id/bookmark_menu_delete"
+ android:title="@string/bookmark.delete"/>
+ </group>
+</menu>
diff --git a/app/src/main/res/menu/select_song_context_offline.xml b/app/src/main/res/menu/select_song_context_offline.xml
new file mode 100644
index 00000000..49445876
--- /dev/null
+++ b/app/src/main/res/menu/select_song_context_offline.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:compat="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@+id/song_menu_info"
+ android:title="@string/common.info"
+ />
+
+ <item
+ android:id="@+id/song_menu_play_now"
+ android:title="@string/common.play_now"
+ />
+
+ <group android:id="@+id/hide_play_next">
+ <item
+ android:id="@+id/song_menu_play_next"
+ android:title="@string/common.play_next"/>
+ </group>
+
+ <group android:id="@+id/hide_play_last">
+ <item
+ android:id="@+id/song_menu_play_last"
+ android:title="@string/common.play_last"/>
+ </group>
+
+ <item
+ android:id="@+id/song_menu_delete"
+ android:title="@string/menu.delete_cache"/>
+
+ <item
+ android:id="@+id/song_menu_star"
+ android:title="@string/common.star"/>
+</menu>
diff --git a/app/src/main/res/menu/select_song_offline.xml b/app/src/main/res/menu/select_song_offline.xml
new file mode 100644
index 00000000..c45405fb
--- /dev/null
+++ b/app/src/main/res/menu/select_song_offline.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:compat="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/menu_play_now"
+ android:icon="?attr/media_button_start"
+ android:title="@string/menu.play"
+ compat:showAsAction="always|withText"/>
+
+ <item
+ android:id="@+id/menu_shuffle"
+ android:icon="?attr/shuffle"
+ android:title="@string/menu.shuffle"
+ compat:showAsAction="ifRoom|withText"/>
+
+ <item
+ android:id="@+id/menu_delete"
+ android:title="@string/menu.delete_cache"/>
+
+ <group android:id="@+id/hide_play_next">
+ <item
+ android:id="@+id/menu_play_next"
+ android:title="@string/common.play_next"/>
+ </group>
+
+ <group android:id="@+id/hide_play_last">
+ <item
+ android:id="@+id/menu_play_last"
+ android:title="@string/menu.play_last"/>
+ </group>
+</menu>
diff --git a/app/src/main/res/menu/select_video_context.xml b/app/src/main/res/menu/select_video_context.xml
new file mode 100644
index 00000000..3eda2df7
--- /dev/null
+++ b/app/src/main/res/menu/select_video_context.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:compat="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/song_menu_info"
+ android:title="@string/common.info"/>
+
+ <item
+ android:id="@+id/song_menu_stream_external"
+ android:title="@string/common.stream_external"/>
+
+ <item
+ android:id="@+id/song_menu_play_external"
+ android:title="@string/common.play_external"/>
+
+ <item
+ android:id="@+id/song_menu_download"
+ android:title="@string/common.download"
+ />
+
+ <item
+ android:id="@+id/song_menu_delete"
+ android:title="@string/menu.delete_cache"/>
+</menu>
diff --git a/app/src/main/res/menu/select_video_context_offline.xml b/app/src/main/res/menu/select_video_context_offline.xml
new file mode 100644
index 00000000..3fea9f5e
--- /dev/null
+++ b/app/src/main/res/menu/select_video_context_offline.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:compat="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/song_menu_info"
+ android:title="@string/common.info"/>
+
+ <item
+ android:id="@+id/song_menu_play_external"
+ android:title="@string/common.play_external"/>
+
+ <item
+ android:id="@+id/song_menu_delete"
+ android:title="@string/menu.delete_cache"/>
+</menu>
diff --git a/app/src/main/res/menu/similar_artists.xml b/app/src/main/res/menu/similar_artists.xml
new file mode 100644
index 00000000..771555b6
--- /dev/null
+++ b/app/src/main/res/menu/similar_artists.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:compat="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@+id/menu_play_now"
+ android:icon="?media_button_start"
+ android:title="@string/menu.play"
+ compat:showAsAction="always|withText"/>
+
+ <item
+ android:id="@+id/menu_shuffle"
+ android:icon="?attr/shuffle"
+ android:title="@string/menu.shuffle"
+ compat:showAsAction="ifRoom|withText"/>
+
+ <item
+ android:id="@+id/menu_show_missing"
+ android:title="@string/menu.show_missing"/>
+</menu> \ No newline at end of file
diff --git a/app/src/main/res/menu/tasker_configuration.xml b/app/src/main/res/menu/tasker_configuration.xml
new file mode 100644
index 00000000..bb49ba27
--- /dev/null
+++ b/app/src/main/res/menu/tasker_configuration.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:compat="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@+id/menu_cancel"
+ android:icon="?attr/remove"
+ android:title="@string/common.cancel"
+ compat:showAsAction="always|withText"/>
+
+ <item
+ android:id="@+id/menu_accept"
+ android:icon="?attr/save"
+ android:title="@string/common.ok"
+ compat:showAsAction="always|withText"/>
+</menu>
diff --git a/app/src/main/res/menu/unstar.xml b/app/src/main/res/menu/unstar.xml
new file mode 100644
index 00000000..502e88ab
--- /dev/null
+++ b/app/src/main/res/menu/unstar.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:compat="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/menu_unstar"
+ android:title="@string/common.unstar"/>
+</menu> \ No newline at end of file
diff --git a/app/src/main/res/menu/user.xml b/app/src/main/res/menu/user.xml
new file mode 100644
index 00000000..eed352e5
--- /dev/null
+++ b/app/src/main/res/menu/user.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:compat="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@+id/menu_update_permissions"
+ android:title="@string/admin.update_permissions"
+ android:icon="?attr/save"
+ compat:showAsAction="always|withText"/>
+
+ <item
+ android:id="@+id/menu_change_password"
+ android:title="@string/admin.change_password"
+ android:icon="?attr/password"
+ compat:showAsAction="always|withText"/>
+
+ <group android:id="@+id/not_touchscreen">
+ <item
+ android:id="@+id/menu_refresh"
+ android:icon="?attr/refresh"
+ android:title="@string/menu.refresh"
+ compat:showAsAction="ifRoom|withText"/>
+ </group>
+
+ <item
+ android:id="@+id/menu_change_email"
+ android:title="@string/admin.change_email"/>
+
+ <item
+ android:id="@+id/menu_exit"
+ android:title="@string/menu.exit"/>
+</menu> \ No newline at end of file
diff --git a/app/src/main/res/menu/user_user.xml b/app/src/main/res/menu/user_user.xml
new file mode 100644
index 00000000..83465b3e
--- /dev/null
+++ b/app/src/main/res/menu/user_user.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:compat="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@+id/menu_change_password"
+ android:title="@string/admin.change_password"
+ android:icon="?attr/password"
+ compat:showAsAction="always|withText"/>
+
+ <item
+ android:id="@+id/menu_exit"
+ android:title="@string/menu.exit"/>
+</menu> \ No newline at end of file
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
new file mode 100644
index 00000000..62d19b3f
--- /dev/null
+++ b/app/src/main/res/values-de/strings.xml
@@ -0,0 +1,557 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="common.appname">DSub</string>
+ <string name="common.ok">OK</string>
+ <string name="common.save">Speichern</string>
+ <string name="common.cancel">Abbrechen</string>
+ <string name="common.play_now">Jetzt wiedergeben</string>
+ <string name="common.play_shuffled">Zufallswiedergabe</string>
+ <string name="common.play_next">Als nächstes abspielen</string>
+ <string name="common.play_last">Als letztes abspielen</string>
+ <string name="common.download">Cache</string>
+ <string name="common.pin">Permanenter Cache</string>
+ <string name="common.delete">Löschen</string>
+ <string name="common.star">Favorit</string>
+ <string name="common.unstar">Kein Favorit</string>
+ <string name="common.info">Details</string>
+ <string name="common.name">Name</string>
+ <string name="common.comment">Kommentar</string>
+ <string name="common.public">Öffentlich</string>
+ <string name="common.play_external">Video abspielen</string>
+ <string name="common.stream_external">Video streamen</string>
+ <string name="common.confirm">Bestätigen</string>
+ <string name="common.confirm_message">Wollen Sie %2$s %1$s?</string>
+ <string name="common.confirm_message_cache">den Cache</string>
+ <string name="common.empty">Nicht gefunden</string>
+ <string name="common.warning">Warnung</string>
+
+ <string name="button_bar.home">Übersicht</string>
+ <string name="button_bar.browse">Bibliothek</string>
+ <string name="button_bar.search">Suchen</string>
+ <string name="button_bar.playlists">Wiedergabeliste</string>
+ <string name="button_bar.now_playing">Aktuelle Wiedergabe</string>
+ <string name="button_bar.podcasts">Podcasts</string>
+ <string name="button_bar.bookmarks">Lesezeichen</string>
+ <string name="button_bar.shares">Freigaben</string>
+ <string name="button_bar.chat">Chat</string>
+ <string name="button_bar.admin">Administration</string>
+ <string name="button_bar.downloading">Downloads</string>
+
+ <string name="main.welcome_title">Willkommen!</string>
+ <string name="main.welcome_text">Willkommen zu DSub! Die App ist aktuell für den Subsonic-Demo-Server konfiguriert. Nachdem Sie Ihren eigenen Server
+ aufgesetzt haben (verfügbar unter <b>subsonic.org</b>) könne Sie diesen unter <b>Einstellungen</b> konfigurieren.</string>
+ <string name="main.about_title">Über DSub</string>
+ <string name="main.about_text">Autor: Scott Jackson
+ \nEmail: dsub.android@gmail.com
+ \nVersion: %1$s
+ \nLokal gespeicherte Titel: %2$s
+ \nGenutzter Speicher: %3$s von %4$s
+ \nVerfügbarer Speicher: %5$s von %6$s</string>
+ <string name="main.faq_title">FAQ</string>
+ <string name="main.faq_text">
+ <![CDATA[
+ <font color="red">Cache vs Permanenter Cache</font>:
+ <br/>Von DSub heruntergelade Titel im Cache können automatisch gelöscht werden, um Platz für neue Titel zu schaffen. Titel im permanenten Cache werden dagegen nie automatisch gelöscht.
+ <p/><font color="red">ChromeCast funktioniert nicht</font>:
+ <br/>Bitte stellen Sie sicher, das keine selbstsignierten Zertifikate verwendet werden, da Chromecast diese nicht akzeptiert.
+ ]]>
+ </string>
+ <string name="main.select_server">Wähle Server</string>
+ <string name="main.shuffle">Zufallswiedergabe</string>
+ <string name="main.offline">Gehe Offline</string>
+ <string name="main.online">Gehe Online</string>
+ <string name="main.settings">Einstellungen</string>
+ <string name="main.albums_title">Album Liste</string>
+ <string name="main.albums_newest">Neue Alben</string>
+ <string name="main.albums_recent">Vor kurzem gespielt</string>
+ <string name="main.albums_frequent">Am meisten gespielt</string>
+ <string name="main.albums_highest">Top bewertet</string>
+ <string name="main.albums_starred">Favoriten</string>
+ <string name="main.albums_random">Zufall</string>
+ <string name="main.albums_genres">Genres</string>
+ <string name="main.albums_year">Jahrzehnt</string>
+ <string name="main.songs_genres">@string/main.albums_genres</string>
+ <string name="main.back_confirm">Zum beenden nochmal zurück drücken</string>
+ <string name="main.scan_complete">Durchsuchen des Server abgeschlossen</string>
+
+ <string name="menu.search">Suche</string>
+ <string name="menu.shuffle">Zufall</string>
+ <string name="menu.refresh">Aktualisieren</string>
+ <string name="menu.play">Abspielen</string>
+ <string name="menu.play_last">Ans Ende der Wiedergabeliste</string>
+ <string name="menu.exit">Beenden</string>
+ <string name="menu.settings">Einstellungen</string>
+ <string name="menu.help">Hilfe</string>
+ <string name="menu.about">Über</string>
+ <string name="menu.add_playlist">Zur Wiedergabeliste hinzufügen</string>
+ <string name="menu.remove_playlist">Von Wiedergabeliste entfernen</string>
+ <string name="menu.deleted_playlist">Lösche Wiedergabeliste %s</string>
+ <string name="menu.deleted_playlist_error">Löschen der Wiedergabeliste %s fehlgeschlagen</string>
+ <string name="menu.log">Sende Log</string>
+ <string name="menu.set_timer">Setze Timer</string>
+ <string name="menu.check_podcasts">Prüfe auf neue Episoden</string>
+ <string name="menu.add_podcast">Podcast hinzufügen</string>
+ <string name="menu.keep_synced">Synchron halten</string>
+ <string name="menu.stop_sync">Synchronisierung stoppen</string>
+ <string name="menu.show_all">Zeige alle Medien</string>
+ <string name="menu.show_artist">Zeige Künstler</string>
+ <string name="menu.share">Freigeben</string>
+ <string name="menu.delete_cache">Lösche Cache</string>
+ <string name="menu.cast">An Chromecast senden</string>
+ <string name="menu.faq">FAQ</string>
+ <string name="menu.add_user">Nutzer hinzufügen</string>
+ <string name="menu.rescan">Server neu durchsuchen</string>
+ <string name="menu.rate">Setze Bewertung</string>
+ <string name="menu.top_tracks">Last.FM Top Medien</string>
+ <string name="menu.similar_artists">Ähnliche Künstler</string>
+ <string name="menu.show_missing">Zeige fehlende</string>
+
+ <string name="playlist.label">Wiedergabelisten</string>
+ <string name="playlist.update_info">Aktualisiere Informationen</string>
+ <string name="playlist.updated_info">Aktualisiere Wiedergabeliste für %s</string>
+ <string name="playlist.updated_info_error">Aktualisierung der Wiedergabeliste %s fehlgeschlagen</string>
+ <string name="playlist.overwrite">Wiedergabeliste überschreiben</string>
+ <string name="playlist.add_to">Zur Wiedergabeliste hinzu</string>
+ <string name="playlist.create_new">Neue Wiedergabeliste</string>
+ <string name="playlist.delete">Lösche Wiedergabeliste</string>
+
+ <string name="search.label">Suche</string>
+ <string name="search.title">Suche</string>
+ <string name="search.search">Zum Suche klicken</string>
+ <string name="search.no_match">Nichts gefunden, bitte erneut versuchen</string>
+ <string name="search.artists">Künstler</string>
+ <string name="search.albums">Alben</string>
+ <string name="search.songs">Lieder</string>
+ <string name="search.more">Zeige mehr</string>
+
+ <string name="progress.wait">Bitte warten...</string>
+
+ <string name="music_library.label">Medienbibliothek</string>
+ <string name="music_library.label_offline">Offline Medien</string>
+
+ <string name="select_album.select">Alle auswählen</string>
+ <string name="select_album.n_selected">%d Lieder ausgewählt.</string>
+ <string name="select_album.n_unselected">%d Lieder deselektiert.</string>
+ <string name="select_album.more">Mehr</string>
+ <string name="select_album.offline">Offline</string>
+ <string name="select_album.searching">Suche...</string>
+ <string name="select_album.no_sdcard">Fehler: Keine SD-Karte verfügbar.</string>
+ <string name="select_album.no_network">Warnung: Kein Netzwerk verfügbar.</string>
+ <string name="select_album.not_licensed">Server ist nicht lizensiert. Testzeitraum läuft ab in %d Tagen.</string>
+ <string name="select_album.donate_dialog_message">Erhalte unbegrenzte Downloads durch eine Spende an Subsonic.</string>
+ <string name="select_album.donate_dialog_now">Jetzt</string>
+ <string name="select_album.donate_dialog_later">Später</string>
+ <string name="select_album.donate_dialog_0_trial_days_left">Testzeitraum ist vorbei</string>
+
+ <string name="offline.sync_dialog_title">Offline Lieder warten auf Synchronisierung</string>
+ <string name="offline.sync_dialog_message">Scrobble %1$d Einträge?
+ \nÜbermittle %2$d neue Favoriten an den Server?
+ </string>
+ <string name="offline.sync_dialog_default">Nutze als Standardaktion</string>
+ <string name="offline.sync_success">Erfolgreich %1$d Lieder synchronisiert</string>
+ <string name="offline.sync_partial">Erfolgreich %1$d von %2$d Liedern synchronisiert</string>
+ <string name="offline.sync_error">Synchronisierung der Lieder fehlgeschlagen</string>
+
+ <string name="select_genre.blank">leer</string>
+ <string name="select_genre.songs">%d Lieder</string>
+ <string name="select_genre.albums">%d Alben</string>
+
+ <string name="select_podcasts.error">Ein Fehler beim herunterladen dieses Podcast durch den Server. Der Podcast muss zuerst vom Server heruntergeladen werden.</string>
+ <string name="select_podcasts.skipped">Der Podcast wurde noch nicht vom Server heruntergeladen. Der Podcast muss zuerst vom Server heruntergalden werden..</string>
+ <string name="select_podcasts.initializing">Der Podcastkanal wird vom Server initialisiert. Bitte nach kurzer Wartezeit erneut laden.</string>
+ <string name="select_podcasts.server_download">Auf den Server herunterladen</string>
+ <string name="select_podcasts.server_delete">Lösche vom Server</string>
+ <string name="select_podcasts.downloading">Lade %s auf den Server herunter</string>
+ <string name="select_podcasts.refreshing">Der Server prüft auf neue Podcasts.</string>
+ <string name="select_podcasts.deleted">Lösche Podcast %s</string>
+ <string name="select_podcasts.deleted_error">Fehler beim löschen des Podcast %s</string>
+ <string name="select_podcasts.add_url">URL:</string>
+ <string name="select_podcasts.created_error">Konnte Podcast nicht hinzufügen</string>
+ <string name="select_podcasts.invalid_podcast_channel">Ungültiger Podcastkanal: %s</string>
+ <string name="select_podcasts.delete">Lösche Podcast</string>
+
+ <string name="download.empty">Wiedergabeliste ist leer</string>
+ <string name="download.shuffle_loading">Wiedergabeliste wird gemischt...</string>
+ <string name="download.playerstate_downloading">Downloade - %s</string>
+ <string name="download.playerstate_buffering">Buffere</string>
+ <string name="download.playerstate_playing_shuffle">Playing shuffle</string>
+ <string name="download.menu_show_album">Zeige Album</string>
+ <string name="download.menu_lyrics">Liedtext</string>
+ <string name="download.menu_remove">Entferne aus Warteschlange</string>
+ <string name="download.menu_remove_all">Alle entfernen</string>
+ <string name="download.menu_screen_on">Bildschirm an</string>
+ <string name="download.menu_shuffle">Mischen</string>
+ <string name="download.menu_toggle">Umschalten</string>
+ <string name="download.menu_save">Wiedergabeliste speichern</string>
+ <string name="download.menu_shuffle_notification">Wiedergabeliste wurde gemischt</string>
+ <string name="download.menu_remove_played_songs">Abgespielte Titel entfernen</string>
+ <string name="download.playlist_title">Speichere Wiedergabeliste</string>
+ <string name="download.playlist_name">Name der Wiedergabeliste eingeben:</string>
+ <string name="download.playlist_saving">Sichere Wiedergabeliste \"%s\"...</string>
+ <string name="download.playlist_done">Wiedergabeliste wurde erfolgreich gespeichert.</string>
+ <string name="download.playlist_error">Fehler beim speichern der Wiedergabeliste, bitte später erneut probieren.</string>
+ <string name="download.repeat_off">Keine Wiederholung</string>
+ <string name="download.repeat_all">Wiederhole alle</string>
+ <string name="download.repeat_single">Aktuelles Lied wiederholen</string>
+ <string name="download.jukebox_on">Fernbedienung aktiviert. Musik wird auf dem Computer abgespielt.</string>
+ <string name="download.jukebox_off">Fernbedienung deaktiviert. Musik wird auf dem Telefon abgespielt.</string>
+ <string name="download.jukebox_volume">Lautstärke</string>
+ <string name="download.jukebox_server_too_old">Fernbedienung wird nicht unterstützt. Aktualisierung des Subsonic-Servers notwendig.</string>
+ <string name="download.jukebox_offline">Fernbedienung im Offline-Modus nicht verfügbar.</string>
+ <string name="download.jukebox_not_authorized">Fernbedienung ist nicht erlaubt. Bitte aktivieren Sie den Jukebox-Modus unter <b>Nutzer &gt; Einstellungen</b> auf Ihrem Subsonic-Server.</string>
+ <string name="download.timer_length">Timer:</string>
+ <string name="download.start_timer">Starte Timer</string>
+ <string name="download.stop_timer">Stoppe Timer</string>
+ <string name="download.need_download">Video muss zuerst heruntergeladen werden</string>
+ <string name="download.no_streaming_player">Stream kann nicht wiedergegeben werden.</string>
+ <string name="download.playing_out_of">Wiedergabe: %1$d/%2$d</string>
+ <string name="download.save_bookmark_title">Setze Lesezeichen</string>
+ <string name="download.save_bookmark">Lesezeichen gesetzt</string>
+ <string name="download.save_bookmark_failed">Lesezeichen konnte nicht gesetzt werden.</string>
+ <string name="download.downloading_title">Lade %1$d Lieder</string>
+ <string name="download.downloading_summary">Aktuell: %1$s</string>
+ <string name="download.downloading_summary_expanded">Aktuell: %1$s
+ \nGeschätzte Größe: %2$s</string>
+ <string name="download.failed_to_load">Laden fehlgeschlagen</string>
+
+ <string name="sync.new_podcasts">Neuer Podcast verfügbar</string>
+ <string name="sync.new_playlists">Neu in Wiedergabeliste</string>
+ <string name="sync.new_albums">Neues Album verfügbar</string>
+ <string name="sync.new_starred">Neue Favoriten verfügbar</string>
+
+ <string name="starring_content_starred">Favorit \"%s\"</string>
+ <string name="starring_content_unstarred">Kein Favorit \"%s\"</string>
+ <string name="starring_content_error">Aktualisierung von \"%s\" fehlgeschlagen, bitte später erneut probieren.</string>
+
+ <string name="playlist_error">Konnte Liste der Wiedergabelisten nicht herunterladen</string>
+ <string name="updated_playlist">%1$s Lieder zu \"%2$s\" hinzugefügt</string>
+ <string name="updated_playlist_error">Aktualisierung \"%s\" fehlgeschlagen, bitte später erneut probieren.</string>
+ <string name="removed_playlist">%1$s entfernt aus \"%2$s\"</string>
+
+ <string name="bookmark.delete">Lösche Lesezeichen</string>
+ <string name="bookmark.delete_title">Lösche Lesezeichen für</string>
+ <string name="bookmark.deleted">Lesezeichen für \"%s\" gelöscht</string>
+ <string name="bookmark.deleted_error">Löschen des Lesezeichen für \"%s\" ist fehlgeschlagen</string>
+ <string name="bookmark.details_title">Lesezeichendetails</string>
+ <string name="bookmark.details">Lied: %1$s
+ \nPosition: %2$s
+ \nErzeugt: %3$s
+ \nZuletzt aktualisiert: %4$s
+ \nKommentar: %5$s</string>
+ <string name="bookmark.resume_title">Wiedergabe fortsetzen?</string>
+ <string name="bookmark.resume">\'%1$s\' fortsetzen bei %2$s</string>
+ <string name="bookmark.action_resume">Fortsetzen</string>
+ <string name="bookmark.action_start_over">Neu beginnen</string>
+
+ <string name="rating.title">Bewerte \"%s\"</string>
+ <string name="rating.set_rating">Setze Bewertung für \"%s\"</string>
+ <string name="rating.set_rating_failed">Konnte Bewertung für \"%s\" nicht setzen</string>
+ <string name="rating.remove_rating">Bewertung für \"%s\" entfernt</string>
+ <string name="rating.remove_rating_failed">Konnte Bewertung für \"%s\" nicht entfernen</string>
+
+ <string name="song_details.error">Fehler</string>
+ <string name="song_details.skipped">Überspringen</string>
+ <string name="song_details.downloading">Wird geladen</string>
+
+ <string name="lyrics.nomatch">Kein Liedtext gefunden</string>
+
+ <string name="error.label">Fehler</string>
+
+ <string name="settings.title">DSub Einstellungen</string>
+ <string name="settings.test_connection_title">Teste Verbindung</string>
+ <string name="settings.servers_add">Server hinzufügen</string>
+ <string name="settings.servers_remove">Server entfernen</string>
+ <string name="settings.servers_title">Server</string>
+ <string name="settings.server_unused">ungenutzt</string>
+ <string name="settings.server_name">Name</string>
+ <string name="settings.server_address">Server Adresse</string>
+ <string name="settings.server_local_network_ssid" >Lokale Netzwerk-SSID</string>
+ <string name="settings.server_local_network_ssid_hint">Aktuelle SSID: %s</string>
+ <string name="settings.server_internal_address">Lokale Netzwerkadresse</string>
+ <string name="settings.server_username">Nutzername</string>
+ <string name="settings.server_password">Passwort</string>
+ <string name="settings.server_open_browser">Im Browser öffnen</string>
+ <string name="settings.server_sync_summary">Synchronisierung für diesen Server de-/aktivieren</string>
+ <string name="settings.server_sync">Synchronisierung aktivieren</string>
+ <string name="settings.cache_title">Musik Cache</string>
+ <string name="settings.preload_wifi">Im Vorraus zu laden (Wifi)</string>
+ <string name="settings.preload_mobile">Im Vorraus zu laden (Mobil)</string>
+ <string name="settings.cache_size">Größe des Cache (MB)</string>
+ <string name="settings.cache_location">Position des Cache</string>
+ <string name="settings.cache_location_error">Ungültiger Pfad zum Cache. Nutze Standard.</string>
+ <string name="settings.cache_location_reset">Der gesetzte Cachepfad ist nicht (mehr) beschreibbar. Durch die Aktualisierung des Telefon-OS auf KitKat 4.4 wurde der Zugriff auf die SD-Karte durch Apps eingeschränkt auf spezielle Pfade. Der von DSub verwendete Pfad wurde entsprechend korrigiert. Um den alten Cache zu löschen, müssen Sie die SD-Karte an einem Computer anschliessend und den Ordner manuell löschen.</string>
+ <string name="settings.cache_clear">Lösche Cache</string>
+ <string name="settings.cache_clear_complete">Cache wurde geleert</string>
+ <string name="settings.testing_connection">Teste Verbindung...</string>
+ <string name="settings.testing_ok">Verbindung ist OK</string>
+ <string name="settings.testing_unlicensed">Verbindung ist OK. Server ist nicht lizensiert.</string>
+ <string name="settings.connection_failure">Verbindung fehlgeschlagen.</string>
+ <string name="settings.invalid_url">Bitte geben Sie eine gültige URL an.</string>
+ <string name="settings.invalid_username">Bitte gültigen Nutzernamen angeben (keine führenden Leerzeichen).</string>
+ <string name="settings.appearance_title">Erscheinung</string>
+ <string name="settings.theme_title">Theme</string>
+ <string name="settings.theme_light">Hell</string>
+ <string name="settings.theme_dark">Dunkel</string>
+ <string name="settings.theme_black">Schwarz</string>
+ <string name="settings.theme_holo">Holo</string>
+ <string name="settings.theme_fullscreen">Vollbildschirm</string>
+ <string name="settings.theme_fullscreen_summary">Verstecke soviel der Benutzerschnittstelle wie von Android ermöglicht.</string>
+ <string name="settings.track_title">Titelnummer anzeigen</string>
+ <string name="settings.track_summary">Zeige Titelnummer vor dem Titel</string>
+ <string name="settings.custom_sort">Sortiere nach Jahr</string>
+ <string name="settings.custom_sort_summary">Sortiere Alben nach Jahr, oder alphabetisch.</string>
+ <string name="settings.network_title">Netzwerk</string>
+ <string name="settings.max_bitrate_wifi">Max Audio Bitrate - Wi-Fi</string>
+ <string name="settings.max_bitrate_mobile">Max Audio Bitrate - Mobil</string>
+ <string name="settings.max_video_bitrate_wifi">Max Video Bitrate - Wi-Fi</string>
+ <string name="settings.max_video_bitrate_mobile">Max Video Bitrate - Mobil</string>
+ <string name="settings.max_bitrate_unlimited">Unbegrenzt</string>
+ <string name="settings.wifi_required_title">Nur Wi-Fi streaming</string>
+ <string name="settings.wifi_required_summary">Medien nur streamen, wenn mit Wi-Fi verbunden</string>
+ <string name="settings.network_timeout_title">Netzwerk Timeout</string>
+ <string name="settings.network_timeout_10000">10 Sekunden</string>
+ <string name="settings.network_timeout_15000">15 Sekunden</string>
+ <string name="settings.network_timeout_30000">30 Sekunden</string>
+ <string name="settings.network_timeout_45000">45 Sekunden</string>
+ <string name="settings.network_timeout_60000">60 Sekunden</string>
+ <string name="settings.preload_0">Kein Lied</string>
+ <string name="settings.preload_1">1 Lied</string>
+ <string name="settings.preload_2">2 Lieder</string>
+ <string name="settings.preload_3">3 Lieder</string>
+ <string name="settings.preload_5">5 Lieder</string>
+ <string name="settings.preload_10">10 Lieder</string>
+ <string name="settings.preload_unlimited">unbegrenzt</string>
+ <string name="settings.clear_search_history">Lösche Suchanfragen</string>
+ <string name="settings.search_history_cleared">Suchanfragen gelöscht</string>
+ <string name="settings.other_title">Sonstige Einstellungen</string>
+ <string name="settings.scrobble_title">Scrobble zu Last.fm</string>
+ <string name="settings.scrobble_summary">Ihr Benutzername und Passwort für Last.fm muss im Subsonic-Server konfiguriert sein</string>
+ <string name="settings.hide_media_title">Verstecke vor anderen</string>
+ <string name="settings.hide_media_summary">Musikdateien vor anderen Apps verstecken.</string>
+ <string name="settings.hide_media_toast">Wird aktiv bei der nächsten Mediensuche durch Android.</string>
+ <string name="settings.media_button_title">Medientasten</string>
+ <string name="settings.media_button_summary">Verwende Medientasten des Telefon, Freisprecheinrichtung und Bluetooth</string>
+ <string name="settings.screen_lit_title">Bildschirm aktiv halten</string>
+ <string name="settings.screen_lit_summary">Ein aktiver Bildschirm verbessert die Downloadgeschwindigkeit.</string>
+ <string name="settings.playlist_title">Wiedergeben</string>
+ <string name="settings.playlist_random_size_title">Länge der Zufallswiedergabeliste</string>
+ <string name="settings.sleep_timer_title">Einschlaftimer</string>
+ <string name="settings.sleep_timer_duration_title">Einschlafdauer</string>
+ <string name="settings.sleep_timer_off">Aus</string>
+ <string name="settings.sleep_timer_on">An</string>
+ <string name="settings.sleep_timer_always_on">Immer An</string>
+ <string name="settings.temp_loss_title">Verhalten bei Benachrichtigungen</string>
+ <string name="settings.temp_loss_pause">Immer pausieren</string>
+ <string name="settings.temp_loss_pause_lower">Pausiere und Lautstärke bei Bedarf veringern</string>
+ <string name="settings.temp_loss_lower">Lautstärke veringern</string>
+ <string name="settings.temp_loss_nothing">Nichts machen</string>
+ <string name="settings.disconnect_pause_title">Verhalten bei Verbindungsverlust</string>
+ <string name="settings.disconnect_pause_both">Immer pausieren</string>
+ <string name="settings.disconnect_pause_neither">Nichts machen</string>
+ <string name="settings.persistent_title">Dauerhafte Benachrichtigung</string>
+ <string name="settings.persistent_summary">Zeige die Benachrichtigung auch nach pausieren der Wiedergabe. Zum entfernen Stop auswählen.</string>
+ <string name="settings.gapless_playback">Lückenlose Wiedergabe</string>
+ <string name="settings.gapless_playback_summary">Das Galaxy S3 scheint Probleme seit der Einführung der lückenlosen Wiedergabe zu haben. Zur Behebung schalten Sie dies ab.</string>
+ <string name="settings.chat_refresh">Chat Aktualisierungsrate (Sekunden)</string>
+ <string name="settings.chat_enabled">Chat aktiv</string>
+ <string name="settings.chat_enabled_summary">Chat im Seitenmenü anzeigen</string>
+ <string name="settings.video_title">Video</string>
+ <string name="settings.video_player">Video Player</string>
+ <string name="settings.video_raw">Raw (benötigt Subsonic 4.8+)</string>
+ <string name="settings.video_hls">HTTP Live Stream (HLS) (benötigt Subsonic 4.8+)</string>
+ <string name="settings.video_transcode">Direkte Transkodierung (benötigt video -> mp4 oder ähnliche Einstellungen auf dem Server)</string>
+ <string name="settings.video_flash">Flash (benötigt Plugin)</string>
+ <string name="settings.cache_screen_title">Cache/Netzwerk</string>
+ <string name="settings.playback_title">Wiedergabe</string>
+ <string name="settings.hide_widget_title">Verstecke Widget</string>
+ <string name="settings.hide_widget_summary">Verstecke Widget nach dem verlassen der App</string>
+ <string name="settings.podcasts_enabled">Podcasts aktiviert</string>
+ <string name="settings.podcasts_enabled_summary">Podcast im Seitenmenü anzeigen</string>
+ <string name="settings.bookmarks_enabled">Lesezeichen aktiviert</string>
+ <string name="settings.bookmarks_enabled_summary">Lesezeichen im Seitenmenü anzeigen</string>
+ <string name="settings.shares_enabled">Freigaben aktiviert</string>
+ <string name="settings.shares_enabled_summary">Freigabe im Seitenmenü anzeigen</string>
+ <string name="settings.sync_title">Synchronisierung</string>
+ <string name="settings.sync_enabled">Synchronisierung aktiv</string>
+ <string name="settings.sync_enabled_summary">Podcast regelmäßig auf Änderungen prüfen</string>
+ <string name="settings.sync_interval">Synchronisierungsintervall</string>
+ <string name="settings.sync_interval_15">15 Minutes</string>
+ <string name="settings.sync_interval_30">30 Minutes</string>
+ <string name="settings.sync_interval_60">1 Stunde</string>
+ <string name="settings.sync_interval_120">2 Stunden</string>
+ <string name="settings.sync_interval_240">4 Stunden</string>
+ <string name="settings.sync_interval_360">6 Stunden</string>
+ <string name="settings.sync_interval_720">12 Stunden</string>
+ <string name="settings.sync_interval_1440">täglich</string>
+ <string name="settings.sync_wifi">Nur per Wifi synchronisieren</string>
+ <string name="settings.sync_wifi_summary">Nur bei Verwendung von Wifi synchronisieren</string>
+ <string name="settings.sync_most_recent">Synchronisiere neu hinzugefügte Lieder</string>
+ <string name="settings.sync_most_recent_summary">Lade neu hinzugefügte Alben automatisch herunter</string>
+ <string name="settings.sync_starred">Synchronisiere Favoriten</string>
+ <string name="settings.sync_starred_summary">Synchronisiere favorisierte Lieder, Alben und Künstler automatisch</string>
+ <string name="settings.sync_notification">Benachrichtigung nach Synchronisierung</string>
+ <string name="settings.sync_notification_summary">Zeige eine Benachrichtigung nachdem neue Medien synchronisiert wurden.</string>
+ <string name="settings.menu_options.title">Optionale Menü Einträge</string>
+ <string name="settings.menu_options.play_next_summary">Zeige \"Als nächstes abspielen\" im Menü</string>
+ <string name="settings.menu_options.play_last_summary">Zeige \"Als letztes abspielen\" im Menü</string>
+ <string name="settings.menu_options.star_summary">Zeige Favorit im Menü</string>
+ <string name="settings.menu_options.shared_summary">Zeige \"Freigeben\" im Menü</string>
+ <string name="settings.menu_options.rate_summary">Zeige \"Bewerten\" im Menü</string>
+ <string name="settings.browse_by_tags">Tags nutzen</string>
+ <string name="settings.browse_by_tags_summary">Tags statt Ordner verwenden. Benötigt Subsonic 4.7+</string>
+ <string name="settings.override_system_language">In Englisch anzeigen</string>
+ <string name="settings.override_system_language_summary">Verwende Englisch anstatt Deutsch für DSub. Benötigt einen Neustart der App.</string>
+ <string name="settings.drawer_items_title">Seitenmenü</string>
+ <string name="settings.play_now_after">Jetzt wiedergeben bis zum Listenende</string>
+ <string name="settings.play_now_after_summary">\"Jetzt wiedergeben\" im Kontextmenü spielt das ausgewählte Lied und alle in der Liste nachfolgenden Lieder ab (wie in der Web-Schnittstelle des Subsonic-Server)</string>
+ <string name="settings.large_album_art">Große Cover anzeigen</string>
+ <string name="settings.large_album_art_summary">Verwende große Cover zur Anzeige der Alben anstatt einer Liste</string>
+ <string name="settings.admin_enabled">Administration aktiviert</string>
+ <string name="settings.admin_enabled_summary">Administration im Seitenmenü anzeigen</string>
+ <string name="settings.replay_gain">Wiedergabeverstärkung</string>
+ <string name="settings.replay_gain_summary">Gibt an, ob die Wiedergabeverstärkung anhand von Tags erfolgen soll.</string>
+ <string name="settings.replay_gain_type">Anhand der Tags</string>
+ <string name="settings.replay_gain_type.smart">Automatische Erkennung</string>
+ <string name="settings.replay_gain_type.album">Album Tag</string>
+ <string name="settings.replay_gain_type.track">Track Tag</string>
+ <string name="settings.replay_gain_bump">Wiedergabevorverstärkung</string>
+ <string name="settings.replay_gain_untagged">Lieder ohne Wiedergabeverstärkung</string>
+
+ <string name="shuffle.title">Mischen von</string>
+ <string name="shuffle.startYear">Startjahr:</string>
+ <string name="shuffle.endYear">Endjahr:</string>
+ <string name="shuffle.genre">Genre:</string>
+ <string name="shuffle.pick_genre">Wähle ein Genre</string>
+
+ <string name="share.info">Eigentümer: %1$s
+ \nBeschreibung: %2$s
+ \nURL: %3$s
+ \nErzeugt: %4$s
+ \nZuletzt besucht: %5$s
+ \nAblauf: %6$s
+ \nBesuchszähler: %7$s
+
+ </string>
+ <string name="share.expires">Ablauf: %s</string>
+ <string name="share.expires_never">nie</string>
+ <string name="share.deleted">Lösche Freigabe %s</string>
+ <string name="share.deleted_error">Löschen der Freigabe %s fehlgeschlagen</string>
+ <string name="share.no_expiration">Kein Ablauf</string>
+ <string name="share.expiration">Ablauf:</string>
+ <string name="share.updated_info">Aktualisiere Informationen der Freigabe %s</string>
+ <string name="share.updated_info_error">Aktualisierung der Freigabe %s fehlgeschlagen</string>
+ <string name="share.via">Teile via</string>
+ <string name="share.delete">Lösche Freigabe</string>
+
+ <string name="admin.add_user_username">Nutzername:</string>
+ <string name="admin.add_user_email">Email:</string>
+ <string name="admin.add_user_password">Passwort:</string>
+ <string name="admin.create_user_success">Neuen Nutzer wurde erfolgreich angelegt</string>
+ <string name="admin.create_user_error">Neuer Nutzer konnte nicht erzeugt werden</string>
+ <string name="admin.change_username_invalid">Bitte gültigen Nutzernamen angeben</string>
+ <string name="admin.update_permissions">Aktualisiere Berechtigungen</string>
+ <string name="admin.update_permissions_success">Berechtigungen für %1$s erfolgreich aktualisiert</string>
+ <string name="admin.update_permissions_error">Änderung der Berechtigungen für %1$s fehlgeschlagen</string>
+ <string name="admin.change_email">Email ändern</string>
+ <string name="admin.change_email_success">Email für %1$s wurde geändert</string>
+ <string name="admin.change_email_error">Konnte Email für %1$s nicht ändern</string>
+ <string name="admin.change_email_label">Neue Email:</string>
+ <string name="admin.change_email_invalid">Bitte gültige Email angeben</string>
+ <string name="admin.change_password">Passwort ändern</string>
+ <string name="admin.change_password_success">Passwort für %1$s erfolgreich geändert</string>
+ <string name="admin.change_password_error">Passwortänderung für %1$s ist fehlgeschlagen</string>
+ <string name="admin.change_password_label">Neues Passwort:</string>
+ <string name="admin.change_password_invalid">Bitte ein gültiges Passwort eingeben</string>
+ <string name="admin.delete_user">Nutzer löschen</string>
+ <string name="admin.delete_user_success">Nutzer %1$s erfolgreich gelöscht</string>
+ <string name="admin.delete_user_error">Nutzer %1$s konnte nicht gelöscht werden</string>
+ <string name="admin.confirm_password">Passwort bestätigen</string>
+ <string name="admin.confirm_password_bad">Eingegebenes Passwort ist falsch</string>
+
+ <string name="admin.scrobblingEnabled">Erlaube Scrobbeln</string>
+ <string name="admin.role.admin">Administrator</string>
+ <string name="admin.role.settings">Einstellungen ändern</string>
+ <string name="admin.role.download">Dateien herunterladen</string>
+ <string name="admin.role.upload">Hochladen zum Server</string>
+ <string name="admin.role.coverArt">Cover ändern</string>
+ <string name="admin.role.comment">Kommentare hinzufügen</string>
+ <string name="admin.role.podcast">Podcasts verwalten</string>
+ <string name="admin.role.stream">Musik streamen</string>
+ <string name="admin.role.jukebox">Jukebox kontrollieren</string>
+ <string name="admin.role.share">Freigaben verwalten</string>
+ <string name="admin.role.lastfm">Last.FM nutzen</string>
+
+ <string name="music_service.retry">Ein Netzwerkfehler ist aufgetreten. Versuch %1$d von %2$d.</string>
+
+ <string name="background_task.wait">Bitte warten...</string>
+ <string name="background_task.loading">Lade.</string>
+ <string name="background_task.no_network">Diese Programm benötigt Netzwerkzugriff. Bitte schalten Sie Wi-Fi oder Mobiles Netzwerk ein.</string>
+ <string name="background_task.network_error">Ein Netzwerkfehler ist aufgetreten. Bitte prüfen Sie die Serveradresse oder versuchen Sie es später nochmal.</string>
+ <string name="background_task.not_found">Quelle wurde nicht gefunden. Bitte prüfen Sie die Serveradresse.</string>
+ <string name="background_task.parse_error">Ein Fehler ist bei der Kommunikation mit dem Server aufgetreten. Bitte prüfen Sie die Serveradresse und stellen Sie sicher, das Sie der Server mit einem Webbrowser erreichen.</string>
+
+ <string name="service.connecting">Kontaktiere Server, bitte warten.</string>
+
+ <string name="parser.upgrade_client">Inkompatible Versionen. Aktualisierung von DSub erforderlich.</string>
+ <string name="parser.upgrade_server">Inkompatible Versionen. Aktualisierung des Subsonic Server erforderlich.</string>
+ <string name="parser.not_authenticated">Benutzername oder/und Passwort falsch.</string>
+ <string name="parser.not_authorized">Nicht authorisiert. Bitte prüfen Sie die Einstellungen im Subsonic-Server.</string>
+ <string name="parser.artist_count">%d Künstler.</string>
+ <string name="parser.server_error">Serverfehler: %s</string>
+ <string name="parser.scan_count">%d Einträge gefunden</string>
+
+ <string name="select_artist.refresh">Aktualisieren</string>
+ <string name="select_artist.folder">Wähle Ordner</string>
+ <string name="select_artist.all_folders">Alle Ordner</string>
+
+ <string name="equalizer.label">Equalizer</string>
+ <string name="equalizer.enabled">aktiv</string>
+ <string name="equalizer.preset">Wähle Vorgabe</string>
+ <string name="equalizer.bass_booster">Bassverstärkung</string>
+ <string name="equalizer.voice_booster">Sprachverstärkung</string>
+ <string name="equalizer.db_size">%d dB</string>
+ <string name="equalizer.bass_size">%d mille</string>
+
+ <string name="widget.initial_text">Zur Musikauswahl berühren</string>
+ <string name="widget.sdcard_busy">SD-Karte nicht verfügbar</string>
+ <string name="widget.sdcard_missing">Keine SD-Karte</string>
+
+ <string name="changelog_full_title">Änderungen</string>
+ <string name="changelog_title">Was ist Neu</string>
+ <string name="changelog_ok_button">OK</string>
+ <string name="changelog_show_full">Mehr…</string>
+
+ <string name="chat.send_a_message">Nachricht senden</string>
+
+ <string name="changelog_version_format" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">Version <xliff:g id="version_name">%s</xliff:g></string>
+
+ <string name="tasker.start_playing">Starte Wiedergabe</string>
+ <string name="tasker.start_playing_shuffled">Starte Zufallswiedergabe</string>
+ <string name="tasker.start_playing_title">Tasker -> Starte DSub</string>
+ <string name="tasker.edit_shuffle_mode">Starte im Zufallsmodus: </string>
+ <string name="tasker.edit_shuffle_start_year">Startjahr:</string>
+ <string name="tasker.edit_shuffle_end_year">Endjahr:</string>
+ <string name="tasker.edit_shuffle_genre">Genre:</string>
+ <string name="tasker.edit_server_offline">Zwischen On-/Offline-Modus wechseln: </string>
+ <string name="tasker.edit_do_nothing">Nichts tun</string>
+
+ <plurals name="select_album_n_songs">
+ <item quantity="zero">Keine Lieder</item>
+ <item quantity="one">Ein Lied</item>
+ <item quantity="other">%d Lieder</item>
+ </plurals>
+ <plurals name="select_album_n_songs_downloading">
+ <item quantity="one">Ein Lied wird heruntergeladen.</item>
+ <item quantity="other">%d Lieder werden heruntergeladen.</item>
+ </plurals>
+ <plurals name="select_album_n_songs_added">
+ <item quantity="one">Ein Lied zur Abspielliste hinzugefügt.</item>
+ <item quantity="other">%d Lieder zur Abspielliste hinzugefügt.</item>
+ </plurals>
+ <plurals name="select_album_donate_dialog_n_trial_days_left">
+ <item quantity="one">Noch ein Tag bis zum Ablauf des Testzeitraums.</item>
+ <item quantity="other">%d Tage bis zum Ablauf des Testzeitraums.</item>
+ </plurals>
+
+</resources>
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
new file mode 100644
index 00000000..cd255e13
--- /dev/null
+++ b/app/src/main/res/values-es/strings.xml
@@ -0,0 +1,580 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="common.appname">DSub</string>
+ <string name="common.ok">OK</string>
+ <string name="common.save">Guardar</string>
+ <string name="common.cancel">Cancelar</string>
+ <string name="common.play_now">Reproducir</string>
+ <string name="common.play_shuffled">Reproducir en aleatorio</string>
+ <string name="common.play_next">Siguiente en la cola</string>
+ <string name="common.play_last">Reproducir al final</string>
+ <string name="common.download">Caché</string>
+ <string name="common.pin">Caché permanente</string>
+ <string name="common.delete">Borrar</string>
+ <string name="common.star">Poner estrella</string>
+ <string name="common.unstar">Quitar estrella</string>
+ <string name="common.info">Detalles</string>
+ <string name="common.name">Nombre</string>
+ <string name="common.comment">Comentar</string>
+ <string name="common.public">Público</string>
+ <string name="common.play_external">Reproducir vídeo</string>
+ <string name="common.stream_external">Stream Video</string>
+ <string name="common.confirm">Confirmar</string>
+ <string name="common.confirm_message">Quieres %1$s %2$s?</string>
+ <string name="common.confirm_message_cache">cache</string>
+ <string name="common.empty">No hay datos</string>
+ <string name="common.warning">Aviso</string>
+
+ <string name="button_bar.home">Inicio</string>
+ <string name="button_bar.browse">Biblioteca</string>
+ <string name="button_bar.search">Buscar</string>
+ <string name="button_bar.playlists">Listas de reproducción</string>
+ <string name="button_bar.now_playing">Ahora suena</string>
+ <string name="button_bar.podcasts">Podcasts</string>
+ <string name="button_bar.bookmarks">Marcadores</string>
+ <string name="button_bar.chat">Chat</string>
+ <string name="button_bar.shares">Compartidos</string>
+ <string name="button_bar.downloading">Descargando</string>
+ <string name="button_bar.admin">Admin</string>
+
+ <string name="main.welcome_title">Bienvenido!</string>
+ <string name="main.welcome_text">Bienvenido a DSub! Ahora la aplicación está configurada para usar el servidor de demostración de Subsonic. Cuando configures tu servidor personal (disponible en <b>subsonic.org</b>), accede a <b>Preferencias</b> y cambia la configuración para conectarte.</string>
+ <string name="main.about_title">Acerca de DSub</string>
+ <string name="main.about_text">Autor: Scott Jackson
+ \nEmail: dsub.android@gmail.com
+ \nVersión: %1$s
+ \nArchivos en caché: %2$s
+ \nEspacio usado: %3$s of %4$s
+ \nEspacio disponible: %5$s of %6$s</string>
+ <string name="main.select_server">Seleccionar servidor</string>
+ <string name="main.shuffle">Reproducción aleatoria</string>
+ <string name="main.offline">Modo Offline</string>
+ <string name="main.online">Modo Online</string>
+ <string name="main.settings">Preferencias</string>
+ <string name="main.albums_title">Discos</string>
+ <string name="main.albums_newest">Añadidos recientemente</string>
+ <string name="main.albums_recent">Reproducidos recientemente</string>
+ <string name="main.albums_frequent">Más reproducidos</string>
+ <string name="main.albums_highest">Mejor valorados</string>
+ <string name="main.albums_starred">Con estrella</string>
+ <string name="main.albums_random">Aleatorio</string>
+ <string name="main.albums_genres">Géneros</string>
+ <string name="main.back_confirm">Pulsa atrás de nuevo para salir</string>
+ <string name="main.albums_year">Décadas</string>
+ <string name="main.faq_text">
+ <![CDATA[
+ <font color="red">Cache vs Cache permanente</font>:
+ <br/>Cuando DSub descarga las canciones, pueden ser eliminadas para dejar espacio a nuevas descargas. La música en Caché Permanente, nunca será eliminada automáticamente.
+ <p/><font color="red">ChromeCast falla</font>:
+ <br/>Asegúrate que no estás utilizando un certificado auto-firmado, porque Chromecast automáticamente lo rechazará.
+ ]]>
+ </string>
+ <string name="main.scan_complete">Escaneado del servidor completado</string>
+ <string name="main.albums_per_folder">Por carpetar</string>
+
+ <string name="menu.search">Buscar</string>
+ <string name="menu.shuffle">Aleatorio</string>
+ <string name="menu.refresh">Actualizar</string>
+ <string name="menu.play">Reproducir</string>
+ <string name="menu.play_last">Reproducir al final</string>
+ <string name="menu.exit">Salir</string>
+ <string name="menu.settings">Preferencias</string>
+ <string name="menu.help">Ayuda</string>
+ <string name="menu.about">Acerca de</string>
+ <string name="menu.add_playlist">Añadir a lista de reproducción</string>
+ <string name="menu.remove_playlist">Borrar de lista de reproducción</string>
+ <string name="menu.deleted_playlist">Listas borradas %s</string>
+ <string name="menu.deleted_playlist_error">Error al borrar la lista de reproducción %s</string>
+ <string name="menu.log">Enviar Log</string>
+ <string name="menu.set_timer">Ajustar temporizador</string>
+ <string name="menu.check_podcasts">Comprobar nuevos episodios</string>
+ <string name="menu.add_podcast">Añadir canal</string>
+ <string name="menu.keep_synced">Mantener sincronizado</string>
+ <string name="menu.stop_sync">Detener sincronización</string>
+ <string name="menu.show_all">Mostrar todo el contenido</string>
+ <string name="menu.show_artist">Mostrar artista</string>
+ <string name="menu.share">Compartir</string>
+ <string name="menu.delete_cache">Borrar cache</string>
+ <string name="menu.cast">Hacer streaming al dispositivo</string>
+ <string name="menu.add_user">Añadir usuario</string>
+ <string name="menu.rescan">Reescanear servidor</string>
+ <string name="menu.rate">Establecer valoración</string>
+ <string name="menu.top_tracks">Tp Tracks de Last.FM</string>
+ <string name="menu.similar_artists">Artistas similares</string>
+ <string name="menu.show_missing">Mostrar los que faltan</string>
+ <string name="menu.start_radio">Iniciar radio</string>
+
+ <string name="playlist.label">Listas de reproducción</string>
+ <string name="playlist.update_info">Actualizar información</string>
+ <string name="playlist.updated_info">Información actualizada para la lista %s</string>
+ <string name="playlist.updated_info_error">Error al actualizar la información de la lista %s</string>
+ <string name="playlist.overwrite">Sobreescribir lista de reproducción actual</string>
+ <string name="playlist.add_to">Añadir a lista de reproducción</string>
+ <string name="playlist.create_new">Crear nueva</string>
+ <string name="playlist.delete">Borrar lista de reproducción</string>
+
+ <string name="search.label">Buscar</string>
+ <string name="search.title">Buscar</string>
+ <string name="search.search">Click para buscar</string>
+ <string name="search.no_match">No hay coincidencias, inténtelo de nuevo</string>
+ <string name="search.artists">Artista</string>
+ <string name="search.albums">Disco</string>
+ <string name="search.songs">Canción</string>
+ <string name="search.more">Mostrar más</string>
+
+ <string name="progress.wait">Espere por favor...</string>
+
+ <string name="music_library.label">Biblioteca de medios</string>
+ <string name="music_library.label_offline">Archivos Offline</string>
+
+ <string name="select_album.select">Seleccionar todo</string>
+ <string name="select_album.n_selected">Seleccionadas %d canciones</string>
+ <string name="select_album.n_unselected">%d canciones deseleccionadas</string>
+ <string name="select_album.more">Más</string>
+ <string name="select_album.offline">Offline</string>
+ <string name="select_album.searching">Buscando...</string>
+ <string name="select_album.no_sdcard">Error: No hay tarjeta SD disponible</string>
+ <string name="select_album.no_network">Aviso: No hay red disponible</string>
+ <string name="select_album.not_licensed">Servidor sin licencia. Quedan %d días de prueba</string>
+ <string name="select_album.donate_dialog_message">Consigue descargas ilimitadas haciendo una donación a Subsonic</string>
+ <string name="select_album.donate_dialog_now">Ahora</string>
+ <string name="select_album.donate_dialog_later">Más tarde</string>
+ <string name="select_album.donate_dialog_0_trial_days_left">Periodo de prueba terminado</string>
+
+ <string name="offline.sync_dialog_title">Canciones offline esperando a ser sincronizadas</string>
+ <string name="offline.sync_dialog_message">Procesar %1$d offline scrobbles?
+ \nProcesar %2$d estrellas offline?
+ </string>
+ <string name="offline.sync_dialog_default">Usar esta acción por defecto</string>
+ <string name="offline.sync_success">%1$d canciones sincronizadas correctamente</string>
+ <string name="offline.sync_partial">%1$d de %2$d canciones sincronizadas</string>
+ <string name="offline.sync_error">Error al sincronizar canciones</string>
+
+ <string name="select_genre.blank">Vacío</string>
+ <string name="select_genre.songs">%d canciones</string>
+ <string name="select_genre.albums">%d albumes</string>
+
+ <string name="select_podcasts.error">Ha habido un error descargando este podcast en el servidor. El servidor debe descargarlo antes.</string>
+ <string name="select_podcasts.skipped">Este podcast no ha sido descargado en el servidor. El servidor debe descargarlo antes.</string>
+ <string name="select_podcasts.initializing">Este podcast está siendo reiniciado en el servidor. Por favor, actualice en unos segundos.</string>
+ <string name="select_podcasts.server_download">Descargar en el servidor</string>
+ <string name="select_podcasts.server_delete">Borrar en el servidor</string>
+ <string name="select_podcasts.downloading">Descargando %s en el servidor</string>
+ <string name="select_podcasts.refreshing">El servidor está comprobando nuevos podcasts ahora</string>
+ <string name="select_podcasts.deleted">Podcasts eliminados %s</string>
+ <string name="select_podcasts.deleted_error">Error al elimintar el podcast %s</string>
+ <string name="select_podcasts.add_url">URL:</string>
+ <string name="select_podcasts.created_error">Error al agregar podcast</string>
+ <string name="select_podcasts.invalid_podcast_channel">Canal de podcast no valido: %s</string>
+ <string name="select_podcasts.delete">Borrar podcast</string>
+
+ <string name="download.empty">Lista de reproducción vaía</string>
+ <string name="download.shuffle_loading">Cargando lista aleatoria...</string>
+ <string name="download.playerstate_downloading">Descargando - %s</string>
+ <string name="download.playerstate_buffering">Buffering</string>
+ <string name="download.playerstate_playing_shuffle">Reproduciendo en aleatorio</string>
+ <string name="download.menu_show_album">Mostrar disco</string>
+ <string name="download.menu_lyrics">Letras</string>
+ <string name="download.menu_remove">Eliminar de la cola</string>
+ <string name="download.menu_remove_all">Borrar todo</string>
+ <string name="download.menu_screen_on">Pantalla encendida</string>
+ <string name="download.menu_shuffle">Aleatorio</string>
+ <string name="download.menu_toggle">Cambiar</string>
+ <string name="download.menu_save">Guardar lista de reproducción</string>
+ <string name="download.menu_shuffle_notification">Lista de reproducción en aleatorio</string>
+ <string name="download.playlist_title">Guardar lista de reproducción</string>
+ <string name="download.playlist_name">Introduce un nombre válido para la lista de reproducción:</string>
+ <string name="download.playlist_saving">Guardando lista de reproducción \"%s\"...</string>
+ <string name="download.playlist_done">Lista de reproducción guardada.</string>
+ <string name="download.playlist_error">Error al guardar la lista de reproducción, inténtelo más tarde.</string>
+ <string name="download.repeat_off">Repetir off</string>
+ <string name="download.repeat_all">Repetir todo</string>
+ <string name="download.repeat_single">Repetir canción</string>
+ <string name="download.jukebox_on">Control remoto encendido. La música se está reproduciendo en el ordenador.</string>
+ <string name="download.jukebox_off">Control remoto apagado. La música se está reproduciendo en el dispositivo móvil.</string>
+ <string name="download.jukebox_volume">Volumen remoto</string>
+ <string name="download.jukebox_server_too_old">Control remoto no soportado. Por favor, actualice su servidor Subsonic.</string>
+ <string name="download.jukebox_offline">Control remoto no disponible en modo offline.</string>
+ <string name="download.jukebox_not_authorized">Control remoto no permitido. Por favor, active el modo jukebox en <b>Users &gt; Settings</b> en su servidor Subsonic.</string>
+ <string name="download.timer_length">Temporizador</string>
+ <string name="download.start_timer">Iniciar temporizador</string>
+ <string name="download.stop_timer">Detener temporizador</string>
+ <string name="download.need_download">El vídeo ha de ser descargado antes</string>
+ <string name="download.no_streaming_player">Ningún reproductor puede reproducir este stream</string>
+ <string name="download.playing_out_of">%1$d/%2$d</string>
+ <string name="download.save_bookmark_title">Crear marcador</string>
+ <string name="download.save_bookmark">Marcador creado</string>
+ <string name="download.downloading_title">Descargando %1$d canciones</string>
+ <string name="download.downloading_summary">En este momento: %1$s</string>
+ <string name="download.downloading_summary_expanded">Actual: %1$s
+ \nTamaño medio: %2$s
+ </string>
+ <string name="download.failed_to_load">Error al cargar</string>
+ <string name="download.menu_remove_played_songs">Borrar canciones reproducidas</string>
+ <string name="download.save_bookmark_failed">Error al crear marcador</string>
+
+ <string name="starring_content_starred">Marcado con estrella \"%s\"</string>
+ <string name="starring_content_unstarred">Sin marca de estrella \"%s\"</string>
+ <string name="starring_content_error">Error al actualizar \"%s\", inténtelo más tarde.</string>
+
+ <string name="playlist_error">Error al obtener las listas de reproducción</string>
+ <string name="updated_playlist">Añadida %1$s canción a \"%2$s\"</string>
+ <string name="updated_playlist_error">Error al actualizar \"%s\", inténtelo más tarde.</string>
+ <string name="removed_playlist">Borrar %1$s canciones de \"%2$s\"</string>
+
+ <string name="bookmark.delete">Borrar marcador</string>
+ <string name="bookmark.deleted">Borrado marcador para \"%s\"</string>
+ <string name="bookmark.deleted_error">Error al borrar el marcador para \"%s\"</string>
+ <string name="bookmark.details">Canción: %1$s
+ \nComentario: %2$s
+ \nPosición: %3$s</string>
+ <string name="bookmark.resume_title">Reanudar reproducción?</string>
+ <string name="bookmark.resume">Reanudar reproducción \'%1$s\' desde%2$s</string>
+ <string name="bookmark.action_resume">Reanudar</string>
+ <string name="bookmark.action_start_over">Volver a comenzar</string>
+ <string name="rating.title">Valoración\"%s\"</string>
+ <string name="rating.set_rating">Valoración establecida para \"%s\"</string>
+ <string name="rating.set_rating_failed">Error al establecer valoración para \"%s\"</string>
+ <string name="rating.remove_rating">Valoración eliminada a \"%s\"</string>
+ <string name="rating.remove_rating_failed">Error al eliminar valoración a \"%s\"</string>
+
+ <string name="song_details.error">Error</string>
+ <string name="song_details.skipped">Saltados</string>
+ <string name="song_details.downloading">Descargando</string>
+
+ <string name="lyrics.nomatch">No se han encontrado letras</string>
+
+ <string name="error.label">Error</string>
+
+ <string name="settings.title">Ajustes de DSub</string>
+ <string name="settings.test_connection_title">Comprobar conexión</string>
+ <string name="settings.servers_add">Añadir servidor</string>
+ <string name="settings.servers_remove">Borrar servidor</string>
+ <string name="settings.servers_title">Servidores</string>
+ <string name="settings.server_unused">Sin usar</string>
+ <string name="settings.server_name">Nombre</string>
+ <string name="settings.server_address">Dirección del servidor</string>
+ <string name="settings.server_username">Usuario</string>
+ <string name="settings.server_password">Contraseña</string>
+ <string name="settings.server_open_browser">Abrir en el navegador</string>
+ <string name="settings.cache_title">Caché de música</string>
+ <string name="settings.preload_wifi">Canciones para precargar (Wifi)</string>
+ <string name="settings.preload_mobile">Canciones pare precargar (Móvil)</string>
+ <string name="settings.cache_size">Tamaño de caché (MB)</string>
+ <string name="settings.cache_location">Ruta de caché</string>
+ <string name="settings.cache_location_error">Ruta de caché no válida. Utilizando la opción por defecto.</string>
+ <string name="settings.cache_clear">Limpiar cache</string>
+ <string name="settings.cache_clear_complete">Caché limpiada</string>
+ <string name="settings.testing_connection">Probando conexión...</string>
+ <string name="settings.testing_ok">Conexión correcta</string>
+ <string name="settings.testing_unlicensed">Conexión correcta. Servidor sin licencia.</string>
+ <string name="settings.connection_failure">Error de conexión.</string>
+ <string name="settings.invalid_url">Introduzca una URL válida.</string>
+ <string name="settings.invalid_username">Por favor, especifique un usuario válido.</string>
+ <string name="settings.appearance_title">Apariencia</string>
+ <string name="settings.theme_title">Tema</string>
+ <string name="settings.theme_light">Claro</string>
+ <string name="settings.theme_dark">Oscuro</string>
+ <string name="settings.theme_black">Negro</string>
+ <string name="settings.theme_holo">Holo</string>
+ <string name="settings.theme_fullscreen">Pantalla completa</string>
+ <string name="settings.theme_fullscreen_summary">Esconder tantos elementos de la interfaz como Android permita</string>
+ <string name="settings.track_title">Mostrar número de pista</string>
+ <string name="settings.track_summary">Mostrar número de pista al inicio de la canción si existe</string>
+ <string name="settings.custom_sort">Ordenar por año</string>
+ <string name="settings.custom_sort_summary">Ordenar discos por año o alfabeticamente</string>
+ <string name="settings.network_title">Red</string>
+ <string name="settings.max_bitrate_wifi">Bitrate máximo - Wi-Fi</string>
+ <string name="settings.max_bitrate_mobile">Bitrate máximo - Móvil</string>
+ <string name="settings.max_bitrate_32">32 Kbps</string>
+ <string name="settings.max_bitrate_64">64 Kbps</string>
+ <string name="settings.max_bitrate_80">80 Kbps</string>
+ <string name="settings.max_bitrate_96">96 Kbps</string>
+ <string name="settings.max_bitrate_112">112 Kbps</string>
+ <string name="settings.max_bitrate_128">128 Kbps</string>
+ <string name="settings.max_bitrate_160">160 Kbps</string>
+ <string name="settings.max_bitrate_192">192 Kbps</string>
+ <string name="settings.max_bitrate_256">256 Kbps</string>
+ <string name="settings.max_bitrate_320">320 Kbps</string>
+ <string name="settings.max_video_bitrate_wifi">Bitrate de vídeo máximo - Wi-Fi</string>
+ <string name="settings.max_video_bitrate_mobile">Bitrate de vídeo máximo - Móvil</string>
+ <string name="settings.max_video_bitrate_200">200 Kbps</string>
+ <string name="settings.max_video_bitrate_300">300 Kbps</string>
+ <string name="settings.max_video_bitrate_400">400 Kbps</string>
+ <string name="settings.max_video_bitrate_500">500 Kbps</string>
+ <string name="settings.max_video_bitrate_700">700 Kbps</string>
+ <string name="settings.max_video_bitrate_1000">1000 Kbps</string>
+ <string name="settings.max_video_bitrate_1500">1500 Kbps</string>
+ <string name="settings.max_video_bitrate_2000">2000 Kbps</string>
+ <string name="settings.max_video_bitrate_3000">3000 Kbps</string>
+ <string name="settings.max_video_bitrate_5000">5000 Kbps</string>
+ <string name="settings.max_bitrate_unlimited">Ilimitado</string>
+ <string name="settings.wifi_required_title">Streaming sólo en Wi-Fi</string>
+ <string name="settings.wifi_required_summary">Sólo utilizar streaming si estás conectado a Wi-Fi</string>
+ <string name="settings.network_timeout_title">Network Timeout</string>
+ <string name="settings.network_timeout_10000">10 seconds</string>
+ <string name="settings.network_timeout_15000">15 seconds</string>
+ <string name="settings.network_timeout_30000">30 seconds</string>
+ <string name="settings.network_timeout_45000">45 seconds</string>
+ <string name="settings.network_timeout_60000">60 seconds</string>
+ <string name="settings.preload_0">0 canción</string>
+ <string name="settings.preload_1">1 canción</string>
+ <string name="settings.preload_2">2 canciones</string>
+ <string name="settings.preload_3">3 canciones</string>
+ <string name="settings.preload_5">5 canciones</string>
+ <string name="settings.preload_10">10 canciones</string>
+ <string name="settings.preload_unlimited">Ilimitado</string>
+ <string name="settings.clear_search_history">Borrar historial de búsqueda</string>
+ <string name="settings.search_history_cleared">Historial de búsqueda borrado</string>
+ <string name="settings.other_title">Otros ajustes</string>
+ <string name="settings.scrobble_title">Scrobblear a Last.fm</string>
+ <string name="settings.scrobble_summary">Recuerde configurar su usuario y contraseña de Last.fm en su servidor</string>
+ <string name="settings.hide_media_title">Ocultar del resto</string>
+ <string name="settings.hide_media_summary">Ocultar archivos de música de otras aplicaciones.</string>
+ <string name="settings.hide_media_toast">Se aplicará la próxima vez que Android escanee la música de su teléfono.</string>
+ <string name="settings.media_button_title">Botones multimedia</string>
+ <string name="settings.media_button_summary">Responder a los botones Bluetooth, teléfono y manos libres</string>
+ <string name="settings.screen_lit_title">Mantener pantalla encendida</string>
+ <string name="settings.screen_lit_summary">Mantener la pantalla encendida durantes las descargas mejora la velocidad de descarga.</string>
+ <string name="settings.playlist_title">Listas de reproducción</string>
+ <string name="settings.playlist_random_size_title">Tamaño de la lista de reproducción en aleatorio</string>
+ <string name="settings.sleep_timer_title">Temporizador</string>
+ <string name="settings.sleep_timer_duration_title">Duración del temporizador</string>
+ <string name="settings.sleep_timer_off">Encendido</string>
+ <string name="settings.sleep_timer_on">Apagado</string>
+ <string name="settings.sleep_timer_always_on">Siempre encendido</string>
+ <string name="settings.temp_loss_title">Pérdida temporal de foco</string>
+ <string name="settings.temp_loss_pause">Pausar siempre</string>
+ <string name="settings.temp_loss_pause_lower">Pausar, bajar el volumen cuando sea solicitado</string>
+ <string name="settings.temp_loss_lower">Bajar volumen siempre</string>
+ <string name="settings.temp_loss_nothing">No hacer nada</string>
+ <string name="settings.disconnect_pause_title">Pausar al desconectar</string>
+ <string name="settings.disconnect_pause_both">Pausar en todos los casos</string>
+ <string name="settings.disconnect_pause_neither">No hacer nada</string>
+ <string name="settings.persistent_title">Notificación permanente</string>
+ <string name="settings.persistent_summary">Mostrar la notificación incluso tras pausar. Pulsar botón stop para quitarlo.</string>
+ <string name="settings.gapless_playback">Reproducción sin pausas</string>
+ <string name="settings.gapless_playback_summary">El Galaxy S3 parece sufrir algunos bloqueos y funcionamentos extraños desde la introducción de la reproducción sin pausas. Desmarca esta opción para solucionar el problema.</string>
+ <string name="settings.chat_refresh">Tiempo de actualización de chat (Segundos)</string>
+ <string name="settings.chat_enabled">Chat habilitado</string>
+ <string name="settings.chat_enabled_summary">Para mostrar o no mostrar la pestaña de chat. Reinicia la aplicación tras el cambio.</string>
+ <string name="settings.video_title">Vídeo</string>
+ <string name="settings.video_player">Reproductor de vídeo</string>
+ <string name="settings.video_raw">Raw (Requiere Subsonic 4.8+)</string>
+ <string name="settings.video_hls">HTTP Live Stream (HLS) (Requiere Subsonic 4.8+)</string>
+ <string name="settings.video_transcode">Transcodificación directa (Requisitos de vídeo -> configurar mp4 o similar en el servidor)</string>
+ <string name="settings.video_flash">Flash (Requiere Plugin)</string>
+ <string name="settings.cache_screen_title">Caché/Red</string>
+ <string name="settings.playback_title">Reproducción</string>
+ <string name="settings.hide_widget_title">Ocultar Widget</string>
+ <string name="settings.hide_widget_summary">Ocultar widget tras abandonar la aplicación</string>
+ <string name="settings.podcasts_enabled">Podcasts Habilitados</string>
+ <string name="settings.podcasts_enabled_summary">Mostrar o no mostrar el apartado Podcasts en el menú</string>
+ <string name="settings.bookmarks_enabled">Marcadores Habilitados</string>
+ <string name="settings.bookmarks_enabled_summary">Mostrar o no mostrar el apartado Marcadoresen el menú</string>
+ <string name="settings.sync_title">Sincronizar</string>
+ <string name="settings.sync_enabled">Sincronización habilitada</string>
+ <string name="settings.sync_enabled_summary">Comprobar o no comprobar periódicamente cambios en las listas de reproducción o podcasts</string>
+ <string name="settings.sync_interval">Intervalo de sincronización</string>
+ <string name="settings.sync_wifi">Sincronizar sólo con Wifi</string>
+ <string name="settings.sync_wifi_summary">Sincronizar únicamente al estar conectado a una red Wifi</string>
+ <string name="settings.sync_most_recent">Sincronizar añadidos recientemente</string>
+ <string name="settings.sync_most_recent_summary">Descargar a caché albumes añadidos recientemente</string>
+ <string name="settings.sync_starred">Sincronizar los elementos con estrella</string>
+ <string name="settings.sync_starred_summary">Descargar a caché automáticamente canciones, albumes y artistas que contengan estrella</string>
+ <string name="settings.sync_notification">Mostrar notificación de sincronización</string>
+ <string name="settings.sync_notification_summary">Mostrar una notificación tras haber sincronizado nuevo contenido</string>
+ <string name="settings.sync_interval_15">15 Minutos</string>
+ <string name="settings.sync_interval_30">30 Minutos</string>
+ <string name="settings.sync_interval_60">1 Hora</string>
+ <string name="settings.sync_interval_120">2 Horas</string>
+ <string name="settings.sync_interval_240">4 Horas</string>
+ <string name="settings.sync_interval_360">6 Horas</string>
+ <string name="settings.sync_interval_720">12 Horas</string>
+ <string name="settings.sync_interval_1440">Diariamente</string>
+ <string name="settings.menu_options.title">Opciones del menú opcionales</string>
+ <string name="settings.menu_options.play_next_summary">Mostrar Reproducir Siguiente en los menús</string>
+ <string name="settings.menu_options.play_last_summary">Mostrar Reproducir Anterior en menús</string>
+ <string name="settings.menu_options.star_summary">Mostrar estrella en menús</string>
+ <string name="settings.menu_options.shared_summary">Mostrar Compartir en menús</string>
+ <string name="settings.shares_enabled">Compartir habilitadoShares Enabled</string>
+ <string name="settings.shares_enabled_summary">Mostrar o no mostrar la lista de compartidos en el menú desplegable</string>
+ <string name="settings.server_internal_address">Dirección de red interna</string>
+ <string name="settings.browse_by_tags">Explorar por tags</string>
+ <string name="settings.browse_by_tags_summary">Explorar por tags en vez de por estructura de carpetas. Requiere Subsonic 4.7+</string>
+ <string name="settings.server_local_network_ssid" >SSID Red local</string>
+ <string name="settings.server_local_network_ssid_hint">SSID Actual: %s</string>
+ <string name="settings.cache_location_reset">No se puede escribir en la ubicación seleccionada para la caché. Si has actualizado recientemente el sistema operativo de tu dispositivo a KitKat 4.4, la manera en la que las apps escriben en la Tarjeta SD ha cambiado, por lo que sólo pueden escribir en una ubicación específica. La ubicación que utiliza DSub ha sido automáticamente cambiada por la ubicación correcta. Para eliminar todos los datos de la app anterior, tendrás que introducir la Tarjeta SD en el ordenador y eliminar la carpeta manualmente.</string>
+ <string name="settings.override_system_language">Sobreescribir idioma del sistema</string>
+ <string name="settings.override_system_language_summary">Mostrar la aplicación en inglés aún teniendo disponible DSub en el idioma del sistema. Probablemente necesite borrar la aplicación de la memoria para efectuar los cambios.</string>
+ <string name="settings.drawer_items_title">Pestañas</string>
+ <string name="settings.play_now_after">Reproducir ahora - Después</string>
+ <string name="settings.play_now_after_summary">Pulsar "Reproducir ahora" desde el menú contextual actúa como la interfaz web de Subsonic, reproduciendo todos los items a partir del seleccionado</string>
+ <string name="settings.large_album_art">Carátulas grandes</string>
+ <string name="settings.large_album_art_summary">Mostrar los discos con carátulas grandes en vez de en lista</string>
+ <string name="settings.admin_enabled">Admin Habilitado</string>
+ <string name="settings.admin_enabled_summary">Mostrar u ocultar la pestaña de Admin en el menú</string>
+ <string name="settings.server_sync_summary">Activar o no la sincronización en este server</string>
+ <string name="settings.server_sync">Sincronización activada</string>
+ <string name="settings.menu_options.rate_summary">Mostrar valoración en los menús</string>
+ <string name="settings.replay_gain">Ganancia de reproducción</string>
+ <string name="settings.replay_gain_summary">Modificar la ganancia de reproducción por tags de pista o album</string>
+ <string name="settings.replay_gain_bump">Ganancia de Pre-amp en la reproducciónReplay Gain Pre-amp</string>
+ <string name="settings.replay_gain_untagged">Canciones sin ganancia de reproducciónSongs without Replay Gain</string>
+ <string name="settings.replay_gain_type">Leer desde los tags</string>
+ <string name="settings.replay_gain_type.smart">Detección inteligente</string>
+ <string name="settings.replay_gain_type.album">Tags de albums</string>
+ <string name="settings.replay_gain_type.track">Tags de pistas</string>
+ <string name="settings.open_to_tab">Abrir en pestaña</string>
+ <string name="settings.open_to_tab_summary">Abrir directamente a esta pestaña</string>
+
+ <string name="share.info">Dueño: %1$s
+ \nDescripción: %2$s
+ \nURL: %3$s
+ \nCreado: %4$s
+ \nÚltima visita: %5$s
+ \nExpira: %6$s
+ \nNúmero de visitas: %7$s
+ </string>
+ <string name="share.expires">Expira: %s</string>
+ <string name="share.expires_never">Nunca expira</string>
+ <string name="share.deleted">Compartición eliminada %s</string>
+ <string name="share.deleted_error">Error al eliminar compartición %s</string>
+ <string name="share.no_expiration">No expira</string>
+ <string name="share.expiration">Expira:</string>
+ <string name="share.updated_info">Información de compartición actualizada para %s</string>
+ <string name="share.updated_info_error">Error al actualizar información de compartición para %s</string>
+ <string name="share.via">Compartir vía</string>
+ <string name="share.delete">Borrar compartido</string>
+
+ <string name="admin.add_user_username">Usuario:</string>
+ <string name="admin.add_user_email">Email:</string>
+ <string name="admin.add_user_password">Contraseña:</string>
+ <string name="admin.create_user_success">Usuario creado satisfactoriamente</string>
+ <string name="admin.create_user_error">Error al crear nuevo usuario</string>
+ <string name="admin.change_username_invalid">Introduce un nombre de usuario válido</string>
+ <string name="admin.update_permissions">Actualizar permisos</string>
+ <string name="admin.update_permissions_success">Permisos de %1$s actualizados satisfactoriamente</string>
+ <string name="admin.update_permissions_error">Error al actualizar permisos de %1$s</string>
+ <string name="admin.change_email">Cambiar email</string>
+ <string name="admin.change_email_success">Email de %1$s cambiado satisfactoriamente</string>
+ <string name="admin.change_email_error">Error al cambiar el email de %1$s</string>
+ <string name="admin.change_email_label">Nuevo email:</string>
+ <string name="admin.change_email_invalid">Introduce un email válido</string>
+ <string name="admin.change_password">Cambiar contraseña</string>
+ <string name="admin.change_password_success">Contraseña de %1$s cambiada satisfactoriamente</string>
+ <string name="admin.change_password_error">Error al cambiar la contraseña de %1$s</string>
+ <string name="admin.change_password_label">Nueva contraseña:</string>
+ <string name="admin.change_password_invalid">Introduce una contraseña válida</string>
+ <string name="admin.delete_user">Eliminar usuario</string>
+ <string name="admin.delete_user_success">%1$s eliminado satisfactoriamente</string>
+ <string name="admin.delete_user_error">Error al eliminar %1$s</string>
+ <string name="admin.confirm_password">Confirmar contraseña</string>
+ <string name="admin.confirm_password_bad">La contraseña no es correcta</string>
+
+ <string name="admin.scrobblingEnabled">Scrobbling permitido</string>
+ <string name="admin.role.admin">Administrador</string>
+ <string name="admin.role.settings">Cambiar ajustes</string>
+ <string name="admin.role.download">Descargar archivos originales</string>
+ <string name="admin.role.upload">Subir al servidor</string>
+ <string name="admin.role.coverArt">Cambiar carátula</string>
+ <string name="admin.role.comment">Añadir comentarios</string>
+ <string name="admin.role.podcast">Gestionar podcasts</string>
+ <string name="admin.role.stream">Hacer Stream de música</string>
+ <string name="admin.role.jukebox">Controlar el jukebox</string>
+ <string name="admin.role.share">Gestionar compartidos</string>
+ <string name="admin.role.lastfm">Utilizar Last.FM</string>
+
+ <string name="sync.new_podcasts">Nuevos podcasts disponibles</string>
+ <string name="sync.new_playlists">Nuevas canciones en la lista de reproducción</string>
+ <string name="sync.new_albums">Nuevos discos disponibles</string>
+ <string name="sync.new_starred">Nuevas canciones favoritas disponibles</string>
+
+ <string name="shuffle.title">Iniciar aleatorio por</string>
+ <string name="shuffle.startYear">Año inicial:</string>
+ <string name="shuffle.endYear">Año final:</string>
+ <string name="shuffle.genre">Género:</string>
+ <string name="shuffle.pick_genre">Seleccionar género</string>
+
+ <string name="music_service.retry">Error de red. Reintentando %1$d de %2$d.</string>
+
+ <string name="background_task.wait">Por favor, espere...</string>
+ <string name="background_task.loading">Cargando.</string>
+ <string name="background_task.no_network">Este programa requiere de acceso a la red. Encienda el Wi-Fi o la conexión de datos móviles.</string>
+ <string name="background_task.network_error">Error de red. Por favor, compruebe la dirección del servidor o inténtelo más tarde.</string>
+ <string name="background_task.not_found">Recurso no encontrado. Por favor, compruebe la dirección del servidor.</string>
+ <string name="background_task.parse_error">Respuesta desconocida. Por favor, compruebe la dirección del servidor.</string>
+
+ <string name="service.connecting">Conectando con el servidor, espere por favor.</string>
+
+ <string name="parser.upgrade_client">Versiones incompatibles. Por favor, actualice la aplicación DSub de Android.</string>
+ <string name="parser.upgrade_server">Versiones incompatibles. Por favor, actualice su servidor Subsonic.</string>
+ <string name="parser.not_authenticated">Usuario o contraseña inconrrectos.</string>
+ <string name="parser.not_authorized">Sin autorización. Compruebe los permisos en el servidor Subsonic.</string>
+ <string name="parser.artist_count">Recibidos %d artistas.</string>
+ <string name="parser.scan_count">Escaneados %d entradas</string>
+
+ <string name="select_artist.refresh">Actualizar</string>
+ <string name="select_artist.folder">Seleccionar carpeta</string>
+ <string name="select_artist.all_folders">Todas las carpetas</string>
+
+ <string name="equalizer.bass_booster">Potenciar bajos</string>
+ <string name="equalizer.voice_booster">Potenciar vocesr</string>
+ <string name="equalizer.db_size">%d dB</string>
+ <string name="equalizer.bass_size">%d mille</string>
+
+ <string name="equalizer.label">Ecualizador</string>
+ <string name="equalizer.enabled">Activado</string>
+ <string name="equalizer.preset">Seleccionar preset</string>
+
+ <string name="widget.4x1">DSub (4x1)</string>
+ <string name="widget.4x2">DSub (4x2)</string>
+ <string name="widget.4x3">DSub (4x3)</string>
+ <string name="widget.4x4">DSub (4x4)</string>
+ <string name="widget.initial_text">Toca para seleccionar música</string>
+ <string name="widget.sdcard_busy">Tarjeta SD no disponible</string>
+ <string name="widget.sdcard_missing">No hay tarjeta SD</string>
+
+ <string name="util.bytes_format.gigabyte">0.00 GB</string>
+ <string name="util.bytes_format.megabyte">0.00 MB</string>
+ <string name="util.bytes_format.kilobyte">0 KB</string>
+ <string name="util.bytes_format.byte">0 B</string>
+
+ <string name="changelog_full_title">Log de cambios</string>
+ <string name="changelog_title">Novedades</string>
+ <string name="changelog_ok_button">OK</string>
+ <string name="changelog_show_full">Más…</string>
+
+ <string name="chat.send_a_message">Enviar un mensaje</string>
+
+ <string name="changelog_version_format" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">Version <xliff:g id="version_name">%s</xliff:g></string>
+
+ <string name="tasker.start_playing">Comenzar reproduciendo</string>
+ <string name="tasker.start_playing_title">Tasker -> Encender DSub</string>
+ <string name="tasker.edit_shuffle_mode">Comenzar en modo aleatorio: </string>
+ <string name="tasker.start_playing_shuffled">Comenzar en modo aleatorio</string>
+ <string name="tasker.edit_do_nothing">No hacer nada</string>
+ <string name="tasker.edit_shuffle_genre">Aleatorio por género:</string>
+ <string name="tasker.edit_shuffle_start_year">Comenzar reproducción aleatoria desde el año:</string>
+ <string name="tasker.edit_shuffle_end_year">Último año de reproducción alteatoriar:</string>
+
+ <plurals name="select_album_n_songs">
+ <item quantity="zero">Sin canciones</item>
+ <item quantity="one">Una canción</item>
+ <item quantity="other">%d canciones</item>
+ </plurals>
+ <plurals name="select_album_n_songs_downloading">
+ <item quantity="one">Una canción programada para descarga.</item>
+ <item quantity="other">%d canciones programadas para descarga.</item>
+ </plurals>
+ <plurals name="select_album_n_songs_added">
+ <item quantity="one">Canción añadida a la cola.</item>
+ <item quantity="other">%d canciones añadidas a la cola.</item>
+ </plurals>
+ <plurals name="select_album_donate_dialog_n_trial_days_left">
+ <item quantity="one">Queda un día del periodo de prueba</item>
+ <item quantity="other">Quedan %d días del periodo de prueba</item>
+ </plurals>
+
+</resources>
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
new file mode 100644
index 00000000..759e3fd8
--- /dev/null
+++ b/app/src/main/res/values-fr/strings.xml
@@ -0,0 +1,570 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="common.appname">DSub</string>
+ <string name="common.ok">OK</string>
+ <string name="common.save">Sauver</string>
+ <string name="common.cancel">Annuler</string>
+ <string name="common.play_now">Jouer</string>
+ <string name="common.play_shuffled">Jouer au hasard</string>
+ <string name="common.play_next">Suivant</string>
+ <string name="common.play_last">Précédent</string>
+ <string name="common.download">Mettre en cache</string>
+ <string name="common.pin">Mettre en cache Permanent</string>
+ <string name="common.delete">Supprimer</string>
+ <string name="common.star">Favori</string>
+ <string name="common.unstar">Supp. favori</string>
+ <string name="common.info">Détails</string>
+ <string name="common.name">Nom</string>
+ <string name="common.comment">Commentaire</string>
+ <string name="common.public">Publique</string>
+ <string name="common.play_external">Jouer Video</string>
+ <string name="common.stream_external">Stream Video</string>
+ <string name="common.confirm">Confirmer</string>
+ <string name="common.confirm_message">Voulez-vous %1$s %2$s ?</string>
+ <string name="common.confirm_message_cache">cache</string>
+ <string name="common.empty">Aucune donnée</string>
+ <string name="common.warning">Avertissement</string>
+
+ <string name="button_bar.home">Accueil</string>
+ <string name="button_bar.browse">Bibliothèque</string>
+ <string name="button_bar.search">Recherche</string>
+ <string name="button_bar.playlists">Playlists</string>
+ <string name="button_bar.now_playing">Lecture en cours</string>
+ <string name="button_bar.podcasts">Podcasts</string>
+ <string name="button_bar.bookmarks">Favoris</string>
+ <string name="button_bar.shares">Partages</string>
+ <string name="button_bar.chat">Chat</string>
+ <string name="button_bar.admin">Admin</string>
+ <string name="button_bar.downloading">Téléchargement</string>
+
+ <string name="main.welcome_title">Bienvenue !</string>
+ <string name="main.welcome_text">Bienvenue dans DSub ! L\'application est actuellement configurée pour se connecter au serveur de démo Subsonic (<b>demo.subsonic.org</b>). Vous pouvez configurer votre propre serveur dans les paramètres. Choisir <b>Paramètres</b> et mettre à jour la configuration pour vous y connecter.</string>
+ <string name="main.about_title">A propos de DSub</string>
+ <string name="main.about_text">Auteur : Scott Jackson
+ \nEmail : dsub.android@gmail.com
+ \nVersion : %1$s
+ \nFichiers en cache : %2$s
+ \nEspace utilisé : %3$s de %4$s
+ \nEspace dispo. : %5$s de %6$s</string>
+ <string name="main.faq_title">FAQ</string>
+ <string name="main.faq_text">
+ <![CDATA[
+ <font color="red">Cache vs Cache permanent</font> :
+ <br/>Lorsque des titres sont téléchargés par DSub, ils peuvent être supprimés pour libérer de l\'espace pour de nouveaux téléchargements. Le cache permanent premet, lui, de ne jamais supprimer automatiquement la musique téléchargée.
+ <p/><font color="red">ChromeCast a échoué</font> :
+ <br/>Assurez-vous de ne pas utiliser un certificat auto-signé, Chromecast les rejette systématiquement.
+ ]]>
+ </string>
+ <string name="main.select_server">Choisir un serveur</string>
+ <string name="main.shuffle">Jouer au hasard</string>
+ <string name="main.offline">Déconnecter</string>
+ <string name="main.online">Connecter</string>
+ <string name="main.settings">Paramètres</string>
+ <string name="main.albums_title">Albums</string>
+ <string name="main.albums_newest">Ajoutés récemments</string>
+ <string name="main.albums_recent">Joués récemment</string>
+ <string name="main.albums_frequent">Les plus joués</string>
+ <string name="main.albums_highest">Les mieux notés</string>
+ <string name="main.albums_starred">Favoris</string>
+ <string name="main.albums_random">Au hasard</string>
+ <string name="main.albums_genres">Par genres</string>
+ <string name="main.albums_year">Par décennies</string>
+ <string name="main.songs_genres">@string/main.albums_genres</string>
+ <string name="main.back_confirm">Presser retour à nouveau pour quitter</string>
+ <string name="main.scan_complete">Completed scan of Server</string>
+
+ <string name="menu.search">Recherche</string>
+ <string name="menu.shuffle">Hasard</string>
+ <string name="menu.refresh">Recharger</string>
+ <string name="menu.play">Jouer</string>
+ <string name="menu.exit">Quitter</string>
+ <string name="menu.play_last">Jouer dernier</string>
+
+ <string name="menu.settings">Paramètres</string>
+ <string name="menu.help">Aide</string>
+ <string name="menu.about">A propos</string>
+ <string name="menu.add_playlist">Ajouter à la playlist</string>
+ <string name="menu.remove_playlist">Supprimer de la playlist</string>
+ <string name="menu.deleted_playlist">Supprimer la playlist %s</string>
+ <string name="menu.deleted_playlist_error">Echec de la suppression de la playlist %s</string>
+ <string name="menu.log">Envoyer le journal</string>
+ <string name="menu.set_timer">Ajuster le minuteur</string>
+ <string name="menu.check_podcasts">Vérifier les nouveaux podcasts</string>
+ <string name="menu.add_podcast">Ajouter une chaîne</string>
+ <string name="menu.keep_synced">Synchronisation automatique</string>
+ <string name="menu.stop_sync">Arrêter la synchro.</string>
+ <string name="menu.show_all">Afficher tous les media</string>
+ <string name="menu.show_artist">Afficher l\'artiste</string>
+ <string name="menu.share">Partager</string>
+ <string name="menu.delete_cache">Supprimer du cache</string>
+ <string name="menu.cast">Diffuser vers appareil</string>
+ <string name="menu.faq">FAQ</string>
+ <string name="menu.add_user">Ajouter utilisateur</string>
+ <string name="menu.rescan">Relire le server</string>
+ <string name="menu.rate">Noter</string>
+
+ <string name="playlist.label">Playlists</string>
+ <string name="playlist.update_info">Mise à jour informations</string>
+ <string name="playlist.updated_info">Informations de la playlist %s mises à jour</string>
+ <string name="playlist.updated_info_error">Echec de la mise à jour des informations de la playlist %s</string>
+ <string name="playlist.overwrite">Remplacer la playlist existante</string>
+ <string name="playlist.add_to">Ajouter à la playlist</string>
+ <string name="playlist.create_new">Créer une nouvelle</string>
+ <string name="playlist.delete">Supprimer la playlist</string>
+
+ <string name="search.label">Recherche</string>
+ <string name="search.title">Recherche</string>
+ <string name="search.search">Cliquer pour rechercher</string>
+ <string name="search.no_match">Aucun résultat, recommencer</string>
+ <string name="search.artists">Artistes</string>
+ <string name="search.albums">Albums</string>
+ <string name="search.songs">Chansons</string>
+ <string name="search.more">Afficher plus</string>
+
+ <string name="progress.wait">Patientez…</string>
+
+ <string name="music_library.label">Bibliothèque</string>
+ <string name="music_library.label_offline">Média mode déconnecté</string>
+
+ <string name="select_album.select">Tout sélectionner</string>
+ <string name="select_album.n_selected">%d pistes sélectionnées.</string>
+ <string name="select_album.n_unselected">%d pistes désélectionnées.</string>
+ <string name="select_album.more">Plus</string>
+ <string name="select_album.offline">Déconnecté</string>
+ <string name="select_album.searching">Recherche en cours...</string>
+ <string name="select_album.no_sdcard">Erreur : Aucune carte SD card disponible.</string>
+ <string name="select_album.no_network">Problème : Aucun réseau disponible.</string>
+ <string name="select_album.not_licensed">Serveur sans licence valide. %d jours restant.</string>
+ <string name="select_album.donate_dialog_message">Téléchargement illimité en supportant Subsonic.</string>
+ <string name="select_album.donate_dialog_now">Maintenant</string>
+ <string name="select_album.donate_dialog_later">Plus tard</string>
+ <string name="select_album.donate_dialog_0_trial_days_left">La période d\'essai est terminée</string>
+
+ <string name="offline.sync_dialog_title">Chanson déconnectées en attente de synchro</string>
+ <string name="offline.sync_dialog_message">Gérer %1$d préférences en mode déconnecté ?
+ \nGérer %2$d favoris en mode déconnecté ?
+ </string>
+ <string name="offline.sync_dialog_default">Utiliser cette action par défaut</string>
+ <string name="offline.sync_success">%1$d chansons synchronisées avec succès</string>
+ <string name="offline.sync_partial">%1$d chansons sur %2$d synchronisées avec succès</string>
+ <string name="offline.sync_error">Echec de la synchro. des chansons</string>
+
+ <string name="select_genre.blank">Vide</string>
+ <string name="select_genre.songs">%d chansons</string>
+ <string name="select_genre.albums">%d albums</string>
+
+ <string name="select_podcasts.error">Une erreur est survenue avec ce podcast pendant le chargement. Le serveur doit d\'abord le télécharger.</string>
+ <string name="select_podcasts.skipped">Ce podcast n\'a pas été chargé sur le serveur. Le serveur doit d\'abord le télécharger.</string>
+ <string name="select_podcasts.initializing">Le chargement du podcast a commencer sur le serveur. Recharger SVP dans quelques instants.</string>
+ <string name="select_podcasts.server_download">Télécharger sur le serveur</string>
+ <string name="select_podcasts.server_delete">Supprimer du serveur</string>
+ <string name="select_podcasts.downloading">Téléchargement %s sur le serveur</string>
+ <string name="select_podcasts.refreshing">Le serveur recherche les mises à jour de podcasts</string>
+ <string name="select_podcasts.deleted">Podcast supprimé %s</string>
+ <string name="select_podcasts.deleted_error">Erreur lors de la suppression du podcast %s</string>
+ <string name="select_podcasts.add_url">URL :</string>
+ <string name="select_podcasts.created_error">Erreur lors de l\'ajout du podcast</string>
+ <string name="select_podcasts.invalid_podcast_channel">Podcast invalide : %s</string>
+ <string name="select_podcasts.delete">Supprimer podcast</string>
+
+ <string name="download.empty">La playlist est vide</string>
+ <string name="download.shuffle_loading">Chargement en cours liste au hasard...</string>
+ <string name="download.playerstate_downloading">Chargement - %s</string>
+ <string name="download.playerstate_buffering">Mise en mémoire tampon</string>
+ <string name="download.playerstate_playing_shuffle">Lecture au hasard</string>
+ <string name="download.menu_show_album">Afficher l\'album</string>
+ <string name="download.menu_lyrics">Paroles</string>
+ <string name="download.menu_remove">Enlever de la queue</string>
+ <string name="download.menu_remove_all">Enlever tout</string>
+ <string name="download.menu_screen_on">Ecran actif</string>
+ <string name="download.menu_shuffle">Hasard</string>
+ <string name="download.menu_toggle">Basculer</string>
+ <string name="download.menu_save">Enregistrer la playlist</string>
+ <string name="download.menu_shuffle_notification">La playlist a été mélangée</string>
+ <string name="download.menu_remove_played_songs">Supprimer les titres déjà joué</string>
+ <string name="download.playlist_title">Enregistrer la playlist</string>
+ <string name="download.playlist_name">Nom de la playlist :</string>
+ <string name="download.playlist_saving">Enregistrement playlist \&quot;%s\&quot;...</string>
+ <string name="download.playlist_done">La playlist a bien été enregistrée.</string>
+ <string name="download.playlist_error">Erreur à l\'enregistrement de la playlist, réessayer plus tard.</string>
+ <string name="download.repeat_off">Répéter inactif</string>
+ <string name="download.repeat_all">Répéter tout</string>
+ <string name="download.repeat_single">Répéter titre</string>
+ <string name="download.jukebox_on">Télécommande activée. La musique est diffusée sur l\'ordinateur.</string>
+ <string name="download.jukebox_off">Télécommande désactivée. La musique est diffusée sur le mobile.</string>
+ <string name="download.jukebox_volume">Volume distant</string>
+ <string name="download.jukebox_server_too_old">Télécommande non supportée. Mettre à jour le serveur Subsonic.</string>
+ <string name="download.jukebox_offline">La télécommande n\'est pas disponible en mode déconnecté.</string>
+ <string name="download.jukebox_not_authorized">Mode télécommande non autorisée. Activer le mode jukebox.<b>Users &gt; Settings</b> on your Subsonic server.</string>
+ <string name="download.timer_length">Minuteur :</string>
+ <string name="download.start_timer">Démarrer le minuteur</string>
+ <string name="download.stop_timer">Arrêter le minuteur</string>
+ <string name="download.need_download">La vidéo doit d\'abord être téléchargée</string>
+ <string name="download.no_streaming_player">Aucun lecteur ne peut afficher ce flux</string>
+ <string name="download.playing_out_of">Lecture : %1$d/%2$d</string>
+ <string name="download.save_bookmark_title">Créer un favori</string>
+ <string name="download.save_bookmark">Favori créé</string>
+ <string name="download.downloading_title">Chargement des titres %1$d</string>
+ <string name="download.downloading_summary">En cours : %1$s</string>
+ <string name="download.downloading_summary_expanded">En cours : %1$s
+ \nTaille estimée : %2$s</string>
+ <string name="download.failed_to_load">Echec du chargement</string>
+ <string name="download.save_bookmark_failed">Echec de la création du favori</string>
+
+ <string name="sync.new_podcasts">Nouveaux podcasts disponibles</string>
+ <string name="sync.new_playlists">Nouveaux titres dans les playlists</string>
+ <string name="sync.new_albums">Nouveaux albums disponibles</string>
+ <string name="sync.new_starred">Nouveaux titres notés disponibles</string>
+
+ <string name="starring_content_starred">Noté \&quot;%s\&quot;</string>
+ <string name="starring_content_unstarred">Dévalués \&quot;%s\&quot;</string>
+ <string name="starring_content_error">Echec de la mise à jour \&quot;%s\&quot;, Réessayer plus tard.</string>
+
+ <string name="playlist_error">Echec de la récupération des playlists</string>
+ <string name="updated_playlist">Titre %1$s ajouté à \&quot;%2$s\&quot;</string>
+ <string name="updated_playlist_error">Echec de la mise à jour \&quot;%s\&quot;, réessayer plus tard.</string>
+ <string name="removed_playlist">Titre %1$s retiré de \&quot;%2$s\&quot;</string>
+
+ <string name="bookmark.delete">Supprimer le favori</string>
+ <string name="bookmark.delete_title">Dupprimer le favori pour</string>
+ <string name="bookmark.deleted">Favori pour \&quot;%s\&quot; supprimé</string>
+ <string name="bookmark.deleted_error">Echec de la suppression du favori pour \&quot;%s\&quot;</string>
+ <string name="bookmark.details_title">Détails du favori</string>
+ <string name="bookmark.details">Titre : %1$s
+ \nPosition : %2$s
+ \nCréé le : %3$s
+ \nMis à jour : %4$s
+ \nCommentaire : %5$s</string>
+ <string name="bookmark.resume_title">Reprendre la lecture ?</string>
+ <string name="bookmark.resume">Reprendre la lecture de \'%1$s\' depuis %2$s</string>
+ <string name="bookmark.action_resume">Reprendre</string>
+ <string name="bookmark.action_start_over">Start Over</string>
+
+ <string name="rating.title">Noter \"%s\"</string>
+ <string name="rating.set_rating">Note attribuée à \"%s\"</string>
+ <string name="rating.set_rating_failed">Echec de l\'attribution de la note à \"%s\"</string>
+ <string name="rating.remove_rating">Note supprimée pour \"%s\"</string>
+ <string name="rating.remove_rating_failed">Echec de la suppression de la note pour \"%s\"</string>
+
+ <string name="song_details.error">Erreur</string>
+ <string name="song_details.skipped">Ignoré</string>
+ <string name="song_details.downloading">Chargement</string>
+
+ <string name="lyrics.nomatch">Aucune paroles trouvées</string>
+
+ <string name="error.label">Erreur</string>
+
+ <string name="settings.title">Paramètres DSub</string>
+ <string name="settings.test_connection_title">Test de connexion</string>
+ <string name="settings.servers_add">Ajouter un serveur</string>
+ <string name="settings.servers_remove">Supprimer le serveur</string>
+ <string name="settings.servers_title">Serveurs</string>
+ <string name="settings.server_unused">Inutilisé</string>
+ <string name="settings.server_name">Nom</string>
+ <string name="settings.server_address">Adresse du serveur</string>
+ <string name="settings.server_local_network_ssid" >SSID du réseau local</string>
+ <string name="settings.server_local_network_ssid_hint">SSID Actuel : %s</string>
+ <string name="settings.server_internal_address">Adresse sur le réseau local</string>
+ <string name="settings.server_username">Nom d\'utilisateur</string>
+ <string name="settings.server_password">Mot de passe</string>
+ <string name="settings.server_open_browser">Ouvrir dans le navigateur</string>
+ <string name="settings.cache_title">Mémoire tampon pour la musique</string>
+ <string name="settings.preload_wifi">Titres à précharger (Wifi)</string>
+ <string name="settings.preload_mobile">Titre à précharger (Mobile)</string>
+ <string name="settings.cache_size">Taille de la mémoire tampon (MB)</string>
+ <string name="settings.cache_location">Emplacement de la mémoire tampon</string>
+ <string name="settings.cache_location_error">Emplacement invalide pour la mémoire tampon. Utilisation emplacement par défaut.</string>
+ <string name="settings.cache_location_reset">L\'emplacement choisi pour la mémoire tampon ne peut plus être utilisé. S\'il y a eu une mise à jour récente vers Android 4.4 KiktKat, la façon dont les application écrivent sur la carte SD à changée afin que ces dernières ne puissent écrire que dans un emplacement spécifique. L\'emplacement à utiliser pour DSub a déjà été mis à jour. Afin de supprimer toutes les données obsolètes, il faut vider l\'ancien emplacement en utilisant un ordinateur.</string>
+ <string name="settings.cache_clear">Vider la mémoire tampon</string>
+ <string name="settings.cache_clear_complete">Mémoire tampon vidée</string>
+ <string name="settings.testing_connection">Test de connexion...</string>
+ <string name="settings.testing_ok">Connexion OK</string>
+ <string name="settings.testing_unlicensed">Connexion OK. Pas de licence.</string>
+ <string name="settings.connection_failure">Echec de la connexion.</string>
+ <string name="settings.invalid_url">Saisir une URL valide.</string>
+ <string name="settings.invalid_username">Saisir un nom d\'utilisateur valide (espaces interdits).</string>
+ <string name="settings.appearance_title">Apparence</string>
+ <string name="settings.theme_title">Thème</string>
+ <string name="settings.theme_light">Light</string>
+ <string name="settings.theme_dark">Dark</string>
+ <string name="settings.theme_black">Black</string>
+ <string name="settings.theme_holo">Holo</string>
+ <string name="settings.theme_fullscreen">Plein écran</string>
+ <string name="settings.theme_fullscreen_summary">Cacher autant d\'élément graphique que possible</string>
+ <string name="settings.track_title">Afficher n° piste</string>
+ <string name="settings.track_summary">Afficher le n° de piste devant les titres</string>
+ <string name="settings.custom_sort">Trier par années</string>
+ <string name="settings.custom_sort_summary">Trier les albums par année, ou par ordre alphabétique</string>
+ <string name="settings.network_title">Réseau</string>
+ <string name="settings.max_bitrate_wifi">Débit audio max (Wifi)</string>
+ <string name="settings.max_bitrate_mobile">Débit audio max (Mobile)</string>
+ <string name="settings.max_bitrate_32">32 Kbps</string>
+ <string name="settings.max_bitrate_64">64 Kbps</string>
+ <string name="settings.max_bitrate_80">80 Kbps</string>
+ <string name="settings.max_bitrate_96">96 Kbps</string>
+ <string name="settings.max_bitrate_112">112 Kbps</string>
+ <string name="settings.max_bitrate_128">128 Kbps</string>
+ <string name="settings.max_bitrate_160">160 Kbps</string>
+ <string name="settings.max_bitrate_192">192 Kbps</string>
+ <string name="settings.max_bitrate_256">256 Kbps</string>
+ <string name="settings.max_bitrate_320">320 Kbps</string>
+ <string name="settings.max_video_bitrate_wifi">Débit vidéo max (Wifi)</string>
+ <string name="settings.max_video_bitrate_mobile">Débit vidéo max (Mobile)</string>
+ <string name="settings.max_video_bitrate_200">200 Kbps</string>
+ <string name="settings.max_video_bitrate_300">300 Kbps</string>
+ <string name="settings.max_video_bitrate_400">400 Kbps</string>
+ <string name="settings.max_video_bitrate_500">500 Kbps</string>
+ <string name="settings.max_video_bitrate_700">700 Kbps</string>
+ <string name="settings.max_video_bitrate_1000">1000 Kbps</string>
+ <string name="settings.max_video_bitrate_1500">1500 Kbps</string>
+ <string name="settings.max_video_bitrate_2000">2000 Kbps</string>
+ <string name="settings.max_video_bitrate_3000">3000 Kbps</string>
+ <string name="settings.max_video_bitrate_5000">5000 Kbps</string>
+ <string name="settings.max_bitrate_unlimited">Illimité</string>
+ <string name="settings.wifi_required_title">Streaming en Wifi uniquement</string>
+ <string name="settings.wifi_required_summary">Ne lire les média qu\'avec une connexion Wifi</string>
+ <string name="settings.network_timeout_title">Délai d\'attente réseau (timeout)</string>
+ <string name="settings.network_timeout_10000">10 secondes</string>
+ <string name="settings.network_timeout_15000">15 secondes</string>
+ <string name="settings.network_timeout_30000">30 secondes</string>
+ <string name="settings.network_timeout_45000">45 secondes</string>
+ <string name="settings.network_timeout_60000">60 secondes</string>
+ <string name="settings.preload_0">0 titre</string>
+ <string name="settings.preload_1">1 titre</string>
+ <string name="settings.preload_2">2 titres</string>
+ <string name="settings.preload_3">3 titres</string>
+ <string name="settings.preload_5">5 titres</string>
+ <string name="settings.preload_10">10 titres</string>
+ <string name="settings.preload_unlimited">Illimité</string>
+ <string name="settings.clear_search_history">Effacer l\'historique de recherche</string>
+ <string name="settings.search_history_cleared">Rechercher dans l\'historique effacé</string>
+ <string name="settings.other_title">Autres paramètres</string>
+ <string name="settings.scrobble_title">Publier vers Last.fm</string>
+ <string name="settings.scrobble_summary">Penser à paramétrer votre compte Last.fm sur le serveur Subsonic</string>
+ <string name="settings.hide_media_title">Invisible pour les autres</string>
+ <string name="settings.hide_media_summary">Rendre les fichiers de musique indisponibles pour les autres applications.</string>
+ <string name="settings.hide_media_toast">Prendra effet au prochain scan de musique d\'Android.</string>
+ <string name="settings.media_button_title">Boutons physique</string>
+ <string name="settings.media_button_summary">Répondre aux boutons du mobile, du casque filaire ou bluetooth</string>
+ <string name="settings.screen_lit_title">Garder l\'écran allumé</string>
+ <string name="settings.screen_lit_summary">Garder l\'écran allumé augmente la vitesse de téléchargement.</string>
+ <string name="settings.playlist_title">Lecture</string>
+ <string name="settings.playlist_random_size_title">Taille de la liste de lecture aléatoire</string>
+ <string name="settings.sleep_timer_title">Temporisateur</string>
+ <string name="settings.sleep_timer_duration_title">Durée temporisation</string>
+ <string name="settings.sleep_timer_off">Eteindre</string>
+ <string name="settings.sleep_timer_on">Allumer</string>
+ <string name="settings.sleep_timer_always_on">Toujours en fonctionnement</string>
+ <string name="settings.temp_loss_title">Perte temporaire de focus</string>
+ <string name="settings.temp_loss_pause">Toujours mettre en pause</string>
+ <string name="settings.temp_loss_pause_lower">Pause, baisser le volume si demandé</string>
+ <string name="settings.temp_loss_lower">Toujours baisser le volume</string>
+ <string name="settings.temp_loss_nothing">Ne rien faire</string>
+ <string name="settings.disconnect_pause_title">Pause à la déconnexion</string>
+ <string name="settings.disconnect_pause_both">Pause</string>
+ <string name="settings.disconnect_pause_neither">Ne rien faire</string>
+ <string name="settings.persistent_title">Notification persistente</string>
+ <string name="settings.persistent_summary">Afficher la notification même après la mise en pause. Appuyer sur stop pour l\'effacer.</string>
+ <string name="settings.gapless_playback">Lecture sans saut</string>
+ <string name="settings.gapless_playback_summary">Si vous rencontrez des problèmes lors de la lecture, désactiver ceci pourrait aider.</string>
+ <string name="settings.chat_refresh">Fréquence de rafraîchissement du chat (Secs)</string>
+ <string name="settings.chat_enabled">Chat autorisé</string>
+ <string name="settings.chat_enabled_summary">Afficher ou non la zone de chat</string>
+ <string name="settings.video_title">Video</string>
+ <string name="settings.video_player">Lecteur vidéo</string>
+ <string name="settings.video_raw">Raw (Necessite Subsonic 4.8+)</string>
+ <string name="settings.video_hls">HTTP Live Stream (HLS) (Necessite Subsonic 4.8+)</string>
+ <string name="settings.video_transcode">Direct Transcode (Necessite video -&gt; mp4 ou paramétrage identique sur le serveur)</string>
+ <string name="settings.video_flash">Flash Necessite le Plugin)</string>
+ <string name="settings.cache_screen_title">Tampon/Réseau</string>
+ <string name="settings.playback_title">Lecture</string>
+ <string name="settings.hide_widget_title">Cacher le Widget</string>
+ <string name="settings.hide_widget_summary">Cacher le widget après avoir quitté l\'application</string>
+ <string name="settings.podcasts_enabled">Podcasts autorisés</string>
+ <string name="settings.podcasts_enabled_summary">Afficher ou non l\'accès aux podcasts</string>
+ <string name="settings.bookmarks_enabled">Favoris autorisés</string>
+ <string name="settings.bookmarks_enabled_summary">Afficher ou non l\'accès aux favoris</string>
+ <string name="settings.shares_enabled">Partages autorisés</string>
+ <string name="settings.shares_enabled_summary">Afficher ou non l\'accès aux partages</string>
+ <string name="settings.sync_title">Sync</string>
+ <string name="settings.sync_enabled">Sync autorisée</string>
+ <string name="settings.sync_enabled_summary">Controler ou non périodiquement si les playlists et podcasts ont été mis à jour</string>
+ <string name="settings.sync_interval">Délai de synchro</string>
+ <string name="settings.sync_interval_15">15 Minutes</string>
+ <string name="settings.sync_interval_30">30 Minutes</string>
+ <string name="settings.sync_interval_60">1 Heure</string>
+ <string name="settings.sync_interval_120">2 Heures</string>
+ <string name="settings.sync_interval_240">4 Heures</string>
+ <string name="settings.sync_interval_360">6 Heures</string>
+ <string name="settings.sync_interval_720">12 Heures</string>
+ <string name="settings.sync_interval_1440">Quotidiennement</string>
+ <string name="settings.sync_wifi">Sync en Wifi uniquement</string>
+ <string name="settings.sync_wifi_summary">Synchroniser uniquement quand la connexion Wifi est active</string>
+ <string name="settings.sync_most_recent">Sync ajouts récents</string>
+ <string name="settings.sync_most_recent_summary">Charger automatiquement les nouveaux albums</string>
+ <string name="settings.sync_starred">Sync notés</string>
+ <string name="settings.sync_starred_summary">Charger automatiquement les titres, albums et artistes bien notés</string>
+ <string name="settings.sync_notification">Afficher des notifications de synchro</string>
+ <string name="settings.sync_notification_summary">Afficher une notification dès qu\'un média a été synchronisé</string>
+ <string name="settings.server_sync_summary">Synchron autorisée ou non sur ce serveur</string>
+ <string name="settings.server_sync">Synchro autorisée</string>
+ <string name="settings.menu_options.title">Options de menu optionnelles</string>
+ <string name="settings.menu_options.play_next_summary">Afficher Lire suivant dans les menus</string>
+ <string name="settings.menu_options.play_last_summary">Afficher Lire dernier dans les menus</string>
+ <string name="settings.menu_options.star_summary">Afficher Noter dans les menus</string>
+ <string name="settings.menu_options.shared_summary">Afficher Partager dans les menus</string>
+ <string name="settings.menu_options.rate_summary">Montrer les notes dans les menus</string>
+ <string name="settings.browse_by_tags">Naviguer via les tags</string>
+ <string name="settings.browse_by_tags_summary">Naviguer via les tags plutôt que via l\'arborescence de fichier. Nécessite Subsonic 4.7+</string>
+ <string name="settings.override_system_language">Ne pas utiliser la langue du système</string>
+ <string name="settings.override_system_language_summary">Afficher DSub en Anglais même si une traduction existe pour la langue système. Peut nécessiter un vidage du cache de l\'application.</string>
+ <string name="settings.drawer_items_title">Entrées de menu</string>
+ <string name="settings.play_now_after">Jouer maintenant - Plus tard</string>
+ <string name="settings.play_now_after_summary">Play Now context menu for a song plays everything after selected item (like the Subsonic web GUI)</string>
+ <string name="settings.large_album_art">Pochettes larges</string>
+ <string name="settings.large_album_art_summary">Afficher les pochettes en grand plutôt qu\'en liste.</string>
+ <string name="settings.admin_enabled">Administration</string>
+ <string name="settings.admin_enabled_summary">Afficher ou non l\'accès aux outils d\'administration</string>
+
+ <string name="shuffle.title">Shuffle By</string>
+ <string name="shuffle.startYear">Année début :</string>
+ <string name="shuffle.endYear">Année fin :</string>
+ <string name="shuffle.genre">Genre :</string>
+ <string name="shuffle.pick_genre">Choisir un genre</string>
+
+ <string name="share.info">Propriétaire : %1$s
+ \nDescription: %2$s
+ \nURL: %3$s
+ \nCréation : %4$s
+ \nDernière visite : %5$s
+ \nExpiration : %6$s
+ \nNombre de visites : %7$s
+
+ </string>
+ <string name="share.expires">Expiration : %s</string>
+ <string name="share.expires_never">N\'expire jamais</string>
+ <string name="share.deleted">Supprimer le partage %s</string>
+ <string name="share.deleted_error">Echec de la suppression du partage %s</string>
+ <string name="share.no_expiration">Pas d\'expiration</string>
+ <string name="share.expiration">Expiration :</string>
+ <string name="share.updated_info">Informations de partage mises à jour pour %s</string>
+ <string name="share.updated_info_error">Echec de la mise à jour des informations de partage pour %s</string>
+ <string name="share.via">Partager via</string>
+ <string name="share.delete">Supprimer le partage</string>
+
+ <string name="admin.add_user_username">Nom d\'utilisateur :</string>
+ <string name="admin.add_user_email">Email :</string>
+ <string name="admin.add_user_password">Mot de passe :</string>
+ <string name="admin.create_user_success">Nouvel utilisateur créé</string>
+ <string name="admin.create_user_error">Erreur à la création de l\'utilisateur</string>
+ <string name="admin.change_username_invalid">Saisir un nom d\'utilisateur valide</string>
+ <string name="admin.update_permissions">Mettre à jour les autorisations</string>
+ <string name="admin.update_permissions_success">Autorisation mises à jour pour %1$s</string>
+ <string name="admin.update_permissions_error">Echec lors de lamise à jour des autorisations de %1$s</string>
+ <string name="admin.change_email">Modifier Email</string>
+ <string name="admin.change_email_success">Email remplacé pour %1$s</string>
+ <string name="admin.change_email_error">Echec lors du remplacement de l\'Email de %1$s</string>
+ <string name="admin.change_email_label">Nouvel Email :</string>
+ <string name="admin.change_email_invalid">Saisir un Email valide</string>
+ <string name="admin.change_password">Modifier le mot de passe</string>
+ <string name="admin.change_password_success">Mot de passe modifié pour %1$s</string>
+ <string name="admin.change_password_error">Echec du remplacement du mot de passe pour %1$s</string>
+ <string name="admin.change_password_label">Nouveau mot de passe :</string>
+ <string name="admin.change_password_invalid">Saisir un mot de passe valide</string>
+ <string name="admin.delete_user">Supprimer l\'utilisateur</string>
+ <string name="admin.delete_user_success">Suppression effectuée %1$s</string>
+ <string name="admin.delete_user_error">Echec de la suppression %1$s</string>
+ <string name="admin.confirm_password">Confirmer le mot de passe</string>
+ <string name="admin.confirm_password_bad">Mot de passe saisi erroné</string>
+
+ <string name="admin.scrobblingEnabled">Diffusion autorisée</string>
+ <string name="admin.role.admin">Administrateur</string>
+ <string name="admin.role.settings">Modifier les paramètres</string>
+ <string name="admin.role.download">Télécharger les fichiers</string>
+ <string name="admin.role.upload">Téléverser sur le serveur</string>
+ <string name="admin.role.coverArt">Modifier les pochettes</string>
+ <string name="admin.role.comment">Ajouter des commentaires</string>
+ <string name="admin.role.podcast">Gérer les podcasts</string>
+ <string name="admin.role.stream">Ecouter de la musique</string>
+ <string name="admin.role.jukebox">Télécommander la lecture (jukebox)</string>
+ <string name="admin.role.share">Gérer les partages</string>
+ <string name="admin.role.lastfm">Utiliser Last.FM</string>
+
+ <string name="music_service.retry">Erreur réseau. Nouvelle tentative %1$d de %2$d.</string>
+
+ <string name="background_task.wait">Patienter...</string>
+ <string name="background_task.loading">Chargement.</string>
+ <string name="background_task.no_network">Cette application nécessite un accès réseau. Activer les connexion Wifi ou mobile.</string>
+ <string name="background_task.network_error">Une erreur réseau est survenue. Merci de vérifier l\'adresse du serveur ou réessayer plus tard.</string>
+ <string name="background_task.not_found">Ressource non trouvée. Vérifier l\'adresse du serveur.</string>
+ <string name="background_task.parse_error">Erreur de communication avec le serveur.Vérifier l\'adresse du serveur et que la connexion via un navigateur fonctionne.</string>
+
+ <string name="service.connecting">Interrogation du serveur, veuillez patienter.</string>
+
+ <string name="parser.upgrade_client"> Versions incompatible. Mettre à jour DSub.</string>
+ <string name="parser.upgrade_server">Versions incompatibles. Mettre à jour le serveur Subsonic.</string>
+ <string name="parser.not_authenticated">Mauvais nom d\'utilisateur ou mot de passe.</string>
+ <string name="parser.not_authorized">Non autorisé. Vérifier les droit de l\'utilisateur sur le serveur Subsonic.</string>
+ <string name="parser.artist_count">%d artistes récupérés.</string>
+ <string name="parser.server_error">Erreur serveur : %s</string>
+ <string name="parser.scan_count">%d entrées trouvées</string>
+
+ <string name="select_artist.refresh">Recharger</string>
+ <string name="select_artist.folder">Sélectionner un dossier</string>
+ <string name="select_artist.all_folders">Tous les dossier</string>
+
+ <string name="equalizer.label">Equaliseur</string>
+ <string name="equalizer.enabled">Activé</string>
+ <string name="equalizer.preset">Selectioner un préréglage</string>
+ <string name="equalizer.bass_booster">Bass Booster</string>
+ <string name="equalizer.voice_booster">Voice Booster</string>
+ <string name="equalizer.db_size">%d dB</string>
+ <string name="equalizer.bass_size">%d mille</string>
+
+ <string name="widget.4x1">DSub (4x1)</string>
+ <string name="widget.4x2">DSub (4x2)</string>
+ <string name="widget.4x3">DSub (4x3)</string>
+ <string name="widget.4x4">DSub (4x4)</string>
+ <string name="widget.initial_text">Toucher pour choisir de la musique</string>
+ <string name="widget.sdcard_busy">Carte SD indisponible</string>
+ <string name="widget.sdcard_missing">Pas de carte SD</string>
+
+ <string name="util.bytes_format.gigabyte">0.00 GB</string>
+ <string name="util.bytes_format.megabyte">0.00 MB</string>
+ <string name="util.bytes_format.kilobyte">0 KB</string>
+ <string name="util.bytes_format.byte">0 B</string>
+
+ <string name="changelog_full_title">Change Log</string>
+ <string name="changelog_title">What\'s New</string>
+ <string name="changelog_ok_button">OK</string>
+ <string name="changelog_show_full">Plus…</string>
+
+ <string name="chat.send_a_message">Envoyer un message</string>
+
+ <string name="changelog_version_format" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">Version <xliff:g id="version_name">%s</xliff:g></string>
+
+ <string name="tasker.start_playing">Commencer la lecture</string>
+ <string name="tasker.start_playing_title">Tasker -> Démarrer DSub</string>
+ <string name="tasker.edit_shuffle_mode">Démarrer en mode lecture aléatoire : </string>
+ <string name="tasker.start_playing_shuffled">Démarrer la lecture en mode lecture aléatoire</string>
+
+ <plurals name="select_album_n_songs">
+ <item quantity="zero">Aucun titre</item>
+ <item quantity="one">Un titre</item>
+ <item quantity="other">%d titres</item>
+ </plurals>
+ <plurals name="select_album_n_songs_downloading">
+ <item quantity="one">Téléchargement programmé pour un titre.</item>
+ <item quantity="other">Téléchargement programmé pour %d titres.</item>
+ </plurals>
+ <plurals name="select_album_n_songs_added">
+ <item quantity="one">Un titre a été ajouté à la liste de lecture en cours.</item>
+ <item quantity="other">%d titres ont été ajoutés à la liste de lecture en cours.</item>
+ </plurals>
+ <plurals name="select_album_donate_dialog_n_trial_days_left">
+ <item quantity="one">Dernier jour avant la fin de période d\'essai.</item>
+ <item quantity="other">%d jours avant la fin de période d\'essai.</item>
+ </plurals>
+
+</resources>
diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml
new file mode 100644
index 00000000..6222989a
--- /dev/null
+++ b/app/src/main/res/values-hu/strings.xml
@@ -0,0 +1,602 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="common.appname">DSub</string>
+ <string name="common.ok">OK</string>
+ <string name="common.save">Mentés</string>
+ <string name="common.cancel">Mégse</string>
+ <string name="common.play_now">Lejátszás</string>
+ <string name="common.play_shuffled">Lejátszás kevert sorrendben</string>
+ <string name="common.play_next">Sorbaállítás következőnek</string>
+ <string name="common.play_last">Sorbaállítás utolsónak</string>
+ <string name="common.download">Letöltés gyorsítótárba</string>
+ <string name="common.pin">Letöltés tárolásra (megőrzés)</string>
+ <string name="common.delete">Törlés</string>
+ <string name="common.star">Csillagozás</string>
+ <string name="common.unstar">Csillagozás ki</string>
+ <string name="common.info">Részletek</string>
+ <string name="common.name">Név</string>
+ <string name="common.comment">Megjegyzés</string>
+ <string name="common.public">Nyilvános</string>
+ <string name="common.play_external">Videó lejátszása</string>
+ <string name="common.stream_external">Videó streamelése</string>
+ <string name="common.confirm">Jóváhagyás</string>
+ <string name="common.confirm_message">Biztos benne? %1$s -> \"%2$s\"</string>
+ <string name="common.confirm_message_cache">cache</string>
+ <string name="common.empty">Nem található!</string>
+ <string name="common.warning">Figyelem!</string>
+
+ <string name="button_bar.home">Főoldal</string>
+ <string name="button_bar.browse">Médiatár</string>
+ <string name="button_bar.search">Keresés</string>
+ <string name="button_bar.playlists">Lejátszási listák</string>
+ <string name="button_bar.now_playing">Várólista</string>
+ <string name="button_bar.podcasts">Podcastok</string>
+ <string name="button_bar.bookmarks">Könyvjelzők</string>
+ <string name="button_bar.shares">Megosztások</string>
+ <string name="button_bar.chat">Csevegés (Chat)</string>
+ <string name="button_bar.admin">Admin</string>
+ <string name="button_bar.downloading">Letöltések</string>
+
+ <string name="main.welcome_title">Üdvözlet!</string>
+ <string name="main.welcome_text">Üdvözli a DSub! Az alkalmazás még nincs beállítva. Miután konfigurálta saját kiszolgálóját
+ (elérhető: <b>subsonic.org</b>), húzza balról jobbra az oldalsávot, lépjen be a <b>Beállítások</b> menüpontba és adja meg a kapcsolódási adatokat!</string>
+ <string name="main.about_title">DSub információk</string>
+ <string name="main.about_text">Fejlesztő: Scott Jackson
+ \nEmail: dsub.android@gmail.com
+ \nVerzió: %1$s
+ \nGyorsítótárazott fájlok: %2$s
+ \nFelhasznált tároló: %3$s/%4$s
+ \nFelhasználható tároló: %5$s/%6$s</string>
+ <string name="main.faq_title">GYIK</string>
+ <string name="main.faq_text">
+ <![CDATA[
+ <font color="red">Gyorsítótárazás vs Tárolás</font>:
+ <br/>Míg a normál módon gyorsítótárazott dalok törlődhetnek amikor újak kerülnek letöltésre, addig a \"Letöltés tárolásra (megőrzés)\" menüpont segítségével letöltött dalok soha nem törlődnek automatikusan.
+ <p/><font color="red">Ha a ChromeCast sikertelen</font>:
+ <br/>Próbálja meg bejelölni: Beállítások -> Lejátszás -> Eszköz használata proxyként. Ez egy kerülő megoldás arra, ha a ChromeCast elutasítja a saját aláírású tanúsítványt.
+ <p/><font color="red">A Médiatár első szintje tulajdonképpen az előadók csoportja</font>:
+ <br/>A Beállítások menüben törölje az "Előadók első szintje" jelölést. Ez teszi lehetővé, hogy a mappák teljes első szintjének megjelenítése előadói csoportonként és ne előadónként legyen kezelve.
+ ]]>
+ </string>
+ <string name="main.select_server">Kiszolgáló kiválasztása</string>
+ <string name="main.shuffle">Lejátszás kevert sorrendben</string>
+ <string name="main.offline">Offline mód</string>
+ <string name="main.online">Online mód</string>
+ <string name="main.settings">Beállítások</string>
+ <string name="main.albums_title">Albumok</string>
+ <string name="main.albums_per_folder">Mappánként</string>
+ <string name="main.albums_newest">Utoljára hozzáadottak</string>
+ <string name="main.albums_recent">Utoljára lejátszottak</string>
+ <string name="main.albums_frequent">Legtöbbször lejátszottak</string>
+ <string name="main.albums_highest">Legjobbra értékeltek</string>
+ <string name="main.albums_starred">Csillagozottak</string>
+ <string name="main.albums_random">Véletlenszerű kiválasztás</string>
+ <string name="main.albums_genres">Műfajok</string>
+ <string name="main.albums_year">Évtizedek</string>
+ <string name="main.albums_alphabetical">Betűrendben</string>
+ <string name="main.videos">Videók</string>
+ <string name="main.songs_genres">@string/main.albums_genres</string>
+ <string name="main.back_confirm">Nyomja meg még egyszer a kilépéshez!</string>
+ <string name="main.scan_complete">A médiatár frissítése befejeződött a kiszolgálón!</string>
+
+ <string name="menu.search">Keresés</string>
+ <string name="menu.shuffle">Lejátszás kevert sorrendben</string>
+ <string name="menu.refresh">Frissítés</string>
+ <string name="menu.play">Lejátszás</string>
+ <string name="menu.play_last">Sorbaállítás utolsónak</string>
+ <string name="menu.exit">Kilépés</string>
+ <string name="menu.settings">Beállítások</string>
+ <string name="menu.help">Súgó</string>
+ <string name="menu.about">Névjegy</string>
+ <string name="menu.add_playlist">Hozzáadás lejátszási listához</string>
+ <string name="menu.remove_playlist">Eltávolítás a lejátszási listából</string>
+ <string name="menu.deleted_playlist">\"%s\" lejátszási lista törölve</string>
+ <string name="menu.deleted_playlist_error">\"%s\" lejátszási lista törlése sikertelen!</string>
+ <string name="menu.log">Log küldése</string>
+ <string name="menu.set_timer">Időzítő beállítása</string>
+ <string name="menu.check_podcasts">Új epizódok ellenőrzése</string>
+ <string name="menu.add_podcast">Csatorna hozzáadása</string>
+ <string name="menu.keep_synced">Tartsa szinkronizálva</string>
+ <string name="menu.stop_sync">Szinkronizálás megállítása</string>
+ <string name="menu.show_all">Összes média megjelenítése</string>
+ <string name="menu.show_artist">Ugrás az előadóhoz</string>
+ <string name="menu.share">Megosztás</string>
+ <string name="menu.delete_cache">Gyorsítótár törlése</string>
+ <string name="menu.cast">Továbbítás eszközhöz</string>
+ <string name="menu.faq">FAQ</string>
+ <string name="menu.add_user">Felhasználó hozzáadása</string>
+ <string name="menu.rescan">Médiatár frissítése a kiszolgálón</string>
+ <string name="menu.rate">Értékelés</string>
+ <string name="menu.top_tracks">Last.fm legjobb dalok</string>
+ <string name="menu.similar_artists">Hasonló előadók</string>
+ <string name="menu.show_missing">Hiányzó megjelenítése</string>
+ <string name="menu.start_radio">Rádió indítása</string>
+ <string name="menu.first_level_artist">Előadók első szintje</string>
+
+ <string name="playlist.label">Lejátszási listák</string>
+ <string name="playlist.update_info">Szerkesztés</string>
+ <string name="playlist.updated_info">\"%s\" lejátszási lista módosítva</string>
+ <string name="playlist.updated_info_error">\"%s\" lejátszási lista módosítása sikertelen!</string>
+ <string name="playlist.overwrite">Létező lejátszási lista felülírása</string>
+ <string name="playlist.add_to">Hozzáadás lejátszási listához</string>
+ <string name="playlist.create_new">Új lejátszási lista</string>
+ <string name="playlist.delete">Lejátszási lista törlése</string>
+
+ <string name="search.label">Keresés</string>
+ <string name="search.title">Keresés</string>
+ <string name="search.search">Érintse meg a kereséshez</string>
+ <string name="search.no_match">Nincs találat, próbálja újra!</string>
+ <string name="search.artists">Előadók</string>
+ <string name="search.albums">Albumok</string>
+ <string name="search.songs">Dalok</string>
+ <string name="search.more">Továbbiak</string>
+
+ <string name="progress.wait">Kérem várjon...</string>
+
+ <string name="music_library.label">Médiatár</string>
+ <string name="music_library.label_offline">Kapcsolat nélküli médiák</string>
+
+ <string name="select_album.select">Összes jelölése be/ki</string>
+ <string name="select_album.n_selected">%d dal kijelölve.</string>
+ <string name="select_album.n_unselected">%d dal visszavonva.</string>
+ <string name="select_album.more">Továbbiak</string>
+ <string name="select_album.offline">Offline</string>
+ <string name="select_album.searching">Keresés...</string>
+ <string name="select_album.no_sdcard">Hiba: SD kártya nem áll rendelkezésre!</string>
+ <string name="select_album.no_network">Figyelem: Hálózat nem áll rendelkezésre!</string>
+ <string name="select_album.no_room">Figyelem: Már csak %s hely áll rendelkezésre!</string>
+ <string name="select_album.not_licensed">A kiszolgálónak nincs licence! %d próbanap van hátra!</string>
+ <string name="select_album.donate_dialog_message">Korlátlan letöltéshez juthat a Subsonic támogatásával!</string>
+ <string name="select_album.donate_dialog_now">Most</string>
+ <string name="select_album.donate_dialog_later">Később</string>
+ <string name="select_album.donate_dialog_0_trial_days_left">A próbaidőszak lejárt!</string>
+
+ <string name="offline.sync_dialog_title">Offline dalok várnak a szinkronizálás befejezésére</string>
+ <string name="offline.sync_dialog_message">%1$d scrobble folyamat offline módban?
+ \n%2$d csillagozás folyamat offline módban?
+ </string>
+ <string name="offline.sync_dialog_default">A művelet használata alapértelmezettként</string>
+ <string name="offline.sync_success">%1$d dal sikeresen szinkronizálva</string>
+ <string name="offline.sync_partial">%1$d/%2$d dal sikeresen szinkronizálva</string>
+ <string name="offline.sync_error">A dalok szinkronizálása sikertelen!</string>
+
+ <string name="select_genre.blank">Üres</string>
+ <string name="select_genre.songs">%d dal</string>
+ <string name="select_genre.albums">%d album</string>
+
+ <string name="select_podcasts.error">A podcast hibát jelzett a kiszolgálóra történő letöltés közben! A kiszolgálónak kell letöltenie először!</string>
+ <string name="select_podcasts.skipped">Ez a podcast nem lett letöltve a kiszolgálóra! A kiszolgálónak kell letöltenie először!</string>
+ <string name="select_podcasts.initializing">A podcast csatorna inicializálása a kiszolgálón. Kérjük, töltse be újra néhány pillanat múlva!</string>
+ <string name="select_podcasts.server_download">Letöltés a kiszolgálóra</string>
+ <string name="select_podcasts.server_delete">Törlés a kiszolgálóról</string>
+ <string name="select_podcasts.downloading">\"%s\" letöltése a kiszolgálóra</string>
+ <string name="select_podcasts.refreshing">A kiszolgáló ellenőrzi az új podcastokat...</string>
+ <string name="select_podcasts.deleted">\"%s\" podcast törölve</string>
+ <string name="select_podcasts.deleted_error">\"%s\" podcast törlése sikertelen!</string>
+ <string name="select_podcasts.add_url">URL:</string>
+ <string name="select_podcasts.created_error">Podcast hozzáadása sikertelen!</string>
+ <string name="select_podcasts.invalid_podcast_channel">Érvénytelen podcast csatorna: \"%s\"</string>
+ <string name="select_podcasts.delete">Podcast törlése</string>
+
+ <string name="download.empty">A várólista üres!</string>
+ <string name="download.shuffle_loading">Kevert sorrendű lista betöltése...</string>
+ <string name="download.playerstate_downloading">Letöltés - \"%s\"</string>
+ <string name="download.playerstate_buffering">Pufferelés</string>
+ <string name="download.playerstate_playing_shuffle">Sorrend keverése</string>
+ <string name="download.menu_show_album">Ugrás az albumhoz</string>
+ <string name="download.menu_lyrics">Dalszöveg</string>
+ <string name="download.menu_remove">Eltávolítás a várólistáról</string>
+ <string name="download.menu_remove_all">Összes eltávolítása</string>
+ <string name="download.menu_screen_on">Kijelző be</string>
+ <string name="download.menu_shuffle">Sorrend keverése</string>
+ <string name="download.menu_toggle">Váltás</string>
+ <string name="download.menu_save">Mentés lejátszási listába</string>
+ <string name="download.menu_shuffle_notification">Lejátszás kevert sorrendben</string>
+ <string name="download.menu_remove_played_songs">Lejátszottak eltávolítása</string>
+ <string name="download.playlist_title">Mentés lejátszási listába</string>
+ <string name="download.playlist_name">Lejátszási lista neve:</string>
+ <string name="download.playlist_saving">\"%s\" lejátszási lista mentése...</string>
+ <string name="download.playlist_done">Lejátszási lista mentése sikeres</string>
+ <string name="download.playlist_error">Lejátszási lista mentése sikertelen, próbálja később!</string>
+ <string name="download.repeat_off">Ismétlés ki</string>
+ <string name="download.repeat_all">Összes ismétlése</string>
+ <string name="download.repeat_single">Dal ismétlése</string>
+ <string name="download.jukebox_on">Távvezérlés bekapcsolása. A zenelejátszás a számítógépen történik.</string>
+ <string name="download.jukebox_off">Távvezérlés kikapcsolása. A zenelejátszás az eszközön történik.</string>
+ <string name="download.jukebox_volume">Hangerő távvezérlése</string>
+ <string name="download.jukebox_server_too_old">A távvezérlés nem támogatott. Kérjük, frissítse a Subsonic kiszolgálót!</string>
+ <string name="download.jukebox_offline">A távvezérlés nem lehetséges offline módban!</string>
+ <string name="download.jukebox_not_authorized">A távvezérlés nem lehetséges! Engedélyezze a Jukebox módot a <b>Users &gt; Settings</b> menüben a Subsonic kiszolgálón!</string>
+ <string name="download.timer_length">Időhossz:</string>
+ <string name="download.start_timer">Időzítő indítása</string>
+ <string name="download.stop_timer">Időzítő megállítása</string>
+ <string name="download.need_download">A videót először le kell tölteni!</string>
+ <string name="download.no_streaming_player">Nincs megfelelő lejátszó a stream megjelenítéséhez!</string>
+ <string name="download.playing_out_of">Lejátszás: %1$d/%2$d</string>
+ <string name="download.save_bookmark_title">Könyvjelző létrehozása</string>
+ <string name="download.save_bookmark">Könyvjelző létrehozva</string>
+ <string name="download.save_bookmark_failed">Könyvjelző létrehozása sikertelen!</string>
+ <string name="download.downloading_title">%1$d dal letöltése</string>
+ <string name="download.downloading_summary">Aktuális: %1$s</string>
+ <string name="download.downloading_summary_expanded">Aktuális: %1$s
+ \nBecsült méret: %2$s</string>
+ <string name="download.failed_to_load">A beolvasás sikertelen!</string>
+ <string name="download.restore_play_queue">Folytatás onnan, ahol egy másik eszközön abbahagyta.</string>
+
+ <string name="sync.new_podcasts">Új podcastok: \"%s\"</string>
+ <string name="sync.new_playlists">Új lejátszási listák: \"%s\"</string>
+ <string name="sync.new_albums">Új albumok: \"%s\"</string>
+ <string name="sync.new_starred">Új csillagozott dalok</string>
+
+ <string name="starring_content_starred">\"%s\" csillagozás be</string>
+ <string name="starring_content_unstarred">\"%s\" csillagozás ki</string>
+ <string name="starring_content_error">Nem sikerült frissíteni \"%s\", próbálja később!</string>
+
+ <string name="playlist_error">Nem sikerült elérni a lejátszási lista adatait!</string>
+ <string name="updated_playlist">%1$s dal hozzáadva: \"%2$s\"</string>
+ <string name="updated_playlist_error">Nem sikerült frissíteni \"%s\", próbálja később!</string>
+ <string name="removed_playlist">%1$s dal eltávolítva: \"%2$s\"</string>
+
+ <string name="bookmark.delete">Könyvjelző törlése</string>
+ <string name="bookmark.delete_title">Könyvjelző törlése</string>
+ <string name="bookmark.deleted">\"%s\" könyvjelző törölve</string>
+ <string name="bookmark.deleted_error">\"%s\" könyvjelző törlése sikertelen!</string>
+ <string name="bookmark.details_title">Könyvjelző részletei</string>
+ <string name="bookmark.details">Dal: %1$s
+ \nPozíció: %2$s
+ \nLétrehozva: %3$s
+ \nUtolsó módosítás: %4$s
+ \nMegjegyzés: %5$s</string>
+ <string name="bookmark.resume_title">Folytatja a lejátszást?</string>
+ <string name="bookmark.resume">\"%1$s\" folytatása innen: \"%2$s\"</string>
+ <string name="bookmark.action_resume">Folytatás</string>
+ <string name="bookmark.action_start_over">Kezdés</string>
+
+ <string name="rating.title">\"%s\" értékelve</string>
+ <string name="rating.set_rating">\"%s\" értékelve</string>
+ <string name="rating.set_rating_failed">\"%s\" értékelése sikertelen!</string>
+ <string name="rating.remove_rating">\"%s\" értékelése visszavonva</string>
+ <string name="rating.remove_rating_failed">\"%s\" értékelésének visszavonása sikertelen!</string>
+
+ <string name="song_details.error">Hiba</string>
+ <string name="song_details.skipped">Átlépve</string>
+ <string name="song_details.downloading">Letöltés</string>
+
+ <string name="lyrics.nomatch">Dalszöveg nem található!</string>
+
+ <string name="error.label">Hiba</string>
+
+ <string name="settings.title">Beállítások</string>
+ <string name="settings.test_connection_title">Kapcsolat tesztelése</string>
+ <string name="settings.servers_add">Kiszolgáló hozzáadása</string>
+ <string name="settings.servers_remove">Kiszolgáló eltávolítása</string>
+ <string name="settings.servers_title">Kiszolgálók</string>
+ <string name="settings.server_unused">Nem használt</string>
+ <string name="settings.server_name">Név</string>
+ <string name="settings.server_address">Kiszolgáló címe</string>
+ <string name="settings.server_local_network_ssid" >Helyi hálózati SSID</string>
+ <string name="settings.server_local_network_ssid_hint">Aktuális SSID: %s</string>
+ <string name="settings.server_internal_address">Belső hálózati cím</string>
+ <string name="settings.server_username">Felhasználónév</string>
+ <string name="settings.server_password">Jelszó</string>
+ <string name="settings.server_open_browser">Megnyitás böngészőben</string>
+ <string name="settings.server_sync_summary">Függetlenül attól, hogy a szinkronizálás engedélyezett-e ezen a kiszolgálón.</string>
+ <string name="settings.server_sync">Szinkronizálás engedélyezve</string>
+ <string name="settings.cache_title">Zene gyorsítótár (Cache)</string>
+ <string name="settings.preload_wifi">Dalok előolvasása (Wi-Fi)</string>
+ <string name="settings.preload_mobile">Dalok előolvasása (Mobilhálózat)</string>
+ <string name="settings.cache_size">Gyorsítótár mérete (MB)</string>
+ <string name="settings.cache_location">Gyorsítótár helye</string>
+ <string name="settings.cache_location_error">Hibás gyorsítótár hely! Az alapértelmezett használata.</string>
+ <string name="settings.cache_location_reset">A beállított gyorsítótár-hely már nem írható! Ha a közelmúltban frissítette telefonja Android rendszerét 4.4.x KitKat verzióra, abban az SD kártya kezelése megváltozott, és az alkalmazások csak egy speciális helyre tudnak írni. A Dsub már automatikusan átállt a megfelelő helyre. Ahhoz, hogy a régi adatokat törölni tudja, csatlakoztassa az SD kártyát a számítógépéhez, és törölje a régi mappát!</string>
+ <string name="settings.cache_clear">Gyorsítótár törlése</string>
+ <string name="settings.cache_clear_complete">Gyorsítótár törlése kész!</string>
+ <string name="settings.testing_connection">Kapcsolat tesztelése...</string>
+ <string name="settings.testing_ok">Kapcsolat OK!</string>
+ <string name="settings.testing_unlicensed">Kapcsolat OK! A kiszolgálónak nincs licence!</string>
+ <string name="settings.connection_failure">Kapcsolódási hiba!</string>
+ <string name="settings.invalid_url">Adjon meg egy érvényes URL-t!</string>
+ <string name="settings.invalid_username">Adjon meg egy érvényes felhasználónevet (szóközt nem tartalmazhat)!</string>
+ <string name="settings.appearance_title">Megjelenés</string>
+ <string name="settings.theme_title">Témák</string>
+ <string name="settings.theme_light">Világos</string>
+ <string name="settings.theme_dark">Sötét</string>
+ <string name="settings.theme_black">Fekete</string>
+ <string name="settings.theme_holo">Holo</string>
+ <string name="settings.theme_fullscreen">Teljes képernyős</string>
+ <string name="settings.theme_fullscreen_summary">Teljes képernyős üzemmód (értesítési sáv elrejtése).</string>
+ <string name="settings.track_title">Dalsorszám megjelenítése</string>
+ <string name="settings.track_summary">Dalsorszám megjelenítése a dal címe előtt, ha létezik.</string>
+ <string name="settings.custom_sort">Egyéni rendezés</string>
+ <string name="settings.custom_sort_summary">A kiszolgáló alapértelmezett rendezésének felülbírálása, rendezés a lemez sorszáma és a kiadás éve alapján.</string>
+ <string name="settings.network_title">Hálózat</string>
+ <string name="settings.max_bitrate_wifi">Max. audió bitráta - Wi-Fi</string>
+ <string name="settings.max_bitrate_mobile">Max. audió bitráta - Mobilhálózat</string>
+ <string name="settings.max_bitrate_32">32 Kbps</string>
+ <string name="settings.max_bitrate_64">64 Kbps</string>
+ <string name="settings.max_bitrate_80">80 Kbps</string>
+ <string name="settings.max_bitrate_96">96 Kbps</string>
+ <string name="settings.max_bitrate_112">112 Kbps</string>
+ <string name="settings.max_bitrate_128">128 Kbps</string>
+ <string name="settings.max_bitrate_160">160 Kbps</string>
+ <string name="settings.max_bitrate_192">192 Kbps</string>
+ <string name="settings.max_bitrate_256">256 Kbps</string>
+ <string name="settings.max_bitrate_320">320 Kbps</string>
+ <string name="settings.max_video_bitrate_wifi">Max. videó bitráta - Wi-Fi</string>
+ <string name="settings.max_video_bitrate_mobile">Max. videó bitráta - Mobilhálózat</string>
+ <string name="settings.max_video_bitrate_200">200 Kbps</string>
+ <string name="settings.max_video_bitrate_300">300 Kbps</string>
+ <string name="settings.max_video_bitrate_400">400 Kbps</string>
+ <string name="settings.max_video_bitrate_500">500 Kbps</string>
+ <string name="settings.max_video_bitrate_700">700 Kbps</string>
+ <string name="settings.max_video_bitrate_1000">1000 Kbps</string>
+ <string name="settings.max_video_bitrate_1500">1500 Kbps</string>
+ <string name="settings.max_video_bitrate_2000">2000 Kbps</string>
+ <string name="settings.max_video_bitrate_3000">3000 Kbps</string>
+ <string name="settings.max_video_bitrate_5000">5000 Kbps</string>
+ <string name="settings.max_bitrate_unlimited">Korlátlan</string>
+ <string name="settings.wifi_required_title">Streamelés csak Wi-Fivel</string>
+ <string name="settings.wifi_required_summary">Streamelés csak Wi-Fi hálózaton keresztül.</string>
+ <string name="settings.network_timeout_title">Hálózati időtúllépés</string>
+ <string name="settings.network_timeout_10000">10 másodperc</string>
+ <string name="settings.network_timeout_15000">15 másodperc</string>
+ <string name="settings.network_timeout_30000">30 másodperc</string>
+ <string name="settings.network_timeout_45000">45 másodperc</string>
+ <string name="settings.network_timeout_60000">60 másodperc</string>
+ <string name="settings.preload_0">0 dal</string>
+ <string name="settings.preload_1">1 dal</string>
+ <string name="settings.preload_2">2 dal</string>
+ <string name="settings.preload_3">3 dal</string>
+ <string name="settings.preload_5">5 dal</string>
+ <string name="settings.preload_10">10 dal</string>
+ <string name="settings.preload_unlimited">Korlátlan</string>
+ <string name="settings.clear_search_history">Keresési előzmények törlése</string>
+ <string name="settings.search_history_cleared">Keresési előzmények törölve</string>
+ <string name="settings.other_title">Egyéb beállítások</string>
+ <string name="settings.scrobble_title">Továbbítás Last.fm-re (Scrobbling)</string>
+ <string name="settings.scrobble_summary">A Last.fm felhasználónevet és jelszót be kell állítani a Subsonic kiszolgálón!</string>
+ <string name="settings.hide_media_title">Elrejtés</string>
+ <string name="settings.hide_media_summary">Zenefájlok elrejtése egyéb alkalmazások elől.</string>
+ <string name="settings.hide_media_toast">A következő alkalomtól lép életbe, amikor az Android zenefájlokat keres az eszközön.</string>
+ <string name="settings.media_button_title">Média vezérlőgombok</string>
+ <string name="settings.media_button_summary">A lejátszó irányítása a bluetooth eszköz vagy a fülhallgató vezérlőgombjaival.</string>
+ <string name="settings.screen_lit_title">Képernyő ébrentartása</string>
+ <string name="settings.screen_lit_summary">Képernyő ébrentartása a letöltés alatt a magasabb letöltési sebesség érdekében.</string>
+ <string name="settings.playlist_title">Lejátszási listák</string>
+ <string name="settings.playlist_random_size_title">Véletlenszerű lejátszási lista mérete</string>
+ <string name="settings.sleep_timer_title">Alvó üzemmód időzítő</string>
+ <string name="settings.sleep_timer_duration_title">Alvó üzemmód időtartam</string>
+ <string name="settings.sleep_timer_off">Ki</string>
+ <string name="settings.sleep_timer_on">Be</string>
+ <string name="settings.sleep_timer_always_on">Mindig be</string>
+ <string name="settings.temp_loss_title">Külső esemény bekövetkeztekor</string>
+ <string name="settings.temp_loss_pause">Megállítás minden esetben</string>
+ <string name="settings.temp_loss_pause_lower">Megállítás, kérésre alacsonyabb hangerő</string>
+ <string name="settings.temp_loss_lower">Alacsonyabb hangerő</string>
+ <string name="settings.temp_loss_nothing">Ne csináljon semmit</string>
+ <string name="settings.disconnect_pause_title">Megállítás kapcsolatbontás esetén</string>
+ <string name="settings.disconnect_pause_both">Megállítás minden esetben</string>
+ <string name="settings.disconnect_pause_neither">Ne csináljon semmit</string>
+ <string name="settings.persistent_title">Állandó kijelzés</string>
+ <string name="settings.persistent_summary">Kijelzés az értesítési sávon a lejátszás megállítása után is. Nyomja meg a bezárás gombot a törléséhez!</string>
+ <string name="settings.gapless_playback">Egybefüggő lejátszás (Gapless)</string>
+ <string name="settings.gapless_playback_summary">Ha lefagyásokat/furcsaságokat tapasztal az egybefüggő lejátszás (Gapless) engedélyezése után, a probléma megoldásához kapcsolja ki a funkciót!</string>
+ <string name="settings.chat_refresh">Csevegés frissítési gyakorisága (mp.)</string>
+ <string name="settings.chat_enabled">Csevegés (Chat) engedélyezése</string>
+ <string name="settings.chat_enabled_summary">Csevegés (Chat) menüpont megjelenítése az elhúzható oldalsávon.</string>
+ <string name="settings.video_title">Videó</string>
+ <string name="settings.video_player">Videó-lejátszó</string>
+ <string name="settings.video_raw">Nyers (Subsonic 4.8-tól)</string>
+ <string name="settings.video_hls">HTTP Live Stream (HLS) (Subsonic 4.8-tól)</string>
+ <string name="settings.video_transcode">Közvetlen transzkódolás (video -> mp4, vagy hasonló beállítás szükséges a kiszolgálón!)</string>
+ <string name="settings.video_flash">Flash (Plugin szükséges!)</string>
+ <string name="settings.cache_screen_title">Gyorsítótár/Hálózat</string>
+ <string name="settings.playback_title">Lejátszás</string>
+ <string name="settings.hide_widget_title">Widget elrejtése</string>
+ <string name="settings.hide_widget_summary">Widget elrejtése kilépés után.</string>
+ <string name="settings.podcasts_enabled">Podcastok engedélyezése</string>
+ <string name="settings.podcasts_enabled_summary">Podcastok menüpont megjelenítése az elhúzható oldalsávon.</string>
+ <string name="settings.bookmarks_enabled">Könyvjelzők engedélyezése</string>
+ <string name="settings.bookmarks_enabled_summary">Könyvjelzők menüpont megjelenítése az elhúzható oldalsávon.</string>
+ <string name="settings.shares_enabled">Megosztások engedélyezése</string>
+ <string name="settings.shares_enabled_summary">Megosztások menüpont megjelenítése az elhúzható oldalsávon.</string>
+ <string name="settings.sync_title">Szinkronizálás</string>
+ <string name="settings.sync_enabled">Szinkronizálás engedélyezése</string>
+ <string name="settings.sync_enabled_summary">Lejátszási listák és podcastok változásainak rendszeres ellenőrzése.</string>
+ <string name="settings.sync_interval">Szinkronizálás gyakorisága</string>
+ <string name="settings.sync_interval_15">15 perc</string>
+ <string name="settings.sync_interval_30">30 perc</string>
+ <string name="settings.sync_interval_60">1 óra</string>
+ <string name="settings.sync_interval_120">2 óra</string>
+ <string name="settings.sync_interval_240">4 óra</string>
+ <string name="settings.sync_interval_360">6 óra</string>
+ <string name="settings.sync_interval_720">12 óra</string>
+ <string name="settings.sync_interval_1440">Naponta</string>
+ <string name="settings.sync_wifi">Szinkronizálás csak Wi-Fivel</string>
+ <string name="settings.sync_wifi_summary">Szinkronizálás csak Wi-Fi hálózaton keresztül.</string>
+ <string name="settings.sync_most_recent">Utoljára hozzáadottak szinkronizálása</string>
+ <string name="settings.sync_most_recent_summary">Az utoljára hozzáadott albumok automatikus gyorsítótárazása.</string>
+ <string name="settings.sync_starred">Csillagozottak szinkronizálása</string>
+ <string name="settings.sync_starred_summary">A csillagozott dalok/albumok/előadók automatikus gyorsítótárazása.</string>
+ <string name="settings.sync_notification">Szinkronizálási értesítések</string>
+ <string name="settings.sync_notification_summary">Értesítés megjelenítése, ha új média került szinkronizálásra.</string>
+ <string name="settings.menu_options.title">Opcionális menübeállítások</string>
+ <string name="settings.menu_options.play_next_summary">Sorbaállítás következőnek opció megjelenítése a menüben.</string>
+ <string name="settings.menu_options.play_last_summary">Sorbaállítás utolsónak opció megjelenítése a menüben.</string>
+ <string name="settings.menu_options.star_summary">Csillagozás opció megjelenítése a menüben.</string>
+ <string name="settings.menu_options.shared_summary">Megosztás opció megjelenítése a menüben.</string>
+ <string name="settings.menu_options.rate_summary">Értékelés opció megjelenítése a menüben.</string>
+ <string name="settings.browse_by_tags">Böngészés ID3 Tag használatával</string>
+ <string name="settings.browse_by_tags_summary">ID3 Tag módszer használata a fájlredszer alapú mód helyett. Subsonic 4.7+ verzió felett!</string>
+ <string name="settings.disable_exit_prompt">Kilépés megerősítésének tiltása</string>
+ <string name="settings.disable_exit_prompt_summary">A főoldalon a vissza gomb megnyomásakor azonnali kilépés az alkalmazásból.</string>
+ <string name="settings.override_system_language">A rendszer nyelvének felülbírálása</string>
+ <string name="settings.override_system_language_summary">A Dsub megjelenítése angol nyelven abban az esetben is, ha rendelkezik fordítással. Az alkalmazást törölni kell a memóriából, mert a beállítás csak újraindítás után lép érvénybe!</string>
+ <string name="settings.drawer_items_title">Oldalsáv elemei</string>
+ <string name="settings.play_now_after">Lejátszás utána</string>
+ <string name="settings.play_now_after_summary">Egy helyi menü, amivel lehetővé válik minden dal lejátszása a kijelölt elem után (mint a Subsonic webes felületén)</string>
+ <string name="settings.large_album_art">Nagy méretű albumborítók</string>
+ <string name="settings.large_album_art_summary">Albumok megjelenítése rácsnézetben és nagy méretű albumborítóval a listanézet helyett.</string>
+ <string name="settings.admin_enabled">Admin engedélyezése</string>
+ <string name="settings.admin_enabled_summary">Admin menüpont megjelenítése az elhúzható oldalsávon.</string>
+ <string name="settings.replay_gain">Hangerő-kiegyenlítés (Replay Gain)</string>
+ <string name="settings.replay_gain_summary">Hangerő kiegyenlítése (normalizálása) a dal, vagy az album hangerőszint értékei (tags) alapján.</string>
+ <string name="settings.replay_gain_type">Hangerőszint meghatározása</string>
+ <string name="settings.replay_gain_type.smart">Intelligens módon</string>
+ <string name="settings.replay_gain_type.album">Album értékeiből</string>
+ <string name="settings.replay_gain_type.track">Dal értékeiből</string>
+ <string name="settings.replay_gain_bump">Hangerő-kiegyenlítés előerősítése</string>
+ <string name="settings.replay_gain_untagged">Dalok hangerő-kiegyenlítés nélkül</string>
+ <string name="settings.casting">Casting (Tartalmak átküldése)</string>
+ <string name="settings.casting_proxy">Eszköz használata proxyként</string>
+ <string name="settings.casting_proxy_summary">Streamelés az eszközön (mint egy proxyn) keresztül. Ez megoldást hozhat néhány esetben, pl. saját aláírású tanúsítvány használatakor.</string>
+ <string name="settings.rename_duplicates">Duplikált dalok átnevezése</string>
+ <string name="settings.rename_duplicates_summary">Duplikált dalok átnevezése az eredeti fájlnévre, így megkülönböztethetővé válnak.</string>
+
+ <string name="shuffle.title">Sorrend keverése</string>
+ <string name="shuffle.startYear">Kezdő év:</string>
+ <string name="shuffle.endYear">Befejező év:</string>
+ <string name="shuffle.genre">Műfaj:</string>
+ <string name="shuffle.pick_genre">Műfaj kiválasztása</string>
+
+ <string name="share.info">Tulajdonos: %1$s
+ \nLeírás: %2$s
+ \nURL: %3$s
+ \nLétrehozva: %4$s
+ \nUtolsó látogatás: %5$s
+ \nLejárati idő: %6$s
+ \nLátogatások száma: %7$s
+
+ </string>
+ <string name="share.expires">Lejárati idő: %s</string>
+ <string name="share.expires_never">Nincs lejárati idő</string>
+ <string name="share.deleted">\"%s\" megosztás törölve</string>
+ <string name="share.deleted_error">\"%s\" megosztás törlése sikertelen!</string>
+ <string name="share.no_expiration">Nincs lejárati idő</string>
+ <string name="share.expiration">Lejárati idő:</string>
+ <string name="share.updated_info">\"%s\" megosztás információi frissítve</string>
+ <string name="share.updated_info_error">\"%s\" megosztás információinak frissítése sikertelen!</string>
+ <string name="share.via">Megosztás ezzel</string>
+ <string name="share.delete">Megosztás törlése</string>
+
+ <string name="admin.add_user_username">Felhasználónév:</string>
+ <string name="admin.add_user_email">Email:</string>
+ <string name="admin.add_user_password">Jelszó:</string>
+ <string name="admin.create_user_success">A felhasználó létrehozva</string>
+ <string name="admin.create_user_error">A felhasználó létrehozása sikertelen!</string>
+ <string name="admin.change_username_invalid">Adjon meg egy érvényes felhasználónevet!</string>
+ <string name="admin.update_permissions">Jogosultságok módosítása</string>
+ <string name="admin.update_permissions_success">\"%1$s\" jogosultságainak módosítása sikerült</string>
+ <string name="admin.update_permissions_error">\"%1$s\" jogosultságainak módosítása sikertelen!</string>
+ <string name="admin.change_email">Email csere</string>
+ <string name="admin.change_email_success">\"%1$s\" email címének módosítása sikerült</string>
+ <string name="admin.change_email_error">\"%1$s\" email címének módosítása sikertelen!</string>
+ <string name="admin.change_email_label">Új email:</string>
+ <string name="admin.change_email_invalid">Adjon meg egy érvényes email címet!</string>
+ <string name="admin.change_password">Jelszó csere</string>
+ <string name="admin.change_password_success">\"%1$s\" jelszavának módosítása sikerült</string>
+ <string name="admin.change_password_error">\"%1$s\" jelszavának módosítása sikertelen!</string>
+ <string name="admin.change_password_label">Új jelszó:</string>
+ <string name="admin.change_password_invalid">Adjon meg egy érvényes jelszót!</string>
+ <string name="admin.delete_user">Felhasználó törlése</string>
+ <string name="admin.delete_user_success">\"%1$s\" felhasználó létrehozva</string>
+ <string name="admin.delete_user_error">\"%1$s\" felhasználó törlése sikertelen!</string>
+ <string name="admin.confirm_password">Jelszó megerősítése</string>
+ <string name="admin.confirm_password_bad">A beírt jelszó nem egyezik!</string>
+
+ <string name="admin.scrobblingEnabled">Scrobbling használata</string>
+ <string name="admin.role.admin">Adminisztrátor</string>
+ <string name="admin.role.settings">Beállítások módosítása</string>
+ <string name="admin.role.download">Eredeti fájlok letöltése</string>
+ <string name="admin.role.upload">Feltöltés a kiszolgálóra</string>
+ <string name="admin.role.coverArt">Albumborító cseréje</string>
+ <string name="admin.role.comment">Megjegyzések hozzáadása</string>
+ <string name="admin.role.podcast">Podcastok kezelése</string>
+ <string name="admin.role.stream">Zene streamelése</string>
+ <string name="admin.role.jukebox">Jukebox vezérlése</string>
+ <string name="admin.role.share">Megosztások kezelése</string>
+ <string name="admin.role.lastfm">Last.fm funkció használata</string>
+
+ <string name="music_service.retry">Hálózati hiba történt! Újrapróbálkozás %1$d/%2$d.</string>
+
+ <string name="background_task.wait">Kérem várjon...</string>
+ <string name="background_task.loading">Betöltés...</string>
+ <string name="background_task.no_network">Az alkalmazás hálózati hozzáférést igényel. Kérjük, kapcsolja be a Wi-Fi-t vagy a mobilhálózatot!</string>
+ <string name="background_task.network_error">Hálózati hiba történt! Kérjük, ellenőrizze a kiszolgáló címét, vagy próbálja később!</string>
+ <string name="background_task.not_found">Az erőforrás nem található! Kérjük, ellenőrizze a kiszolgáló címét!</string>
+ <string name="background_task.parse_error">Hiba történt a kiszolgálóval történő kommunikációban. Kérjük, ellenőrizze a kiszolgáló címét, és próbáljon meg web böngészővel kapcsolódni a kiszolgálóhoz!</string>
+
+ <string name="service.connecting">Kapcsolódás a kiszolgálóhoz, kérem várjon...</string>
+
+ <string name="parser.upgrade_client">Nem kompatibilis verzió. Kérjük, frissítse a DSub Android alkalmazást!</string>
+ <string name="parser.upgrade_server">Nem kompatibilis verzió. Kérjük, frissítse a Subsonic kiszolgálót!</string>
+ <string name="parser.not_authenticated">Hibás felhasználónév vagy jelszó!</string>
+ <string name="parser.not_authorized">Nincs engedélyezve! Ellenőrizze a felhasználó jogosultságait a Subsonic kiszolgálón!</string>
+ <string name="parser.artist_count">%d előadó található a médiatárban.</string>
+ <string name="parser.server_error">Kiszolgáló hiba: %s</string>
+ <string name="parser.scan_count">%d tétel átvizsgálva.</string>
+
+ <string name="select_artist.refresh">Frissítés</string>
+ <string name="select_artist.folder">Mappa kiválasztása</string>
+ <string name="select_artist.all_folders">Összes mappa</string>
+
+ <string name="equalizer.label">Equalizer</string>
+ <string name="equalizer.enabled">Engedélyezve</string>
+ <string name="equalizer.preset">Profil kiválasztása</string>
+ <string name="equalizer.bass_booster">Basszus fokozás</string>
+ <string name="equalizer.voice_booster">Beszédhang fokozás</string>
+ <string name="equalizer.db_size">%d dB</string>
+ <string name="equalizer.bass_size">%d ezer</string>
+
+ <string name="widget.4x1">DSub (4x1)</string>
+ <string name="widget.4x2">DSub (4x2)</string>
+ <string name="widget.4x3">DSub (4x3)</string>
+ <string name="widget.4x4">DSub (4x4)</string>
+ <string name="widget.initial_text">Érintse meg a zene kiválasztásához!</string>
+ <string name="widget.sdcard_busy">Az SD kártya nem elérhető!</string>
+ <string name="widget.sdcard_missing">Nincs SD kártya!</string>
+
+ <string name="util.bytes_format.gigabyte">0.00 GB</string>
+ <string name="util.bytes_format.megabyte">0.00 MB</string>
+ <string name="util.bytes_format.kilobyte">0 KB</string>
+ <string name="util.bytes_format.byte">0 B</string>
+
+ <string name="changelog_full_title">Újdonságok</string>
+ <string name="changelog_title">Újdonságok</string>
+ <string name="changelog_ok_button">OK</string>
+ <string name="changelog_show_full">Továbbiak…</string>
+
+ <string name="chat.send_a_message">Üzenet küldése</string>
+
+ <string name="changelog_version_format" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">Version <xliff:g id="version_name">%s</xliff:g></string>
+
+ <string name="tasker.start_playing">Lejátszás indítása</string>
+ <string name="tasker.start_playing_shuffled">Lejátszás indítása kevert sorrendben</string>
+ <string name="tasker.start_playing_title">Tasker -> DSub indítása</string>
+ <string name="tasker.edit_shuffle_mode">Indítás kevert sorrendben: </string>
+ <string name="tasker.edit_shuffle_start_year">Kevert sorrend kezdő év:</string>
+ <string name="tasker.edit_shuffle_end_year">Kevert sorrend utolsó év:</string>
+ <string name="tasker.edit_shuffle_genre">Kevert sorrend műfaja:</string>
+ <string name="tasker.edit_server_offline">Offline kapcsoló: </string>
+ <string name="tasker.edit_do_nothing">Ne csináljon semmit</string>
+
+ <plurals name="select_album_n_songs">
+ <item quantity="zero">Nincsenek dalok</item>
+ <item quantity="one">1 dal</item>
+ <item quantity="other">%d dal</item>
+ </plurals>
+ <plurals name="select_album_n_songs_downloading">
+ <item quantity="one">1 dal kijelölve letöltésre.</item>
+ <item quantity="other">%d dal kijelölve letöltésre.</item>
+ </plurals>
+ <plurals name="select_album_n_songs_added">
+ <item quantity="one">1 dal hozzáadva a várólistához.</item>
+ <item quantity="other">%d dal hozzáadva a várólistához.</item>
+ </plurals>
+ <plurals name="select_album_donate_dialog_n_trial_days_left">
+ <item quantity="one">1 nap van hátra a próbaidőszakból.</item>
+ <item quantity="other">%d nap van hátra a próbaidőszakból.</item>
+ </plurals>
+
+</resources>
diff --git a/app/src/main/res/values-land/integers.xml b/app/src/main/res/values-land/integers.xml
new file mode 100644
index 00000000..40071f39
--- /dev/null
+++ b/app/src/main/res/values-land/integers.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <integer name="Grid.Columns">3</integer>
+</resources> \ No newline at end of file
diff --git a/app/src/main/res/values-large/dimens.xml b/app/src/main/res/values-large/dimens.xml
new file mode 100644
index 00000000..b08dda86
--- /dev/null
+++ b/app/src/main/res/values-large/dimens.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="Button">64dip</dimen>
+ <dimen name="Button.Small">54dip</dimen>
+ <dimen name="AlbumArt.Small">96dip</dimen>
+ <dimen name="AlbumArt.Header">210dip</dimen>
+</resources> \ No newline at end of file
diff --git a/app/src/main/res/values-large/integers.xml b/app/src/main/res/values-large/integers.xml
new file mode 100644
index 00000000..914ec84a
--- /dev/null
+++ b/app/src/main/res/values-large/integers.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <integer name="Grid.Columns">3</integer>
+ <integer name="TextDescriptionLength">10</integer>
+</resources> \ No newline at end of file
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
new file mode 100644
index 00000000..cfae3ea0
--- /dev/null
+++ b/app/src/main/res/values-ru/strings.xml
@@ -0,0 +1,312 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="common.appname">DSub</string>
+ <string name="common.ok">OK</string>
+ <string name="common.save">Сохранить</string>
+ <string name="common.cancel">Отмена</string>
+ <string name="common.play_now">Воспроизвести сейчас</string>
+ <string name="common.play_shuffled">Случайное воспроизведение</string>
+ <string name="common.play_next">Воспроизвести следующим</string>
+ <string name="common.play_last">Воспроизвести последним</string>
+ <string name="common.download">Скачать</string>
+ <string name="common.pin">Кешировать</string>
+ <string name="common.delete">Удалить</string>
+ <string name="common.star">Добавить в закладки</string>
+ <string name="common.unstar">Удалить из закладок</string>
+ <string name="common.info">Информация</string>
+ <string name="common.name">Название</string>
+ <string name="common.comment">Комментарий</string>
+ <string name="common.public">Общедоступный</string>
+ <string name="common.play_external">Воспроизвести во внешнем плеере</string>
+ <string name="common.stream_external">Воспроизвести поток во внешнем плеере</string>
+ <string name="common.confirm">Подтверждение</string>
+
+ <string name="button_bar.home">Домой</string>
+ <string name="button_bar.browse">Медиатека</string>
+ <string name="button_bar.search">Поиск</string>
+ <string name="button_bar.playlists">Списки</string>
+ <string name="button_bar.now_playing">Плеер</string>
+
+ <string name="main.welcome_title">Здравствуйте!</string>
+ <string name="main.welcome_text">Добро пожаловать в DSub! Это приложение настроено на работу с демо сервером Subsonic. После настройки Вашего персонального сервера (доступен на <b>subsonic.org</b>), пожалуйста, перейдите в <b>Настройки</b> и измените параметры для подключения.</string>
+
+ <string name="main.about_title">О программе DSub</string>
+ <string name="main.about_text">Автор: Scott Jackson
+ \nEmail: dsub.android@gmail.com
+ \nВерсия: %1$s
+ \nFiles Cached: %2$s
+ \nИспользовано места: %3$s из %4$s
+ \nДоступно места: %5$s из %6$s</string>
+ <string name="main.select_server">Выбрать сервер</string>
+ <string name="main.shuffle">Случайное воспроизведение</string>
+ <string name="main.offline">Отключиться</string>
+ <string name="main.online">Подключиться</string>
+ <string name="main.settings">Настройки</string>
+ <string name="main.albums_title">Альбомы</string>
+ <string name="main.albums_newest">Недавно добавленные</string>
+ <string name="main.albums_recent">Недавно прослушанные</string>
+ <string name="main.albums_frequent">Часто прослушиваемые</string>
+ <string name="main.albums_highest">Максимальный рейтинг</string>
+ <string name="main.albums_starred">Закладки</string>
+ <string name="main.albums_random">Случайные</string>
+
+ <string name="menu.search">Поиск</string>
+ <string name="menu.shuffle">Перемешать</string>
+ <string name="menu.refresh">Обновить</string>
+ <string name="menu.play">Воспроизвести</string>
+ <string name="menu.play_last">Воспроизвести последним</string>
+ <string name="menu.exit">Выход</string>
+ <string name="menu.settings">Настройки</string>
+ <string name="menu.help">Помощь</string>
+ <string name="menu.about">О программе</string>
+ <string name="menu.add_playlist">Добавить в список</string>
+ <string name="menu.remove_playlist">Удалить из списка</string>
+ <string name="menu.deleted_playlist">Список воспроизведения %s удален</string>
+ <string name="menu.deleted_playlist_error">Не удалось удалить список %s</string>
+ <string name="menu.log">Отправить журнал событий</string>
+ <string name="menu.set_timer">Установить таймер</string>
+ <string name="menu.delete_cache">Удалить кэш</string>
+
+ <string name="playlist.label">Списки</string>
+ <string name="playlist.update_info">Изменить информацию</string>
+ <string name="playlist.updated_info">Информация для списка воспроизведения %s обновлена</string>
+ <string name="playlist.updated_info_error">Не удалось обновить информацию для списка воспроизведения %s</string>
+
+ <string name="search.label">Поиск</string>
+ <string name="search.title">Поиск</string>
+ <string name="search.search">Нажмите для поиска</string>
+ <string name="search.no_match">Ничего не найдено, пожалуйста, попробуйте снова</string>
+ <string name="search.artists">Исполнители</string>
+ <string name="search.albums">Альбомы</string>
+ <string name="search.songs">Композиции</string>
+ <string name="search.more">Показать еще</string>
+
+ <string name="progress.wait">Пожалуйста, подождите...</string>
+
+ <string name="music_library.label">Медиатека</string>
+ <string name="music_library.label_offline">Оффлайн медиа</string>
+
+ <string name="select_album.select">Выбрать все</string>
+ <string name="select_album.n_selected">%d композиций выбрано.</string>
+ <string name="select_album.n_unselected">Выбор снят с %d композиций.</string>
+ <string name="select_album.more">Еще</string>
+ <string name="select_album.offline">Оффлайн</string>
+ <string name="select_album.searching">Выполняется поиск...</string>
+ <string name="select_album.no_sdcard">Ошибка: SD карта недоступна</string>
+ <string name="select_album.no_network">Внимание: сеть недоступна.</string>
+ <string name="select_album.not_licensed">Сервер не лицензирован. %d дней до окончания пробного периода.</string>
+ <string name="select_album.donate_dialog_message">Осуществите пожертвование для Subsonic и получите возможность неограниченного скачивания.</string>
+ <string name="select_album.donate_dialog_now">Сейчас</string>
+ <string name="select_album.donate_dialog_later">Позже</string>
+ <string name="select_album.donate_dialog_0_trial_days_left">Пробный период закончился</string>
+
+ <string name="download.empty">Список воспроизведения пуст</string>
+ <string name="download.shuffle_loading">Загружается случайный список...</string>
+ <string name="download.playerstate_downloading">Загрузка - %s</string>
+ <string name="download.playerstate_buffering">Буферизация</string>
+ <string name="download.playerstate_playing_shuffle">Воспроизводится случайно</string>
+ <string name="download.menu_show_album">Показать альбом</string>
+ <string name="download.menu_lyrics">Текст</string>
+ <string name="download.menu_remove">Убрать из очереди</string>
+ <string name="download.menu_remove_all">Очистить</string>
+ <string name="download.menu_screen_on">Включить подсветку</string>
+ <string name="download.menu_shuffle">Перемешать</string>
+ <string name="download.menu_toggle">Переключатель</string>
+ <string name="download.menu_save">Сохранить список</string>
+ <string name="download.menu_shuffle_notification">Список воспроизведения был перемешан</string>
+ <string name="download.playlist_title">Сохранение списка воспроизведения</string>
+ <string name="download.playlist_name">Введите название:</string>
+ <string name="download.playlist_saving">Сохранение списка воспроизведения \"%s\"...</string>
+ <string name="download.playlist_done">Список воспроизведения сохранен</string>
+ <string name="download.playlist_error">Не удалось сохранить список воспроизведения, пожалуйста, попробуйте позже.</string>
+ <string name="download.repeat_off">Повторение отключено</string>
+ <string name="download.repeat_all">Повторять все</string>
+ <string name="download.repeat_single">Повторять композицию</string>
+ <string name="download.jukebox_on">Удаленное управление включено. Музыка воспроизводится на компьютере.</string>
+ <string name="download.jukebox_off">Удаленное управление отключено. Музыка воспроизводится на устройстве.</string>
+ <string name="download.jukebox_volume">Удаленное управление громкостью</string>
+ <string name="download.jukebox_server_too_old">Удаленное управление не поддерживается. Пожалуйста, обновите Ваш сервер Subsonic.</string>
+ <string name="download.jukebox_offline">Удаленное управление не поддерживается в оффлайн режиме.</string>
+ <string name="download.jukebox_not_authorized">Удаленное управление запрещено. Пожалуйста, активируйте режим jukebox в разделе <b>Настройки &gt; Проигрыватели</b> на вашем сервере Subsonic.</string>
+ <string name="download.timer_length">Длительность</string>
+ <string name="download.start_timer">Запустить таймер</string>
+ <string name="download.stop_timer">Остановить таймер</string>
+ <string name="download.need_download">Необходимо сначала скачать видео</string>
+ <string name="download.no_streaming_player">Нет плеера для воспроизведения потока</string>
+
+ <string name="starring_content_starred">\"%s\" добавлено в закладки</string>
+ <string name="starring_content_unstarred">\"%s\" удалено из закладок</string>
+ <string name="starring_content_error">Не удалось обновить \"%s\", пожалуйста, попробуйте позже.</string>
+
+ <string name="playlist_error">Не удалось прочитать списки воспроизведения</string>
+ <string name="updated_playlist">Добавлено %1$s композиций в \"%2$s\"</string>
+ <string name="updated_playlist_error">Не удалось обновить \"%s\", пожалуйста, попробуйте позже.</string>
+ <string name="removed_playlist">Удалено %1$s из \"%2$s\" композиций</string>
+
+ <string name="lyrics.nomatch">Текст не найден</string>
+
+ <string name="error.label">Ошибка</string>
+
+ <string name="settings.title">Настройки DSub</string>
+ <string name="settings.test_connection_title">Проверить соединение</string>
+ <string name="settings.servers_title">Серверы</string>
+ <string name="settings.server_name">Название</string>
+ <string name="settings.server_address">Адрес сервера</string>
+ <string name="settings.server_username">Имя пользователя</string>
+ <string name="settings.server_password">Пароль</string>
+ <string name="settings.cache_title">Кэш музыки</string>
+ <string name="settings.cache_size">Размер кэша (Мб)</string>
+ <string name="settings.cache_location">Путь кэша</string>
+ <string name="settings.cache_location_error">Некорректный путь. Используем путь по умолчанию.</string>
+ <string name="settings.testing_connection">Проверка соединения...</string>
+ <string name="settings.testing_ok">Подключение прошло успешно!</string>
+ <string name="settings.testing_unlicensed">Подключение прошло успешно. Сервер нелицензирован.</string>
+ <string name="settings.connection_failure">Не удалось подключиться.</string>
+ <string name="settings.invalid_url">Пожалуйста, укажите правильный адрес</string>
+ <string name="settings.invalid_username">Пожалуйста, укажите правильное имя пользователя (не должно быть пробелов в конце)</string>
+ <string name="settings.appearance_title">Внешний вид</string>
+ <string name="settings.theme_title">Тема</string>
+ <string name="settings.theme_light">Светлая</string>
+ <string name="settings.theme_dark">Темная</string>
+ <string name="settings.theme_holo">Holo</string>
+ <string name="settings.network_title">Сеть</string>
+ <string name="settings.max_bitrate_wifi">Макс. битрейт аудио по Wi-Fi</string>
+ <string name="settings.max_bitrate_mobile">Макс. битрейт видео по сети</string>
+ <string name="settings.max_bitrate_32">32 Kbps</string>
+ <string name="settings.max_bitrate_64">64 Kbps</string>
+ <string name="settings.max_bitrate_80">80 Kbps</string>
+ <string name="settings.max_bitrate_96">96 Kbps</string>
+ <string name="settings.max_bitrate_112">112 Kbps</string>
+ <string name="settings.max_bitrate_128">128 Kbps</string>
+ <string name="settings.max_bitrate_160">160 Kbps</string>
+ <string name="settings.max_bitrate_192">192 Kbps</string>
+ <string name="settings.max_bitrate_256">256 Kbps</string>
+ <string name="settings.max_bitrate_320">320 Kbps</string>
+ <string name="settings.max_video_bitrate_wifi">Макс. битрейт видео по Wi-Fi</string>
+ <string name="settings.max_video_bitrate_mobile">Макс. битрейт видео по сети</string>
+ <string name="settings.max_video_bitrate_200">200 Kbps</string>
+ <string name="settings.max_video_bitrate_300">300 Kbps</string>
+ <string name="settings.max_video_bitrate_400">400 Kbps</string>
+ <string name="settings.max_video_bitrate_500">500 Kbps</string>
+ <string name="settings.max_video_bitrate_700">700 Kbps</string>
+ <string name="settings.max_video_bitrate_1000">1000 Kbps</string>
+ <string name="settings.max_video_bitrate_1500">1500 Kbps</string>
+ <string name="settings.max_video_bitrate_2000">2000 Kbps</string>
+ <string name="settings.max_video_bitrate_3000">3000 Kbps</string>
+ <string name="settings.max_video_bitrate_5000">5000 Kbps</string>
+ <string name="settings.max_bitrate_unlimited">Неограничен</string>
+ <string name="settings.wifi_required_title">Поток по Wi-Fi</string>
+ <string name="settings.wifi_required_summary">Потокое воспроизведение будет работать только при подключении через Wi-Fi</string>
+ <string name="settings.network_timeout_title">Таймаут сети</string>
+ <string name="settings.network_timeout_10000">10 секунд</string>
+ <string name="settings.network_timeout_15000">15 секунд</string>
+ <string name="settings.network_timeout_30000">30 секунд</string>
+ <string name="settings.network_timeout_45000">45 секунд</string>
+ <string name="settings.network_timeout_60000">60 секунд</string>
+ <string name="settings.preload_0">0 композиция</string>
+ <string name="settings.preload_1">1 композиция</string>
+ <string name="settings.preload_2">2 композиции</string>
+ <string name="settings.preload_3">3 композиции</string>
+ <string name="settings.preload_5">5 композиций</string>
+ <string name="settings.preload_10">10 композиций</string>
+ <string name="settings.preload_unlimited">Неограничено</string>
+ <string name="settings.clear_search_history">Очистить историю поиска</string>
+ <string name="settings.search_history_cleared">История поиска очищена</string>
+ <string name="settings.other_title">Другие настройки</string>
+ <string name="settings.scrobble_title">Скробблинг на Last.fm</string>
+ <string name="settings.scrobble_summary">Не забудьте установить логин и пароль от Last.fm на сервере DSub</string>
+ <string name="settings.hide_media_title">Прятать от других</string>
+ <string name="settings.hide_media_summary">Прятать музыкальные файлы от других приложений</string>
+ <string name="settings.hide_media_toast">Изменения вступят в силу при следующем поиске музыки на Вашем устройстве.</string>
+ <string name="settings.media_button_title">Кнопки управления</string>
+ <string name="settings.media_button_summary">Разрешить управление кнопками мультимедиа на устройстве и гарнитуре</string>
+ <string name="settings.screen_lit_title">Держать экран включенным</string>
+ <string name="settings.screen_lit_summary">Оставить экран включенным для повышения скорости при скачивании.</string>
+ <string name="settings.playlist_title">Списки воспроизведения</string>
+ <string name="settings.playlist_random_size_title">Размер случайного списка</string>
+ <string name="settings.sleep_timer_title">Таймер сна</string>
+ <string name="settings.sleep_timer_duration_title">Продолжительность таймера сна</string>
+ <string name="settings.sleep_timer_off">Выключен</string>
+ <string name="settings.sleep_timer_on">Включен</string>
+ <string name="settings.sleep_timer_always_on">Всегда включен</string>
+ <string name="settings.temp_loss_title">Временная потеря связи</string>
+ <string name="settings.temp_loss_pause">Всегда останавливать</string>
+ <string name="settings.temp_loss_pause_lower">Останавливать, понижать громкость, если требуется</string>
+ <string name="settings.temp_loss_lower">Всегда понижать громкость</string>
+ <string name="settings.temp_loss_nothing">Ничего не делать</string>
+
+ <string name="shuffle.startYear">Год начала:</string>
+ <string name="shuffle.endYear">Год окончания:</string>
+ <string name="shuffle.genre">Жанр:</string>
+
+ <string name="music_service.retry">Ошибка подключения. Попытка %1$d из %2$d.</string>
+
+ <string name="background_task.wait">Пожалуйста, подождите...</string>
+ <string name="background_task.loading">Загрузка</string>
+ <string name="background_task.no_network">Эта программа требует доступ к сети. Пожалуйста, включите Wi-Fi или мобильный интернет</string>
+ <string name="background_task.network_error">Ошибка сети. Пожалуйста, проверьте адрес сервера и попробуйте снова</string>
+ <string name="background_task.not_found">Ресурс не найден. Пожалуйста, проверьте адрес сервера</string>
+ <string name="background_task.parse_error">Неизвестный ответ. Пожалуйста, проверьте адрес сервера</string>
+
+ <string name="service.connecting">Подключение к серверу. Пожалуйста, подождите.</string>
+
+ <string name="parser.upgrade_client">Несовместимые версии. Пожалуйста, обновите приложение DSub для Android.</string>
+ <string name="parser.upgrade_server">Несовместимые версии. Пожалуйста, обновите сервер Subsonic.</string>
+ <string name="parser.not_authenticated">Неправильное имя пользователя или пароль.</string>
+ <string name="parser.not_authorized">Не авторизирован. Проверьте права пользователя на сервере Subsonic.</string>
+ <string name="parser.artist_count">Получено %d исполнителей.</string>
+
+ <string name="select_artist.refresh">Обновить</string>
+ <string name="select_artist.folder">Выбрать папку</string>
+ <string name="select_artist.all_folders">Все папки</string>
+
+ <string name="equalizer.label">Эквалайзер</string>
+ <string name="equalizer.enabled">Включен</string>
+ <string name="equalizer.preset">Готовые настройки</string>
+
+ <string name="widget.initial_text">Коснитесь для выбора музыки</string>
+ <string name="widget.sdcard_busy">SD карта недоступна</string>
+ <string name="widget.sdcard_missing">Нет SD карты</string>
+
+ <string name="util.bytes_format.gigabyte">0.00 ГБ</string>
+ <string name="util.bytes_format.megabyte">0.00 МБ</string>
+ <string name="util.bytes_format.kilobyte">0 КБ</string>
+ <string name="util.bytes_format.byte">0 Б</string>
+
+ <string name="button_bar.chat">Чат</string>
+ <string name="main.back_confirm">Нажмите "назад" еще раз для выхода</string>
+ <string name="download.playing_out_of">Воспроизведение: %1$d/%2$d</string>
+ <string name="settings.persistent_title">Постоянное уведомление</string>
+ <string name="settings.persistent_summary">Показывать уведомление даже во время паузы. Остановка воспроизведения уберет это уведомление.</string>
+ <string name="settings.gapless_playback">Непрерывное воспроизведение</string>
+ <string name="settings.gapless_playback_summary">Galaxy S3 может зависать или испытывать прочие трудности с момента начала непрерывного воспроизведения. Выключите эту функцию для исправления данной проблемы.</string>
+ <string name="settings.chat_refresh">Частота обновления чата (сек)</string>
+ <string name="settings.chat_enabled">Чат активен</string>
+ <string name="settings.chat_enabled_summary">Показывать или нет вкладку чата</string>
+ <string name="changelog_full_title">Журнал изменений</string>
+ <string name="changelog_title">Что нового</string>
+ <string name="changelog_ok_button">OK</string>
+ <string name="changelog_show_full">Еще…</string>
+ <string name="chat.send_a_message">Отправить сообщение</string>
+
+
+ <plurals name="select_album_n_songs">
+ <item quantity="zero">Нет композиций</item>
+ <item quantity="one">1 композиция</item>
+ <item quantity="other">%d композиций</item>
+ </plurals>
+ <plurals name="select_album_n_songs_downloading">
+ <item quantity="one">1 композиция запланирована для скачивания</item>
+ <item quantity="other">%d композиций запланировано для скачивания</item>
+ </plurals>
+ <plurals name="select_album_n_songs_added">
+ <item quantity="one">1 композиция добавлена в очередь воспроизведения</item>
+ <item quantity="other">%d композиций добавлено в очередь воспроизведения</item>
+ </plurals>
+ <plurals name="select_album_donate_dialog_n_trial_days_left">
+ <item quantity="one">1 день до конца пробного периода</item>
+ <item quantity="other">%d дней до конца пробного периода</item>
+ </plurals>
+
+</resources>
diff --git a/app/src/main/res/values-v11/colors.xml b/app/src/main/res/values-v11/colors.xml
new file mode 100644
index 00000000..f5a422bb
--- /dev/null
+++ b/app/src/main/res/values-v11/colors.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="notificationArtist">#bababa</color>
+ <color name="notificationTitle">#dddddd</color>
+</resources>
diff --git a/app/src/main/res/values-v11/styles.xml b/app/src/main/res/values-v11/styles.xml
new file mode 100644
index 00000000..9a7cb2b2
--- /dev/null
+++ b/app/src/main/res/values-v11/styles.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <style name="BasicButton">
+ <item name="android:background">?android:selectableItemBackground</item>
+ </style>
+</resources> \ No newline at end of file
diff --git a/app/src/main/res/values-v16/themes.xml b/app/src/main/res/values-v16/themes.xml
new file mode 100644
index 00000000..013ac0aa
--- /dev/null
+++ b/app/src/main/res/values-v16/themes.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <style name="DSub.TextViewStyle" parent="android:Widget.TextView">
+ <item name="android:fontFamily">sans-serif-light</item>
+ </style>
+
+ <style name="DSub.TextViewStyle.Bold" parent="android:Widget.TextView">
+ <item name="android:fontFamily">sans-serif</item>
+ <item name="android:textStyle">bold</item>
+ </style>
+
+ <style name="DSub.ButtonStyle" parent="android:Widget.Holo.Button">
+ <item name="android:fontFamily">sans-serif-light</item>
+ </style>
+</resources> \ No newline at end of file
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
new file mode 100644
index 00000000..37b15d12
--- /dev/null
+++ b/app/src/main/res/values/arrays.xml
@@ -0,0 +1,264 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string-array name="drawerItems">
+ <item>@string/button_bar.home</item>
+ <item>@string/button_bar.browse</item>
+ <item>@string/button_bar.playlists</item>
+ <item>@string/button_bar.podcasts</item>
+ <item>@string/button_bar.bookmarks</item>
+ <item>@string/button_bar.shares</item>
+ <item>@string/button_bar.chat</item>
+ <item>@string/button_bar.admin</item>
+ <item>@string/button_bar.downloading</item>
+ <item>@string/menu.settings</item>
+ </string-array>
+
+ <string-array name="drawerItemsDescriptions">
+ <item>Home</item>
+ <item>Artist</item>
+ <item>Playlist</item>
+ <item>Podcast</item>
+ <item>Bookmark</item>
+ <item>Share</item>
+ <item>Chat</item>
+ <item>Admin</item>
+ <item>Download</item>
+ <item>Settings</item>
+ </string-array>
+
+ <string-array name="defaultDrawerItems">
+ <item>@string/button_bar.home</item>
+ <item>@string/button_bar.browse</item>
+ <item>@string/button_bar.playlists</item>
+ <item>@string/button_bar.podcasts</item>
+ <item>@string/button_bar.bookmarks</item>
+ <item>@string/button_bar.shares</item>
+ <item>@string/button_bar.chat</item>
+ </string-array>
+
+ <string-array name="defaultDrawerItemsDescriptions">
+ <item>Home</item>
+ <item>Artist</item>
+ <item>Playlist</item>
+ <item>Podcast</item>
+ <item>Bookmark</item>
+ <item>Share</item>
+ <item>Chat</item>
+ </string-array>
+
+ <array name="drawerItemIconsLight">
+ <item>@drawable/main_offline_light</item>
+ <item>@drawable/ic_menu_library_light</item>
+ <item>@drawable/ic_menu_playlist_light</item>
+ <item>@drawable/ic_menu_podcast_light</item>
+ <item>@drawable/ic_menu_bookmark_light</item>
+ <item>@drawable/ic_menu_share_light</item>
+ <item>@drawable/ic_menu_chat_light</item>
+ <item>@drawable/ic_menu_admin_light</item>
+ <item>@drawable/ic_menu_download_light</item>
+ <item>@drawable/ic_menu_settings_light</item>
+ </array>
+
+ <array name="drawerItemIconsDark">
+ <item>@drawable/main_offline_dark</item>
+ <item>@drawable/ic_menu_library_dark</item>
+ <item>@drawable/ic_menu_playlist_dark</item>
+ <item>@drawable/ic_menu_podcast_dark</item>
+ <item>@drawable/ic_menu_bookmark_dark</item>
+ <item>@drawable/ic_menu_share_dark</item>
+ <item>@drawable/ic_menu_chat_dark</item>
+ <item>@drawable/ic_menu_admin_dark</item>
+ <item>@drawable/ic_menu_download_dark</item>
+ <item>@drawable/ic_menu_settings_dark</item>
+ </array>
+
+ <string-array name="themeValues">
+ <item>light</item>
+ <item>dark</item>
+ <item>black</item>
+ <item>holo</item>
+ </string-array>
+
+ <string-array name="themeNames">
+ <item>@string/settings.theme_light</item>
+ <item>@string/settings.theme_dark</item>
+ <item>@string/settings.theme_black</item>
+ <item>@string/settings.theme_holo</item>
+ </string-array>
+
+ <string-array name="sleepTimerValues">
+ <item>0</item>
+ <item>1</item>
+ <item>2</item>
+ </string-array>
+
+ <string-array name="sleepTimerNames">
+ <item>@string/settings.sleep_timer_off</item>
+ <item>@string/settings.sleep_timer_on</item>
+ <item>@string/settings.sleep_timer_always_on</item>
+ </string-array>
+
+ <string-array name="preloadCountValues">
+ <item>0</item>
+ <item>1</item>
+ <item>2</item>
+ <item>3</item>
+ <item>5</item>
+ <item>10</item>
+ <item>-1</item>
+ </string-array>
+
+ <string-array name="preloadCountNames">
+ <item>@string/settings.preload_0</item>
+ <item>@string/settings.preload_1</item>
+ <item>@string/settings.preload_2</item>
+ <item>@string/settings.preload_3</item>
+ <item>@string/settings.preload_5</item>
+ <item>@string/settings.preload_10</item>
+ <item>@string/settings.preload_unlimited</item>
+ </string-array>
+
+ <string-array name="maxBitrateValues">
+ <item>32</item>
+ <item>64</item>
+ <item>80</item>
+ <item>96</item>
+ <item>112</item>
+ <item>128</item>
+ <item>160</item>
+ <item>192</item>
+ <item>256</item>
+ <item>320</item>
+ <item>0</item>
+ </string-array>
+
+ <string-array name="maxBitrateNames">
+ <item>@string/settings.max_bitrate_32</item>
+ <item>@string/settings.max_bitrate_64</item>
+ <item>@string/settings.max_bitrate_80</item>
+ <item>@string/settings.max_bitrate_96</item>
+ <item>@string/settings.max_bitrate_112</item>
+ <item>@string/settings.max_bitrate_128</item>
+ <item>@string/settings.max_bitrate_160</item>
+ <item>@string/settings.max_bitrate_192</item>
+ <item>@string/settings.max_bitrate_256</item>
+ <item>@string/settings.max_bitrate_320</item>
+ <item>@string/settings.max_bitrate_unlimited</item>
+ </string-array>
+
+ <string-array name="maxVideoBitrateValues">
+ <item>200</item>
+ <item>300</item>
+ <item>400</item>
+ <item>500</item>
+ <item>700</item>
+ <item>1000</item>
+ <item>1500</item>
+ <item>2000</item>
+ <item>3000</item>
+ <item>5000</item>
+ <item>0</item>
+ </string-array>
+
+ <string-array name="maxVideoBitrateNames">
+ <item>@string/settings.max_video_bitrate_200</item>
+ <item>@string/settings.max_video_bitrate_300</item>
+ <item>@string/settings.max_video_bitrate_400</item>
+ <item>@string/settings.max_video_bitrate_500</item>
+ <item>@string/settings.max_video_bitrate_700</item>
+ <item>@string/settings.max_video_bitrate_1000</item>
+ <item>@string/settings.max_video_bitrate_1500</item>
+ <item>@string/settings.max_video_bitrate_2000</item>
+ <item>@string/settings.max_video_bitrate_3000</item>
+ <item>@string/settings.max_video_bitrate_5000</item>
+ <item>@string/settings.max_bitrate_unlimited</item>
+ </string-array>
+
+ <string-array name="networkTimeoutValues">
+ <item>10000</item>
+ <item>15000</item>
+ <item>30000</item>
+ <item>45000</item>
+ <item>60000</item>
+ </string-array>
+ <string-array name="networkTimeoutNames">
+ <item>@string/settings.network_timeout_10000</item>
+ <item>@string/settings.network_timeout_15000</item>
+ <item>@string/settings.network_timeout_30000</item>
+ <item>@string/settings.network_timeout_45000</item>
+ <item>@string/settings.network_timeout_60000</item>
+ </string-array>
+
+ <string-array name="tempLossValues">
+ <item>0</item>
+ <item>1</item>
+ <item>2</item>
+ <item>3</item>
+ </string-array>
+ <string-array name="tempLossNames">
+ <item>@string/settings.temp_loss_pause</item>
+ <item>@string/settings.temp_loss_pause_lower</item>
+ <item>@string/settings.temp_loss_lower</item>
+ <item>@string/settings.temp_loss_nothing</item>
+ </string-array>
+
+ <string-array name="disconnectPauseValues">
+ <item>0</item>
+ <item>3</item>
+ </string-array>
+ <string-array name="disconnectPauseNames">
+ <item>@string/settings.disconnect_pause_both</item>
+ <item>@string/settings.disconnect_pause_neither</item>
+ </string-array>
+
+ <string-array name="videoPlayerValues">
+ <item>raw</item>
+ <item>hls</item>
+ <item>transcode</item>
+ <item>flash</item>
+ </string-array>
+ <string-array name="videoPlayerNames">
+ <item>@string/settings.video_raw</item>
+ <item>@string/settings.video_hls</item>
+ <item>@string/settings.video_transcode</item>
+ <item>@string/settings.video_flash</item>
+ </string-array>
+
+ <string-array name="syncIntervalValues">
+ <item>15</item>
+ <item>30</item>
+ <item>60</item>
+ <item>120</item>
+ <item>240</item>
+ <item>360</item>
+ <item>720</item>
+ <item>1440</item>
+ </string-array>
+ <string-array name="syncIntervalNames">
+ <item>@string/settings.sync_interval_15</item>
+ <item>@string/settings.sync_interval_30</item>
+ <item>@string/settings.sync_interval_60</item>
+ <item>@string/settings.sync_interval_120</item>
+ <item>@string/settings.sync_interval_240</item>
+ <item>@string/settings.sync_interval_360</item>
+ <item>@string/settings.sync_interval_720</item>
+ <item>@string/settings.sync_interval_1440</item>
+ </string-array>
+
+ <string-array name="replayGainTypeValues">
+ <item>1</item>
+ <item>2</item>
+ <item>3</item>
+ </string-array>
+ <string-array name="replayGainTypeNames">
+ <item>@string/settings.replay_gain_type.smart</item>
+ <item>@string/settings.replay_gain_type.album</item>
+ <item>@string/settings.replay_gain_type.track</item>
+ </string-array>
+
+ <string-array name="editServerOptions">
+ <item>@string/tasker.edit_do_nothing</item>
+ <item>@string/main.online</item>
+ <item>@string/main.offline</item>
+ </string-array>
+</resources>
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
new file mode 100644
index 00000000..9667117c
--- /dev/null
+++ b/app/src/main/res/values/attrs.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <attr name="offline_icon" format="reference"/>
+ <attr name="media_button_backward" format="reference"/>
+ <attr name="media_button_forward" format="reference"/>
+ <attr name="media_button_pause" format="reference"/>
+ <attr name="media_button_repeat_off" format="reference"/>
+ <attr name="media_button_start" format="reference"/>
+ <attr name="media_button_stop" format="reference"/>
+ <attr name="chat_send" format="reference"/>
+ <attr name="add" format="reference"/>
+ <attr name="download_none" format="reference"/>
+ <attr name="shuffle" format="reference"/>
+ <attr name="refresh" format="reference"/>
+ <attr name="search" format="reference"/>
+ <attr name="remove" format="reference"/>
+ <attr name="save" format="reference"/>
+ <attr name="volume" format="reference"/>
+ <attr name="toggle_list" format="reference"/>
+ <attr name="select_server" format="reference"/>
+ <attr name="downloading" format="reference"/>
+ <attr name="bookmark" format="reference"/>
+ <attr name="share" format="reference"/>
+ <attr name="add_person" format="reference"/>
+ <attr name="password" format="reference"/>
+ <attr name="rating_bad" format="reference"/>
+ <attr name="rating_good" format="reference"/>
+ <attr name="radio" format="reference"/>
+ <attr name="drawerItemsIcons" format="reference"/>
+
+ <declare-styleable name="SeekBarPreference">
+ <attr name="min" format="integer"/>
+ <attr name="max" format="integer"/>
+ <attr name="stepSize" format="float"/>
+ <attr name="display" format="string"/>
+ </declare-styleable>
+</resources>
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 00000000..b1422ad6
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="lightBackground">#F1F0E6</color>
+ <color name="dividerColor">#FF33B5E5</color>
+ <color name="appwidget_text">#FFFFFF</color>
+ <color name="notificationArtist">#434343</color>
+ <color name="notificationTitle">#000000</color>
+ <color name="background_holo_light">#ff33b5e5</color>
+ <color name="overlayColor">#80000000</color>
+ <color name="ics_opaque">#8033b5e5</color>
+ <color name="cyan">#ff0099cc</color>
+
+ <color name="holo_blue_light">#ff33b5e5</color>
+ <color name="holo_orange_light">#ffffbb33</color>
+ <color name="holo_green_light">#ff99cc00</color>
+ <color name="holo_red_light">#ffff4444</color>
+</resources> \ No newline at end of file
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
new file mode 100644
index 00000000..be3e843d
--- /dev/null
+++ b/app/src/main/res/values/dimens.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="Button">54dip</dimen>
+ <dimen name="Button.Small">46dip</dimen>
+ <dimen name="AlbumArt.Small">78dip</dimen>
+ <dimen name="AlbumArt.Header">120dip</dimen>
+</resources> \ No newline at end of file
diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml
new file mode 100644
index 00000000..edb3bbec
--- /dev/null
+++ b/app/src/main/res/values/ids.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources>
+ <item name="drag_handle" type="id"/>
+</resources> \ No newline at end of file
diff --git a/app/src/main/res/values/integers.xml b/app/src/main/res/values/integers.xml
new file mode 100644
index 00000000..57371a11
--- /dev/null
+++ b/app/src/main/res/values/integers.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <integer name="Grid.Columns">2</integer>
+ <integer name="TextDescriptionLength">5</integer>
+</resources> \ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 00000000..b897cad6
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,607 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="common.appname">DSub</string>
+ <string name="common.ok">OK</string>
+ <string name="common.save">Save</string>
+ <string name="common.cancel">Cancel</string>
+ <string name="common.play_now">Play Now</string>
+ <string name="common.play_shuffled">Play Shuffled</string>
+ <string name="common.play_next">Play Next</string>
+ <string name="common.play_last">Play Last</string>
+ <string name="common.download">Cache</string>
+ <string name="common.pin">Permanent Cache</string>
+ <string name="common.delete">Delete</string>
+ <string name="common.star">Star</string>
+ <string name="common.unstar">Unstar</string>
+ <string name="common.info">Details</string>
+ <string name="common.name">Name</string>
+ <string name="common.comment">Comment</string>
+ <string name="common.public">Public</string>
+ <string name="common.play_external">Play Video</string>
+ <string name="common.stream_external">Stream Video</string>
+ <string name="common.confirm">Confirm</string>
+ <string name="common.confirm_message">Do you want to %1$s %2$s?</string>
+ <string name="common.confirm_message_cache">cache</string>
+ <string name="common.empty">No data</string>
+ <string name="common.warning">Warning</string>
+
+ <string name="button_bar.home">Home</string>
+ <string name="button_bar.browse">Library</string>
+ <string name="button_bar.search">Search</string>
+ <string name="button_bar.playlists">Playlists</string>
+ <string name="button_bar.now_playing">Now Playing</string>
+ <string name="button_bar.podcasts">Podcasts</string>
+ <string name="button_bar.bookmarks">Bookmarks</string>
+ <string name="button_bar.shares">Shares</string>
+ <string name="button_bar.chat">Chat</string>
+ <string name="button_bar.admin">Admin</string>
+ <string name="button_bar.downloading">Downloading</string>
+
+ <string name="main.welcome_title">Welcome!</string>
+ <string name="main.welcome_text">Welcome to DSub! The app is currently configured to use the Subsonic demo server. After you\'ve
+ set up your personal server (available from <b>subsonic.org</b>), please go to <b>Settings</b> and change the configuration to connect to it.</string>
+ <string name="main.about_title">About DSub</string>
+ <string name="main.about_text">Author: Scott Jackson
+ \nEmail: dsub.android@gmail.com
+ \nVersion: %1$s
+ \nFiles Cached: %2$s
+ \nUsed Space: %3$s of %4$s
+ \nAvailable Space: %5$s of %6$s</string>
+ <string name="main.faq_title">FAQ</string>
+ <string name="main.faq_text">
+ <![CDATA[
+ <font color="red">Cache vs Permanent Cache</font>:
+ <br/>When songs are downloaded by DSub, they can be deleted to make room for new downloads. Permanently cached music on the other hand will never be automatically deleted.
+ <p/><font color="red">ChromeCast fails</font>:
+ <br/>Try setting the option Settings -> Playback -> Use device proxy. It is a work around for Chromecast not accepting self-signed certificates.
+ <p/><font color="red">First level in Library are actually groups of artists</font>:
+ <br/>In the option menu, deselect "First level artists". This will make it so that the entire first level of directories shown will be treated like groups of artists instead of the artists themselves.
+ ]]>
+ </string>
+ <string name="main.select_server">Select server</string>
+ <string name="main.shuffle">Shuffle play</string>
+ <string name="main.offline">Go Offline</string>
+ <string name="main.online">Go Online</string>
+ <string name="main.settings">Settings</string>
+ <string name="main.albums_title">Album Lists</string>
+ <string name="main.albums_per_folder">Per Folder</string>
+ <string name="main.albums_newest">Recently added</string>
+ <string name="main.albums_recent">Recently played</string>
+ <string name="main.albums_frequent">Most played</string>
+ <string name="main.albums_highest">Top rated</string>
+ <string name="main.albums_starred">Starred</string>
+ <string name="main.albums_random">Random</string>
+ <string name="main.albums_genres">Genres</string>
+ <string name="main.albums_year">Decades</string>
+ <string name="main.albums_alphabetical">Alphabetically</string>
+ <string name="main.videos">Videos</string>
+ <string name="main.songs_genres">@string/main.albums_genres</string>
+ <string name="main.back_confirm">Press back again to exit</string>
+ <string name="main.scan_complete">Completed scan of Server</string>
+
+ <string name="menu.search">Search</string>
+ <string name="menu.shuffle">Shuffle</string>
+ <string name="menu.refresh">Refresh</string>
+ <string name="menu.play">Play</string>
+ <string name="menu.play_last">Play Last</string>
+ <string name="menu.exit">Exit</string>
+ <string name="menu.settings">Settings</string>
+ <string name="menu.help">Help</string>
+ <string name="menu.about">About</string>
+ <string name="menu.add_playlist">Add To Playlist</string>
+ <string name="menu.remove_playlist">Remove From Playlist</string>
+ <string name="menu.deleted_playlist">Deleted playlist %s</string>
+ <string name="menu.deleted_playlist_error">Failed to delete playlist %s</string>
+ <string name="menu.log">Send Log</string>
+ <string name="menu.set_timer">Set Timer</string>
+ <string name="menu.check_podcasts">Check For New Episodes</string>
+ <string name="menu.add_podcast">Add Channel</string>
+ <string name="menu.keep_synced">Keep Synced</string>
+ <string name="menu.stop_sync">Stop syncing</string>
+ <string name="menu.show_all">Show all media</string>
+ <string name="menu.show_artist">Show Artist</string>
+ <string name="menu.share">Share</string>
+ <string name="menu.delete_cache">Delete Cache</string>
+ <string name="menu.cast">Cast To Device</string>
+ <string name="menu.faq">FAQ</string>
+ <string name="menu.add_user">Add User</string>
+ <string name="menu.rescan">Rescan Server</string>
+ <string name="menu.rate">Set Rating</string>
+ <string name="menu.top_tracks">Last.FM Top Tracks</string>
+ <string name="menu.similar_artists">Similar Artists</string>
+ <string name="menu.show_missing">Show missing</string>
+ <string name="menu.start_radio">Start Radio</string>
+ <string name="menu.first_level_artist">First level artists</string>
+
+ <string name="playlist.label">Playlists</string>
+ <string name="playlist.update_info">Update Information</string>
+ <string name="playlist.updated_info">Updated playlist information for %s</string>
+ <string name="playlist.updated_info_error">Failed to update playlist information for %s</string>
+ <string name="playlist.overwrite">Overwrite existing playlist</string>
+ <string name="playlist.add_to">Add to Playlist</string>
+ <string name="playlist.create_new">Create New</string>
+ <string name="playlist.delete">Delete Playlist</string>
+
+ <string name="search.label">Search</string>
+ <string name="search.title">Search</string>
+ <string name="search.search">Click to search</string>
+ <string name="search.no_match">No matches, please try again</string>
+ <string name="search.artists">Artists</string>
+ <string name="search.albums">Albums</string>
+ <string name="search.songs">Songs</string>
+ <string name="search.more">Show more</string>
+
+ <string name="progress.wait">Please wait...</string>
+ <string name="progress.artist_info">Loading Artist Bio</string>
+
+ <string name="music_library.label">Media library</string>
+ <string name="music_library.label_offline">Offline media</string>
+
+ <string name="select_album.select">Select all</string>
+ <string name="select_album.n_selected">%d tracks selected.</string>
+ <string name="select_album.n_unselected">%d tracks unselected.</string>
+ <string name="select_album.more">More</string>
+ <string name="select_album.offline">Offline</string>
+ <string name="select_album.searching">Searching...</string>
+ <string name="select_album.no_sdcard">Error: No SD card available.</string>
+ <string name="select_album.no_network">Warning: No network available.</string>
+ <string name="select_album.no_room">Warning: you only have %s left</string>
+ <string name="select_album.not_licensed">Server not licensed. %d trial days left.</string>
+ <string name="select_album.donate_dialog_message">Get unlimited downloads by donating to Subsonic.</string>
+ <string name="select_album.donate_dialog_now">Now</string>
+ <string name="select_album.donate_dialog_later">Later</string>
+ <string name="select_album.donate_dialog_0_trial_days_left">Trial period is over</string>
+
+ <string name="offline.sync_dialog_title">Offline songs waiting to be synced</string>
+ <string name="offline.sync_dialog_message">Process %1$d offline scrobbles?
+ \nProcess %2$d offline stars?
+ </string>
+ <string name="offline.sync_dialog_default">Use action as default</string>
+ <string name="offline.sync_success">Successfully synced %1$d songs</string>
+ <string name="offline.sync_partial">Successfully synced %1$d of %2$d songs</string>
+ <string name="offline.sync_error">Failed to sync songs</string>
+
+ <string name="select_genre.blank">Blank</string>
+ <string name="select_genre.songs">%d songs</string>
+ <string name="select_genre.albums">%d albums</string>
+
+ <string name="select_podcasts.error">This podcast had an error while downloading on the server. The server must download it first.</string>
+ <string name="select_podcasts.skipped">This podcast has not been downloaded on the server. The server must download it first.</string>
+ <string name="select_podcasts.initializing">This podcast channel is being initialized on the server. Please reload after a moment.</string>
+ <string name="select_podcasts.server_download">Download on server</string>
+ <string name="select_podcasts.server_delete">Delete from server</string>
+ <string name="select_podcasts.downloading">Now downloading %s on the server</string>
+ <string name="select_podcasts.refreshing">The server is checking for new podcasts now</string>
+ <string name="select_podcasts.deleted">Deleted podcast %s</string>
+ <string name="select_podcasts.deleted_error">Failed to delete podcast %s</string>
+ <string name="select_podcasts.add_url">URL:</string>
+ <string name="select_podcasts.created_error">Failed to add podcast</string>
+ <string name="select_podcasts.invalid_podcast_channel">Invalid podcast channel: %s</string>
+ <string name="select_podcasts.delete">Delete podcast</string>
+
+ <string name="download.empty">Playlist is empty</string>
+ <string name="download.shuffle_loading">Shuffle list is loading...</string>
+ <string name="download.playerstate_downloading">Downloading - %s</string>
+ <string name="download.playerstate_buffering">Buffering</string>
+ <string name="download.playerstate_playing_shuffle">Playing shuffle</string>
+ <string name="download.menu_show_album">Show Album</string>
+ <string name="download.menu_lyrics">Lyrics</string>
+ <string name="download.menu_remove">Remove from queue</string>
+ <string name="download.menu_remove_all">Remove all</string>
+ <string name="download.menu_screen_on">Screen on</string>
+ <string name="download.menu_shuffle">Shuffle</string>
+ <string name="download.menu_toggle">Toggle</string>
+ <string name="download.menu_save">Save playlist</string>
+ <string name="download.menu_shuffle_notification">Playlist was shuffled</string>
+ <string name="download.menu_remove_played_songs">Remove played songs</string>
+ <string name="download.playlist_title">Save playlist</string>
+ <string name="download.playlist_name">Enter the playlist name:</string>
+ <string name="download.playlist_saving">Saving playlist \"%s\"...</string>
+ <string name="download.playlist_done">Playlist was successfully saved.</string>
+ <string name="download.playlist_error">Failed to save playlist, please try later.</string>
+ <string name="download.repeat_off">Repeat off</string>
+ <string name="download.repeat_all">Repeat all</string>
+ <string name="download.repeat_single">Repeat song</string>
+ <string name="download.jukebox_on">Turned on remote control. Music is played on the computer.</string>
+ <string name="download.jukebox_off">Turned off remote control. Music is played on the phone.</string>
+ <string name="download.jukebox_volume">Remote volume</string>
+ <string name="download.jukebox_server_too_old">Remote control is not supported. Please upgrade your Subsonic server.</string>
+ <string name="download.jukebox_offline">Remote control is not available in offline mode.</string>
+ <string name="download.jukebox_not_authorized">Remote control is not allowed. Please enable jukebox mode in <b>Users &gt; Settings</b> on your Subsonic server.</string>
+ <string name="download.timer_length">Timer:</string>
+ <string name="download.start_timer">Start Timer</string>
+ <string name="download.stop_timer">Stop Timer</string>
+ <string name="download.need_download">Video needs to be downloaded first</string>
+ <string name="download.no_streaming_player">No player can play this stream</string>
+ <string name="download.playing_out_of">Playing: %1$d/%2$d</string>
+ <string name="download.save_bookmark_title">Create bookmark</string>
+ <string name="download.save_bookmark">Bookmark created</string>
+ <string name="download.save_bookmark_failed">Failed to create bookmark</string>
+ <string name="download.downloading_title">Downloading %1$d songs</string>
+ <string name="download.downloading_summary">Current: %1$s</string>
+ <string name="download.downloading_summary_expanded">Current: %1$s
+ \nEstimated Size: %2$s</string>
+ <string name="download.failed_to_load">Failed to load</string>
+ <string name="download.restore_play_queue">continue from where you left off on another device at</string>
+
+ <string name="sync.new_podcasts">New podcasts available</string>
+ <string name="sync.new_playlists">New songs in playlists</string>
+ <string name="sync.new_albums">New albums available</string>
+ <string name="sync.new_starred">New starred songs available</string>
+
+ <string name="starring_content_starred">Starred \"%s\"</string>
+ <string name="starring_content_unstarred">Unstarred \"%s\"</string>
+ <string name="starring_content_error">Failed to update \"%s\", please try later.</string>
+
+ <string name="playlist_error">Failed to grab list of playlists</string>
+ <string name="updated_playlist">Added %1$s songs to \"%2$s\"</string>
+ <string name="updated_playlist_error">Failed to update \"%s\", please try later.</string>
+ <string name="removed_playlist">Removed %1$s songs from \"%2$s\"</string>
+
+ <string name="bookmark.delete">Delete bookmark</string>
+ <string name="bookmark.delete_title">Delete the bookmark for</string>
+ <string name="bookmark.deleted">Deleted the bookmark for \"%s\"</string>
+ <string name="bookmark.deleted_error">Failed to delete the bookmark for \"%s\"</string>
+ <string name="bookmark.details_title">Bookmark Details</string>
+ <string name="bookmark.details">Song: %1$s
+ \nPosition: %2$s
+ \nCreated: %3$s
+ \nLast Updated: %4$s
+ \nComment: %5$s</string>
+ <string name="bookmark.resume_title">Resume playing?</string>
+ <string name="bookmark.resume">Resume playing \'%1$s\' from %2$s</string>
+ <string name="bookmark.action_resume">Resume</string>
+ <string name="bookmark.action_start_over">Start Over</string>
+
+ <string name="rating.title">Rate \"%s\"</string>
+ <string name="rating.set_rating">Rating set for \"%s\"</string>
+ <string name="rating.set_rating_failed">Failed to set rating for \"%s\"</string>
+ <string name="rating.remove_rating">Rating removed for \"%s\"</string>
+ <string name="rating.remove_rating_failed">Failed to remove rating for \"%s\"</string>
+
+ <string name="song_details.error">Error</string>
+ <string name="song_details.skipped">Skipped</string>
+ <string name="song_details.downloading">Downloading</string>
+
+ <string name="lyrics.nomatch">No lyrics found</string>
+
+ <string name="error.label">Error</string>
+
+ <string name="settings.title">Settings</string>
+ <string name="settings.test_connection_title">Test connection</string>
+ <string name="settings.servers_add">Add Server</string>
+ <string name="settings.servers_remove">Remove Server</string>
+ <string name="settings.servers_title">Servers</string>
+ <string name="settings.server_unused">Unused</string>
+ <string name="settings.server_name">Name</string>
+ <string name="settings.server_address">Server address</string>
+ <string name="settings.server_local_network_ssid" >Local network SSID</string>
+ <string name="settings.server_local_network_ssid_hint">Current SSID: %s</string>
+ <string name="settings.server_internal_address">Local network address</string>
+ <string name="settings.server_username">Username</string>
+ <string name="settings.server_password">Password</string>
+ <string name="settings.server_open_browser">Open in browser</string>
+ <string name="settings.server_sync_summary">Whether or not syncing is enabled for this server</string>
+ <string name="settings.server_sync">Sync Enabled</string>
+ <string name="settings.cache_title">Music cache</string>
+ <string name="settings.preload_wifi">Songs to preload (Wifi)</string>
+ <string name="settings.preload_mobile">Songs to preload (Mobile)</string>
+ <string name="settings.cache_size">Cache size</string>
+ <string name="settings.cache_location">Cache location</string>
+ <string name="settings.cache_location_error">Invalid cache location. Using default.</string>
+ <string name="settings.cache_location_reset">The cache location you have set is no longer writable. If you recently upgraded your phone OS to KitKat 4.4, then the way apps write to the SD Card has changed so that they can only write to a specific location. The location that DSub uses has already been automatically changed to the correct location. In order to delete all of the old app data, you will need to mount the SD Card on your computer and delete the old folder manually</string>
+ <string name="settings.cache_clear">Clear Cache</string>
+ <string name="settings.cache_clear_complete">Finished clearing cache</string>
+ <string name="settings.testing_connection">Testing connection...</string>
+ <string name="settings.testing_ok">Connection is OK</string>
+ <string name="settings.testing_unlicensed">Connection is OK. Server unlicensed.</string>
+ <string name="settings.connection_failure">Connection failure.</string>
+ <string name="settings.invalid_url">Please specify a valid URL.</string>
+ <string name="settings.invalid_username">Please specify a valid username (no trailing spaces).</string>
+ <string name="settings.appearance_title">Appearance</string>
+ <string name="settings.theme_title">Theme</string>
+ <string name="settings.theme_light">Light</string>
+ <string name="settings.theme_dark">Dark</string>
+ <string name="settings.theme_black">Black</string>
+ <string name="settings.theme_holo">Holo</string>
+ <string name="settings.theme_fullscreen">Fullscreen</string>
+ <string name="settings.theme_fullscreen_summary">Hide as many UI elements as Android will allow</string>
+ <string name="settings.track_title">Display Track #</string>
+ <string name="settings.track_summary">Display Track # in front of songs if one exists</string>
+ <string name="settings.custom_sort">Sort By Year</string>
+ <string name="settings.custom_sort_summary">Sort albums by year, or by alphabetical</string>
+ <string name="settings.open_to_tab">Open To Tab</string>
+ <string name="settings.open_to_tab_summary">Open directly to this tab</string>
+ <string name="settings.network_title">Network</string>
+ <string name="settings.max_bitrate_wifi">Max Audio bitrate - Wi-Fi</string>
+ <string name="settings.max_bitrate_mobile">Max Audio bitrate - Mobile</string>
+ <string name="settings.max_bitrate_32">32 Kbps</string>
+ <string name="settings.max_bitrate_64">64 Kbps</string>
+ <string name="settings.max_bitrate_80">80 Kbps</string>
+ <string name="settings.max_bitrate_96">96 Kbps</string>
+ <string name="settings.max_bitrate_112">112 Kbps</string>
+ <string name="settings.max_bitrate_128">128 Kbps</string>
+ <string name="settings.max_bitrate_160">160 Kbps</string>
+ <string name="settings.max_bitrate_192">192 Kbps</string>
+ <string name="settings.max_bitrate_256">256 Kbps</string>
+ <string name="settings.max_bitrate_320">320 Kbps</string>
+ <string name="settings.max_video_bitrate_wifi">Max Video bitrate - Wi-Fi</string>
+ <string name="settings.max_video_bitrate_mobile">Max Video bitrate - Mobile</string>
+ <string name="settings.max_video_bitrate_200">200 Kbps</string>
+ <string name="settings.max_video_bitrate_300">300 Kbps</string>
+ <string name="settings.max_video_bitrate_400">400 Kbps</string>
+ <string name="settings.max_video_bitrate_500">500 Kbps</string>
+ <string name="settings.max_video_bitrate_700">700 Kbps</string>
+ <string name="settings.max_video_bitrate_1000">1000 Kbps</string>
+ <string name="settings.max_video_bitrate_1500">1500 Kbps</string>
+ <string name="settings.max_video_bitrate_2000">2000 Kbps</string>
+ <string name="settings.max_video_bitrate_3000">3000 Kbps</string>
+ <string name="settings.max_video_bitrate_5000">5000 Kbps</string>
+ <string name="settings.max_bitrate_unlimited">Unlimited</string>
+ <string name="settings.wifi_required_title">Wi-Fi streaming only</string>
+ <string name="settings.wifi_required_summary">Only stream media if connected to Wi-Fi</string>
+ <string name="settings.network_timeout_title">Network Timeout</string>
+ <string name="settings.network_timeout_10000">10 seconds</string>
+ <string name="settings.network_timeout_15000">15 seconds</string>
+ <string name="settings.network_timeout_30000">30 seconds</string>
+ <string name="settings.network_timeout_45000">45 seconds</string>
+ <string name="settings.network_timeout_60000">60 seconds</string>
+ <string name="settings.preload_0">0 song</string>
+ <string name="settings.preload_1">1 song</string>
+ <string name="settings.preload_2">2 songs</string>
+ <string name="settings.preload_3">3 songs</string>
+ <string name="settings.preload_5">5 songs</string>
+ <string name="settings.preload_10">10 songs</string>
+ <string name="settings.preload_unlimited">Unlimited</string>
+ <string name="settings.clear_search_history">Clear search history</string>
+ <string name="settings.search_history_cleared">Search history cleared</string>
+ <string name="settings.other_title">Other settings</string>
+ <string name="settings.scrobble_title">Scrobble to Last.fm</string>
+ <string name="settings.scrobble_summary">Remember to set up your Last.fm user and password on the Subsonic server</string>
+ <string name="settings.hide_media_title">Hide from other</string>
+ <string name="settings.hide_media_summary">Hide music files from other apps.</string>
+ <string name="settings.hide_media_toast">Takes effect next time Android scans your phone for music.</string>
+ <string name="settings.media_button_title">Media buttons</string>
+ <string name="settings.media_button_summary">Respond to phone, headset and Bluetooth media buttons</string>
+ <string name="settings.screen_lit_title">Keep screen on</string>
+ <string name="settings.screen_lit_summary">Keeping the screen on while downloading improves download speed.</string>
+ <string name="settings.playlist_title">Play</string>
+ <string name="settings.playlist_random_size_title">Shuffle Playlist Size</string>
+ <string name="settings.sleep_timer_title">Sleep Timer</string>
+ <string name="settings.sleep_timer_duration_title">Sleep Timer Duration</string>
+ <string name="settings.sleep_timer_off">Off</string>
+ <string name="settings.sleep_timer_on">On</string>
+ <string name="settings.sleep_timer_always_on">Always On</string>
+ <string name="settings.temp_loss_title">Temporary Loss of Focus</string>
+ <string name="settings.temp_loss_pause">Always Pause</string>
+ <string name="settings.temp_loss_pause_lower">Pause, lower volume when requested</string>
+ <string name="settings.temp_loss_lower">Always lower volume</string>
+ <string name="settings.temp_loss_nothing">Do Nothing</string>
+ <string name="settings.disconnect_pause_title">Pause on Disconnect</string>
+ <string name="settings.disconnect_pause_both">Pause</string>
+ <string name="settings.disconnect_pause_neither">Do Nothing</string>
+ <string name="settings.persistent_title">Persistent Notification</string>
+ <string name="settings.persistent_summary">Show the notification even after pausing. Press the stop button to clear it away.</string>
+ <string name="settings.gapless_playback">Gapless Playback</string>
+ <string name="settings.gapless_playback_summary">If you are seeing strange bugs during playback, turning this off may help.</string>
+ <string name="settings.chat_refresh">Chat Refresh Rate (Secs)</string>
+ <string name="settings.chat_enabled">Chat Enabled</string>
+ <string name="settings.chat_enabled_summary">Whether or not to display the chat listing in the pull out drawer</string>
+ <string name="settings.video_title">Video</string>
+ <string name="settings.video_player">Video Player</string>
+ <string name="settings.video_raw">Raw (Requires Subsonic 4.8+)</string>
+ <string name="settings.video_hls">HTTP Live Stream (HLS) (Requires Subsonic 4.8+)</string>
+ <string name="settings.video_transcode">Direct Transcode (Requires video -> mp4 or similar setup on Server)</string>
+ <string name="settings.video_flash">Flash (Requires Plugin)</string>
+ <string name="settings.cache_screen_title">Cache/Network</string>
+ <string name="settings.playback_title">Playback</string>
+ <string name="settings.hide_widget_title">Hide Widget</string>
+ <string name="settings.hide_widget_summary">Hide widget after exiting app</string>
+ <string name="settings.podcasts_enabled">Podcasts Enabled</string>
+ <string name="settings.podcasts_enabled_summary">Whether or not to display the podcast listing in the pull out drawer</string>
+ <string name="settings.bookmarks_enabled">Bookmarks Enabled</string>
+ <string name="settings.bookmarks_enabled_summary">Whether or not to display the bookmarks listing in the pull out drawer</string>
+ <string name="settings.shares_enabled">Shares Enabled</string>
+ <string name="settings.shares_enabled_summary">Whether or not to display the shares listing in the pull out drawer</string>
+ <string name="settings.sync_title">Sync</string>
+ <string name="settings.sync_enabled">Sync Enabled</string>
+ <string name="settings.sync_enabled_summary">Whether or not playlists or podcasts are periodically checked for changes</string>
+ <string name="settings.sync_interval">Sync Interval</string>
+ <string name="settings.sync_interval_15">15 Minutes</string>
+ <string name="settings.sync_interval_30">30 Minutes</string>
+ <string name="settings.sync_interval_60">1 Hour</string>
+ <string name="settings.sync_interval_120">2 Hours</string>
+ <string name="settings.sync_interval_240">4 Hours</string>
+ <string name="settings.sync_interval_360">6 Hours</string>
+ <string name="settings.sync_interval_720">12 Hours</string>
+ <string name="settings.sync_interval_1440">Daily</string>
+ <string name="settings.sync_wifi">Sync on Wifi only</string>
+ <string name="settings.sync_wifi_summary">Only sync while on wifi</string>
+ <string name="settings.sync_most_recent">Sync Recently Added</string>
+ <string name="settings.sync_most_recent_summary">Automatically cache newly added albums</string>
+ <string name="settings.sync_starred">Sync Starred</string>
+ <string name="settings.sync_starred_summary">Automatically cache songs, albums, and artists which are starred</string>
+ <string name="settings.sync_notification">Show Sync Notification</string>
+ <string name="settings.sync_notification_summary">Show a notification after new media has been synced</string>
+ <string name="settings.menu_options.title">Optional Menu Options</string>
+ <string name="settings.menu_options.play_next_summary">Show Play next in menus</string>
+ <string name="settings.menu_options.play_last_summary">Show Play last in menus</string>
+ <string name="settings.menu_options.star_summary">Show Star in menus</string>
+ <string name="settings.menu_options.shared_summary">Show Share in menus</string>
+ <string name="settings.menu_options.rate_summary">Show Rating in menus</string>
+ <string name="settings.browse_by_tags">Browse By Tags</string>
+ <string name="settings.browse_by_tags_summary">Browse by tags instead of by folder structure. Requires Subsonic 4.7+</string>
+ <string name="settings.disable_exit_prompt">Disable Exit Prompt</string>
+ <string name="settings.disable_exit_prompt_summary">Exit the app immediately upon pressing back from the home screen</string>
+ <string name="settings.override_system_language">Override System Language</string>
+ <string name="settings.override_system_language_summary">Display app in english even if the system language is something DSub has a translation for. May need to clear the app from memory for changes to take affect.</string>
+ <string name="settings.drawer_items_title">Drawer Tabs</string>
+ <string name="settings.play_now_after">Play Now - After</string>
+ <string name="settings.play_now_after_summary">Play Now context menu for a song plays everything after selected item (like the Subsonic web GUI)</string>
+ <string name="settings.large_album_art">Large Album Art</string>
+ <string name="settings.large_album_art_summary">Display albums with large album art instead of in a list</string>
+ <string name="settings.admin_enabled">Admin Enabled</string>
+ <string name="settings.admin_enabled_summary">Whether or not to display the admin listing in the pull out drawer</string>
+ <string name="settings.replay_gain">Replay Gain</string>
+ <string name="settings.replay_gain_summary">Whether or not to scale playback volume by track and album replay gain tags</string>
+ <string name="settings.replay_gain_type">Read from tags</string>
+ <string name="settings.replay_gain_type.smart">Smart detection</string>
+ <string name="settings.replay_gain_type.album">Album tags</string>
+ <string name="settings.replay_gain_type.track">Track tags</string>
+ <string name="settings.replay_gain_bump">Replay Gain Pre-amp</string>
+ <string name="settings.replay_gain_untagged">Songs without Replay Gain</string>
+ <string name="settings.casting">Casting</string>
+ <string name="settings.casting_proxy">Use device proxy</string>
+ <string name="settings.casting_proxy_summary">Stream everything through the device as a proxy. This gets around issues such as using self-signed certificates.</string>
+ <string name="settings.rename_duplicates">Rename duplicate tracks</string>
+ <string name="settings.rename_duplicates_summary">Rename duplicate tracks to the original filename so you can tell them apart.</string>
+ <string name="settings.start_on_headphones">Start on headphones</string>
+ <string name="settings.start_on_headphones_summary">Start when headphones are plugged in. This requires the use of a service which starts on boot up to check for the headphone plug event even when DSub is not running.</string>
+
+ <string name="shuffle.title">Shuffle By</string>
+ <string name="shuffle.startYear">Start Year:</string>
+ <string name="shuffle.endYear">End Year:</string>
+ <string name="shuffle.genre">Genre:</string>
+ <string name="shuffle.pick_genre">Pick a genre</string>
+
+ <string name="share.info">Owner: %1$s
+ \nDescription: %2$s
+ \nURL: %3$s
+ \nCreation: %4$s
+ \nLast Visited: %5$s
+ \nExpiration: %6$s
+ \nVisit Count: %7$s
+
+ </string>
+ <string name="share.expires">Expires: %s</string>
+ <string name="share.expires_never">Never Expires</string>
+ <string name="share.deleted">Deleted share %s</string>
+ <string name="share.deleted_error">Failed to delete share %s</string>
+ <string name="share.no_expiration">No expiration</string>
+ <string name="share.expiration">Expires:</string>
+ <string name="share.updated_info">Updated share information for %s</string>
+ <string name="share.updated_info_error">Failed to update share information for %s</string>
+ <string name="share.via">Share via</string>
+ <string name="share.delete">Delete Share</string>
+
+ <string name="admin.add_user_username">Username:</string>
+ <string name="admin.add_user_email">Email:</string>
+ <string name="admin.add_user_password">Password:</string>
+ <string name="admin.create_user_success">Successfully created new user</string>
+ <string name="admin.create_user_error">Failed to create new user</string>
+ <string name="admin.change_username_invalid">Enter a valid username</string>
+ <string name="admin.update_permissions">Update Permissions</string>
+ <string name="admin.update_permissions_success">Successfully updated permission for %1$s</string>
+ <string name="admin.update_permissions_error">Failed to update permissions for %1$s</string>
+ <string name="admin.change_email">Change Email</string>
+ <string name="admin.change_email_success">Successfully changed email for %1$s</string>
+ <string name="admin.change_email_error">Failed to change email for %1$s</string>
+ <string name="admin.change_email_label">New Email:</string>
+ <string name="admin.change_email_invalid">Enter a valid email</string>
+ <string name="admin.change_password">Change Password</string>
+ <string name="admin.change_password_success">Successfully changed password for %1$s</string>
+ <string name="admin.change_password_error">Failed to change password for %1$s</string>
+ <string name="admin.change_password_label">New Password:</string>
+ <string name="admin.change_password_invalid">Enter a valid password</string>
+ <string name="admin.delete_user">Delete User</string>
+ <string name="admin.delete_user_success">Successfully deleted %1$s</string>
+ <string name="admin.delete_user_error">Failed to delete %1$s</string>
+ <string name="admin.confirm_password">Confirm Password</string>
+ <string name="admin.confirm_password_bad">Entered password is wrong</string>
+
+ <string name="admin.scrobblingEnabled">Scrobbling allowed</string>
+ <string name="admin.role.admin">Administrator</string>
+ <string name="admin.role.settings">Change settings</string>
+ <string name="admin.role.download">Download original files</string>
+ <string name="admin.role.upload">Upload to server</string>
+ <string name="admin.role.coverArt">Change cover art</string>
+ <string name="admin.role.comment">Add comments</string>
+ <string name="admin.role.podcast">Manage podcasts</string>
+ <string name="admin.role.stream">Stream music</string>
+ <string name="admin.role.jukebox">Control jukebox</string>
+ <string name="admin.role.share">Manage shares</string>
+ <string name="admin.role.lastfm">Use Last.FM feature</string>
+
+ <string name="music_service.retry">A network error occurred. Retrying %1$d of %2$d.</string>
+
+ <string name="background_task.wait">Please wait...</string>
+ <string name="background_task.loading">Loading.</string>
+ <string name="background_task.no_network">This program requires network access. Please turn on Wi-Fi or mobile network.</string>
+ <string name="background_task.network_error">A network error occurred. Please check the server address or try again later.</string>
+ <string name="background_task.not_found">Resource not found. Please check the server address.</string>
+ <string name="background_task.parse_error">A problem occurred communicating with the server. Please check the server address and verify that you can connect using a regular browser on your device.</string>
+
+ <string name="service.connecting">Contacting server, please wait.</string>
+
+ <string name="parser.upgrade_client">Incompatible versions. Please upgrade DSub.</string>
+ <string name="parser.upgrade_server">Incompatible versions. Please upgrade Subsonic server.</string>
+ <string name="parser.not_authenticated">Wrong username or password.</string>
+ <string name="parser.not_authorized">Not authorized. Check user permissions in Subsonic server.</string>
+ <string name="parser.artist_count">Got %d artists.</string>
+ <string name="parser.server_error">Server error: %s</string>
+ <string name="parser.scan_count">Scanned %d entries</string>
+
+ <string name="select_artist.refresh">Refresh</string>
+ <string name="select_artist.folder">Select folder</string>
+ <string name="select_artist.all_folders">All folders</string>
+
+ <string name="equalizer.label">Equalizer</string>
+ <string name="equalizer.enabled">Enabled</string>
+ <string name="equalizer.preset">Select preset</string>
+ <string name="equalizer.bass_booster">Bass Booster</string>
+ <string name="equalizer.voice_booster">Voice Booster</string>
+ <string name="equalizer.db_size">%d dB</string>
+ <string name="equalizer.bass_size">%d mille</string>
+
+ <string name="widget.4x1">DSub (4x1)</string>
+ <string name="widget.4x2">DSub (4x2)</string>
+ <string name="widget.4x3">DSub (4x3)</string>
+ <string name="widget.4x4">DSub (4x4)</string>
+ <string name="widget.initial_text">Touch to select music</string>
+ <string name="widget.sdcard_busy">SD card unavailable</string>
+ <string name="widget.sdcard_missing">No SD card</string>
+
+ <string name="util.bytes_format.gigabyte">0.00 GB</string>
+ <string name="util.bytes_format.megabyte">0.00 MB</string>
+ <string name="util.bytes_format.kilobyte">0 KB</string>
+ <string name="util.bytes_format.byte">0 B</string>
+
+ <string name="changelog_full_title">Change Log</string>
+ <string name="changelog_title">What\'s New</string>
+ <string name="changelog_ok_button">OK</string>
+ <string name="changelog_show_full">More…</string>
+
+ <string name="chat.send_a_message">Send a message</string>
+
+ <string name="changelog_version_format" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">Version <xliff:g id="version_name">%s</xliff:g></string>
+
+ <string name="tasker.start_playing">Start playing</string>
+ <string name="tasker.start_playing_shuffled">Start playing in Shuffle Mode</string>
+ <string name="tasker.start_playing_title">Tasker -> Start DSub</string>
+ <string name="tasker.edit_shuffle_mode">Start in shuffle mode: </string>
+ <string name="tasker.edit_shuffle_start_year">Shuffle start year:</string>
+ <string name="tasker.edit_shuffle_end_year">Shuffle end year:</string>
+ <string name="tasker.edit_shuffle_genre">Shuffle from genre:</string>
+ <string name="tasker.edit_server_offline">Toggle offline: </string>
+ <string name="tasker.edit_do_nothing">Do Nothing</string>
+
+ <plurals name="select_album_n_songs">
+ <item quantity="zero">No songs</item>
+ <item quantity="one">One song</item>
+ <item quantity="other">%d songs</item>
+ </plurals>
+ <plurals name="select_album_n_songs_downloading">
+ <item quantity="one">One song scheduled for download.</item>
+ <item quantity="other">%d songs scheduled for download.</item>
+ </plurals>
+ <plurals name="select_album_n_songs_added">
+ <item quantity="one">One song added to play queue.</item>
+ <item quantity="other">%d songs added to play queue.</item>
+ </plurals>
+ <plurals name="select_album_donate_dialog_n_trial_days_left">
+ <item quantity="one">One day left of trial period</item>
+ <item quantity="other">%d days left of trial period</item>
+ </plurals>
+
+</resources>
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
new file mode 100644
index 00000000..43271afd
--- /dev/null
+++ b/app/src/main/res/values/styles.xml
@@ -0,0 +1,95 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <style name="BasicButton">
+ <item name="android:background">@drawable/abc_item_background_holo_light</item>
+ </style>
+
+ <style name="MoreButton" parent="BasicButton">
+ <item name="android:paddingRight">14dip</item>
+ </style>
+
+ <style name="PlaybackControl" parent="@style/BasicButton">
+ <item name="android:scaleType">fitCenter</item>
+ <item name="android:padding">6dip</item>
+ <item name="android:layout_marginLeft">4dip</item>
+ <item name="android:layout_marginRight">4dip</item>
+ <item name="android:layout_width">@dimen/Button</item>
+ <item name="android:layout_height">@dimen/Button</item>
+ <item name="android:contentDescription">@null</item>
+ </style>
+
+ <style name="PlaybackControl.Small" parent="@style/PlaybackControl">
+ <item name="android:padding">4dip</item>
+ <item name="android:layout_width">@dimen/Button.Small</item>
+ <item name="android:layout_height">@dimen/Button.Small</item>
+ </style>
+
+ <style name="MenuBarButton" parent="@style/BasicButton">
+ <item name="android:layout_width">0dip</item>
+ <item name="android:layout_height">45dip</item>
+ <item name="android:layout_weight">1</item>
+ <item name="android:textSize">14sp</item>
+ <item name="android:textStyle">bold</item>
+ <item name="android:textColor">?android:textColorPrimary</item>
+ </style>
+
+ <style name="DownloadActionButton" parent="@style/BasicButton">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:textStyle">bold</item>
+ <item name="android:textSize">22sp</item>
+ <item name="android:paddingTop">4dip</item>
+ <item name="android:paddingBottom">4dip</item>
+ <item name="android:paddingRight">4dip</item>
+ <item name="android:paddingLeft">4dip</item>
+ </style>
+
+ <style name="DownloadActionImageButton" parent="@style/BasicButton">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:paddingTop">8dip</item>
+ <item name="android:paddingBottom">8dip</item>
+ <item name="android:paddingRight">16dip</item>
+ <item name="android:paddingLeft">16dip</item>
+ </style>
+
+ <style name="DragDropListView">
+ <item name="drag_enabled">true</item>
+ <item name="collapsed_height">1dp</item>
+ <item name="drag_scroll_start">1.0</item>
+ <item name="max_drag_scroll_speed">2.0</item>
+ <item name="float_alpha">0.6</item>
+ <item name="slide_shuffle_speed">0.3</item>
+ <item name="track_drag_sort">false</item>
+ <item name="use_default_controller">true</item>
+ <item name="drag_handle_id">@id/drag_handle</item>
+ <item name="sort_enabled">true</item>
+ <item name="remove_enabled">false</item>
+ <item name="remove_mode">flingRemove</item>
+ <item name="drag_start_mode">onLongPress</item>
+ <item name="float_background_color">@android:color/transparent</item>
+ </style>
+
+ <style name="MainAlbumButton">
+ <item name="android:drawablePadding">6dip</item>
+ <item name="android:layout_width">fill_parent</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:textAppearance">?android:attr/textAppearanceMedium</item>
+ <item name="android:gravity">center_vertical</item>
+ <item name="android:paddingLeft">6dip</item>
+ <item name="android:paddingRight">6dip</item>
+ <item name="android:minHeight">46dip</item>
+ </style>
+
+ <style name="MainAlbumButtonLabel">
+ <item name="android:layout_width">fill_parent</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:textAppearance">?android:attr/textAppearanceSmall</item>
+ <item name="android:textColor">@color/cyan</item>
+ <item name="android:gravity">center_vertical</item>
+ <item name="android:paddingLeft">6dp</item>
+ <item name="android:textAllCaps">true</item>
+ <item name="android:textStyle">bold</item>
+ <item name="android:textSize">16sp</item>
+ </style>
+</resources> \ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
new file mode 100644
index 00000000..78a2c34d
--- /dev/null
+++ b/app/src/main/res/values/themes.xml
@@ -0,0 +1,109 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <style name="Theme.DSub.Light" parent="@style/Theme.AppCompat.Light">
+ <item name="actionBarStyle">@style/Widget.DSub.ActionBarStyle.Light</item>
+ <item name="android:actionBarStyle">@style/Widget.DSub.ActionBarStyle.Light</item>
+ <item name="offline_icon">@drawable/main_offline_light</item>
+ <item name="media_button_backward">@drawable/media_backward_light</item>
+ <item name="media_button_forward">@drawable/media_forward_light</item>
+ <item name="media_button_pause">@drawable/media_pause_light</item>
+ <item name="media_button_repeat_off">@drawable/media_repeat_off_light</item>
+ <item name="media_button_start">@drawable/media_start_light</item>
+ <item name="media_button_stop">@drawable/media_stop_light</item>
+ <item name="chat_send">@drawable/ic_menu_chat_send_light</item>
+ <item name="add">@drawable/ic_action_add_light</item>
+ <item name="download_none">@drawable/download_none_light</item>
+ <item name="shuffle">@drawable/ic_menu_shuffle_light</item>
+ <item name="refresh">@drawable/ic_menu_refresh_light</item>
+ <item name="search">@drawable/ic_menu_search_light</item>
+ <item name="remove">@drawable/ic_menu_remove_light</item>
+ <item name="save">@drawable/ic_menu_save_light</item>
+ <item name="volume">@drawable/ic_action_volume_light</item>
+ <item name="toggle_list">@drawable/action_toggle_list_light</item>
+ <item name="select_server">@drawable/main_select_server_light</item>
+ <item name="downloading">@drawable/downloading_light</item>
+ <item name="bookmark">@drawable/ic_menu_bookmark_light</item>
+ <item name="share">@drawable/ic_menu_share_light</item>
+ <item name="add_person">@drawable/ic_menu_add_person_light</item>
+ <item name="password">@drawable/ic_menu_password_light</item>
+ <item name="rating_bad">@drawable/ic_action_rating_bad_light</item>
+ <item name="rating_good">@drawable/ic_action_rating_good_light</item>
+ <item name="radio">@drawable/ic_menu_radio_light</item>
+ <item name="drawerItemsIcons">@array/drawerItemIconsLight</item>
+ <item name="android:textViewStyle">@style/DSub.TextViewStyle</item>
+ <item name="android:buttonStyle">@style/DSub.ButtonStyle.Light</item>
+ <item name="drawerArrowStyle">@style/DSub.DrawerArrow</item>
+ <item name="colorAccent">@color/cyan</item>
+ </style>
+ <style name="Theme.DSub.Dark" parent="@style/Theme.AppCompat">
+ <item name="actionBarStyle">@style/Widget.DSub.ActionBarStyle.Dark</item>
+ <item name="android:actionBarStyle">@style/Widget.DSub.ActionBarStyle.Dark</item>
+ <item name="offline_icon">@drawable/main_offline_dark</item>
+ <item name="media_button_backward">@drawable/media_backward_dark</item>
+ <item name="media_button_forward">@drawable/media_forward_dark</item>
+ <item name="media_button_pause">@drawable/media_pause_dark</item>
+ <item name="media_button_repeat_off">@drawable/media_repeat_off</item>
+ <item name="media_button_start">@drawable/media_start_dark</item>
+ <item name="media_button_stop">@drawable/media_stop_dark</item>
+ <item name="chat_send">@drawable/ic_menu_chat_send_dark</item>
+ <item name="add">@drawable/ic_action_add_dark</item>
+ <item name="download_none">@drawable/download_none_dark</item>
+ <item name="shuffle">@drawable/ic_menu_shuffle_dark</item>
+ <item name="refresh">@drawable/ic_menu_refresh_dark</item>
+ <item name="search">@drawable/ic_menu_search_dark</item>
+ <item name="remove">@drawable/ic_menu_remove_dark</item>
+ <item name="save">@drawable/ic_menu_save_dark</item>
+ <item name="volume">@drawable/ic_action_volume_dark</item>
+ <item name="toggle_list">@drawable/action_toggle_list_dark</item>
+ <item name="select_server">@drawable/main_select_server_dark</item>
+ <item name="downloading">@drawable/downloading_dark</item>
+ <item name="bookmark">@drawable/ic_menu_bookmark_dark</item>
+ <item name="share">@drawable/ic_menu_share_dark</item>
+ <item name="add_person">@drawable/ic_menu_add_person_dark</item>
+ <item name="password">@drawable/ic_menu_password_dark</item>
+ <item name="rating_bad">@drawable/ic_action_rating_bad_dark</item>
+ <item name="rating_good">@drawable/ic_action_rating_good_dark</item>
+ <item name="radio">@drawable/ic_menu_radio_dark</item>
+ <item name="drawerItemsIcons">@array/drawerItemIconsDark</item>
+ <item name="android:textViewStyle">@style/DSub.TextViewStyle</item>
+ <item name="android:buttonStyle">@style/DSub.ButtonStyle.Dark</item>
+ <item name="drawerArrowStyle">@style/DSub.DrawerArrow</item>
+ <item name="colorAccent">@color/cyan</item>
+ </style>
+ <style name="Theme.DSub.Black" parent="Theme.DSub.Dark">
+ <item name="android:windowBackground">@android:color/black</item>
+ </style>
+ <style name="Theme.DSub.Holo" parent="Theme.DSub.Dark">
+ <item name="android:windowBackground">@drawable/background</item>
+ </style>
+
+ <style name="Widget.DSub.ActionBarStyle.Light" parent="Widget.AppCompat.Light.ActionBar.Solid">
+ <item name="background">@android:color/transparent</item>
+ <item name="android:background">@android:color/transparent</item>
+ <item name="backgroundStacked">@android:color/transparent</item>
+ <item name="android:backgroundStacked">@android:color/transparent</item>
+ </style>
+
+ <style name="Widget.DSub.ActionBarStyle.Dark" parent="Widget.AppCompat.ActionBar.Solid">
+ <item name="background">@android:color/transparent</item>
+ <item name="android:background">@android:color/transparent</item>
+ <item name="backgroundStacked">@android:color/transparent</item>
+ <item name="android:backgroundStacked">@android:color/transparent</item>
+ </style>
+
+ <style name="DSub.TextViewStyle" parent="android:Widget.TextView">
+ </style>
+
+ <style name="DSub.TextViewStyle.Bold" parent="android:Widget.TextView">
+ <item name="android:textStyle">bold</item>
+ </style>
+
+ <style name="DSub.ButtonStyle.Dark" parent="android:Widget.Holo.Button">
+ </style>
+ <style name="DSub.ButtonStyle.Light" parent="android:Widget.Holo.Light.Button">
+ </style>
+
+ <style name="DSub.DrawerArrow" parent="Widget.AppCompat.DrawerArrowToggle">
+ <item name="spinBars">true</item>
+ </style>
+</resources>
diff --git a/app/src/main/res/xml/appwidget4x1.xml b/app/src/main/res/xml/appwidget4x1.xml
new file mode 100644
index 00000000..f3ab5d56
--- /dev/null
+++ b/app/src/main/res/xml/appwidget4x1.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
+ android:minWidth="272dip"
+ android:minHeight="56dip"
+ android:updatePeriodMillis="0"
+ android:resizeMode="horizontal|vertical"
+ android:previewImage="@drawable/appwidget4x1_preview"
+ android:initialLayout="@layout/appwidget4x1"/>
diff --git a/app/src/main/res/xml/appwidget4x2.xml b/app/src/main/res/xml/appwidget4x2.xml
new file mode 100644
index 00000000..d687d952
--- /dev/null
+++ b/app/src/main/res/xml/appwidget4x2.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
+ android:minWidth="272dip"
+ android:minHeight="110dip"
+ android:updatePeriodMillis="0"
+ android:resizeMode="horizontal|vertical"
+ android:previewImage="@drawable/appwidget4x2_preview"
+ android:initialLayout="@layout/appwidget4x2"/>
diff --git a/app/src/main/res/xml/appwidget4x3.xml b/app/src/main/res/xml/appwidget4x3.xml
new file mode 100644
index 00000000..4d1b4e08
--- /dev/null
+++ b/app/src/main/res/xml/appwidget4x3.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
+ android:minWidth="272dip"
+ android:minHeight="180dp"
+ android:updatePeriodMillis="0"
+ android:resizeMode="horizontal|vertical"
+ android:previewImage="@drawable/appwidget4x3_preview"
+ android:initialLayout="@layout/appwidget4x3"/>
diff --git a/app/src/main/res/xml/appwidget4x4.xml b/app/src/main/res/xml/appwidget4x4.xml
new file mode 100644
index 00000000..74a8ed0c
--- /dev/null
+++ b/app/src/main/res/xml/appwidget4x4.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
+ android:minWidth="272dip"
+ android:minHeight="250dp"
+ android:updatePeriodMillis="0"
+ android:resizeMode="horizontal|vertical"
+ android:initialLayout="@layout/appwidget4x4"
+ android:initialKeyguardLayout="@layout/appwidget4x4"
+ android:previewImage="@drawable/appwidget4x4_preview"
+ android:widgetCategory="keyguard|home_screen"/>
diff --git a/app/src/main/res/xml/authenticator.xml b/app/src/main/res/xml/authenticator.xml
new file mode 100644
index 00000000..3055240b
--- /dev/null
+++ b/app/src/main/res/xml/authenticator.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<account-authenticator
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:accountType="subsonic.org"
+ android:icon="@drawable/launch"
+ android:smallIcon="@drawable/launch"
+ android:label="@string/common.appname"/> \ No newline at end of file
diff --git a/app/src/main/res/xml/changelog.xml b/app/src/main/res/xml/changelog.xml
new file mode 100644
index 00000000..005ddf44
--- /dev/null
+++ b/app/src/main/res/xml/changelog.xml
@@ -0,0 +1,214 @@
+<?xml version="1.0" encoding="utf-8"?>
+<changelog>
+ <release version="4.9.6" versioncode="150" releasedate="4/20/2015">
+ <change>New setting: Automatic renaming of duplicate songs</change>
+ <change>New setting: auto play on headphone insert</change>
+ <change>New setting: make automatic renaming of duplicate songs optional</change>
+ <change>Persistent Notification: dismissible when not playing</change>
+ <change>Duplicate artists: combine them into a single listing when browsing all folders</change>
+ <change>No internet: display last loaded version of albums lists</change>
+ <change>Bug fixes for Ampache users</change>
+ </release>
+ <release version="4.9.5" versioncode="149" releasedate="3/25/2015">
+ <change>Handoff: move to another DSub client and pickup where you left off (Subsonic 5.2+)</change>
+ <change>DLNA: gapless playback on supported renders</change>
+ <change>Option to warning on back press</change>
+ <change>Better handle date for Subsonic 5.1+</change>
+ <change>Handle more ReplayGain tag formats</change>
+ <change>Minor bug fixes</change>
+ </release>
+ <release version="4.9.4" versioncode="147" releasedate="3/9/2015">
+ <change>Cast proxy setting for fixing casting with self-signed certificates</change>
+ <change>View all albums in alphabetical order from Home tab</change>
+ <change>View all videso from Home tab</change>
+ <change>Artist Radio for Madsonic users</change>
+ <change>Minor bug fixes</change>
+ </release>
+ <release version="4.9.3" versioncode="146" releasedate="2/27/2015">
+ <change>Browse starred by folder as well (Subsonic 5.2+)</change>
+ <change>Crash fixes</change>
+ </release>
+ <release version="4.9.2" versioncode="145" releasedate="2/20/2015">
+ <change>Fix some more DLNA issues (still working on others)</change>
+ <change>Artist Radio (Subsonic 5.1+)</change>
+ <change>Checkbox to browse Recently Added, etc by folder (Subsonic 5.1+)</change>
+ <change>Ability to open straight to other tabs</change>
+ <change>Add play/shuffle to similar artists menu</change>
+ <change>Offline Podcasts: dont show duplicate folder</change>
+ <change>Fix crash with Madsonic servers</change>
+ <change>Fix widget not showing info when app is not running</change>
+ <change>Show folder as empty immediately after deleting last song in it</change>
+ </release>
+ <release version="4.9.1" versioncode="143" releasedate="1/29/2015">
+ <change>Fix searching with spaces in it</change>
+ <change>Fix pressing play on widget before app starts not working</change>
+ </release>
+ <release version="4.9" versioncode="142" releasedate="1/27/2015">
+ <change>Cast audio to DLNA (email me with issues)</change>
+ <change>Artist image + details header (Subsonic 5.1+)</change>
+ <change>Similar Artists option (Subsonic 5.1+)</change>
+ <change>View similar artists missing from your server (Subsonic 5.1+)</change>
+ <change>On starred list, load artist image (Subsonic 5.1+)</change>
+ <change>Podcasts: clicking on description wrapers around image</change>
+ <change>Hide folder selection if user only has one</change>
+ <change>Fix seeking after file finished downloading restarting the song</change>
+ <change>Fix sleep timer incrementing on it's own</change>
+ <change>Fix bookmarks not being auto deleted while casting</change>
+ <change>Fix clean cache not deleting artwork/avatars</change>
+ <change>Fix search with tag browsing on Ampache servers</change>
+ </release>
+ <release version="4.8.6" versioncode="134" releasedate="12/27/2014">
+ <change>Play/shuffle quick album lists such as Recently Added or Random</change>
+ <change>Change download status to a percentage</change>
+ <change>Improved unknown album art</change>
+ <change>Allow any size cache to be set</change>
+ <change>Improved search sort order</change>
+ <change>Fix settings coloring on older versions of Android</change>
+ <change>Fix sleep timer not remembering last value</change>
+ <change>Fix caching not working while casting</change>
+ </release>
+ <release version="4.8.5" versioncode="133" releasedate="11/26/2014">
+ <change>Fix crash on GB</change>
+ <change>Fix some theme issues</change>
+ </release>
+ <release version="4.8.4" versioncode="132" releasedate="11/22/2014">
+ <change>Partial Material Theme update</change>
+ <change>Make playing notification public for Lolipop</change>
+ <change>Fix Lolipop connectivity issues for some users</change>
+ <change>Fix cache from playlist view downloading starred songs instead</change>
+ <change>Fix remove from playlist not showing up on MusicCabinet servers</change>
+ </release>
+ <release version="4.8.3" versioncode="131" releasedate="11/14/2014">
+ <change>Fix color on Lolipop lockscreen notification</change>
+ <change>Various bug fixes</change>
+ </release>
+ <release version="4.8.2" versioncode="130" releasedate="11/2/2014">
+ <change>Improve automatic bookmark logic</change>
+ <change>Tasker: Toggle online/offline</change>
+ <change>Tasker: Set start/end year + genre for shuffle</change>
+ <change>Remove files no longer on server</change>
+ <change>Calculate bitrate instead of relying on tags</change>
+ <change>Work around for issue of EQ sometimes not starting</change>
+ <change>Fix issues with offline playlists</change>
+ <change>Fix all servers sharing a recently added count</change>
+ <change>Fix star options not showing up when using Show Album</change>
+ </release>
+ <release version="4.8.1" versioncode="128" releasedate="10/12/2014">
+ <change>Delete songs removed from server</change>
+ <change>Fix crash on GB devices</change>
+ </release>
+ <release version="4.8" versioncode="127" releasedate="10/8/2014">
+ <change>ReplayGain: read tags to normalize sound</change>
+ <change>ReplayGain: choose smart tag reading or specify track/album tags</change>
+ <change>Scrobbling: implemented according to Last.FM standard</change>
+ <change>Scrobbling: scrobble if more then 4 minutes or 50% in</change>
+ <change>Scrobbling: do not scrobble if less then 30 seconds</change>
+ <change>Madsonic: view artists Top Tracks from Last.FM (Madsonic 5.1+)</change>
+ <change>Settings: changed a few to be seek bars</change>
+ <change>Tasker: fix issue with not always starting</change>
+ <change>Artist view: add recursive cache/delete menu options</change>
+ <change>SSID Settings: added copy current button</change>
+ <change>Conditionally remove Cache/Delete Cache from context menus</change>
+ <change>Added transition animations</change>
+ <change>Fix crash when casting on GB</change>
+ <change>Fix widget showing blank on restart</change>
+ <change>Fix Show all media</change>
+ <change>Various bug fixes</change>
+ </release>
+ <release version="4.7.8" versioncode="126" releasedate="9/17/2014">
+ <change>Rating: show rating directly on albums</change>
+ <change>Albums: rate/star from album's screen</change>
+ <change>Limit misc artwork from growing above 100 MB</change>
+ <change>Fix cover art bug with latest Subsonic Beta</change>
+ <change>Fix Set Rating from now playing screen</change>
+ <change>Fix offline starring of albums with tag browsing</change>
+ <change>Fix current position on Nexus 5 going above duration</change>
+ <change>Windows Server: SNI support</change>
+ <change>Minor memory optimizations</change>
+ <change>Misc bug fixes</change>
+ </release>
+ <release version="4.7.7" versioncode="124" releasedate="9/8/2014">
+ <change>Add Set Rating to Now Playing's songs menu</change>
+ <change>Fix "overwrite existing playlist" not showing</change>
+ <change>Fix duplicate songs display status in Now Playing</change>
+ </release>
+ <release version="4.7.6" versioncode="123" releasedate="9/5/2014">
+ <change>Ratings: set album/song ratings</change>
+ <change>Ratings: quick thumbs up/down from now playing screen</change>
+ <change>Ratings: automatically ignore 1-starred songs</change>
+ <change>Add To Playlist: show when song already in</change>
+ <change>Recently Added: show count from Home</change>
+ <change>Bookmarks: auto bookmark tagged Audio Books</change>
+ <change>Bookmarks: show indicator in listing/now playing screen</change>
+ <change>Video: Download using hls transcode settings</change>
+ <change>Sync: Disable per server</change>
+ <change>Global Shuffle: optimize for large list preferences</change>
+ <change>Global Shuffle: don't add duplicates</change>
+ <change>Offline Shuffle: improve randomness</change>
+ <change>Move EQ to options menu</change>
+ <change>Fix issue with empty folder</change>
+ <change>Fix various cache coherency issues</change>
+ <change>Old Servers: fix broken scrobbling</change>
+ <change>Fix cache location reseting</change>
+ </release>
+ <release version="4.7.5" versioncode="121" releasedate="8/24/2014">
+ <change>Bookmarks: Resume from albums/playlists</change>
+ <change>Bookmarks: Auto bookmark long songs/podcasts</change>
+ <change>Bookmarks: Auto delete after played</change>
+ <change>Bookmarks Tab: Show artist/album</change>
+ <change>Bookmarks: Delete from album/playlist</change>
+ <change>Stars: Update without refresh</change>
+ </release>
+ <release version="4.7.4" versioncode="120" releasedate="8/8/2014">
+ <change>Tasker Plugin: Start + optional start Shuffle Mode</change>
+ <change>Madsonic 5.1+: Add Rescan Server option to Home tab</change>
+ <change>Sort quick search results by closeness to query (instead of Artists -> Albums -> Songs)</change>
+ <change>Clicking on Sync notifications goes to corresponding tab</change>
+ <change>Fix no podcasts showing up if one of them is the error state</change>
+ <change>Fix starring some albums when browsing by tags</change>
+ <change>A few crash fixes</change>
+ </release>
+ <release version="4.7.3" versioncode="119" releasedate="7/18/2014">
+ <change>Require password for entering admin tab for security</change>
+ <change>Improvements to sync notifications</change>
+ <change>Fix crash on sharing</change>
+ <change>Misc bug fixes</change>
+ </release>
+ <release version="4.7.2" versioncode="116" releasedate="7/10/2014">
+ <change>Android Wear notification support</change>
+ <change>Selected drawer tab is now bold</change>
+ <change>Scroll to song when clicked in search results</change>
+ <change>Custom sort is now a toggle between sorting by year and alphabetical</change>
+ <change>For persistent notification, use standard notification layout + x in corner</change>
+ <change>Don't display type information for each song</change>
+ <change>Properly save songs for use with external players</change>
+ <change>Return fast scrolling hack for devices still on Android 4.4.2</change>
+ <change>Various bug fixes and enhancements</change>
+ </release>
+ <release version="4.7.1" versioncode="113" releasedate="6/25/2014">
+ <change>Only show podcast, share, and jukebox options when user has server permissions</change>
+ <change>Remove playlist options from public, non-owned lists (throws error anyways)</change>
+ <change>Add avatars to chat tab</change>
+ <change>Better refresh status</change>
+ <change>Better downloading notification</change>
+ <change>Use system volume menu for Jukebox/casting</change>
+ <change>Fix infinite errors when selecting bad server</change>
+ <change>Fix context menu errors on old small album art view</change>
+ <change>Fix issue with background downloads stalling</change>
+ <change>New language: French (thanks Cotib)</change>
+ </release>
+ <release version="4.7" versioncode="112" releasedate="6/16/2014">
+ <change>Admin tab</change>
+ <change>Create/delete users, change others passwords/emails/permissions if admin</change>
+ <change>Change own password either way</change>
+ <change>FAQ dialog</change>
+ <change>Click on Podcast Channel description to expand it</change>
+ <change>Add Podcast Episode description to Details dialog</change>
+ <change>Don't switch views when clicking on bottom portion of album art</change>
+ <change>Keep notification open when receiving a call</change>
+ <change>Fix for delay between headset disconnect and pause</change>
+ <change>Fix background bluetooth disconnects causing playback to stop</change>
+ <change>Fix app trying to upsample songs in mp3 -> mp3 transcode</change>
+ <change>Fix for DSub preempting bluetooth events</change>
+ </release>
+</changelog> \ No newline at end of file
diff --git a/app/src/main/res/xml/mostrecent_syncadapter.xml b/app/src/main/res/xml/mostrecent_syncadapter.xml
new file mode 100644
index 00000000..0195edeb
--- /dev/null
+++ b/app/src/main/res/xml/mostrecent_syncadapter.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
+ android:contentAuthority="github.daneren2005.dsub.mostrecent.provider"
+ android:accountType="subsonic.org"
+ android:userVisible="true"
+ android:supportsUploading="false"
+ android:allowParallelSyncs="false"
+ android:isAlwaysSyncable="true"/> \ No newline at end of file
diff --git a/app/src/main/res/xml/playlists_syncadapter.xml b/app/src/main/res/xml/playlists_syncadapter.xml
new file mode 100644
index 00000000..6c56557b
--- /dev/null
+++ b/app/src/main/res/xml/playlists_syncadapter.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
+ android:contentAuthority="github.daneren2005.dsub.playlists.provider"
+ android:accountType="subsonic.org"
+ android:userVisible="true"
+ android:supportsUploading="false"
+ android:allowParallelSyncs="false"
+ android:isAlwaysSyncable="true"/> \ No newline at end of file
diff --git a/app/src/main/res/xml/podcasts_syncadapter.xml b/app/src/main/res/xml/podcasts_syncadapter.xml
new file mode 100644
index 00000000..52340ae4
--- /dev/null
+++ b/app/src/main/res/xml/podcasts_syncadapter.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
+ android:contentAuthority="github.daneren2005.dsub.podcasts.provider"
+ android:accountType="subsonic.org"
+ android:userVisible="true"
+ android:supportsUploading="false"
+ android:allowParallelSyncs="false"
+ android:isAlwaysSyncable="true"/> \ No newline at end of file
diff --git a/app/src/main/res/xml/searchable.xml b/app/src/main/res/xml/searchable.xml
new file mode 100644
index 00000000..35ff18f3
--- /dev/null
+++ b/app/src/main/res/xml/searchable.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<searchable xmlns:android="http://schemas.android.com/apk/res/android"
+ android:label="@string/common.appname"
+ android:hint="@string/search.title"
+ android:voiceSearchMode="showVoiceSearchButton|launchRecognizer"
+ android:voiceLanguageModel="web_search"
+ android:searchSuggestAuthority="github.daneren2005.dsub.provider.DSubSearchProvider"
+ android:searchSuggestSelection=" unused"
+ android:searchSuggestIntentAction="android.intent.action.VIEW">
+</searchable> \ No newline at end of file
diff --git a/app/src/main/res/xml/settings.xml b/app/src/main/res/xml/settings.xml
new file mode 100644
index 00000000..b1cbdd8c
--- /dev/null
+++ b/app/src/main/res/xml/settings.xml
@@ -0,0 +1,450 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:myns="http://schemas.android.com/apk/res/github.daneren2005.dsub"
+ android:title="@string/settings.title">
+
+ <PreferenceScreen
+ android:title="@string/settings.servers_title">
+
+ <PreferenceCategory
+ android:key="server"
+ android:title="@string/settings.servers_title">
+
+ <Preference
+ android:key="serverAdd"
+ android:order="1000000"
+ android:title="@string/settings.servers_add"/>
+ </PreferenceCategory>
+
+ </PreferenceScreen>
+
+ <PreferenceScreen
+ android:title="@string/settings.appearance_title">
+
+ <PreferenceCategory
+ android:title="@string/settings.appearance_title">
+
+ <ListPreference
+ android:title="@string/settings.theme_title"
+ android:key="theme"
+ android:defaultValue="light"
+ android:entryValues="@array/themeValues"
+ android:entries="@array/themeNames"/>
+
+ <CheckBoxPreference
+ android:title="@string/settings.theme_fullscreen"
+ android:summary="@string/settings.theme_fullscreen_summary"
+ android:key="fullScreen"
+ android:defaultValue="false"/>
+
+ <CheckBoxPreference
+ android:title="@string/settings.track_title"
+ android:summary="@string/settings.track_summary"
+ android:key="displayTrack"
+ android:defaultValue="false"/>
+
+ <CheckBoxPreference
+ android:title="@string/settings.hide_widget_title"
+ android:summary="@string/settings.hide_widget_summary"
+ android:key="hideWidget"
+ android:defaultValue="false"/>
+
+ <CheckBoxPreference
+ android:title="@string/settings.custom_sort"
+ android:summary="@string/settings.custom_sort_summary"
+ android:key="customSortEnabled"
+ android:defaultValue="true"/>
+
+ <CheckBoxPreference
+ android:title="@string/settings.rename_duplicates"
+ android:summary="@string/settings.rename_duplicates_summary"
+ android:key="renameDuplicates"
+ android:defaultValue="true"/>
+
+ <ListPreference
+ android:title="@string/settings.open_to_tab"
+ android:summary="@string/settings.open_to_tab_summary"
+ android:key="openToTab"
+ android:entryValues="@array/defaultDrawerItemsDescriptions"
+ android:entries="@array/defaultDrawerItems"
+ android:defaultValue="Home"/>
+
+ <CheckBoxPreference
+ android:title="@string/settings.disable_exit_prompt"
+ android:summary="@string/settings.disable_exit_prompt_summary"
+ android:key="disableExitPrompt"
+ android:defaultValue="false"/>
+
+ <CheckBoxPreference
+ android:title="@string/settings.override_system_language"
+ android:summary="@string/settings.override_system_language_summary"
+ android:key="overrideSystemLanguage"
+ android:defaultValue="false"/>
+
+ <CheckBoxPreference
+ android:title="@string/settings.large_album_art"
+ android:summary="@string/settings.large_album_art_summary"
+ android:key="largeAlbumArt"
+ android:defaultValue="true"/>
+ </PreferenceCategory>
+
+ <PreferenceCategory
+ android:title="@string/settings.menu_options.title">
+
+ <CheckBoxPreference
+ android:title="@string/common.play_next"
+ android:summary="@string/settings.menu_options.play_next_summary"
+ android:key="showPlayNext"
+ android:defaultValue="true"/>
+
+ <CheckBoxPreference
+ android:title="@string/common.play_last"
+ android:summary="@string/settings.menu_options.play_last_summary"
+ android:key="showPlayLast"
+ android:defaultValue="true"/>
+
+ <CheckBoxPreference
+ android:title="@string/common.star"
+ android:summary="@string/settings.menu_options.star_summary"
+ android:key="showStar"
+ android:defaultValue="true"/>
+
+ <CheckBoxPreference
+ android:title="@string/menu.share"
+ android:summary="@string/settings.menu_options.shared_summary"
+ android:key="showShared"
+ android:defaultValue="true"/>
+
+ <CheckBoxPreference
+ android:title="@string/menu.rate"
+ android:summary="@string/settings.menu_options.rate_summary"
+ android:key="showRating"
+ android:defaultValue="true"/>
+
+ </PreferenceCategory>
+ </PreferenceScreen>
+
+ <PreferenceScreen
+ android:title="@string/settings.drawer_items_title">
+
+ <PreferenceCategory
+ android:title="@string/settings.drawer_items_title">
+
+ <CheckBoxPreference
+ android:title="@string/settings.podcasts_enabled"
+ android:summary="@string/settings.podcasts_enabled_summary"
+ android:key="podcastsEnabled"
+ android:defaultValue="true"/>
+
+ <CheckBoxPreference
+ android:title="@string/settings.bookmarks_enabled"
+ android:summary="@string/settings.bookmarks_enabled_summary"
+ android:key="bookmarksEnabled"
+ android:defaultValue="true"/>
+
+ <CheckBoxPreference
+ android:title="@string/settings.shares_enabled"
+ android:summary="@string/settings.shares_enabled_summary"
+ android:key="sharedEnabled"
+ android:defaultValue="true"/>
+
+ <CheckBoxPreference
+ android:title="@string/settings.chat_enabled"
+ android:summary="@string/settings.chat_enabled_summary"
+ android:key="chatEnabled"
+ android:defaultValue="true"/>
+
+ <CheckBoxPreference
+ android:title="@string/settings.admin_enabled"
+ android:summary="@string/settings.admin_enabled_summary"
+ android:key="adminEnabled"
+ android:defaultValue="true"/>
+ </PreferenceCategory>
+
+ <PreferenceCategory
+ android:title="@string/button_bar.chat">
+
+ <github.daneren2005.dsub.view.SeekBarPreference
+ android:title="@string/settings.chat_refresh"
+ android:key="chatRefreshRate"
+ android:defaultValue="30"
+ android:dialogLayout="@layout/seekbar_preference"
+ myns:max="120"
+ myns:display="%.0f seconds"/>
+ </PreferenceCategory>
+ </PreferenceScreen>
+
+ <PreferenceScreen
+ android:title="@string/settings.cache_screen_title">
+
+ <PreferenceCategory
+ android:title="@string/settings.network_title">
+
+ <ListPreference
+ android:title="@string/settings.max_bitrate_wifi"
+ android:key="maxBitrateWifi"
+ android:defaultValue="0"
+ android:entryValues="@array/maxBitrateValues"
+ android:entries="@array/maxBitrateNames"/>
+
+ <ListPreference
+ android:title="@string/settings.max_bitrate_mobile"
+ android:key="maxBitrateMobile"
+ android:defaultValue="0"
+ android:entryValues="@array/maxBitrateValues"
+ android:entries="@array/maxBitrateNames"/>
+
+ <ListPreference
+ android:title="@string/settings.max_video_bitrate_wifi"
+ android:key="maxVideoBitrateWifi"
+ android:defaultValue="0"
+ android:entryValues="@array/maxVideoBitrateValues"
+ android:entries="@array/maxVideoBitrateNames"/>
+
+ <ListPreference
+ android:title="@string/settings.max_video_bitrate_mobile"
+ android:key="maxVideoBitrateMobile"
+ android:defaultValue="0"
+ android:entryValues="@array/maxVideoBitrateValues"
+ android:entries="@array/maxVideoBitrateNames"/>
+
+ <CheckBoxPreference
+ android:title="@string/settings.wifi_required_title"
+ android:summary="@string/settings.wifi_required_summary"
+ android:key="wifiRequiredForDownload"
+ android:defaultValue="false"/>
+
+ <ListPreference
+ android:title="@string/settings.network_timeout_title"
+ android:key="networkTimeout"
+ android:defaultValue="15000"
+ android:entryValues="@array/networkTimeoutValues"
+ android:entries="@array/networkTimeoutNames"/>
+ </PreferenceCategory>
+
+ <PreferenceCategory
+ android:title="@string/settings.cache_title">
+
+ <EditTextPreference
+ android:title="@string/settings.cache_size"
+ android:key="cacheSize"
+ android:defaultValue="2000"
+ android:digits="0123456789"/>
+
+ <EditTextPreference
+ android:title="@string/settings.cache_location"
+ android:key="cacheLocation"/>
+
+ <ListPreference
+ android:title="@string/settings.preload_wifi"
+ android:key="preloadCountWifi"
+ android:defaultValue="3"
+ android:entryValues="@array/preloadCountValues"
+ android:entries="@array/preloadCountNames"/>
+
+ <ListPreference
+ android:title="@string/settings.preload_mobile"
+ android:key="preloadCountMobile"
+ android:defaultValue="3"
+ android:entryValues="@array/preloadCountValues"
+ android:entries="@array/preloadCountNames"/>
+
+ <Preference
+ android:key="clearCache"
+ android:title="@string/settings.cache_clear"
+ android:persistent="false"/>
+ </PreferenceCategory>
+
+ <PreferenceCategory
+ android:title="@string/settings.other_title">
+
+ <CheckBoxPreference
+ android:title="@string/settings.hide_media_title"
+ android:summary="@string/settings.hide_media_summary"
+ android:key="hideMedia"
+ android:defaultValue="false"/>
+
+ <CheckBoxPreference
+ android:title="@string/settings.screen_lit_title"
+ android:summary="@string/settings.screen_lit_summary"
+ android:key="screenLitOnDownload"
+ android:defaultValue="true"/>
+ </PreferenceCategory>
+ </PreferenceScreen>
+
+ <PreferenceScreen
+ android:title="@string/settings.sync_title">
+ <PreferenceCategory
+ android:title="@string/settings.sync_title">
+
+ <CheckBoxPreference
+ android:title="@string/settings.sync_enabled"
+ android:summary="@string/settings.sync_enabled_summary"
+ android:key="syncEnabled"
+ android:defaultValue="true"/>
+
+ <ListPreference
+ android:title="@string/settings.sync_interval"
+ android:key="syncInterval"
+ android:defaultValue="60"
+ android:entryValues="@array/syncIntervalValues"
+ android:entries="@array/syncIntervalNames"/>
+
+ <CheckBoxPreference
+ android:title="@string/settings.sync_wifi"
+ android:summary="@string/settings.sync_wifi_summary"
+ android:key="syncWifi"
+ android:defaultValue="true"/>
+
+ <CheckBoxPreference
+ android:title="@string/settings.sync_notification"
+ android:summary="@string/settings.sync_notification_summary"
+ android:key="syncNotification"
+ android:defaultValue="true"/>
+ </PreferenceCategory>
+
+ <PreferenceCategory
+ android:title="@string/settings.other_title">
+
+ <CheckBoxPreference
+ android:title="@string/settings.sync_starred"
+ android:summary="@string/settings.sync_starred_summary"
+ android:key="syncStarred"
+ android:defaultValue="false"/>
+
+ <CheckBoxPreference
+ android:title="@string/settings.sync_most_recent"
+ android:summary="@string/settings.sync_most_recent_summary"
+ android:key="syncMostRecent"
+ android:defaultValue="false"/>
+ </PreferenceCategory>
+ </PreferenceScreen>
+
+ <PreferenceScreen
+ android:title="@string/settings.playback_title">
+
+ <PreferenceCategory
+ android:title="@string/settings.playback_title">
+
+ <github.daneren2005.dsub.view.SeekBarPreference
+ android:title="@string/settings.playlist_random_size_title"
+ android:key="randomSize"
+ android:defaultValue="20"
+ android:dialogLayout="@layout/seekbar_preference"
+ myns:max="100"/>
+
+ <ListPreference
+ android:title="@string/settings.temp_loss_title"
+ android:key="tempLoss"
+ android:defaultValue="1"
+ android:entryValues="@array/tempLossValues"
+ android:entries="@array/tempLossNames"/>
+
+ <ListPreference
+ android:title="@string/settings.disconnect_pause_title"
+ android:key="pauseOnDisconnect"
+ android:defaultValue="0"
+ android:entryValues="@array/disconnectPauseValues"
+ android:entries="@array/disconnectPauseNames"/>
+
+ <CheckBoxPreference
+ android:title="@string/settings.persistent_title"
+ android:summary="@string/settings.persistent_summary"
+ android:key="persistentNotification"
+ android:defaultValue="false"/>
+
+ <CheckBoxPreference
+ android:title="@string/settings.play_now_after"
+ android:summary="@string/settings.play_now_after_summary"
+ android:key="playNowAfter"
+ android:defaultValue="false"/>
+ </PreferenceCategory>
+
+ <PreferenceCategory
+ android:title="@string/settings.video_title">
+
+ <ListPreference
+ android:title="@string/settings.video_player"
+ android:key="videoPlayer"
+ android:defaultValue="raw"
+ android:entryValues="@array/videoPlayerValues"
+ android:entries="@array/videoPlayerNames"/>
+ </PreferenceCategory>
+
+ <PreferenceCategory
+ android:title="@string/settings.casting">
+
+ <CheckBoxPreference
+ android:title="@string/settings.casting_proxy"
+ android:summary="@string/settings.casting_proxy_summary"
+ android:key="castProxy"
+ android:defaultValue="false"/>
+ </PreferenceCategory>
+
+ <PreferenceCategory
+ android:title="@string/settings.replay_gain">
+
+ <CheckBoxPreference
+ android:title="@string/settings.replay_gain"
+ android:summary="@string/settings.replay_gain_summary"
+ android:key="replayGain"
+ android:defaultValue="false"/>
+
+ <ListPreference
+ android:title="@string/settings.replay_gain_type"
+ android:key="replayGainType"
+ android:defaultValue="1"
+ android:entryValues="@array/replayGainTypeValues"
+ android:entries="@array/replayGainTypeNames"/>
+
+ <github.daneren2005.dsub.view.SeekBarPreference
+ android:key="replayGainBump2"
+ android:dialogLayout="@layout/seekbar_preference"
+ android:title="@string/settings.replay_gain_bump"
+ android:defaultValue="150"
+ myns:max="150"
+ myns:min="-150"
+ myns:stepSize="10"
+ myns:display="%+.1f dB"/>
+
+ <github.daneren2005.dsub.view.SeekBarPreference
+ android:key="replayGainUntagged2"
+ android:dialogLayout="@layout/seekbar_preference"
+ android:title="@string/settings.replay_gain_untagged"
+ android:defaultValue="150"
+ myns:max="0"
+ myns:min="-150"
+ myns:stepSize="10"
+ myns:display="%+.1f dB"/>
+ </PreferenceCategory>
+
+ <PreferenceCategory
+ android:title="@string/settings.other_title">
+
+ <CheckBoxPreference
+ android:title="@string/settings.scrobble_title"
+ android:summary="@string/settings.scrobble_summary"
+ android:key="scrobble"
+ android:defaultValue="true"/>
+
+ <CheckBoxPreference
+ android:title="@string/settings.media_button_title"
+ android:summary="@string/settings.media_button_summary"
+ android:key="mediaButtons"
+ android:defaultValue="true"/>
+
+ <CheckBoxPreference
+ android:title="@string/settings.gapless_playback"
+ android:summary="@string/settings.gapless_playback_summary"
+ android:key="gaplessPlayback"
+ android:defaultValue="true"/>
+
+ <CheckBoxPreference
+ android:title="@string/settings.start_on_headphones"
+ android:summary="@string/settings.start_on_headphones_summary"
+ android:key="startOnHeadphones"
+ android:defaultValue="false"/>
+ </PreferenceCategory>
+ </PreferenceScreen>
+</PreferenceScreen>
diff --git a/app/src/main/res/xml/starred_syncadapter.xml b/app/src/main/res/xml/starred_syncadapter.xml
new file mode 100644
index 00000000..d1d0e1e2
--- /dev/null
+++ b/app/src/main/res/xml/starred_syncadapter.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
+ android:contentAuthority="github.daneren2005.dsub.starred.provider"
+ android:accountType="subsonic.org"
+ android:userVisible="true"
+ android:supportsUploading="false"
+ android:allowParallelSyncs="false"
+ android:isAlwaysSyncable="true"/> \ No newline at end of file