From 48c9a75a7f364a39d4738db6b6a79afd08f3216b Mon Sep 17 00:00:00 2001 From: Andrew Rabert Date: Sun, 18 Dec 2016 12:41:30 -0500 Subject: [PATCH] Initial release --- .gitignore | 16 + .gitmodules | 3 + Audinaut.iml | 19 + README.md | 19 +- ServerProxy | 1 + app/.gitignore | 2 + app/build.gradle | 63 + app/libs/kryo-2.21-all.jar | Bin 0 -> 236628 bytes app/proguard.cfg | 62 + .../nvllsvm/audinaut/ApplicationTest.java | 13 + .../SubsonicFragmentActivityTest.java | 34 + .../audinaut/domain/GenreComparatorTest.java | 68 + .../audinaut/service/DownloadServiceTest.java | 296 +++ app/src/audinaut-stacktrace.txt | 26 + .../3.1/taskArtifacts/cache.properties | 1 + .../3.1/taskArtifacts/cache.properties.lock | Bin 0 -> 17 bytes .../3.1/taskArtifacts/fileSnapshots.bin | Bin 0 -> 18537 bytes .../3.1/taskArtifacts/taskArtifacts.bin | Bin 0 -> 18917 bytes app/src/main/AndroidManifest.xml | 171 ++ .../activity/EditPlayActionActivity.java | 245 ++ .../activity/QueryReceiverActivity.java | 85 + .../audinaut/activity/SettingsActivity.java | 58 + .../audinaut/activity/SubsonicActivity.java | 1055 ++++++++ .../activity/SubsonicFragmentActivity.java | 929 +++++++ .../activity/VoiceQueryReceiverActivity.java | 62 + .../adapter/AlphabeticalAlbumAdapter.java | 44 + .../audinaut/adapter/ArtistAdapter.java | 162 ++ .../audinaut/adapter/BasicListAdapter.java | 48 + .../audinaut/adapter/DetailsAdapter.java | 62 + .../audinaut/adapter/DownloadFileAdapter.java | 74 + .../audinaut/adapter/EntryGridAdapter.java | 156 ++ .../adapter/EntryInfiniteGridAdapter.java | 152 ++ .../adapter/ExpandableSectionAdapter.java | 150 ++ .../audinaut/adapter/GenreAdapter.java | 54 + .../nvllsvm/audinaut/adapter/MainAdapter.java | 96 + .../audinaut/adapter/PlaylistAdapter.java | 72 + .../audinaut/adapter/SearchAdapter.java | 140 + .../audinaut/adapter/SectionAdapter.java | 516 ++++ .../audinaut/adapter/SettingsAdapter.java | 121 + .../audiofx/AudioEffectsController.java | 69 + .../audinaut/audiofx/EqualizerController.java | 198 ++ .../audiofx/LoudnessEnhancerController.java | 77 + .../nvllsvm/audinaut/domain/Artist.java | 138 + .../github/nvllsvm/audinaut/domain/Genre.java | 69 + .../nvllsvm/audinaut/domain/Indexes.java | 87 + .../audinaut/domain/MusicDirectory.java | 628 +++++ .../nvllsvm/audinaut/domain/MusicFolder.java | 80 + .../nvllsvm/audinaut/domain/PlayerQueue.java | 30 + .../nvllsvm/audinaut/domain/PlayerState.java | 47 + .../nvllsvm/audinaut/domain/Playlist.java | 187 ++ .../nvllsvm/audinaut/domain/RemoteStatus.java | 63 + .../nvllsvm/audinaut/domain/RepeatMode.java | 28 + .../audinaut/domain/SearchCritera.java | 93 + .../nvllsvm/audinaut/domain/SearchResult.java | 62 + .../github/nvllsvm/audinaut/domain/User.java | 146 ++ .../nvllsvm/audinaut/domain/Version.java | 187 ++ .../audinaut/fragments/DownloadFragment.java | 190 ++ .../audinaut/fragments/EqualizerFragment.java | 459 ++++ .../audinaut/fragments/MainFragment.java | 357 +++ .../fragments/NowPlayingFragment.java | 1109 ++++++++ .../fragments/PreferenceCompatFragment.java | 334 +++ .../audinaut/fragments/SearchFragment.java | 291 +++ .../fragments/SelectArtistFragment.java | 253 ++ .../fragments/SelectDirectoryFragment.java | 840 ++++++ .../fragments/SelectGenreFragment.java | 77 + .../fragments/SelectPlaylistFragment.java | 341 +++ .../fragments/SelectRecyclerFragment.java | 219 ++ .../fragments/SelectYearFragment.java | 88 + .../audinaut/fragments/SettingsFragment.java | 806 ++++++ .../audinaut/fragments/SubsonicFragment.java | 1633 ++++++++++++ .../provider/AudinautSearchProvider.java | 209 ++ .../audinaut/provider/AudinautWidget4x1.java | 28 + .../audinaut/provider/AudinautWidget4x2.java | 28 + .../audinaut/provider/AudinautWidget4x3.java | 28 + .../audinaut/provider/AudinautWidget4x4.java | 28 + .../provider/AudinautWidgetProvider.java | 305 +++ .../provider/MostRecentStubProvider.java | 61 + .../provider/PlaylistStubProvider.java | 61 + .../audinaut/receiver/A2dpIntentReceiver.java | 47 + .../audinaut/receiver/AudioNoisyReceiver.java | 51 + .../audinaut/receiver/BootReceiver.java | 34 + .../receiver/HeadphonePlugReceiver.java | 40 + .../receiver/MediaButtonIntentReceiver.java | 57 + .../audinaut/receiver/PlayActionReceiver.java | 46 + .../audinaut/service/CachedMusicService.java | 1129 ++++++++ .../audinaut/service/DownloadFile.java | 633 +++++ .../audinaut/service/DownloadService.java | 2271 +++++++++++++++++ .../DownloadServiceLifecycleSupport.java | 430 ++++ .../service/HeadphoneListenerService.java | 66 + .../audinaut/service/MediaStoreService.java | 151 ++ .../audinaut/service/MusicService.java | 123 + .../audinaut/service/MusicServiceFactory.java | 36 + .../audinaut/service/OfflineException.java | 32 + .../audinaut/service/OfflineMusicService.java | 638 +++++ .../audinaut/service/RESTMusicService.java | 1366 ++++++++++ .../service/parser/AbstractParser.java | 159 ++ .../service/parser/EntryListParser.java | 66 + .../audinaut/service/parser/ErrorParser.java | 49 + .../audinaut/service/parser/GenreParser.java | 122 + .../service/parser/IndexesParser.java | 129 + .../parser/MusicDirectoryEntryParser.java | 79 + .../service/parser/MusicDirectoryParser.java | 107 + .../service/parser/MusicFoldersParser.java | 65 + .../service/parser/PlayQueueParser.java | 83 + .../service/parser/PlaylistParser.java | 63 + .../service/parser/PlaylistsParser.java | 71 + .../service/parser/RandomSongsParser.java | 60 + .../service/parser/ScanStatusParser.java | 56 + .../service/parser/SearchResult2Parser.java | 75 + .../service/parser/SearchResultParser.java | 65 + .../service/parser/SubsonicRESTException.java | 19 + .../service/parser/TopSongsParser.java | 58 + .../audinaut/service/parser/UserParser.java | 108 + .../service/ssl/SSLSocketFactory.java | 553 ++++ .../service/ssl/TrustManagerDecorator.java | 65 + .../service/ssl/TrustSelfSignedStrategy.java | 44 + .../audinaut/service/ssl/TrustStrategy.java | 57 + .../service/sync/AuthenticatorService.java | 90 + .../service/sync/SubsonicSyncAdapter.java | 200 ++ .../nvllsvm/audinaut/updates/Updater.java | 102 + .../audinaut/updates/UpdaterSongPress.java | 42 + .../nvllsvm/audinaut/util/BackgroundTask.java | 325 +++ .../nvllsvm/audinaut/util/CacheCleaner.java | 292 +++ .../nvllsvm/audinaut/util/Constants.java | 180 ++ .../util/DownloadFileItemHelperCallback.java | 115 + .../nvllsvm/audinaut/util/DrawableTint.java | 102 + .../audinaut/util/EnvironmentVariables.java | 20 + .../nvllsvm/audinaut/util/FileUtil.java | 800 ++++++ .../nvllsvm/audinaut/util/ImageLoader.java | 523 ++++ .../nvllsvm/audinaut/util/LoadingTask.java | 73 + .../nvllsvm/audinaut/util/MenuUtil.java | 83 + .../nvllsvm/audinaut/util/Notifications.java | 330 +++ .../github/nvllsvm/audinaut/util/Pair.java | 54 + .../audinaut/util/ProgressListener.java | 28 + .../audinaut/util/SettingsBackupAgent.java | 44 + .../audinaut/util/ShufflePlayBuffer.java | 212 ++ .../audinaut/util/SilentBackgroundTask.java | 48 + .../audinaut/util/SilentServiceTask.java | 41 + .../audinaut/util/SimpleServiceBinder.java | 37 + .../nvllsvm/audinaut/util/SongDBHandler.java | 260 ++ .../nvllsvm/audinaut/util/SyncUtil.java | 156 ++ .../audinaut/util/TabBackgroundTask.java | 51 + .../nvllsvm/audinaut/util/ThemeUtil.java | 98 + .../audinaut/util/TimeLimitedCache.java | 55 + .../nvllsvm/audinaut/util/UpdateHelper.java | 92 + .../nvllsvm/audinaut/util/UserUtil.java | 122 + .../github/nvllsvm/audinaut/util/Util.java | 1389 ++++++++++ .../nvllsvm/audinaut/util/tags/Bastp.java | 85 + .../nvllsvm/audinaut/util/tags/BastpUtil.java | 73 + .../nvllsvm/audinaut/util/tags/Common.java | 111 + .../nvllsvm/audinaut/util/tags/FlacFile.java | 85 + .../nvllsvm/audinaut/util/tags/ID3v2File.java | 180 ++ .../audinaut/util/tags/LameHeader.java | 70 + .../nvllsvm/audinaut/util/tags/OggFile.java | 114 + .../audinaut/view/AlbumListCountView.java | 130 + .../nvllsvm/audinaut/view/AlbumView.java | 116 + .../audinaut/view/ArtistEntryView.java | 69 + .../nvllsvm/audinaut/view/ArtistView.java | 70 + .../audinaut/view/AutoRepeatButton.java | 86 + .../audinaut/view/BasicHeaderView.java | 40 + .../nvllsvm/audinaut/view/BasicListView.java | 42 + .../view/CacheLocationPreference.java | 146 ++ .../nvllsvm/audinaut/view/CardView.java | 67 + .../nvllsvm/audinaut/view/ErrorDialog.java | 75 + .../audinaut/view/FadeOutAnimation.java | 77 + .../nvllsvm/audinaut/view/FastScroller.java | 335 +++ .../nvllsvm/audinaut/view/GenreView.java | 57 + .../audinaut/view/GridSpacingDecoration.java | 133 + .../audinaut/view/MyLeadingMarginSpan2.java | 34 + .../audinaut/view/PlaylistSongView.java | 95 + .../nvllsvm/audinaut/view/PlaylistView.java | 66 + .../audinaut/view/RecyclingImageView.java | 121 + .../audinaut/view/SeekBarPreference.java | 156 ++ .../nvllsvm/audinaut/view/SettingView.java | 88 + .../nvllsvm/audinaut/view/SongView.java | 239 ++ .../audinaut/view/SquareImageView.java | 32 + .../nvllsvm/audinaut/view/UpdateView.java | 310 +++ .../nvllsvm/audinaut/view/UpdateView2.java | 55 + app/src/main/res/anim/enter_from_left.xml | 12 + app/src/main/res/anim/enter_from_right.xml | 12 + app/src/main/res/anim/exit_to_left.xml | 12 + app/src/main/res/anim/exit_to_right.xml | 12 + app/src/main/res/anim/fade_in.xml | 5 + app/src/main/res/anim/fade_out.xml | 5 + app/src/main/res/anim/push_down_in.xml | 22 + app/src/main/res/anim/push_down_out.xml | 22 + app/src/main/res/anim/push_up_in.xml | 22 + app/src/main/res/anim/push_up_out.xml | 22 + .../drawable-hdpi/action_toggle_list_dark.png | Bin 0 -> 400 bytes .../action_toggle_list_light.png | Bin 0 -> 421 bytes .../drawable-hdpi/appwidget_art_default.png | Bin 0 -> 2847 bytes .../drawable-hdpi/appwidget_art_unknown.png | Bin 0 -> 2847 bytes .../main/res/drawable-hdpi/appwidget_bg.9.png | Bin 0 -> 349 bytes app/src/main/res/drawable-hdpi/background.png | Bin 0 -> 1626 bytes .../res/drawable-hdpi/download_cached.png | Bin 0 -> 905 bytes .../res/drawable-hdpi/download_none_dark.png | Bin 0 -> 154 bytes .../res/drawable-hdpi/download_none_light.png | Bin 0 -> 167 bytes .../res/drawable-hdpi/download_pinned.png | Bin 0 -> 907 bytes .../res/drawable-hdpi/downloading_dark.png | Bin 0 -> 263 bytes .../res/drawable-hdpi/downloading_light.png | Bin 0 -> 274 bytes .../res/drawable-hdpi/ic_action_add_dark.png | Bin 0 -> 138 bytes .../res/drawable-hdpi/ic_action_add_light.png | Bin 0 -> 145 bytes .../res/drawable-hdpi/ic_action_album.png | Bin 0 -> 407 bytes .../res/drawable-hdpi/ic_action_artist.png | Bin 0 -> 411 bytes .../ic_action_playback_speed_dark.png | Bin 0 -> 540 bytes .../ic_action_playback_speed_light.png | Bin 0 -> 578 bytes .../drawable-hdpi/ic_action_rating_bad.png | Bin 0 -> 440 bytes .../ic_action_rating_bad_dark.png | Bin 0 -> 344 bytes .../ic_action_rating_bad_light.png | Bin 0 -> 361 bytes .../ic_action_rating_bad_selected.png | Bin 0 -> 290 bytes .../drawable-hdpi/ic_action_rating_good.png | Bin 0 -> 430 bytes .../ic_action_rating_good_dark.png | Bin 0 -> 345 bytes .../ic_action_rating_good_light.png | Bin 0 -> 364 bytes .../ic_action_rating_good_selected.png | Bin 0 -> 294 bytes .../main/res/drawable-hdpi/ic_action_song.png | Bin 0 -> 241 bytes .../drawable-hdpi/ic_menu_add_person_dark.png | Bin 0 -> 401 bytes .../ic_menu_add_person_light.png | Bin 0 -> 425 bytes .../res/drawable-hdpi/ic_menu_admin_dark.png | Bin 0 -> 455 bytes .../res/drawable-hdpi/ic_menu_admin_light.png | Bin 0 -> 479 bytes .../drawable-hdpi/ic_menu_bookmark_dark.png | Bin 0 -> 234 bytes .../drawable-hdpi/ic_menu_bookmark_light.png | Bin 0 -> 248 bytes .../ic_menu_bookmark_selected.png | Bin 0 -> 206 bytes .../res/drawable-hdpi/ic_menu_chat_dark.png | Bin 0 -> 244 bytes .../res/drawable-hdpi/ic_menu_chat_light.png | Bin 0 -> 256 bytes .../drawable-hdpi/ic_menu_chat_send_dark.png | Bin 0 -> 355 bytes .../drawable-hdpi/ic_menu_chat_send_light.png | Bin 0 -> 365 bytes .../drawable-hdpi/ic_menu_download_dark.png | Bin 0 -> 239 bytes .../drawable-hdpi/ic_menu_download_light.png | Bin 0 -> 255 bytes .../drawable-hdpi/ic_menu_library_dark.png | Bin 0 -> 207 bytes .../drawable-hdpi/ic_menu_library_light.png | Bin 0 -> 222 bytes .../drawable-hdpi/ic_menu_password_dark.png | Bin 0 -> 309 bytes .../drawable-hdpi/ic_menu_password_light.png | Bin 0 -> 323 bytes .../drawable-hdpi/ic_menu_playlist_dark.png | Bin 0 -> 340 bytes .../drawable-hdpi/ic_menu_playlist_light.png | Bin 0 -> 370 bytes .../drawable-hdpi/ic_menu_podcast_dark.png | Bin 0 -> 428 bytes .../drawable-hdpi/ic_menu_podcast_light.png | Bin 0 -> 469 bytes .../res/drawable-hdpi/ic_menu_radio_dark.png | Bin 0 -> 434 bytes .../res/drawable-hdpi/ic_menu_radio_light.png | Bin 0 -> 482 bytes .../drawable-hdpi/ic_menu_refresh_dark.png | Bin 0 -> 451 bytes .../drawable-hdpi/ic_menu_refresh_light.png | Bin 0 -> 494 bytes .../res/drawable-hdpi/ic_menu_remove_dark.png | Bin 0 -> 240 bytes .../drawable-hdpi/ic_menu_remove_light.png | Bin 0 -> 256 bytes .../res/drawable-hdpi/ic_menu_save_dark.png | Bin 0 -> 314 bytes .../res/drawable-hdpi/ic_menu_save_light.png | Bin 0 -> 335 bytes .../res/drawable-hdpi/ic_menu_search_dark.png | Bin 0 -> 503 bytes .../drawable-hdpi/ic_menu_search_light.png | Bin 0 -> 555 bytes .../drawable-hdpi/ic_menu_settings_dark.png | Bin 0 -> 453 bytes .../drawable-hdpi/ic_menu_settings_light.png | Bin 0 -> 489 bytes .../res/drawable-hdpi/ic_menu_share_dark.png | Bin 0 -> 458 bytes .../res/drawable-hdpi/ic_menu_share_light.png | Bin 0 -> 478 bytes .../drawable-hdpi/ic_menu_shuffle_dark.png | Bin 0 -> 405 bytes .../drawable-hdpi/ic_menu_shuffle_light.png | Bin 0 -> 423 bytes .../res/drawable-hdpi/ic_number_border.png | Bin 0 -> 1201 bytes .../res/drawable-hdpi/ic_social_person.png | Bin 0 -> 922 bytes .../res/drawable-hdpi/ic_toggle_played.png | Bin 0 -> 501 bytes .../main/res/drawable-hdpi/ic_toggle_star.png | Bin 0 -> 491 bytes .../drawable-hdpi/ic_toggle_star_outline.png | Bin 0 -> 718 bytes .../ic_toggle_star_outline_dark.png | Bin 0 -> 512 bytes .../ic_toggle_star_outline_light.png | Bin 0 -> 569 bytes app/src/main/res/drawable-hdpi/launch.png | Bin 0 -> 6450 bytes .../res/drawable-hdpi/main_offline_dark.png | Bin 0 -> 387 bytes .../res/drawable-hdpi/main_offline_light.png | Bin 0 -> 415 bytes .../drawable-hdpi/main_select_server_dark.png | Bin 0 -> 238 bytes .../main_select_server_light.png | Bin 0 -> 260 bytes .../drawable-hdpi/main_select_tabs_dark.png | Bin 0 -> 230 bytes .../drawable-hdpi/main_select_tabs_light.png | Bin 0 -> 243 bytes .../res/drawable-hdpi/media_backward_dark.png | Bin 0 -> 593 bytes .../drawable-hdpi/media_backward_light.png | Bin 0 -> 597 bytes .../drawable-hdpi/media_fastforward_dark.png | Bin 0 -> 521 bytes .../drawable-hdpi/media_fastforward_light.png | Bin 0 -> 557 bytes .../res/drawable-hdpi/media_forward_dark.png | Bin 0 -> 447 bytes .../res/drawable-hdpi/media_forward_light.png | Bin 0 -> 486 bytes .../res/drawable-hdpi/media_pause_dark.png | Bin 0 -> 166 bytes .../res/drawable-hdpi/media_pause_light.png | Bin 0 -> 166 bytes .../drawable-hdpi/media_repeat_all_dark.png | Bin 0 -> 876 bytes .../drawable-hdpi/media_repeat_all_light.png | Bin 0 -> 935 bytes .../drawable-hdpi/media_repeat_off_dark.png | Bin 0 -> 486 bytes .../drawable-hdpi/media_repeat_off_light.png | Bin 0 -> 500 bytes .../media_repeat_single_dark.png | Bin 0 -> 570 bytes .../media_repeat_single_light.png | Bin 0 -> 600 bytes .../res/drawable-hdpi/media_rewind_dark.png | Bin 0 -> 563 bytes .../res/drawable-hdpi/media_rewind_light.png | Bin 0 -> 582 bytes .../res/drawable-hdpi/media_start_dark.png | Bin 0 -> 364 bytes .../res/drawable-hdpi/media_start_light.png | Bin 0 -> 369 bytes .../res/drawable-hdpi/media_stop_dark.png | Bin 0 -> 114 bytes .../res/drawable-hdpi/media_stop_light.png | Bin 0 -> 114 bytes .../drawable-hdpi/notification_close_dark.png | Bin 0 -> 281 bytes .../notification_close_light.png | Bin 0 -> 268 bytes .../main/res/drawable-hdpi/playing_dark.png | Bin 0 -> 254 bytes .../main/res/drawable-hdpi/playing_light.png | Bin 0 -> 266 bytes .../res/drawable-hdpi/stat_notify_playing.png | Bin 0 -> 238 bytes .../res/drawable-hdpi/stat_notify_sync.png | Bin 0 -> 436 bytes .../main/res/drawable-hdpi/unknown_album.png | Bin 0 -> 3971 bytes .../res/drawable-hdpi/unknown_album_large.png | Bin 0 -> 36093 bytes .../main/res/drawable-large/unknown_album.png | Bin 0 -> 7445 bytes .../drawable-mdpi/action_toggle_list_dark.png | Bin 0 -> 309 bytes .../action_toggle_list_light.png | Bin 0 -> 327 bytes .../res/drawable-mdpi/download_cached.png | Bin 0 -> 656 bytes .../res/drawable-mdpi/download_none_dark.png | Bin 0 -> 120 bytes .../res/drawable-mdpi/download_none_light.png | Bin 0 -> 127 bytes .../res/drawable-mdpi/download_pinned.png | Bin 0 -> 610 bytes .../res/drawable-mdpi/downloading_dark.png | Bin 0 -> 187 bytes .../res/drawable-mdpi/downloading_light.png | Bin 0 -> 189 bytes .../res/drawable-mdpi/ic_action_add_dark.png | Bin 0 -> 140 bytes .../res/drawable-mdpi/ic_action_add_light.png | Bin 0 -> 140 bytes .../res/drawable-mdpi/ic_action_album.png | Bin 0 -> 284 bytes .../res/drawable-mdpi/ic_action_artist.png | Bin 0 -> 267 bytes .../ic_action_playback_speed_dark.png | Bin 0 -> 365 bytes .../ic_action_playback_speed_light.png | Bin 0 -> 384 bytes .../drawable-mdpi/ic_action_rating_bad.png | Bin 0 -> 263 bytes .../ic_action_rating_bad_dark.png | Bin 0 -> 240 bytes .../ic_action_rating_bad_light.png | Bin 0 -> 246 bytes .../ic_action_rating_bad_selected.png | Bin 0 -> 197 bytes .../drawable-mdpi/ic_action_rating_good.png | Bin 0 -> 260 bytes .../ic_action_rating_good_dark.png | Bin 0 -> 232 bytes .../ic_action_rating_good_light.png | Bin 0 -> 241 bytes .../ic_action_rating_good_selected.png | Bin 0 -> 197 bytes .../main/res/drawable-mdpi/ic_action_song.png | Bin 0 -> 158 bytes .../drawable-mdpi/ic_action_volume_dark.png | Bin 0 -> 466 bytes .../drawable-mdpi/ic_action_volume_light.png | Bin 0 -> 501 bytes .../drawable-mdpi/ic_menu_add_person_dark.png | Bin 0 -> 289 bytes .../ic_menu_add_person_light.png | Bin 0 -> 307 bytes .../res/drawable-mdpi/ic_menu_admin_dark.png | Bin 0 -> 332 bytes .../res/drawable-mdpi/ic_menu_admin_light.png | Bin 0 -> 372 bytes .../drawable-mdpi/ic_menu_bookmark_dark.png | Bin 0 -> 163 bytes .../drawable-mdpi/ic_menu_bookmark_light.png | Bin 0 -> 163 bytes .../ic_menu_bookmark_selected.png | Bin 0 -> 150 bytes .../res/drawable-mdpi/ic_menu_chat_dark.png | Bin 0 -> 194 bytes .../res/drawable-mdpi/ic_menu_chat_light.png | Bin 0 -> 190 bytes .../drawable-mdpi/ic_menu_chat_send_dark.png | Bin 0 -> 237 bytes .../drawable-mdpi/ic_menu_chat_send_light.png | Bin 0 -> 231 bytes .../drawable-mdpi/ic_menu_download_dark.png | Bin 0 -> 167 bytes .../drawable-mdpi/ic_menu_download_light.png | Bin 0 -> 169 bytes .../drawable-mdpi/ic_menu_library_dark.png | Bin 0 -> 170 bytes .../drawable-mdpi/ic_menu_library_light.png | Bin 0 -> 170 bytes .../drawable-mdpi/ic_menu_password_dark.png | Bin 0 -> 234 bytes .../drawable-mdpi/ic_menu_password_light.png | Bin 0 -> 237 bytes .../drawable-mdpi/ic_menu_playlist_dark.png | Bin 0 -> 266 bytes .../drawable-mdpi/ic_menu_playlist_light.png | Bin 0 -> 274 bytes .../drawable-mdpi/ic_menu_podcast_dark.png | Bin 0 -> 290 bytes .../drawable-mdpi/ic_menu_podcast_light.png | Bin 0 -> 325 bytes .../res/drawable-mdpi/ic_menu_radio_dark.png | Bin 0 -> 309 bytes .../res/drawable-mdpi/ic_menu_radio_light.png | Bin 0 -> 339 bytes .../drawable-mdpi/ic_menu_refresh_dark.png | Bin 0 -> 292 bytes .../drawable-mdpi/ic_menu_refresh_light.png | Bin 0 -> 314 bytes .../res/drawable-mdpi/ic_menu_remove_dark.png | Bin 0 -> 162 bytes .../drawable-mdpi/ic_menu_remove_light.png | Bin 0 -> 171 bytes .../res/drawable-mdpi/ic_menu_save_dark.png | Bin 0 -> 203 bytes .../res/drawable-mdpi/ic_menu_save_light.png | Bin 0 -> 219 bytes .../res/drawable-mdpi/ic_menu_search_dark.png | Bin 0 -> 323 bytes .../drawable-mdpi/ic_menu_search_light.png | Bin 0 -> 358 bytes .../drawable-mdpi/ic_menu_settings_dark.png | Bin 0 -> 282 bytes .../drawable-mdpi/ic_menu_settings_light.png | Bin 0 -> 293 bytes .../res/drawable-mdpi/ic_menu_share_dark.png | Bin 0 -> 303 bytes .../res/drawable-mdpi/ic_menu_share_light.png | Bin 0 -> 310 bytes .../drawable-mdpi/ic_menu_shuffle_dark.png | Bin 0 -> 263 bytes .../drawable-mdpi/ic_menu_shuffle_light.png | Bin 0 -> 294 bytes .../res/drawable-mdpi/ic_number_border.png | Bin 0 -> 747 bytes .../res/drawable-mdpi/ic_social_person.png | Bin 0 -> 636 bytes .../res/drawable-mdpi/ic_toggle_played.png | Bin 0 -> 299 bytes .../main/res/drawable-mdpi/ic_toggle_star.png | Bin 0 -> 341 bytes .../drawable-mdpi/ic_toggle_star_outline.png | Bin 0 -> 476 bytes .../ic_toggle_star_outline_dark.png | Bin 0 -> 349 bytes .../ic_toggle_star_outline_light.png | Bin 0 -> 387 bytes app/src/main/res/drawable-mdpi/launch.png | Bin 0 -> 3433 bytes .../res/drawable-mdpi/main_offline_dark.png | Bin 0 -> 262 bytes .../res/drawable-mdpi/main_offline_light.png | Bin 0 -> 279 bytes .../drawable-mdpi/main_select_server_dark.png | Bin 0 -> 172 bytes .../main_select_server_light.png | Bin 0 -> 177 bytes .../drawable-mdpi/main_select_tabs_dark.png | Bin 0 -> 161 bytes .../drawable-mdpi/main_select_tabs_light.png | Bin 0 -> 167 bytes .../res/drawable-mdpi/media_backward_dark.png | Bin 0 -> 397 bytes .../drawable-mdpi/media_backward_light.png | Bin 0 -> 412 bytes .../drawable-mdpi/media_fastforward_dark.png | Bin 0 -> 347 bytes .../drawable-mdpi/media_fastforward_light.png | Bin 0 -> 366 bytes .../res/drawable-mdpi/media_forward_dark.png | Bin 0 -> 306 bytes .../res/drawable-mdpi/media_forward_light.png | Bin 0 -> 326 bytes .../res/drawable-mdpi/media_pause_dark.png | Bin 0 -> 137 bytes .../res/drawable-mdpi/media_pause_light.png | Bin 0 -> 141 bytes .../drawable-mdpi/media_repeat_all_dark.png | Bin 0 -> 617 bytes .../drawable-mdpi/media_repeat_all_light.png | Bin 0 -> 681 bytes .../drawable-mdpi/media_repeat_off_dark.png | Bin 0 -> 344 bytes .../drawable-mdpi/media_repeat_off_light.png | Bin 0 -> 356 bytes .../media_repeat_single_dark.png | Bin 0 -> 405 bytes .../media_repeat_single_light.png | Bin 0 -> 423 bytes .../res/drawable-mdpi/media_rewind_dark.png | Bin 0 -> 373 bytes .../res/drawable-mdpi/media_rewind_light.png | Bin 0 -> 385 bytes .../res/drawable-mdpi/media_start_dark.png | Bin 0 -> 294 bytes .../res/drawable-mdpi/media_start_light.png | Bin 0 -> 308 bytes .../res/drawable-mdpi/media_stop_dark.png | Bin 0 -> 112 bytes .../res/drawable-mdpi/media_stop_light.png | Bin 0 -> 112 bytes .../drawable-mdpi/notification_close_dark.png | Bin 0 -> 173 bytes .../notification_close_light.png | Bin 0 -> 205 bytes .../main/res/drawable-mdpi/playing_dark.png | Bin 0 -> 188 bytes .../main/res/drawable-mdpi/playing_light.png | Bin 0 -> 186 bytes .../res/drawable-mdpi/stat_notify_playing.png | Bin 0 -> 191 bytes .../res/drawable-mdpi/stat_notify_sync.png | Bin 0 -> 376 bytes .../drawable-v21/notification_backward.xml | 4 + .../res/drawable-v21/notification_close.xml | 4 + .../drawable-v21/notification_fastforward.xml | 4 + .../res/drawable-v21/notification_forward.xml | 4 + .../res/drawable-v21/notification_pause.xml | 4 + .../res/drawable-v21/notification_rewind.xml | 4 + .../res/drawable-v21/notification_start.xml | 4 + .../action_toggle_list_dark.png | Bin 0 -> 498 bytes .../action_toggle_list_light.png | Bin 0 -> 528 bytes .../res/drawable-xhdpi/download_cached.png | Bin 0 -> 1181 bytes .../res/drawable-xhdpi/download_none_dark.png | Bin 0 -> 193 bytes .../drawable-xhdpi/download_none_light.png | Bin 0 -> 204 bytes .../res/drawable-xhdpi/download_pinned.png | Bin 0 -> 1144 bytes .../res/drawable-xhdpi/downloading_dark.png | Bin 0 -> 285 bytes .../res/drawable-xhdpi/downloading_light.png | Bin 0 -> 305 bytes .../res/drawable-xhdpi/ic_action_add_dark.png | Bin 0 -> 191 bytes .../drawable-xhdpi/ic_action_add_light.png | Bin 0 -> 202 bytes .../res/drawable-xhdpi/ic_action_album.png | Bin 0 -> 586 bytes .../res/drawable-xhdpi/ic_action_artist.png | Bin 0 -> 521 bytes .../ic_action_playback_speed_dark.png | Bin 0 -> 744 bytes .../ic_action_playback_speed_light.png | Bin 0 -> 785 bytes .../drawable-xhdpi/ic_action_rating_bad.png | Bin 0 -> 425 bytes .../ic_action_rating_bad_dark.png | Bin 0 -> 410 bytes .../ic_action_rating_bad_light.png | Bin 0 -> 438 bytes .../ic_action_rating_bad_selected.png | Bin 0 -> 308 bytes .../drawable-xhdpi/ic_action_rating_good.png | Bin 0 -> 430 bytes .../ic_action_rating_good_dark.png | Bin 0 -> 407 bytes .../ic_action_rating_good_light.png | Bin 0 -> 433 bytes .../ic_action_rating_good_selected.png | Bin 0 -> 370 bytes .../res/drawable-xhdpi/ic_action_song.png | Bin 0 -> 268 bytes .../drawable-xhdpi/ic_action_volume_dark.png | Bin 0 -> 969 bytes .../drawable-xhdpi/ic_action_volume_light.png | Bin 0 -> 1031 bytes .../ic_menu_add_person_dark.png | Bin 0 -> 513 bytes .../ic_menu_add_person_light.png | Bin 0 -> 548 bytes .../res/drawable-xhdpi/ic_menu_admin_dark.png | Bin 0 -> 605 bytes .../drawable-xhdpi/ic_menu_admin_light.png | Bin 0 -> 658 bytes .../drawable-xhdpi/ic_menu_bookmark_dark.png | Bin 0 -> 265 bytes .../drawable-xhdpi/ic_menu_bookmark_light.png | Bin 0 -> 283 bytes .../ic_menu_bookmark_selected.png | Bin 0 -> 248 bytes .../res/drawable-xhdpi/ic_menu_chat_dark.png | Bin 0 -> 284 bytes .../res/drawable-xhdpi/ic_menu_chat_light.png | Bin 0 -> 299 bytes .../drawable-xhdpi/ic_menu_chat_send_dark.png | Bin 0 -> 405 bytes .../ic_menu_chat_send_light.png | Bin 0 -> 435 bytes .../drawable-xhdpi/ic_menu_download_dark.png | Bin 0 -> 237 bytes .../drawable-xhdpi/ic_menu_download_light.png | Bin 0 -> 249 bytes .../drawable-xhdpi/ic_menu_library_dark.png | Bin 0 -> 252 bytes .../drawable-xhdpi/ic_menu_library_light.png | Bin 0 -> 266 bytes .../drawable-xhdpi/ic_menu_password_dark.png | Bin 0 -> 398 bytes .../drawable-xhdpi/ic_menu_password_light.png | Bin 0 -> 416 bytes .../drawable-xhdpi/ic_menu_playlist_dark.png | Bin 0 -> 420 bytes .../drawable-xhdpi/ic_menu_playlist_light.png | Bin 0 -> 448 bytes .../drawable-xhdpi/ic_menu_podcast_dark.png | Bin 0 -> 514 bytes .../drawable-xhdpi/ic_menu_podcast_light.png | Bin 0 -> 563 bytes .../res/drawable-xhdpi/ic_menu_radio_dark.png | Bin 0 -> 531 bytes .../drawable-xhdpi/ic_menu_radio_light.png | Bin 0 -> 580 bytes .../drawable-xhdpi/ic_menu_refresh_dark.png | Bin 0 -> 583 bytes .../drawable-xhdpi/ic_menu_refresh_light.png | Bin 0 -> 636 bytes .../drawable-xhdpi/ic_menu_remove_dark.png | Bin 0 -> 269 bytes .../drawable-xhdpi/ic_menu_remove_light.png | Bin 0 -> 268 bytes .../res/drawable-xhdpi/ic_menu_save_dark.png | Bin 0 -> 350 bytes .../res/drawable-xhdpi/ic_menu_save_light.png | Bin 0 -> 376 bytes .../drawable-xhdpi/ic_menu_search_dark.png | Bin 0 -> 632 bytes .../drawable-xhdpi/ic_menu_search_light.png | Bin 0 -> 685 bytes .../drawable-xhdpi/ic_menu_settings_dark.png | Bin 0 -> 581 bytes .../drawable-xhdpi/ic_menu_settings_light.png | Bin 0 -> 624 bytes .../res/drawable-xhdpi/ic_menu_share_dark.png | Bin 0 -> 568 bytes .../drawable-xhdpi/ic_menu_share_light.png | Bin 0 -> 615 bytes .../drawable-xhdpi/ic_menu_shuffle_dark.png | Bin 0 -> 431 bytes .../drawable-xhdpi/ic_menu_shuffle_light.png | Bin 0 -> 470 bytes .../res/drawable-xhdpi/ic_number_border.png | Bin 0 -> 1591 bytes .../res/drawable-xhdpi/ic_social_person.png | Bin 0 -> 1172 bytes .../res/drawable-xhdpi/ic_toggle_played.png | Bin 0 -> 620 bytes .../res/drawable-xhdpi/ic_toggle_star.png | Bin 0 -> 657 bytes .../drawable-xhdpi/ic_toggle_star_outline.png | Bin 0 -> 985 bytes .../ic_toggle_star_outline_dark.png | Bin 0 -> 674 bytes .../ic_toggle_star_outline_light.png | Bin 0 -> 747 bytes app/src/main/res/drawable-xhdpi/launch.png | Bin 0 -> 10697 bytes .../res/drawable-xhdpi/main_offline_dark.png | Bin 0 -> 491 bytes .../res/drawable-xhdpi/main_offline_light.png | Bin 0 -> 537 bytes .../main_select_server_dark.png | Bin 0 -> 281 bytes .../main_select_server_light.png | Bin 0 -> 287 bytes .../drawable-xhdpi/main_select_tabs_dark.png | Bin 0 -> 266 bytes .../drawable-xhdpi/main_select_tabs_light.png | Bin 0 -> 279 bytes .../drawable-xhdpi/media_backward_dark.png | Bin 0 -> 802 bytes .../drawable-xhdpi/media_backward_light.png | Bin 0 -> 806 bytes .../drawable-xhdpi/media_fastforward_dark.png | Bin 0 -> 681 bytes .../media_fastforward_light.png | Bin 0 -> 732 bytes .../res/drawable-xhdpi/media_forward_dark.png | Bin 0 -> 597 bytes .../drawable-xhdpi/media_forward_light.png | Bin 0 -> 635 bytes .../res/drawable-xhdpi/media_pause_dark.png | Bin 0 -> 192 bytes .../res/drawable-xhdpi/media_pause_light.png | Bin 0 -> 192 bytes .../drawable-xhdpi/media_repeat_all_dark.png | Bin 0 -> 1215 bytes .../drawable-xhdpi/media_repeat_all_light.png | Bin 0 -> 1295 bytes .../drawable-xhdpi/media_repeat_off_dark.png | Bin 0 -> 578 bytes .../drawable-xhdpi/media_repeat_off_light.png | Bin 0 -> 567 bytes .../media_repeat_single_dark.png | Bin 0 -> 710 bytes .../media_repeat_single_light.png | Bin 0 -> 724 bytes .../res/drawable-xhdpi/media_rewind_dark.png | Bin 0 -> 746 bytes .../res/drawable-xhdpi/media_rewind_light.png | Bin 0 -> 773 bytes .../res/drawable-xhdpi/media_start_dark.png | Bin 0 -> 579 bytes .../res/drawable-xhdpi/media_start_light.png | Bin 0 -> 599 bytes .../res/drawable-xhdpi/media_stop_dark.png | Bin 0 -> 121 bytes .../res/drawable-xhdpi/media_stop_light.png | Bin 0 -> 121 bytes .../notification_close_dark.png | Bin 0 -> 289 bytes .../notification_close_light.png | Bin 0 -> 338 bytes .../main/res/drawable-xhdpi/playing_dark.png | Bin 0 -> 320 bytes .../main/res/drawable-xhdpi/playing_light.png | Bin 0 -> 337 bytes .../drawable-xhdpi/stat_notify_playing.png | Bin 0 -> 308 bytes .../res/drawable-xhdpi/stat_notify_sync.png | Bin 0 -> 654 bytes .../action_toggle_list_dark.png | Bin 0 -> 765 bytes .../action_toggle_list_light.png | Bin 0 -> 812 bytes .../res/drawable-xxhdpi/download_cached.png | Bin 0 -> 1716 bytes .../drawable-xxhdpi/download_none_dark.png | Bin 0 -> 258 bytes .../drawable-xxhdpi/download_none_light.png | Bin 0 -> 269 bytes .../res/drawable-xxhdpi/download_pinned.png | Bin 0 -> 1701 bytes .../res/drawable-xxhdpi/downloading_dark.png | Bin 0 -> 338 bytes .../res/drawable-xxhdpi/downloading_light.png | Bin 0 -> 358 bytes .../drawable-xxhdpi/ic_action_add_dark.png | Bin 0 -> 179 bytes .../drawable-xxhdpi/ic_action_add_light.png | Bin 0 -> 188 bytes .../res/drawable-xxhdpi/ic_action_artist.png | Bin 0 -> 666 bytes .../ic_action_playback_speed_dark.png | Bin 0 -> 1116 bytes .../ic_action_playback_speed_light.png | Bin 0 -> 1196 bytes .../drawable-xxhdpi/ic_action_rating_bad.png | Bin 0 -> 770 bytes .../ic_action_rating_bad_dark.png | Bin 0 -> 645 bytes .../ic_action_rating_bad_light.png | Bin 0 -> 548 bytes .../ic_action_rating_bad_selected.png | Bin 0 -> 562 bytes .../drawable-xxhdpi/ic_action_rating_good.png | Bin 0 -> 776 bytes .../ic_action_rating_good_dark.png | Bin 0 -> 628 bytes .../ic_action_rating_good_light.png | Bin 0 -> 542 bytes .../ic_action_rating_good_selected.png | Bin 0 -> 536 bytes .../res/drawable-xxhdpi/ic_action_song.png | Bin 0 -> 363 bytes .../drawable-xxhdpi/ic_action_volume_dark.png | Bin 0 -> 1476 bytes .../ic_action_volume_light.png | Bin 0 -> 1555 bytes .../ic_menu_add_person_dark.png | Bin 0 -> 589 bytes .../ic_menu_add_person_light.png | Bin 0 -> 628 bytes .../drawable-xxhdpi/ic_menu_admin_dark.png | Bin 0 -> 716 bytes .../drawable-xxhdpi/ic_menu_admin_light.png | Bin 0 -> 772 bytes .../drawable-xxhdpi/ic_menu_bookmark_dark.png | Bin 0 -> 332 bytes .../ic_menu_bookmark_light.png | Bin 0 -> 356 bytes .../ic_menu_bookmark_selected.png | Bin 0 -> 334 bytes .../res/drawable-xxhdpi/ic_menu_chat_dark.png | Bin 0 -> 372 bytes .../drawable-xxhdpi/ic_menu_chat_light.png | Bin 0 -> 340 bytes .../ic_menu_chat_send_dark.png | Bin 0 -> 681 bytes .../ic_menu_chat_send_light.png | Bin 0 -> 741 bytes .../drawable-xxhdpi/ic_menu_download_dark.png | Bin 0 -> 317 bytes .../ic_menu_download_light.png | Bin 0 -> 324 bytes .../drawable-xxhdpi/ic_menu_library_dark.png | Bin 0 -> 293 bytes .../drawable-xxhdpi/ic_menu_library_light.png | Bin 0 -> 313 bytes .../drawable-xxhdpi/ic_menu_password_dark.png | Bin 0 -> 581 bytes .../ic_menu_password_light.png | Bin 0 -> 614 bytes .../drawable-xxhdpi/ic_menu_playlist_dark.png | Bin 0 -> 574 bytes .../ic_menu_playlist_light.png | Bin 0 -> 519 bytes .../drawable-xxhdpi/ic_menu_podcast_dark.png | Bin 0 -> 676 bytes .../drawable-xxhdpi/ic_menu_podcast_light.png | Bin 0 -> 721 bytes .../drawable-xxhdpi/ic_menu_radio_dark.png | Bin 0 -> 649 bytes .../drawable-xxhdpi/ic_menu_radio_light.png | Bin 0 -> 701 bytes .../drawable-xxhdpi/ic_menu_refresh_dark.png | Bin 0 -> 799 bytes .../drawable-xxhdpi/ic_menu_refresh_light.png | Bin 0 -> 850 bytes .../drawable-xxhdpi/ic_menu_remove_dark.png | Bin 0 -> 326 bytes .../drawable-xxhdpi/ic_menu_remove_light.png | Bin 0 -> 328 bytes .../res/drawable-xxhdpi/ic_menu_save_dark.png | Bin 0 -> 627 bytes .../drawable-xxhdpi/ic_menu_save_light.png | Bin 0 -> 508 bytes .../drawable-xxhdpi/ic_menu_search_dark.png | Bin 0 -> 826 bytes .../drawable-xxhdpi/ic_menu_search_light.png | Bin 0 -> 884 bytes .../drawable-xxhdpi/ic_menu_settings_dark.png | Bin 0 -> 930 bytes .../ic_menu_settings_light.png | Bin 0 -> 991 bytes .../drawable-xxhdpi/ic_menu_share_dark.png | Bin 0 -> 784 bytes .../drawable-xxhdpi/ic_menu_share_light.png | Bin 0 -> 843 bytes .../drawable-xxhdpi/ic_menu_shuffle_dark.png | Bin 0 -> 705 bytes .../drawable-xxhdpi/ic_menu_shuffle_light.png | Bin 0 -> 729 bytes .../res/drawable-xxhdpi/ic_number_border.png | Bin 0 -> 2383 bytes .../res/drawable-xxhdpi/ic_social_person.png | Bin 0 -> 1897 bytes .../res/drawable-xxhdpi/ic_toggle_played.png | Bin 0 -> 1047 bytes .../res/drawable-xxhdpi/ic_toggle_star.png | Bin 0 -> 948 bytes .../ic_toggle_star_outline.png | Bin 0 -> 1430 bytes .../ic_toggle_star_outline_dark.png | Bin 0 -> 991 bytes .../ic_toggle_star_outline_light.png | Bin 0 -> 1065 bytes app/src/main/res/drawable-xxhdpi/launch.png | Bin 0 -> 18684 bytes .../res/drawable-xxhdpi/main_offline_dark.png | Bin 0 -> 611 bytes .../drawable-xxhdpi/main_offline_light.png | Bin 0 -> 652 bytes .../main_select_server_dark.png | Bin 0 -> 414 bytes .../main_select_server_light.png | Bin 0 -> 435 bytes .../drawable-xxhdpi/main_select_tabs_dark.png | Bin 0 -> 414 bytes .../main_select_tabs_light.png | Bin 0 -> 428 bytes .../drawable-xxhdpi/media_backward_dark.png | Bin 0 -> 941 bytes .../drawable-xxhdpi/media_backward_light.png | Bin 0 -> 910 bytes .../media_fastforward_dark.png | Bin 0 -> 1032 bytes .../media_fastforward_light.png | Bin 0 -> 1100 bytes .../drawable-xxhdpi/media_forward_dark.png | Bin 0 -> 924 bytes .../drawable-xxhdpi/media_forward_light.png | Bin 0 -> 907 bytes .../res/drawable-xxhdpi/media_pause_dark.png | Bin 0 -> 208 bytes .../res/drawable-xxhdpi/media_pause_light.png | Bin 0 -> 208 bytes .../drawable-xxhdpi/media_repeat_all_dark.png | Bin 0 -> 1877 bytes .../media_repeat_all_light.png | Bin 0 -> 1976 bytes .../drawable-xxhdpi/media_repeat_off_dark.png | Bin 0 -> 798 bytes .../media_repeat_off_light.png | Bin 0 -> 782 bytes .../media_repeat_single_dark.png | Bin 0 -> 967 bytes .../media_repeat_single_light.png | Bin 0 -> 962 bytes .../res/drawable-xxhdpi/media_rewind_dark.png | Bin 0 -> 1110 bytes .../drawable-xxhdpi/media_rewind_light.png | Bin 0 -> 1138 bytes .../res/drawable-xxhdpi/media_start_dark.png | Bin 0 -> 718 bytes .../res/drawable-xxhdpi/media_start_light.png | Bin 0 -> 710 bytes .../res/drawable-xxhdpi/media_stop_dark.png | Bin 0 -> 130 bytes .../res/drawable-xxhdpi/media_stop_light.png | Bin 0 -> 130 bytes .../notification_close_dark.png | Bin 0 -> 590 bytes .../notification_close_light.png | Bin 0 -> 611 bytes .../main/res/drawable-xxhdpi/playing_dark.png | Bin 0 -> 514 bytes .../res/drawable-xxhdpi/playing_light.png | Bin 0 -> 547 bytes .../drawable-xxhdpi/stat_notify_playing.png | Bin 0 -> 424 bytes .../res/drawable-xxhdpi/stat_notify_sync.png | Bin 0 -> 1190 bytes .../action_toggle_list_dark.png | Bin 0 -> 1140 bytes .../action_toggle_list_light.png | Bin 0 -> 1067 bytes .../drawable-xxxhdpi/download_none_dark.png | Bin 0 -> 396 bytes .../drawable-xxxhdpi/download_none_light.png | Bin 0 -> 419 bytes .../res/drawable-xxxhdpi/downloading_dark.png | Bin 0 -> 499 bytes .../drawable-xxxhdpi/downloading_light.png | Bin 0 -> 533 bytes .../drawable-xxxhdpi/ic_action_add_dark.png | Bin 0 -> 315 bytes .../drawable-xxxhdpi/ic_action_add_light.png | Bin 0 -> 326 bytes .../res/drawable-xxxhdpi/ic_action_artist.png | Bin 0 -> 1043 bytes .../ic_action_playback_speed_dark.png | Bin 0 -> 1772 bytes .../ic_action_playback_speed_light.png | Bin 0 -> 1900 bytes .../drawable-xxxhdpi/ic_action_rating_bad.png | Bin 0 -> 580 bytes .../ic_action_rating_bad_dark.png | Bin 0 -> 763 bytes .../ic_action_rating_bad_light.png | Bin 0 -> 804 bytes .../ic_action_rating_bad_selected.png | Bin 0 -> 615 bytes .../ic_action_rating_good.png | Bin 0 -> 571 bytes .../ic_action_rating_good_dark.png | Bin 0 -> 744 bytes .../ic_action_rating_good_light.png | Bin 0 -> 800 bytes .../ic_action_rating_good_selected.png | Bin 0 -> 599 bytes .../res/drawable-xxxhdpi/ic_action_song.png | Bin 0 -> 527 bytes .../ic_menu_add_person_dark.png | Bin 0 -> 914 bytes .../ic_menu_add_person_light.png | Bin 0 -> 990 bytes .../drawable-xxxhdpi/ic_menu_admin_dark.png | Bin 0 -> 1172 bytes .../drawable-xxxhdpi/ic_menu_admin_light.png | Bin 0 -> 1277 bytes .../ic_menu_bookmark_dark.png | Bin 0 -> 490 bytes .../ic_menu_bookmark_light.png | Bin 0 -> 518 bytes .../ic_menu_bookmark_selected.png | Bin 0 -> 475 bytes .../drawable-xxxhdpi/ic_menu_chat_dark.png | Bin 0 -> 464 bytes .../drawable-xxxhdpi/ic_menu_chat_light.png | Bin 0 -> 487 bytes .../ic_menu_chat_send_dark.png | Bin 0 -> 815 bytes .../ic_menu_chat_send_light.png | Bin 0 -> 862 bytes .../ic_menu_download_dark.png | Bin 0 -> 390 bytes .../ic_menu_download_light.png | Bin 0 -> 397 bytes .../drawable-xxxhdpi/ic_menu_library_dark.png | Bin 0 -> 444 bytes .../ic_menu_library_light.png | Bin 0 -> 477 bytes .../ic_menu_password_dark.png | Bin 0 -> 861 bytes .../ic_menu_password_light.png | Bin 0 -> 926 bytes .../ic_menu_playlist_dark.png | Bin 0 -> 723 bytes .../ic_menu_playlist_light.png | Bin 0 -> 786 bytes .../drawable-xxxhdpi/ic_menu_podcast_dark.png | Bin 0 -> 1042 bytes .../ic_menu_podcast_light.png | Bin 0 -> 1122 bytes .../drawable-xxxhdpi/ic_menu_radio_dark.png | Bin 0 -> 1001 bytes .../drawable-xxxhdpi/ic_menu_radio_light.png | Bin 0 -> 1101 bytes .../drawable-xxxhdpi/ic_menu_refresh_dark.png | Bin 0 -> 1219 bytes .../ic_menu_refresh_light.png | Bin 0 -> 1309 bytes .../drawable-xxxhdpi/ic_menu_remove_dark.png | Bin 0 -> 473 bytes .../drawable-xxxhdpi/ic_menu_remove_light.png | Bin 0 -> 503 bytes .../drawable-xxxhdpi/ic_menu_save_dark.png | Bin 0 -> 706 bytes .../drawable-xxxhdpi/ic_menu_save_light.png | Bin 0 -> 763 bytes .../drawable-xxxhdpi/ic_menu_search_dark.png | Bin 0 -> 1251 bytes .../drawable-xxxhdpi/ic_menu_search_light.png | Bin 0 -> 1315 bytes .../ic_menu_settings_dark.png | Bin 0 -> 1436 bytes .../ic_menu_settings_light.png | Bin 0 -> 1533 bytes .../drawable-xxxhdpi/ic_menu_share_dark.png | Bin 0 -> 1243 bytes .../drawable-xxxhdpi/ic_menu_share_light.png | Bin 0 -> 1332 bytes .../drawable-xxxhdpi/ic_menu_shuffle_dark.png | Bin 0 -> 813 bytes .../ic_menu_shuffle_light.png | Bin 0 -> 838 bytes .../res/drawable-xxxhdpi/ic_social_person.png | Bin 0 -> 2343 bytes .../res/drawable-xxxhdpi/ic_toggle_played.png | Bin 0 -> 1594 bytes .../res/drawable-xxxhdpi/ic_toggle_star.png | Bin 0 -> 1366 bytes .../ic_toggle_star_outline.png | Bin 0 -> 2071 bytes .../ic_toggle_star_outline_dark.png | Bin 0 -> 1514 bytes .../ic_toggle_star_outline_light.png | Bin 0 -> 1647 bytes .../drawable-xxxhdpi/main_offline_dark.png | Bin 0 -> 958 bytes .../drawable-xxxhdpi/main_offline_light.png | Bin 0 -> 1041 bytes .../main_select_server_dark.png | Bin 0 -> 488 bytes .../main_select_server_light.png | Bin 0 -> 502 bytes .../main_select_tabs_dark.png | Bin 0 -> 435 bytes .../main_select_tabs_light.png | Bin 0 -> 456 bytes .../drawable-xxxhdpi/media_backward_dark.png | Bin 0 -> 1449 bytes .../drawable-xxxhdpi/media_backward_light.png | Bin 0 -> 1392 bytes .../media_fastforward_dark.png | Bin 0 -> 1561 bytes .../media_fastforward_light.png | Bin 0 -> 1511 bytes .../drawable-xxxhdpi/media_forward_dark.png | Bin 0 -> 1453 bytes .../drawable-xxxhdpi/media_forward_light.png | Bin 0 -> 1407 bytes .../res/drawable-xxxhdpi/media_pause_dark.png | Bin 0 -> 281 bytes .../drawable-xxxhdpi/media_pause_light.png | Bin 0 -> 281 bytes .../media_repeat_all_dark.png | Bin 0 -> 2624 bytes .../media_repeat_all_light.png | Bin 0 -> 2475 bytes .../media_repeat_off_dark.png | Bin 0 -> 902 bytes .../media_repeat_off_light.png | Bin 0 -> 904 bytes .../media_repeat_single_dark.png | Bin 0 -> 1069 bytes .../media_repeat_single_light.png | Bin 0 -> 1069 bytes .../drawable-xxxhdpi/media_rewind_dark.png | Bin 0 -> 1587 bytes .../drawable-xxxhdpi/media_rewind_light.png | Bin 0 -> 1519 bytes .../res/drawable-xxxhdpi/media_start_dark.png | Bin 0 -> 1255 bytes .../drawable-xxxhdpi/media_start_light.png | Bin 0 -> 1331 bytes .../res/drawable-xxxhdpi/media_stop_dark.png | Bin 0 -> 137 bytes .../res/drawable-xxxhdpi/media_stop_light.png | Bin 0 -> 137 bytes .../notification_close_dark.png | Bin 0 -> 641 bytes .../notification_close_light.png | Bin 0 -> 648 bytes .../res/drawable-xxxhdpi/playing_dark.png | Bin 0 -> 808 bytes .../res/drawable-xxxhdpi/playing_light.png | Bin 0 -> 876 bytes .../res/drawable/appwidget4x1_preview.png | Bin 0 -> 3328 bytes .../res/drawable/appwidget4x2_preview.png | Bin 0 -> 5772 bytes .../res/drawable/appwidget4x3_preview.png | Bin 0 -> 5986 bytes .../res/drawable/appwidget4x4_preview.png | Bin 0 -> 8988 bytes app/src/main/res/drawable/audinaut.png | Bin 0 -> 48298 bytes .../drawable/card_rounded_corners_black.xml | 6 + .../drawable/card_rounded_corners_dark.xml | 6 + .../drawable/card_rounded_corners_light.xml | 6 + .../res/drawable/fast_scroller_bubble.xml | 16 + .../res/drawable/fast_scroller_handle.xml | 26 + .../res/drawable/notification_backward.xml | 4 + .../main/res/drawable/notification_close.xml | 4 + .../res/drawable/notification_divider.xml | 5 + .../res/drawable/notification_fastforward.xml | 4 + .../res/drawable/notification_forward.xml | 4 + .../main/res/drawable/notification_pause.xml | 4 + .../main/res/drawable/notification_rewind.xml | 4 + .../main/res/drawable/notification_start.xml | 4 + app/src/main/res/layout-land/download.xml | 97 + .../abstract_fragment_container.xml | 21 + .../main/res/layout-large-land/download.xml | 96 + app/src/main/res/layout-port/download.xml | 89 + app/src/main/res/layout/abstract_activity.xml | 22 + .../res/layout/abstract_fragment_activity.xml | 147 ++ .../layout/abstract_fragment_container.xml | 6 + .../res/layout/abstract_recycler_fragment.xml | 36 + app/src/main/res/layout/actionbar_spinner.xml | 8 + app/src/main/res/layout/album_cell_item.xml | 77 + app/src/main/res/layout/album_list_header.xml | 29 + app/src/main/res/layout/album_list_item.xml | 56 + app/src/main/res/layout/appwidget4x1.xml | 110 + app/src/main/res/layout/appwidget4x2.xml | 136 + app/src/main/res/layout/appwidget4x3.xml | 119 + app/src/main/res/layout/appwidget4x4.xml | 121 + app/src/main/res/layout/basic_art_item.xml | 34 + app/src/main/res/layout/basic_cell_item.xml | 45 + app/src/main/res/layout/basic_choice_item.xml | 27 + app/src/main/res/layout/basic_count_item.xml | 37 + app/src/main/res/layout/basic_header.xml | 13 + app/src/main/res/layout/basic_list_item.xml | 28 + .../res/layout/cache_location_buttons.xml | 19 + app/src/main/res/layout/complex_list_item.xml | 41 + app/src/main/res/layout/create_bookmark.xml | 27 + app/src/main/res/layout/details_item.xml | 28 + .../res/layout/download_media_buttons.xml | 78 + app/src/main/res/layout/download_playlist.xml | 42 + app/src/main/res/layout/download_slider.xml | 43 + app/src/main/res/layout/drawer_header.xml | 51 + app/src/main/res/layout/edit_play_action.xml | 124 + app/src/main/res/layout/equalizer.xml | 51 + app/src/main/res/layout/equalizer_bar.xml | 33 + app/src/main/res/layout/expandable_header.xml | 32 + app/src/main/res/layout/fast_scroller.xml | 25 + app/src/main/res/layout/genre_list_item.xml | 45 + app/src/main/res/layout/notification.xml | 66 + .../main/res/layout/notification_expanded.xml | 98 + app/src/main/res/layout/preferences.xml | 10 + app/src/main/res/layout/save_playlist.xml | 27 + .../main/res/layout/seekbar_preference.xml | 19 + .../main/res/layout/select_album_header.xml | 125 + .../main/res/layout/select_artist_header.xml | 52 + app/src/main/res/layout/settings_activity.xml | 21 + app/src/main/res/layout/shuffle_dialog.xml | 83 + app/src/main/res/layout/song_list_item.xml | 112 + .../res/layout/static_drawer_activity.xml | 23 + app/src/main/res/layout/tab_progress.xml | 32 + app/src/main/res/layout/update_playlist.xml | 73 + app/src/main/res/layout/user_list_item.xml | 28 + app/src/main/res/menu/abstract_top_menu.xml | 18 + app/src/main/res/menu/downloading.xml | 9 + app/src/main/res/menu/drawer_navigation.xml | 30 + app/src/main/res/menu/empty.xml | 12 + app/src/main/res/menu/main.xml | 16 + app/src/main/res/menu/multiselect_media.xml | 47 + .../res/menu/multiselect_media_offline.xml | 31 + .../main/res/menu/multiselect_nowplaying.xml | 23 + .../menu/multiselect_nowplaying_offline.xml | 9 + app/src/main/res/menu/nowplaying.xml | 38 + app/src/main/res/menu/nowplaying_context.xml | 28 + .../res/menu/nowplaying_context_offline.xml | 22 + app/src/main/res/menu/nowplaying_offline.xml | 31 + app/src/main/res/menu/search.xml | 10 + app/src/main/res/menu/select_album.xml | 35 + .../main/res/menu/select_album_context.xml | 54 + .../res/menu/select_album_context_offline.xml | 43 + app/src/main/res/menu/select_album_list.xml | 23 + app/src/main/res/menu/select_artist.xml | 29 + .../main/res/menu/select_artist_context.xml | 44 + .../menu/select_artist_context_offline.xml | 32 + .../main/res/menu/select_playlist_context.xml | 35 + .../menu/select_playlist_context_offline.xml | 18 + app/src/main/res/menu/select_song.xml | 43 + app/src/main/res/menu/select_song_context.xml | 63 + .../res/menu/select_song_context_offline.xml | 41 + app/src/main/res/menu/select_song_offline.xml | 27 + .../main/res/menu/tasker_configuration.xml | 16 + app/src/main/res/values-land/integers.xml | 4 + .../main/res/values-large-land/integers.xml | 4 + app/src/main/res/values-large/dimens.xml | 11 + app/src/main/res/values-large/integers.xml | 6 + app/src/main/res/values-v21/styles.xml | 20 + app/src/main/res/values-v21/themes.xml | 26 + .../main/res/values-xlarge-land/integers.xml | 4 + app/src/main/res/values/arrays.xml | 156 ++ app/src/main/res/values/attrs.xml | 60 + app/src/main/res/values/colors.xml | 175 ++ app/src/main/res/values/dimens.xml | 15 + app/src/main/res/values/ids.xml | 4 + app/src/main/res/values/integers.xml | 7 + app/src/main/res/values/strings.xml | 397 +++ app/src/main/res/values/styles.xml | 95 + app/src/main/res/values/themes.xml | 201 ++ app/src/main/res/xml/appwidget4x1.xml | 8 + app/src/main/res/xml/appwidget4x2.xml | 8 + app/src/main/res/xml/appwidget4x3.xml | 8 + app/src/main/res/xml/appwidget4x4.xml | 10 + app/src/main/res/xml/authenticator.xml | 7 + app/src/main/res/xml/auto_app_description.xml | 4 + .../main/res/xml/playlists_syncadapter.xml | 8 + app/src/main/res/xml/searchable.xml | 10 + app/src/main/res/xml/settings.xml | 24 + app/src/main/res/xml/settings_appearance.xml | 56 + app/src/main/res/xml/settings_cache.xml | 85 + app/src/main/res/xml/settings_playback.xml | 122 + app/src/main/res/xml/settings_servers.xml | 14 + audinaut.svg | 301 +++ build.gradle | 22 + debug.keystore | Bin 0 -> 1268 bytes gradle.properties | 19 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 49896 bytes gradle/wrapper/gradle-wrapper.properties | 6 + settings.gradle | 2 + 832 files changed, 39387 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 Audinaut.iml create mode 160000 ServerProxy create mode 100644 app/.gitignore create mode 100644 app/build.gradle create mode 100644 app/libs/kryo-2.21-all.jar create mode 100644 app/proguard.cfg create mode 100644 app/src/androidTest/java/github/nvllsvm/audinaut/ApplicationTest.java create mode 100644 app/src/androidTest/java/github/nvllsvm/audinaut/activity/SubsonicFragmentActivityTest.java create mode 100644 app/src/androidTest/java/github/nvllsvm/audinaut/domain/GenreComparatorTest.java create mode 100644 app/src/androidTest/java/github/nvllsvm/audinaut/service/DownloadServiceTest.java create mode 100644 app/src/audinaut-stacktrace.txt create mode 100644 app/src/main/.gradle/3.1/taskArtifacts/cache.properties create mode 100644 app/src/main/.gradle/3.1/taskArtifacts/cache.properties.lock create mode 100644 app/src/main/.gradle/3.1/taskArtifacts/fileSnapshots.bin create mode 100644 app/src/main/.gradle/3.1/taskArtifacts/taskArtifacts.bin create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/github/nvllsvm/audinaut/activity/EditPlayActionActivity.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/activity/QueryReceiverActivity.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/activity/SettingsActivity.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/activity/SubsonicActivity.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/activity/SubsonicFragmentActivity.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/activity/VoiceQueryReceiverActivity.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/adapter/AlphabeticalAlbumAdapter.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/adapter/ArtistAdapter.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/adapter/BasicListAdapter.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/adapter/DetailsAdapter.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/adapter/DownloadFileAdapter.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/adapter/EntryGridAdapter.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/adapter/EntryInfiniteGridAdapter.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/adapter/ExpandableSectionAdapter.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/adapter/GenreAdapter.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/adapter/MainAdapter.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/adapter/PlaylistAdapter.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/adapter/SearchAdapter.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/adapter/SectionAdapter.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/adapter/SettingsAdapter.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/audiofx/AudioEffectsController.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/audiofx/EqualizerController.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/audiofx/LoudnessEnhancerController.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/domain/Artist.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/domain/Genre.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/domain/Indexes.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/domain/MusicDirectory.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/domain/MusicFolder.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/domain/PlayerQueue.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/domain/PlayerState.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/domain/Playlist.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/domain/RemoteStatus.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/domain/RepeatMode.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/domain/SearchCritera.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/domain/SearchResult.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/domain/User.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/domain/Version.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/fragments/DownloadFragment.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/fragments/EqualizerFragment.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/fragments/MainFragment.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/fragments/NowPlayingFragment.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/fragments/PreferenceCompatFragment.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/fragments/SearchFragment.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/fragments/SelectArtistFragment.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/fragments/SelectDirectoryFragment.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/fragments/SelectGenreFragment.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/fragments/SelectPlaylistFragment.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/fragments/SelectRecyclerFragment.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/fragments/SelectYearFragment.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/fragments/SettingsFragment.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/fragments/SubsonicFragment.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/provider/AudinautSearchProvider.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/provider/AudinautWidget4x1.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/provider/AudinautWidget4x2.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/provider/AudinautWidget4x3.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/provider/AudinautWidget4x4.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/provider/AudinautWidgetProvider.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/provider/MostRecentStubProvider.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/provider/PlaylistStubProvider.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/receiver/A2dpIntentReceiver.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/receiver/AudioNoisyReceiver.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/receiver/BootReceiver.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/receiver/HeadphonePlugReceiver.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/receiver/MediaButtonIntentReceiver.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/receiver/PlayActionReceiver.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/service/CachedMusicService.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/service/DownloadFile.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/service/DownloadService.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/service/DownloadServiceLifecycleSupport.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/service/HeadphoneListenerService.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/service/MediaStoreService.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/service/MusicService.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/service/MusicServiceFactory.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/service/OfflineException.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/service/OfflineMusicService.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/service/RESTMusicService.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/service/parser/AbstractParser.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/service/parser/EntryListParser.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/service/parser/ErrorParser.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/service/parser/GenreParser.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/service/parser/IndexesParser.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/service/parser/MusicDirectoryEntryParser.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/service/parser/MusicDirectoryParser.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/service/parser/MusicFoldersParser.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/service/parser/PlayQueueParser.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/service/parser/PlaylistParser.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/service/parser/PlaylistsParser.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/service/parser/RandomSongsParser.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/service/parser/ScanStatusParser.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/service/parser/SearchResult2Parser.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/service/parser/SearchResultParser.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/service/parser/SubsonicRESTException.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/service/parser/TopSongsParser.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/service/parser/UserParser.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/service/ssl/SSLSocketFactory.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/service/ssl/TrustManagerDecorator.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/service/ssl/TrustSelfSignedStrategy.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/service/ssl/TrustStrategy.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/service/sync/AuthenticatorService.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/service/sync/SubsonicSyncAdapter.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/updates/Updater.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/updates/UpdaterSongPress.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/util/BackgroundTask.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/util/CacheCleaner.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/util/Constants.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/util/DownloadFileItemHelperCallback.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/util/DrawableTint.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/util/EnvironmentVariables.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/util/FileUtil.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/util/ImageLoader.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/util/LoadingTask.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/util/MenuUtil.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/util/Notifications.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/util/Pair.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/util/ProgressListener.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/util/SettingsBackupAgent.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/util/ShufflePlayBuffer.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/util/SilentBackgroundTask.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/util/SilentServiceTask.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/util/SimpleServiceBinder.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/util/SongDBHandler.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/util/SyncUtil.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/util/TabBackgroundTask.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/util/ThemeUtil.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/util/TimeLimitedCache.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/util/UpdateHelper.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/util/UserUtil.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/util/Util.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/util/tags/Bastp.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/util/tags/BastpUtil.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/util/tags/Common.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/util/tags/FlacFile.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/util/tags/ID3v2File.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/util/tags/LameHeader.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/util/tags/OggFile.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/view/AlbumListCountView.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/view/AlbumView.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/view/ArtistEntryView.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/view/ArtistView.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/view/AutoRepeatButton.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/view/BasicHeaderView.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/view/BasicListView.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/view/CacheLocationPreference.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/view/CardView.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/view/ErrorDialog.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/view/FadeOutAnimation.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/view/FastScroller.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/view/GenreView.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/view/GridSpacingDecoration.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/view/MyLeadingMarginSpan2.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/view/PlaylistSongView.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/view/PlaylistView.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/view/RecyclingImageView.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/view/SeekBarPreference.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/view/SettingView.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/view/SongView.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/view/SquareImageView.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/view/UpdateView.java create mode 100644 app/src/main/java/github/nvllsvm/audinaut/view/UpdateView2.java create mode 100644 app/src/main/res/anim/enter_from_left.xml create mode 100644 app/src/main/res/anim/enter_from_right.xml create mode 100644 app/src/main/res/anim/exit_to_left.xml create mode 100644 app/src/main/res/anim/exit_to_right.xml create mode 100644 app/src/main/res/anim/fade_in.xml create mode 100644 app/src/main/res/anim/fade_out.xml create mode 100644 app/src/main/res/anim/push_down_in.xml create mode 100644 app/src/main/res/anim/push_down_out.xml create mode 100644 app/src/main/res/anim/push_up_in.xml create mode 100644 app/src/main/res/anim/push_up_out.xml create mode 100644 app/src/main/res/drawable-hdpi/action_toggle_list_dark.png create mode 100644 app/src/main/res/drawable-hdpi/action_toggle_list_light.png create mode 100644 app/src/main/res/drawable-hdpi/appwidget_art_default.png create mode 100644 app/src/main/res/drawable-hdpi/appwidget_art_unknown.png create mode 100644 app/src/main/res/drawable-hdpi/appwidget_bg.9.png create mode 100644 app/src/main/res/drawable-hdpi/background.png create mode 100644 app/src/main/res/drawable-hdpi/download_cached.png create mode 100644 app/src/main/res/drawable-hdpi/download_none_dark.png create mode 100644 app/src/main/res/drawable-hdpi/download_none_light.png create mode 100644 app/src/main/res/drawable-hdpi/download_pinned.png create mode 100644 app/src/main/res/drawable-hdpi/downloading_dark.png create mode 100644 app/src/main/res/drawable-hdpi/downloading_light.png create mode 100644 app/src/main/res/drawable-hdpi/ic_action_add_dark.png create mode 100644 app/src/main/res/drawable-hdpi/ic_action_add_light.png create mode 100644 app/src/main/res/drawable-hdpi/ic_action_album.png create mode 100644 app/src/main/res/drawable-hdpi/ic_action_artist.png create mode 100644 app/src/main/res/drawable-hdpi/ic_action_playback_speed_dark.png create mode 100644 app/src/main/res/drawable-hdpi/ic_action_playback_speed_light.png create mode 100644 app/src/main/res/drawable-hdpi/ic_action_rating_bad.png create mode 100644 app/src/main/res/drawable-hdpi/ic_action_rating_bad_dark.png create mode 100644 app/src/main/res/drawable-hdpi/ic_action_rating_bad_light.png create mode 100644 app/src/main/res/drawable-hdpi/ic_action_rating_bad_selected.png create mode 100644 app/src/main/res/drawable-hdpi/ic_action_rating_good.png create mode 100644 app/src/main/res/drawable-hdpi/ic_action_rating_good_dark.png create mode 100644 app/src/main/res/drawable-hdpi/ic_action_rating_good_light.png create mode 100644 app/src/main/res/drawable-hdpi/ic_action_rating_good_selected.png create mode 100644 app/src/main/res/drawable-hdpi/ic_action_song.png create mode 100644 app/src/main/res/drawable-hdpi/ic_menu_add_person_dark.png create mode 100644 app/src/main/res/drawable-hdpi/ic_menu_add_person_light.png create mode 100644 app/src/main/res/drawable-hdpi/ic_menu_admin_dark.png create mode 100644 app/src/main/res/drawable-hdpi/ic_menu_admin_light.png create mode 100644 app/src/main/res/drawable-hdpi/ic_menu_bookmark_dark.png create mode 100644 app/src/main/res/drawable-hdpi/ic_menu_bookmark_light.png create mode 100644 app/src/main/res/drawable-hdpi/ic_menu_bookmark_selected.png create mode 100644 app/src/main/res/drawable-hdpi/ic_menu_chat_dark.png create mode 100644 app/src/main/res/drawable-hdpi/ic_menu_chat_light.png create mode 100644 app/src/main/res/drawable-hdpi/ic_menu_chat_send_dark.png create mode 100644 app/src/main/res/drawable-hdpi/ic_menu_chat_send_light.png create mode 100644 app/src/main/res/drawable-hdpi/ic_menu_download_dark.png create mode 100644 app/src/main/res/drawable-hdpi/ic_menu_download_light.png create mode 100644 app/src/main/res/drawable-hdpi/ic_menu_library_dark.png create mode 100644 app/src/main/res/drawable-hdpi/ic_menu_library_light.png create mode 100644 app/src/main/res/drawable-hdpi/ic_menu_password_dark.png create mode 100644 app/src/main/res/drawable-hdpi/ic_menu_password_light.png create mode 100644 app/src/main/res/drawable-hdpi/ic_menu_playlist_dark.png create mode 100644 app/src/main/res/drawable-hdpi/ic_menu_playlist_light.png create mode 100644 app/src/main/res/drawable-hdpi/ic_menu_podcast_dark.png create mode 100644 app/src/main/res/drawable-hdpi/ic_menu_podcast_light.png create mode 100644 app/src/main/res/drawable-hdpi/ic_menu_radio_dark.png create mode 100644 app/src/main/res/drawable-hdpi/ic_menu_radio_light.png create mode 100644 app/src/main/res/drawable-hdpi/ic_menu_refresh_dark.png create mode 100644 app/src/main/res/drawable-hdpi/ic_menu_refresh_light.png create mode 100644 app/src/main/res/drawable-hdpi/ic_menu_remove_dark.png create mode 100644 app/src/main/res/drawable-hdpi/ic_menu_remove_light.png create mode 100644 app/src/main/res/drawable-hdpi/ic_menu_save_dark.png create mode 100644 app/src/main/res/drawable-hdpi/ic_menu_save_light.png create mode 100644 app/src/main/res/drawable-hdpi/ic_menu_search_dark.png create mode 100644 app/src/main/res/drawable-hdpi/ic_menu_search_light.png create mode 100644 app/src/main/res/drawable-hdpi/ic_menu_settings_dark.png create mode 100644 app/src/main/res/drawable-hdpi/ic_menu_settings_light.png create mode 100644 app/src/main/res/drawable-hdpi/ic_menu_share_dark.png create mode 100644 app/src/main/res/drawable-hdpi/ic_menu_share_light.png create mode 100644 app/src/main/res/drawable-hdpi/ic_menu_shuffle_dark.png create mode 100644 app/src/main/res/drawable-hdpi/ic_menu_shuffle_light.png create mode 100644 app/src/main/res/drawable-hdpi/ic_number_border.png create mode 100644 app/src/main/res/drawable-hdpi/ic_social_person.png create mode 100644 app/src/main/res/drawable-hdpi/ic_toggle_played.png create mode 100644 app/src/main/res/drawable-hdpi/ic_toggle_star.png create mode 100644 app/src/main/res/drawable-hdpi/ic_toggle_star_outline.png create mode 100644 app/src/main/res/drawable-hdpi/ic_toggle_star_outline_dark.png create mode 100644 app/src/main/res/drawable-hdpi/ic_toggle_star_outline_light.png create mode 100644 app/src/main/res/drawable-hdpi/launch.png create mode 100644 app/src/main/res/drawable-hdpi/main_offline_dark.png create mode 100644 app/src/main/res/drawable-hdpi/main_offline_light.png create mode 100644 app/src/main/res/drawable-hdpi/main_select_server_dark.png create mode 100644 app/src/main/res/drawable-hdpi/main_select_server_light.png create mode 100644 app/src/main/res/drawable-hdpi/main_select_tabs_dark.png create mode 100644 app/src/main/res/drawable-hdpi/main_select_tabs_light.png create mode 100644 app/src/main/res/drawable-hdpi/media_backward_dark.png create mode 100644 app/src/main/res/drawable-hdpi/media_backward_light.png create mode 100644 app/src/main/res/drawable-hdpi/media_fastforward_dark.png create mode 100644 app/src/main/res/drawable-hdpi/media_fastforward_light.png create mode 100644 app/src/main/res/drawable-hdpi/media_forward_dark.png create mode 100644 app/src/main/res/drawable-hdpi/media_forward_light.png create mode 100644 app/src/main/res/drawable-hdpi/media_pause_dark.png create mode 100644 app/src/main/res/drawable-hdpi/media_pause_light.png create mode 100644 app/src/main/res/drawable-hdpi/media_repeat_all_dark.png create mode 100644 app/src/main/res/drawable-hdpi/media_repeat_all_light.png create mode 100644 app/src/main/res/drawable-hdpi/media_repeat_off_dark.png create mode 100644 app/src/main/res/drawable-hdpi/media_repeat_off_light.png create mode 100644 app/src/main/res/drawable-hdpi/media_repeat_single_dark.png create mode 100644 app/src/main/res/drawable-hdpi/media_repeat_single_light.png create mode 100644 app/src/main/res/drawable-hdpi/media_rewind_dark.png create mode 100644 app/src/main/res/drawable-hdpi/media_rewind_light.png create mode 100644 app/src/main/res/drawable-hdpi/media_start_dark.png create mode 100644 app/src/main/res/drawable-hdpi/media_start_light.png create mode 100644 app/src/main/res/drawable-hdpi/media_stop_dark.png create mode 100644 app/src/main/res/drawable-hdpi/media_stop_light.png create mode 100644 app/src/main/res/drawable-hdpi/notification_close_dark.png create mode 100644 app/src/main/res/drawable-hdpi/notification_close_light.png create mode 100644 app/src/main/res/drawable-hdpi/playing_dark.png create mode 100644 app/src/main/res/drawable-hdpi/playing_light.png create mode 100644 app/src/main/res/drawable-hdpi/stat_notify_playing.png create mode 100644 app/src/main/res/drawable-hdpi/stat_notify_sync.png create mode 100644 app/src/main/res/drawable-hdpi/unknown_album.png create mode 100644 app/src/main/res/drawable-hdpi/unknown_album_large.png create mode 100644 app/src/main/res/drawable-large/unknown_album.png create mode 100644 app/src/main/res/drawable-mdpi/action_toggle_list_dark.png create mode 100644 app/src/main/res/drawable-mdpi/action_toggle_list_light.png create mode 100644 app/src/main/res/drawable-mdpi/download_cached.png create mode 100644 app/src/main/res/drawable-mdpi/download_none_dark.png create mode 100644 app/src/main/res/drawable-mdpi/download_none_light.png create mode 100644 app/src/main/res/drawable-mdpi/download_pinned.png create mode 100644 app/src/main/res/drawable-mdpi/downloading_dark.png create mode 100644 app/src/main/res/drawable-mdpi/downloading_light.png create mode 100644 app/src/main/res/drawable-mdpi/ic_action_add_dark.png create mode 100644 app/src/main/res/drawable-mdpi/ic_action_add_light.png create mode 100644 app/src/main/res/drawable-mdpi/ic_action_album.png create mode 100644 app/src/main/res/drawable-mdpi/ic_action_artist.png create mode 100644 app/src/main/res/drawable-mdpi/ic_action_playback_speed_dark.png create mode 100644 app/src/main/res/drawable-mdpi/ic_action_playback_speed_light.png create mode 100644 app/src/main/res/drawable-mdpi/ic_action_rating_bad.png create mode 100644 app/src/main/res/drawable-mdpi/ic_action_rating_bad_dark.png create mode 100644 app/src/main/res/drawable-mdpi/ic_action_rating_bad_light.png create mode 100644 app/src/main/res/drawable-mdpi/ic_action_rating_bad_selected.png create mode 100644 app/src/main/res/drawable-mdpi/ic_action_rating_good.png create mode 100644 app/src/main/res/drawable-mdpi/ic_action_rating_good_dark.png create mode 100644 app/src/main/res/drawable-mdpi/ic_action_rating_good_light.png create mode 100644 app/src/main/res/drawable-mdpi/ic_action_rating_good_selected.png create mode 100644 app/src/main/res/drawable-mdpi/ic_action_song.png create mode 100644 app/src/main/res/drawable-mdpi/ic_action_volume_dark.png create mode 100644 app/src/main/res/drawable-mdpi/ic_action_volume_light.png create mode 100644 app/src/main/res/drawable-mdpi/ic_menu_add_person_dark.png create mode 100644 app/src/main/res/drawable-mdpi/ic_menu_add_person_light.png create mode 100644 app/src/main/res/drawable-mdpi/ic_menu_admin_dark.png create mode 100644 app/src/main/res/drawable-mdpi/ic_menu_admin_light.png create mode 100644 app/src/main/res/drawable-mdpi/ic_menu_bookmark_dark.png create mode 100644 app/src/main/res/drawable-mdpi/ic_menu_bookmark_light.png create mode 100644 app/src/main/res/drawable-mdpi/ic_menu_bookmark_selected.png create mode 100644 app/src/main/res/drawable-mdpi/ic_menu_chat_dark.png create mode 100644 app/src/main/res/drawable-mdpi/ic_menu_chat_light.png create mode 100644 app/src/main/res/drawable-mdpi/ic_menu_chat_send_dark.png create mode 100644 app/src/main/res/drawable-mdpi/ic_menu_chat_send_light.png create mode 100644 app/src/main/res/drawable-mdpi/ic_menu_download_dark.png create mode 100644 app/src/main/res/drawable-mdpi/ic_menu_download_light.png create mode 100644 app/src/main/res/drawable-mdpi/ic_menu_library_dark.png create mode 100644 app/src/main/res/drawable-mdpi/ic_menu_library_light.png create mode 100644 app/src/main/res/drawable-mdpi/ic_menu_password_dark.png create mode 100644 app/src/main/res/drawable-mdpi/ic_menu_password_light.png create mode 100644 app/src/main/res/drawable-mdpi/ic_menu_playlist_dark.png create mode 100644 app/src/main/res/drawable-mdpi/ic_menu_playlist_light.png create mode 100644 app/src/main/res/drawable-mdpi/ic_menu_podcast_dark.png create mode 100644 app/src/main/res/drawable-mdpi/ic_menu_podcast_light.png create mode 100644 app/src/main/res/drawable-mdpi/ic_menu_radio_dark.png create mode 100644 app/src/main/res/drawable-mdpi/ic_menu_radio_light.png create mode 100644 app/src/main/res/drawable-mdpi/ic_menu_refresh_dark.png create mode 100644 app/src/main/res/drawable-mdpi/ic_menu_refresh_light.png create mode 100644 app/src/main/res/drawable-mdpi/ic_menu_remove_dark.png create mode 100644 app/src/main/res/drawable-mdpi/ic_menu_remove_light.png create mode 100644 app/src/main/res/drawable-mdpi/ic_menu_save_dark.png create mode 100644 app/src/main/res/drawable-mdpi/ic_menu_save_light.png create mode 100644 app/src/main/res/drawable-mdpi/ic_menu_search_dark.png create mode 100644 app/src/main/res/drawable-mdpi/ic_menu_search_light.png create mode 100644 app/src/main/res/drawable-mdpi/ic_menu_settings_dark.png create mode 100644 app/src/main/res/drawable-mdpi/ic_menu_settings_light.png create mode 100644 app/src/main/res/drawable-mdpi/ic_menu_share_dark.png create mode 100644 app/src/main/res/drawable-mdpi/ic_menu_share_light.png create mode 100644 app/src/main/res/drawable-mdpi/ic_menu_shuffle_dark.png create mode 100644 app/src/main/res/drawable-mdpi/ic_menu_shuffle_light.png create mode 100644 app/src/main/res/drawable-mdpi/ic_number_border.png create mode 100644 app/src/main/res/drawable-mdpi/ic_social_person.png create mode 100644 app/src/main/res/drawable-mdpi/ic_toggle_played.png create mode 100644 app/src/main/res/drawable-mdpi/ic_toggle_star.png create mode 100644 app/src/main/res/drawable-mdpi/ic_toggle_star_outline.png create mode 100644 app/src/main/res/drawable-mdpi/ic_toggle_star_outline_dark.png create mode 100644 app/src/main/res/drawable-mdpi/ic_toggle_star_outline_light.png create mode 100644 app/src/main/res/drawable-mdpi/launch.png create mode 100644 app/src/main/res/drawable-mdpi/main_offline_dark.png create mode 100644 app/src/main/res/drawable-mdpi/main_offline_light.png create mode 100644 app/src/main/res/drawable-mdpi/main_select_server_dark.png create mode 100644 app/src/main/res/drawable-mdpi/main_select_server_light.png create mode 100644 app/src/main/res/drawable-mdpi/main_select_tabs_dark.png create mode 100644 app/src/main/res/drawable-mdpi/main_select_tabs_light.png create mode 100644 app/src/main/res/drawable-mdpi/media_backward_dark.png create mode 100644 app/src/main/res/drawable-mdpi/media_backward_light.png create mode 100644 app/src/main/res/drawable-mdpi/media_fastforward_dark.png create mode 100644 app/src/main/res/drawable-mdpi/media_fastforward_light.png create mode 100644 app/src/main/res/drawable-mdpi/media_forward_dark.png create mode 100644 app/src/main/res/drawable-mdpi/media_forward_light.png create mode 100644 app/src/main/res/drawable-mdpi/media_pause_dark.png create mode 100644 app/src/main/res/drawable-mdpi/media_pause_light.png create mode 100644 app/src/main/res/drawable-mdpi/media_repeat_all_dark.png create mode 100644 app/src/main/res/drawable-mdpi/media_repeat_all_light.png create mode 100644 app/src/main/res/drawable-mdpi/media_repeat_off_dark.png create mode 100644 app/src/main/res/drawable-mdpi/media_repeat_off_light.png create mode 100644 app/src/main/res/drawable-mdpi/media_repeat_single_dark.png create mode 100644 app/src/main/res/drawable-mdpi/media_repeat_single_light.png create mode 100644 app/src/main/res/drawable-mdpi/media_rewind_dark.png create mode 100644 app/src/main/res/drawable-mdpi/media_rewind_light.png create mode 100644 app/src/main/res/drawable-mdpi/media_start_dark.png create mode 100644 app/src/main/res/drawable-mdpi/media_start_light.png create mode 100644 app/src/main/res/drawable-mdpi/media_stop_dark.png create mode 100644 app/src/main/res/drawable-mdpi/media_stop_light.png create mode 100644 app/src/main/res/drawable-mdpi/notification_close_dark.png create mode 100644 app/src/main/res/drawable-mdpi/notification_close_light.png create mode 100644 app/src/main/res/drawable-mdpi/playing_dark.png create mode 100644 app/src/main/res/drawable-mdpi/playing_light.png create mode 100644 app/src/main/res/drawable-mdpi/stat_notify_playing.png create mode 100644 app/src/main/res/drawable-mdpi/stat_notify_sync.png create mode 100644 app/src/main/res/drawable-v21/notification_backward.xml create mode 100644 app/src/main/res/drawable-v21/notification_close.xml create mode 100644 app/src/main/res/drawable-v21/notification_fastforward.xml create mode 100644 app/src/main/res/drawable-v21/notification_forward.xml create mode 100644 app/src/main/res/drawable-v21/notification_pause.xml create mode 100644 app/src/main/res/drawable-v21/notification_rewind.xml create mode 100644 app/src/main/res/drawable-v21/notification_start.xml create mode 100644 app/src/main/res/drawable-xhdpi/action_toggle_list_dark.png create mode 100644 app/src/main/res/drawable-xhdpi/action_toggle_list_light.png create mode 100644 app/src/main/res/drawable-xhdpi/download_cached.png create mode 100644 app/src/main/res/drawable-xhdpi/download_none_dark.png create mode 100644 app/src/main/res/drawable-xhdpi/download_none_light.png create mode 100644 app/src/main/res/drawable-xhdpi/download_pinned.png create mode 100644 app/src/main/res/drawable-xhdpi/downloading_dark.png create mode 100644 app/src/main/res/drawable-xhdpi/downloading_light.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_action_add_dark.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_action_add_light.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_action_album.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_action_artist.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_action_playback_speed_dark.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_action_playback_speed_light.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_action_rating_bad.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_action_rating_bad_dark.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_action_rating_bad_light.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_action_rating_bad_selected.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_action_rating_good.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_action_rating_good_dark.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_action_rating_good_light.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_action_rating_good_selected.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_action_song.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_action_volume_dark.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_action_volume_light.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_menu_add_person_dark.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_menu_add_person_light.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_menu_admin_dark.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_menu_admin_light.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_menu_bookmark_dark.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_menu_bookmark_light.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_menu_bookmark_selected.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_menu_chat_dark.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_menu_chat_light.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_menu_chat_send_dark.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_menu_chat_send_light.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_menu_download_dark.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_menu_download_light.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_menu_library_dark.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_menu_library_light.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_menu_password_dark.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_menu_password_light.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_menu_playlist_dark.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_menu_playlist_light.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_menu_podcast_dark.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_menu_podcast_light.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_menu_radio_dark.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_menu_radio_light.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_menu_refresh_dark.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_menu_refresh_light.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_menu_remove_dark.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_menu_remove_light.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_menu_save_dark.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_menu_save_light.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_menu_search_dark.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_menu_search_light.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_menu_settings_dark.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_menu_settings_light.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_menu_share_dark.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_menu_share_light.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_menu_shuffle_dark.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_menu_shuffle_light.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_number_border.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_social_person.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_toggle_played.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_toggle_star.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_toggle_star_outline.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_toggle_star_outline_dark.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_toggle_star_outline_light.png create mode 100644 app/src/main/res/drawable-xhdpi/launch.png create mode 100644 app/src/main/res/drawable-xhdpi/main_offline_dark.png create mode 100644 app/src/main/res/drawable-xhdpi/main_offline_light.png create mode 100644 app/src/main/res/drawable-xhdpi/main_select_server_dark.png create mode 100644 app/src/main/res/drawable-xhdpi/main_select_server_light.png create mode 100644 app/src/main/res/drawable-xhdpi/main_select_tabs_dark.png create mode 100644 app/src/main/res/drawable-xhdpi/main_select_tabs_light.png create mode 100644 app/src/main/res/drawable-xhdpi/media_backward_dark.png create mode 100644 app/src/main/res/drawable-xhdpi/media_backward_light.png create mode 100644 app/src/main/res/drawable-xhdpi/media_fastforward_dark.png create mode 100644 app/src/main/res/drawable-xhdpi/media_fastforward_light.png create mode 100644 app/src/main/res/drawable-xhdpi/media_forward_dark.png create mode 100644 app/src/main/res/drawable-xhdpi/media_forward_light.png create mode 100644 app/src/main/res/drawable-xhdpi/media_pause_dark.png create mode 100644 app/src/main/res/drawable-xhdpi/media_pause_light.png create mode 100644 app/src/main/res/drawable-xhdpi/media_repeat_all_dark.png create mode 100644 app/src/main/res/drawable-xhdpi/media_repeat_all_light.png create mode 100644 app/src/main/res/drawable-xhdpi/media_repeat_off_dark.png create mode 100644 app/src/main/res/drawable-xhdpi/media_repeat_off_light.png create mode 100644 app/src/main/res/drawable-xhdpi/media_repeat_single_dark.png create mode 100644 app/src/main/res/drawable-xhdpi/media_repeat_single_light.png create mode 100644 app/src/main/res/drawable-xhdpi/media_rewind_dark.png create mode 100644 app/src/main/res/drawable-xhdpi/media_rewind_light.png create mode 100644 app/src/main/res/drawable-xhdpi/media_start_dark.png create mode 100644 app/src/main/res/drawable-xhdpi/media_start_light.png create mode 100644 app/src/main/res/drawable-xhdpi/media_stop_dark.png create mode 100644 app/src/main/res/drawable-xhdpi/media_stop_light.png create mode 100644 app/src/main/res/drawable-xhdpi/notification_close_dark.png create mode 100644 app/src/main/res/drawable-xhdpi/notification_close_light.png create mode 100644 app/src/main/res/drawable-xhdpi/playing_dark.png create mode 100644 app/src/main/res/drawable-xhdpi/playing_light.png create mode 100644 app/src/main/res/drawable-xhdpi/stat_notify_playing.png create mode 100644 app/src/main/res/drawable-xhdpi/stat_notify_sync.png create mode 100644 app/src/main/res/drawable-xxhdpi/action_toggle_list_dark.png create mode 100644 app/src/main/res/drawable-xxhdpi/action_toggle_list_light.png create mode 100644 app/src/main/res/drawable-xxhdpi/download_cached.png create mode 100644 app/src/main/res/drawable-xxhdpi/download_none_dark.png create mode 100644 app/src/main/res/drawable-xxhdpi/download_none_light.png create mode 100644 app/src/main/res/drawable-xxhdpi/download_pinned.png create mode 100644 app/src/main/res/drawable-xxhdpi/downloading_dark.png create mode 100644 app/src/main/res/drawable-xxhdpi/downloading_light.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_action_add_dark.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_action_add_light.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_action_artist.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_action_playback_speed_dark.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_action_playback_speed_light.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_action_rating_bad.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_action_rating_bad_dark.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_action_rating_bad_light.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_action_rating_bad_selected.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_action_rating_good.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_action_rating_good_dark.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_action_rating_good_light.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_action_rating_good_selected.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_action_song.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_action_volume_dark.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_action_volume_light.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_menu_add_person_dark.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_menu_add_person_light.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_menu_admin_dark.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_menu_admin_light.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_menu_bookmark_dark.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_menu_bookmark_light.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_menu_bookmark_selected.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_menu_chat_dark.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_menu_chat_light.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_menu_chat_send_dark.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_menu_chat_send_light.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_menu_download_dark.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_menu_download_light.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_menu_library_dark.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_menu_library_light.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_menu_password_dark.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_menu_password_light.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_menu_playlist_dark.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_menu_playlist_light.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_menu_podcast_dark.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_menu_podcast_light.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_menu_radio_dark.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_menu_radio_light.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_menu_refresh_dark.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_menu_refresh_light.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_menu_remove_dark.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_menu_remove_light.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_menu_save_dark.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_menu_save_light.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_menu_search_dark.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_menu_search_light.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_menu_settings_dark.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_menu_settings_light.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_menu_share_dark.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_menu_share_light.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_menu_shuffle_dark.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_menu_shuffle_light.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_number_border.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_social_person.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_toggle_played.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_toggle_star.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_toggle_star_outline.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_toggle_star_outline_dark.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_toggle_star_outline_light.png create mode 100644 app/src/main/res/drawable-xxhdpi/launch.png create mode 100644 app/src/main/res/drawable-xxhdpi/main_offline_dark.png create mode 100644 app/src/main/res/drawable-xxhdpi/main_offline_light.png create mode 100644 app/src/main/res/drawable-xxhdpi/main_select_server_dark.png create mode 100644 app/src/main/res/drawable-xxhdpi/main_select_server_light.png create mode 100644 app/src/main/res/drawable-xxhdpi/main_select_tabs_dark.png create mode 100644 app/src/main/res/drawable-xxhdpi/main_select_tabs_light.png create mode 100644 app/src/main/res/drawable-xxhdpi/media_backward_dark.png create mode 100644 app/src/main/res/drawable-xxhdpi/media_backward_light.png create mode 100644 app/src/main/res/drawable-xxhdpi/media_fastforward_dark.png create mode 100644 app/src/main/res/drawable-xxhdpi/media_fastforward_light.png create mode 100644 app/src/main/res/drawable-xxhdpi/media_forward_dark.png create mode 100644 app/src/main/res/drawable-xxhdpi/media_forward_light.png create mode 100644 app/src/main/res/drawable-xxhdpi/media_pause_dark.png create mode 100644 app/src/main/res/drawable-xxhdpi/media_pause_light.png create mode 100644 app/src/main/res/drawable-xxhdpi/media_repeat_all_dark.png create mode 100644 app/src/main/res/drawable-xxhdpi/media_repeat_all_light.png create mode 100644 app/src/main/res/drawable-xxhdpi/media_repeat_off_dark.png create mode 100644 app/src/main/res/drawable-xxhdpi/media_repeat_off_light.png create mode 100644 app/src/main/res/drawable-xxhdpi/media_repeat_single_dark.png create mode 100644 app/src/main/res/drawable-xxhdpi/media_repeat_single_light.png create mode 100644 app/src/main/res/drawable-xxhdpi/media_rewind_dark.png create mode 100644 app/src/main/res/drawable-xxhdpi/media_rewind_light.png create mode 100644 app/src/main/res/drawable-xxhdpi/media_start_dark.png create mode 100644 app/src/main/res/drawable-xxhdpi/media_start_light.png create mode 100644 app/src/main/res/drawable-xxhdpi/media_stop_dark.png create mode 100644 app/src/main/res/drawable-xxhdpi/media_stop_light.png create mode 100644 app/src/main/res/drawable-xxhdpi/notification_close_dark.png create mode 100644 app/src/main/res/drawable-xxhdpi/notification_close_light.png create mode 100644 app/src/main/res/drawable-xxhdpi/playing_dark.png create mode 100644 app/src/main/res/drawable-xxhdpi/playing_light.png create mode 100644 app/src/main/res/drawable-xxhdpi/stat_notify_playing.png create mode 100644 app/src/main/res/drawable-xxhdpi/stat_notify_sync.png create mode 100644 app/src/main/res/drawable-xxxhdpi/action_toggle_list_dark.png create mode 100644 app/src/main/res/drawable-xxxhdpi/action_toggle_list_light.png create mode 100644 app/src/main/res/drawable-xxxhdpi/download_none_dark.png create mode 100644 app/src/main/res/drawable-xxxhdpi/download_none_light.png create mode 100644 app/src/main/res/drawable-xxxhdpi/downloading_dark.png create mode 100644 app/src/main/res/drawable-xxxhdpi/downloading_light.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_action_add_dark.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_action_add_light.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_action_artist.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_action_playback_speed_dark.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_action_playback_speed_light.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_action_rating_bad.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_action_rating_bad_dark.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_action_rating_bad_light.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_action_rating_bad_selected.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_action_rating_good.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_action_rating_good_dark.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_action_rating_good_light.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_action_rating_good_selected.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_action_song.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_menu_add_person_dark.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_menu_add_person_light.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_menu_admin_dark.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_menu_admin_light.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_menu_bookmark_dark.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_menu_bookmark_light.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_menu_bookmark_selected.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_menu_chat_dark.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_menu_chat_light.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_menu_chat_send_dark.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_menu_chat_send_light.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_menu_download_dark.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_menu_download_light.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_menu_library_dark.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_menu_library_light.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_menu_password_dark.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_menu_password_light.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_menu_playlist_dark.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_menu_playlist_light.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_menu_podcast_dark.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_menu_podcast_light.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_menu_radio_dark.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_menu_radio_light.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_menu_refresh_dark.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_menu_refresh_light.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_menu_remove_dark.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_menu_remove_light.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_menu_save_dark.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_menu_save_light.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_menu_search_dark.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_menu_search_light.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_menu_settings_dark.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_menu_settings_light.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_menu_share_dark.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_menu_share_light.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_menu_shuffle_dark.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_menu_shuffle_light.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_social_person.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_toggle_played.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_toggle_star.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_toggle_star_outline.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_toggle_star_outline_dark.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_toggle_star_outline_light.png create mode 100644 app/src/main/res/drawable-xxxhdpi/main_offline_dark.png create mode 100644 app/src/main/res/drawable-xxxhdpi/main_offline_light.png create mode 100644 app/src/main/res/drawable-xxxhdpi/main_select_server_dark.png create mode 100644 app/src/main/res/drawable-xxxhdpi/main_select_server_light.png create mode 100644 app/src/main/res/drawable-xxxhdpi/main_select_tabs_dark.png create mode 100644 app/src/main/res/drawable-xxxhdpi/main_select_tabs_light.png create mode 100644 app/src/main/res/drawable-xxxhdpi/media_backward_dark.png create mode 100644 app/src/main/res/drawable-xxxhdpi/media_backward_light.png create mode 100644 app/src/main/res/drawable-xxxhdpi/media_fastforward_dark.png create mode 100644 app/src/main/res/drawable-xxxhdpi/media_fastforward_light.png create mode 100644 app/src/main/res/drawable-xxxhdpi/media_forward_dark.png create mode 100644 app/src/main/res/drawable-xxxhdpi/media_forward_light.png create mode 100644 app/src/main/res/drawable-xxxhdpi/media_pause_dark.png create mode 100644 app/src/main/res/drawable-xxxhdpi/media_pause_light.png create mode 100644 app/src/main/res/drawable-xxxhdpi/media_repeat_all_dark.png create mode 100644 app/src/main/res/drawable-xxxhdpi/media_repeat_all_light.png create mode 100644 app/src/main/res/drawable-xxxhdpi/media_repeat_off_dark.png create mode 100644 app/src/main/res/drawable-xxxhdpi/media_repeat_off_light.png create mode 100644 app/src/main/res/drawable-xxxhdpi/media_repeat_single_dark.png create mode 100644 app/src/main/res/drawable-xxxhdpi/media_repeat_single_light.png create mode 100644 app/src/main/res/drawable-xxxhdpi/media_rewind_dark.png create mode 100644 app/src/main/res/drawable-xxxhdpi/media_rewind_light.png create mode 100644 app/src/main/res/drawable-xxxhdpi/media_start_dark.png create mode 100644 app/src/main/res/drawable-xxxhdpi/media_start_light.png create mode 100644 app/src/main/res/drawable-xxxhdpi/media_stop_dark.png create mode 100644 app/src/main/res/drawable-xxxhdpi/media_stop_light.png create mode 100644 app/src/main/res/drawable-xxxhdpi/notification_close_dark.png create mode 100644 app/src/main/res/drawable-xxxhdpi/notification_close_light.png create mode 100644 app/src/main/res/drawable-xxxhdpi/playing_dark.png create mode 100644 app/src/main/res/drawable-xxxhdpi/playing_light.png create mode 100644 app/src/main/res/drawable/appwidget4x1_preview.png create mode 100644 app/src/main/res/drawable/appwidget4x2_preview.png create mode 100644 app/src/main/res/drawable/appwidget4x3_preview.png create mode 100644 app/src/main/res/drawable/appwidget4x4_preview.png create mode 100644 app/src/main/res/drawable/audinaut.png create mode 100644 app/src/main/res/drawable/card_rounded_corners_black.xml create mode 100644 app/src/main/res/drawable/card_rounded_corners_dark.xml create mode 100644 app/src/main/res/drawable/card_rounded_corners_light.xml create mode 100644 app/src/main/res/drawable/fast_scroller_bubble.xml create mode 100644 app/src/main/res/drawable/fast_scroller_handle.xml create mode 100644 app/src/main/res/drawable/notification_backward.xml create mode 100644 app/src/main/res/drawable/notification_close.xml create mode 100644 app/src/main/res/drawable/notification_divider.xml create mode 100644 app/src/main/res/drawable/notification_fastforward.xml create mode 100644 app/src/main/res/drawable/notification_forward.xml create mode 100644 app/src/main/res/drawable/notification_pause.xml create mode 100644 app/src/main/res/drawable/notification_rewind.xml create mode 100644 app/src/main/res/drawable/notification_start.xml create mode 100644 app/src/main/res/layout-land/download.xml create mode 100644 app/src/main/res/layout-large-land/abstract_fragment_container.xml create mode 100644 app/src/main/res/layout-large-land/download.xml create mode 100644 app/src/main/res/layout-port/download.xml create mode 100644 app/src/main/res/layout/abstract_activity.xml create mode 100644 app/src/main/res/layout/abstract_fragment_activity.xml create mode 100644 app/src/main/res/layout/abstract_fragment_container.xml create mode 100644 app/src/main/res/layout/abstract_recycler_fragment.xml create mode 100644 app/src/main/res/layout/actionbar_spinner.xml create mode 100644 app/src/main/res/layout/album_cell_item.xml create mode 100644 app/src/main/res/layout/album_list_header.xml create mode 100644 app/src/main/res/layout/album_list_item.xml create mode 100644 app/src/main/res/layout/appwidget4x1.xml create mode 100644 app/src/main/res/layout/appwidget4x2.xml create mode 100644 app/src/main/res/layout/appwidget4x3.xml create mode 100644 app/src/main/res/layout/appwidget4x4.xml create mode 100644 app/src/main/res/layout/basic_art_item.xml create mode 100644 app/src/main/res/layout/basic_cell_item.xml create mode 100644 app/src/main/res/layout/basic_choice_item.xml create mode 100644 app/src/main/res/layout/basic_count_item.xml create mode 100644 app/src/main/res/layout/basic_header.xml create mode 100644 app/src/main/res/layout/basic_list_item.xml create mode 100644 app/src/main/res/layout/cache_location_buttons.xml create mode 100644 app/src/main/res/layout/complex_list_item.xml create mode 100644 app/src/main/res/layout/create_bookmark.xml create mode 100644 app/src/main/res/layout/details_item.xml create mode 100644 app/src/main/res/layout/download_media_buttons.xml create mode 100644 app/src/main/res/layout/download_playlist.xml create mode 100644 app/src/main/res/layout/download_slider.xml create mode 100644 app/src/main/res/layout/drawer_header.xml create mode 100644 app/src/main/res/layout/edit_play_action.xml create mode 100644 app/src/main/res/layout/equalizer.xml create mode 100644 app/src/main/res/layout/equalizer_bar.xml create mode 100644 app/src/main/res/layout/expandable_header.xml create mode 100644 app/src/main/res/layout/fast_scroller.xml create mode 100644 app/src/main/res/layout/genre_list_item.xml create mode 100644 app/src/main/res/layout/notification.xml create mode 100644 app/src/main/res/layout/notification_expanded.xml create mode 100644 app/src/main/res/layout/preferences.xml create mode 100644 app/src/main/res/layout/save_playlist.xml create mode 100644 app/src/main/res/layout/seekbar_preference.xml create mode 100644 app/src/main/res/layout/select_album_header.xml create mode 100644 app/src/main/res/layout/select_artist_header.xml create mode 100644 app/src/main/res/layout/settings_activity.xml create mode 100644 app/src/main/res/layout/shuffle_dialog.xml create mode 100644 app/src/main/res/layout/song_list_item.xml create mode 100644 app/src/main/res/layout/static_drawer_activity.xml create mode 100644 app/src/main/res/layout/tab_progress.xml create mode 100644 app/src/main/res/layout/update_playlist.xml create mode 100644 app/src/main/res/layout/user_list_item.xml create mode 100644 app/src/main/res/menu/abstract_top_menu.xml create mode 100644 app/src/main/res/menu/downloading.xml create mode 100644 app/src/main/res/menu/drawer_navigation.xml create mode 100644 app/src/main/res/menu/empty.xml create mode 100644 app/src/main/res/menu/main.xml create mode 100644 app/src/main/res/menu/multiselect_media.xml create mode 100644 app/src/main/res/menu/multiselect_media_offline.xml create mode 100644 app/src/main/res/menu/multiselect_nowplaying.xml create mode 100644 app/src/main/res/menu/multiselect_nowplaying_offline.xml create mode 100644 app/src/main/res/menu/nowplaying.xml create mode 100644 app/src/main/res/menu/nowplaying_context.xml create mode 100644 app/src/main/res/menu/nowplaying_context_offline.xml create mode 100644 app/src/main/res/menu/nowplaying_offline.xml create mode 100644 app/src/main/res/menu/search.xml create mode 100644 app/src/main/res/menu/select_album.xml create mode 100644 app/src/main/res/menu/select_album_context.xml create mode 100644 app/src/main/res/menu/select_album_context_offline.xml create mode 100644 app/src/main/res/menu/select_album_list.xml create mode 100644 app/src/main/res/menu/select_artist.xml create mode 100644 app/src/main/res/menu/select_artist_context.xml create mode 100644 app/src/main/res/menu/select_artist_context_offline.xml create mode 100644 app/src/main/res/menu/select_playlist_context.xml create mode 100644 app/src/main/res/menu/select_playlist_context_offline.xml create mode 100644 app/src/main/res/menu/select_song.xml create mode 100644 app/src/main/res/menu/select_song_context.xml create mode 100644 app/src/main/res/menu/select_song_context_offline.xml create mode 100644 app/src/main/res/menu/select_song_offline.xml create mode 100644 app/src/main/res/menu/tasker_configuration.xml create mode 100644 app/src/main/res/values-land/integers.xml create mode 100644 app/src/main/res/values-large-land/integers.xml create mode 100644 app/src/main/res/values-large/dimens.xml create mode 100644 app/src/main/res/values-large/integers.xml create mode 100644 app/src/main/res/values-v21/styles.xml create mode 100644 app/src/main/res/values-v21/themes.xml create mode 100644 app/src/main/res/values-xlarge-land/integers.xml create mode 100644 app/src/main/res/values/arrays.xml create mode 100644 app/src/main/res/values/attrs.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/dimens.xml create mode 100644 app/src/main/res/values/ids.xml create mode 100644 app/src/main/res/values/integers.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 app/src/main/res/values/themes.xml create mode 100644 app/src/main/res/xml/appwidget4x1.xml create mode 100644 app/src/main/res/xml/appwidget4x2.xml create mode 100644 app/src/main/res/xml/appwidget4x3.xml create mode 100644 app/src/main/res/xml/appwidget4x4.xml create mode 100644 app/src/main/res/xml/authenticator.xml create mode 100644 app/src/main/res/xml/auto_app_description.xml create mode 100644 app/src/main/res/xml/playlists_syncadapter.xml create mode 100644 app/src/main/res/xml/searchable.xml create mode 100644 app/src/main/res/xml/settings.xml create mode 100644 app/src/main/res/xml/settings_appearance.xml create mode 100644 app/src/main/res/xml/settings_cache.xml create mode 100644 app/src/main/res/xml/settings_playback.xml create mode 100644 app/src/main/res/xml/settings_servers.xml create mode 100644 audinaut.svg create mode 100644 build.gradle create mode 100644 debug.keystore create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4838c7c --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +.classpath +.project +bin/* +gen/* +private/* +nbandroid/* +.idea +subsonic-android.iml +releases/ +proguard_logs/ +/gen/ +/out/ +.gradle/* +/build/ +local.properties +*Thumbs.db \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..91dd332 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "ServerProxy"] + path = ServerProxy + url = https://github.com/daneren2005/ServerProxy.git diff --git a/Audinaut.iml b/Audinaut.iml new file mode 100644 index 0000000..561e0d8 --- /dev/null +++ b/Audinaut.iml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 54999e6..ff1c333 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,19 @@ # Audinaut -A Libresonic client for Android. +
+A FOSS Libresonic client for Android. + +## Building +``` +git submodule update --init +gradle assemble +``` + +## SDK Project Dependencies +Under sdk -> extras:
+android -> support -> v7 -> appcompat
+android -> support -> v7 -> mediarouter
+ +## SDK Library Dependencies +android -> support -> v4 -> android-support-v4.jar
+android -> support -> v7 -> appcompat -> libs android-support-v7-appcompat.jar
+android -> support -> v7 -> mediarouter -> libs -> android-support-v7-mediarouter.jar
diff --git a/ServerProxy b/ServerProxy new file mode 160000 index 0000000..a4d9573 --- /dev/null +++ b/ServerProxy @@ -0,0 +1 @@ +Subproject commit a4d957353db2634906e0d5099d7a078a111bfab9 diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..e8fa30f --- /dev/null +++ b/app/.gitignore @@ -0,0 +1,2 @@ +/build +*.iml diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..dcd8a59 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,63 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 23 + buildToolsVersion "23.0.3" + useLibrary 'org.apache.http.legacy' + + defaultConfig { + applicationId "github.nvllsvm.audinaut" + minSdkVersion 19 + targetSdkVersion 23 + versionCode 186 + versionName '0.1.0' + setProperty("archivesBaseName", "Audinaut $versionName") + resConfigs "de", "es", "fr", "hu", "nl", "pt-rPT", "ru", "sv" + } + buildTypes { + release { + minifyEnabled true + shrinkResources true + proguardFiles 'proguard.cfg' + zipAlignEnabled true + } + fix { + minifyEnabled true + shrinkResources true + proguardFiles 'proguard.cfg' + zipAlignEnabled true + } + } + + packagingOptions { + exclude 'META-INF/beans.xml' + } + + lintOptions { + checkReleaseBuilds false + warning 'InvalidPackage' + } + + signingConfigs { + debug { + storeFile file('../debug.keystore') + } + } +} + +dependencies { + compile project(':Server Proxy') + compile fileTree(include: ['*.jar'], dir: 'libs') + compile 'com.android.support:support-v4:23.4.+' + compile 'com.android.support:appcompat-v7:23.4.+' + compile 'com.android.support:mediarouter-v7:23.4.+' + compile 'com.android.support:recyclerview-v7:23.4.+' + compile 'com.android.support:design:23.4.+' + compile 'com.sothree.slidinguppanel:library:3.0.0' + compile 'de.hdodenhof:circleimageview:1.2.1' + compile group: 'org.fourthline.cling', name: 'cling-core', version:'2.1.1' + compile group: 'org.fourthline.cling', name: 'cling-support', version:'2.1.1' + compile group: 'org.eclipse.jetty', name: 'jetty-server', version:'8.1.16.v20140903' + compile group: 'org.eclipse.jetty', name: 'jetty-servlet', version:'8.1.16.v20140903' + compile group: 'org.eclipse.jetty', name: 'jetty-client', version:'8.1.16.v20140903' +} diff --git a/app/libs/kryo-2.21-all.jar b/app/libs/kryo-2.21-all.jar new file mode 100644 index 0000000000000000000000000000000000000000..83f8b0f0b78b2f08e17dbfdd5505410f2b40e96a GIT binary patch literal 236628 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
  • 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­5X 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 literal 0 HcmV?d00001 diff --git a/app/proguard.cfg b/app/proguard.cfg new file mode 100644 index 0000000..a18ae91 --- /dev/null +++ b/app/proguard.cfg @@ -0,0 +1,62 @@ +-dontobfuscate +-optimizationpasses 5 +-dontusemixedcaseclassnames +-dontskipnonpubliclibraryclasses +-dontpreverify +-verbose +-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*,!code/allocation/variable + +-keep public class * extends android.app.Activity +-keep public class * extends android.app.Application +-keep public class * extends android.app.Service +-keep public class * extends android.content.BroadcastReceiver +-keep public class * extends android.content.ContentProvider +-keep public class * extends android.app.backup.BackupAgentHelper +-keep public class * extends android.preference.Preference + +# Kryo +-keep,allowshrinking class java.beans.** { *; } +-keep,allowshrinking class sun.reflect.** { *; } +-dontwarn sun.reflect.** +-dontwarn java.beans.** +-keepclassmembers public class com.esotericsoftware.** { *; } + +-keepclasseswithmembernames class * { + native ; +} + +-keepclassmembers class * extends android.app.Activity { + public void *(android.view.View); +} + +-keepclassmembers public class * extends android.view.View { + void set*(***); + *** get*(); +} + +-keepclassmembers enum * { + public static **[] values(); + public static ** valueOf(java.lang.String); +} + +-keep class * implements android.os.Parcelable { + public static final android.os.Parcelable$Creator *; +} + +-keep class android.support.v7.app.MediaRouteButton { *; } +-keep class android.support.v7.widget.SearchView { *; } + +-dontwarn android.support.** + +# DLNA/Cling +-keep class org.fourthline.cling.** { *; } +-keep interface org.fourthline.cling.** { *; } +-dontwarn javax.** +-dontwarn org.objectweb.** +-dontwarn org.slf4j.** +-dontwarn org.mortbay.** +-dontwarn org.fourthline.** +-dontwarn org.seamless.** +-dontwarn org.eclipse.** +-dontwarn java.** +-keepattributes *Annotation*, InnerClasses \ No newline at end of file diff --git a/app/src/androidTest/java/github/nvllsvm/audinaut/ApplicationTest.java b/app/src/androidTest/java/github/nvllsvm/audinaut/ApplicationTest.java new file mode 100644 index 0000000..e5d0567 --- /dev/null +++ b/app/src/androidTest/java/github/nvllsvm/audinaut/ApplicationTest.java @@ -0,0 +1,13 @@ +package github.nvllsvm.audinaut; + +import android.app.Application; +import android.test.ApplicationTestCase; + +/** + * Testing Fundamentals + */ +public class ApplicationTest extends ApplicationTestCase { + public ApplicationTest() { + super(Application.class); + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/github/nvllsvm/audinaut/activity/SubsonicFragmentActivityTest.java b/app/src/androidTest/java/github/nvllsvm/audinaut/activity/SubsonicFragmentActivityTest.java new file mode 100644 index 0000000..0458300 --- /dev/null +++ b/app/src/androidTest/java/github/nvllsvm/audinaut/activity/SubsonicFragmentActivityTest.java @@ -0,0 +1,34 @@ +package github.nvllsvm.audinaut.activity; + +import github.nvllsvm.audinaut.R; +import android.test.ActivityInstrumentationTestCase2; + +public class SubsonicFragmentActivityTest extends + ActivityInstrumentationTestCase2 { + + 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/nvllsvm/audinaut/domain/GenreComparatorTest.java b/app/src/androidTest/java/github/nvllsvm/audinaut/domain/GenreComparatorTest.java new file mode 100644 index 0000000..df36d9e --- /dev/null +++ b/app/src/androidTest/java/github/nvllsvm/audinaut/domain/GenreComparatorTest.java @@ -0,0 +1,68 @@ +package github.nvllsvm.audinaut.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 genres = new ArrayList(); + genres.add(g1); + genres.add(g2); + + List 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 genres = new ArrayList(); + genres.add(g1); + genres.add(g2); + + List 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 genres = new ArrayList(); + genres.add(g1); + genres.add(g2); + genres.add(g3); + + List 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/nvllsvm/audinaut/service/DownloadServiceTest.java b/app/src/androidTest/java/github/nvllsvm/audinaut/service/DownloadServiceTest.java new file mode 100644 index 0000000..9366f2b --- /dev/null +++ b/app/src/androidTest/java/github/nvllsvm/audinaut/service/DownloadServiceTest.java @@ -0,0 +1,296 @@ +package github.nvllsvm.audinaut.service; + +import static github.nvllsvm.audinaut.domain.PlayerState.COMPLETED; +import static github.nvllsvm.audinaut.domain.PlayerState.IDLE; +import static github.nvllsvm.audinaut.domain.PlayerState.PAUSED; +import static github.nvllsvm.audinaut.domain.PlayerState.STARTED; +import static github.nvllsvm.audinaut.domain.PlayerState.STOPPED; +import java.util.List; + +import github.nvllsvm.audinaut.activity.SubsonicFragmentActivity; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.PlayerState; + +import java.util.LinkedList; +import android.test.ActivityInstrumentationTestCase2; +import android.util.Log; + +public class DownloadServiceTest extends + ActivityInstrumentationTestCase2 { + + 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("testPrevWithPlayList", "Start waiting to downloads"); + Thread.sleep(5000); + Log.w("testPrevWithPlayList", "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 testPrevWithPlayList() throws InterruptedException { + // Download two songs + downloadService.getDownloads().clear(); + downloadService.download(this.createMusicSongs(2), false, false, false, + false, 0, 0); + + Log.w("testPrevWithPlayList", "Start waiting downloads"); + Thread.sleep(5000); + Log.w("testPrevWithPlayList", "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("testPrevWithPlayList", "Start waiting downloads"); + Thread.sleep(5000); + Log.w("testPrevWithPlayList", "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("testPrevWithPlayList", "Start waiting downloads"); + Thread.sleep(5000); + Log.w("testPrevWithPlayList", "Stop waiting downloads"); + + PlayerState playerState = downloadService.getPlayerState(); + assertEquals(STARTED, playerState); + } + + /** + * Test if the download list is empty. + */ + public void testGetDownloadsEmptyList() { + List 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 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 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 musicEntries = new LinkedList(); + + for (int i = 0; i < size; i++) { + musicEntries.add(musicEntry); + } + + return musicEntries; + + } + +} diff --git a/app/src/audinaut-stacktrace.txt b/app/src/audinaut-stacktrace.txt new file mode 100644 index 0000000..57a6a54 --- /dev/null +++ b/app/src/audinaut-stacktrace.txt @@ -0,0 +1,26 @@ +Android API level: 23 +Subsonic version name: 5.3 +Subsonic version code: 186 + +android.content.res.Resources$NotFoundException: Resource ID #0xff33b5e5 + at android.content.res.Resources.getValue(Resources.java:1432) + at android.content.res.Resources.getValue(Resources.java:1412) + at android.content.res.Resources.getColor(Resources.java:1028) + at android.content.res.Resources.getColor(Resources.java:1001) + at android.support.v4.widget.SwipeRefreshLayout.setColorSchemeResources(SwipeRefreshLayout.java:529) + at github.nvllsvm.audinaut.fragments.SubsonicFragment.setupScrollList(SubsonicFragment.java:691) + at github.nvllsvm.audinaut.fragments.SelectRecyclerFragment.onCreateView(SelectRecyclerFragment.java:89) + at github.nvllsvm.audinaut.fragments.SelectArtistFragment.onCreateView(SelectArtistFragment.java:77) + at android.support.v4.app.Fragment.performCreateView(Fragment.java:1974) + at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1067) + at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1252) + at android.support.v4.app.BackStackRecord.run(BackStackRecord.java:742) + at android.support.v4.app.FragmentManagerImpl.execPendingActions(FragmentManager.java:1617) + at android.support.v4.app.FragmentManagerImpl$1.run(FragmentManager.java:517) + at android.os.Handler.handleCallback(Handler.java:739) + at android.os.Handler.dispatchMessage(Handler.java:95) + at android.os.Looper.loop(Looper.java:148) + at android.app.ActivityThread.main(ActivityThread.java:5461) + at java.lang.reflect.Method.invoke(Native Method) + at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726) + at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616) diff --git a/app/src/main/.gradle/3.1/taskArtifacts/cache.properties b/app/src/main/.gradle/3.1/taskArtifacts/cache.properties new file mode 100644 index 0000000..9e3a6ad --- /dev/null +++ b/app/src/main/.gradle/3.1/taskArtifacts/cache.properties @@ -0,0 +1 @@ +#Sat Oct 01 15:10:08 EDT 2016 diff --git a/app/src/main/.gradle/3.1/taskArtifacts/cache.properties.lock b/app/src/main/.gradle/3.1/taskArtifacts/cache.properties.lock new file mode 100644 index 0000000000000000000000000000000000000000..405bc39f9d6244eee3cde744400f3898cc6d9bab GIT binary patch literal 17 UcmZQ>G+li;)>>}^BHvv+sGa27)2d4q+!z(<0j&$8J&Hr FeE^TmEe`+y literal 0 HcmV?d00001 diff --git a/app/src/main/.gradle/3.1/taskArtifacts/taskArtifacts.bin b/app/src/main/.gradle/3.1/taskArtifacts/taskArtifacts.bin new file mode 100644 index 0000000000000000000000000000000000000000..60fe4e4db1d7d3a8ff3e9223e96c1aa5d4d6c803 GIT binary patch literal 18917 zcmeI)F-yZh6u|L2H>GGCgsu(_q8_5qQBYH;j-rAHN=QOTTa%hZ3p(lMU7{8uLlyo9@80EJhIhX;PF`7LyBY*$` z2q1s}0tg_000IagfB*srAbk)P~K7ejR+rgM5B%ePbK>!077 zS-JGs{eIBRdVf5tF(ZHg0tg_000IagfB*srAb=&0RU I??mL_FFL`s4FCWD literal 0 HcmV?d00001 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..3b66ddb --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/github/nvllsvm/audinaut/activity/EditPlayActionActivity.java b/app/src/main/java/github/nvllsvm/audinaut/activity/EditPlayActionActivity.java new file mode 100644 index 0000000..47b6ffb --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/activity/EditPlayActionActivity.java @@ -0,0 +1,245 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2014 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.activity; + +import android.app.Activity; +import android.support.v7.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.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.Genre; +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.service.MusicServiceFactory; +import github.nvllsvm.audinaut.service.OfflineException; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.LoadingTask; +import github.nvllsvm.audinaut.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>(context, true) { + @Override + protected List doInBackground() throws Throwable { + MusicService musicService = MusicServiceFactory.getMusicService(context); + return musicService.getGenres(false, context, this); + } + + @Override + protected void done(final List genres) { + List names = new ArrayList(); + 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 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) { + 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 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/nvllsvm/audinaut/activity/QueryReceiverActivity.java b/app/src/main/java/github/nvllsvm/audinaut/activity/QueryReceiverActivity.java new file mode 100644 index 0000000..52a8f19 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/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 . + + Copyright 2009 (C) Sindre Mehus + */ + +package github.nvllsvm.audinaut.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.nvllsvm.audinaut.fragments.SubsonicFragment; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.Util; +import github.nvllsvm.audinaut.provider.AudinautSearchProvider; + +/** + * 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/nvllsvm/audinaut/activity/SettingsActivity.java b/app/src/main/java/github/nvllsvm/audinaut/activity/SettingsActivity.java new file mode 100644 index 0000000..06a314b --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/activity/SettingsActivity.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.activity; + +import android.annotation.TargetApi; +import android.os.Build; +import android.os.Bundle; +import android.support.v7.widget.Toolbar; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.fragments.PreferenceCompatFragment; +import github.nvllsvm.audinaut.fragments.SettingsFragment; +import github.nvllsvm.audinaut.util.Constants; + +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); + lastSelectedPosition = R.id.drawer_settings; + setContentView(R.layout.settings_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(); + } + + Toolbar mainToolbar = (Toolbar) findViewById(R.id.main_toolbar); + setSupportActionBar(mainToolbar); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/activity/SubsonicActivity.java b/app/src/main/java/github/nvllsvm/audinaut/activity/SubsonicActivity.java new file mode 100644 index 0000000..3789292 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/activity/SubsonicActivity.java @@ -0,0 +1,1055 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.activity; + +import android.app.UiModeManager; +import android.content.Context; +import android.content.DialogInterface; +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.media.AudioManager; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +import android.support.design.widget.NavigationView; +import android.support.v4.app.ActivityCompat; +import android.support.v4.content.ContextCompat; +import android.support.v7.app.ActionBarDrawerToggle; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentTransaction; +import android.support.v4.widget.DrawerLayout; +import android.support.v7.app.AlertDialog; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.app.AppCompatDelegate; +import android.support.v7.widget.Toolbar; +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.CheckBox; +import android.widget.ImageView; +import android.widget.Spinner; +import android.widget.TextView; + +import java.io.File; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.fragments.SubsonicFragment; +import github.nvllsvm.audinaut.service.DownloadService; +import github.nvllsvm.audinaut.service.HeadphoneListenerService; +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.service.MusicServiceFactory; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.DrawableTint; +import github.nvllsvm.audinaut.util.ImageLoader; +import github.nvllsvm.audinaut.util.SilentBackgroundTask; +import github.nvllsvm.audinaut.util.ThemeUtil; +import github.nvllsvm.audinaut.util.Util; +import github.nvllsvm.audinaut.view.UpdateView; +import github.nvllsvm.audinaut.util.UserUtil; + +import static android.Manifest.*; + +public class SubsonicActivity extends AppCompatActivity implements OnItemSelectedListener { + private static final String TAG = SubsonicActivity.class.getSimpleName(); + private static ImageLoader IMAGE_LOADER; + protected static String theme; + protected static boolean fullScreen; + protected static boolean actionbarColored; + private static final int MENU_GROUP_SERVER = 10; + private static final int MENU_ITEM_SERVER_BASE = 100; + private static final int PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE = 1; + + private final List afterServiceAvailable = new ArrayList<>(); + private boolean drawerIdle = true; + private boolean destroyed = false; + private boolean finished = false; + protected List backStack = new ArrayList(); + protected SubsonicFragment currentFragment; + protected View primaryContainer; + protected View secondaryContainer; + protected boolean tv = false; + protected boolean touchscreen = true; + protected Handler handler = new Handler(); + Spinner actionBarSpinner; + ArrayAdapter spinnerAdapter; + ViewGroup rootView; + DrawerLayout drawer; + ActionBarDrawerToggle drawerToggle; + NavigationView drawerList; + View drawerHeader; + ImageView drawerHeaderToggle; + TextView drawerServerName; + TextView drawerUserName; + int lastSelectedPosition = 0; + boolean showingTabs = true; + boolean drawerOpen = false; + SharedPreferences.OnSharedPreferenceChangeListener preferencesListener; + + static { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO); + } + + @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); + + if(getIntent().hasExtra(Constants.FRAGMENT_POSITION)) { + lastSelectedPosition = getIntent().getIntExtra(Constants.FRAGMENT_POSITION, 0); + } + + if(preferencesListener == null) { + Util.getPreferences(this).registerOnSharedPreferenceChangeListener(preferencesListener); + } + + if (ContextCompat.checkSelfPermission(this, permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(this, new String[]{ permission.WRITE_EXTERNAL_STORAGE }, PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { + switch (requestCode) { + case PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE: { + // If request is cancelled, the result arrays are empty. + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + + } else { + Util.toast(this, R.string.permission_external_storage_failed); + finish(); + } + } + } + } + + @Override + protected void onPostCreate(Bundle savedInstanceState) { + super.onPostCreate(savedInstanceState); + + if(spinnerAdapter == null) { + createCustomActionBarView(); + } + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setHomeButtonEnabled(true); + + if(Util.shouldStartOnHeadphones(this)) { + Intent serviceIntent = new Intent(); + serviceIntent.setClassName(this.getPackageName(), HeadphoneListenerService.class.getName()); + this.startService(serviceIntent); + } + } + + protected void createCustomActionBarView() { + actionBarSpinner = (Spinner) getLayoutInflater().inflate(R.layout.actionbar_spinner, null); + if((this instanceof SubsonicFragmentActivity || this instanceof SettingsActivity) && (Util.getPreferences(this).getBoolean(Constants.PREFERENCES_KEY_COLOR_ACTION_BAR, true) || ThemeUtil.getThemeRes(this) != R.style.Theme_Audinaut_Light_No_Color)) { + actionBarSpinner.setBackgroundDrawable(DrawableTint.getTintedDrawableFromColor(this, R.drawable.abc_spinner_mtrl_am_alpha, android.R.color.white)); + } + 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(actionBarSpinner); + } + + @Override + protected void onResume() { + super.onResume(); + Util.registerMediaButtonEventReceiver(this); + + // Make sure to update theme + SharedPreferences prefs = Util.getPreferences(this); + if (theme != null && !theme.equals(ThemeUtil.getTheme(this)) || fullScreen != prefs.getBoolean(Constants.PREFERENCES_KEY_FULL_SCREEN, false) || actionbarColored != prefs.getBoolean(Constants.PREFERENCES_KEY_COLOR_ACTION_BAR, true)) { + restart(); + overridePendingTransition(R.anim.fade_in, R.anim.fade_out); + DrawableTint.wipeTintCache(); + } + + populateTabs(); + getImageLoader().onUIVisible(); + UpdateView.addActiveActivity(); + } + + @Override + protected void onPause() { + super.onPause(); + + UpdateView.removeActiveActivity(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + destroyed = true; + Util.getPreferences(this).unregisterOnSharedPreferenceChangeListener(preferencesListener); + } + + @Override + public void finish() { + super.finish(); + Util.disablePendingTransition(this); + } + + @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 = (NavigationView) findViewById(R.id.left_drawer); + drawerList.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() { + @Override + public boolean onNavigationItemSelected(final MenuItem menuItem) { + if(showingTabs) { + // Settings are on a different selectable track + if (menuItem.getItemId() != R.id.drawer_settings && menuItem.getItemId() != R.id.drawer_offline) { + menuItem.setChecked(true); + lastSelectedPosition = menuItem.getItemId(); + } + + switch (menuItem.getItemId()) { + case R.id.drawer_library: + drawerItemSelected("Artist"); + return true; + case R.id.drawer_playlists: + drawerItemSelected("Playlist"); + return true; + case R.id.drawer_downloading: + drawerItemSelected("Download"); + return true; + case R.id.drawer_offline: + toggleOffline(); + return true; + case R.id.drawer_settings: + startActivity(new Intent(SubsonicActivity.this, SettingsActivity.class)); + drawer.closeDrawers(); + return true; + } + } else { + int activeServer = menuItem.getItemId() - MENU_ITEM_SERVER_BASE; + SubsonicActivity.this.setActiveServer(activeServer); + populateTabs(); + return true; + } + + return false; + } + }); + + drawerHeader = drawerList.inflateHeaderView(R.layout.drawer_header); + drawerHeader.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if(showingTabs) { + populateServers(); + } else { + populateTabs(); + } + } + }); + + drawerHeaderToggle = (ImageView) drawerHeader.findViewById(R.id.header_select_image); + drawerServerName = (TextView) drawerHeader.findViewById(R.id.header_server_name); + drawerUserName = (TextView) drawerHeader.findViewById(R.id.header_user_name); + + updateDrawerHeader(); + + if(!isTv()) { + drawer = (DrawerLayout) findViewById(R.id.drawer_layout); + + // Pass in toolbar if it exists + Toolbar toolbar = (Toolbar) findViewById(R.id.main_toolbar); + drawerToggle = new ActionBarDrawerToggle(this, drawer, toolbar, R.string.common_appname, R.string.common_appname) { + @Override + public void onDrawerClosed(View view) { + drawerIdle = true; + drawerOpen = false; + + if(!showingTabs) { + populateTabs(); + } + } + + @Override + public void onDrawerOpened(View view) { + DownloadService downloadService = getDownloadService(); + boolean downloadingVisible = downloadService != null && !downloadService.getBackgroundDownloads().isEmpty(); + if(lastSelectedPosition == R.id.drawer_downloading) { + downloadingVisible = true; + } + setDrawerItemVisible(R.id.drawer_downloading, downloadingVisible); + + drawerIdle = true; + drawerOpen = true; + } + + @Override + public void onDrawerSlide(View drawerView, float slideOffset) { + super.onDrawerSlide(drawerView, slideOffset); + drawerIdle = false; + } + }; + drawer.setDrawerListener(drawerToggle); + drawerToggle.setDrawerIndicatorEnabled(true); + + 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); + if(lastSelectedPosition != 0) { + MenuItem item = drawerList.getMenu().findItem(lastSelectedPosition); + if(item != null) { + item.setChecked(true); + } + } + recreateSpinner(); + } + + @Override + public void onNewIntent(Intent intent) { + super.onNewIntent(intent); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater menuInflater = getMenuInflater(); + SubsonicFragment currentFragment = getCurrentFragment(); + if(currentFragment != null) { + try { + SubsonicFragment fragment = getCurrentFragment(); + fragment.setContext(this); + fragment.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 getCurrentFragment().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; + + return super.onKeyDown(keyCode, event); + } + + @Override + public void setTitle(CharSequence title) { + if(title != null && getSupportActionBar() != 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 >= 0; i--) { + removeCurrent(); + } + } + } + + @Override + public void onNothingSelected(AdapterView parent) { + + } + + private void populateTabs() { + drawerList.getMenu().clear(); + drawerList.inflateMenu(R.menu.drawer_navigation); + + SharedPreferences prefs = Util.getPreferences(this); + boolean sharedEnabled = prefs.getBoolean(Constants.PREFERENCES_KEY_SHARED_ENABLED, true) && !Util.isOffline(this); + + MenuItem offlineMenuItem = drawerList.getMenu().findItem(R.id.drawer_offline); + if(Util.isOffline(this)) { + setDrawerItemVisible(R.id.drawer_library, false); + + if(lastSelectedPosition == 0 || lastSelectedPosition == R.id.drawer_library) { + String newFragment = Util.openToTab(this); + if(newFragment == null || "Library".equals(newFragment)) { + newFragment = "Artist"; + } + + lastSelectedPosition = getDrawerItemId(newFragment); + drawerItemSelected(newFragment); + } + + offlineMenuItem.setTitle(R.string.main_online); + } else { + offlineMenuItem.setTitle(R.string.main_offline); + } + + if(lastSelectedPosition != 0) { + MenuItem item = drawerList.getMenu().findItem(lastSelectedPosition); + if(item != null) { + item.setChecked(true); + } + } + drawerHeaderToggle.setImageResource(R.drawable.main_select_server_dark); + + showingTabs = true; + } + private void populateServers() { + drawerList.getMenu().clear(); + + int serverCount = Util.getServerCount(this); + int activeServer = Util.getActiveServer(this); + for(int i = 1; i <= serverCount; i++) { + MenuItem item = drawerList.getMenu().add(MENU_GROUP_SERVER, MENU_ITEM_SERVER_BASE + i, MENU_ITEM_SERVER_BASE + i, Util.getServerName(this, i)); + if(activeServer == i) { + item.setChecked(true); + } + } + drawerList.getMenu().setGroupCheckable(MENU_GROUP_SERVER, true, true); + drawerHeaderToggle.setImageResource(R.drawable.main_select_tabs_dark); + + showingTabs = false; + } + private void setDrawerItemVisible(int id, boolean visible) { + MenuItem item = drawerList.getMenu().findItem(id); + if(item != null) { + item.setVisible(visible); + } + } + + protected void drawerItemSelected(String fragmentType) { + if(currentFragment != null) { + currentFragment.stopActionMode(); + } + startFragmentActivity(fragmentType); + } + + 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); + } + if(lastSelectedPosition != 0) { + intent.putExtra(Constants.FRAGMENT_POSITION, lastSelectedPosition); + } + 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 SubsonicFragment getCurrentFragment() { + return this.currentFragment; + } + + 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 || oldFragment.isAlwaysFullscreen() || currentFragment.isAlwaysStartFullscreen()) { + 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(); + + oldFragment.setIsOnlyVisible(false); + currentFragment.setIsOnlyVisible(false); + } + recreateSpinner(); + } + public void removeCurrent() { + // Don't try to remove current if there is no backstack to remove from + if(backStack.isEmpty()) { + return; + } + + if(currentFragment != null) { + currentFragment.setPrimaryFragment(false); + } + SubsonicFragment oldFragment = currentFragment; + + currentFragment = backStack.remove(backStack.size() - 1); + currentFragment.setPrimaryFragment(true, false); + supportInvalidateOptionsMenu(); + + if(secondaryContainer == null || currentFragment.isAlwaysFullscreen() || oldFragment.isAlwaysStartFullscreen()) { + 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(oldFragment); + 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(oldFragment); + + // Only switch places if there is a backstack, otherwise primary container is correct + if(backStack.size() > 0 && !backStack.get(backStack.size() - 1).isAlwaysFullscreen() && !currentFragment.isAlwaysStartFullscreen()) { + 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); + + currentFragment.setIsOnlyVisible(true); + } + + trans.commit(); + } + recreateSpinner(); + } + public void replaceExistingFragment(SubsonicFragment fragment, int tag) { + FragmentTransaction trans = getSupportFragmentManager().beginTransaction(); + trans.remove(currentFragment); + trans.add(R.id.fragment_container, fragment, tag + ""); + trans.commit(); + + currentFragment = fragment; + currentFragment.setPrimaryFragment(true); + supportInvalidateOptionsMenu(); + } + + public void invalidate() { + if(currentFragment != null) { + while(backStack.size() > 0) { + removeCurrent(); + } + + currentFragment.invalidate(); + populateTabs(); + } + + supportInvalidateOptionsMenu(); + } + + protected void recreateSpinner() { + if(currentFragment == null || currentFragment.getTitle() == null) { + return; + } + if(spinnerAdapter == null || getSupportActionBar().getCustomView() == null) { + createCustomActionBarView(); + } + + if(backStack.size() > 0) { + createCustomActionBarView(); + 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().setDisplayShowTitleEnabled(false); + getSupportActionBar().setDisplayShowCustomEnabled(true); + } + + if(drawerToggle.isDrawerIndicatorEnabled()) { + getSupportActionBar().setDisplayHomeAsUpEnabled(false); + drawerToggle.setDrawerIndicatorEnabled(false); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + } + } else if(!isTv()) { + getSupportActionBar().setDisplayShowTitleEnabled(true); + getSupportActionBar().setTitle(currentFragment.getTitle()); + getSupportActionBar().setDisplayShowCustomEnabled(false); + drawerToggle.setDrawerIndicatorEnabled(true); + } + } + + protected void restart() { + restart(true); + } + protected void restart(boolean resumePosition) { + Intent intent = new Intent(this, this.getClass()); + intent.putExtras(getIntent()); + if(resumePosition) { + intent.putExtra(Constants.FRAGMENT_POSITION, lastSelectedPosition); + } else { + String fragmentType = Util.openToTab(this); + intent.putExtra(Constants.INTENT_EXTRA_FRAGMENT_TYPE, fragmentType); + intent.putExtra(Constants.FRAGMENT_POSITION, getDrawerItemId(fragmentType)); + } + finish(); + Util.startActivityWithoutTransition(this, intent); + } + + private void applyTheme() { + theme = ThemeUtil.getTheme(this); + + if(theme != null && theme.indexOf("fullscreen") != -1) { + theme = theme.substring(0, theme.indexOf("_fullscreen")); + ThemeUtil.setTheme(this, theme); + } + + ThemeUtil.applyTheme(this, theme); + actionbarColored = Util.getPreferences(this).getBoolean(Constants.PREFERENCES_KEY_COLOR_ACTION_BAR, true); + } + 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) { + break; + } + Log.w(TAG, "DownloadService not running. Attempting to start it."); + startService(new Intent(this, DownloadService.class)); + Util.sleepQuietly(50L); + } + + final DownloadService downloadService = DownloadService.getInstance(); + if(downloadService != null && afterServiceAvailable.size() > 0) { + for(Runnable runnable: afterServiceAvailable) { + handler.post(runnable); + } + afterServiceAvailable.clear(); + } + return downloadService; + } + public void runWhenServiceAvailable(Runnable runnable) { + if(getDownloadService() != null) { + runnable.run(); + } else { + afterServiceAvailable.add(runnable); + checkIfServiceAvailable(); + } + } + private void checkIfServiceAvailable() { + if(getDownloadService() == null) { + handler.postDelayed(new Runnable() { + @Override + public void run() { + checkIfServiceAvailable(); + } + }, 50); + } else if(afterServiceAvailable.size() > 0) { + for(Runnable runnable: afterServiceAvailable) { + handler.post(runnable); + } + afterServiceAvailable.clear(); + } + } + + public static String getThemeName() { + return theme; + } + + public boolean isTv() { + return tv; + } + public boolean isTouchscreen() { + return touchscreen; + } + + public void openNowPlaying() { + + } + public void closeNowPlaying() { + + } + + public void setActiveServer(int instance) { + if (Util.getActiveServer(this) != instance) { + final DownloadService service = getDownloadService(); + if (service != null) { + new SilentBackgroundTask(this) { + @Override + protected Void doInBackground() throws Throwable { + service.clearIncomplete(); + return null; + } + }.execute(); + + } + Util.setActiveServer(this, instance); + invalidate(); + UserUtil.refreshCurrentUser(this, false, true); + updateDrawerHeader(); + } + } + public void updateDrawerHeader() { + if(Util.isOffline(this)) { + drawerServerName.setText(R.string.select_album_offline); + drawerUserName.setText(""); + drawerHeader.setClickable(false); + drawerHeaderToggle.setVisibility(View.GONE); + } else { + drawerServerName.setText(Util.getServerName(this)); + drawerUserName.setText(UserUtil.getCurrentUsername(this)); + drawerHeader.setClickable(true); + drawerHeaderToggle.setVisibility(View.VISIBLE); + } + } + + public void toggleOffline() { + boolean isOffline = Util.isOffline(this); + Util.setOffline(this, !isOffline); + invalidate(); + DownloadService service = getDownloadService(); + if (service != null) { + service.setOnline(isOffline); + } + + UserUtil.seedCurrentUser(this); + this.updateDrawerHeader(); + drawer.closeDrawers(); + } + + public int getDrawerItemId(String fragmentType) { + if(fragmentType == null) { + return R.id.drawer_library; + } + + switch(fragmentType) { + case "Artist": + return R.id.drawer_library; + case "Playlist": + return R.id.drawer_playlists; + default: + return R.id.drawer_library; + } + } + + 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.nvllsvm.audinaut", 0); + file = new File(Environment.getExternalStorageDirectory(), "audinaut-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/nvllsvm/audinaut/activity/SubsonicFragmentActivity.java b/app/src/main/java/github/nvllsvm/audinaut/activity/SubsonicFragmentActivity.java new file mode 100644 index 0000000..7dca8a4 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/activity/SubsonicFragmentActivity.java @@ -0,0 +1,929 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.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.FragmentManager; +import android.support.v4.app.FragmentTransaction; +import android.support.v7.app.AlertDialog; +import android.support.v7.widget.Toolbar; +import android.util.Log; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.TextView; + +import com.sothree.slidinguppanel.SlidingUpPanelLayout; + +import java.io.File; +import java.util.Date; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.PlayerQueue; +import github.nvllsvm.audinaut.domain.PlayerState; +import github.nvllsvm.audinaut.fragments.DownloadFragment; +import github.nvllsvm.audinaut.fragments.NowPlayingFragment; +import github.nvllsvm.audinaut.fragments.SearchFragment; +import github.nvllsvm.audinaut.fragments.SelectArtistFragment; +import github.nvllsvm.audinaut.fragments.SelectDirectoryFragment; +import github.nvllsvm.audinaut.fragments.SelectPlaylistFragment; +import github.nvllsvm.audinaut.fragments.SubsonicFragment; +import github.nvllsvm.audinaut.service.DownloadFile; +import github.nvllsvm.audinaut.service.DownloadService; +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.service.MusicServiceFactory; +import github.nvllsvm.audinaut.updates.Updater; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.FileUtil; +import github.nvllsvm.audinaut.util.SilentBackgroundTask; +import github.nvllsvm.audinaut.util.UserUtil; +import github.nvllsvm.audinaut.util.Util; + +/** + * Created by Scott on 10/14/13. + */ +public class SubsonicFragmentActivity extends SubsonicActivity implements DownloadService.OnSongChangedListener { + private static String TAG = SubsonicFragmentActivity.class.getSimpleName(); + private static boolean infoDialogDisplayed; + private static boolean sessionInitialized = false; + private static long ALLOWED_SKEW = 30000L; + + private SlidingUpPanelLayout slideUpPanel; + private SlidingUpPanelLayout.PanelSlideListener panelSlideListener; + private boolean isPanelClosing = false; + private NowPlayingFragment nowPlayingFragment; + private SubsonicFragment secondaryFragment; + private Toolbar mainToolbar; + private Toolbar nowPlayingToolbar; + + private View bottomBar; + private ImageView coverArtView; + private TextView trackView; + private TextView artistView; + private ImageButton startButton; + private long lastBackPressTime = 0; + private DownloadFile currentPlaying; + private PlayerState currentState; + private ImageButton previousButton; + private ImageButton nextButton; + private ImageButton rewindButton; + private ImageButton fastforwardButton; + + @Override + public void onCreate(Bundle savedInstanceState) { + if(savedInstanceState == null) { + String fragmentType = getIntent().getStringExtra(Constants.INTENT_EXTRA_FRAGMENT_TYPE); + boolean firstRun = false; + if (fragmentType == null) { + fragmentType = Util.openToTab(this); + if (fragmentType != null) { + firstRun = true; + } + } + + if ("".equals(fragmentType) || fragmentType == null || firstRun) { + // Initial startup stuff + if (!sessionInitialized) { + loadSession(); + } + } + } + + 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"); + lastSelectedPosition = R.id.drawer_downloading; + } + setContentView(R.layout.abstract_fragment_activity); + + if (findViewById(R.id.fragment_container) != null && savedInstanceState == null) { + String fragmentType = getIntent().getStringExtra(Constants.INTENT_EXTRA_FRAGMENT_TYPE); + if(fragmentType == null) { + fragmentType = Util.openToTab(this); + if(fragmentType != null) { + getIntent().putExtra(Constants.INTENT_EXTRA_FRAGMENT_TYPE, fragmentType); + lastSelectedPosition = getDrawerItemId(fragmentType); + } else { + lastSelectedPosition = R.id.drawer_library; + } + + MenuItem item = drawerList.getMenu().findItem(lastSelectedPosition); + if(item != null) { + item.setChecked(true); + } + } else { + lastSelectedPosition = getDrawerItemId(fragmentType); + } + + currentFragment = getNewFragment(fragmentType); + if(getIntent().hasExtra(Constants.INTENT_EXTRA_NAME_ID)) { + Bundle currentArguments = currentFragment.getArguments(); + if(currentArguments == null) { + currentArguments = new Bundle(); + } + currentArguments.putString(Constants.INTENT_EXTRA_NAME_ID, getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_ID)); + currentFragment.setArguments(currentArguments); + } + 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()); + } + } + + slideUpPanel = (SlidingUpPanelLayout) findViewById(R.id.slide_up_panel); + panelSlideListener = new SlidingUpPanelLayout.PanelSlideListener() { + @Override + public void onPanelSlide(View panel, float slideOffset) { + + } + + @Override + public void onPanelCollapsed(View panel) { + isPanelClosing = false; + if(bottomBar.getVisibility() == View.GONE) { + bottomBar.setVisibility(View.VISIBLE); + nowPlayingToolbar.setVisibility(View.GONE); + nowPlayingFragment.setPrimaryFragment(false); + setSupportActionBar(mainToolbar); + recreateSpinner(); + } + } + + @Override + public void onPanelExpanded(View panel) { + isPanelClosing = false; + currentFragment.stopActionMode(); + + // Disable custom view before switching + getSupportActionBar().setDisplayShowCustomEnabled(false); + getSupportActionBar().setDisplayShowTitleEnabled(true); + + bottomBar.setVisibility(View.GONE); + nowPlayingToolbar.setVisibility(View.VISIBLE); + setSupportActionBar(nowPlayingToolbar); + + if(secondaryFragment == null) { + nowPlayingFragment.setPrimaryFragment(true); + } else { + secondaryFragment.setPrimaryFragment(true); + } + + drawerToggle.setDrawerIndicatorEnabled(false); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + } + + @Override + public void onPanelAnchored(View panel) { + + } + + @Override + public void onPanelHidden(View panel) { + + } + }; + slideUpPanel.setPanelSlideListener(panelSlideListener); + + if(getIntent().hasExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD)) { + // Post this later so it actually runs + handler.postDelayed(new Runnable() { + @Override + public void run() { + openNowPlaying(); + } + }, 200); + + getIntent().removeExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD); + } + + bottomBar = findViewById(R.id.bottom_bar); + mainToolbar = (Toolbar) findViewById(R.id.main_toolbar); + nowPlayingToolbar = (Toolbar) findViewById(R.id.now_playing_toolbar); + coverArtView = (ImageView) bottomBar.findViewById(R.id.album_art); + trackView = (TextView) bottomBar.findViewById(R.id.track_name); + artistView = (TextView) bottomBar.findViewById(R.id.artist_name); + + setSupportActionBar(mainToolbar); + + if (findViewById(R.id.fragment_container) != null && savedInstanceState == null) { + nowPlayingFragment = new NowPlayingFragment(); + FragmentTransaction trans = getSupportFragmentManager().beginTransaction(); + trans.add(R.id.now_playing_fragment_container, nowPlayingFragment, nowPlayingFragment.getTag() + ""); + trans.commit(); + } + + rewindButton = (ImageButton) findViewById(R.id.download_rewind); + rewindButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + new SilentBackgroundTask(SubsonicFragmentActivity.this) { + @Override + protected Void doInBackground() throws Throwable { + if (getDownloadService() == null) { + return null; + } + + getDownloadService().rewind(); + return null; + } + }.execute(); + } + }); + + previousButton = (ImageButton) findViewById(R.id.download_previous); + previousButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + new SilentBackgroundTask(SubsonicFragmentActivity.this) { + @Override + protected Void doInBackground() throws Throwable { + if(getDownloadService() == null) { + return null; + } + + getDownloadService().previous(); + return null; + } + }.execute(); + } + }); + + startButton = (ImageButton) findViewById(R.id.download_start); + startButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + new SilentBackgroundTask(SubsonicFragmentActivity.this) { + @Override + protected Void doInBackground() throws Throwable { + PlayerState state = getDownloadService().getPlayerState(); + if(state == PlayerState.STARTED) { + getDownloadService().pause(); + } else { + getDownloadService().start(); + } + + return null; + } + }.execute(); + } + }); + + nextButton = (ImageButton) findViewById(R.id.download_next); + nextButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + new SilentBackgroundTask(SubsonicFragmentActivity.this) { + @Override + protected Void doInBackground() throws Throwable { + if(getDownloadService() == null) { + return null; + } + + getDownloadService().next(); + return null; + } + }.execute(); + } + }); + + fastforwardButton = (ImageButton) findViewById(R.id.download_fastforward); + fastforwardButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + new SilentBackgroundTask(SubsonicFragmentActivity.this) { + @Override + protected Void doInBackground() throws Throwable { + if (getDownloadService() == null) { + return null; + } + + getDownloadService().fastForward(); + return null; + } + }.execute(); + } + }); + } + + @Override + protected void onPostCreate(Bundle bundle) { + super.onPostCreate(bundle); + + showInfoDialog(); + checkUpdates(); + } + + @Override + public void onNewIntent(Intent intent) { + super.onNewIntent(intent); + + if(currentFragment != null && intent.getStringExtra(Constants.INTENT_EXTRA_NAME_QUERY) != null) { + if(slideUpPanel.getPanelState() == SlidingUpPanelLayout.PanelState.EXPANDED) { + closeNowPlaying(); + } + + if(currentFragment instanceof SearchFragment) { + String query = intent.getStringExtra(Constants.INTENT_EXTRA_NAME_QUERY); + boolean autoplay = intent.getBooleanExtra(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false); + + if (query != null) { + ((SearchFragment)currentFragment).search(query, autoplay); + } + getIntent().removeExtra(Constants.INTENT_EXTRA_NAME_QUERY); + } else { + setIntent(intent); + + SearchFragment fragment = new SearchFragment(); + replaceFragment(fragment, fragment.getSupportTag()); + } + } else if(intent.getBooleanExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD, false)) { + if(slideUpPanel.getPanelState() != SlidingUpPanelLayout.PanelState.EXPANDED) { + openNowPlaying(); + } + } else { + if(slideUpPanel.getPanelState() == SlidingUpPanelLayout.PanelState.EXPANDED) { + closeNowPlaying(); + } + setIntent(intent); + } + if(drawer != null) { + drawer.closeDrawers(); + } + } + + @Override + public void onResume() { + super.onResume(); + + 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); + } + + UserUtil.seedCurrentUser(this); + createAccount(); + runWhenServiceAvailable(new Runnable() { + @Override + public void run() { + getDownloadService().addOnSongChangedListener(SubsonicFragmentActivity.this, true); + } + }); + } + + @Override + public void onPause() { + super.onPause(); + DownloadService downloadService = getDownloadService(); + if(downloadService != null) { + downloadService.removeOnSongChangeListener(this); + } + } + + @Override + public void onSaveInstanceState(Bundle savedInstanceState) { + super.onSaveInstanceState(savedInstanceState); + savedInstanceState.putString(Constants.MAIN_NOW_PLAYING, nowPlayingFragment.getTag()); + if(secondaryFragment != null) { + savedInstanceState.putString(Constants.MAIN_NOW_PLAYING_SECONDARY, secondaryFragment.getTag()); + } + savedInstanceState.putInt(Constants.MAIN_SLIDE_PANEL_STATE, slideUpPanel.getPanelState().hashCode()); + } + @Override + public void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + + String id = savedInstanceState.getString(Constants.MAIN_NOW_PLAYING); + FragmentManager fm = getSupportFragmentManager(); + nowPlayingFragment = (NowPlayingFragment) fm.findFragmentByTag(id); + + String secondaryId = savedInstanceState.getString(Constants.MAIN_NOW_PLAYING_SECONDARY); + if(secondaryId != null) { + secondaryFragment = (SubsonicFragment) fm.findFragmentByTag(secondaryId); + + nowPlayingFragment.setPrimaryFragment(false); + secondaryFragment.setPrimaryFragment(true); + + FragmentTransaction trans = getSupportFragmentManager().beginTransaction(); + trans.hide(nowPlayingFragment); + trans.commit(); + } + + if(drawerToggle != null && backStack.size() > 0) { + drawerToggle.setDrawerIndicatorEnabled(false); + } + + if(savedInstanceState.getInt(Constants.MAIN_SLIDE_PANEL_STATE, -1) == SlidingUpPanelLayout.PanelState.EXPANDED.hashCode()) { + panelSlideListener.onPanelExpanded(null); + } + } + + @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(slideUpPanel.getPanelState() == SlidingUpPanelLayout.PanelState.EXPANDED && secondaryFragment == null) { + slideUpPanel.setPanelState(SlidingUpPanelLayout.PanelState.COLLAPSED); + } else if(onBackPressedSupport()) { + finish(); + } + } + + @Override + public boolean onBackPressedSupport() { + if(slideUpPanel.getPanelState() == SlidingUpPanelLayout.PanelState.EXPANDED) { + removeCurrent(); + return false; + } else { + return super.onBackPressedSupport(); + } + } + + @Override + public SubsonicFragment getCurrentFragment() { + if(slideUpPanel.getPanelState() == SlidingUpPanelLayout.PanelState.EXPANDED) { + if(secondaryFragment == null) { + return nowPlayingFragment; + } else { + return secondaryFragment; + } + } else { + return super.getCurrentFragment(); + } + } + + @Override + public void replaceFragment(SubsonicFragment fragment, int tag, boolean replaceCurrent) { + if(slideUpPanel != null && slideUpPanel.getPanelState() == SlidingUpPanelLayout.PanelState.EXPANDED && !isPanelClosing) { + secondaryFragment = fragment; + nowPlayingFragment.setPrimaryFragment(false); + secondaryFragment.setPrimaryFragment(true); + supportInvalidateOptionsMenu(); + + 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(nowPlayingFragment); + trans.add(R.id.now_playing_fragment_container, secondaryFragment, tag + ""); + trans.commit(); + } else { + super.replaceFragment(fragment, tag, replaceCurrent); + } + } + @Override + public void removeCurrent() { + if(slideUpPanel.getPanelState() == SlidingUpPanelLayout.PanelState.EXPANDED && secondaryFragment != 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(secondaryFragment); + trans.show(nowPlayingFragment); + trans.commit(); + + secondaryFragment = null; + nowPlayingFragment.setPrimaryFragment(true); + supportInvalidateOptionsMenu(); + } else { + super.removeCurrent(); + } + } + + @Override + public void setTitle(CharSequence title) { + if(slideUpPanel.getPanelState() == SlidingUpPanelLayout.PanelState.EXPANDED) { + getSupportActionBar().setTitle(title); + } else { + super.setTitle(title); + } + } + + @Override + protected void drawerItemSelected(String fragmentType) { + super.drawerItemSelected(fragmentType); + + if(slideUpPanel.getPanelState() == SlidingUpPanelLayout.PanelState.EXPANDED) { + slideUpPanel.setPanelState(SlidingUpPanelLayout.PanelState.COLLAPSED); + } + } + + @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); + } + } + + @Override + public void openNowPlaying() { + slideUpPanel.setPanelState(SlidingUpPanelLayout.PanelState.EXPANDED); + } + @Override + public void closeNowPlaying() { + slideUpPanel.setPanelState(SlidingUpPanelLayout.PanelState.COLLAPSED); + isPanelClosing = true; + } + + private SubsonicFragment getNewFragment(String fragmentType) { + if("Artist".equals(fragmentType)) { + return new SelectArtistFragment(); + } else if("Playlist".equals(fragmentType)) { + return new SelectPlaylistFragment(); + } else if("Download".equals(fragmentType)) { + return new DownloadFragment(); + } else { + return new SelectArtistFragment(); + } + } + + 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 we are on Subsonic 5.2+, save play queue + if(!Util.isOffline(this)) { + loadRemotePlayQueue(); + } + + sessionInitialized = true; + } + private void loadSettings() { + PreferenceManager.setDefaultValues(this, R.xml.settings_appearance, false); + PreferenceManager.setDefaultValues(this, R.xml.settings_cache, false); + PreferenceManager.setDefaultValues(this, R.xml.settings_playback, 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, "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 loadRemotePlayQueue() { + if(Util.getPreferences(this).getBoolean(Constants.PREFERENCES_KEY_RESUME_PLAY_QUEUE_NEVER, false)) { + return; + } + + final SubsonicActivity context = this; + new SilentBackgroundTask(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) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + String message = getResources().getString(R.string.common_confirm_message, Util.formatDate(remoteState.changed)); + builder.setIcon(android.R.drawable.ic_dialog_alert) + .setTitle(R.string.common_confirm) + .setMessage(message) + .setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + new SilentBackgroundTask(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(); + } + }) + .setNeutralButton(R.string.common_cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + new SilentBackgroundTask(SubsonicFragmentActivity.this) { + @Override + protected Void doInBackground() throws Throwable { + DownloadService downloadService = getDownloadService(); + downloadService.serializeQueue(false); + return null; + } + }.execute(); + } + }) + .setNegativeButton(R.string.common_never, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + new SilentBackgroundTask(SubsonicFragmentActivity.this) { + @Override + protected Void doInBackground() throws Throwable { + DownloadService downloadService = getDownloadService(); + downloadService.serializeQueue(false); + + SharedPreferences.Editor editor = Util.getPreferences(SubsonicFragmentActivity.this).edit(); + editor.putBoolean(Constants.PREFERENCES_KEY_RESUME_PLAY_QUEUE_NEVER, true); + editor.commit(); + return null; + } + }.execute(); + } + }); + + builder.create().show(); + } + + private void createAccount() { + final Context context = this; + + new SilentBackgroundTask(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 syncing + ContentResolver.setSyncAutomatically(account, Constants.SYNC_ACCOUNT_PLAYLIST_AUTHORITY, syncEnabled); + ContentResolver.addPeriodicSync(account, Constants.SYNC_ACCOUNT_PLAYLIST_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); + } + } + } + + public Toolbar getActiveToolbar() { + return slideUpPanel.getPanelState() == SlidingUpPanelLayout.PanelState.EXPANDED ? nowPlayingToolbar : mainToolbar; + } + + @Override + public void onSongChanged(DownloadFile currentPlaying, int currentPlayingIndex) { + this.currentPlaying = currentPlaying; + + MusicDirectory.Entry song = null; + if (currentPlaying != null) { + song = currentPlaying.getSong(); + trackView.setText(song.getTitle()); + + if(song.getArtist() != null) { + artistView.setVisibility(View.VISIBLE); + artistView.setText(song.getArtist()); + } else { + artistView.setVisibility(View.GONE); + } + } else { + trackView.setText(R.string.main_title); + artistView.setText(R.string.main_artist); + } + + if (coverArtView != null) { + int height = coverArtView.getHeight(); + if (height <= 0) { + int[] attrs = new int[]{R.attr.actionBarSize}; + TypedArray typedArray = this.obtainStyledAttributes(attrs); + height = typedArray.getDimensionPixelSize(0, 0); + typedArray.recycle(); + } + getImageLoader().loadImage(coverArtView, song, false, height, false); + } + + previousButton.setVisibility(View.VISIBLE); + nextButton.setVisibility(View.VISIBLE); + + rewindButton.setVisibility(View.GONE); + fastforwardButton.setVisibility(View.GONE); + } + + @Override + public void onSongsChanged(List songs, DownloadFile currentPlaying, int currentPlayingIndex) { + if(this.currentPlaying != currentPlaying || this.currentPlaying == null) { + onSongChanged(currentPlaying, currentPlayingIndex); + } + } + + @Override + public void onSongProgress(DownloadFile currentPlaying, int millisPlayed, Integer duration, boolean isSeekable) { + + } + + @Override + public void onStateUpdate(DownloadFile downloadFile, PlayerState playerState) { + int[] attrs = new int[]{(playerState == PlayerState.STARTED) ? R.attr.actionbar_pause : R.attr.actionbar_start}; + TypedArray typedArray = this.obtainStyledAttributes(attrs); + startButton.setImageResource(typedArray.getResourceId(0, 0)); + typedArray.recycle(); + } + + @Override + public void onMetadataUpdate(MusicDirectory.Entry song, int fieldChange) { + if(song != null && coverArtView != null && fieldChange == DownloadService.METADATA_UPDATED_COVER_ART) { + int height = coverArtView.getHeight(); + if (height <= 0) { + int[] attrs = new int[]{R.attr.actionBarSize}; + TypedArray typedArray = this.obtainStyledAttributes(attrs); + height = typedArray.getDimensionPixelSize(0, 0); + typedArray.recycle(); + } + getImageLoader().loadImage(coverArtView, song, false, height, false); + + // We need to update it immediately since it won't update if updater is not running for it + if(nowPlayingFragment != null && slideUpPanel.getPanelState() == SlidingUpPanelLayout.PanelState.COLLAPSED) { + nowPlayingFragment.onMetadataUpdate(song, fieldChange); + } + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/activity/VoiceQueryReceiverActivity.java b/app/src/main/java/github/nvllsvm/audinaut/activity/VoiceQueryReceiverActivity.java new file mode 100644 index 0000000..4f22542 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/activity/VoiceQueryReceiverActivity.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 . + + Copyright 2009 (C) Sindre Mehus + */ + +package github.nvllsvm.audinaut.activity; + +import android.app.Activity; +import android.app.SearchManager; +import android.content.Intent; +import android.os.Bundle; +import android.provider.MediaStore; +import android.provider.SearchRecentSuggestions; +import android.util.Log; + +import github.nvllsvm.audinaut.fragments.SubsonicFragment; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.Util; +import github.nvllsvm.audinaut.provider.AudinautSearchProvider; + +/** + * 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 final String TAG = VoiceQueryReceiverActivity.class.getSimpleName(); + + @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); + intent.putExtra(Constants.INTENT_EXTRA_NAME_AUTOPLAY, true); + intent.putExtra(MediaStore.EXTRA_MEDIA_FOCUS, getIntent().getStringExtra(MediaStore.EXTRA_MEDIA_FOCUS)); + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP); + Util.startActivityWithoutTransition(VoiceQueryReceiverActivity.this, intent); + } + finish(); + Util.disablePendingTransition(this); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/adapter/AlphabeticalAlbumAdapter.java b/app/src/main/java/github/nvllsvm/audinaut/adapter/AlphabeticalAlbumAdapter.java new file mode 100644 index 0000000..282562e --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/adapter/AlphabeticalAlbumAdapter.java @@ -0,0 +1,44 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.adapter; + +import android.content.Context; + +import java.util.List; + +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.util.ImageLoader; +import github.nvllsvm.audinaut.view.FastScroller; + +public class AlphabeticalAlbumAdapter extends EntryInfiniteGridAdapter implements FastScroller.BubbleTextGetter { + public AlphabeticalAlbumAdapter(Context context, List entries, ImageLoader imageLoader, boolean largeCell) { + super(context, entries, imageLoader, largeCell); + } + + @Override + public String getTextToShowInBubble(int position) { + // Make sure that we are not trying to get an item for the loading placeholder + if(position >= sections.get(0).size()) { + if(sections.get(0).size() > 0) { + return getTextToShowInBubble(position - 1); + } else { + return "*"; + } + } else { + return getNameIndex(getItemForPosition(position).getAlbum()); + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/adapter/ArtistAdapter.java b/app/src/main/java/github/nvllsvm/audinaut/adapter/ArtistAdapter.java new file mode 100644 index 0000000..ef934b3 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/adapter/ArtistAdapter.java @@ -0,0 +1,162 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.adapter; + +import android.content.Context; +import android.support.v7.widget.PopupMenu; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import java.io.Serializable; +import java.util.List; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.Artist; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.MusicDirectory.Entry; +import github.nvllsvm.audinaut.domain.MusicFolder; +import github.nvllsvm.audinaut.util.Util; +import github.nvllsvm.audinaut.view.ArtistView; +import github.nvllsvm.audinaut.view.FastScroller; +import github.nvllsvm.audinaut.view.SongView; +import github.nvllsvm.audinaut.view.UpdateView; + +public class ArtistAdapter extends SectionAdapter implements FastScroller.BubbleTextGetter { + public static int VIEW_TYPE_SONG = 3; + public static int VIEW_TYPE_ARTIST = 4; + + private List musicFolders; + private OnMusicFolderChanged onMusicFolderChanged; + + public ArtistAdapter(Context context, List artists, OnItemClickedListener listener) { + this(context, artists, null, listener, null); + } + + public ArtistAdapter(Context context, List artists, List musicFolders, OnItemClickedListener onItemClickedListener, OnMusicFolderChanged onMusicFolderChanged) { + super(context, artists); + this.musicFolders = musicFolders; + this.onItemClickedListener = onItemClickedListener; + this.onMusicFolderChanged = onMusicFolderChanged; + + if(musicFolders != null) { + this.singleSectionHeader = true; + } + } + + @Override + public UpdateView.UpdateViewHolder onCreateHeaderHolder(ViewGroup parent) { + final View header = LayoutInflater.from(context).inflate(R.layout.select_artist_header, parent, false); + header.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + PopupMenu popup = new PopupMenu(context, header.findViewById(R.id.select_artist_folder_2)); + + popup.getMenu().add(R.string.select_artist_all_folders); + for (MusicFolder musicFolder : musicFolders) { + popup.getMenu().add(musicFolder.getName()); + } + + popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + for (MusicFolder musicFolder : musicFolders) { + if(item.getTitle().equals(musicFolder.getName())) { + if(onMusicFolderChanged != null) { + onMusicFolderChanged.onMusicFolderChanged(musicFolder); + } + return true; + } + } + + if(onMusicFolderChanged != null) { + onMusicFolderChanged.onMusicFolderChanged(null); + } + return true; + } + }); + popup.show(); + } + }); + + return new UpdateView.UpdateViewHolder(header, false); + } + @Override + public void onBindHeaderHolder(UpdateView.UpdateViewHolder holder, String header, int sectionIndex) { + TextView folderName = (TextView) holder.getView().findViewById(R.id.select_artist_folder_2); + + String musicFolderId = Util.getSelectedMusicFolderId(context); + if(musicFolderId != null) { + for (MusicFolder musicFolder : musicFolders) { + if (musicFolder.getId().equals(musicFolderId)) { + folderName.setText(musicFolder.getName()); + break; + } + } + } else { + folderName.setText(R.string.select_artist_all_folders); + } + } + + @Override + public UpdateView.UpdateViewHolder onCreateSectionViewHolder(ViewGroup parent, int viewType) { + UpdateView updateView = null; + if(viewType == VIEW_TYPE_ARTIST) { + updateView = new ArtistView(context); + } else if(viewType == VIEW_TYPE_SONG) { + updateView = new SongView(context); + } + + return new UpdateView.UpdateViewHolder(updateView); + } + + @Override + public void onBindViewHolder(UpdateView.UpdateViewHolder holder, Serializable item, int viewType) { + UpdateView view = holder.getUpdateView(); + if(viewType == VIEW_TYPE_ARTIST) { + view.setObject(item); + } else if(viewType == VIEW_TYPE_SONG) { + SongView songView = (SongView) view; + Entry entry = (Entry) item; + songView.setObject(entry, checkable); + } + } + + @Override + public int getItemViewType(Serializable item) { + if(item instanceof Artist) { + return VIEW_TYPE_ARTIST; + } else { + return VIEW_TYPE_SONG; + } + } + + @Override + public String getTextToShowInBubble(int position) { + Object item = getItemForPosition(position); + if(item instanceof Artist) { + return getNameIndex(((Artist) item).getName(), true); + } else { + return null; + } + } + + public interface OnMusicFolderChanged { + void onMusicFolderChanged(MusicFolder musicFolder); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/adapter/BasicListAdapter.java b/app/src/main/java/github/nvllsvm/audinaut/adapter/BasicListAdapter.java new file mode 100644 index 0000000..c6f7c3b --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/adapter/BasicListAdapter.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 . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.adapter; + +import android.content.Context; +import android.view.ViewGroup; + +import java.util.List; + +import github.nvllsvm.audinaut.view.BasicListView; +import github.nvllsvm.audinaut.view.UpdateView; + +public class BasicListAdapter extends SectionAdapter { + public static int VIEW_TYPE_LINE = 1; + + public BasicListAdapter(Context context, List strings, OnItemClickedListener listener) { + super(context, strings); + this.onItemClickedListener = listener; + } + + @Override + public UpdateView.UpdateViewHolder onCreateSectionViewHolder(ViewGroup parent, int viewType) { + return new UpdateView.UpdateViewHolder(new BasicListView(context)); + } + + @Override + public void onBindViewHolder(UpdateView.UpdateViewHolder holder, String item, int viewType) { + holder.getUpdateView().setObject(item); + } + + @Override + public int getItemViewType(String item) { + return VIEW_TYPE_LINE; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/adapter/DetailsAdapter.java b/app/src/main/java/github/nvllsvm/audinaut/adapter/DetailsAdapter.java new file mode 100644 index 0000000..f3ef084 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/adapter/DetailsAdapter.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 . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.adapter; + +import android.content.Context; +import android.text.SpannableString; +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.TextView; + +import java.util.List; + +import github.nvllsvm.audinaut.R; + +public class DetailsAdapter extends ArrayAdapter { + private List headers; + private List details; + + public DetailsAdapter(Context context, int layout, List headers, List details) { + super(context, layout, headers); + + this.headers = headers; + this.details = details; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent){ + View view; + if(convertView == null) { + view = LayoutInflater.from(getContext()).inflate(R.layout.details_item, null); + } else { + view = convertView; + } + + TextView nameView = (TextView) view.findViewById(R.id.detail_name); + TextView detailsView = (TextView) view.findViewById(R.id.detail_value); + + nameView.setText(headers.get(position)); + + detailsView.setText(details.get(position)); + Linkify.addLinks(detailsView, Linkify.WEB_URLS | Linkify.EMAIL_ADDRESSES); + + return view; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/adapter/DownloadFileAdapter.java b/app/src/main/java/github/nvllsvm/audinaut/adapter/DownloadFileAdapter.java new file mode 100644 index 0000000..e9b7b49 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/adapter/DownloadFileAdapter.java @@ -0,0 +1,74 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2014 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.adapter; + +import android.content.Context; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; + +import java.util.List; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.service.DownloadFile; +import github.nvllsvm.audinaut.util.Util; +import github.nvllsvm.audinaut.view.FastScroller; +import github.nvllsvm.audinaut.view.SongView; +import github.nvllsvm.audinaut.view.UpdateView; + +public class DownloadFileAdapter extends SectionAdapter implements FastScroller.BubbleTextGetter { + public static int VIEW_TYPE_DOWNLOAD_FILE = 1; + + public DownloadFileAdapter(Context context, List entries, OnItemClickedListener onItemClickedListener) { + super(context, entries); + this.onItemClickedListener = onItemClickedListener; + this.checkable = true; + } + + @Override + public UpdateView.UpdateViewHolder onCreateSectionViewHolder(ViewGroup parent, int viewType) { + return new UpdateView.UpdateViewHolder(new SongView(context)); + } + + @Override + public void onBindViewHolder(UpdateView.UpdateViewHolder holder, DownloadFile item, int viewType) { + SongView songView = (SongView) holder.getUpdateView(); + songView.setObject(item.getSong(), Util.isBatchMode(context)); + songView.setDownloadFile(item); + } + + @Override + public int getItemViewType(DownloadFile item) { + return VIEW_TYPE_DOWNLOAD_FILE; + } + + @Override + public String getTextToShowInBubble(int position) { + return null; + } + + @Override + public void onCreateActionModeMenu(Menu menu, MenuInflater menuInflater) { + if(Util.isOffline(context)) { + menuInflater.inflate(R.menu.multiselect_nowplaying_offline, menu); + } else { + menuInflater.inflate(R.menu.multiselect_nowplaying, menu); + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/adapter/EntryGridAdapter.java b/app/src/main/java/github/nvllsvm/audinaut/adapter/EntryGridAdapter.java new file mode 100644 index 0000000..88d9a03 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/adapter/EntryGridAdapter.java @@ -0,0 +1,156 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.adapter; + +import android.content.Context; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; + +import java.util.List; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.MusicDirectory.Entry; +import github.nvllsvm.audinaut.util.ImageLoader; +import github.nvllsvm.audinaut.util.Util; +import github.nvllsvm.audinaut.view.AlbumView; +import github.nvllsvm.audinaut.view.SongView; +import github.nvllsvm.audinaut.view.UpdateView; +import github.nvllsvm.audinaut.view.UpdateView.UpdateViewHolder; + +public class EntryGridAdapter extends SectionAdapter { + private static String TAG = EntryGridAdapter.class.getSimpleName(); + + public static int VIEW_TYPE_ALBUM_CELL = 1; + public static int VIEW_TYPE_ALBUM_LINE = 2; + public static int VIEW_TYPE_SONG = 3; + + private ImageLoader imageLoader; + private boolean largeAlbums; + private boolean showArtist = false; + private boolean showAlbum = false; + private boolean removeFromPlaylist = false; + private View header; + + public EntryGridAdapter(Context context, List entries, ImageLoader imageLoader, boolean largeCell) { + super(context, entries); + this.imageLoader = imageLoader; + this.largeAlbums = largeCell; + + // Always show artist if they aren't all the same + String artist = null; + for(MusicDirectory.Entry entry: entries) { + if(artist == null) { + artist = entry.getArtist(); + } + + if(artist != null && !artist.equals(entry.getArtist())) { + showArtist = true; + } + } + checkable = true; + } + + @Override + public UpdateViewHolder onCreateSectionViewHolder(ViewGroup parent, int viewType) { + UpdateView updateView = null; + if(viewType == VIEW_TYPE_ALBUM_LINE || viewType == VIEW_TYPE_ALBUM_CELL) { + updateView = new AlbumView(context, viewType == VIEW_TYPE_ALBUM_CELL); + } else if(viewType == VIEW_TYPE_SONG) { + updateView = new SongView(context); + } + + return new UpdateViewHolder(updateView); + } + + @Override + public void onBindViewHolder(UpdateViewHolder holder, Entry entry, int viewType) { + UpdateView view = holder.getUpdateView(); + if(viewType == VIEW_TYPE_ALBUM_CELL || viewType == VIEW_TYPE_ALBUM_LINE) { + AlbumView albumView = (AlbumView) view; + albumView.setShowArtist(showArtist); + albumView.setObject(entry, imageLoader); + } else if(viewType == VIEW_TYPE_SONG) { + SongView songView = (SongView) view; + songView.setShowAlbum(showAlbum); + songView.setObject(entry, checkable); + } + } + + public UpdateViewHolder onCreateHeaderHolder(ViewGroup parent) { + return new UpdateViewHolder(header, false); + } + public void onBindHeaderHolder(UpdateViewHolder holder, String header, int sectionIndex) { + + } + + @Override + public int getItemViewType(Entry entry) { + if(entry.isDirectory()) { + if (largeAlbums) { + return VIEW_TYPE_ALBUM_CELL; + } else { + return VIEW_TYPE_ALBUM_LINE; + } + } else { + return VIEW_TYPE_SONG; + } + } + + public void setHeader(View header) { + this.header = header; + this.singleSectionHeader = true; + } + public View getHeader() { + return header; + } + + public void setShowArtist(boolean showArtist) { + this.showArtist = showArtist; + } + + public void setShowAlbum(boolean showAlbum) { + this.showAlbum = showAlbum; + } + + public void removeAt(int index) { + sections.get(0).remove(index); + if(header != null) { + index++; + } + notifyItemRemoved(index); + } + + public void setRemoveFromPlaylist(boolean removeFromPlaylist) { + this.removeFromPlaylist = removeFromPlaylist; + } + + @Override + public void onCreateActionModeMenu(Menu menu, MenuInflater menuInflater) { + if(Util.isOffline(context)) { + menuInflater.inflate(R.menu.multiselect_media_offline, menu); + } else { + menuInflater.inflate(R.menu.multiselect_media, menu); + } + + if(!removeFromPlaylist) { + menu.removeItem(R.id.menu_remove_playlist); + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/adapter/EntryInfiniteGridAdapter.java b/app/src/main/java/github/nvllsvm/audinaut/adapter/EntryInfiniteGridAdapter.java new file mode 100644 index 0000000..dfd1a4b --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/adapter/EntryInfiniteGridAdapter.java @@ -0,0 +1,152 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.adapter; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import java.util.List; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.MusicDirectory.Entry; +import github.nvllsvm.audinaut.fragments.MainFragment; +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.service.MusicServiceFactory; +import github.nvllsvm.audinaut.util.ImageLoader; +import github.nvllsvm.audinaut.util.SilentBackgroundTask; +import github.nvllsvm.audinaut.view.UpdateView; + +public class EntryInfiniteGridAdapter extends EntryGridAdapter { + public static int VIEW_TYPE_LOADING = 4; + + private String type; + private String extra; + private int size; + + private boolean loading = false; + private boolean allLoaded = false; + + public EntryInfiniteGridAdapter(Context context, List entries, ImageLoader imageLoader, boolean largeCell) { + super(context, entries, imageLoader, largeCell); + } + + @Override + public UpdateView.UpdateViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + if(viewType == VIEW_TYPE_LOADING) { + View progress = LayoutInflater.from(context).inflate(R.layout.tab_progress, null); + progress.setVisibility(View.VISIBLE); + return new UpdateView.UpdateViewHolder(progress, false); + } + + return super.onCreateViewHolder(parent, viewType); + } + + @Override + public int getItemViewType(int position) { + if(isLoadingView(position)) { + return VIEW_TYPE_LOADING; + } + + return super.getItemViewType(position); + } + + @Override + public void onBindViewHolder(UpdateView.UpdateViewHolder holder, int position) { + if(!isLoadingView(position)) { + super.onBindViewHolder(holder, position); + } + } + + @Override + public int getItemCount() { + int size = super.getItemCount(); + + if(!allLoaded) { + size++; + } + + return size; + } + + public void setData(String type, String extra, int size) { + this.type = type; + this.extra = extra; + this.size = size; + + if(super.getItemCount() < size) { + allLoaded = true; + } + } + + public void loadMore() { + if(loading || allLoaded) { + return; + } + loading = true; + + new SilentBackgroundTask(context) { + private List newData; + + @Override + protected Void doInBackground() throws Throwable { + newData = cacheInBackground(); + return null; + } + + @Override + protected void done(Void result) { + appendCachedData(newData); + loading = false; + + if(newData.size() < size) { + allLoaded = true; + notifyDataSetChanged(); + } + } + }.execute(); + } + + protected List cacheInBackground() throws Exception { + MusicService service = MusicServiceFactory.getMusicService(context); + MusicDirectory result; + int offset = sections.get(0).size(); + if("genres".equals(type) || "years".equals(type)) { + result = service.getAlbumList(type, extra, size, offset, false, context, null); + } else if("genres".equals(type) || "genres-songs".equals(type)) { + result = service.getSongsByGenre(extra, size, offset, context, null); + }else if(type.indexOf(MainFragment.SONGS_LIST_PREFIX) != -1) { + result = service.getSongList(type, size, offset, context, null); + } else { + result = service.getAlbumList(type, size, offset, false, context, null); + } + return result.getChildren(); + } + + protected void appendCachedData(List newData) { + if(newData.size() > 0) { + int start = sections.get(0).size(); + sections.get(0).addAll(newData); + this.notifyItemRangeInserted(start, newData.size()); + } + } + + protected boolean isLoadingView(int position) { + return !allLoaded && position >= sections.get(0).size(); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/adapter/ExpandableSectionAdapter.java b/app/src/main/java/github/nvllsvm/audinaut/adapter/ExpandableSectionAdapter.java new file mode 100644 index 0000000..6fdf3d4 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/adapter/ExpandableSectionAdapter.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 . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.adapter; + +import android.content.Context; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.util.DrawableTint; +import github.nvllsvm.audinaut.view.BasicHeaderView; +import github.nvllsvm.audinaut.view.UpdateView; + +public abstract class ExpandableSectionAdapter extends SectionAdapter { + private static final String TAG = ExpandableSectionAdapter.class.getSimpleName(); + private static final int DEFAULT_VISIBLE = 4; + private static final int EXPAND_TOGGLE = R.attr.select_server; + private static final int COLLAPSE_TOGGLE = R.attr.select_tabs; + + protected List sectionsDefaultVisible; + protected List> sectionsExtras; + protected int expandToggleRes; + protected int collapseToggleRes; + + protected ExpandableSectionAdapter() {} + public ExpandableSectionAdapter(Context context, List section) { + List> sections = new ArrayList<>(); + sections.add(section); + + init(context, Arrays.asList("Section"), sections, Arrays.asList((Integer) null)); + } + public ExpandableSectionAdapter(Context context, List headers, List> sections) { + init(context, headers, sections, null); + } + public ExpandableSectionAdapter(Context context, List headers, List> sections, List sectionsDefaultVisible) { + init(context, headers, sections, sectionsDefaultVisible); + } + protected void init(Context context, List headers, List> fullSections, List sectionsDefaultVisible) { + this.context = context; + this.headers = headers; + this.sectionsDefaultVisible = sectionsDefaultVisible; + if(sectionsDefaultVisible == null) { + sectionsDefaultVisible = new ArrayList<>(fullSections.size()); + for(int i = 0; i < fullSections.size(); i++) { + sectionsDefaultVisible.add(DEFAULT_VISIBLE); + } + } + + this.sections = new ArrayList<>(); + this.sectionsExtras = new ArrayList<>(); + int i = 0; + for(List fullSection: fullSections) { + List visibleSection = new ArrayList<>(); + + Integer defaultVisible = sectionsDefaultVisible.get(i); + if(defaultVisible == null || defaultVisible >= fullSection.size()) { + visibleSection.addAll(fullSection); + this.sectionsExtras.add(null); + } else { + visibleSection.addAll(fullSection.subList(0, defaultVisible)); + this.sectionsExtras.add(fullSection.subList(defaultVisible, fullSection.size())); + } + this.sections.add(visibleSection); + + i++; + } + + expandToggleRes = DrawableTint.getDrawableRes(context, EXPAND_TOGGLE); + collapseToggleRes = DrawableTint.getDrawableRes(context, COLLAPSE_TOGGLE); + } + + @Override + public UpdateView.UpdateViewHolder onCreateHeaderHolder(ViewGroup parent) { + return new UpdateView.UpdateViewHolder(new BasicHeaderView(context, R.layout.expandable_header)); + } + + @Override + public void onBindHeaderHolder(UpdateView.UpdateViewHolder holder, String header, final int sectionIndex) { + UpdateView view = holder.getUpdateView(); + ImageView toggleSelectionView = (ImageView) view.findViewById(R.id.item_select); + + List visibleSelection = sections.get(sectionIndex); + List sectionExtras = sectionsExtras.get(sectionIndex); + + if(sectionExtras != null && !sectionExtras.isEmpty()) { + toggleSelectionView.setVisibility(View.VISIBLE); + toggleSelectionView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + List visibleSelection = sections.get(sectionIndex); + List sectionExtras = sectionsExtras.get(sectionIndex); + + // Update icon + int selectToggleAttr; + if (!visibleSelection.contains(sectionExtras.get(0))) { + selectToggleAttr = COLLAPSE_TOGGLE; + + // Update how many are displayed + int lastIndex = getItemPosition(visibleSelection.get(visibleSelection.size() - 1)); + visibleSelection.addAll(sectionExtras); + notifyItemRangeInserted(lastIndex, sectionExtras.size()); + } else { + selectToggleAttr = EXPAND_TOGGLE; + + // Update how many are displayed + visibleSelection.removeAll(sectionExtras); + int lastIndex = getItemPosition(visibleSelection.get(visibleSelection.size() - 1)); + notifyItemRangeRemoved(lastIndex, sectionExtras.size()); + } + + ((ImageView) v).setImageResource(DrawableTint.getDrawableRes(context, selectToggleAttr)); + } + }); + + int selectToggleAttr; + if (!visibleSelection.contains(sectionExtras.get(0))) { + selectToggleAttr = EXPAND_TOGGLE; + } else { + selectToggleAttr = COLLAPSE_TOGGLE; + } + + toggleSelectionView.setImageResource(DrawableTint.getDrawableRes(context, selectToggleAttr)); + } else { + toggleSelectionView.setVisibility(View.GONE); + } + + if(view != null) { + view.setObject(header); + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/adapter/GenreAdapter.java b/app/src/main/java/github/nvllsvm/audinaut/adapter/GenreAdapter.java new file mode 100644 index 0000000..aa97469 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/adapter/GenreAdapter.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 . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.adapter; + +import android.content.Context; +import android.view.ViewGroup; +import github.nvllsvm.audinaut.domain.Genre; +import github.nvllsvm.audinaut.view.FastScroller; +import github.nvllsvm.audinaut.view.GenreView; +import github.nvllsvm.audinaut.view.UpdateView; + +import java.util.List; + +public class GenreAdapter extends SectionAdapter implements FastScroller.BubbleTextGetter{ + public static int VIEW_TYPE_GENRE = 1; + + public GenreAdapter(Context context, List genres, OnItemClickedListener listener) { + super(context, genres); + this.onItemClickedListener = listener; + } + + @Override + public UpdateView.UpdateViewHolder onCreateSectionViewHolder(ViewGroup parent, int viewType) { + return new UpdateView.UpdateViewHolder(new GenreView(context)); + } + + @Override + public void onBindViewHolder(UpdateView.UpdateViewHolder holder, Genre item, int viewType) { + holder.getUpdateView().setObject(item); + } + + @Override + public int getItemViewType(Genre item) { + return VIEW_TYPE_GENRE; + } + + @Override + public String getTextToShowInBubble(int position) { + return getNameIndex(getItemForPosition(position).getName()); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/adapter/MainAdapter.java b/app/src/main/java/github/nvllsvm/audinaut/adapter/MainAdapter.java new file mode 100644 index 0000000..5a8bca6 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/adapter/MainAdapter.java @@ -0,0 +1,96 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.adapter; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.CompoundButton; + +import java.util.List; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.util.Util; +import github.nvllsvm.audinaut.view.AlbumListCountView; +import github.nvllsvm.audinaut.view.BasicHeaderView; +import github.nvllsvm.audinaut.view.BasicListView; +import github.nvllsvm.audinaut.view.UpdateView; + +public class MainAdapter extends SectionAdapter { + public static final int VIEW_TYPE_ALBUM_LIST = 1; + public static final int VIEW_TYPE_ALBUM_COUNT_LIST = 2; + + public MainAdapter(Context context, List headers, List> sections, OnItemClickedListener onItemClickedListener) { + super(context, headers, sections); + this.onItemClickedListener = onItemClickedListener; + } + + @Override + public UpdateView.UpdateViewHolder onCreateSectionViewHolder(ViewGroup parent, int viewType) { + UpdateView updateView; + if(viewType == VIEW_TYPE_ALBUM_LIST) { + updateView = new BasicListView(context); + } else { + updateView = new AlbumListCountView(context); + } + + return new UpdateView.UpdateViewHolder(updateView); + } + + @Override + public void onBindViewHolder(UpdateView.UpdateViewHolder holder, Integer item, int viewType) { + UpdateView updateView = holder.getUpdateView(); + + if(viewType == VIEW_TYPE_ALBUM_LIST) { + updateView.setObject(context.getResources().getString(item)); + } else { + updateView.setObject(item); + } + } + + @Override + public int getItemViewType(Integer item) { + if(item == R.string.main_albums_newest) { + return VIEW_TYPE_ALBUM_COUNT_LIST; + } else { + return VIEW_TYPE_ALBUM_LIST; + } + } + + @Override + public UpdateView.UpdateViewHolder onCreateHeaderHolder(ViewGroup parent) { + return new UpdateView.UpdateViewHolder(new BasicHeaderView(context, R.layout.album_list_header)); + } + @Override + public void onBindHeaderHolder(UpdateView.UpdateViewHolder holder, String header, int sectionIndex) { + UpdateView view = holder.getUpdateView(); + CheckBox checkBox = (CheckBox) view.findViewById(R.id.item_checkbox); + + String display; + if("songs".equals(header)) { + display = context.getResources().getString(R.string.search_songs); + checkBox.setVisibility(View.GONE); + } else { + display = header; + checkBox.setVisibility(View.GONE); + } + + if(view != null) { + view.setObject(display); + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/adapter/PlaylistAdapter.java b/app/src/main/java/github/nvllsvm/audinaut/adapter/PlaylistAdapter.java new file mode 100644 index 0000000..9ac61dc --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/adapter/PlaylistAdapter.java @@ -0,0 +1,72 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ +package github.nvllsvm.audinaut.adapter; + +import android.content.Context; + +import java.util.List; + +import android.view.ViewGroup; +import github.nvllsvm.audinaut.domain.Playlist; +import github.nvllsvm.audinaut.util.ImageLoader; +import github.nvllsvm.audinaut.view.FastScroller; +import github.nvllsvm.audinaut.view.PlaylistView; +import github.nvllsvm.audinaut.view.UpdateView; + +public class PlaylistAdapter extends SectionAdapter implements FastScroller.BubbleTextGetter { + public static int VIEW_TYPE_PLAYLIST = 1; + + private ImageLoader imageLoader; + private boolean largeCell; + + public PlaylistAdapter(Context context, List playlists, ImageLoader imageLoader, boolean largeCell, OnItemClickedListener listener) { + super(context, playlists); + this.imageLoader = imageLoader; + this.largeCell = largeCell; + this.onItemClickedListener = listener; + } + public PlaylistAdapter(Context context, List headers, List> sections, ImageLoader imageLoader, boolean largeCell, OnItemClickedListener listener) { + super(context, headers, sections); + this.imageLoader = imageLoader; + this.largeCell = largeCell; + this.onItemClickedListener = listener; + } + + @Override + public UpdateView.UpdateViewHolder onCreateSectionViewHolder(ViewGroup parent, int viewType) { + return new UpdateView.UpdateViewHolder(new PlaylistView(context, imageLoader, largeCell)); + } + + @Override + public void onBindViewHolder(UpdateView.UpdateViewHolder holder, Playlist playlist, int viewType) { + holder.getUpdateView().setObject(playlist); + holder.setItem(playlist); + } + + @Override + public int getItemViewType(Playlist playlist) { + return VIEW_TYPE_PLAYLIST; + } + + @Override + public String getTextToShowInBubble(int position) { + Object item = getItemForPosition(position); + if(item instanceof Playlist) { + return getNameIndex(((Playlist) item).getName()); + } else { + return null; + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/adapter/SearchAdapter.java b/app/src/main/java/github/nvllsvm/audinaut/adapter/SearchAdapter.java new file mode 100644 index 0000000..1e7376c --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/adapter/SearchAdapter.java @@ -0,0 +1,140 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.adapter; + +import android.content.Context; +import android.content.res.Resources; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.MusicDirectory.Entry; +import github.nvllsvm.audinaut.domain.SearchResult; +import github.nvllsvm.audinaut.util.DrawableTint; +import github.nvllsvm.audinaut.util.ImageLoader; +import github.nvllsvm.audinaut.util.Util; +import github.nvllsvm.audinaut.view.AlbumView; +import github.nvllsvm.audinaut.view.ArtistView; +import github.nvllsvm.audinaut.view.BasicHeaderView; +import github.nvllsvm.audinaut.view.SongView; +import github.nvllsvm.audinaut.view.UpdateView; + +import static github.nvllsvm.audinaut.adapter.ArtistAdapter.VIEW_TYPE_ARTIST; +import static github.nvllsvm.audinaut.adapter.EntryGridAdapter.VIEW_TYPE_ALBUM_CELL; +import static github.nvllsvm.audinaut.adapter.EntryGridAdapter.VIEW_TYPE_ALBUM_LINE; +import static github.nvllsvm.audinaut.adapter.EntryGridAdapter.VIEW_TYPE_SONG; + +public class SearchAdapter extends ExpandableSectionAdapter { + private ImageLoader imageLoader; + private boolean largeAlbums; + + private static final int MAX_ARTISTS = 10; + private static final int MAX_ALBUMS = 4; + private static final int MAX_SONGS = 10; + + public SearchAdapter(Context context, SearchResult searchResult, ImageLoader imageLoader, boolean largeAlbums, OnItemClickedListener listener) { + this.imageLoader = imageLoader; + this.largeAlbums = largeAlbums; + + List> sections = new ArrayList<>(); + List headers = new ArrayList<>(); + List defaultVisible = new ArrayList<>(); + Resources res = context.getResources(); + if(!searchResult.getArtists().isEmpty()) { + sections.add((List) (List) searchResult.getArtists()); + headers.add(res.getString(R.string.search_artists)); + defaultVisible.add(MAX_ARTISTS); + } + if(!searchResult.getAlbums().isEmpty()) { + sections.add((List) (List) searchResult.getAlbums()); + headers.add(res.getString(R.string.search_albums)); + defaultVisible.add(MAX_ALBUMS); + } + if(!searchResult.getSongs().isEmpty()) { + sections.add((List) (List) searchResult.getSongs()); + headers.add(res.getString(R.string.search_songs)); + defaultVisible.add(MAX_SONGS); + } + init(context, headers, sections, defaultVisible); + + this.onItemClickedListener = listener; + checkable = true; + } + + @Override + public UpdateView.UpdateViewHolder onCreateSectionViewHolder(ViewGroup parent, int viewType) { + UpdateView updateView = null; + if(viewType == VIEW_TYPE_ALBUM_CELL || viewType == VIEW_TYPE_ALBUM_LINE) { + updateView = new AlbumView(context, viewType == VIEW_TYPE_ALBUM_CELL); + } else if(viewType == VIEW_TYPE_SONG) { + updateView = new SongView(context); + } else if(viewType == VIEW_TYPE_ARTIST) { + updateView = new ArtistView(context); + } + + return new UpdateView.UpdateViewHolder(updateView); + } + + @Override + public void onBindViewHolder(UpdateView.UpdateViewHolder holder, Serializable item, int viewType) { + UpdateView view = holder.getUpdateView(); + if(viewType == VIEW_TYPE_ALBUM_CELL || viewType == VIEW_TYPE_ALBUM_LINE) { + AlbumView albumView = (AlbumView) view; + albumView.setObject((Entry) item, imageLoader); + } else if(viewType == VIEW_TYPE_SONG) { + SongView songView = (SongView) view; + songView.setObject((Entry) item, true); + } else if(viewType == VIEW_TYPE_ARTIST) { + view.setObject(item); + } + } + + @Override + public int getItemViewType(Serializable item) { + if(item instanceof Entry) { + Entry entry = (Entry) item; + if (entry.isDirectory()) { + if (largeAlbums) { + return VIEW_TYPE_ALBUM_CELL; + } else { + return VIEW_TYPE_ALBUM_LINE; + } + } else { + return VIEW_TYPE_SONG; + } + } else { + return VIEW_TYPE_ARTIST; + } + } + + @Override + public void onCreateActionModeMenu(Menu menu, MenuInflater menuInflater) { + if(Util.isOffline(context)) { + menuInflater.inflate(R.menu.multiselect_media_offline, menu); + } else { + menuInflater.inflate(R.menu.multiselect_media, menu); + } + + menu.removeItem(R.id.menu_remove_playlist); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/adapter/SectionAdapter.java b/app/src/main/java/github/nvllsvm/audinaut/adapter/SectionAdapter.java new file mode 100644 index 0000000..238a9aa --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/adapter/SectionAdapter.java @@ -0,0 +1,516 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.adapter; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.os.Build; +import android.support.v7.view.ActionMode; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.util.TypedValue; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.widget.PopupMenu; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.activity.SubsonicFragmentActivity; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.MenuUtil; +import github.nvllsvm.audinaut.util.Util; +import github.nvllsvm.audinaut.view.BasicHeaderView; +import github.nvllsvm.audinaut.view.UpdateView; +import github.nvllsvm.audinaut.view.UpdateView.UpdateViewHolder; + +public abstract class SectionAdapter extends RecyclerView.Adapter> { + private static String TAG = SectionAdapter.class.getSimpleName(); + public static int VIEW_TYPE_HEADER = 0; + public static String[] ignoredArticles; + + protected Context context; + protected List headers; + protected List> sections; + protected boolean singleSectionHeader; + protected OnItemClickedListener onItemClickedListener; + protected List selected = new ArrayList<>(); + protected List selectedViews = new ArrayList<>(); + protected ActionMode currentActionMode; + protected boolean checkable = false; + + protected SectionAdapter() {} + public SectionAdapter(Context context, List section) { + this(context, section, false); + } + public SectionAdapter(Context context, List section, boolean singleSectionHeader) { + this.context = context; + this.headers = Arrays.asList("Section"); + this.sections = new ArrayList<>(); + this.sections.add(section); + this.singleSectionHeader = singleSectionHeader; + } + public SectionAdapter(Context context, List headers, List> sections) { + this(context, headers, sections, true); + } + public SectionAdapter(Context context, List headers, List> sections, boolean singleSectionHeader){ + this.context = context; + this.headers = headers; + this.sections = sections; + this.singleSectionHeader = singleSectionHeader; + } + + public void replaceExistingData(List section) { + this.sections = new ArrayList<>(); + this.sections.add(section); + notifyDataSetChanged(); + } + public void replaceExistingData(List headers, List> sections) { + this.headers = headers; + this.sections = sections; + notifyDataSetChanged(); + } + + @Override + public UpdateViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + if(viewType == VIEW_TYPE_HEADER) { + return onCreateHeaderHolder(parent); + } else { + final UpdateViewHolder holder = onCreateSectionViewHolder(parent, viewType); + final UpdateView updateView = holder.getUpdateView(); + + if(updateView != null) { + updateView.getChildAt(0).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + T item = holder.getItem(); + updateView.onClick(); + if (currentActionMode != null) { + if(updateView.isCheckable()) { + if (selected.contains(item)) { + selected.remove(item); + selectedViews.remove(updateView); + setChecked(updateView, false); + } else { + selected.add(item); + selectedViews.add(updateView); + setChecked(updateView, true); + } + + if (selected.isEmpty()) { + currentActionMode.finish(); + } else { + currentActionMode.setTitle(context.getResources().getString(R.string.select_album_n_selected, selected.size())); + } + } + } else if (onItemClickedListener != null) { + onItemClickedListener.onItemClicked(updateView, item); + } + } + }); + + View moreButton = updateView.findViewById(R.id.item_more); + if (moreButton != null) { + moreButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + try { + final T item = holder.getItem(); + if (onItemClickedListener != null) { + PopupMenu popup = new PopupMenu(context, v); + onItemClickedListener.onCreateContextMenu(popup.getMenu(), popup.getMenuInflater(), updateView, item); + + popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem menuItem) { + return onItemClickedListener.onContextItemSelected(menuItem, updateView, item); + } + }); + popup.show(); + } + } catch(Exception e) { + Log.w(TAG, "Failed to show popup", e); + } + } + }); + + if(checkable) { + updateView.getChildAt(0).setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + if(updateView.isCheckable()) { + if (currentActionMode == null) { + startActionMode(holder); + } else { + updateView.getChildAt(0).performClick(); + } + } + return true; + } + }); + } + } + } + + return holder; + } + } + + @Override + public void onBindViewHolder(UpdateViewHolder holder, int position) { + UpdateView updateView = holder.getUpdateView(); + + if(sections.size() == 1 && !singleSectionHeader) { + T item = sections.get(0).get(position); + onBindViewHolder(holder, item, getItemViewType(position)); + postBindView(updateView, item); + holder.setItem(item); + return; + } + + int subPosition = 0; + int subHeader = 0; + for(List section: sections) { + boolean validHeader = headers.get(subHeader) != null; + if(position == subPosition && validHeader) { + onBindHeaderHolder(holder, headers.get(subHeader), subHeader); + return; + } + + int headerOffset = validHeader ? 1 : 0; + if(position < (subPosition + section.size() + headerOffset)) { + T item = section.get(position - subPosition - headerOffset); + onBindViewHolder(holder, item, getItemViewType(item)); + + postBindView(updateView, item); + holder.setItem(item); + return; + } + + subPosition += section.size(); + if(validHeader) { + subPosition += 1; + } + subHeader++; + } + } + + private void postBindView(UpdateView updateView, T item) { + if(updateView.isCheckable()) { + setChecked(updateView, selected.contains(item)); + } + + View moreButton = updateView.findViewById(R.id.item_more); + if(moreButton != null) { + if(onItemClickedListener != null) { + PopupMenu popup = new PopupMenu(context, moreButton); + Menu menu = popup.getMenu(); + onItemClickedListener.onCreateContextMenu(popup.getMenu(), popup.getMenuInflater(), updateView, item); + if (menu.size() == 0) { + moreButton.setVisibility(View.GONE); + } else { + moreButton.setVisibility(View.VISIBLE); + } + } else { + moreButton.setVisibility(View.VISIBLE); + } + } + } + + @Override + public int getItemCount() { + if(sections.size() == 1 && !singleSectionHeader) { + return sections.get(0).size(); + } + + int count = 0; + for(String header: headers) { + if(header != null) { + count++; + } + } + for(List section: sections) { + count += section.size(); + } + + return count; + } + + @Override + public int getItemViewType(int position) { + if(sections.size() == 1 && !singleSectionHeader) { + return getItemViewType(sections.get(0).get(position)); + } + + int subPosition = 0; + int subHeader = 0; + for(List section: sections) { + boolean validHeader = headers.get(subHeader) != null; + if(position == subPosition && validHeader) { + return VIEW_TYPE_HEADER; + } + + int headerOffset = validHeader ? 1 : 0; + if(position < (subPosition + section.size() + headerOffset)) { + return getItemViewType(section.get(position - subPosition - headerOffset)); + } + + subPosition += section.size(); + if(validHeader) { + subPosition += 1; + } + subHeader++; + } + + return -1; + } + + public UpdateViewHolder onCreateHeaderHolder(ViewGroup parent) { + return new UpdateViewHolder(new BasicHeaderView(context)); + } + public void onBindHeaderHolder(UpdateViewHolder holder, String header, int sectionIndex) { + UpdateView view = holder.getUpdateView(); + if(view != null) { + view.setObject(header); + } + } + + public T getItemForPosition(int position) { + if(sections.size() == 1 && !singleSectionHeader) { + return sections.get(0).get(position); + } + + int subPosition = 0; + for(List section: sections) { + if(position == subPosition) { + return null; + } + + if(position <= (subPosition + section.size())) { + return section.get(position - subPosition - 1); + } + + subPosition += section.size() + 1; + } + + return null; + } + public int getItemPosition(T item) { + if(sections.size() == 1 && !singleSectionHeader) { + return sections.get(0).indexOf(item); + } + + int subPosition = 0; + for(List section: sections) { + subPosition += section.size() + 1; + + int position = section.indexOf(item); + if(position != -1) { + return position + subPosition; + } + } + + return -1; + } + + public void setOnItemClickedListener(OnItemClickedListener onItemClickedListener) { + this.onItemClickedListener = onItemClickedListener; + } + + public void addSelected(T item) { + selected.add(item); + } + public List getSelected() { + List selected = new ArrayList<>(); + selected.addAll(this.selected); + return selected; + } + + public void clearSelected() { + // TODO: This needs to work with multiple sections + for(T item: selected) { + int index = sections.get(0).indexOf(item); + + if(singleSectionHeader) { + index++; + } + } + selected.clear(); + + for(UpdateView updateView: selectedViews) { + updateView.setChecked(false); + } + } + + public void moveItem(int from, int to) { + List section = sections.get(0); + int max = section.size(); + if(to >= max) { + to = max - 1; + } else if(to < 0) { + to = 0; + } + + T moved = section.remove(from); + section.add(to, moved); + + notifyItemMoved(from, to); + } + public void removeItem(T item) { + int subPosition = 0; + for(List section: sections) { + if(sections.size() > 1 || singleSectionHeader) { + subPosition++; + } + + int index = section.indexOf(item); + if (index != -1) { + section.remove(item); + notifyItemRemoved(subPosition + index); + break; + } + + subPosition += section.size(); + } + } + + public abstract UpdateView.UpdateViewHolder onCreateSectionViewHolder(ViewGroup parent, int viewType); + public abstract void onBindViewHolder(UpdateViewHolder holder, T item, int viewType); + public abstract int getItemViewType(T item); + public void setCheckable(boolean checkable) { + this.checkable = checkable; + } + public void setChecked(UpdateView updateView, boolean checked) { + updateView.setChecked(checked); + } + public void onCreateActionModeMenu(Menu menu, MenuInflater menuInflater) {} + + private void startActionMode(final UpdateView.UpdateViewHolder holder) { + final UpdateView updateView = holder.getUpdateView(); + if (context instanceof SubsonicFragmentActivity && currentActionMode == null) { + final SubsonicFragmentActivity fragmentActivity = (SubsonicFragmentActivity) context; + fragmentActivity.startSupportActionMode(new ActionMode.Callback() { + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + currentActionMode = mode; + + T item = holder.getItem(); + selected.add(item); + selectedViews.add(updateView); + setChecked(updateView, true); + + onCreateActionModeMenu(menu, mode.getMenuInflater()); + MenuUtil.hideMenuItems(context, menu, updateView); + + mode.setTitle(context.getResources().getString(R.string.select_album_n_selected, selected.size())); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_COLOR_ACTION_BAR, true)) { + TypedValue typedValue = new TypedValue(); + Resources.Theme theme = context.getTheme(); + theme.resolveAttribute(R.attr.colorPrimaryDark, typedValue, true); + int colorPrimaryDark = typedValue.data; + + Window window = ((SubsonicFragmentActivity) context).getWindow(); + window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + window.setStatusBarColor(colorPrimaryDark); + } + return true; + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return false; + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + if (fragmentActivity.onOptionsItemSelected(item)) { + currentActionMode.finish(); + return true; + } else { + return false; + } + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + currentActionMode = null; + selected.clear(); + for (UpdateView updateView : selectedViews) { + updateView.setChecked(false); + } + selectedViews.clear(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_COLOR_ACTION_BAR, true)) { + Window window = ((SubsonicFragmentActivity) context).getWindow(); + window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + } + } + }); + } + } + public void stopActionMode() { + if(currentActionMode != null) { + currentActionMode.finish(); + } + } + + public String getNameIndex(String name) { + return getNameIndex(name, false); + } + public String getNameIndex(String name, boolean removeIgnoredArticles) { + if(name == null) { + return "*"; + } + + if(removeIgnoredArticles) { + if (ignoredArticles == null) { + SharedPreferences prefs = Util.getPreferences(context); + String ignoredArticlesString = prefs.getString(Constants.CACHE_KEY_IGNORE, "The El La Los Las Le Les"); + ignoredArticles = ignoredArticlesString.split(" "); + } + + name = name.toLowerCase(); + for (String article : ignoredArticles) { + int index = name.indexOf(article.toLowerCase() + " "); + if (index == 0) { + name = name.substring(article.length() + 1); + } + } + } + + String index = name.substring(0, 1).toUpperCase(); + if (!Character.isLetter(index.charAt(0))) { + index = "#"; + } + + return index; + } + + public interface OnItemClickedListener { + void onItemClicked(UpdateView updateView, T item); + void onCreateContextMenu(Menu menu, MenuInflater menuInflater, UpdateView updateView, T item); + boolean onContextItemSelected(MenuItem menuItem, UpdateView updateView, T item); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/adapter/SettingsAdapter.java b/app/src/main/java/github/nvllsvm/audinaut/adapter/SettingsAdapter.java new file mode 100644 index 0000000..308b662 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/adapter/SettingsAdapter.java @@ -0,0 +1,121 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2014 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.adapter; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.List; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.User; +import github.nvllsvm.audinaut.util.ImageLoader; +import github.nvllsvm.audinaut.util.UserUtil; +import github.nvllsvm.audinaut.view.BasicHeaderView; +import github.nvllsvm.audinaut.view.RecyclingImageView; +import github.nvllsvm.audinaut.view.SettingView; +import github.nvllsvm.audinaut.view.UpdateView; + +import static github.nvllsvm.audinaut.domain.User.Setting; + +public class SettingsAdapter extends SectionAdapter { + private static final String TAG = SettingsAdapter.class.getSimpleName(); + public final int VIEW_TYPE_SETTING = 1; + public final int VIEW_TYPE_SETTING_HEADER = 2; + + private final User user; + private final boolean editable; + private final ImageLoader imageLoader; + + public SettingsAdapter(Context context, User user, List headers, List> settingSections, ImageLoader imageLoader, boolean editable, OnItemClickedListener onItemClickedListener) { + super(context, headers, settingSections, imageLoader != null); + this.user = user; + this.imageLoader = imageLoader; + this.editable = editable; + this.onItemClickedListener = onItemClickedListener; + + for(List settings: sections) { + for (Setting setting : settings) { + if (setting.getValue()) { + addSelected(setting); + } + } + } + } + + @Override + public int getItemViewType(int position) { + int viewType = super.getItemViewType(position); + if(viewType == SectionAdapter.VIEW_TYPE_HEADER) { + if(position == 0 && imageLoader != null) { + return VIEW_TYPE_HEADER; + } else { + return VIEW_TYPE_SETTING_HEADER; + } + } else { + return viewType; + } + } + + public void onBindHeaderHolder(UpdateView.UpdateViewHolder holder, String description, int sectionIndex) { + View header = holder.getView(); + } + + @Override + public UpdateView.UpdateViewHolder onCreateSectionViewHolder(ViewGroup parent, int viewType) { + if(viewType == VIEW_TYPE_SETTING_HEADER) { + return new UpdateView.UpdateViewHolder(new BasicHeaderView(context)); + } else { + return new UpdateView.UpdateViewHolder(new SettingView(context)); + } + } + + @Override + public void onBindViewHolder(UpdateView.UpdateViewHolder holder, Setting item, int viewType) { + holder.getUpdateView().setObject(item, editable); + } + + @Override + public int getItemViewType(Setting item) { + return VIEW_TYPE_SETTING; + } + + @Override + public void setChecked(UpdateView updateView, boolean checked) { + if(updateView instanceof SettingView) { + updateView.setChecked(checked); + } + } + + public static SettingsAdapter getSettingsAdapter(Context context, User user, ImageLoader imageLoader, OnItemClickedListener onItemClickedListener) { + return getSettingsAdapter(context, user, imageLoader, true, onItemClickedListener); + } + public static SettingsAdapter getSettingsAdapter(Context context, User user, ImageLoader imageLoader, boolean isEditable, OnItemClickedListener onItemClickedListener) { + List headers = new ArrayList<>(); + List> settingsSections = new ArrayList<>(); + settingsSections.add(user.getSettings()); + + if(user.getMusicFolderSettings() != null) { + settingsSections.add(user.getMusicFolderSettings()); + } + + return new SettingsAdapter(context, user, headers, settingsSections, imageLoader, isEditable, onItemClickedListener); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/audiofx/AudioEffectsController.java b/app/src/main/java/github/nvllsvm/audinaut/audiofx/AudioEffectsController.java new file mode 100644 index 0000000..a852f93 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/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 . + + Copyright 2014 (C) Scott Jackson +*/ +package github.nvllsvm.audinaut.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/nvllsvm/audinaut/audiofx/EqualizerController.java b/app/src/main/java/github/nvllsvm/audinaut/audiofx/EqualizerController.java new file mode 100644 index 0000000..59915a4 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/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 . + + Copyright 2011 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.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.nvllsvm.audinaut.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/nvllsvm/audinaut/audiofx/LoudnessEnhancerController.java b/app/src/main/java/github/nvllsvm/audinaut/audiofx/LoudnessEnhancerController.java new file mode 100644 index 0000000..75bfbd4 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/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 . + + Copyright 2014 (C) Scott Jackson +*/ +package github.nvllsvm.audinaut.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/nvllsvm/audinaut/domain/Artist.java b/app/src/main/java/github/nvllsvm/audinaut/domain/Artist.java new file mode 100644 index 0000000..a183d6b --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/domain/Artist.java @@ -0,0 +1,138 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.domain; + +import android.util.Log; + +import java.io.Serializable; +import java.text.Collator; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; + +/** + * @author Sindre Mehus + */ +public class Artist implements Serializable { + private static final String TAG = Artist.class.getSimpleName(); + public static final String ROOT_ID = "-1"; + public static final String MISSING_ID = "-2"; + + private String id; + private String name; + private String index; + private int closeness; + + public Artist() { + + } + public Artist(String id, String name) { + this.id = id; + this.name = name; + } + + 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 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 { + private String[] ignoredArticles; + private Collator collator; + + public ArtistComparator(String[] ignoredArticles) { + this.ignoredArticles = ignoredArticles; + this.collator = Collator.getInstance(Locale.US); + this.collator.setStrength(Collator.PRIMARY); + } + + public int compare(Artist lhsArtist, Artist rhsArtist) { + String lhs = lhsArtist.getName().toLowerCase(); + String rhs = rhsArtist.getName().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 collator.compare(lhs, rhs); + } + } + + public static void sort(List artists, String[] ignoredArticles) { + try { + Collections.sort(artists, new ArtistComparator(ignoredArticles)); + } catch (Exception e) { + Log.w(TAG, "Failed to sort artists", e); + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/domain/Genre.java b/app/src/main/java/github/nvllsvm/audinaut/domain/Genre.java new file mode 100644 index 0000000..77255f0 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/domain/Genre.java @@ -0,0 +1,69 @@ +package github.nvllsvm.audinaut.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.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.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 { + @Override + public int compare(Genre genre1, Genre genre2) { + return genre1.getName().compareToIgnoreCase(genre2.getName()); + } + + public static List sort(List genres) { + Collections.sort(genres, new GenreComparator()); + return genres; + } + + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/domain/Indexes.java b/app/src/main/java/github/nvllsvm/audinaut/domain/Indexes.java new file mode 100644 index 0000000..0de26c2 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/domain/Indexes.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.domain; + +import android.content.Context; +import android.content.SharedPreferences; + +import java.util.ArrayList; +import java.util.List; +import java.io.Serializable; + +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.Util; + +/** + * @author Sindre Mehus + */ +public class Indexes implements Serializable { + + private long lastModified; + private List shortcuts; + private List artists; + private List entries; + + public Indexes() { + + } + public Indexes(long lastModified, List shortcuts, List artists) { + this.lastModified = lastModified; + this.shortcuts = shortcuts; + this.artists = artists; + this.entries = new ArrayList(); + } + public Indexes(long lastModified, List shortcuts, List artists, List entries) { + this.lastModified = lastModified; + this.shortcuts = shortcuts; + this.artists = artists; + this.entries = entries; + } + + public long getLastModified() { + return lastModified; + } + + public List getShortcuts() { + return shortcuts; + } + + public List getArtists() { + return artists; + } + + public void setArtists(List artists) { + this.shortcuts = new ArrayList(); + this.artists.clear(); + this.artists.addAll(artists); + } + + public List 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/nvllsvm/audinaut/domain/MusicDirectory.java b/app/src/main/java/github/nvllsvm/audinaut/domain/MusicDirectory.java new file mode 100644 index 0000000..28f7308 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/domain/MusicDirectory.java @@ -0,0 +1,628 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.domain; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.SharedPreferences; +import android.media.MediaMetadataRetriever; +import android.os.Build; +import android.util.Log; + +import java.text.Collator; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.io.File; +import java.io.Serializable; +import java.util.Collections; +import java.util.Comparator; +import java.util.Locale; + +import github.nvllsvm.audinaut.service.DownloadService; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.UpdateHelper; +import github.nvllsvm.audinaut.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 children; + + public MusicDirectory() { + children = new ArrayList(); + } + public MusicDirectory(List 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 children) { + this.children.addAll(children); + } + + public void replaceChildren(List children) { + this.children = children; + } + + public synchronized List getChildren() { + return getChildren(true, true); + } + + public synchronized List getChildren(boolean includeDirs, boolean includeFiles) { + if (includeDirs && includeFiles) { + return children; + } + + List result = new ArrayList(children.size()); + for (Entry child : children) { + if (child != null && child.isDirectory() && includeDirs || !child.isDirectory() && includeFiles) { + result.add(child); + } + } + return result; + } + public synchronized List getSongs() { + List result = new ArrayList(); + for (Entry child : children) { + if (child != null && !child.isDirectory()) { + result.add(child); + } + } + return result; + } + + public synchronized int getChildrenSize() { + return children.size(); + } + + public void shuffleChildren() { + Collections.shuffle(this.children); + } + + public void sortChildren(Context context, int instance) { + sortChildren(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_CUSTOM_SORT_ENABLED, true)); + } + public void sortChildren(boolean byYear) { + EntryComparator.sort(children, byYear); + } + + public synchronized boolean updateMetadata(MusicDirectory refreshedDirectory) { + boolean metadataUpdated = false; + Iterator it = children.iterator(); + while(it.hasNext()) { + Entry entry = it.next(); + int index = refreshedDirectory.children.indexOf(entry); + if(index != -1) { + final Entry refreshed = refreshedDirectory.children.get(index); + + entry.setTitle(refreshed.getTitle()); + entry.setAlbum(refreshed.getAlbum()); + entry.setArtist(refreshed.getArtist()); + entry.setTrack(refreshed.getTrack()); + entry.setYear(refreshed.getYear()); + entry.setGenre(refreshed.getGenre()); + entry.setTranscodedContentType(refreshed.getTranscodedContentType()); + entry.setTranscodedSuffix(refreshed.getTranscodedSuffix()); + entry.setDiscNumber(refreshed.getDiscNumber()); + entry.setType(refreshed.getType()); + if(!Util.equals(entry.getCoverArt(), refreshed.getCoverArt())) { + metadataUpdated = true; + entry.setCoverArt(refreshed.getCoverArt()); + } + + new UpdateHelper.EntryInstanceUpdater(entry) { + @Override + public void update(Entry found) { + found.setTitle(refreshed.getTitle()); + found.setAlbum(refreshed.getAlbum()); + found.setArtist(refreshed.getArtist()); + found.setTrack(refreshed.getTrack()); + found.setYear(refreshed.getYear()); + found.setGenre(refreshed.getGenre()); + found.setTranscodedContentType(refreshed.getTranscodedContentType()); + found.setTranscodedSuffix(refreshed.getTranscodedSuffix()); + found.setDiscNumber(refreshed.getDiscNumber()); + found.setType(refreshed.getType()); + if(!Util.equals(found.getCoverArt(), refreshed.getCoverArt())) { + found.setCoverArt(refreshed.getCoverArt()); + metadataUpdate = DownloadService.METADATA_UPDATED_COVER_ART; + } + } + }.execute(); + } + } + + return metadataUpdated; + } + public synchronized boolean updateEntriesList(Context context, int instance, MusicDirectory refreshedDirectory) { + boolean changed = false; + Iterator it = children.iterator(); + while(it.hasNext()) { + Entry entry = it.next(); + // No longer exists in here + if(refreshedDirectory.children.indexOf(entry) == -1) { + it.remove(); + changed = true; + } + } + + // Make sure we contain all children from refreshed set + boolean resort = false; + for(Entry refreshed: refreshedDirectory.children) { + if(!this.children.contains(refreshed)) { + this.children.add(refreshed); + resort = true; + changed = true; + } + } + + if(resort) { + this.sortChildren(context, instance); + } + + return changed; + } + + public static class Entry implements Serializable { + public static final int TYPE_SONG = 0; + + 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 Integer discNumber; + private int type = 0; + private int closeness; + private transient Artist linkedArtist; + + public Entry() { + + } + public Entry(String id) { + this.id = id; + } + public Entry(Artist artist) { + this.id = artist.getId(); + this.title = artist.getName(); + this.directory = true; + this.linkedArtist = artist; + } + + @TargetApi(Build.VERSION_CODES.GINGERBREAD_MR1) + 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(); + if(filename == null) { + return; + } + + 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 Integer getDiscNumber() { + return discNumber; + } + + public void setDiscNumber(Integer discNumber) { + this.discNumber = discNumber; + } + + public int getType() { + return type; + } + public void setType(int type) { + this.type = type; + } + public boolean isSong() { + return type == TYPE_SONG; + } + + public int getCloseness() { + return closeness; + } + + public void setCloseness(int closeness) { + this.closeness = closeness; + } + + public boolean isOnlineId(Context context) { + try { + String cacheLocation = Util.getPreferences(context).getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, null); + return cacheLocation == null || id == null || id.indexOf(cacheLocation) == -1; + } catch(Exception e) { + Log.w(TAG, "Failed to check online id validity"); + + // Err on the side of default functionality + return true; + } + } + + @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 { + private boolean byYear; + private Collator collator; + + public EntryComparator(boolean byYear) { + this.byYear = byYear; + this.collator = Collator.getInstance(Locale.US); + this.collator.setStrength(Collator.PRIMARY); + } + + 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 collator.compare(lhs.getAlbumDisplay(), rhs.getAlbumDisplay()); + } + + 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 && lhsTrack != rhsTrack) { + return lhsTrack.compareTo(rhsTrack); + } else if(lhsTrack != null) { + return -1; + } else if(rhsTrack != null) { + return 1; + } + + return collator.compare(lhs.getTitle(), rhs.getTitle()); + } + + public static void sort(List entries) { + sort(entries, true); + } + public static void sort(List 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/nvllsvm/audinaut/domain/MusicFolder.java b/app/src/main/java/github/nvllsvm/audinaut/domain/MusicFolder.java new file mode 100644 index 0000000..5906e68 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/domain/MusicFolder.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.domain; + +import android.util.Log; + +import java.io.Serializable; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * Represents a top level directory in which music or other media is stored. + * + * @author Sindre Mehus + * @version $Id$ + */ +public class MusicFolder implements Serializable { + private static final String TAG = MusicFolder.class.getSimpleName(); + private String id; + private String name; + private boolean enabled; + + public MusicFolder() { + + } + public MusicFolder(String id, String name) { + this.id = id; + this.name = name; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + public boolean getEnabled() { + return enabled; + } + + public static class MusicFolderComparator implements Comparator { + public int compare(MusicFolder lhsMusicFolder, MusicFolder rhsMusicFolder) { + if(lhsMusicFolder == rhsMusicFolder || lhsMusicFolder.getName().equals(rhsMusicFolder.getName())) { + return 0; + } else { + return lhsMusicFolder.getName().compareToIgnoreCase(rhsMusicFolder.getName()); + } + } + } + + public static void sort(List musicFolders) { + try { + Collections.sort(musicFolders, new MusicFolderComparator()); + } catch (Exception e) { + Log.w(TAG, "Failed to sort music folders", e); + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/domain/PlayerQueue.java b/app/src/main/java/github/nvllsvm/audinaut/domain/PlayerQueue.java new file mode 100644 index 0000000..6232203 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/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 . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.domain; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +public class PlayerQueue implements Serializable { + public List songs = new ArrayList(); + public List toDelete = new ArrayList(); + public int currentPlayingIndex; + public int currentPlayingPosition; + public boolean renameCurrent = false; + public Date changed = null; +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/domain/PlayerState.java b/app/src/main/java/github/nvllsvm/audinaut/domain/PlayerState.java new file mode 100644 index 0000000..fe90e89 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/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 . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.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/nvllsvm/audinaut/domain/Playlist.java b/app/src/main/java/github/nvllsvm/audinaut/domain/Playlist.java new file mode 100644 index 0000000..c79021a --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/domain/Playlist.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.domain; + +import java.io.Serializable; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +/** + * @author Sindre Mehus + */ +public class Playlist implements Serializable { + + private String id; + private String name; + private String owner; + private String comment; + private String songCount; + private Boolean pub; + private Date created; + private Date changed; + private Integer duration; + + 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 pub, String created, String changed, Integer duration) { + this.id = id; + this.name = name; + this.owner = (owner == null) ? "" : owner; + this.comment = (comment == null) ? "" : comment; + this.songCount = (songCount == null) ? "" : songCount; + this.pub = (pub == null) ? null : (pub.equals("true")); + setCreated(created); + setChanged(changed); + this.duration = duration; + } + + 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 Boolean getPublic() { + return this.pub; + } + public void setPublic(Boolean pub) { + this.pub = pub; + } + + 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; + } + + public Integer getDuration() { + return duration; + } + public void setDuration(Integer duration) { + this.duration = duration; + } + + @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); + } + + public static class PlaylistComparator implements Comparator { + @Override + public int compare(Playlist playlist1, Playlist playlist2) { + return playlist1.getName().compareToIgnoreCase(playlist2.getName()); + } + + public static List sort(List playlists) { + Collections.sort(playlists, new PlaylistComparator()); + return playlists; + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/domain/RemoteStatus.java b/app/src/main/java/github/nvllsvm/audinaut/domain/RemoteStatus.java new file mode 100644 index 0000000..eba6a10 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/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 . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.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/nvllsvm/audinaut/domain/RepeatMode.java b/app/src/main/java/github/nvllsvm/audinaut/domain/RepeatMode.java new file mode 100644 index 0000000..57f1ec8 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/domain/RepeatMode.java @@ -0,0 +1,28 @@ +package github.nvllsvm.audinaut.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/nvllsvm/audinaut/domain/SearchCritera.java b/app/src/main/java/github/nvllsvm/audinaut/domain/SearchCritera.java new file mode 100644 index 0000000..631a7c5 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/domain/SearchCritera.java @@ -0,0 +1,93 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.domain; + +import java.util.regex.Pattern; + +/** + * 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; + private Pattern pattern; + + 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; + } + + /** + * Returns and caches a pattern instance that can be used to check if a + * string matches the query. + */ + public Pattern getPattern() { + + // If the pattern wasn't already cached, create a new regular expression + // from the search string : + // * Surround the search string with ".*" (match anything) + // * Replace spaces and wildcard '*' characters with ".*" + // * All other characters are properly quoted + if (this.pattern == null) { + String regex = ".*"; + String currentPart = ""; + for (int i = 0; i < query.length(); i++) { + char c = query.charAt(i); + if (c == '*' || c == ' ') { + regex += Pattern.quote(currentPart); + regex += ".*"; + currentPart = ""; + } else { + currentPart += c; + } + } + if (currentPart.length() > 0) { + regex += Pattern.quote(currentPart); + } + + regex += ".*"; + this.pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE); + } + + return this.pattern; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/domain/SearchResult.java b/app/src/main/java/github/nvllsvm/audinaut/domain/SearchResult.java new file mode 100644 index 0000000..bd15043 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/domain/SearchResult.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.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 artists; + private final List albums; + private final List songs; + + public SearchResult(List artists, List albums, List songs) { + this.artists = artists; + this.albums = albums; + this.songs = songs; + } + + public List getArtists() { + return artists; + } + + public List getAlbums() { + return albums; + } + + public List getSongs() { + return songs; + } + + public boolean hasArtists() { + return !artists.isEmpty(); + } + public boolean hasAlbums() { + return !albums.isEmpty(); + } + public boolean hasSongs() { + return !songs.isEmpty(); + } +} \ No newline at end of file diff --git a/app/src/main/java/github/nvllsvm/audinaut/domain/User.java b/app/src/main/java/github/nvllsvm/audinaut/domain/User.java new file mode 100644 index 0000000..4a5e88b --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/domain/User.java @@ -0,0 +1,146 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2014 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.domain; + +import android.util.Pair; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +public class User implements Serializable { + 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 STREAM = "streamRole"; + public static final List ROLES = new ArrayList<>(); + + static { + ROLES.add(ADMIN); + ROLES.add(SETTINGS); + ROLES.add(STREAM); + ROLES.add(DOWNLOAD); + ROLES.add(UPLOAD); + ROLES.add(COVERART); + ROLES.add(COMMENT); + } + + private String username; + private String password; + private String email; + + private List settings = new ArrayList(); + private List musicFolders; + + 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 getSettings() { + return settings; + } + public void setSettings(List settings) { + this.settings.clear(); + this.settings.addAll(settings); + } + public void addSetting(String name, Boolean value) { + settings.add(new Setting(name, value)); + } + + public void addMusicFolder(MusicFolder musicFolder) { + if(musicFolders == null) { + musicFolders = new ArrayList<>(); + } + + musicFolders.add(new MusicFolderSetting(musicFolder.getId(), musicFolder.getName(), false)); + } + public void addMusicFolder(MusicFolderSetting musicFolderSetting, boolean defaultValue) { + if(musicFolders == null) { + musicFolders = new ArrayList<>(); + } + + musicFolders.add(new MusicFolderSetting(musicFolderSetting.getName(), musicFolderSetting.getLabel(), defaultValue)); + } + public List getMusicFolderSettings() { + return musicFolders; + } + + public static class Setting implements Serializable { + private String name; + private 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; + } + } + + public static class MusicFolderSetting extends Setting { + private String label; + + public MusicFolderSetting() { + + } + public MusicFolderSetting(String name, String label, Boolean value) { + super(name, value); + this.label = label; + } + + public String getLabel() { + return label; + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/domain/Version.java b/app/src/main/java/github/nvllsvm/audinaut/domain/Version.java new file mode 100644 index 0000000..a069dfd --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/domain/Version.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.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, 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"; + case 12: + return "5.2"; + case 13: + return "5.3"; + case 14: + return "6.0"; + } + } + 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/nvllsvm/audinaut/fragments/DownloadFragment.java b/app/src/main/java/github/nvllsvm/audinaut/fragments/DownloadFragment.java new file mode 100644 index 0000000..1247575 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/fragments/DownloadFragment.java @@ -0,0 +1,190 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2014 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.fragments; + +import android.content.DialogInterface; +import android.os.Bundle; +import android.os.Handler; +import android.support.v7.widget.helper.ItemTouchHelper; +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 java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.adapter.SectionAdapter; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.service.DownloadFile; +import github.nvllsvm.audinaut.service.DownloadService; +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.util.DownloadFileItemHelperCallback; +import github.nvllsvm.audinaut.util.ProgressListener; +import github.nvllsvm.audinaut.util.SilentBackgroundTask; +import github.nvllsvm.audinaut.util.Util; +import github.nvllsvm.audinaut.adapter.DownloadFileAdapter; +import github.nvllsvm.audinaut.view.UpdateView; + +public class DownloadFragment extends SelectRecyclerFragment implements SectionAdapter.OnItemClickedListener { + private long currentRevision; + private ScheduledExecutorService executorService; + + public DownloadFragment() { + serialize = false; + pullToRefresh = false; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) { + super.onCreateView(inflater, container, bundle); + + ItemTouchHelper touchHelper = new ItemTouchHelper(new DownloadFileItemHelperCallback(this, false)); + touchHelper.attachToRecyclerView(recyclerView); + + 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() { + 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 SectionAdapter getAdapter(List objs) { + return new DownloadFileAdapter(context, objs, this); + } + + @Override + public List getObjects(MusicService musicService, boolean refresh, ProgressListener listener) throws Exception { + DownloadService downloadService = getDownloadService(); + if(downloadService == null) { + return new ArrayList(); + } + + List songList = new ArrayList(); + songList.addAll(downloadService.getBackgroundDownloads()); + currentRevision = downloadService.getDownloadListUpdateRevision(); + return songList; + } + + @Override + public int getTitleResource() { + return R.string.button_bar_downloading; + } + + @Override + public void onItemClicked(UpdateView updateView, DownloadFile item) { + + } + + @Override + public void onCreateContextMenu(Menu menu, MenuInflater menuInflater, UpdateView updateView, DownloadFile downloadFile) { + MusicDirectory.Entry selectedItem = downloadFile.getSong(); + onCreateContextMenuSupport(menu, menuInflater, updateView, selectedItem); + if(!Util.isOffline(context)) { + menu.removeItem(R.id.song_menu_remove_playlist); + } + + recreateContextMenu(menu); + } + + @Override + public boolean onContextItemSelected(MenuItem menuItem, UpdateView updateView, DownloadFile downloadFile) { + MusicDirectory.Entry selectedItem = downloadFile.getSong(); + return onContextItemSelected(menuItem, selectedItem); + } + + @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(context) { + @Override + protected Void doInBackground() throws Throwable { + getDownloadService().clearBackground(); + return null; + } + + @Override + protected void done(Void result) { + update(); + } + }.execute(); + } + }); + return true; + } + + return false; + } + + private void update() { + DownloadService downloadService = getDownloadService(); + if (downloadService == null || objects == null || adapter == null) { + return; + } + + if (currentRevision != downloadService.getDownloadListUpdateRevision()) { + List downloadFileList = downloadService.getBackgroundDownloads(); + objects.clear(); + objects.addAll(downloadFileList); + adapter.notifyDataSetChanged(); + + currentRevision = downloadService.getDownloadListUpdateRevision(); + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/fragments/EqualizerFragment.java b/app/src/main/java/github/nvllsvm/audinaut/fragments/EqualizerFragment.java new file mode 100644 index 0000000..c2c477c --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/fragments/EqualizerFragment.java @@ -0,0 +1,459 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2010 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.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.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.audiofx.EqualizerController; +import github.nvllsvm.audinaut.audiofx.LoudnessEnhancerController; +import github.nvllsvm.audinaut.service.DownloadService; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.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 bars = new HashMap(); + 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); + setSubtitle(null); + + return rootView; + } + + @Override + public void onPause() { + super.onPause(); + + try { + equalizerController.saveSettings(); + + if (!equalizer.getEnabled()) { + equalizerController.release(); + } + } catch(Exception e) { + Log.w(TAG, "Failed to release controller", e); + } + } + + @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) { + try { + boolean isEnabled = equalizer.getEnabled(); + short minEQLevel = equalizer.getBandLevelRange()[0]; + short maxEQLevel = equalizer.getBandLevelRange()[1]; + for (Map.Entry 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(); + } + } catch(Exception e) { + Log.e(TAG, "Failed to update bars"); + } + } + + 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) { + try { + short level = (short) (progress + minEQLevel); + if (fromUser) { + equalizer.setBandLevel(band, (short) (level + masterLevel)); + } + updateLevelText(levelTextView, level); + } catch(Exception e) { + Log.e(TAG, "Failed to change equalizer", e); + } + } + + @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) { + try { + 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); + } catch(Exception e) { + Log.e(TAG, "Failed to change equalizer", e); + } + } + + @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/nvllsvm/audinaut/fragments/MainFragment.java b/app/src/main/java/github/nvllsvm/audinaut/fragments/MainFragment.java new file mode 100644 index 0000000..e8d1325 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/fragments/MainFragment.java @@ -0,0 +1,357 @@ +package github.nvllsvm.audinaut.fragments; + +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.res.Resources; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.os.StatFs; +import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.adapter.MainAdapter; +import github.nvllsvm.audinaut.adapter.SectionAdapter; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.EnvironmentVariables; +import github.nvllsvm.audinaut.util.FileUtil; +import github.nvllsvm.audinaut.util.LoadingTask; +import github.nvllsvm.audinaut.util.ProgressListener; +import github.nvllsvm.audinaut.util.UserUtil; +import github.nvllsvm.audinaut.util.Util; +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.service.MusicServiceFactory; +import github.nvllsvm.audinaut.view.UpdateView; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.net.URL; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.net.ssl.HttpsURLConnection; + +public class MainFragment extends SelectRecyclerFragment { + private static final String TAG = MainFragment.class.getSimpleName(); + public static final String SONGS_LIST_PREFIX = "songs-"; + public static final String SONGS_NEWEST = SONGS_LIST_PREFIX + "newest"; + public static final String SONGS_TOP_PLAYED = SONGS_LIST_PREFIX + "topPlayed"; + public static final String SONGS_RECENT = SONGS_LIST_PREFIX + "recent"; + public static final String SONGS_FREQUENT = SONGS_LIST_PREFIX + "frequent"; + + public MainFragment() { + super(); + pullToRefresh = false; + serialize = false; + backgroundUpdate = false; + alwaysFullscreen = true; + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) { + menuInflater.inflate(R.menu.main, menu); + onFinishSetupOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if(super.onOptionsItemSelected(item)) { + return true; + } + + return false; + } + + @Override + public int getOptionsMenu() { + return 0; + } + + @Override + public SectionAdapter getAdapter(List objs) { + List> sections = new ArrayList<>(); + List headers = new ArrayList<>(); + + List albums = new ArrayList<>(); + albums.add(R.string.main_albums_random); + albums.add(R.string.main_albums_alphabetical); + albums.add(R.string.main_albums_genres); + albums.add(R.string.main_albums_year); + albums.add(R.string.main_albums_recent); + albums.add(R.string.main_albums_frequent); + + sections.add(albums); + headers.add("albums"); + + return new MainAdapter(context, headers, sections, this); + } + + @Override + public List getObjects(MusicService musicService, boolean refresh, ProgressListener listener) throws Exception { + return Arrays.asList(0); + } + + @Override + public int getTitleResource() { + return R.string.common_appname; + } + + 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(); + } + + 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 showAboutDialog() { + new LoadingTask(context) { + Long[] used; + long bytesTotalFs; + long bytesAvailableFs; + + @Override + protected Void doInBackground() throws Throwable { + File rootFolder = FileUtil.getMusicDirectory(context); + StatFs stat = new StatFs(rootFolder.getPath()); + bytesTotalFs = (long) stat.getBlockCount() * (long) stat.getBlockSize(); + bytesAvailableFs = (long) stat.getAvailableBlocks() * (long) stat.getBlockSize(); + + used = FileUtil.getUsedSize(context, rootFolder); + return null; + } + + @Override + protected void done(Void result) { + List headers = new ArrayList<>(); + List details = new ArrayList<>(); + + headers.add(R.string.details_author); + details.add("Andrew Rabert"); + + headers.add(R.string.details_email); + details.add("ar@nullsum.net"); + + try { + headers.add(R.string.details_version); + details.add(context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName); + } catch(Exception e) { + details.add(""); + } + + Resources res = context.getResources(); + headers.add(R.string.details_files_cached); + details.add(Long.toString(used[0])); + + headers.add(R.string.details_files_permanent); + details.add(Long.toString(used[1])); + + headers.add(R.string.details_used_space); + details.add(res.getString(R.string.details_of, Util.formatLocalizedBytes(used[2], context), Util.formatLocalizedBytes(Util.getCacheSizeMB(context) * 1024L * 1024L, context))); + + headers.add(R.string.details_available_space); + details.add(res.getString(R.string.details_of, Util.formatLocalizedBytes(bytesAvailableFs, context), Util.formatLocalizedBytes(bytesTotalFs, context))); + + Util.showDetailsDialog(context, R.string.main_about_title, headers, details); + } + }.execute(); + } + + private void rescanServer() { + new LoadingTask(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 PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); + new LoadingTask(context) { + @Override + protected String doInBackground() throws Throwable { + updateProgress("Gathering Logs"); + File logcat = new File(Environment.getExternalStorageDirectory(), "audinaut-logcat.txt"); + Util.delete(logcat); + Process logcatProc = null; + + try { + List progs = new ArrayList(); + 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(); + } finally { + if(logcatProc != null) { + logcatProc.destroy(); + } + } + + URL url = new URL("https://pastebin.com/api/api_post.php"); + HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection(); + StringBuffer responseBuffer = new StringBuffer(); + try { + urlConnection.setReadTimeout(10000); + urlConnection.setConnectTimeout(15000); + urlConnection.setRequestMethod("POST"); + urlConnection.setDoInput(true); + urlConnection.setDoOutput(true); + + OutputStream os = urlConnection.getOutputStream(); + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(os, Constants.UTF_8)); + writer.write("api_dev_key=" + URLEncoder.encode(EnvironmentVariables.PASTEBIN_DEV_KEY, Constants.UTF_8) + "&api_option=paste&api_paste_private=1&api_paste_code="); + + BufferedReader reader = null; + try { + reader = new BufferedReader(new InputStreamReader(new FileInputStream(logcat))); + String line; + while ((line = reader.readLine()) != null) { + writer.write(URLEncoder.encode(line + "\n", Constants.UTF_8)); + } + } finally { + Util.close(reader); + } + + File stacktrace = new File(Environment.getExternalStorageDirectory(), "audinaut-stacktrace.txt"); + if(stacktrace.exists() && stacktrace.isFile()) { + writer.write("\n\nMost Recent Stacktrace:\n\n"); + + reader = null; + try { + reader = new BufferedReader(new InputStreamReader(new FileInputStream(stacktrace))); + String line; + while ((line = reader.readLine()) != null) { + writer.write(URLEncoder.encode(line + "\n", Constants.UTF_8)); + } + } finally { + Util.close(reader); + } + } + + writer.flush(); + writer.close(); + os.close(); + + BufferedReader in = new BufferedReader(new InputStreamReader(urlConnection.getInputStream())); + String inputLine; + while ((inputLine = in.readLine()) != null) { + responseBuffer.append(inputLine); + } + in.close(); + } finally { + urlConnection.disconnect(); + } + + String response = responseBuffer.toString(); + if(response.indexOf("http") == 0) { + return response.replace("http:", "https:"); + } else { + throw new Exception("Pastebin Error: " + response); + } + } + + @Override + protected void error(Throwable error) { + Log.e(TAG, "Failed to gather logs", error); + Util.toast(context, "Failed to gather logs"); + } + + @Override + protected void done(String logcat) { + String footer = "Android SDK: " + Build.VERSION.SDK; + footer += "\nDevice Model: " + Build.MODEL; + footer += "\nDevice Name: " + Build.MANUFACTURER + " " + Build.PRODUCT; + footer += "\nROM: " + Build.DISPLAY; + footer += "\nLogs: " + logcat; + footer += "\nBuild Number: " + packageInfo.versionCode; + + Intent email = new Intent(Intent.ACTION_SENDTO, + Uri.fromParts("mailto", "ar@nullsum.net", null)); + email.putExtra(Intent.EXTRA_SUBJECT, "Audinaut " + packageInfo.versionName + " Error Logs"); + email.putExtra(Intent.EXTRA_TEXT, "Describe the problem here\n\n\n" + footer); + startActivity(email); + } + }.execute(); + } catch(Exception e) {} + } + + @Override + public void onItemClicked(UpdateView updateView, Integer item) { + if (item == R.string.main_albums_newest) { + showAlbumList("newest"); + } else if (item == R.string.main_albums_random) { + showAlbumList("random"); + } else if (item == R.string.main_albums_recent) { + showAlbumList("recent"); + } else if (item == R.string.main_albums_frequent) { + showAlbumList("frequent"); + } else if(item == R.string.main_albums_genres) { + showAlbumList("genres"); + } else if(item == R.string.main_albums_year) { + showAlbumList("years"); + } else if(item == R.string.main_albums_alphabetical) { + showAlbumList("alphabeticalByName"); + } else if (item == R.string.main_songs_newest) { + showAlbumList(SONGS_NEWEST); + } else if (item == R.string.main_songs_top_played) { + showAlbumList(SONGS_TOP_PLAYED); + } else if (item == R.string.main_songs_recent) { + showAlbumList(SONGS_RECENT); + } else if (item == R.string.main_songs_frequent) { + showAlbumList(SONGS_FREQUENT); + } + } + + @Override + public void onCreateContextMenu(Menu menu, MenuInflater menuInflater, UpdateView updateView, Integer item) {} + + @Override + public boolean onContextItemSelected(MenuItem menuItem, UpdateView updateView, Integer item) { + return false; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/fragments/NowPlayingFragment.java b/app/src/main/java/github/nvllsvm/audinaut/fragments/NowPlayingFragment.java new file mode 100644 index 0000000..f7c40f0 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/fragments/NowPlayingFragment.java @@ -0,0 +1,1109 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2014 (C) Scott Jackson +*/ +package github.nvllsvm.audinaut.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.annotation.TargetApi; +import android.support.v7.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.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.helper.ItemTouchHelper; +import android.util.Log; +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.EditText; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.PopupMenu; +import android.widget.SeekBar; +import android.widget.TextView; +import android.widget.ViewFlipper; +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.activity.SubsonicFragmentActivity; +import github.nvllsvm.audinaut.adapter.SectionAdapter; +import github.nvllsvm.audinaut.audiofx.EqualizerController; +import github.nvllsvm.audinaut.domain.PlayerState; +import github.nvllsvm.audinaut.domain.RepeatMode; +import github.nvllsvm.audinaut.service.DownloadFile; +import github.nvllsvm.audinaut.service.DownloadService; +import github.nvllsvm.audinaut.service.DownloadService.OnSongChangedListener; +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.service.MusicServiceFactory; +import github.nvllsvm.audinaut.service.OfflineException; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.SilentBackgroundTask; +import github.nvllsvm.audinaut.adapter.DownloadFileAdapter; +import github.nvllsvm.audinaut.view.FadeOutAnimation; +import github.nvllsvm.audinaut.view.FastScroller; +import github.nvllsvm.audinaut.view.UpdateView; +import github.nvllsvm.audinaut.util.Util; + +import static github.nvllsvm.audinaut.domain.MusicDirectory.Entry; +import static github.nvllsvm.audinaut.domain.PlayerState.*; +import github.nvllsvm.audinaut.util.*; +import github.nvllsvm.audinaut.view.AutoRepeatButton; +import java.util.ArrayList; +import java.util.concurrent.ScheduledFuture; + +public class NowPlayingFragment extends SubsonicFragment implements OnGestureListener, SectionAdapter.OnItemClickedListener, OnSongChangedListener { + private static final String TAG = NowPlayingFragment.class.getSimpleName(); + private static final int PERCENTAGE_OF_SCREEN_FOR_SWIPE = 10; + + 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 RecyclerView playlistView; + private TextView positionTextView; + private TextView durationTextView; + private TextView statusTextView; + private SeekBar progressBar; + private AutoRepeatButton previousButton; + private AutoRepeatButton nextButton; + private AutoRepeatButton rewindButton; + private AutoRepeatButton fastforwardButton; + private View pauseButton; + private View stopButton; + private View startButton; + private ImageButton repeatButton; + private View toggleListButton; + + private ScheduledExecutorService executorService; + private DownloadFile currentPlaying; + private int swipeDistance; + private int swipeVelocity; + private ScheduledFuture hideControlsFuture; + private List songList; + private DownloadFileAdapter songListAdapter; + private boolean seekInProgress = false; + private boolean startFlipped = false; + private boolean scrollWhenLoaded = false; + private int lastY = 0; + private int currentPlayingSize = 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; + } + } + primaryFragment = false; + } + + @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); + + 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); + previousButton = (AutoRepeatButton)rootView.findViewById(R.id.download_previous); + nextButton = (AutoRepeatButton)rootView.findViewById(R.id.download_next); + rewindButton = (AutoRepeatButton) rootView.findViewById(R.id.download_rewind); + fastforwardButton = (AutoRepeatButton) rootView.findViewById(R.id.download_fastforward); + 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); + toggleListButton =rootView.findViewById(R.id.download_toggle_list); + + playlistView = (RecyclerView)rootView.findViewById(R.id.download_list); + FastScroller fastScroller = (FastScroller) rootView.findViewById(R.id.download_fast_scroller); + fastScroller.attachRecyclerView(playlistView); + setupLayoutManager(playlistView, false); + ItemTouchHelper touchHelper = new ItemTouchHelper(new DownloadFileItemHelperCallback(this, true)); + touchHelper.attachToRecyclerView(playlistView); + + 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); + 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(context) { + @Override + protected Void doInBackground() throws Throwable { + getDownloadService().previous(); + return null; + } + }.execute(); + setControlsVisible(true); + } + }); + previousButton.setOnRepeatListener(new Runnable() { + public void run() { + changeProgress(true); + } + }); + + nextButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + warnIfStorageUnavailable(); + new SilentBackgroundTask(context) { + @Override + protected Boolean doInBackground() throws Throwable { + getDownloadService().next(); + return true; + } + }.execute(); + setControlsVisible(true); + } + }); + nextButton.setOnRepeatListener(new Runnable() { + public void run() { + changeProgress(false); + } + }); + + rewindButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + changeProgress(true); + } + }); + rewindButton.setOnRepeatListener(new Runnable() { + public void run() { + changeProgress(true); + } + }); + + fastforwardButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + changeProgress(false); + } + }); + fastforwardButton.setOnRepeatListener(new Runnable() { + public void run() { + changeProgress(false); + } + }); + + + pauseButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + new SilentBackgroundTask(context) { + @Override + protected Void doInBackground() throws Throwable { + getDownloadService().pause(); + return null; + } + }.execute(); + } + }); + + stopButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + new SilentBackgroundTask(context) { + @Override + protected Void doInBackground() throws Throwable { + getDownloadService().reset(); + return null; + } + }.execute(); + } + }); + + startButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + warnIfStorageUnavailable(); + new SilentBackgroundTask(context) { + @Override + protected Void doInBackground() throws Throwable { + start(); + return null; + } + }.execute(); + } + }); + + repeatButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + RepeatMode repeatMode = getDownloadService().getRepeatMode().next(); + getDownloadService().setRepeatMode(repeatMode); + 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; + } + updateRepeatButton(); + setControlsVisible(true); + } + }); + + 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(context) { + @Override + protected Void doInBackground() throws Throwable { + getDownloadService().seekTo(progressBar.getProgress()); + return null; + } + + @Override + protected void done(Void result) { + seekInProgress = false; + } + }.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) { + positionTextView.setText(Util.formatDuration(position / 1000)); + setControlsVisible(true); + } + } + }); + + 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.isRemovePlayed()) { + menu.findItem(R.id.menu_remove_played).setChecked(true); + } + + boolean equalizerAvailable = downloadService != null && downloadService.getEqualizerAvailable(); + if(equalizerAvailable) { + SharedPreferences prefs = Util.getPreferences(context); + boolean equalizerOn = prefs.getBoolean(Constants.PREFERENCES_EQUALIZER_ON, false); + if (equalizerOn && downloadService != null) { + if(downloadService.getEqualizerController() != null && downloadService.getEqualizerController().isEnabled()) { + menu.findItem(R.id.menu_equalizer).setChecked(true); + } + } + } else { + menu.removeItem(R.id.menu_equalizer); + } + + if(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_BATCH_MODE, false)) { + menu.findItem(R.id.menu_batch_mode).setChecked(true); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem menuItem) { + if(menuItemSelected(menuItem.getItemId(), null)) { + return true; + } + + return super.onOptionsItemSelected(menuItem); + } + + @Override + public void onCreateContextMenu(Menu menu, MenuInflater menuInflater, UpdateView updateView, DownloadFile downloadFile) { + if(Util.isOffline(context)) { + menuInflater.inflate(R.menu.nowplaying_context_offline, menu); + } else { + menuInflater.inflate(R.menu.nowplaying_context, menu); + } + + if (downloadFile.getSong().getParent() == null) { + menu.findItem(R.id.menu_show_album).setVisible(false); + menu.findItem(R.id.menu_show_artist).setVisible(false); + } + + MenuUtil.hideMenuItems(context, menu, updateView); + } + + @Override + public boolean onContextItemSelected(MenuItem menuItem, UpdateView updateView, DownloadFile downloadFile) { + if(onContextItemSelected(menuItem, downloadFile.getSong())) { + return true; + } + + return menuItemSelected(menuItem.getItemId(), downloadFile); + } + + private boolean menuItemSelected(int menuItemId, final DownloadFile song) { + List songs; + 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_remove_all: + Util.confirmDialog(context, R.string.download_menu_remove_all, "", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + new SilentBackgroundTask(context) { + @Override + protected Void doInBackground() throws Throwable { + getDownloadService().setShufflePlayEnabled(false); + getDownloadService().clear(); + return null; + } + + @Override + protected void done(Void result) { + context.closeNowPlaying(); + } + }.execute(); + } + }); + 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(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 entries = new LinkedList(); + for (DownloadFile downloadFile : getDownloadService().getSongs()) { + entries.add(downloadFile.getSong()); + } + createNewPlaylist(entries, true); + return true; + case R.id.menu_info: + displaySongInfo(song.getSong()); + 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; + }case R.id.menu_batch_mode: + if(Util.isBatchMode(context)) { + Util.setBatchMode(context, false); + songListAdapter.notifyDataSetChanged(); + } else { + Util.setBatchMode(context, true); + songListAdapter.notifyDataSetChanged(); + } + context.supportInvalidateOptionsMenu(); + + return true; + default: + return false; + } + } + + @Override + public void onResume() { + super.onResume(); + if(this.primaryFragment) { + onResumeHandlers(); + } else { + update(); + } + } + private void onResumeHandlers() { + executorService = Executors.newSingleThreadScheduledExecutor(); + setControlsVisible(true); + + final DownloadService downloadService = getDownloadService(); + if (downloadService == null || downloadService.getCurrentPlaying() == null || startFlipped) { + playlistFlipper.setDisplayedChild(1); + startFlipped = false; + } + + updateButtons(); + + if(currentPlaying == null && downloadService != null && currentPlaying == downloadService.getCurrentPlaying()) { + getImageLoader().loadImage(albumArtImageView, (Entry) null, true, false); + } + + context.runWhenServiceAvailable(new Runnable() { + @Override + public void run() { + if (primaryFragment) { + DownloadService downloadService = getDownloadService(); + downloadService.addOnSongChangedListener(NowPlayingFragment.this, true); + } + updateRepeatButton(); + updateTitle(); + } + }); + } + + @Override + public void onPause() { + super.onPause(); + onPauseHandlers(); + } + private void onPauseHandlers() { + if(executorService != null) { + DownloadService downloadService = getDownloadService(); + if (downloadService != null) { + downloadService.removeOnSongChangeListener(this); + } + playlistFlipper.setDisplayedChild(0); + } + } + + @Override + public void setPrimaryFragment(boolean primary) { + super.setPrimaryFragment(primary); + if(rootView != null) { + if(primary) { + onResumeHandlers(); + } else { + onPauseHandlers(); + } + } + } + + @Override + public void setTitle(int title) { + this.title = context.getResources().getString(title); + if(this.primaryFragment) { + context.setTitle(this.title); + } + } + @Override + public void setSubtitle(CharSequence title) { + this.subtitle = title; + if(this.primaryFragment) { + context.setSubtitle(title); + } + } + + @Override + public SectionAdapter getCurrentAdapter() { + return songListAdapter; + } + + 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) { + DownloadService downloadService = getDownloadService(); + 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; + } + } + + // Scroll to current playing/downloading. + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private void scrollToCurrent() { + if (getDownloadService() == null || songListAdapter == null) { + scrollWhenLoaded = true; + return; + } + + // Try to get position of current playing/downloading + int position = songListAdapter.getItemPosition(currentPlaying); + if(position == -1) { + DownloadFile currentDownloading = getDownloadService().getCurrentDownloading(); + position = songListAdapter.getItemPosition(currentDownloading); + } + + // If found, scroll to it + if(position != -1) { + // RecyclerView.scrollToPosition just puts it on the screen (ie: bottom if scrolled below it) + LinearLayoutManager layoutManager = (LinearLayoutManager) playlistView.getLayoutManager(); + layoutManager.scrollToPositionWithOffset(position, 0); + } + } + + private void update() { + if(startFlipped) { + startFlipped = false; + scrollToCurrent(); + } + } + + private int getMinutes(int progress) { + if(progress < 30) { + return progress + 1; + } else if(progress < 49) { + return (progress - 30) * 5 + getMinutes(29); + } else if(progress < 57) { + return (progress - 48) * 30 + getMinutes(48); + } else if(progress < 81) { + return (progress - 56) * 60 + getMinutes(56); + } else { + return (progress - 80) * 150 + getMinutes(80); + } + } + + 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 changeProgress(final boolean rewind) { + final DownloadService downloadService = getDownloadService(); + if(downloadService == null) { + return; + } + + new SilentBackgroundTask(context) { + int seekTo; + + @Override + protected Void doInBackground() throws Throwable { + if(rewind) { + seekTo = downloadService.rewind(); + } else { + seekTo = downloadService.fastForward(); + } + return null; + } + + @Override + protected void done(Void result) { + progressBar.setProgress(seekTo); + } + }.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(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; + } + 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; + } + + @Override + public void onItemClicked(UpdateView updateView, final DownloadFile item) { + warnIfStorageUnavailable(); + new SilentBackgroundTask(context) { + @Override + protected Void doInBackground() throws Throwable { + getDownloadService().play(item); + return null; + } + }.execute(); + } + + @Override + public void onSongChanged(DownloadFile currentPlaying, int currentPlayingIndex) { + this.currentPlaying = currentPlaying; + setupSubtitle(currentPlayingIndex); + + if (currentPlaying != null && !currentPlaying.isSong()) { + previousButton.setVisibility(View.GONE); + nextButton.setVisibility(View.GONE); + + rewindButton.setVisibility(View.VISIBLE); + fastforwardButton.setVisibility(View.VISIBLE); + } else { + previousButton.setVisibility(View.VISIBLE); + nextButton.setVisibility(View.VISIBLE); + + rewindButton.setVisibility(View.GONE); + fastforwardButton.setVisibility(View.GONE); + } + updateTitle(); + } + + private void setupSubtitle(int currentPlayingIndex) { + if (currentPlaying != null) { + Entry song = currentPlaying.getSong(); + songTitleTextView.setText(song.getTitle()); + getImageLoader().loadImage(albumArtImageView, song, true, true); + + DownloadService downloadService = getDownloadService(); + if(downloadService.isShufflePlayEnabled()) { + setSubtitle(context.getResources().getString(R.string.download_playerstate_playing_shuffle)); + } else { + setSubtitle(context.getResources().getString(R.string.download_playing_out_of, currentPlayingIndex + 1, currentPlayingSize)); + } + } else { + songTitleTextView.setText(null); + getImageLoader().loadImage(albumArtImageView, (Entry) null, true, false); + setSubtitle(null); + } + } + + @Override + public void onSongsChanged(List songs, DownloadFile currentPlaying, int currentPlayingIndex) { + currentPlayingSize = songs.size(); + + DownloadService downloadService = getDownloadService(); + if(downloadService.isShufflePlayEnabled()) { + emptyTextView.setText(R.string.download_shuffle_loading); + } + else { + emptyTextView.setText(R.string.download_empty); + } + + if(songListAdapter == null) { + songList = new ArrayList<>(); + songList.addAll(songs); + playlistView.setAdapter(songListAdapter = new DownloadFileAdapter(context, songList, NowPlayingFragment.this)); + } else { + songList.clear(); + songList.addAll(songs); + songListAdapter.notifyDataSetChanged(); + } + + emptyTextView.setVisibility(songs.isEmpty() ? View.VISIBLE : View.GONE); + + if(scrollWhenLoaded) { + scrollToCurrent(); + scrollWhenLoaded = false; + } + + if(this.currentPlaying != currentPlaying) { + onSongChanged(currentPlaying, currentPlayingIndex); + onMetadataUpdate(currentPlaying != null ? currentPlaying.getSong() : null, DownloadService.METADATA_UPDATED_ALL); + } else { + setupSubtitle(currentPlayingIndex); + } + + toggleListButton.setVisibility(View.VISIBLE); + repeatButton.setVisibility(View.VISIBLE); + } + + @Override + public void onSongProgress(DownloadFile currentPlaying, int millisPlayed, Integer duration, boolean isSeekable) { + 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); + } + } + + @Override + public void onStateUpdate(DownloadFile downloadFile, PlayerState playerState) { + switch (playerState) { + case DOWNLOADING: + if(currentPlaying != null) { + if(Util.isWifiRequiredForDownload(context)) { + statusTextView.setText(context.getResources().getString(R.string.download_playerstate_mobile_disabled)); + } else { + 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) { + Entry entry = currentPlaying.getSong(); + if(entry.getAlbum() != null) { + String artist = ""; + if (entry.getArtist() != null) { + artist = currentPlaying.getSong().getArtist() + " - "; + } + statusTextView.setText(artist + entry.getAlbum()); + } else { + statusTextView.setText(null); + } + } 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; + } + } + + @Override + public void onMetadataUpdate(Entry song, int fieldChange) { + if(song != null && albumArtImageView != null && fieldChange == DownloadService.METADATA_UPDATED_COVER_ART) { + getImageLoader().loadImage(albumArtImageView, song, true, true); + } + } + + public void updateRepeatButton() { + DownloadService downloadService = getDownloadService(); + switch (downloadService.getRepeatMode()) { + case OFF: + repeatButton.setImageResource(DrawableTint.getDrawableRes(context, R.attr.media_button_repeat_off)); + break; + case ALL: + repeatButton.setImageResource(DrawableTint.getDrawableRes(context, R.attr.media_button_repeat_all)); + break; + case SINGLE: + repeatButton.setImageResource(DrawableTint.getDrawableRes(context, R.attr.media_button_repeat_single)); + break; + default: + break; + } + } + private void updateTitle() { + DownloadService downloadService = getDownloadService(); + + String title = context.getResources().getString(R.string.button_bar_now_playing); + + setTitle(title); + } + + @Override + protected List getSelectedEntries() { + List selected = getCurrentAdapter().getSelected(); + List entries = new ArrayList<>(); + + for(DownloadFile downloadFile: selected) { + if(downloadFile.getSong() != null) { + entries.add(downloadFile.getSong()); + } + } + + return entries; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/fragments/PreferenceCompatFragment.java b/app/src/main/java/github/nvllsvm/audinaut/fragments/PreferenceCompatFragment.java new file mode 100644 index 0000000..f7084b8 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/fragments/PreferenceCompatFragment.java @@ -0,0 +1,334 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2014 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.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.util.Log; +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.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.util.Constants; + +public abstract class PreferenceCompatFragment extends SubsonicFragment { + private static final String TAG = PreferenceCompatFragment.class.getSimpleName(); + 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 PreferenceScreen addPreferencesFromResource(int resId) { + requirePreferenceManager(); + PreferenceScreen screen = inflateFromResource(getActivity(), resId, getPreferenceScreen()); + setPreferenceScreen(screen); + + for(int i = 0; i < screen.getPreferenceCount(); i++) { + Preference preference = screen.getPreference(i); + if(preference instanceof PreferenceScreen && preference.getKey() != null) { + preference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + onStartNewFragment(preference.getKey()); + return false; + } + }); + } + } + + return 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(View.SCROLLBARS_INSIDE_OVERLAY); + 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) { + PreferenceScreen preferenceScreen = addPreferencesFromResource(res); + onInitPreferences(preferenceScreen); + } + } + + @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 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); + } + } + + protected 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; + } + + protected abstract void onInitPreferences(PreferenceScreen preferenceScreen); + protected abstract void onStartNewFragment(String name); +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/fragments/SearchFragment.java b/app/src/main/java/github/nvllsvm/audinaut/fragments/SearchFragment.java new file mode 100644 index 0000000..c1a7763 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/fragments/SearchFragment.java @@ -0,0 +1,291 @@ +package github.nvllsvm.audinaut.fragments; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.view.MenuItemCompat; +import android.support.v4.widget.SwipeRefreshLayout; +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.View; +import android.view.MenuItem; +import android.net.Uri; +import android.view.ViewGroup; +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.adapter.ArtistAdapter; +import github.nvllsvm.audinaut.adapter.EntryGridAdapter; +import github.nvllsvm.audinaut.adapter.SearchAdapter; +import github.nvllsvm.audinaut.adapter.SectionAdapter; +import github.nvllsvm.audinaut.domain.Artist; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.SearchCritera; +import github.nvllsvm.audinaut.domain.SearchResult; +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.service.MusicServiceFactory; +import github.nvllsvm.audinaut.service.DownloadService; +import github.nvllsvm.audinaut.util.BackgroundTask; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.TabBackgroundTask; +import github.nvllsvm.audinaut.util.Util; +import github.nvllsvm.audinaut.view.UpdateView; + +public class SearchFragment extends SubsonicFragment implements SectionAdapter.OnItemClickedListener { + private static final String TAG = SearchFragment.class.getSimpleName(); + + private static final int MAX_ARTISTS = 20; + private static final int MAX_ALBUMS = 20; + private static final int MAX_SONGS = 50; + private static final int MIN_CLOSENESS = 1; + + protected RecyclerView recyclerView; + protected SearchAdapter adapter; + protected boolean largeAlbums = false; + + private SearchResult searchResult; + private boolean skipSearch = false; + private String currentQuery; + + public SearchFragment() { + super(); + alwaysStartFullscreen = true; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if(savedInstanceState != null) { + searchResult = (SearchResult) savedInstanceState.getSerializable(Constants.FRAGMENT_LIST); + } + largeAlbums = Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_LARGE_ALBUM_ART, true); + } + + @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_recycler_fragment, container, false); + setTitle(R.string.search_title); + + refreshLayout = (SwipeRefreshLayout) rootView.findViewById(R.id.refresh_layout); + refreshLayout.setEnabled(false); + + recyclerView = (RecyclerView) rootView.findViewById(R.id.fragment_recycler); + setupLayoutManager(recyclerView, largeAlbums); + + registerForContextMenu(recyclerView); + context.onNewIntent(context.getIntent()); + + if(searchResult != null) { + skipSearch = true; + recyclerView.setAdapter(adapter = new SearchAdapter(context, searchResult, getImageLoader(), largeAlbums, this)); + } + + return rootView; + } + + @Override + public void setIsOnlyVisible(boolean isOnlyVisible) { + boolean update = this.isOnlyVisible != isOnlyVisible; + super.setIsOnlyVisible(isOnlyVisible); + if(update && adapter != null) { + RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); + if(layoutManager instanceof GridLayoutManager) { + ((GridLayoutManager) layoutManager).setSpanCount(getRecyclerColumnCount()); + } + } + } + + @Override + public GridLayoutManager.SpanSizeLookup getSpanSizeLookup(final GridLayoutManager gridLayoutManager) { + return new GridLayoutManager.SpanSizeLookup() { + @Override + public int getSpanSize(int position) { + int viewType = adapter.getItemViewType(position); + if(viewType == EntryGridAdapter.VIEW_TYPE_SONG || viewType == EntryGridAdapter.VIEW_TYPE_HEADER || viewType == ArtistAdapter.VIEW_TYPE_ARTIST) { + return gridLayoutManager.getSpanCount(); + } else { + return 1; + } + } + }; + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) { + menuInflater.inflate(R.menu.search, menu); + onFinishSetupOptionsMenu(menu); + } + + @Override + public void onCreateContextMenu(Menu menu, MenuInflater menuInflater, UpdateView updateView, Serializable item) { + onCreateContextMenuSupport(menu, menuInflater, updateView, item); + if(item instanceof MusicDirectory.Entry && !Util.isOffline(context)) { + menu.removeItem(R.id.song_menu_remove_playlist); + } + recreateContextMenu(menu); + } + + @Override + public boolean onContextItemSelected(MenuItem menuItem, UpdateView updateView, Serializable item) { + return onContextItemSelected(menuItem, item); + } + + @Override + public void refresh(boolean refresh) { + context.onNewIntent(context.getIntent()); + } + + @Override + public void onItemClicked(UpdateView updateView, Serializable item) { + 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 { + onSongSelected(entry, false, true, true, false); + } + } + } + + @Override + protected List getSelectedEntries() { + List selected = adapter.getSelected(); + List selectedMedia = new ArrayList<>(); + for(Serializable ser: selected) { + if(ser instanceof MusicDirectory.Entry) { + selectedMedia.add((MusicDirectory.Entry) ser); + } + } + + return selectedMedia; + } + + @Override + protected boolean isShowArtistEnabled() { + return true; + } + + public void search(final String query, final boolean autoplay) { + if(skipSearch) { + skipSearch = false; + return; + } + currentQuery = query; + + BackgroundTask task = new TabBackgroundTask(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; + recyclerView.setAdapter(adapter = new SearchAdapter(context, searchResult, getImageLoader(), largeAlbums, SearchFragment.this)); + if (autoplay) { + autoplay(query); + } + + } + }; + task.execute(); + + if(searchItem != null) { + MenuItemCompat.collapseActionView(searchItem); + } + } + + protected String getCurrentQuery() { + return currentQuery; + } + + 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 autoplay(String query) { + query = query.toLowerCase(); + + Artist artist = null; + if(!searchResult.getArtists().isEmpty()) { + artist = searchResult.getArtists().get(0); + artist.setCloseness(Util.getStringDistance(artist.getName().toLowerCase(), query)); + } + MusicDirectory.Entry album = null; + if(!searchResult.getAlbums().isEmpty()) { + album = searchResult.getAlbums().get(0); + album.setCloseness(Util.getStringDistance(album.getTitle().toLowerCase(), query)); + } + MusicDirectory.Entry song = null; + if(!searchResult.getSongs().isEmpty()) { + song = searchResult.getSongs().get(0); + song.setCloseness(Util.getStringDistance(song.getTitle().toLowerCase(), query)); + } + + if(artist != null && (artist.getCloseness() <= MIN_CLOSENESS || + (album == null || artist.getCloseness() <= album.getCloseness()) && + (song == null || artist.getCloseness() <= song.getCloseness()))) { + onArtistSelected(artist, true); + } else if(album != null && (album.getCloseness() <= MIN_CLOSENESS || + song == null || album.getCloseness() <= song.getCloseness())) { + onAlbumSelected(album, true); + } else if(song != null) { + onSongSelected(song, false, false, true, false); + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/fragments/SelectArtistFragment.java b/app/src/main/java/github/nvllsvm/audinaut/fragments/SelectArtistFragment.java new file mode 100644 index 0000000..81d5f29 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/fragments/SelectArtistFragment.java @@ -0,0 +1,253 @@ +package github.nvllsvm.audinaut.fragments; + +import android.annotation.TargetApi; +import android.os.Build; +import android.os.Bundle; +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.LinearLayout; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.adapter.ArtistAdapter; +import github.nvllsvm.audinaut.adapter.SectionAdapter; +import github.nvllsvm.audinaut.domain.Artist; +import github.nvllsvm.audinaut.domain.Indexes; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.MusicDirectory.Entry; +import github.nvllsvm.audinaut.domain.MusicFolder; +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.ProgressListener; +import github.nvllsvm.audinaut.util.Util; +import github.nvllsvm.audinaut.view.UpdateView; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +public class SelectArtistFragment extends SelectRecyclerFragment implements ArtistAdapter.OnMusicFolderChanged { + private static final String TAG = SelectArtistFragment.class.getSimpleName(); + + private List musicFolders = null; + private List entries; + private String groupId; + private String groupName; + + public SelectArtistFragment() { + super(); + } + + @Override + public void onCreate(Bundle bundle) { + super.onCreate(bundle); + + if(bundle != null) { + musicFolders = (List) 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) { + if(args.getBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, false)) { + groupId = args.getString(Constants.INTENT_EXTRA_NAME_ID); + groupName = args.getString(Constants.INTENT_EXTRA_NAME_NAME); + + if (groupName != null) { + setTitle(groupName); + context.invalidateOptionsMenu(); + } + } + } + + super.onCreateView(inflater, container, bundle); + + return rootView; + } + + @Override + public void onCreateContextMenu(Menu menu, MenuInflater menuInflater, UpdateView updateView, Serializable item) { + onCreateContextMenuSupport(menu, menuInflater, updateView, item); + recreateContextMenu(menu); + } + + @Override + public boolean onContextItemSelected(MenuItem menuItem, UpdateView updateView, Serializable item) { + return onContextItemSelected(menuItem, item); + } + + @Override + public void onItemClicked(UpdateView updateView, Serializable item) { + SubsonicFragment fragment; + if(item instanceof Artist) { + Artist artist = (Artist) item; + + if ((Util.isFirstLevelArtist(context) || Util.isOffline(context) || Util.isTagBrowsing(context)) || 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 (!Util.isOffline(context)) { + args.putSerializable(Constants.INTENT_EXTRA_NAME_DIRECTORY, new Entry(artist)); + } + 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()); + args.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, true); + if (!Util.isOffline(context)) { + args.putSerializable(Constants.INTENT_EXTRA_NAME_DIRECTORY, new Entry(artist)); + } + + fragment.setArguments(args); + } + + replaceFragment(fragment); + } else { + Entry entry = (Entry) item; + onSongPress(entries, entry); + } + } + + @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 SectionAdapter getAdapter(List objects) { + return new ArtistAdapter(context, objects, musicFolders, this, this); + } + + @Override + public List getObjects(MusicService musicService, boolean refresh, ProgressListener listener) throws Exception { + List items; + 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); + } + } else { + musicFolders = null; + } + String musicFolderId = Util.getSelectedMusicFolderId(context); + + Indexes indexes = musicService.getIndexes(musicFolderId, refresh, context, listener); + indexes.sortChildren(context); + items = new ArrayList<>(indexes.getShortcuts().size() + indexes.getArtists().size()); + items.addAll(indexes.getShortcuts()); + items.addAll(indexes.getArtists()); + entries = indexes.getEntries(); + items.addAll(entries); + } else { + List artists = new ArrayList<>(); + items = new ArrayList<>(); + MusicDirectory dir = musicService.getMusicDirectory(groupId, groupName, refresh, context, listener); + for(Entry entry: dir.getChildren(true, false)) { + Artist artist = new Artist(); + artist.setId(entry.getId()); + artist.setName(entry.getTitle()); + artists.add(artist); + } + + Indexes indexes = new Indexes(0, new ArrayList(), artists); + indexes.sortChildren(context); + items.addAll(indexes.getArtists()); + + entries = dir.getChildren(false, true); + for(Entry entry: entries) { + items.add(entry); + } + } + + return items; + } + + @Override + public int getTitleResource() { + return groupId == null ? R.string.button_bar_browse : 0; + } + + @Override + public void setEmpty(boolean empty) { + super.setEmpty(empty); + + if(empty && !Util.isOffline(context)) { + objects.clear(); + recyclerView.setAdapter(new ArtistAdapter(context, objects, musicFolders, this, this)); + recyclerView.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 toggleFirstLevelArtist() { + Util.toggleFirstLevelArtist(context); + context.invalidateOptionsMenu(); + } + + @Override + public void onMusicFolderChanged(MusicFolder selectedFolder) { + String startMusicFolderId = Util.getSelectedMusicFolderId(context); + String musicFolderId = selectedFolder == null ? null : selectedFolder.getId(); + + if(!Util.equals(startMusicFolderId, musicFolderId)) { + Util.setSelectedMusicFolderId(context, musicFolderId); + context.invalidate(); + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/fragments/SelectDirectoryFragment.java b/app/src/main/java/github/nvllsvm/audinaut/fragments/SelectDirectoryFragment.java new file mode 100644 index 0000000..f47f79b --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/fragments/SelectDirectoryFragment.java @@ -0,0 +1,840 @@ +package github.nvllsvm.audinaut.fragments; + +import android.annotation.TargetApi; +import android.support.v7.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.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.text.Html; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.method.LinkMovementMethod; +import android.util.Log; +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.ImageButton; +import android.widget.ImageView; +import android.widget.RelativeLayout; +import android.widget.TextView; +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.adapter.AlphabeticalAlbumAdapter; +import github.nvllsvm.audinaut.adapter.EntryInfiniteGridAdapter; +import github.nvllsvm.audinaut.adapter.EntryGridAdapter; +import github.nvllsvm.audinaut.adapter.SectionAdapter; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.service.CachedMusicService; +import github.nvllsvm.audinaut.service.DownloadService; +import github.nvllsvm.audinaut.util.DrawableTint; +import github.nvllsvm.audinaut.util.ImageLoader; + +import java.io.Serializable; +import java.util.Iterator; +import java.util.List; + +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.service.MusicServiceFactory; +import github.nvllsvm.audinaut.service.OfflineException; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.LoadingTask; +import github.nvllsvm.audinaut.util.Pair; +import github.nvllsvm.audinaut.util.SilentBackgroundTask; +import github.nvllsvm.audinaut.util.TabBackgroundTask; +import github.nvllsvm.audinaut.util.UpdateHelper; +import github.nvllsvm.audinaut.util.UserUtil; +import github.nvllsvm.audinaut.util.Util; +import github.nvllsvm.audinaut.view.FastScroller; +import github.nvllsvm.audinaut.view.GridSpacingDecoration; +import github.nvllsvm.audinaut.view.MyLeadingMarginSpan2; +import github.nvllsvm.audinaut.view.RecyclingImageView; +import github.nvllsvm.audinaut.view.UpdateView; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import static github.nvllsvm.audinaut.domain.MusicDirectory.Entry; + +public class SelectDirectoryFragment extends SubsonicFragment implements SectionAdapter.OnItemClickedListener { + private static final String TAG = SelectDirectoryFragment.class.getSimpleName(); + + private RecyclerView recyclerView; + private FastScroller fastScroller; + private EntryGridAdapter entryGridAdapter; + private List albums; + private List entries; + private LoadTask currentTask; + + private SilentBackgroundTask updateCoverArtTask; + private ImageView coverArtView; + private Entry coverArtRep; + private String coverArtId; + + String id; + String name; + Entry directory; + String playlistId; + String playlistName; + boolean playlistOwner; + String albumListType; + String albumListExtra; + int albumListSize; + boolean refreshListing = 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) bundle.getSerializable(Constants.FRAGMENT_LIST); + albums = (List) bundle.getSerializable(Constants.FRAGMENT_LIST2); + if(albums == null) { + albums = new ArrayList<>(); + } + 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); + } + + @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); + Object shareObj = args.getSerializable(Constants.INTENT_EXTRA_NAME_SHARE); + 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); + + String childId = args.getString(Constants.INTENT_EXTRA_NAME_CHILD_ID); + if(childId != null) { + id = childId; + lookupParent = true; + } + if(entries == null) { + entries = (List) args.getSerializable(Constants.FRAGMENT_LIST); + albums = (List) args.getSerializable(Constants.FRAGMENT_LIST2); + + if(albums == null) { + albums = new ArrayList(); + } + } + } + + rootView = inflater.inflate(R.layout.abstract_recycler_fragment, container, false); + + refreshLayout = (SwipeRefreshLayout) rootView.findViewById(R.id.refresh_layout); + refreshLayout.setOnRefreshListener(this); + + if(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_LARGE_ALBUM_ART, true)) { + largeAlbums = true; + } + + recyclerView = (RecyclerView) rootView.findViewById(R.id.fragment_recycler); + recyclerView.setHasFixedSize(true); + fastScroller = (FastScroller) rootView.findViewById(R.id.fragment_fast_scroller); + setupScrollList(recyclerView); + setupLayoutManager(recyclerView, largeAlbums); + + if(entries == null) { + if(primaryFragment || secondaryFragment) { + load(false); + } else { + invalidated = true; + } + } else { + finishLoading(); + } + + if(name != null) { + setTitle(name); + } + + return rootView; + } + + @Override + public void setIsOnlyVisible(boolean isOnlyVisible) { + boolean update = this.isOnlyVisible != isOnlyVisible; + super.setIsOnlyVisible(isOnlyVisible); + if(update && entryGridAdapter != null) { + RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); + if(layoutManager instanceof GridLayoutManager) { + ((GridLayoutManager) layoutManager).setSpanCount(getRecyclerColumnCount()); + } + } + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) { + if(albumListType != null) { + menuInflater.inflate(R.menu.select_album_list, menu); + } else if(artist) { + menuInflater.inflate(R.menu.select_album, menu); + } else { + 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); + } + } + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_remove_playlist: + removeFromPlaylist(playlistId, playlistName, getSelectedIndexes()); + return true; + } + + return super.onOptionsItemSelected(item); + + } + + @Override + public void onCreateContextMenu(Menu menu, MenuInflater menuInflater, UpdateView updateView, Entry entry) { + onCreateContextMenuSupport(menu, menuInflater, updateView, entry); + if(!Util.isOffline(context) && (playlistId == null || !playlistOwner)) { + menu.removeItem(R.id.song_menu_remove_playlist); + } + + recreateContextMenu(menu); + } + @Override + public boolean onContextItemSelected(MenuItem menuItem, UpdateView updateView, Entry entry) { + if(onContextItemSelected(menuItem, entry)) { + return true; + } + + switch (menuItem.getItemId()) { + case R.id.song_menu_remove_playlist: + removeFromPlaylist(playlistId, playlistName, Arrays.asList(entries.indexOf(entry))); + break; + } + + return true; + } + + @Override + public void onItemClicked(UpdateView updateView, Entry entry) { + 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.isAlbum()) { + args.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, true); + } + fragment.setArguments(args); + + replaceFragment(fragment, true); + } else { + onSongPress(entries, entry, albumListType == null); + } + } + + @Override + protected void refresh(boolean refresh) { + load(refresh); + } + + @Override + protected boolean isShowArtistEnabled() { + return albumListType != null; + } + + private void load(boolean refresh) { + if(refreshListing) { + refresh = true; + } + + if(currentTask != null) { + currentTask.cancel(); + } + + recyclerView.setVisibility(View.INVISIBLE); + if (playlistId != null) { + getPlaylist(playlistId, playlistName, refresh); + } else if (albumListType != null) { + getAlbumList(albumListType, albumListSize, 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) { + 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 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 = getMusicDirectory(id, name, refresh, service, this); + List songs = new ArrayList(); + getSongsRecursively(root, songs); + root.replaceChildren(songs); + return root; + } + + private void getSongsRecursively(MusicDirectory parent, List 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 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 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, 50, context, this); + } + }.execute(); + } + + private void getAlbumList(final String albumListType, final int size, final boolean refresh) { + if ("newest".equals(albumListType)) { + setTitle(R.string.main_albums_newest); + } else if ("random".equals(albumListType)) { + setTitle(R.string.main_albums_random); + } else if ("recent".equals(albumListType)) { + setTitle(R.string.main_albums_recent); + } else if ("frequent".equals(albumListType)) { + setTitle(R.string.main_albums_frequent); + } else if("genres".equals(albumListType) || "years".equals(albumListType)) { + setTitle(albumListExtra); + } else if("alphabeticalByName".equals(albumListType)) { + setTitle(R.string.main_albums_alphabetical); + } if (MainFragment.SONGS_NEWEST.equals(albumListType)) { + setTitle(R.string.main_songs_newest); + } else if (MainFragment.SONGS_TOP_PLAYED.equals(albumListType)) { + setTitle(R.string.main_songs_top_played); + } else if (MainFragment.SONGS_RECENT.equals(albumListType)) { + setTitle(R.string.main_songs_recent); + } else if (MainFragment.SONGS_FREQUENT.equals(albumListType)) { + setTitle(R.string.main_songs_frequent); + } + + new LoadTask(true) { + @Override + protected MusicDirectory load(MusicService service) throws Exception { + MusicDirectory result; + if("genres".equals(albumListType) || "years".equals(albumListType)) { + result = service.getAlbumList(albumListType, albumListExtra, size, 0, refresh, 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 if(albumListType.indexOf(MainFragment.SONGS_LIST_PREFIX) != -1) { + result = service.getSongList(albumListType, size, 0, context, this); + } else { + result = service.getAlbumList(albumListType, size, 0, refresh, context, this); + } + return result; + } + }.execute(); + } + + private abstract class LoadTask extends TabBackgroundTask> { + 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 doInBackground() throws Throwable { + MusicService musicService = MusicServiceFactory.getMusicService(context); + MusicDirectory dir = load(musicService); + + albums = dir.getChildren(true, false); + entries = dir.getChildren(); + + // This isn't really an artist if no albums on it! + if(albums.size() == 0) { + artist = false; + } + + return new Pair<>(dir, true); + } + + @Override + protected void done(Pair result) { + finishLoading(); + currentTask = null; + } + + @Override + public void updateCache(int changeCode) { + if(entryGridAdapter != null && changeCode == CachedMusicService.CACHE_UPDATE_LIST) { + entryGridAdapter.notifyDataSetChanged(); + } else if(changeCode == CachedMusicService.CACHE_UPDATE_METADATA) { + if(coverArtView != null && coverArtRep != null && !Util.equals(coverArtRep.getCoverArt(), coverArtId)) { + synchronized (coverArtRep) { + if (updateCoverArtTask != null && updateCoverArtTask.isRunning()) { + updateCoverArtTask.cancel(); + } + updateCoverArtTask = getImageLoader().loadImage(coverArtView, coverArtRep, false, true); + coverArtId = coverArtRep.getCoverArt(); + } + } + } + } + } + + @Override + public SectionAdapter getCurrentAdapter() { + return entryGridAdapter; + } + + @Override + public GridLayoutManager.SpanSizeLookup getSpanSizeLookup(final GridLayoutManager gridLayoutManager) { + return new GridLayoutManager.SpanSizeLookup() { + @Override + public int getSpanSize(int position) { + int viewType = entryGridAdapter.getItemViewType(position); + if(viewType == EntryGridAdapter.VIEW_TYPE_SONG || viewType == EntryGridAdapter.VIEW_TYPE_HEADER || viewType == EntryInfiniteGridAdapter.VIEW_TYPE_LOADING) { + return gridLayoutManager.getSpanCount(); + } else { + return 1; + } + } + }; + } + + private void finishLoading() { + boolean validData = !entries.isEmpty() || !albums.isEmpty(); + if(!validData) { + setEmpty(true); + } + + if(validData) { + recyclerView.setVisibility(View.VISIBLE); + } + + if(albumListType == null) { + entryGridAdapter = new EntryGridAdapter(context, entries, getImageLoader(), largeAlbums); + entryGridAdapter.setRemoveFromPlaylist(playlistId != null); + } else { + if("alphabeticalByName".equals(albumListType)) { + entryGridAdapter = new AlphabeticalAlbumAdapter(context, entries, getImageLoader(), largeAlbums); + } else { + entryGridAdapter = new EntryInfiniteGridAdapter(context, entries, getImageLoader(), largeAlbums); + } + + // Setup infinite loading based on scrolling + final EntryInfiniteGridAdapter infiniteGridAdapter = (EntryInfiniteGridAdapter) entryGridAdapter; + infiniteGridAdapter.setData(albumListType, albumListExtra, albumListSize); + + recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrollStateChanged(RecyclerView recyclerView, int newState) { + super.onScrollStateChanged(recyclerView, newState); + } + + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + super.onScrolled(recyclerView, dx, dy); + + RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); + int totalItemCount = layoutManager.getItemCount(); + int lastVisibleItem; + if(layoutManager instanceof GridLayoutManager) { + lastVisibleItem = ((GridLayoutManager) layoutManager).findLastVisibleItemPosition(); + } else if(layoutManager instanceof LinearLayoutManager) { + lastVisibleItem = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition(); + } else { + return; + } + + if(totalItemCount > 0 && lastVisibleItem >= totalItemCount - 2) { + infiniteGridAdapter.loadMore(); + } + } + }); + } + entryGridAdapter.setOnItemClickedListener(this); + // Always show artist if this is not a artist we are viewing + if(!artist) { + entryGridAdapter.setShowArtist(true); + } + if(topTracks) { + entryGridAdapter.setShowAlbum(true); + } + + int scrollToPosition = -1; + if(lookupEntry != null) { + for(int i = 0; i < entries.size(); i++) { + if(lookupEntry.equals(entries.get(i).getTitle())) { + scrollToPosition = i; + entryGridAdapter.addSelected(entries.get(i)); + lookupEntry = null; + break; + } + } + } + + recyclerView.setAdapter(entryGridAdapter); + fastScroller.attachRecyclerView(recyclerView); + context.supportInvalidateOptionsMenu(); + + if(scrollToPosition != -1) { + recyclerView.scrollToPosition(scrollToPosition); + } + + 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, false); + } + } + + @Override + protected void playNow(final boolean shuffle, final boolean append, final boolean playNext) { + List songs = getSelectedEntries(); + if(!songs.isEmpty()) { + download(songs, append, false, !append, playNext, shuffle); + entryGridAdapter.clearSelected(); + } else { + playAll(shuffle, append, playNext); + } + } + private void playAll(final boolean shuffle, final boolean append, final boolean playNext) { + boolean hasSubFolders = albums != null && !albums.isEmpty(); + + if (hasSubFolders && id != null) { + downloadRecursively(id, false, append, !append, shuffle, false, playNext); + } else if(hasSubFolders && albumListType != null) { + downloadRecursively(albums, shuffle, append, playNext); + } else { + download(entries, append, false, !append, playNext, shuffle); + } + } + + private List getSelectedIndexes() { + List selected = entryGridAdapter.getSelected(); + List indexes = new ArrayList(); + + for(Entry entry: selected) { + indexes.add(entries.indexOf(entry)); + } + + return indexes; + } + + @Override + protected void downloadBackground(final boolean save) { + List songs = getSelectedEntries(); + if(playlistId != null) { + songs = entries; + } + + if(songs.isEmpty()) { + // Get both songs and albums + downloadRecursively(id, save, false, false, false, true); + } else { + downloadBackground(save, songs); + } + } + @Override + protected void downloadBackground(final boolean save, final List entries) { + if (getDownloadService() == null) { + return; + } + + warnIfStorageUnavailable(); + RecursiveLoader onValid = new RecursiveLoader(context) { + @Override + protected Boolean doInBackground() throws Throwable { + getSongsRecursively(entries, true); + getDownloadService().downloadBackground(songs, save); + return null; + } + + @Override + protected void done(Boolean result) { + Util.toast(context, context.getResources().getQuantityString(R.plurals.select_album_n_songs_downloading, songs.size(), songs.size())); + } + }; + } + + @Override + protected void download(List entries, boolean append, boolean save, boolean autoplay, boolean playNext, boolean shuffle) { + download(entries, append, save, autoplay, playNext, shuffle, playlistName, playlistId); + } + + @Override + protected void delete() { + List songs = getSelectedEntries(); + if(songs.isEmpty()) { + for(Entry entry: entries) { + if(entry.isDirectory()) { + deleteRecursively(entry); + } else { + songs.add(entry); + } + } + } + if (getDownloadService() != null) { + getDownloadService().delete(songs); + } + } + + public void removeFromPlaylist(final String id, final String name, final List indexes) { + new LoadingTask(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(Integer index: indexes) { + entryGridAdapter.removeAt(index); + } + 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) { + msg = getErrorMessage(error); + } else { + msg = context.getResources().getString(R.string.updated_playlist_error, name) + " " + getErrorMessage(error); + } + + Util.toast(context, msg, false); + } + }.execute(); + } + + 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 View createHeader() { + View header = LayoutInflater.from(context).inflate(R.layout.select_album_header, null, false); + + setupCoverArt(header); + setupTextDisplay(header); + + return header; + } + + private void setupCoverArt(View header) { + setupCoverArtImpl((RecyclingImageView) header.findViewById(R.id.select_album_art)); + } + private void setupCoverArtImpl(RecyclingImageView coverArtView) { + final ImageLoader imageLoader = getImageLoader(); + + if(entries.size() > 0) { + coverArtRep = null; + this.coverArtView = coverArtView; + for (int i = 0; (i < 3) && (coverArtRep == null || coverArtRep.getCoverArt() == null); i++) { + coverArtRep = entries.get(random.nextInt(entries.size())); + } + + coverArtView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (coverArtRep == null || coverArtRep.getCoverArt() == null) { + return; + } + + AlertDialog.Builder builder = new AlertDialog.Builder(context); + ImageView fullScreenView = new ImageView(context); + imageLoader.loadImage(fullScreenView, coverArtRep, 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(); + } + }); + synchronized (coverArtRep) { + coverArtId = coverArtRep.getCoverArt(); + updateCoverArtTask = imageLoader.loadImage(coverArtView, coverArtRep, false, true); + } + } + + coverArtView.setOnInvalidated(new RecyclingImageView.OnInvalidated() { + @Override + public void onInvalidated(RecyclingImageView imageView) { + setupCoverArtImpl(imageView); + } + }); + } + 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(name != null) { + titleView.setText(name); + } + + int songCount = 0; + + Set artists = new HashSet(); + Set years = new HashSet(); + 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 (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); + String s = context.getResources().getQuantityString(R.plurals.select_album_n_songs, songCount, songCount); + songCountView.setText(s.toUpperCase()); + songLengthView.setText(Util.formatDuration(totalDuration)); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/fragments/SelectGenreFragment.java b/app/src/main/java/github/nvllsvm/audinaut/fragments/SelectGenreFragment.java new file mode 100644 index 0000000..ca93101 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/fragments/SelectGenreFragment.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 . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.fragments; + +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.adapter.SectionAdapter; +import github.nvllsvm.audinaut.domain.Genre; +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.ProgressListener; +import github.nvllsvm.audinaut.adapter.GenreAdapter; +import github.nvllsvm.audinaut.view.UpdateView; + +import java.util.List; + +public class SelectGenreFragment extends SelectRecyclerFragment { + private static final String TAG = SelectGenreFragment.class.getSimpleName(); + + @Override + public int getOptionsMenu() { + return R.menu.empty; + } + + @Override + public SectionAdapter getAdapter(List objs) { + return new GenreAdapter(context, objs, this); + } + + @Override + public List 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 onItemClicked(UpdateView updateView, Genre genre) { + 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); + } + + @Override + public void onCreateContextMenu(Menu menu, MenuInflater menuInflater, UpdateView updateView, Genre item) {} + + @Override + public boolean onContextItemSelected(MenuItem menuItem, UpdateView updateView, Genre item) { + return false; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/fragments/SelectPlaylistFragment.java b/app/src/main/java/github/nvllsvm/audinaut/fragments/SelectPlaylistFragment.java new file mode 100644 index 0000000..2cc6730 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/fragments/SelectPlaylistFragment.java @@ -0,0 +1,341 @@ +package github.nvllsvm.audinaut.fragments; + +import android.support.v7.app.AlertDialog; +import android.content.DialogInterface; +import android.content.res.Resources; +import android.os.Bundle; +import android.support.v4.app.FragmentTransaction; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.CheckBox; +import android.widget.EditText; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.adapter.SectionAdapter; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.Playlist; +import github.nvllsvm.audinaut.service.DownloadFile; +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.service.MusicServiceFactory; +import github.nvllsvm.audinaut.service.OfflineException; +import github.nvllsvm.audinaut.util.ProgressListener; +import github.nvllsvm.audinaut.util.SyncUtil; +import github.nvllsvm.audinaut.util.CacheCleaner; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.LoadingTask; +import github.nvllsvm.audinaut.util.UserUtil; +import github.nvllsvm.audinaut.util.Util; +import github.nvllsvm.audinaut.adapter.PlaylistAdapter; +import github.nvllsvm.audinaut.view.UpdateView; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class SelectPlaylistFragment extends SelectRecyclerFragment { + private static final String TAG = SelectPlaylistFragment.class.getSimpleName(); + + @Override + public void onCreate(Bundle bundle) { + super.onCreate(bundle); + if (Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_LARGE_ALBUM_ART, true)) { + largeAlbums = true; + } + } + + @Override + public void onCreateContextMenu(Menu menu, MenuInflater menuInflater, UpdateView updateView, Playlist playlist) { + if (Util.isOffline(context)) { + menuInflater.inflate(R.menu.select_playlist_context_offline, menu); + } + else { + menuInflater.inflate(R.menu.select_playlist_context, menu); + + 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, UpdateView updateView, Playlist playlist) { + 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_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; + } + + return false; + } + + @Override + public int getOptionsMenu() { + return R.menu.abstract_top_menu; + } + + @Override + public SectionAdapter getAdapter(List playlists) { + List mine = new ArrayList<>(); + + String currentUsername = UserUtil.getCurrentUsername(context); + for(Playlist playlist: playlists) { + if(playlist.getOwner() == null || playlist.getOwner().equals(currentUsername)) { + mine.add(playlist); + } + } + + return new PlaylistAdapter(context, playlists, getImageLoader(), largeAlbums, this); + } + + @Override + public List getObjects(MusicService musicService, boolean refresh, ProgressListener listener) throws Exception { + List 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 onItemClicked(UpdateView updateView, Playlist playlist) { + 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((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); + } + + @Override + public void onFinishRefresh() { + Bundle args = getArguments(); + if(args != null) { + String playlistId = args.getString(Constants.INTENT_EXTRA_NAME_ID, null); + if (playlistId != null && objects != null) { + for (Playlist playlist : objects) { + if (playlistId.equals(playlist.getId())) { + onItemClicked(null, playlist); + break; + } + } + } + } + } + + 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(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.removeItem(playlist); + 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) { + 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) { + List headers = new ArrayList<>(); + List details = new ArrayList<>(); + + headers.add(R.string.details_title); + details.add(playlist.getName()); + + if(playlist.getOwner() != null) { + headers.add(R.string.details_owner); + details.add(playlist.getOwner()); + } + + if(playlist.getComment() != null) { + headers.add(R.string.details_comments); + details.add(playlist.getComment()); + } + + headers.add(R.string.details_song_count); + details.add(playlist.getSongCount()); + + if(playlist.getDuration() != null) { + headers.add(R.string.details_length); + details.add(Util.formatDuration(playlist.getDuration())); + } + + if(playlist.getPublic() != null) { + headers.add(R.string.details_public); + details.add(Util.formatBoolean(context, playlist.getPublic())); + } + + if(playlist.getCreated() != null) { + headers.add(R.string.details_created); + details.add(Util.formatDate(playlist.getCreated())); + } + if(playlist.getChanged() != null) { + headers.add(R.string.details_updated); + details.add(Util.formatDate(playlist.getChanged())); + } + + Util.showDetailsDialog(context, R.string.details_title_playlist, headers, details); + } + + 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(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) { + 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(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/nvllsvm/audinaut/fragments/SelectRecyclerFragment.java b/app/src/main/java/github/nvllsvm/audinaut/fragments/SelectRecyclerFragment.java new file mode 100644 index 0000000..61ff449 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/fragments/SelectRecyclerFragment.java @@ -0,0 +1,219 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.fragments; + +import android.app.SearchManager; +import android.app.SearchableInfo; +import android.content.Context; +import android.os.Bundle; +import android.support.v4.view.MenuItemCompat; +import android.support.v4.widget.SwipeRefreshLayout; +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.SearchView; +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 java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.adapter.SectionAdapter; +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.service.MusicServiceFactory; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.ProgressListener; +import github.nvllsvm.audinaut.util.TabBackgroundTask; +import github.nvllsvm.audinaut.view.FastScroller; + +public abstract class SelectRecyclerFragment extends SubsonicFragment implements SectionAdapter.OnItemClickedListener { + private static final String TAG = SelectRecyclerFragment.class.getSimpleName(); + protected RecyclerView recyclerView; + protected FastScroller fastScroller; + protected SectionAdapter adapter; + protected UpdateTask currentTask; + protected List objects; + protected boolean serialize = true; + protected boolean largeAlbums = false; + protected boolean pullToRefresh = true; + protected boolean backgroundUpdate = true; + + @Override + public void onCreate(Bundle bundle) { + super.onCreate(bundle); + + if(bundle != null && serialize) { + objects = (List) 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_recycler_fragment, container, false); + + refreshLayout = (SwipeRefreshLayout) rootView.findViewById(R.id.refresh_layout); + refreshLayout.setOnRefreshListener(this); + + recyclerView = (RecyclerView) rootView.findViewById(R.id.fragment_recycler); + fastScroller = (FastScroller) rootView.findViewById(R.id.fragment_fast_scroller); + setupLayoutManager(); + + if(pullToRefresh) { + setupScrollList(recyclerView); + } else { + refreshLayout.setEnabled(false); + } + + if(objects == null) { + refresh(false); + } else { + recyclerView.setAdapter(adapter = getAdapter(objects)); + } + + return rootView; + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) { + if(!primaryFragment) { + return; + } + + menuInflater.inflate(getOptionsMenu(), menu); + onFinishSetupOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + return super.onOptionsItemSelected(item); + } + + @Override + public void setIsOnlyVisible(boolean isOnlyVisible) { + boolean update = this.isOnlyVisible != isOnlyVisible; + super.setIsOnlyVisible(isOnlyVisible); + if(update && adapter != null) { + RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); + if(layoutManager instanceof GridLayoutManager) { + ((GridLayoutManager) layoutManager).setSpanCount(getRecyclerColumnCount()); + } + } + } + + @Override + protected void refresh(final boolean refresh) { + int titleRes = getTitleResource(); + if(titleRes != 0) { + setTitle(getTitleResource()); + } + if(backgroundUpdate) { + recyclerView.setVisibility(View.GONE); + } + + // Cancel current running task before starting another one + if(currentTask != null) { + currentTask.cancel(); + } + + currentTask = new UpdateTask(this, refresh); + + if(backgroundUpdate) { + currentTask.execute(); + } else { + objects = new ArrayList(); + + try { + objects = getObjects(null, refresh, null); + } catch (Exception x) { + Log.e(TAG, "Failed to load", x); + } + + currentTask.done(objects); + } + } + + public SectionAdapter getCurrentAdapter() { + return adapter; + } + + private void setupLayoutManager() { + setupLayoutManager(recyclerView, largeAlbums); + } + + public abstract int getOptionsMenu(); + public abstract SectionAdapter getAdapter(List objs); + public abstract List getObjects(MusicService musicService, boolean refresh, ProgressListener listener) throws Exception; + public abstract int getTitleResource(); + + public void onFinishRefresh() { + + } + + private class UpdateTask extends TabBackgroundTask> { + private boolean refresh; + + public UpdateTask(SubsonicFragment fragment, boolean refresh) { + super(fragment); + this.refresh = refresh; + } + + @Override + public List doInBackground() throws Exception { + MusicService musicService = MusicServiceFactory.getMusicService(context); + + objects = new ArrayList(); + + try { + objects = getObjects(musicService, refresh, this); + } catch (Exception x) { + Log.e(TAG, "Failed to load", x); + } + + return objects; + } + + @Override + public void done(List result) { + if (result != null && !result.isEmpty()) { + recyclerView.setAdapter(adapter = getAdapter(result)); + if(!fastScroller.isAttached()) { + fastScroller.attachRecyclerView(recyclerView); + } + + onFinishRefresh(); + recyclerView.setVisibility(View.VISIBLE); + } else { + setEmpty(true); + } + + currentTask = null; + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/fragments/SelectYearFragment.java b/app/src/main/java/github/nvllsvm/audinaut/fragments/SelectYearFragment.java new file mode 100644 index 0000000..054c4bc --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/fragments/SelectYearFragment.java @@ -0,0 +1,88 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.fragments; + +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; + +import java.util.ArrayList; +import java.util.List; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.adapter.BasicListAdapter; +import github.nvllsvm.audinaut.adapter.SectionAdapter; +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.ProgressListener; +import github.nvllsvm.audinaut.view.UpdateView; + +public class SelectYearFragment extends SelectRecyclerFragment { + + public SelectYearFragment() { + super(); + pullToRefresh = false; + serialize = false; + backgroundUpdate = false; + } + + @Override + public int getOptionsMenu() { + return R.menu.empty; + } + + @Override + public SectionAdapter getAdapter(List objs) { + return new BasicListAdapter(context, objs, this); + } + + @Override + public List getObjects(MusicService musicService, boolean refresh, ProgressListener listener) throws Exception { + List decades = new ArrayList<>(); + for(int i = 2010; i >= 1800; i -= 10) { + decades.add(String.valueOf(i)); + } + + return decades; + } + + @Override + public int getTitleResource() { + return R.string.main_albums_year; + } + + @Override + public void onItemClicked(UpdateView updateView, String decade) { + 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, decade); + fragment.setArguments(args); + + replaceFragment(fragment); + } + + @Override + public void onCreateContextMenu(Menu menu, MenuInflater menuInflater, UpdateView updateView, String item) {} + + @Override + public boolean onContextItemSelected(MenuItem menuItem, UpdateView updateView, String item) { + return false; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/fragments/SettingsFragment.java b/app/src/main/java/github/nvllsvm/audinaut/fragments/SettingsFragment.java new file mode 100644 index 0000000..a75cbd3 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/fragments/SettingsFragment.java @@ -0,0 +1,806 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2014 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.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.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.service.DownloadService; +import github.nvllsvm.audinaut.service.HeadphoneListenerService; +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.service.MusicServiceFactory; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.FileUtil; +import github.nvllsvm.audinaut.util.LoadingTask; +import github.nvllsvm.audinaut.util.SyncUtil; +import github.nvllsvm.audinaut.util.Util; +import github.nvllsvm.audinaut.view.CacheLocationPreference; +import github.nvllsvm.audinaut.view.ErrorDialog; + +public class SettingsFragment extends PreferenceCompatFragment implements SharedPreferences.OnSharedPreferenceChangeListener { + private final static String TAG = SettingsFragment.class.getSimpleName(); + + private final Map serverSettings = new LinkedHashMap(); + private boolean testingConnection; + private ListPreference theme; + private ListPreference maxBitrateWifi; + private ListPreference maxBitrateMobile; + private ListPreference networkTimeout; + private CacheLocationPreference cacheLocation; + private ListPreference preloadCountWifi; + private ListPreference preloadCountMobile; + private ListPreference keepPlayedCount; + private ListPreference tempLoss; + private ListPreference pauseDisconnect; + private Preference addServerPreference; + private PreferenceCategory serversCategory; + private ListPreference songPressAction; + private ListPreference syncInterval; + private CheckBoxPreference syncEnabled; + private CheckBoxPreference syncWifi; + private CheckBoxPreference syncNotification; + 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 int serverCount = 3; + private SharedPreferences settings; + private DecimalFormat megabyteFromat; + + @Override + public void onCreate(Bundle bundle) { + super.onCreate(bundle); + + int instance = this.getArguments().getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, -1); + if (instance != -1) { + PreferenceScreen preferenceScreen = expandServer(instance); + setPreferenceScreen(preferenceScreen); + + serverSettings.put(Integer.toString(instance), new ServerSettings(instance)); + onInitPreferences(preferenceScreen); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + + SharedPreferences prefs = Util.getPreferences(context); + prefs.unregisterOnSharedPreferenceChangeListener(this); + } + + @Override + protected void onStartNewFragment(String name) { + SettingsFragment newFragment = new SettingsFragment(); + Bundle args = new Bundle(); + + int xml = 0; + if("appearance".equals(name)) { + xml = R.xml.settings_appearance; + } else if("cache".equals(name)) { + xml = R.xml.settings_cache; + } else if("playback".equals(name)) { + xml = R.xml.settings_playback; + } else if("servers".equals(name)) { + xml = R.xml.settings_servers; + } + + if(xml != 0) { + args.putInt(Constants.INTENT_EXTRA_FRAGMENT_TYPE, xml); + newFragment.setArguments(args); + replaceFragment(newFragment); + } + } + + @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_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(); + } + + @Override + protected void onInitPreferences(PreferenceScreen preferenceScreen) { + this.setTitle(preferenceScreen.getTitle()); + + 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); + networkTimeout = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_NETWORK_TIMEOUT); + cacheLocation = (CacheLocationPreference) 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); + keepPlayedCount = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_KEEP_PLAYED_CNT); + 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); + songPressAction = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_SONG_PRESS_ACTION); + 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); + 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); + + settings = Util.getPreferences(context); + serverCount = settings.getInt(Constants.PREFERENCES_KEY_SERVER_COUNT, 1); + + if(cacheSize != null) { + 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(context, false) { + @Override + protected Void doInBackground() throws Throwable { + FileUtil.deleteMusicDirectory(context); + FileUtil.deleteSerializedCache(context); + FileUtil.deleteArtworkCache(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; + } + }); + } + + if(syncEnabled != null) { + 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); + + 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); + + return true; + } + }); + } + + if(serversCategory != null) { + addServerPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + serverCount++; + int instance = 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.putString(Constants.PREFERENCES_KEY_SERVER_URL + instance, "http://yourhost"); + editor.putString(Constants.PREFERENCES_KEY_SERVER_NAME + instance, getResources().getString(R.string.settings_server_unused)); + editor.commit(); + + ServerSettings ss = new ServerSettings(instance); + serverSettings.put(String.valueOf(instance), ss); + ss.update(); + + return true; + } + }); + + serversCategory.setOrderingAsAdded(false); + for (int i = 1; i <= serverCount; i++) { + serversCategory.addPreference(addServer(i)); + serverSettings.put(String.valueOf(i), new ServerSettings(i)); + } + } + + 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; + } + + if(theme != null) { + theme.setSummary(theme.getEntry()); + } + + if(cacheSize != null) { + maxBitrateWifi.setSummary(maxBitrateWifi.getEntry()); + maxBitrateMobile.setSummary(maxBitrateMobile.getEntry()); + networkTimeout.setSummary(networkTimeout.getEntry()); + cacheLocation.setSummary(cacheLocation.getText()); + preloadCountWifi.setSummary(preloadCountWifi.getEntry()); + preloadCountMobile.setSummary(preloadCountMobile.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(keepPlayedCount != null) { + keepPlayedCount.setSummary(keepPlayedCount.getEntry()); + tempLoss.setSummary(tempLoss.getEntry()); + pauseDisconnect.setSummary(pauseDisconnect.getEntry()); + songPressAction.setSummary(songPressAction.getEntry()); + + 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()); + } + + if(syncEnabled != null) { + syncInterval.setSummary(syncInterval.getEntry()); + + if(syncEnabled.isChecked()) { + if(!syncInterval.isEnabled()) { + syncInterval.setEnabled(true); + syncWifi.setEnabled(true); + syncNotification.setEnabled(true); + syncMostRecent.setEnabled(true); + } + } else { + if(syncInterval.isEnabled()) { + syncInterval.setEnabled(false); + syncWifi.setEnabled(false); + syncNotification.setEnabled(false); + syncMostRecent.setEnabled(false); + } + } + } + + for (ServerSettings ss : serverSettings.values()) { + ss.update(); + } + } + public void checkForRemoved() { + for (ServerSettings ss : serverSettings.values()) { + if(!ss.update()) { + serversCategory.removePreference(ss.getScreen()); + serverCount--; + } + } + } + + private PreferenceScreen addServer(final int instance) { + final PreferenceScreen screen = this.getPreferenceManager().createPreferenceScreen(context); + screen.setKey(Constants.PREFERENCES_KEY_SERVER_KEY + instance); + screen.setOrder(instance); + + screen.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + SettingsFragment newFragment = new SettingsFragment(); + + Bundle args = new Bundle(); + args.putInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, instance); + newFragment.setArguments(args); + + replaceFragment(newFragment); + return false; + } + }); + + return screen; + } + + private PreferenceScreen expandServer(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 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); + + // Don't use Util.getActiveServer since it is 0 if offline + int activeServer = Util.getPreferences(context).getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1); + 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(); + + removeCurrent(); + + SubsonicFragment parentFragment = context.getCurrentFragment(); + if(parentFragment instanceof SettingsFragment) { + SettingsFragment serverSelectionFragment = (SettingsFragment) parentFragment; + serverSelectionFragment.checkForRemoved(); + } + } + }); + + 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(serverTestConnectionPreference); + screen.addPreference(serverOpenBrowser); + screen.addPreference(serverRemoveServerPreference); + + 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 (!hide && 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(); + + if(cacheLocation != null) { + cacheLocation.setSummary(defaultPath); + cacheLocation.setText(defaultPath); + } + } + + // Clear download queue. + DownloadService downloadService = DownloadService.getInstance(); + downloadService.clear(); + } + } + + private void testConnection(final int instance) { + LoadingTask task = new LoadingTask(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 true; + } finally { + musicService.setInstance(null); + testingConnection = false; + } + } + + @Override + protected void done(Boolean licenseValid) { + Util.toast(context, R.string.settings_testing_ok); + } + + @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 int instance; + private EditTextPreference serverName; + private EditTextPreference serverUrl; + private EditTextPreference serverLocalNetworkSSID; + private EditTextPreference serverInternalUrl; + private EditTextPreference username; + private PreferenceScreen screen; + + private ServerSettings(int instance) { + this.instance = instance; + screen = (PreferenceScreen) SettingsFragment.this.findPreference(Constants.PREFERENCES_KEY_SERVER_KEY + 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); + + if(serverName != null) { + 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 PreferenceScreen getScreen() { + return screen; + } + + public boolean update() { + SharedPreferences prefs = Util.getPreferences(context); + + if(prefs.contains(Constants.PREFERENCES_KEY_SERVER_NAME + instance)) { + if (serverName != null) { + serverName.setSummary(serverName.getText()); + serverUrl.setSummary(serverUrl.getText()); + serverLocalNetworkSSID.setSummary(serverLocalNetworkSSID.getText()); + serverInternalUrl.setSummary(serverInternalUrl.getText()); + username.setSummary(username.getText()); + + setTitle(serverName.getText()); + } + + + String title = prefs.getString(Constants.PREFERENCES_KEY_SERVER_NAME + instance, null); + String summary = prefs.getString(Constants.PREFERENCES_KEY_SERVER_URL + instance, null); + + if (title != null) { + screen.setTitle(title); + } else { + screen.setTitle(R.string.settings_server_unused); + } + if (summary != null) { + screen.setSummary(summary); + } + + return true; + } else { + return false; + } + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/fragments/SubsonicFragment.java b/app/src/main/java/github/nvllsvm/audinaut/fragments/SubsonicFragment.java new file mode 100644 index 0000000..f4baa44 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/fragments/SubsonicFragment.java @@ -0,0 +1,1633 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.fragments; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.app.SearchManager; +import android.app.SearchableInfo; +import android.support.v4.view.MenuItemCompat; +import android.support.v7.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.Build; +import android.os.Bundle; +import android.os.StatFs; +import android.support.v4.app.Fragment; +import android.support.v4.widget.SwipeRefreshLayout; +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.SearchView; +import android.util.Log; +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.ArrayAdapter; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.TextView; +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.activity.SubsonicActivity; +import github.nvllsvm.audinaut.activity.SubsonicFragmentActivity; +import github.nvllsvm.audinaut.adapter.SectionAdapter; +import github.nvllsvm.audinaut.domain.Artist; +import github.nvllsvm.audinaut.domain.Genre; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.Playlist; +import github.nvllsvm.audinaut.service.DownloadFile; +import github.nvllsvm.audinaut.service.DownloadService; +import github.nvllsvm.audinaut.service.MediaStoreService; +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.service.MusicServiceFactory; +import github.nvllsvm.audinaut.service.OfflineException; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.FileUtil; +import github.nvllsvm.audinaut.util.ImageLoader; +import github.nvllsvm.audinaut.util.MenuUtil; +import github.nvllsvm.audinaut.util.ProgressListener; +import github.nvllsvm.audinaut.util.SilentBackgroundTask; +import github.nvllsvm.audinaut.util.LoadingTask; +import github.nvllsvm.audinaut.util.SongDBHandler; +import github.nvllsvm.audinaut.util.UpdateHelper; +import github.nvllsvm.audinaut.util.UserUtil; +import github.nvllsvm.audinaut.util.Util; +import github.nvllsvm.audinaut.view.AlbumView; +import github.nvllsvm.audinaut.view.ArtistEntryView; +import github.nvllsvm.audinaut.view.ArtistView; +import github.nvllsvm.audinaut.view.GridSpacingDecoration; +import github.nvllsvm.audinaut.view.PlaylistSongView; +import github.nvllsvm.audinaut.view.SongView; +import github.nvllsvm.audinaut.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.nvllsvm.audinaut.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 isOnlyVisible = true; + protected boolean alwaysFullscreen = false; + protected boolean alwaysStartFullscreen = false; + protected boolean invalidated = false; + protected static Random random = new Random(); + protected GestureDetector gestureScanner; + protected boolean artist = false; + protected boolean artistOverride = false; + protected SwipeRefreshLayout refreshLayout; + protected boolean firstRun; + protected MenuItem searchItem; + protected SearchView searchView; + + 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; + } + + protected void onFinishSetupOptionsMenu(final Menu menu) { + searchItem = menu.findItem(R.id.menu_global_search); + if(searchItem != null) { + searchView = (SearchView) MenuItemCompat.getActionView(searchItem); + SearchManager searchManager = (SearchManager) context.getSystemService(Context.SEARCH_SERVICE); + SearchableInfo searchableInfo = searchManager.getSearchableInfo(context.getComponentName()); + if(searchableInfo == null) { + Log.w(TAG, "Failed to get SearchableInfo"); + } else { + searchView.setSearchableInfo(searchableInfo); + } + + String currentQuery = getCurrentQuery(); + if(currentQuery != null) { + searchView.setOnSearchClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + searchView.setQuery(getCurrentQuery(), false); + } + }); + } + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_global_shuffle: + onShuffleRequested(); + return true; + case R.id.menu_refresh: + refresh(); + return true; + 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); + clearSelected(); + return true; + case R.id.menu_cache: + downloadBackground(true); + clearSelected(); + return true; + case R.id.menu_delete: + delete(); + clearSelected(); + return true; + case R.id.menu_add_playlist: + List songs = getSelectedEntries(); + addToPlaylist(songs); + clearSelected(); + return true; + } + + return false; + } + + public void onCreateContextMenuSupport(Menu menu, MenuInflater menuInflater, UpdateView updateView, Object selected) { + if(selected instanceof Entry) { + Entry entry = (Entry) selected; + if (entry.isDirectory()) { + if(Util.isOffline(context)) { + menuInflater.inflate(R.menu.select_album_context_offline, menu); + } + else { + menuInflater.inflate(R.menu.select_album_context, menu); + } + } else { + if(Util.isOffline(context)) { + menuInflater.inflate(R.menu.select_song_context_offline, menu); + } + else { + menuInflater.inflate(R.menu.select_song_context, menu); + + + String songPressAction = Util.getSongPressAction(context); + if(!"next".equals(songPressAction) && !"last".equals(songPressAction)) { + menu.setGroupVisible(R.id.hide_play_now, false); + } + } + } + + if(!isShowArtistEnabled() || (!Util.isTagBrowsing(context) && entry.getParent() == null) || (Util.isTagBrowsing(context) && entry.getArtistId() == null)) { + menu.setGroupVisible(R.id.hide_show_artist, false); + } + } else if(selected instanceof Artist) { + Artist artist = (Artist) selected; + if(Util.isOffline(context)) { + menuInflater.inflate(R.menu.select_artist_context_offline, menu); + } else { + menuInflater.inflate(R.menu.select_artist_context, menu); + } + } + + MenuUtil.hideMenuItems(context, menu, updateView); + } + + protected void recreateContextMenu(Menu menu) { + List menuItems = new ArrayList(); + 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()); + } + } + + // For reverting specific removals: https://github.com/daneren2005/Subsonic/commit/fbd1a68042dfc3601eaa0a9e37b3957bbdd51420 + public boolean onContextItemSelected(MenuItem menuItem, Object selectedItem) { + Artist artist = selectedItem instanceof Artist ? (Artist) selectedItem : null; + Entry entry = selectedItem instanceof Entry ? (Entry) selectedItem : null; + if(selectedItem instanceof DownloadFile) { + entry = ((DownloadFile) selectedItem).getSong(); + } + List songs = new ArrayList(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.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_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.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: + deleteSongs(songs); + break; + case R.id.song_menu_add_playlist: + addToPlaylist(songs); + break; + case R.id.song_menu_info: + displaySongInfo(entry); + break; + case R.id.song_menu_show_album: + showAlbum((Entry) selectedItem); + break; + case R.id.song_menu_show_artist: + showArtist((Entry) selectedItem); + 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 void replaceExistingFragment(SubsonicFragment fragment) { + context.replaceExistingFragment(fragment, fragment.getSupportTag()); + } + public void removeCurrent() { + context.removeCurrent(); + } + + 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 setIsOnlyVisible(boolean isOnlyVisible) { + this.isOnlyVisible = isOnlyVisible; + } + public boolean isAlwaysFullscreen() { + return alwaysFullscreen; + } + public boolean isAlwaysStartFullscreen() { + return alwaysStartFullscreen; + } + + 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(); + } + + 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); + } + }); + } + } + protected void setupScrollList(final RecyclerView recyclerView) { + if(!context.isTouchscreen()) { + refreshLayout.setEnabled(false); + } else { + recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrollStateChanged(RecyclerView recyclerView, int newState) { + super.onScrollStateChanged(recyclerView, newState); + } + + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + refreshLayout.setEnabled(!recyclerView.canScrollVertically(-1)); + } + }); + } + } + + public void setupLayoutManager(RecyclerView recyclerView, boolean largeAlbums) { + recyclerView.setLayoutManager(getLayoutManager(recyclerView, largeAlbums)); + } + public RecyclerView.LayoutManager getLayoutManager(RecyclerView recyclerView, boolean largeCells) { + if(largeCells) { + return getGridLayoutManager(recyclerView); + } else { + return getLinearLayoutManager(); + } + } + public GridLayoutManager getGridLayoutManager(RecyclerView recyclerView) { + final int columns = getRecyclerColumnCount(); + GridLayoutManager gridLayoutManager = new GridLayoutManager(context, columns); + + GridLayoutManager.SpanSizeLookup spanSizeLookup = getSpanSizeLookup(gridLayoutManager); + if(spanSizeLookup != null) { + gridLayoutManager.setSpanSizeLookup(spanSizeLookup); + } + RecyclerView.ItemDecoration itemDecoration = getItemDecoration(); + if(itemDecoration != null) { + recyclerView.addItemDecoration(itemDecoration); + } + return gridLayoutManager; + } + public LinearLayoutManager getLinearLayoutManager() { + LinearLayoutManager layoutManager = new LinearLayoutManager(context); + layoutManager.setOrientation(LinearLayoutManager.VERTICAL); + return layoutManager; + } + public GridLayoutManager.SpanSizeLookup getSpanSizeLookup(final GridLayoutManager gridLayoutManager) { + return new GridLayoutManager.SpanSizeLookup() { + @Override + public int getSpanSize(int position) { + SectionAdapter adapter = getCurrentAdapter(); + if(adapter != null) { + int viewType = adapter.getItemViewType(position); + if (viewType == SectionAdapter.VIEW_TYPE_HEADER) { + return gridLayoutManager.getSpanCount(); + } else { + return 1; + } + } else { + return 1; + } + } + }; + } + public RecyclerView.ItemDecoration getItemDecoration() { + return new GridSpacingDecoration(); + } + public int getRecyclerColumnCount() { + if(isOnlyVisible) { + return context.getResources().getInteger(R.integer.Grid_FullScreen_Columns); + } else { + return context.getResources().getInteger(R.integer.Grid_Columns); + } + } + + 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)) { + DownloadService downloadService = getDownloadService(); + if(downloadService == null) { + return; + } + downloadService.clear(); + downloadService.setShufflePlayEnabled(true); + context.openNowPlaying(); + 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; + genreBox.setVisibility(View.GONE); + genreCombo.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + new LoadingTask>(context, true) { + @Override + protected List doInBackground() throws Throwable { + MusicService musicService = MusicServiceFactory.getMusicService(context); + return musicService.getGenres(false, context, this); + } + + @Override + protected void done(final List genres) { + List names = new ArrayList(); + String blank = context.getResources().getString(R.string.select_genre_blank); + names.add(blank); + for(Genre genre: genres) { + names.add(genre.getName()); + } + final List 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) { + msg = getErrorMessage(error); + } else { + msg = context.getResources().getString(R.string.playlist_error) + " " + getErrorMessage(error); + } + + Util.toast(context, msg, false); + } + }.execute(); + } + }); + _useCombo = true; + 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) { + 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(); + + DownloadService downloadService = getDownloadService(); + if (downloadService == null) { + return; + } + + downloadService.clear(); + downloadService.setShufflePlayEnabled(true); + context.openNowPlaying(); + } + }) + .setNegativeButton(R.string.common_cancel, null); + AlertDialog dialog = builder.create(); + dialog.show(); + } + + 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(isDirectory && id != null) { + root = getMusicDirectory(id, name, false, musicService, this); + } else { + root = musicService.getPlaylist(true, id, name, context, this); + } + + boolean shuffleByAlbum = Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_SHUFFLE_BY_ALBUM, true); + if(shuffle && shuffleByAlbum) { + Collections.shuffle(root.getChildren()); + } + + songs = new LinkedList(); + getSongsRecursively(root, songs); + + if(shuffle && !shuffleByAlbum) { + Collections.shuffle(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 albums, final boolean shuffle, final boolean append, final boolean playNext) { + new RecursiveLoader(context) { + @Override + protected Boolean doInBackground() throws Throwable { + musicService = MusicServiceFactory.getMusicService(context); + + if(shuffle) { + Collections.shuffle(albums); + } + + songs = new LinkedList(); + 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, playNext, 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 songs) { + Iterator it = songs.iterator(); + while(it.hasNext()) { + Entry entry = it.next(); + if(entry.isDirectory()) { + it.remove(); + } + } + + if(songs.isEmpty()) { + Util.toast(context, "No songs selected"); + return; + } + + new LoadingTask>(context, true) { + @Override + protected List doInBackground() throws Throwable { + MusicService musicService = MusicServiceFactory.getMusicService(context); + List playlists = new ArrayList(); + playlists.addAll(musicService.getPlaylists(false, context, this)); + + // Iterate through and remove all non owned public playlists + Iterator 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 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(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) { + 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 songs) { + new SilentBackgroundTask(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) { + 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 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(Integer.parseInt(getDownloadService().getSuggestedPlaylistId()) != -1) { + overwriteCheckBox.setChecked(true); + overwriteCheckBox.setVisibility(View.VISIBLE); + } + } catch(Exception e) { + Log.i(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 songs, final String name) { + new SilentBackgroundTask(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 songs, final String name, final String id) { + new SilentBackgroundTask(context) { + @Override + protected Void doInBackground() throws Throwable { + MusicService musicService = MusicServiceFactory.getMusicService(context); + MusicDirectory playlist = musicService.getPlaylist(true, id, name, context, null); + List 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) { + 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"); + } + } + if(duration == null) { + duration = song.getDuration(); + } + + List headers = new ArrayList<>(); + List details = new ArrayList<>(); + + if(!song.isDirectory()) { + headers.add(R.string.details_title); + details.add(song.getTitle()); + } + + if(song.getArtist() != null && !"".equals(song.getArtist())) { + headers.add(R.string.details_artist); + details.add(song.getArtist()); + } + if(song.getAlbum() != null && !"".equals(song.getAlbum())) { + headers.add(R.string.details_album); + details.add(song.getAlbum()); + } + if(song.getTrack() != null && song.getTrack() != 0) { + headers.add(R.string.details_track); + details.add(Integer.toString(song.getTrack())); + } + if(song.getGenre() != null && !"".equals(song.getGenre())) { + headers.add(R.string.details_genre); + details.add(song.getGenre()); + } + if(song.getYear() != null && song.getYear() != 0) { + headers.add(R.string.details_year); + details.add(Integer.toString(song.getYear())); + } + if(!Util.isOffline(context) && song.getSuffix() != null) { + headers.add(R.string.details_server_format); + details.add(song.getSuffix()); + + if(song.getBitRate() != null && song.getBitRate() != 0) { + headers.add(R.string.details_server_bitrate); + details.add(song.getBitRate() + " kbps"); + } + } + if(format != null && !"".equals(format)) { + headers.add(R.string.details_cached_format); + details.add(format); + } + if(bitrate != null && bitrate != 0) { + headers.add(R.string.details_cached_bitrate); + details.add(bitrate + " kbps"); + } + if(size != 0) { + headers.add(R.string.details_size); + details.add(Util.formatLocalizedBytes(size, context)); + } + if(duration != null && duration != 0) { + headers.add(R.string.details_length); + details.add(Util.formatDuration(duration)); + } + + try { + Long[] dates = SongDBHandler.getHandler(context).getLastPlayed(song); + if(dates != null && dates[0] != null && dates[0] > 0) { + headers.add(R.string.details_last_played); + details.add(Util.formatDate((dates[1] != null && dates[1] > dates[0]) ? dates[1] : dates[0])); + } + } catch(Exception e) { + Log.e(TAG, "Failed to get last played", e); + } + + int title; + if(song.isDirectory()) { + title = R.string.details_title_album; + } else { + title = R.string.details_title_song; + } + Util.showDetailsDialog(context, title, headers, details); + } + + + protected boolean entryExists(Entry entry) { + DownloadFile check = new DownloadFile(context, entry, false); + return check.isCompleteFileAvailable(); + } + + public void deleteRecursively(Artist artist) { + deleteRecursively(artist, FileUtil.getArtistDirectory(context, artist)); + } + + public void deleteRecursively(Entry album) { + deleteRecursively(album, FileUtil.getAlbumDirectory(context, album)); + } + + public void deleteRecursively(final Object remove, final File dir) { + if(dir == null) { + return; + } + + new LoadingTask(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)) { + SectionAdapter adapter = getCurrentAdapter(); + if(adapter != null) { + adapter.removeItem(remove); + } else { + refresh(); + } + } else { + UpdateView.triggerUpdate(); + } + } + }.execute(); + } + public void deleteSongs(final List songs) { + new LoadingTask(context) { + @Override + protected Void doInBackground() throws Throwable { + getDownloadService().delete(songs); + return null; + } + + @Override + protected void done(Void result) { + if(Util.isOffline(context)) { + SectionAdapter adapter = getCurrentAdapter(); + if(adapter != null) { + for(Entry song: songs) { + adapter.removeItem(song); + } + } else { + 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 GestureDetector getGestureDetector() { + return gestureScanner; + } + + protected void onSongPress(List entries, Entry entry) { + onSongPress(entries, entry, 0, true); + } + protected void onSongPress(List entries, Entry entry, boolean allowPlayAll) { + onSongPress(entries, entry, 0, allowPlayAll); + } + protected void onSongPress(List entries, Entry entry, int position, boolean allowPlayAll) { + List songs = new ArrayList(); + + String songPressAction = Util.getSongPressAction(context); + if("all".equals(songPressAction) && allowPlayAll) { + for(Entry song: entries) { + if(!song.isDirectory()) { + songs.add(song); + } + } + playNow(songs, entry, position); + } else if("next".equals(songPressAction)) { + getDownloadService().download(Arrays.asList(entry), false, false, true, false); + } else if("last".equals(songPressAction)) { + getDownloadService().download(Arrays.asList(entry), false, false, false, false); + } else { + songs.add(entry); + playNow(songs); + } + } + + protected void playNow(List entries) { + playNow(entries, null, null); + } + protected void playNow(final List entries, final String playlistName, final String playlistId) { + new RecursiveLoader(context) { + @Override + protected Boolean doInBackground() throws Throwable { + getSongsRecursively(entries, songs); + return null; + } + + @Override + protected void done(Boolean result) { + playNow(songs, 0, playlistName, playlistId); + } + }.execute(); + } + protected void playNow(List entries, int position) { + playNow(entries, position, null, null); + } + protected void playNow(List 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 entries, Entry song, int position) { + playNow(entries, song, position, null, null); + } + + protected void playNow(final List entries, final Entry song, final int position, final String playlistName, final String playlistId) { + new LoadingTask(context) { + @Override + protected Void doInBackground() throws Throwable { + playNowInTask(entries, song, position, playlistName, playlistId); + return null; + } + + @Override + protected void done(Void result) { + context.openNowPlaying(); + } + }.execute(); + } + protected void playNowInTask(final List entries, final Entry song, final int position) { + playNowInTask(entries, song, position, null, null); + } + protected void playNowInTask(final List entries, final Entry song, final int position, final String playlistName, final String playlistId) { + DownloadService downloadService = getDownloadService(); + if(downloadService == null) { + return; + } + + downloadService.clear(); + downloadService.download(entries, false, true, true, false, entries.indexOf(song), position); + downloadService.setSuggestedPlaylistName(playlistName, playlistId); + } + + public SectionAdapter getCurrentAdapter() { return null; } + public void stopActionMode() { + SectionAdapter adapter = getCurrentAdapter(); + if(adapter != null) { + adapter.stopActionMode(); + } + } + protected void clearSelected() { + if(getCurrentAdapter() != null) { + getCurrentAdapter().clearSelected(); + } + } + protected List getSelectedEntries() { + return getCurrentAdapter().getSelected(); + } + + protected void playNow(final boolean shuffle, final boolean append) { + playNow(shuffle, append, false); + } + protected void playNow(final boolean shuffle, final boolean append, final boolean playNext) { + List songs = getSelectedEntries(); + if(!songs.isEmpty()) { + download(songs, append, false, !append, playNext, shuffle); + clearSelected(); + } + } + + protected void download(List entries, boolean append, boolean save, boolean autoplay, boolean playNext, boolean shuffle) { + download(entries, append, save, autoplay, playNext, shuffle, null, null); + } + protected void download(final List entries, final boolean append, final boolean save, final boolean autoplay, final boolean playNext, final boolean shuffle, final String playlistName, final String playlistId) { + final DownloadService downloadService = getDownloadService(); + if (downloadService == null) { + return; + } + warnIfStorageUnavailable(); + + // Conditions for using play now button + if(!append && !save && autoplay && !playNext && !shuffle) { + // Call playNow which goes through and tries to use information + playNow(entries, playlistName, playlistId); + return; + } + + RecursiveLoader onValid = new RecursiveLoader(context) { + @Override + protected Boolean doInBackground() throws Throwable { + if (!append) { + getDownloadService().clear(); + } + getSongsRecursively(entries, songs); + + downloadService.download(songs, save, autoplay, playNext, shuffle); + if (playlistName != null) { + downloadService.setSuggestedPlaylistName(playlistName, playlistId); + } else { + downloadService.setSuggestedPlaylistName(null, null); + } + return null; + } + + @Override + protected void done(Boolean result) { + if (autoplay) { + context.openNowPlaying(); + } 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())); + } + } + }; + + executeOnValid(onValid); + } + protected void executeOnValid(RecursiveLoader onValid) { + onValid.execute(); + } + protected void downloadBackground(final boolean save) { + List songs = getSelectedEntries(); + if(!songs.isEmpty()) { + downloadBackground(save, songs); + } + } + + protected void downloadBackground(final boolean save, final List entries) { + if (getDownloadService() == null) { + return; + } + + warnIfStorageUnavailable(); + new RecursiveLoader(context) { + @Override + protected Boolean doInBackground() throws Throwable { + getSongsRecursively(entries, true); + getDownloadService().downloadBackground(songs, save); + return null; + } + + @Override + protected void done(Boolean result) { + Util.toast(context, context.getResources().getQuantityString(R.plurals.select_album_n_songs_downloading, songs.size(), songs.size())); + } + }.execute(); + } + + protected void delete() { + List songs = getSelectedEntries(); + if(!songs.isEmpty()) { + DownloadService downloadService = getDownloadService(); + if(downloadService != null) { + downloadService.delete(songs); + } + } + } + + protected boolean isShowArtistEnabled() { + return false; + } + + protected String getCurrentQuery() { + return null; + } + + public abstract class RecursiveLoader extends LoadingTask { + protected MusicService musicService; + protected static final int MAX_SONGS = 500; + protected boolean playNowOverride = false; + protected List songs = new ArrayList<>(); + + public RecursiveLoader(Activity context) { + super(context); + musicService = MusicServiceFactory.getMusicService(context); + } + + protected void getSiblingsRecursively(Entry entry) throws Exception { + MusicDirectory parent = new MusicDirectory(); + if(Util.isTagBrowsing(context) && !Util.isOffline(context)) { + parent.setId(entry.getAlbumId()); + } else { + parent.setId(entry.getParent()); + } + + if(parent.getId() == null) { + songs.add(entry); + } else { + MusicDirectory.Entry dir = new Entry(parent.getId()); + dir.setDirectory(true); + parent.addChild(dir); + getSongsRecursively(parent, songs); + } + } + protected void getSongsRecursively(List entry) throws Exception { + getSongsRecursively(entry, false); + } + protected void getSongsRecursively(List entry, boolean allowVideo) throws Exception { + getSongsRecursively(entry, songs, allowVideo); + } + protected void getSongsRecursively(List entry, List songs) throws Exception { + getSongsRecursively(entry, songs, false); + } + protected void getSongsRecursively(List entry, List songs, boolean allowVideo) throws Exception { + MusicDirectory dir = new MusicDirectory(); + dir.addChildren(entry); + getSongsRecursively(dir, songs, allowVideo); + } + + protected void getSongsRecursively(MusicDirectory parent, List songs) throws Exception { + getSongsRecursively(parent, songs, false); + } + protected void getSongsRecursively(MusicDirectory parent, List songs, boolean allowVideo) throws Exception { + if (songs.size() > MAX_SONGS) { + return; + } + + for (Entry dir : parent.getChildren(true, false)) { + 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); + } + + for (Entry song : parent.getChildren(false, true)) { + songs.add(song); + } + } + + @Override + protected void done(Boolean result) { + warnIfStorageUnavailable(); + + if(playNowOverride) { + playNow(songs); + return; + } + + if(result) { + context.openNowPlaying(); + } + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/provider/AudinautSearchProvider.java b/app/src/main/java/github/nvllsvm/audinaut/provider/AudinautSearchProvider.java new file mode 100644 index 0000000..0739aa5 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/provider/AudinautSearchProvider.java @@ -0,0 +1,209 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2010 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.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.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.Artist; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.SearchCritera; +import github.nvllsvm.audinaut.domain.SearchResult; +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.service.MusicServiceFactory; +import github.nvllsvm.audinaut.util.Util; + +/** + * Provides search suggestions based on recent searches. + * + * @author Sindre Mehus + */ +public class AudinautSearchProvider extends ContentProvider { + private static final String TAG = AudinautSearchProvider.class.getSimpleName(); + + private static final String RESOURCE_PREFIX = "android.resource://github.nvllsvm.audinaut/"; + 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) { + if(selectionArgs[0].isEmpty()) { + return null; + } + + 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 results = new ArrayList(); + 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() { + @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(); + } + + String artistDisplay; + if(entry.getArtist() == null) { + if(entry.getAlbum() != null) { + artistDisplay = entry.getAlbumDisplay(); + } else { + artistDisplay = ""; + } + } else if(entry.getAlbum() != null) { + artistDisplay = entry.getArtist() + " - " + entry.getAlbumDisplay(); + } else { + artistDisplay = entry.getArtist(); + } + + cursor.addRow(new Object[]{entry.getId().hashCode(), entry.getTitle(), artistDisplay, "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/nvllsvm/audinaut/provider/AudinautWidget4x1.java b/app/src/main/java/github/nvllsvm/audinaut/provider/AudinautWidget4x1.java new file mode 100644 index 0000000..5a81e2a --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/provider/AudinautWidget4x1.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 . + + Copyright 2010 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.provider; + +import github.nvllsvm.audinaut.R; + +public class AudinautWidget4x1 extends AudinautWidgetProvider { + @Override + protected int getLayout() { + return R.layout.appwidget4x1; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/provider/AudinautWidget4x2.java b/app/src/main/java/github/nvllsvm/audinaut/provider/AudinautWidget4x2.java new file mode 100644 index 0000000..df42c92 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/provider/AudinautWidget4x2.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 . + + Copyright 2010 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.provider; + +import github.nvllsvm.audinaut.R; + +public class AudinautWidget4x2 extends AudinautWidgetProvider { + @Override + protected int getLayout() { + return R.layout.appwidget4x2; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/provider/AudinautWidget4x3.java b/app/src/main/java/github/nvllsvm/audinaut/provider/AudinautWidget4x3.java new file mode 100644 index 0000000..ad9f6da --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/provider/AudinautWidget4x3.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 . + + Copyright 2010 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.provider; + +import github.nvllsvm.audinaut.R; + +public class AudinautWidget4x3 extends AudinautWidgetProvider { + @Override + protected int getLayout() { + return R.layout.appwidget4x3; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/provider/AudinautWidget4x4.java b/app/src/main/java/github/nvllsvm/audinaut/provider/AudinautWidget4x4.java new file mode 100644 index 0000000..8c7ef02 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/provider/AudinautWidget4x4.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 . + + Copyright 2010 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.provider; + +import github.nvllsvm.audinaut.R; + +public class AudinautWidget4x4 extends AudinautWidgetProvider { + @Override + protected int getLayout() { + return R.layout.appwidget4x4; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/provider/AudinautWidgetProvider.java b/app/src/main/java/github/nvllsvm/audinaut/provider/AudinautWidgetProvider.java new file mode 100644 index 0000000..30341c3 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/provider/AudinautWidgetProvider.java @@ -0,0 +1,305 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2010 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.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.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.activity.SubsonicActivity; +import github.nvllsvm.audinaut.activity.SubsonicFragmentActivity; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.PlayerQueue; +import github.nvllsvm.audinaut.service.DownloadService; +import github.nvllsvm.audinaut.service.DownloadServiceLifecycleSupport; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.FileUtil; +import github.nvllsvm.audinaut.util.ImageLoader; +import github.nvllsvm.audinaut.util.Util; + +/** + * Simple widget to show currently playing album art along + * with play/pause and next track buttons. + *

    + * Based on source code from the stock Android Music app. + * + * @author Sindre Mehus + */ +public class AudinautWidgetProvider extends AppWidgetProvider { + private static final String TAG = AudinautWidgetProvider.class.getSimpleName(); + private static AudinautWidget4x1 instance4x1; + private static AudinautWidget4x2 instance4x2; + private static AudinautWidget4x3 instance4x3; + private static AudinautWidget4x4 instance4x4; + + public static synchronized void notifyInstances(Context context, DownloadService service, boolean playing) { + if(instance4x1 == null) { + instance4x1 = new AudinautWidget4x1(); + } + if(instance4x2 == null) { + instance4x2 = new AudinautWidget4x2(); + } + if(instance4x3 == null) { + instance4x3 = new AudinautWidget4x3(); + } + if(instance4x4 == null) { + instance4x4 = new AudinautWidget4x4(); + } + + 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 + try { + PlayerQueue state = FileUtil.deserialize(context, DownloadServiceLifecycleSupport.FILENAME_DOWNLOADS_SER, PlayerQueue.class); + if (state != null && state.currentPlayingIndex != -1) { + currentPlaying = state.songs.get(state.currentPlayingIndex); + } + } catch(Exception e) { + Log.e(TAG, "Failed to grab current playing", e); + } + } 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.media_pause_dark); + } else { + views.setImageViewResource(R.id.control_play, R.drawable.media_start_dark); + } + + // 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 @param playerActive True if player is active in background. Launch {@link github.nvllsvm.audinaut.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_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("Audinaut.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("Audinaut.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("Audinaut.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/nvllsvm/audinaut/provider/MostRecentStubProvider.java b/app/src/main/java/github/nvllsvm/audinaut/provider/MostRecentStubProvider.java new file mode 100644 index 0000000..a8c0289 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/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 . + + Copyright 2009 (C) Sindre Mehus + */ + +package github.nvllsvm.audinaut.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/nvllsvm/audinaut/provider/PlaylistStubProvider.java b/app/src/main/java/github/nvllsvm/audinaut/provider/PlaylistStubProvider.java new file mode 100644 index 0000000..aa841ed --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/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 . + + Copyright 2009 (C) Sindre Mehus + */ + +package github.nvllsvm.audinaut.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/nvllsvm/audinaut/receiver/A2dpIntentReceiver.java b/app/src/main/java/github/nvllsvm/audinaut/receiver/A2dpIntentReceiver.java new file mode 100644 index 0000000..0009b53 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/receiver/A2dpIntentReceiver.java @@ -0,0 +1,47 @@ +package github.nvllsvm.audinaut.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; +import github.nvllsvm.audinaut.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/nvllsvm/audinaut/receiver/AudioNoisyReceiver.java b/app/src/main/java/github/nvllsvm/audinaut/receiver/AudioNoisyReceiver.java new file mode 100644 index 0000000..302598a --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/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 . + Copyright 2014 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.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.nvllsvm.audinaut.domain.PlayerState; +import github.nvllsvm.audinaut.service.DownloadService; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.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.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/nvllsvm/audinaut/receiver/BootReceiver.java b/app/src/main/java/github/nvllsvm/audinaut/receiver/BootReceiver.java new file mode 100644 index 0000000..ba5915f --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/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 . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import github.nvllsvm.audinaut.service.HeadphoneListenerService; +import github.nvllsvm.audinaut.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/nvllsvm/audinaut/receiver/HeadphonePlugReceiver.java b/app/src/main/java/github/nvllsvm/audinaut/receiver/HeadphonePlugReceiver.java new file mode 100644 index 0000000..a7c0e5c --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/receiver/HeadphonePlugReceiver.java @@ -0,0 +1,40 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +import github.nvllsvm.audinaut.service.DownloadService; +import github.nvllsvm.audinaut.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); + if(headphoneState == 1 && Util.shouldStartOnHeadphones(context)) { + Intent start = new Intent(context, DownloadService.class); + start.setAction(DownloadService.START_PLAY); + context.startService(start); + } + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/receiver/MediaButtonIntentReceiver.java b/app/src/main/java/github/nvllsvm/audinaut/receiver/MediaButtonIntentReceiver.java new file mode 100644 index 0000000..a172322 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/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 . + + Copyright 2010 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; +import android.view.KeyEvent; + +import github.nvllsvm.audinaut.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/nvllsvm/audinaut/receiver/PlayActionReceiver.java b/app/src/main/java/github/nvllsvm/audinaut/receiver/PlayActionReceiver.java new file mode 100644 index 0000000..f05ed16 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/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 . + Copyright 2014 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; + +import github.nvllsvm.audinaut.service.DownloadService; +import github.nvllsvm.audinaut.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/nvllsvm/audinaut/service/CachedMusicService.java b/app/src/main/java/github/nvllsvm/audinaut/service/CachedMusicService.java new file mode 100644 index 0000000..4badd07 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/CachedMusicService.java @@ -0,0 +1,1129 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.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 android.util.Log; + +import github.nvllsvm.audinaut.domain.Artist; +import github.nvllsvm.audinaut.domain.Genre; +import github.nvllsvm.audinaut.domain.Indexes; +import github.nvllsvm.audinaut.domain.PlayerQueue; +import github.nvllsvm.audinaut.domain.RemoteStatus; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.MusicFolder; +import github.nvllsvm.audinaut.domain.Playlist; +import github.nvllsvm.audinaut.domain.SearchCritera; +import github.nvllsvm.audinaut.domain.SearchResult; +import github.nvllsvm.audinaut.domain.User; +import github.nvllsvm.audinaut.util.SilentBackgroundTask; +import github.nvllsvm.audinaut.util.ProgressListener; +import github.nvllsvm.audinaut.util.SongDBHandler; +import github.nvllsvm.audinaut.util.TimeLimitedCache; +import github.nvllsvm.audinaut.util.FileUtil; +import github.nvllsvm.audinaut.util.Util; + +import static github.nvllsvm.audinaut.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 + public static final int CACHE_UPDATE_LIST = 1; + public static final int CACHE_UPDATE_METADATA = 2; + private static final int CACHED_LAST_FM = 24 * 60; + + private final RESTMusicService musicService; + private final TimeLimitedCache cachedIndexes = new TimeLimitedCache(60 * 60, TimeUnit.SECONDS); + private final TimeLimitedCache> cachedPlaylists = new TimeLimitedCache>(3600, TimeUnit.SECONDS); + private final TimeLimitedCache> cachedMusicFolders = new TimeLimitedCache>(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 List getMusicFolders(boolean refresh, Context context, ProgressListener progressListener) throws Exception { + checkSettingsChanged(context); + if (refresh) { + cachedMusicFolders.clear(); + } + List 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(result), getCacheName(context, "musicFolders")); + } + + MusicFolder.sort(result); + 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(final String id, final String name, final boolean refresh, final Context context, final ProgressListener progressListener) throws Exception { + MusicDirectory dir = null; + final MusicDirectory cached = FileUtil.deserialize(context, getCacheName(context, "directory", id), MusicDirectory.class); + if(!refresh && cached != null) { + dir = cached; + + new SilentBackgroundTask(context) { + MusicDirectory refreshed; + private boolean metadataUpdated; + + @Override + protected Void doInBackground() throws Throwable { + refreshed = musicService.getMusicDirectory(id, name, true, context, null); + updateAllSongs(context, refreshed); + metadataUpdated = cached.updateMetadata(refreshed); + deleteRemovedEntries(context, refreshed, cached); + FileUtil.serialize(context, refreshed, getCacheName(context, "directory", id)); + return null; + } + + // Update which entries exist + @Override + public void done(Void result) { + if(progressListener != null) { + if(cached.updateEntriesList(context, musicService.getInstance(context), refreshed)) { + progressListener.updateCache(CACHE_UPDATE_LIST); + } + if(metadataUpdated) { + progressListener.updateCache(CACHE_UPDATE_METADATA); + } + } + } + + @Override + public void error(Throwable error) { + Log.e(TAG, "Failed to refresh music directory", error); + } + }.execute(); + } + + if(dir == null) { + dir = musicService.getMusicDirectory(id, name, refresh, context, progressListener); + updateAllSongs(context, dir); + FileUtil.serialize(context, dir, getCacheName(context, "directory", id)); + + // If a cached copy exists to check against, look for removes + deleteRemovedEntries(context, dir, cached); + } + dir.sortChildren(context, musicService.getInstance(context)); + + return dir; + } + + @Override + public MusicDirectory getArtist(final String id, final String name, final boolean refresh, final Context context, final ProgressListener progressListener) throws Exception { + MusicDirectory dir = null; + final MusicDirectory cached = FileUtil.deserialize(context, getCacheName(context, "artist", id), MusicDirectory.class); + if(!refresh && cached != null) { + dir = cached; + + new SilentBackgroundTask(context) { + MusicDirectory refreshed; + + @Override + protected Void doInBackground() throws Throwable { + refreshed = musicService.getArtist(id, name, refresh, context, null); + cached.updateMetadata(refreshed); + deleteRemovedEntries(context, refreshed, cached); + FileUtil.serialize(context, refreshed, getCacheName(context, "artist", id)); + return null; + } + + // Update which entries exist + @Override + public void done(Void result) { + if(progressListener != null) { + if(cached.updateEntriesList(context, musicService.getInstance(context), refreshed)) { + progressListener.updateCache(CACHE_UPDATE_LIST); + } + } + } + + @Override + public void error(Throwable error) { + Log.e(TAG, "Failed to refresh getArtist", error); + } + }.execute(); + } + + 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); + } + dir.sortChildren(context, musicService.getInstance(context)); + + return dir; + } + + @Override + public MusicDirectory getAlbum(final String id, final String name, final boolean refresh, final Context context, final ProgressListener progressListener) throws Exception { + MusicDirectory dir = null; + final MusicDirectory cached = FileUtil.deserialize(context, getCacheName(context, "album", id), MusicDirectory.class); + if(!refresh && cached != null) { + dir = cached; + + new SilentBackgroundTask(context) { + MusicDirectory refreshed; + private boolean metadataUpdated; + + @Override + protected Void doInBackground() throws Throwable { + refreshed = musicService.getAlbum(id, name, refresh, context, null); + updateAllSongs(context, refreshed); + metadataUpdated = cached.updateMetadata(refreshed); + deleteRemovedEntries(context, refreshed, cached); + FileUtil.serialize(context, refreshed, getCacheName(context, "album", id)); + return null; + } + + // Update which entries exist + @Override + public void done(Void result) { + if(progressListener != null) { + if(cached.updateEntriesList(context, musicService.getInstance(context), refreshed)) { + progressListener.updateCache(CACHE_UPDATE_LIST); + } + if(metadataUpdated) { + progressListener.updateCache(CACHE_UPDATE_METADATA); + } + } + } + + @Override + public void error(Throwable error) { + Log.e(TAG, "Failed to refresh getAlbum", error); + } + }.execute(); + } + + if(dir == null) { + dir = musicService.getAlbum(id, name, refresh, context, progressListener); + updateAllSongs(context, dir); + FileUtil.serialize(context, dir, getCacheName(context, "album", id)); + + // If a cached copy exists to check against, look for removes + deleteRemovedEntries(context, dir, cached); + } + dir.sortChildren(context, musicService.getInstance(context)); + + 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); + updateAllSongs(context, dir); + 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 getPlaylists(boolean refresh, Context context, ProgressListener progressListener) throws Exception { + checkSettingsChanged(context); + List 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(result), getCacheName(context, "playlist")); + } + cachedPlaylists.set(result); + } + return result; + } + + @Override + public void createPlaylist(String id, String name, List 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(final String id, Context context, ProgressListener progressListener) throws Exception { + musicService.deletePlaylist(id, context, progressListener); + + new PlaylistUpdater(context, id) { + @Override + public void updateResult(List objects, Playlist result) { + objects.remove(result); + cachedPlaylists.set(objects); + } + }.execute(); + } + + @Override + public void addToPlaylist(String id, final List 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 objects, Entry result) { + objects.addAll(toAdd); + } + }.execute(); + } + + @Override + public void removeFromPlaylist(final String id, final List 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 objects, Entry result) { + // Make sure this playlist is supposed to be synced + boolean supposedToUnpin = false; + + // Remove in reverse order so indexes are still correct as we iterate through + for(ListIterator iterator = toRemove.listIterator(toRemove.size()); iterator.hasPrevious(); ) { + int index = iterator.previous(); + if(supposedToUnpin) { + Entry entry = objects.get(index); + DownloadFile file = new DownloadFile(context, entry, true); + file.unpin(); + } + + objects.remove(index); + } + } + }.execute(); + } + + @Override + public void overwritePlaylist(String id, String name, int toRemove, final List 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 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 objects, Playlist result) { + result.setName(name); + result.setComment(comment); + result.setPublic(pub); + + cachedPlaylists.set(objects); + } + }.execute(); + } + + @Override + public MusicDirectory getAlbumList(String type, int size, int offset, boolean refresh, Context context, ProgressListener progressListener) throws Exception { + try { + MusicDirectory dir = musicService.getAlbumList(type, size, offset, refresh, context, progressListener); + + // Do some serialization updates for changes to recently added + if ("newest".equals(type) && offset == 0) { + String recentlyAddedFile = getCacheName(context, type); + ArrayList recents = FileUtil.deserialize(context, recentlyAddedFile, ArrayList.class); + if (recents == null) { + recents = new ArrayList(); + } + + // 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 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 objects) { + // Only save if actually added to artist + if (changed) { + musicDirectory.replaceChildren(objects); + 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 objects, Artist result) { + if (!objects.contains(artist)) { + objects.add(artist); + changed = true; + } + } + + @Override + public void save(ArrayList objects) { + if (changed) { + indexes.setArtists(objects); + 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) { + Log.w(TAG, "Failed to refresh album list: ", e); + if(refresh) { + throw 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, boolean refresh, Context context, ProgressListener progressListener) throws Exception { + try { + MusicDirectory dir = musicService.getAlbumList(type, extra, size, offset, refresh, context, progressListener); + FileUtil.serialize(context, dir, getCacheName(context, type + extra, Integer.toString(offset))); + return dir; + } catch(IOException e) { + Log.w(TAG, "Failed to refresh album list: ", e); + if(refresh) { + throw 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 getSongList(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception { + return musicService.getSongList(type, size, offset, context, progressListener); + } + + @Override + public MusicDirectory getRandomSongs(int size, String artistId, Context context, ProgressListener progressListener) throws Exception { + return musicService.getRandomSongs(size, artistId, context, progressListener); + } + + @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 List getGenres(boolean refresh, Context context, ProgressListener progressListener) throws Exception { + List 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(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 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 getUsers(boolean refresh, Context context, ProgressListener progressListener) throws Exception { + List 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(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 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 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 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 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 getBitmap(String url, int size, Context context, ProgressListener progressListener, SilentBackgroundTask task) throws Exception { + return musicService.getBitmap(url, size, context, progressListener, task); + } + +@Override + public void savePlayQueue(List 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 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 oldList = new ArrayList(); + 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 { + 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 getArrayList() { + return FileUtil.deserialize(context, cacheName, ArrayList.class); + } + public abstract boolean checkResult(T check); + public abstract void updateResult(List objects, T result); + public void save(ArrayList objects) { + FileUtil.serialize(context, objects, cacheName); + } + + public void execute() { + ArrayList objects = getArrayList(); + + // Only execute if something to check against + if(objects != null) { + List results = new ArrayList(); + 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 { + 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 { + 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 { + 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 getArrayList() { + musicDirectory = FileUtil.deserialize(context, cacheName, MusicDirectory.class); + if(musicDirectory != null) { + return new ArrayList<>(musicDirectory.getChildren()); + } else { + return null; + } + } + public void save(ArrayList 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 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 objects, Entry result) { + PlaylistDirectoryUpdater.this.updateResult(result); + } + }.execute(); + } + } + } + private abstract class GenericEntryUpdater { + Context context; + List entries; + + public GenericEntryUpdater(Context context, Entry entry) { + this.context = context; + this.entries = Arrays.asList(entry); + } + public GenericEntryUpdater(Context context, List 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 songs = new ArrayList(); + 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 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 objects, Entry result) { + GenericEntryUpdater.this.updateResult(result); + } + }.execute(); + } + + 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 StarUpdater extends GenericEntryUpdater { + public StarUpdater(Context context, List entries) { + super(context, entries); + } + + @Override + public boolean checkResult(Entry entry, Entry check) { + if (!entry.getId().equals(check.getId())) { + return false; + } + + return true; + } + + @Override + public void updateResult(Entry result) { + + } + }; + private abstract class IndexesUpdater extends SerializeUpdater { + Indexes indexes; + + IndexesUpdater(Context context, String name) { + super(context, name, Util.getSelectedMusicFolderId(context, musicService.getInstance(context))); + } + + @Override + public ArrayList getArrayList() { + indexes = FileUtil.deserialize(context, cacheName, Indexes.class); + if(indexes == null) { + return null; + } + + ArrayList artists = new ArrayList(); + artists.addAll(indexes.getArtists()); + artists.addAll(indexes.getShortcuts()); + return artists; + } + + public void save(ArrayList objects) { + indexes.setArtists(objects); + FileUtil.serialize(context, indexes, cacheName); + cachedIndexes.set(indexes); + } + } + + protected void updateAllSongs(Context context, MusicDirectory dir) { + List songs = dir.getSongs(); + if(!songs.isEmpty()) { + SongDBHandler.getHandler(context).addSongs(musicService.getInstance(context), songs); + } + } + + private void checkSettingsChanged(Context context) { + int instance = musicService.getInstance(context); + String newUrl = musicService.getRestUrl(context, null, false); + boolean newIsTagBrowsing = Util.isTagBrowsing(context, instance); + if (!Util.equals(newUrl, restUrl) || isTagBrowsing != newIsTagBrowsing) { + cachedMusicFolders.clear(); + cachedIndexes.clear(); + cachedPlaylists.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/nvllsvm/audinaut/service/DownloadFile.java b/app/src/main/java/github/nvllsvm/audinaut/service/DownloadFile.java new file mode 100644 index 0000000..1f8ed4c --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/DownloadFile.java @@ -0,0 +1,633 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.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.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.SilentBackgroundTask; +import github.nvllsvm.audinaut.util.FileUtil; +import github.nvllsvm.audinaut.util.Util; +import github.nvllsvm.audinaut.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; + } + public boolean isSong() { + return song.isSong(); + } + + public Context getContext() { + return context; + } + + /** + * 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 = 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 == 0 || 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 getSaveFile() { + 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 + ")"; + } + + // Don't do this. Causes infinite loop if two instances of same song + /*@Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + DownloadFile downloadFile = (DownloadFile) o; + return Util.equals(this.getSong(), downloadFile.getSong()); + }*/ + + private class DownloadTask extends SilentBackgroundTask { + 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 + DownloadService downloadService = DownloadService.getInstance(); + if(downloadService != null && !isCancelled()) { + new CacheCleaner(context, downloadService).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/nvllsvm/audinaut/service/DownloadService.java b/app/src/main/java/github/nvllsvm/audinaut/service/DownloadService.java new file mode 100644 index 0000000..b8a377a --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/DownloadService.java @@ -0,0 +1,2271 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.service; + +import static github.nvllsvm.audinaut.domain.PlayerState.COMPLETED; +import static github.nvllsvm.audinaut.domain.PlayerState.DOWNLOADING; +import static github.nvllsvm.audinaut.domain.PlayerState.IDLE; +import static github.nvllsvm.audinaut.domain.PlayerState.PAUSED; +import static github.nvllsvm.audinaut.domain.PlayerState.PAUSED_TEMP; +import static github.nvllsvm.audinaut.domain.PlayerState.PREPARED; +import static github.nvllsvm.audinaut.domain.PlayerState.PREPARING; +import static github.nvllsvm.audinaut.domain.PlayerState.STARTED; +import static github.nvllsvm.audinaut.domain.PlayerState.STOPPED; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.activity.SubsonicActivity; +import github.nvllsvm.audinaut.audiofx.AudioEffectsController; +import github.nvllsvm.audinaut.audiofx.EqualizerController; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.PlayerState; +import github.nvllsvm.audinaut.domain.RepeatMode; +import github.nvllsvm.audinaut.receiver.MediaButtonIntentReceiver; +import github.nvllsvm.audinaut.util.ImageLoader; +import github.nvllsvm.audinaut.util.Notifications; +import github.nvllsvm.audinaut.util.SilentBackgroundTask; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.ShufflePlayBuffer; +import github.nvllsvm.audinaut.util.SimpleServiceBinder; +import github.nvllsvm.audinaut.util.UpdateHelper; +import github.nvllsvm.audinaut.util.Util; +import github.nvllsvm.audinaut.util.tags.BastpUtil; +import github.nvllsvm.audinaut.view.UpdateView; +import github.daneren2005.serverproxy.BufferProxy; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.Iterator; +import java.util.List; + +import android.annotation.TargetApi; +import android.app.Service; +import android.content.ComponentCallbacks2; +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.PlaybackParams; +import android.media.audiofx.AudioEffect; +import android.net.wifi.WifiManager; +import android.os.Build; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.PowerManager; +import android.util.Log; +import android.support.v4.util.LruCache; +import android.view.KeyEvent; + +/** + * @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.nvllsvm.audinaut.CMD_PLAY"; + public static final String CMD_TOGGLEPAUSE = "github.nvllsvm.audinaut.CMD_TOGGLEPAUSE"; + public static final String CMD_PAUSE = "github.nvllsvm.audinaut.CMD_PAUSE"; + public static final String CMD_STOP = "github.nvllsvm.audinaut.CMD_STOP"; + public static final String CMD_PREVIOUS = "github.nvllsvm.audinaut.CMD_PREVIOUS"; + public static final String CMD_NEXT = "github.nvllsvm.audinaut.CMD_NEXT"; + public static final String CANCEL_DOWNLOADS = "github.nvllsvm.audinaut.CANCEL_DOWNLOADS"; + public static final String START_PLAY = "github.nvllsvm.audinaut.START_PLAYING"; + public static final int FAST_FORWARD = 30000; + public static final int REWIND = 10000; + private static final long DEFAULT_DELAY_UPDATE_PROGRESS = 1000L; + private static final double DELETE_CUTOFF = 0.84; + private static final int REQUIRED_ALBUM_MATCHES = 4; + private static final int REMOTE_PLAYLIST_TOTAL = 3; + private static final int SHUFFLE_MODE_NONE = 0; + private static final int SHUFFLE_MODE_ALL = 1; + private static final int SHUFFLE_MODE_ARTIST = 2; + + public static final int METADATA_UPDATED_ALL = 0; + public static final int METADATA_UPDATED_STAR = 1; + public static final int METADATA_UPDATED_COVER_ART = 8; + + private final IBinder binder = new SimpleServiceBinder<>(this); + private Looper mediaPlayerLooper; + private MediaPlayer mediaPlayer; + private MediaPlayer nextMediaPlayer; + private int audioSessionId; + private boolean nextSetup = false; + private final List downloadList = new ArrayList(); + private final List backgroundDownloadList = new ArrayList(); + private final List toDelete = new ArrayList(); + private final Handler handler = new Handler(); + private Handler mediaPlayerHandler; + private final DownloadServiceLifecycleSupport lifecycleSupport = new DownloadServiceLifecycleSupport(this); + private ShufflePlayBuffer shufflePlayBuffer; + + private final LruCache downloadFileCache = new LruCache(100); + private final List cleanupCandidates = new ArrayList(); + 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 final List onSongChangedListeners = new ArrayList<>(); + private long revision; + private static DownloadService instance; + private String suggestedPlaylistName; + private String suggestedPlaylistId; + private PowerManager.WakeLock wakeLock; + private WifiManager.WifiLock wifiLock; + private boolean keepScreenOn; + private int cachedPosition = 0; + private boolean downloadOngoing = false; + private float volume = 1.0f; + private long delayUpdateProgress = DEFAULT_DELAY_UPDATE_PROGRESS; + + private AudioEffectsController effectsController; + private PositionCache positionCache; + private BufferProxy proxy; + + private boolean autoPlayStart = false; + private boolean runListenersOnInit = false; + + // 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); + + audioSessionId = -1; + Integer id = prefs.getInt(Constants.CACHE_AUDIO_SESSION_ID, -1); + if(id != -1) { + try { + audioSessionId = id; + mediaPlayer.setAudioSessionId(audioSessionId); + } catch (Throwable e) { + audioSessionId = -1; + } + } + + if(audioSessionId == -1) { + mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); + try { + audioSessionId = mediaPlayer.getAudioSessionId(); + prefs.edit().putInt(Constants.CACHE_AUDIO_SESSION_ID, audioSessionId).commit(); + } catch (Throwable t) { + // Froyo or lower + } + } + + 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 { + 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); + + if(runListenersOnInit) { + onSongsChanged(); + onSongProgress(); + onStateUpdate(); + } + + Looper.loop(); + } + }, "DownloadService").start(); + + Util.registerMediaButtonEventReceiver(this); + + PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE); + wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, this.getClass().getName()); + wakeLock.setReferenceCounted(false); + + WifiManager wifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE); + wifiLock = wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL, "downloadServiceLock"); + + keepScreenOn = prefs.getBoolean(Constants.PREFERENCES_KEY_KEEP_SCREEN_ON, false); + + instance = this; + shufflePlayBuffer = new ShufflePlayBuffer(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 onTrimMemory(int level) { + ImageLoader imageLoader = SubsonicActivity.getStaticImageLoader(this); + if(imageLoader != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + Log.i(TAG, "Memory Trim Level: " + level); + if (level < ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) { + if (level >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) { + imageLoader.onLowMemory(0.75f); + } else if (level >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW) { + imageLoader.onLowMemory(0.50f); + } else if(level >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE) { + imageLoader.onLowMemory(0.25f); + } + } else if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) { + imageLoader.onLowMemory(0.25f); + } else if(level >= ComponentCallbacks2.TRIM_MEMORY_COMPLETE) { + imageLoader.onLowMemory(0.75f); + } + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + instance = null; + + if(currentPlaying != null) currentPlaying.setPlaying(false); + 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(bufferTask != null) { + bufferTask.cancel(); + bufferTask = null; + } + if(nextPlayingTask != null) { + nextPlayingTask.cancel(); + nextPlayingTask = null; + } + if(proxy != null) { + proxy.stop(); + proxy = null; + } + 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 songs, boolean save, boolean autoplay, boolean playNext, boolean shuffle) { + download(songs, save, autoplay, playNext, shuffle, 0, 0); + } + public synchronized void download(List songs, boolean save, boolean autoplay, boolean playNext, boolean shuffle, int start, int position) { + setShufflePlayEnabled(false); + 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(); + } else { + int size = size(); + int index = getCurrentPlayingIndex(); + for (MusicDirectory.Entry song : songs) { + if(song == null) { + continue; + } + + 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++; + onSongsChanged(); + 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 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 synchronized void updateRemotePlaylist() { + List playlist = new ArrayList<>(); + if(currentPlaying != null) { + int index = downloadList.indexOf(currentPlaying); + if(index == -1) { + index = 0; + } + + int size = size(); + int end = index + REMOTE_PLAYLIST_TOTAL; + for(int i = index; i < size && i < end; i++) { + playlist.add(downloadList.get(i)); + } + } + } + + public synchronized void restore(List songs, List toDelete, int currentPlayingIndex, int currentPlayingPosition) { + SharedPreferences prefs = Util.getPreferences(this); + 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; + } + 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) { + 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 synchronized void shuffle() { + Collections.shuffle(downloadList); + currentPlayingIndex = downloadList.indexOf(currentPlaying); + if (currentPlaying != null) { + downloadList.remove(getCurrentPlayingIndex()); + downloadList.add(0, currentPlaying); + currentPlayingIndex = 0; + } + revision++; + onSongsChanged(); + 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 clearBackground() { + if(currentDownloading != null && backgroundDownloadList.contains(currentDownloading)) { + currentDownloading.cancelDownload(); + currentDownloading = null; + } + backgroundDownloadList.clear(); + revision++; + Notifications.hideDownloadingNotification(this, this, handler); + } + + public synchronized void clearIncomplete() { + Iterator 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(); + onSongsChanged(); + } + + public void setOnline(final boolean online) { + if(shufflePlay) { + setShufflePlayEnabled(false); + } + + lifecycleSupport.post(new Runnable() { + @Override + public void run() { + if (online) { + checkDownloads(); + } else { + clearIncomplete(); + } + } + }); + } + + public synchronized int size() { + return downloadList.size(); + } + + public synchronized void clear() { + clear(true); + } + public synchronized void clear(boolean serialize) { + // Delete podcast if fully listened to + int position = getPlayerPosition(); + int duration = getPlayerDuration(); + boolean cutoff = isPastCutoff(position, duration, true); + for(DownloadFile podcast: toDelete) { + podcast.delete(); + } + toDelete.clear(); + + reset(); + downloadList.clear(); + onSongsChanged(); + 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; + + setShufflePlayEnabled(false); + checkDownloads(); + } + + 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++; + onSongsChanged(); + lifecycleSupport.serializeDownloadQueue(); + updateRemotePlaylist(); + if(downloadFile == nextPlaying) { + setNextPlaying(); + } + + checkDownloads(); + } + public synchronized void removeBackground(DownloadFile downloadFile) { + if (downloadFile == currentDownloading && downloadFile != currentPlaying && downloadFile != nextPlaying) { + currentDownloading.cancelDownload(); + currentDownloading = null; + } + + backgroundDownloadList.remove(downloadFile); + revision++; + checkDownloads(); + } + + public synchronized void delete(List songs) { + for (MusicDirectory.Entry song : songs) { + forSong(song).delete(); + } + } + + public synchronized void unpin(List 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 && currentPlaying.getSong() != null) { + Util.broadcastNewTrackInfo(this, currentPlaying.getSong()); + } else { + Util.broadcastNewTrackInfo(this, null); + Notifications.hidePlayingNotification(this, this, handler); + } + onSongChanged(); + } + + synchronized void setNextPlaying() { + SharedPreferences prefs = Util.getPreferences(DownloadService.this); + + boolean gaplessPlayback = prefs.getBoolean(Constants.PREFERENCES_KEY_GAPLESS_PLAYBACK, true); + if (!gaplessPlayback) { + nextPlaying = null; + nextPlayerState = IDLE; + return; + } + setNextPlayerState(IDLE); + + int index = getNextPlayingIndex(); + + if(nextPlayingTask != null) { + nextPlayingTask.cancel(); + nextPlayingTask = null; + } + resetNext(); + + if(index < size() && index != -1 && index != currentPlayingIndex) { + nextPlaying = downloadList.get(index); + + nextPlayingTask = new CheckCompletionTask(nextPlaying); + nextPlayingTask.execute(); + } else { + nextPlaying = null; + } + } + + public int getCurrentPlayingIndex() { + return currentPlayingIndex; + } + private int getNextPlayingIndex() { + int index = getCurrentPlayingIndex(); + if (index != -1) { + RepeatMode repeatMode = getRepeatMode(); + switch (repeatMode) { + case OFF: + index = index + 1; + break; + case ALL: + index = (index + 1) % size(); + break; + case SINGLE: + break; + default: + break; + } + + index = checkNextIndexValid(index, repeatMode); + } + return index; + } + private int checkNextIndexValid(int index, RepeatMode repeatMode) { + int startIndex = index; + int size = size(); + if(index < size && index != -1) { + if(!Util.isAllowedToDownload(this)){ + DownloadFile next = downloadList.get(index); + while(!next.isCompleteFileAvailable()) { + index++; + + if (index >= size) { + if(repeatMode == RepeatMode.ALL) { + index = 0; + } else { + return -1; + } + } else if(index == startIndex) { + handler.post(new Runnable() { + @Override + public void run() { + Util.toast(DownloadService.this, R.string.download_playerstate_mobile_disabled); + } + }); + return -1; + } + + next = downloadList.get(index); + } + } + } + + return index; + } + + public DownloadFile getCurrentPlaying() { + return currentPlaying; + } + + public DownloadFile getCurrentDownloading() { + return currentDownloading; + } + + public DownloadFile getNextPlaying() { + return nextPlaying; + } + + public List getSongs() { + return downloadList; + } + + public List getToDelete() { return toDelete; } + + public synchronized List getDownloads() { + List temp = new ArrayList(); + temp.addAll(downloadList); + temp.addAll(backgroundDownloadList); + return temp; + } + + public List 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); + } + public synchronized void play(DownloadFile downloadFile) { + play(downloadList.indexOf(downloadFile)); + } + 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(); + cachedPosition = 0; + 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); + 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(); + updateRemotePlaylist(); + } + + /** 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(proxy != null && currentPlaying.isCompleteFileAvailable()) { + doPlay(currentPlaying, position, playerState == STARTED); + return; + } + + mediaPlayer.seekTo(position); + subtractPosition = 0; + cachedPosition = position; + + onSongProgress(); + if(playerState == PAUSED) { + lifecycleSupport.serializeDownloadQueue(); + } + } catch (Exception x) { + handleError(x); + } + } + public synchronized int rewind() { + return seekToWrapper(-REWIND); + } + public synchronized int fastForward() { + return seekToWrapper(FAST_FORWARD); + } + protected int seekToWrapper(int difference) { + int msPlayed = Math.max(0, getPlayerPosition()); + Integer duration = getPlayerDuration(); + int msTotal = duration == null ? 0 : duration; + + int seekTo; + if(msPlayed + difference > msTotal) { + seekTo = msTotal; + } else { + seekTo = msPlayed + difference; + } + seekTo(seekTo); + + return seekTo; + } + + public synchronized void previous() { + int index = getCurrentPlayingIndex(); + if (index == -1) { + return; + } + + // If only one song, just skip within song + if(size() == 1 || (currentPlaying != null && !currentPlaying.isSong())) { + 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 || (currentPlaying != null && !currentPlaying.isSong())) { + fastForward(); + 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); + } + + 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() { + setPlayerStateCompleted(); + postPlayCleanup(); + play(getNextPlayingIndex()); + } + public void onNextStarted(DownloadFile nextPlaying) { + setPlayerStateCompleted(); + postPlayCleanup(); + setCurrentPlaying(nextPlaying, true); + setPlayerState(STARTED); + setNextPlayerState(IDLE); + } + + public synchronized void pause() { + pause(false); + } + public synchronized void pause(boolean temp) { + try { + if (playerState == STARTED) { + 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) { + mediaPlayer.pause(); + setPlayerState(STOPPED); + } else if(playerState == PAUSED) { + setPlayerState(STOPPED); + } + } catch(Exception x) { + handleError(x); + } + } + + public synchronized void start() { + try { + // 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 { + 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; + } else if(nextSetup) { + nextSetup = false; + } + } 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; + } + return Math.max(0, cachedPosition - subtractPosition); + } catch (Exception x) { + handleError(x); + return 0; + } + } + + public synchronized int getPlayerDuration() { + if (playerState != IDLE && playerState != DOWNLOADING && playerState != PlayerState.PREPARING) { + int duration = 0; + try { + duration = mediaPlayer.getDuration(); + } catch (Exception x) { + duration = 0; + } + + if(duration != 0) { + return duration; + } + } + + if (currentPlaying != null) { + Integer duration = currentPlaying.getSong().getDuration(); + if (duration != null) { + return duration * 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(playerState == STARTED && positionCache == null) { + positionCache = new LocalPositionCache(); + Thread thread = new Thread(positionCache, "PositionCache"); + thread.start(); + } else if(playerState != STARTED && positionCache != null) { + positionCache.stop(); + positionCache = null; + } + + + onStateUpdate(); + } + + public void setPlayerStateCompleted() { + // Acquire a temporary wakelock + acquireWakelock(); + + Log.i(TAG, this.playerState.name() + " -> " + PlayerState.COMPLETED + " (" + currentPlaying + ")"); + this.playerState = PlayerState.COMPLETED; + if(positionCache != null) { + positionCache.stop(); + positionCache = null; + } + + onStateUpdate(); + } + + 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 { + onSongProgress(); + Thread.sleep(delayUpdateProgress); + } + catch(Exception e) { + isRunning = false; + positionCache = null; + } + } + } + } + private class LocalPositionCache extends PositionCache { + 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; + } + } + onSongProgress(cachedPosition < 2000 ? true: false); + Thread.sleep(delayUpdateProgress); + } + catch(Exception e) { + Log.w(TAG, "Crashed getting current position", e); + isRunning = false; + positionCache = null; + } + } + } + } + + 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 boolean isSeekable() { + return currentPlaying != null && currentPlaying.isWorkDone() && playerState != PREPARING; + } + + 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); + } + + 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()) { + if(Util.isAllowedToDownload(this)) { + reset(); + + bufferTask = new BufferTask(currentPlaying, position, start); + bufferTask.execute(); + } else { + next(false, start); + } + } else { + doPlay(currentPlaying, position, start); + } + } + + private synchronized void doPlay(final DownloadFile downloadFile, final int position, final boolean start) { + try { + 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; + boolean isPartial = false; + downloadFile.setPlaying(true); + final File file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteFile() : downloadFile.getPartialFile(); + isPartial = file.equals(downloadFile.getPartialFile()); + downloadFile.updateModificationDate(); + + 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); + onSongProgress(); + } + + updateRemotePlaylist(); + } + + // 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(); + + 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) { + // Changed to different while preparing so ignore + if(nextMediaPlayer != mp) { + return; + } + + 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) { + 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 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, DownloadFile from, DownloadFile to) { + List list = mainList ? downloadList : backgroundDownloadList; + swap(mainList, list.indexOf(from), list.indexOf(to)); + } + public synchronized void swap(boolean mainList, int from, int to) { + List 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) { + // Moving next playing, current playing, or moving a song to be next playing + if(movedSong == nextPlaying || movedSong == currentPlaying || (currentPlayingIndex + 1) == to) { + setNextPlaying(); + } + } + } + + 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 (!Util.isAllowedToDownload(this)) { + return; + } + + if (downloadList.isEmpty() && backgroundDownloadList.isEmpty()) { + return; + } + + // Need to download current playing? + if (currentPlaying != null && 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() || !backgroundDownloadList.isEmpty())) { + currentDownloading = null; + int n = size(); + + int preloaded = 0; + + if(n != 0) { + 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()) && !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() { + boolean changed = false; + SharedPreferences prefs = Util.getPreferences(this); + int keepCount = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_KEEP_PLAYED_CNT, "0")); + while(currentPlayingIndex > keepCount) { + downloadList.remove(0); + currentPlayingIndex = downloadList.indexOf(currentPlaying); + changed = true; + } + + if(changed) { + revision++; + onSongsChanged(); + } + } + + private synchronized void checkShufflePlay() { + + // Get users desired random playlist size + SharedPreferences prefs = Util.getPreferences(this); + int listSize = Math.max(1, 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) { + onSongsChanged(); + updateRemotePlaylist(); + } + + if (wasEmpty && !downloadList.isEmpty()) { + play(0); + } + } + + public long getDownloadListUpdateRevision() { + return revision; + } + + private synchronized void cleanup() { + Iterator 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; + } + } + + 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; + } + + // Make cutoff a maximum of 10 minutes + int cutoffPoint = Math.max((int) (duration * DELETE_CUTOFF), duration - 10 * 60 * 1000); + boolean isPastCutoff = duration > 0 && position > cutoffPoint; + + return isPastCutoff; + } + + private void applyReplayGain(MediaPlayer mediaPlayer, DownloadFile downloadFile) { + if(currentPlaying == null) { + return; + } + + SharedPreferences prefs = Util.getPreferences(this); + try { + float adjust = 0f; + if (prefs.getBoolean(Constants.PREFERENCES_KEY_REPLAY_GAIN, false)) { + float[] rg = BastpUtil.getReplayGainValues(downloadFile.getFile().getCanonicalPath()); /* track, album */ + 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 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 synchronized boolean isNextPlayingSameAlbum() { + return isNextPlayingSameAlbum(currentPlaying, nextPlaying); + } + private synchronized boolean isNextPlayingSameAlbum(DownloadFile currentPlaying, DownloadFile nextPlaying) { + if(currentPlaying == null || nextPlaying == null) { + return false; + } else { + return currentPlaying.getSong().getAlbum().equals(nextPlaying.getSong().getAlbum()); + } + } + + public void acquireWakelock() { + acquireWakelock(30000); + } + public void acquireWakelock(int ms) { + wakeLock.acquire(ms); + } + + public void handleKeyEvent(KeyEvent keyEvent) { + lifecycleSupport.handleKeyEvent(keyEvent); + } + + public void addOnSongChangedListener(OnSongChangedListener listener) { + addOnSongChangedListener(listener, false); + } + public void addOnSongChangedListener(OnSongChangedListener listener, boolean run) { + synchronized(onSongChangedListeners) { + int index = onSongChangedListeners.indexOf(listener); + if (index == -1) { + onSongChangedListeners.add(listener); + } + } + + if(run) { + if(mediaPlayerHandler != null) { + mediaPlayerHandler.post(new Runnable() { + @Override + public void run() { + onSongsChanged(); + onSongProgress(); + onStateUpdate(); + onMetadataUpdate(METADATA_UPDATED_ALL); + } + }); + } else { + runListenersOnInit = true; + } + } + } + public void removeOnSongChangeListener(OnSongChangedListener listener) { + synchronized(onSongChangedListeners) { + int index = onSongChangedListeners.indexOf(listener); + if (index != -1) { + onSongChangedListeners.remove(index); + } + } + } + + private void onSongChanged() { + final long atRevision = revision; + synchronized(onSongChangedListeners) { + for (final OnSongChangedListener listener : onSongChangedListeners) { + handler.post(new Runnable() { + @Override + public void run() { + if (revision == atRevision && instance != null) { + listener.onSongChanged(currentPlaying, currentPlayingIndex); + + MusicDirectory.Entry entry = currentPlaying != null ? currentPlaying.getSong() : null; + listener.onMetadataUpdate(entry, METADATA_UPDATED_ALL); + } + } + }); + } + + if (mediaPlayerHandler != null && !onSongChangedListeners.isEmpty()) { + mediaPlayerHandler.post(new Runnable() { + @Override + public void run() { + onSongProgress(); + } + }); + } + } + } + private void onSongsChanged() { + final long atRevision = revision; + synchronized(onSongChangedListeners) { + for (final OnSongChangedListener listener : onSongChangedListeners) { + handler.post(new Runnable() { + @Override + public void run() { + if (revision == atRevision && instance != null) { + listener.onSongsChanged(downloadList, currentPlaying, currentPlayingIndex); + } + } + }); + } + } + } + + private void onSongProgress() { + onSongProgress(true); + } + private synchronized void onSongProgress(boolean manual) { + final long atRevision = revision; + final Integer duration = getPlayerDuration(); + final boolean isSeekable = isSeekable(); + final int position = getPlayerPosition(); + + synchronized(onSongChangedListeners) { + for (final OnSongChangedListener listener : onSongChangedListeners) { + handler.post(new Runnable() { + @Override + public void run() { + if (revision == atRevision && instance != null) { + listener.onSongProgress(currentPlaying, position, duration, isSeekable); + } + } + }); + } + } + + if(manual) { + handler.post(new Runnable() { + @Override + public void run() { + } + }); + } + } + private void onStateUpdate() { + final long atRevision = revision; + synchronized(onSongChangedListeners) { + for (final OnSongChangedListener listener : onSongChangedListeners) { + handler.post(new Runnable() { + @Override + public void run() { + if (revision == atRevision && instance != null) { + listener.onStateUpdate(currentPlaying, playerState); + } + } + }); + } + } + } + public void onMetadataUpdate() { + onMetadataUpdate(METADATA_UPDATED_ALL); + } + public void onMetadataUpdate(final int updateType) { + synchronized(onSongChangedListeners) { + for (final OnSongChangedListener listener : onSongChangedListeners) { + handler.post(new Runnable() { + @Override + public void run() { + if (instance != null) { + MusicDirectory.Entry entry = currentPlaying != null ? currentPlaying.getSong() : null; + listener.onMetadataUpdate(entry, updateType); + } + } + }); + } + } + + handler.post(new Runnable() { + @Override + public void run() { + } + }); + } + + private class BufferTask extends SilentBackgroundTask { + 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 { + 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() { + if(!CheckCompletionTask.this.isCancelled()) { + 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 + ")"; + } + } + + public interface OnSongChangedListener { + void onSongChanged(DownloadFile currentPlaying, int currentPlayingIndex); + void onSongsChanged(List songs, DownloadFile currentPlaying, int currentPlayingIndex); + void onSongProgress(DownloadFile currentPlaying, int millisPlayed, Integer duration, boolean isSeekable); + void onStateUpdate(DownloadFile downloadFile, PlayerState playerState); + void onMetadataUpdate(MusicDirectory.Entry entry, int fieldChange); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/service/DownloadServiceLifecycleSupport.java b/app/src/main/java/github/nvllsvm/audinaut/service/DownloadServiceLifecycleSupport.java new file mode 100644 index 0000000..3b9b075 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/DownloadServiceLifecycleSupport.java @@ -0,0 +1,430 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.service; + +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.os.Handler; +import android.os.Looper; +import android.telephony.PhoneStateListener; +import android.telephony.TelephonyManager; +import android.util.Log; +import android.view.KeyEvent; + +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.PlayerQueue; +import github.nvllsvm.audinaut.domain.PlayerState; +import github.nvllsvm.audinaut.util.CacheCleaner; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.FileUtil; +import github.nvllsvm.audinaut.util.Pair; +import github.nvllsvm.audinaut.util.SilentBackgroundTask; +import github.nvllsvm.audinaut.util.SongDBHandler; +import github.nvllsvm.audinaut.util.Util; + +import static github.nvllsvm.audinaut.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 static final int DEBOUNCE_TIME = 200; + + 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 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(); + + // Android 6.0 removes requirement for android.Manifest.permission.READ_PHONE_STATE; + 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.clear(); + 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 songs = new ArrayList(downloadService.getSongs()); + eventHandler.post(new Runnable() { + @Override + public void run() { + if(lock.tryLock()) { + try { + serializeDownloadQueueNow(songs, serializeRemote); + } finally { + lock.unlock(); + } + } + } + }); + } + + public void serializeDownloadQueueNow(List 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); + } + + 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; + } + + public void handleKeyEvent(KeyEvent event) { + if(event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() > 0) { + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_MEDIA_PREVIOUS: + downloadService.fastForward(); + break; + case KeyEvent.KEYCODE_MEDIA_NEXT: + downloadService.rewind(); + break; + } + } else if(event.getAction() == KeyEvent.ACTION_UP) { + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_HEADSETHOOK: + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + if(lastPressTime < (System.currentTimeMillis() - 500)) { + lastPressTime = System.currentTimeMillis(); + downloadService.togglePlayPause(); + } else { + downloadService.next(false, true); + } + break; + case KeyEvent.KEYCODE_MEDIA_PREVIOUS: + if(lastPressTime < (System.currentTimeMillis() - DEBOUNCE_TIME)) { + lastPressTime = System.currentTimeMillis(); + downloadService.previous(); + } + break; + case KeyEvent.KEYCODE_MEDIA_NEXT: + if(lastPressTime < (System.currentTimeMillis() - DEBOUNCE_TIME)) { + lastPressTime = System.currentTimeMillis(); + downloadService.next(); + } + break; + case KeyEvent.KEYCODE_MEDIA_REWIND: + downloadService.rewind(); + break; + case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: + downloadService.fastForward(); + break; + case KeyEvent.KEYCODE_MEDIA_STOP: + downloadService.stop(); + break; + case KeyEvent.KEYCODE_MEDIA_PLAY: + if(downloadService.getPlayerState() != PlayerState.STARTED) { + downloadService.start(); + } + break; + 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/nvllsvm/audinaut/service/HeadphoneListenerService.java b/app/src/main/java/github/nvllsvm/audinaut/service/HeadphoneListenerService.java new file mode 100644 index 0000000..6bcdec6 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/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 . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.service; + +import android.app.Service; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.IBinder; + +import github.nvllsvm.audinaut.receiver.HeadphonePlugReceiver; +import github.nvllsvm.audinaut.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/nvllsvm/audinaut/service/MediaStoreService.java b/app/src/main/java/github/nvllsvm/audinaut/service/MediaStoreService.java new file mode 100644 index 0000000..94fa79b --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/MediaStoreService.java @@ -0,0 +1,151 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.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.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.util.FileUtil; +import github.nvllsvm.audinaut.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(); + 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(); + } + + public void deleteFromMediaStore(DownloadFile downloadFile) { + ContentResolver contentResolver = context.getContentResolver(); + MusicDirectory.Entry song = downloadFile.getSong(); + File file = downloadFile.getCompleteFile(); + + Uri 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 = 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/nvllsvm/audinaut/service/MusicService.java b/app/src/main/java/github/nvllsvm/audinaut/service/MusicService.java new file mode 100644 index 0000000..b672681 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/MusicService.java @@ -0,0 +1,123 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.service; + +import java.util.List; + +import org.apache.http.HttpResponse; + +import android.content.Context; +import android.graphics.Bitmap; + +import github.nvllsvm.audinaut.domain.Genre; +import github.nvllsvm.audinaut.domain.Indexes; +import github.nvllsvm.audinaut.domain.PlayerQueue; +import github.nvllsvm.audinaut.domain.RemoteStatus; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.MusicFolder; +import github.nvllsvm.audinaut.domain.Playlist; +import github.nvllsvm.audinaut.domain.SearchCritera; +import github.nvllsvm.audinaut.domain.SearchResult; +import github.nvllsvm.audinaut.domain.User; +import github.nvllsvm.audinaut.domain.Version; +import github.nvllsvm.audinaut.util.SilentBackgroundTask; +import github.nvllsvm.audinaut.util.ProgressListener; + +/** + * @author Sindre Mehus + */ +public interface MusicService { + + void ping(Context context, ProgressListener progressListener) throws Exception; + + List 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 getPlaylist(boolean refresh, String id, String name, Context context, ProgressListener progressListener) throws Exception; + + List getPlaylists(boolean refresh, Context context, ProgressListener progressListener) throws Exception; + + void createPlaylist(String id, String name, List entries, Context context, ProgressListener progressListener) throws Exception; + + void deletePlaylist(String id, Context context, ProgressListener progressListener) throws Exception; + + void addToPlaylist(String id, List toAdd, Context context, ProgressListener progressListener) throws Exception; + + void removeFromPlaylist(String id, List toRemove, Context context, ProgressListener progressListener) throws Exception; + + void overwritePlaylist(String id, String name, int toRemove, List toAdd, Context context, ProgressListener progressListener) throws Exception; + + void updatePlaylist(String id, String name, String comment, boolean pub, Context context, ProgressListener progressListener) throws Exception; + + MusicDirectory getAlbumList(String type, int size, int offset, boolean refresh, Context context, ProgressListener progressListener) throws Exception; + + MusicDirectory getAlbumList(String type, String extra, int size, int offset, boolean refresh, Context context, ProgressListener progressListener) throws Exception; + + MusicDirectory getSongList(String type, 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; + + List 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; + + User getUser(boolean refresh, String username, Context context, ProgressListener progressListener) throws Exception; + + List 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 getBitmap(String url, int size, Context context, ProgressListener progressListener, SilentBackgroundTask task) throws Exception; + + void savePlayQueue(List songs, MusicDirectory.Entry currentPlaying, int position, Context context, ProgressListener progressListener) throws Exception; + + PlayerQueue getPlayQueue(Context context, ProgressListener progressListener) throws Exception; + + void setInstance(Integer instance) throws Exception; +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/service/MusicServiceFactory.java b/app/src/main/java/github/nvllsvm/audinaut/service/MusicServiceFactory.java new file mode 100644 index 0000000..7a30acc --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/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 . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.service; + +import android.content.Context; +import github.nvllsvm.audinaut.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/nvllsvm/audinaut/service/OfflineException.java b/app/src/main/java/github/nvllsvm/audinaut/service/OfflineException.java new file mode 100644 index 0000000..d0c7b18 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/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 . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.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/nvllsvm/audinaut/service/OfflineMusicService.java b/app/src/main/java/github/nvllsvm/audinaut/service/OfflineMusicService.java new file mode 100644 index 0000000..c6e9bde --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/OfflineMusicService.java @@ -0,0 +1,638 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.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.nvllsvm.audinaut.domain.Artist; +import github.nvllsvm.audinaut.domain.Genre; +import github.nvllsvm.audinaut.domain.Indexes; +import github.nvllsvm.audinaut.domain.MusicDirectory.Entry; +import github.nvllsvm.audinaut.domain.PlayerQueue; +import github.nvllsvm.audinaut.domain.RemoteStatus; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.MusicFolder; +import github.nvllsvm.audinaut.domain.Playlist; +import github.nvllsvm.audinaut.domain.SearchCritera; +import github.nvllsvm.audinaut.domain.SearchResult; +import github.nvllsvm.audinaut.domain.User; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.FileUtil; +import github.nvllsvm.audinaut.util.Pair; +import github.nvllsvm.audinaut.util.ProgressListener; +import github.nvllsvm.audinaut.util.SilentBackgroundTask; +import github.nvllsvm.audinaut.util.SongDBHandler; +import github.nvllsvm.audinaut.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 Indexes getIndexes(String musicFolderId, boolean refresh, Context context, ProgressListener progressListener) throws Exception { + List artists = new ArrayList(); + List entries = new ArrayList<>(); + 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); + } else if(!file.getName().equals("albumart.jpg") && !file.getName().equals(".nomedia")) { + entries.add(createEntry(context, file)); + } + } + + Indexes indexes = new Indexes(0L, Collections.emptyList(), artists, entries); + 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 names = new HashSet(); + + 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 Entry createEntry(Context context, File file) { + return createEntry(context, file, getName(file)); + } + private Entry createEntry(Context context, File file, String name) { + return createEntry(context, file, name, true); + } + private Entry createEntry(Context context, File file, String name, boolean load) { + return createEntry(context, file, name, load, false); + } + private Entry createEntry(Context context, File file, String name, boolean load, boolean isPodcast) { + Entry entry; + entry = new 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()); + } + return entry; + } + + @Override + public Bitmap getCoverArt(Context context, 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, Entry song, long offset, int maxBitrate, SilentBackgroundTask task) throws Exception { + throw new OfflineException(ERRORMSG); + } + + @Override + public String getMusicUrl(Context context, Entry song, int maxBitrate) throws Exception { + throw new OfflineException(ERRORMSG); + } + + @Override + public List 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 artists = new ArrayList(); + List albums = new ArrayList(); + List songs = new ArrayList(); + 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() { + 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() { + public int compare(Entry lhs, 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() { + public int compare(Entry lhs, Entry rhs) { + if(lhs.getCloseness() == rhs.getCloseness()) { + return 0; + } + else if(lhs.getCloseness() > rhs.getCloseness()) { + return -1; + } + else { + return 1; + } + } + }); + + // Respect counts in search criteria + int artistCount = Math.min(artists.size(), criteria.getArtistCount()); + int albumCount = Math.min(albums.size(), criteria.getAlbumCount()); + int songCount = Math.min(songs.size(), criteria.getSongCount()); + artists = artists.subList(0, artistCount); + albums = albums.subList(0, albumCount); + songs = songs.subList(0, songCount); + + return new SearchResult(artists, albums, songs); + } + + private void recursiveAlbumSearch(String artistName, File file, SearchCritera criteria, Context context, List albums, List songs) { + int closeness; + for(File albumFile : FileUtil.listMediaFiles(file)) { + if(albumFile.isDirectory()) { + String albumName = getName(albumFile); + if((closeness = matchCriteria(criteria, albumName)) > 0) { + 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){ + 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) { + 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) { + if (criteria.getPattern().matcher(name).matches()) { + return Util.getStringDistance( + criteria.getQuery().toLowerCase(), + name.toLowerCase()); + } else { + return 0; + } + } + + @Override + public List getPlaylists(boolean refresh, Context context, ProgressListener progressListener) throws Exception { + List playlists = new ArrayList(); + 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 fileList = FileUtil.listFiles(folder); + for(File file: fileList) { + if(FileUtil.isPlaylistFile(file)) { + String id = file.getName(); + String filename = FileUtil.getBaseName(id); + String name = server + ": " + filename; + Playlist playlist = new Playlist(server, name); + playlist.setComment(filename); + + Reader reader = null; + BufferedReader buffer = null; + int songCount = 0; + try { + reader = new FileReader(file); + buffer = new BufferedReader(reader); + + String line = buffer.readLine(); + 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){ + songCount++; + } + } + + playlist.setSongCount(Integer.toString(songCount)); + } catch(Exception e) { + Log.w(TAG, "Failed to count songs in playlist", e); + } finally { + Util.close(buffer); + Util.close(reader); + } + + if(songCount > 0) { + 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 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 toAdd, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException(ERRORMSG); + } + + @Override + public void removeFromPlaylist(String id, List toRemove, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException(ERRORMSG); + } + + @Override + public void overwritePlaylist(String id, String name, int toRemove, List 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 MusicDirectory getAlbumList(String type, int size, int offset, boolean refresh, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException(ERRORMSG); + } + + @Override + public MusicDirectory getAlbumList(String type, String extra, int size, int offset, boolean refresh, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException(ERRORMSG); + } + + @Override + public MusicDirectory getSongList(String type, 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 List 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 children = new LinkedList(); + 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, Entry entry) 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 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 getBitmap(String url, int size, Context context, ProgressListener progressListener, SilentBackgroundTask task) throws Exception { + throw new OfflineException(ERRORMSG); + } + + @Override + public void savePlayQueue(List songs, 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 void setInstance(Integer instance) throws Exception{ + throw new OfflineException(ERRORMSG); + } + + private void listFilesRecursively(File parent, List 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/nvllsvm/audinaut/service/RESTMusicService.java b/app/src/main/java/github/nvllsvm/audinaut/service/RESTMusicService.java new file mode 100644 index 0000000..b742292 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/RESTMusicService.java @@ -0,0 +1,1366 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.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.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpRequestBase; +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.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.*; +import github.nvllsvm.audinaut.fragments.MainFragment; +import github.nvllsvm.audinaut.service.parser.EntryListParser; +import github.nvllsvm.audinaut.service.parser.ErrorParser; +import github.nvllsvm.audinaut.service.parser.GenreParser; +import github.nvllsvm.audinaut.service.parser.IndexesParser; +import github.nvllsvm.audinaut.service.parser.MusicDirectoryParser; +import github.nvllsvm.audinaut.service.parser.MusicFoldersParser; +import github.nvllsvm.audinaut.service.parser.PlayQueueParser; +import github.nvllsvm.audinaut.service.parser.PlaylistParser; +import github.nvllsvm.audinaut.service.parser.PlaylistsParser; +import github.nvllsvm.audinaut.service.parser.RandomSongsParser; +import github.nvllsvm.audinaut.service.parser.ScanStatusParser; +import github.nvllsvm.audinaut.service.parser.SearchResult2Parser; +import github.nvllsvm.audinaut.service.parser.SearchResultParser; +import github.nvllsvm.audinaut.service.parser.TopSongsParser; +import github.nvllsvm.audinaut.service.parser.UserParser; +import github.nvllsvm.audinaut.service.ssl.SSLSocketFactory; +import github.nvllsvm.audinaut.service.ssl.TrustSelfSignedStrategy; +import github.nvllsvm.audinaut.util.BackgroundTask; +import github.nvllsvm.audinaut.util.Pair; +import github.nvllsvm.audinaut.util.SilentBackgroundTask; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.FileUtil; +import github.nvllsvm.audinaut.util.ProgressListener; +import github.nvllsvm.audinaut.util.SongDBHandler; +import github.nvllsvm.audinaut.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); + } + } + + public List 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 parameterNames = new ArrayList(); + List parameterValues = new ArrayList(); + + 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()); + } + + 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 { + return searchNew(critera, context, progressListener); + } + + /** + * Search using the "search" REST method. + */ + private SearchResult searchOld(SearchCritera critera, Context context, ProgressListener progressListener) throws Exception { + List parameterNames = Arrays.asList("any", "songCount"); + List parameterValues = Arrays.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 { + List parameterNames = Arrays.asList("query", "artistCount", "albumCount", "songCount"); + List parameterValues = Arrays.asList(critera.getQuery(), critera.getArtistCount(), critera.getAlbumCount(), critera.getSongCount()); + + int instance = getInstance(context); + String method; + if(Util.isTagBrowsing(context, instance)) { + method = "search3"; + } else { + method = "search2"; + } + Reader reader = getReader(context, progressListener, method, 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 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 entries, Context context, ProgressListener progressListener) throws Exception { + List parameterNames = new LinkedList(); + List parameterValues = new LinkedList(); + + 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 toAdd, Context context, ProgressListener progressListener) throws Exception { + List names = new ArrayList(); + List values = new ArrayList(); + 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 toRemove, Context context, ProgressListener progressListener) throws Exception { + List names = new ArrayList(); + List values = new ArrayList(); + 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 toAdd, Context context, ProgressListener progressListener) throws Exception { + List names = new ArrayList(); + List values = new ArrayList(); + 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 { + Reader reader = getReader(context, progressListener, "updatePlaylist", null, Arrays.asList("playlistId", "name", "comment", "public"), Arrays.asList(id, name, comment, pub)); + try { + new ErrorParser(context, getInstance(context)).parse(reader); + } finally { + Util.close(reader); + } + } + + @Override + public MusicDirectory getAlbumList(String type, int size, int offset, boolean refresh, Context context, ProgressListener progressListener) throws Exception { + List names = new ArrayList(); + List values = new ArrayList(); + + 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); + } + } + + String method; + if(Util.isTagBrowsing(context, instance)) { + method = "getAlbumList2"; + } else { + method = "getAlbumList"; + } + + Reader reader = getReader(context, progressListener, method, null, names, values, true); + try { + return new EntryListParser(context, getInstance(context)).parse(reader, progressListener); + } finally { + Util.close(reader); + } + } + + @Override + public MusicDirectory getAlbumList(String type, String extra, int size, int offset, boolean refresh, Context context, ProgressListener progressListener) throws Exception { + List names = new ArrayList(); + List values = new ArrayList(); + + names.add("size"); + names.add("offset"); + + values.add(size); + values.add(offset); + + int instance = getInstance(context); + 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); + // Reverse chronological order only supported in 5.3+ + values.add(decade + 9); + values.add(decade); + } + + // Add folder if it was set and is non null + if(Util.getAlbumListsPerFolder(context, instance)) { + String folderId = Util.getSelectedMusicFolderId(context, instance); + if(folderId != null) { + names.add("musicFolderId"); + values.add(folderId); + } + } + + String method; + if(Util.isTagBrowsing(context, instance)) { + method = "getAlbumList2"; + } else { + method = "getAlbumList"; + } + + Reader reader = getReader(context, progressListener, method, null, names, values, true); + try { + return new EntryListParser(context, instance).parse(reader, progressListener); + } finally { + Util.close(reader); + } + } + + @Override + public MusicDirectory getSongList(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception { + List names = new ArrayList(); + List values = new ArrayList(); + + names.add("size"); + values.add(size); + names.add("offset"); + values.add(offset); + + String method; + switch(type) { + case MainFragment.SONGS_NEWEST: + method = "getNewaddedSongs"; + break; + case MainFragment.SONGS_TOP_PLAYED: + method = "getTopplayedSongs"; + break; + case MainFragment.SONGS_RECENT: + method = "getLastplayedSongs"; + break; + case MainFragment.SONGS_FREQUENT: + method = "getMostplayedSongs"; + break; + default: + method = "getNewaddedSongs"; + } + + Reader reader = getReader(context, progressListener, method, null, names, values, true); + try { + return new EntryListParser(context, getInstance(context)).parse(reader, progressListener); + } finally { + Util.close(reader); + } + } + + @Override + public MusicDirectory getRandomSongs(int size, String artistId, Context context, ProgressListener progressListener) throws Exception { + List names = new ArrayList(); + List values = new ArrayList(); + + names.add("id"); + names.add("count"); + + values.add(artistId); + values.add(size); + + int instance = getInstance(context); + String method; + 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 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 names = new ArrayList(); + List values = new ArrayList(); + + 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)) { + // Check to make sure user isn't doing 2015 -> 2010 since Subsonic will return no results + if(endYear != null && !"".equals(endYear)) { + try { + int startYearInt = Integer.parseInt(startYear); + int endYearInt = Integer.parseInt(endYear); + + if(startYearInt > endYearInt) { + String tmp = startYear; + startYear = endYear; + endYear = tmp; + } + } catch(Exception e) { + Log.w(TAG, "Failed to convert start/end year into ints", e); + } + } + + 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); + } + } + + @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 = 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 parameterNames = Arrays.asList("id"); + List parameterValues = Arrays.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
    headers = new ArrayList
    (); + if (offset > 0) { + headers.add(new BasicHeader("Range", "bytes=" + offset + "-")); + } + + List parameterNames = new ArrayList(); + parameterNames.add("id"); + parameterNames.add("maxBitRate"); + + List parameterValues = new ArrayList(); + parameterValues.add(song.getId()); + parameterValues.add(maxBitrate); + + 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()); + + // Allow user to specify to stream raw formats if available + builder.append("&maxBitRate=").append(maxBitrate); + + String url = builder.toString(); + url = rewriteUrlWithRedirect(context, url); + Log.i(TAG, "Using music URL: " + stripUrlInfo(url)); + return url; + } + + @Override + public List getGenres(boolean refresh, Context context, ProgressListener progressListener) throws Exception { + 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 { + HttpParams params = new BasicHttpParams(); + HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_GET_RANDOM_SONGS); + + List parameterNames = new ArrayList(); + List parameterValues = new ArrayList(); + + 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 parameterNames = new ArrayList(); + List parameterValues = new ArrayList(); + + parameterNames.add("artist"); + parameterValues.add(artist); + parameterNames.add("size"); + parameterValues.add(size); + + String method = "getTopSongs"; + Reader reader = getReader(context, progressListener, method, null, parameterNames, parameterValues); + try { + return new TopSongsParser(context, getInstance(context)).parse(reader, progressListener); + } 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.asList(username)); + try { + List 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 getUsers(boolean refresh, Context context, ProgressListener progressListener) throws Exception { + 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 names = new ArrayList(); + List values = new ArrayList(); + + 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()); + } + + if(user.getMusicFolderSettings() != null) { + for(User.Setting setting: user.getMusicFolderSettings()) { + if(setting.getValue()) { + names.add("musicFolderId"); + values.add(setting.getName()); + } + } + } + + 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 { + List names = new ArrayList(); + List values = new ArrayList(); + + 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()); + } + } + + if(user.getMusicFolderSettings() != null) { + for(User.Setting setting: user.getMusicFolderSettings()) { + if(setting.getValue()) { + names.add("musicFolderId"); + values.add(setting.getName()); + } + } + } + + 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.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.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.asList(username, password)); + try { + new ErrorParser(context, getInstance(context)).parse(reader); + } 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 void savePlayQueue(List songs, MusicDirectory.Entry currentPlaying, int position, Context context, ProgressListener progressListener) throws Exception { + List parameterNames = new LinkedList(); + List parameterValues = new LinkedList(); + + 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); + } + } + + 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) { + Pair cachedSongId = SongDBHandler.getHandler(context).getIdFromPath(Util.getRestUrlHash(context, getInstance(context)), id); + if(cachedSongId != null) { + id = cachedSongId.getSecond(); + } else { + String searchCriteria = Util.parseOfflineIDSearch(context, id, cacheLocn); + SearchCritera critera = new SearchCritera(searchCriteria, 0, 0, 1); + SearchResult result = searchNew(critera, context, progressListener); + if (result.getSongs().size() == 1) { + id = result.getSongs().get(0).getId(); + } + } + } + + 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.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.asList(parameterValue)); + } + + private Reader getReader(Context context, ProgressListener progressListener, String method, + HttpParams requestParams, List parameterNames, List parameterValues) throws Exception { + return getReader(context, progressListener, method, requestParams, parameterNames, parameterValues, false); + } + private Reader getReader(Context context, ProgressListener progressListener, String method, + HttpParams requestParams, List parameterNames, List 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 parameterNames, + List parameterValues, ProgressListener progressListener) throws Exception { + return getReaderForURL(context, url, requestParams, parameterNames, parameterValues, progressListener, true); + } + private Reader getReaderForURL(Context context, String url, HttpParams requestParams, List parameterNames, + List 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 parameterNames, + List 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 parameterNames, + List 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 parameterNames, + List 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 parameterNames, List parameterValues, + List
    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", "'"); + 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 parameterNames, List parameterValues, + List
    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 isCancelled = new AtomicReference(false); + int attempts = 0; + while (true) { + attempts++; + HttpContext httpContext = new BasicHttpContext(); + final HttpRequestBase request = (url.indexOf("rest") == -1) ? new HttpGet(url) : 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(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 && request instanceof HttpPost) { + List params = new ArrayList(); + for (int i = 0; i < parameterNames.size(); i++) { + params.add(new BasicNameValuePair(parameterNames.get(i), String.valueOf(parameterValues.get(i)))); + } + ((HttpPost) 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) { + request.addHeader("Accept-Encoding", "gzip"); + } + request.addHeader("User-Agent", Constants.REST_CLIENT_ID); + + // Set credentials to get through apache proxies that require authentication. + int instance = getInstance(context); + 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/nvllsvm/audinaut/service/parser/AbstractParser.java b/app/src/main/java/github/nvllsvm/audinaut/service/parser/AbstractParser.java new file mode 100644 index 0000000..2007d6f --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/parser/AbstractParser.java @@ -0,0 +1,159 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.service.parser; + +import java.io.IOException; +import java.io.Reader; + +import org.xmlpull.v1.XmlPullParser; + +import android.content.Context; +import android.util.Log; +import android.util.Xml; +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.Version; +import github.nvllsvm.audinaut.util.ProgressListener; +import github.nvllsvm.audinaut.util.Util; + +/** + * @author Sindre Mehus + */ +public abstract class AbstractParser { + private static final String TAG = AbstractParser.class.getSimpleName(); + private static final String SUBSONIC_RESPONSE = "subsonic-response"; + private static final String SUBSONIC = "subsonic"; + + 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 41: + Util.setBlockTokenUse(context, instance, true); + + // Throw IOException so RESTMusicService knows to retry + throw new IOException(); + 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); + try { + return (s == null || "".equals(s)) ? null : Integer.valueOf(s); + } catch(Exception e) { + Log.w(TAG, "Failed to parse " + s + " into integer"); + return null; + } + } + + 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 { + try { + return parser.next(); + } catch(Exception e) { + throw e; + } + } + + protected String getElementName() { + String name = parser.getName(); + if (SUBSONIC_RESPONSE.equals(name)) { + rootElementFound = true; + String version = get("version"); + } + 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/nvllsvm/audinaut/service/parser/EntryListParser.java b/app/src/main/java/github/nvllsvm/audinaut/service/parser/EntryListParser.java new file mode 100644 index 0000000..d49240c --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/parser/EntryListParser.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.service.parser; + +import android.content.Context; +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.util.ProgressListener; +import org.xmlpull.v1.XmlPullParser; + +import java.io.Reader; + +/** + * @author Sindre Mehus + */ +public class EntryListParser extends MusicDirectoryEntryParser { + + public EntryListParser(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(""); + if(get("isDir") == null) { + entry.setDirectory(true); + } + dir.addChild(entry); + } else if ("song".equals(name)) { + MusicDirectory.Entry entry = parseEntry(""); + 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/nvllsvm/audinaut/service/parser/ErrorParser.java b/app/src/main/java/github/nvllsvm/audinaut/service/parser/ErrorParser.java new file mode 100644 index 0000000..0c1ab2c --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/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 . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.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/nvllsvm/audinaut/service/parser/GenreParser.java b/app/src/main/java/github/nvllsvm/audinaut/service/parser/GenreParser.java new file mode 100644 index 0000000..ee43fa6 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/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 . + + Copyright 2010 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.service.parser; + +import android.content.Context; +import android.text.Html; +import android.util.Log; +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.Genre; +import github.nvllsvm.audinaut.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 parse(Reader reader, ProgressListener progressListener) throws Exception { + List result = new ArrayList(); + 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 (&apos;) + xml = xml.replaceAll("(?:&)(amp;|lt;|gt;|#37;|apos;)", "&$1"); + + // Replace unescaped ampersand + xml = xml.replaceAll("&(?!amp;|lt;|gt;|#37;|apos;)", "&"); + + // Replace unescaped percent symbol + // No replacements for <> at this time + xml = xml.replaceAll("%", "%"); + + xml = xml.replaceAll("'", "'"); + + 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/nvllsvm/audinaut/service/parser/IndexesParser.java b/app/src/main/java/github/nvllsvm/audinaut/service/parser/IndexesParser.java new file mode 100644 index 0000000..72a5a08 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/parser/IndexesParser.java @@ -0,0 +1,129 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.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.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.Artist; +import github.nvllsvm.audinaut.domain.Indexes; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.util.ProgressListener; +import android.util.Log; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.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 artists = new ArrayList(); + List shortcuts = new ArrayList(); + List entries = new ArrayList(); + Long lastModified = null; + int eventType; + String index = "#"; + String ignoredArticles = null; + boolean changed = false; + Map artistList = new HashMap(); + + 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); + + // Combine the id's for the two artists + if(artistList.containsKey(artist.getName())) { + Artist originalArtist = artistList.get(artist.getName()); + 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("*"); + 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); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/service/parser/MusicDirectoryEntryParser.java b/app/src/main/java/github/nvllsvm/audinaut/service/parser/MusicDirectoryEntryParser.java new file mode 100644 index 0000000..796714e --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/parser/MusicDirectoryEntryParser.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.service.parser; + +import android.content.Context; + +import github.nvllsvm.audinaut.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.setYear(getInteger("year")); + entry.setGenre(get("genre")); + entry.setAlbum(get("album")); + + 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.setDiscNumber(getInteger("discNumber")); + + String type = get("type"); + } 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.setDirectory(true); + + return entry; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/service/parser/MusicDirectoryParser.java b/app/src/main/java/github/nvllsvm/audinaut/service/parser/MusicDirectoryParser.java new file mode 100644 index 0000000..eb6ca6e --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/parser/MusicDirectoryParser.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.service.parser; + +import android.content.Context; +import android.util.Log; +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.ProgressListener; +import github.nvllsvm.audinaut.util.Util; +import org.xmlpull.v1.XmlPullParser; + +import java.io.Reader; +import java.util.HashMap; +import java.util.Map; + +import static github.nvllsvm.audinaut.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; + Map titleMap = new HashMap(); + do { + eventType = nextParseEvent(); + if (eventType == XmlPullParser.START_TAG) { + String name = getElementName(); + if ("child".equals(name) || "song".equals(name)) { + Entry entry = parseEntry(artist); + entry.setGrandParent(dir.getParent()); + + // Only check for songs + if(!entry.isDirectory()) { + // Check if duplicates + String disc = (entry.getDiscNumber() != null) ? Integer.toString(entry.getDiscNumber()) : ""; + String track = (entry.getTrack() != null) ? Integer.toString(entry.getTrack()) : ""; + String duplicateId = disc + "-" + track + "-" + entry.getTitle(); + + Entry duplicate = titleMap.get(duplicateId); + 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(duplicateId, 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(); + + return dir; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/service/parser/MusicFoldersParser.java b/app/src/main/java/github/nvllsvm/audinaut/service/parser/MusicFoldersParser.java new file mode 100644 index 0000000..dc1898b --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/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 . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.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.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.MusicFolder; +import github.nvllsvm.audinaut.util.ProgressListener; + +/** + * @author Sindre Mehus + */ +public class MusicFoldersParser extends AbstractParser { + + public MusicFoldersParser(Context context, int instance) { + super(context, instance); + } + + public List parse(Reader reader, ProgressListener progressListener) throws Exception { + init(reader); + + List result = new ArrayList(); + 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/nvllsvm/audinaut/service/parser/PlayQueueParser.java b/app/src/main/java/github/nvllsvm/audinaut/service/parser/PlayQueueParser.java new file mode 100644 index 0000000..a3dac5c --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/parser/PlayQueueParser.java @@ -0,0 +1,83 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.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.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.PlayerQueue; +import github.nvllsvm.audinaut.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 + 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/nvllsvm/audinaut/service/parser/PlaylistParser.java b/app/src/main/java/github/nvllsvm/audinaut/service/parser/PlaylistParser.java new file mode 100644 index 0000000..62da232 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/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 . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.service.parser; + +import android.content.Context; +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.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/nvllsvm/audinaut/service/parser/PlaylistsParser.java b/app/src/main/java/github/nvllsvm/audinaut/service/parser/PlaylistsParser.java new file mode 100644 index 0000000..36fb942 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/parser/PlaylistsParser.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.service.parser; + +import android.content.Context; + +import github.nvllsvm.audinaut.domain.Playlist; +import github.nvllsvm.audinaut.util.ProgressListener; +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 parse(Reader reader, ProgressListener progressListener) throws Exception { + init(reader); + + List result = new ArrayList(); + 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 pub = get("public"); + String created = get("created"); + String changed = get("changed"); + Integer duration = getInteger("duration"); + result.add(new Playlist(id, name, owner, comment, songCount, pub, created, changed, duration)); + } else if ("error".equals(tag)) { + handleError(); + } + } + } while (eventType != XmlPullParser.END_DOCUMENT); + + validate(); + + return Playlist.PlaylistComparator.sort(result); + } + +} \ No newline at end of file diff --git a/app/src/main/java/github/nvllsvm/audinaut/service/parser/RandomSongsParser.java b/app/src/main/java/github/nvllsvm/audinaut/service/parser/RandomSongsParser.java new file mode 100644 index 0000000..2e3453c --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/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 . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.service.parser; + +import android.content.Context; +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.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/nvllsvm/audinaut/service/parser/ScanStatusParser.java b/app/src/main/java/github/nvllsvm/audinaut/service/parser/ScanStatusParser.java new file mode 100644 index 0000000..e16db38 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/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 . + Copyright 2014 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.service.parser; + +import android.content.Context; +import org.xmlpull.v1.XmlPullParser; + +import java.io.Reader; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.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/nvllsvm/audinaut/service/parser/SearchResult2Parser.java b/app/src/main/java/github/nvllsvm/audinaut/service/parser/SearchResult2Parser.java new file mode 100644 index 0000000..24d29d7 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/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 . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.service.parser; + +import android.content.Context; +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.SearchResult; +import github.nvllsvm.audinaut.domain.Artist; +import github.nvllsvm.audinaut.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 artists = new ArrayList(); + List albums = new ArrayList(); + List songs = new ArrayList(); + 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/nvllsvm/audinaut/service/parser/SearchResultParser.java b/app/src/main/java/github/nvllsvm/audinaut/service/parser/SearchResultParser.java new file mode 100644 index 0000000..41ee05d --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/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 . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.service.parser; + +import android.content.Context; +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.SearchResult; +import github.nvllsvm.audinaut.domain.Artist; +import github.nvllsvm.audinaut.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 songs = new ArrayList(); + 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.emptyList(), Collections.emptyList(), songs); + } + +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/service/parser/SubsonicRESTException.java b/app/src/main/java/github/nvllsvm/audinaut/service/parser/SubsonicRESTException.java new file mode 100644 index 0000000..561e8eb --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/parser/SubsonicRESTException.java @@ -0,0 +1,19 @@ +package github.nvllsvm.audinaut.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/nvllsvm/audinaut/service/parser/TopSongsParser.java b/app/src/main/java/github/nvllsvm/audinaut/service/parser/TopSongsParser.java new file mode 100644 index 0000000..48650c7 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/parser/TopSongsParser.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 . + Copyright 2016 (C) Scott Jackson +*/ +package github.nvllsvm.audinaut.service.parser; + +import android.content.Context; + +import org.xmlpull.v1.XmlPullParser; + +import java.io.Reader; + +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.util.ProgressListener; + +public class TopSongsParser extends MusicDirectoryEntryParser { + + public TopSongsParser(Context context, int instance) { + super(context, instance); + } + + public MusicDirectory parse(Reader reader, ProgressListener progressListener) throws Exception { + init(reader); + + MusicDirectory dir = new MusicDirectory(); + int eventType; + int trackNumber = 1; + do { + eventType = nextParseEvent(); + if (eventType == XmlPullParser.START_TAG) { + String name = getElementName(); + if ("song".equals(name)) { + MusicDirectory.Entry entry = parseEntry(""); + entry.setTrack(trackNumber); + dir.addChild(entry); + + trackNumber++; + } 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/nvllsvm/audinaut/service/parser/UserParser.java b/app/src/main/java/github/nvllsvm/audinaut/service/parser/UserParser.java new file mode 100644 index 0000000..dbcdaed --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/parser/UserParser.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 . + Copyright 2014 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.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.nvllsvm.audinaut.domain.MusicFolder; +import github.nvllsvm.audinaut.domain.User; +import github.nvllsvm.audinaut.domain.User.MusicFolderSetting; +import github.nvllsvm.audinaut.domain.User.Setting; +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.service.MusicServiceFactory; +import github.nvllsvm.audinaut.util.ProgressListener; + +public class UserParser extends AbstractParser { + private static final String TAG = UserParser.class.getSimpleName(); + + public UserParser(Context context, int instance) { + super(context, instance); + } + + public List parse(Reader reader, ProgressListener progressListener) throws Exception { + init(reader); + List result = new ArrayList(); + List musicFolders = null; + User user = null; + int eventType; + + String tagName = null; + do { + eventType = nextParseEvent(); + if (eventType == XmlPullParser.START_TAG) { + tagName = getElementName(); + if ("user".equals(tagName)) { + user = new User(); + + user.setUsername(get("username")); + user.setEmail(get("email")); + for(String role: User.ROLES) { + parseSetting(user, role); + } + + result.add(user); + } else if ("error".equals(tagName)) { + handleError(); + } + } else if(eventType == XmlPullParser.TEXT) { + if("folder".equals(tagName)) { + String id = getText(); + if(musicFolders == null) { + musicFolders = getMusicFolders(); + } + + if(user != null) { + if(user.getMusicFolderSettings() == null) { + for (MusicFolder musicFolder : musicFolders) { + user.addMusicFolder(musicFolder); + } + } + + for(Setting musicFolder: user.getMusicFolderSettings()) { + if(musicFolder.getName().equals(id)) { + musicFolder.setValue(true); + break; + } + } + } + } + } + } while (eventType != XmlPullParser.END_DOCUMENT); + + validate(); + + return result; + } + + private List getMusicFolders() throws Exception{ + MusicService musicService = MusicServiceFactory.getMusicService(context); + return musicService.getMusicFolders(false, context, null); + } + + 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/nvllsvm/audinaut/service/ssl/SSLSocketFactory.java b/app/src/main/java/github/nvllsvm/audinaut/service/ssl/SSLSocketFactory.java new file mode 100644 index 0000000..b685ed7 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/ssl/SSLSocketFactory.java @@ -0,0 +1,553 @@ +/* + * ==================================================================== + * 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 + * . + * + */ + +package github.nvllsvm.audinaut.service.ssl; + +import android.os.Build; +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. + *

    + * 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. + *

    + * 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. + *

    + * Use JDK keytool utility to import a trusted certificate and generate a trust-store file: + *

    + *     keytool -import -alias "my server cert" -file server.crt -keystore my.truststore
    + *    
    + *

    + * 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. + *

    + * The following parameters can be used to customize the behavior of this + * class: + *

      + *
    • {@link org.apache.http.params.CoreConnectionPNames#CONNECTION_TIMEOUT}
    • + *
    • {@link org.apache.http.params.CoreConnectionPNames#SO_TIMEOUT}
    • + *
    + *

    + * 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 + *

    + * Use the following sequence of actions to generate a key-store file + *

    + *
      + *
    • + *

      + * Use JDK keytool utility to generate a new key + *

      keytool -genkey -v -alias "my client key" -validity 365 -keystore my.keystore
      + * For simplicity use the same password for the key as that of the key-store + *

      + *
    • + *
    • + *

      + * Issue a certificate signing request (CSR) + *

      keytool -certreq -alias "my client key" -file mycertreq.csr -keystore my.keystore
      + *

      + *
    • + *
    • + *

      + * 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. + *

      + *
    • + *
    • + *

      + * Import the trusted CA root certificate + *

      keytool -import -alias "my trusted ca" -file caroot.crt -keystore my.keystore
      + *

      + *
    • + *
    • + *

      + * Import the PKCS#7 file containg the complete certificate chain + *

      keytool -import -alias "my client key" -file mycert.p7 -keystore my.keystore
      + *

      + *
    • + *
    • + *

      + * Verify the content the resultant keystore file + *

      keytool -list -v -keystore my.keystore
      + *

      + *
    • + *
    + * + * @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. + *
    + * Derived classes may override this method to perform + * runtime checks, for example based on the cypher suite. + * + * @param sock the connected socket + * + * @return true + * + * @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 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 enabledCiphers = new ArrayList(Arrays.asList(ciphers)); + // On Android 5.0 release, Jetty doesn't seem to play nice with these ciphers + // Issue seems to have been fixed in M, and now won't work without them. Because Google + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1) { + 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/nvllsvm/audinaut/service/ssl/TrustManagerDecorator.java b/app/src/main/java/github/nvllsvm/audinaut/service/ssl/TrustManagerDecorator.java new file mode 100644 index 0000000..f56955c --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/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 + * . + * + */ +package github.nvllsvm.audinaut.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/nvllsvm/audinaut/service/ssl/TrustSelfSignedStrategy.java b/app/src/main/java/github/nvllsvm/audinaut/service/ssl/TrustSelfSignedStrategy.java new file mode 100644 index 0000000..e9c2f08 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/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 + * . + * + */ +package github.nvllsvm.audinaut.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/nvllsvm/audinaut/service/ssl/TrustStrategy.java b/app/src/main/java/github/nvllsvm/audinaut/service/ssl/TrustStrategy.java new file mode 100644 index 0000000..504407b --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/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 + * . + * + */ +package github.nvllsvm.audinaut.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. + *

    + * Please note that, if this method returns false, 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 true if the certificate can be trusted without verification by + * the trust manager, false 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/nvllsvm/audinaut/service/sync/AuthenticatorService.java b/app/src/main/java/github/nvllsvm/audinaut/service/sync/AuthenticatorService.java new file mode 100644 index 0000000..956e5b1 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/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 . + + Copyright 2009 (C) Sindre Mehus + */ + +package github.nvllsvm.audinaut.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/nvllsvm/audinaut/service/sync/SubsonicSyncAdapter.java b/app/src/main/java/github/nvllsvm/audinaut/service/sync/SubsonicSyncAdapter.java new file mode 100644 index 0000000..fb19e6f --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/service/sync/SubsonicSyncAdapter.java @@ -0,0 +1,200 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ + +package github.nvllsvm.audinaut.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.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.service.CachedMusicService; +import github.nvllsvm.audinaut.service.DownloadFile; +import github.nvllsvm.audinaut.service.RESTMusicService; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.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) { + String invalidMessage = isNetworkValid(); + if(invalidMessage != null) { + Log.w(TAG, "Not running sync: " + invalidMessage); + 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; + } + } + + executeSync(context); + } + + private String isNetworkValid() { + 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)) { + return "Not connected to any network"; + } + + // 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) { + return null; + } else { + return "Not connected to WIFI"; + } + } else { + return null; + } + } + protected void throwIfNetworkInvalid() throws NetworkNotValidException { + String invalidMessage = isNetworkValid(); + if(invalidMessage != null) { + throw new NetworkNotValidException(invalidMessage); + } + } + + 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); + try { + for (int i = 1; i <= servers; i++) { + try { + throwIfNetworkInvalid(); + + 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); + } + } + } catch (NetworkNotValidException e) { + Log.e(TAG, "Stopped sync due to network loss", e); + } + + Log.i(TAG, className + " executed in " + (System.currentTimeMillis() - start) + " ms"); + } + public void onExecuteSync(Context context, int instance) throws NetworkNotValidException { + + } + + protected boolean downloadRecursively(List paths, MusicDirectory parent, Context context, boolean save) throws Exception,NetworkNotValidException { + boolean downloaded = false; + for (MusicDirectory.Entry song: parent.getChildren(false, true)) { + DownloadFile file = new DownloadFile(context, song, save); + while(!(save && file.isSaved() || !save && file.isCompleteFileAvailable()) && !file.isFailedMax()) { + throwIfNetworkInvalid(); + 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")); + } + + public class NetworkNotValidException extends Throwable { + public NetworkNotValidException(String reason) { + super("Not running sync: " + reason); + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/updates/Updater.java b/app/src/main/java/github/nvllsvm/audinaut/updates/Updater.java new file mode 100644 index 0000000..6d48f3d --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/updates/Updater.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.updates; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.SilentBackgroundTask; +import github.nvllsvm.audinaut.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) { + // 5.2 should show as 520 instead of 52 + if(version < 100) { + version *= 10; + } + this.version = version; + } + + public void checkUpdates(Context context) { + this.context = context; + List updaters = new ArrayList(); + updaters.add(new UpdaterSongPress()); + + 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 { + 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/nvllsvm/audinaut/updates/UpdaterSongPress.java b/app/src/main/java/github/nvllsvm/audinaut/updates/UpdaterSongPress.java new file mode 100644 index 0000000..76d3107 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/updates/UpdaterSongPress.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 . + Copyright 2016 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.updates; + +import android.content.Context; +import android.content.SharedPreferences; + +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.Util; + +public class UpdaterSongPress extends Updater { + public UpdaterSongPress() { + super(521); + TAG = this.getClass().getSimpleName(); + } + + @Override + public void update(Context context) { + SharedPreferences prefs = Util.getPreferences(context); + boolean playNowAfter = prefs.getBoolean("playNowAfter", true); + + // Migrate the old preference so behavior stays the same + if(playNowAfter == false) { + SharedPreferences.Editor editor = prefs.edit(); + editor.putString(Constants.PREFERENCES_KEY_SONG_PRESS_ACTION, "single"); + editor.commit(); + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/BackgroundTask.java b/app/src/main/java/github/nvllsvm/audinaut/util/BackgroundTask.java new file mode 100644 index 0000000..092a087 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/BackgroundTask.java @@ -0,0 +1,325 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.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.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.view.ErrorDialog; + +/** + * @author Sindre Mehus + */ +public abstract class BackgroundTask 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 threads = Collections.synchronizedCollection(new ArrayList()); + protected static final BlockingQueue queue = new LinkedBlockingQueue(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 Context getContext() { + return context; + } + 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)); + } + + @Override + public void updateCache(int changeCode) { + + } + + 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()) { + try { + onDone(result); + } catch (Throwable t) { + if(!isCancelled()) { + try { + onError(t); + } catch(Exception e) { + // Don't care + } + } + } + } + + 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/nvllsvm/audinaut/util/CacheCleaner.java b/app/src/main/java/github/nvllsvm/audinaut/util/CacheCleaner.java new file mode 100644 index 0000000..ccd2150 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/CacheCleaner.java @@ -0,0 +1,292 @@ +package github.nvllsvm.audinaut.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.nvllsvm.audinaut.domain.Playlist; +import github.nvllsvm.audinaut.service.DownloadFile; +import github.nvllsvm.audinaut.service.DownloadService; +import github.nvllsvm.audinaut.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 playlists) { + new BackgroundPlaylistsCleanup(context, playlists).execute(); + } + + private void deleteEmptyDirs(List dirs, Set undeletable) { + for (File dir : dirs) { + if (undeletable.contains(dir)) { + continue; + } + + FileUtil.deleteEmptyDir(dir); + } + } + + private long getMinimumDelete(List files, List 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 files, Set 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 files, List pinned, List 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 files) { + Collections.sort(files, new Comparator() { + @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 findUndeletableFiles() { + Set undeletable = new HashSet(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 files = new ArrayList(); + 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 { + 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 files = new ArrayList(); + List pinned = new ArrayList(); + List dirs = new ArrayList(); + + findCandidatesForDeletion(FileUtil.getMusicDirectory(context), files, pinned, dirs); + sortByAscendingModificationTime(files); + + Set 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 { + 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 files = new ArrayList(); + List pinned = new ArrayList(); + List dirs = new ArrayList(); + findCandidatesForDeletion(FileUtil.getMusicDirectory(context), files, pinned, dirs); + + long bytesToDelete = getMinimumDelete(files, pinned); + if(bytesToDelete > 0L) { + sortByAscendingModificationTime(files); + Set 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 { + private final List playlists; + + public BackgroundPlaylistsCleanup(Context context, List playlists) { + super(context); + this.playlists = playlists; + } + + @Override + protected Void doInBackground() { + try { + String server = Util.getServerName(context); + SortedSet 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/nvllsvm/audinaut/util/Constants.java b/app/src/main/java/github/nvllsvm/audinaut/util/Constants.java new file mode 100644 index 0000000..c446755 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/Constants.java @@ -0,0 +1,180 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.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_SUBSONIC = "1.2.0"; + public static final String REST_CLIENT_ID = "Audinaut"; + 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_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_PLAY_LAST = "playLast"; + public static final String INTENT_EXTRA_ENTRY = "passedEntry"; + + // 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_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_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_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_KEEP_PLAYED_CNT = "keepPlayedCount"; + 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_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_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_CUSTOM_SORT_ENABLED = "customSortEnabled"; + public static final String PREFERENCES_KEY_SHARED_ENABLED = "sharedEnabled"; + public static final String PREFERENCES_KEY_OPEN_TO_TAB = "openToTab"; + // public static final String PREFERENCES_KEY_PLAY_NOW_AFTER = "playNowAfter"; + public static final String PREFERENCES_KEY_SONG_PRESS_ACTION = "songPressAction"; + public static final String PREFERENCES_KEY_LARGE_ALBUM_ART = "largeAlbumArt"; + 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_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_FIRST_LEVEL_ARTIST = "firstLevelArtist"; + public static final String PREFERENCES_KEY_START_ON_HEADPHONES = "startOnHeadphones"; + public static final String PREFERENCES_KEY_COLOR_ACTION_BAR = "colorActionBar"; + public static final String PREFERENCES_KEY_SHUFFLE_BY_ALBUM = "shuffleByAlbum"; + public static final String PREFERENCES_KEY_RESUME_PLAY_QUEUE_NEVER = "neverResumePlayQueue"; + public static final String PREFERENCES_KEY_BATCH_MODE = "batchMode"; + public static final String PREFERENCES_KEY_HEADS_UP_NOTIFICATION = "headsUpNotification"; + + 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 CACHE_AUDIO_SESSION_ID = "audioSessionId"; + public static final String CACHE_BLOCK_TOKEN_USE = "blockTokenUse"; + + public static final String MAIN_BACK_STACK = "backStackIds"; + public static final String MAIN_BACK_STACK_SIZE = "backStackIdsSize"; + public static final String MAIN_NOW_PLAYING = "nowPlayingId"; + public static final String MAIN_NOW_PLAYING_SECONDARY = "nowPlayingSecondaryId"; + public static final String MAIN_SLIDE_PANEL_STATE = "slidePanelState"; + 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.nvllsvm.audinaut_preferences"; + public static final String OFFLINE_SYNC_NAME = "github.nvllsvm.audinaut.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.nvllsvm.audinaut.playlists.provider"; + public static final String SYNC_ACCOUNT_MOST_RECENT_AUTHORITY = "github.nvllsvm.audinaut.mostrecent.provider"; + + public static final String TASKER_EXTRA_BUNDLE = "com.twofortyfouram.locale.intent.extra.BUNDLE"; + + public static final String ALBUM_ART_FILE = "albumart.jpg"; + + private Constants() { + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/DownloadFileItemHelperCallback.java b/app/src/main/java/github/nvllsvm/audinaut/util/DownloadFileItemHelperCallback.java new file mode 100644 index 0000000..bde9266 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/DownloadFileItemHelperCallback.java @@ -0,0 +1,115 @@ +package github.nvllsvm.audinaut.util; + +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.helper.ItemTouchHelper; +import android.util.Log; + +import org.eclipse.jetty.util.ArrayQueue; + +import java.util.Queue; + +import github.nvllsvm.audinaut.adapter.SectionAdapter; +import github.nvllsvm.audinaut.fragments.SubsonicFragment; +import github.nvllsvm.audinaut.service.DownloadFile; +import github.nvllsvm.audinaut.service.DownloadService; +import github.nvllsvm.audinaut.view.SongView; +import github.nvllsvm.audinaut.view.UpdateView; + +public class DownloadFileItemHelperCallback extends ItemTouchHelper.SimpleCallback { + private static final String TAG = DownloadFileItemHelperCallback.class.getSimpleName(); + + private SubsonicFragment fragment; + private boolean mainList; + + private BackgroundTask pendingTask = null; + private Queue pendingOperations = new ArrayQueue(); + + public DownloadFileItemHelperCallback(SubsonicFragment fragment, boolean mainList) { + super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT); + this.fragment = fragment; + this.mainList = mainList; + } + + @Override + public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder fromHolder, RecyclerView.ViewHolder toHolder) { + int from = fromHolder.getAdapterPosition(); + int to = toHolder.getAdapterPosition(); + getSectionAdapter().moveItem(from, to); + + synchronized (pendingOperations) { + pendingOperations.add(new Pair<>(from, to)); + updateDownloadService(); + } + return true; + } + + @Override + public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { + SongView songView = (SongView) ((UpdateView.UpdateViewHolder) viewHolder).getUpdateView(); + DownloadFile downloadFile = songView.getDownloadFile(); + + getSectionAdapter().removeItem(downloadFile); + synchronized (pendingOperations) { + pendingOperations.add(downloadFile); + updateDownloadService(); + } + } + + public DownloadService getDownloadService() { + return fragment.getDownloadService(); + } + public SectionAdapter getSectionAdapter() { + return fragment.getCurrentAdapter(); + } + + private void updateDownloadService() { + if(pendingTask == null) { + final DownloadService downloadService = getDownloadService(); + if(downloadService == null) { + return; + } + + pendingTask = new SilentBackgroundTask(downloadService) { + @Override + protected Void doInBackground() throws Throwable { + boolean running = true; + while(running) { + Object nextOperation = null; + synchronized (pendingOperations) { + if(!pendingOperations.isEmpty()) { + nextOperation = pendingOperations.remove(); + } + } + + if(nextOperation != null) { + if(nextOperation instanceof Pair) { + Pair swap = (Pair) nextOperation; + downloadService.swap(mainList, swap.getFirst(), swap.getSecond()); + } else if(nextOperation instanceof DownloadFile) { + DownloadFile downloadFile = (DownloadFile) nextOperation; + if(mainList) { + downloadService.remove(downloadFile); + } else { + downloadService.removeBackground(downloadFile); + } + } + } else { + running = false; + } + } + + synchronized (pendingOperations) { + pendingTask = null; + + // Start a task if this is non-empty. Means someone added while we were running operations + if(!pendingOperations.isEmpty()) { + updateDownloadService(); + } + } + return null; + } + }; + pendingTask.execute(); + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/DrawableTint.java b/app/src/main/java/github/nvllsvm/audinaut/util/DrawableTint.java new file mode 100644 index 0000000..d7585b8 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/DrawableTint.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 . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.util; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +import android.support.annotation.AttrRes; +import android.support.annotation.ColorRes; +import android.support.annotation.DrawableRes; +import android.util.TypedValue; + +import java.util.HashMap; +import java.util.Map; +import java.util.WeakHashMap; + +import github.nvllsvm.audinaut.R; + +public class DrawableTint { + private static final Map attrMap = new HashMap<>(); + private static final WeakHashMap tintedDrawables = new WeakHashMap<>(); + + public static Drawable getTintedDrawable(Context context, @DrawableRes int drawableRes) { + return getTintedDrawable(context, drawableRes, R.attr.colorAccent); + } + public static Drawable getTintedDrawable(Context context, @DrawableRes int drawableRes, @AttrRes int colorAttr) { + if(tintedDrawables.containsKey(drawableRes)) { + return tintedDrawables.get(drawableRes); + } + + int color = getColorRes(context, colorAttr); + Drawable background = context.getResources().getDrawable(drawableRes); + background.setColorFilter(color, PorterDuff.Mode.SRC_IN); + tintedDrawables.put(drawableRes, background); + return background; + } + public static Drawable getTintedDrawableFromColor(Context context, @DrawableRes int drawableRes, @ColorRes int colorRes) { + if(tintedDrawables.containsKey(drawableRes)) { + return tintedDrawables.get(drawableRes); + } + + int color = context.getResources().getColor(colorRes); + Drawable background = context.getResources().getDrawable(drawableRes); + background.setColorFilter(color, PorterDuff.Mode.SRC_IN); + tintedDrawables.put(drawableRes, background); + return background; + } + public static int getColorRes(Context context, @AttrRes int colorAttr) { + int color; + if(attrMap.containsKey(colorAttr)) { + color = attrMap.get(colorAttr); + } else { + TypedValue typedValue = new TypedValue(); + Resources.Theme theme = context.getTheme(); + theme.resolveAttribute(colorAttr, typedValue, true); + color = typedValue.data; + attrMap.put(colorAttr, color); + } + + return color; + } + public static int getDrawableRes(Context context, @AttrRes int drawableAttr) { + if(attrMap.containsKey(drawableAttr)) { + return attrMap.get(drawableAttr); + } else { + int[] attrs = new int[]{drawableAttr}; + TypedArray typedArray = context.obtainStyledAttributes(attrs); + @DrawableRes int drawableRes = typedArray.getResourceId(0, 0); + typedArray.recycle(); + attrMap.put(drawableAttr, drawableRes); + return drawableRes; + } + } + public static Drawable getTintedAttrDrawable(Context context, @AttrRes int drawableAttr, @AttrRes int colorAttr) { + if(tintedDrawables.containsKey(drawableAttr)) { + return getTintedDrawable(context, attrMap.get(drawableAttr), colorAttr); + } + + @DrawableRes int drawableRes = getDrawableRes(context, drawableAttr); + return getTintedDrawable(context, drawableRes, colorAttr); + } + + public static void wipeTintCache() { + attrMap.clear(); + tintedDrawables.clear(); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/EnvironmentVariables.java b/app/src/main/java/github/nvllsvm/audinaut/util/EnvironmentVariables.java new file mode 100644 index 0000000..30d6c23 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/EnvironmentVariables.java @@ -0,0 +1,20 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2016 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.util; + +public final class EnvironmentVariables { + public static final String PASTEBIN_DEV_KEY = ""; +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/FileUtil.java b/app/src/main/java/github/nvllsvm/audinaut/util/FileUtil.java new file mode 100644 index 0000000..ec4dc7c --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/FileUtil.java @@ -0,0 +1,800 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.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.nvllsvm.audinaut.domain.Artist; +import github.nvllsvm.audinaut.domain.Genre; +import github.nvllsvm.audinaut.domain.Indexes; +import github.nvllsvm.audinaut.domain.Playlist; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.MusicFolder; +import github.nvllsvm.audinaut.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 MUSIC_FILE_EXTENSIONS = Arrays.asList("mp3", "ogg", "aac", "flac", "m4a", "wav", "wma"); + private static final List PLAYLIST_FILE_EXTENSIONS = Arrays.asList("m3u"); + private static final int MAX_FILENAME_LENGTH = 254 - ".complete.mp3".length(); + private static File DEFAULT_MUSIC_DIR; + private static final Kryo kryo = new Kryo(); + private static HashMap entryLookup; + + static { + kryo.register(MusicDirectory.Entry.class); + kryo.register(Indexes.class); + kryo.register(Artist.class); + kryo.register(MusicFolder.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())); + if(fileName.length() >= MAX_FILENAME_LENGTH) { + fileName.setLength(MAX_FILENAME_LENGTH); + } + + fileName.append("."); + 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) { + if(entry.getId().indexOf(ImageLoader.PLAYLIST_PREFIX) != -1) { + File dir = getAlbumArtDirectory(context); + return new File(dir, Util.md5Hex(ImageLoader.PLAYLIST_PREFIX + entry.getTitle()) + ".jpeg"); + } else { + 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 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) { + 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())); + String folder = getMusicDirectory(context).getPath(); + if(entry.isDirectory()) { + folder += "/" + f.getPath(); + } else if(f.getParent() != null) { + folder += "/" + f.getParent(); + } + dir = new File(folder); + } 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(); + } + } + + // 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 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 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"); + + 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++) { + try { + if (dirs[i] != null && Environment.isExternalStorageRemovable(dirs[i])) { + return dirs[i]; + } + } catch (Exception e) { + Log.e(TAG, "Failed to check if is external", e); + } + } + } + + // 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 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 Audinaut 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 listFiles(File dir) { + File[] files = dir.listFiles(); + if (files == null) { + Log.w(TAG, "Failed to list children for " + dir.getPath()); + return new TreeSet(); + } + + return new TreeSet(Arrays.asList(files)); + } + + public static SortedSet listMediaFiles(File dir) { + SortedSet files = listFiles(dir); + Iterator 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); + } + + public static boolean isMusicFile(File file) { + String extension = getExtension(file.getName()); + return MUSIC_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 Long[] getUsedSize(Context context, File file) { + long number = 0L; + long permanent = 0L; + long size = 0L; + + if(file.isFile()) { + if(isMediaFile(file)) { + if(file.getAbsolutePath().indexOf(".complete") == -1) { + permanent++; + } + return new Long[] {1L, permanent, file.length()}; + } else { + return new Long[] {0L, 0L, 0L}; + } + } else { + for (File child : FileUtil.listFiles(file)) { + Long[] pair = getUsedSize(context, child); + number += pair[0]; + permanent += pair[1]; + size += pair[2]; + } + return new Long[] {number, permanent, size}; + } + } + + public static 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 deserialize(Context context, String fileName, Class tClass) { + return deserialize(context, fileName, tClass, 0); + } + + public static T deserialize(Context context, String fileName, Class 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 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 deserializeCompressed(Context context, String fileName, Class 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/nvllsvm/audinaut/util/ImageLoader.java b/app/src/main/java/github/nvllsvm/audinaut/util/ImageLoader.java new file mode 100644 index 0000000..1f9128c --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/ImageLoader.java @@ -0,0 +1,523 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.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.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 java.lang.ref.WeakReference; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.Playlist; +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.service.MusicServiceFactory; + +/** + * Asynchronous loading of images, with caching. + *

    + * There should normally be only one instance of this class. + * + * @author Sindre Mehus + */ +public class ImageLoader { + private static final String TAG = ImageLoader.class.getSimpleName(); + public static final String PLAYLIST_PREFIX = "pl-"; + + private Context context; + private LruCache cache; + private Handler handler; + private Bitmap nowPlaying; + private Bitmap nowPlayingSmall; + private final int imageSizeDefault; + private final int imageSizeLarge; + private boolean clearingCache = false; + private final int cacheSize; + + 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); + 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)); + + cache = new LruCache(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 && oldBitmap != nowPlayingSmall) || clearingCache) { + oldBitmap.recycle(); + } else if(oldBitmap != newBitmap) { + cache.put(key, oldBitmap); + } + } + } + }; + } + + public void clearCache() { + nowPlaying = null; + nowPlayingSmall = null; + new SilentBackgroundTask(context) { + @Override + protected Void doInBackground() throws Throwable { + clearingCache = true; + cache.evictAll(); + clearingCache = false; + return null; + } + }.execute(); + } + public void onLowMemory(float percent) { + Log.i(TAG, "Cache size: " + cache.size() + " => " + Math.round(cacheSize * (1 - percent)) + " out of " + cache.maxSize()); + cache.resize(Math.round(cacheSize * (1 - percent))); + } + public void onUIVisible() { + if(cache.maxSize() != cacheSize) { + Log.i(TAG, "Returned to full cache size"); + cache.resize(cacheSize); + } + } + + public void setNowPlayingSmall(Bitmap bitmap) { + nowPlayingSmall = bitmap; + } + + 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); + } + + if(bitmap != null && bitmap.isRecycled()) { + bitmap = null; + } + return bitmap; + } + + public SilentBackgroundTask loadImage(View view, MusicDirectory.Entry entry, boolean large, boolean crossfade) { + int size = large ? imageSizeLarge : imageSizeDefault; + return loadImage(view, entry, large, size, crossfade); + } + public SilentBackgroundTask loadImage(View view, MusicDirectory.Entry entry, boolean large, int size, boolean crossfade) { + // If we know this a artist, try to load artist info instead + if(entry != null && !entry.isAlbum() && !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, null, false); + } + ImageTask task = new ViewImageTask(view.getContext(), entry, size, imageSizeLarge, large, view, crossfade); + task.execute(); + return task; + } + + public SilentBackgroundTask 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; + } + setImage(view, null, false); + + SilentBackgroundTask task = new ViewUrlTask(view.getContext(), view, url, size); + task.execute(); + return task; + } + + public SilentBackgroundTask loadImage(View view, Playlist playlist, boolean large, boolean crossfade) { + MusicDirectory.Entry entry = new MusicDirectory.Entry(); + String id; + if(Util.isOffline(context)) { + id = PLAYLIST_PREFIX + playlist.getName(); + entry.setTitle(playlist.getComment()); + } else { + id = PLAYLIST_PREFIX + playlist.getId(); + entry.setTitle(playlist.getName()); + } + entry.setId(id); + entry.setCoverArt(id); + // So this isn't treated as a artist + entry.setParent(""); + + return loadImage(view, entry, large, crossfade); + } + + 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); + } + } + } + + public abstract class ImageTask extends SilentBackgroundTask { + private final Context mContext; + protected 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); + if(bitmap != null) { + 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; + } + } else { + bitmap = getUnknownImage(mEntry, mSize); + } + + 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 ArtistImageTask extends SilentBackgroundTask { + 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 { + try { + MusicService musicService = MusicServiceFactory.getMusicService(mContext); + + // Figure out whether we are going to get a artist image or the standard image + 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(); + } catch (Throwable x) { + Log.e(TAG, "Failed to get artist info", x); + cancelled.set(true); + } + 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 { + 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() { + + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/LoadingTask.java b/app/src/main/java/github/nvllsvm/audinaut/util/LoadingTask.java new file mode 100644 index 0000000..5436e09 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/LoadingTask.java @@ -0,0 +1,73 @@ +package github.nvllsvm.audinaut.util; + +import android.app.Activity; +import android.app.ProgressDialog; +import android.content.DialogInterface; + +import github.nvllsvm.audinaut.activity.SubsonicActivity; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public abstract class LoadingTask extends BackgroundTask { + + 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/nvllsvm/audinaut/util/MenuUtil.java b/app/src/main/java/github/nvllsvm/audinaut/util/MenuUtil.java new file mode 100644 index 0000000..c690a95 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/MenuUtil.java @@ -0,0 +1,83 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.util; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; +import android.view.Menu; + +import java.io.File; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.service.DownloadFile; +import github.nvllsvm.audinaut.view.AlbumView; +import github.nvllsvm.audinaut.view.ArtistEntryView; +import github.nvllsvm.audinaut.view.ArtistView; +import github.nvllsvm.audinaut.view.SongView; +import github.nvllsvm.audinaut.view.UpdateView; + +public final class MenuUtil { + private final static String TAG = MenuUtil.class.getSimpleName(); + + public static void hideMenuItems(Context context, Menu menu, UpdateView updateView) { + if(!Util.isOffline(context)) { + // If we are looking at a standard song view, get downloadFile to cache what options to show + if(updateView instanceof SongView) { + SongView songView = (SongView) updateView; + DownloadFile downloadFile = songView.getDownloadFile(); + + try { + if(downloadFile != null) { + if(downloadFile.isWorkDone()) { + // Remove permanent cache menu if already perma cached + if(downloadFile.isSaved()) { + menu.setGroupVisible(R.id.hide_pin, false); + } + + // Remove cache option no matter what if already downloaded + menu.setGroupVisible(R.id.hide_download, false); + } else { + // Remove delete option if nothing to delete + menu.setGroupVisible(R.id.hide_delete, false); + } + } + } catch(Exception e) { + Log.w(TAG, "Failed to lookup downloadFile info", e); + } + } + // Apply similar logic to album views + else if(updateView instanceof AlbumView || updateView instanceof ArtistView || updateView instanceof ArtistEntryView) { + File folder = null; + if(updateView instanceof AlbumView) { + folder = ((AlbumView) updateView).getFile(); + } else if(updateView instanceof ArtistView) { + folder = ((ArtistView) updateView).getFile(); + } else if(updateView instanceof ArtistEntryView) { + folder = ((ArtistEntryView) updateView).getFile(); + } + + try { + if(folder != null && !folder.exists()) { + menu.setGroupVisible(R.id.hide_delete, false); + } + } catch(Exception e) { + Log.w(TAG, "Failed to lookup album directory info", e); + } + } + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/Notifications.java b/app/src/main/java/github/nvllsvm/audinaut/util/Notifications.java new file mode 100644 index 0000000..7a28ecf --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/Notifications.java @@ -0,0 +1,330 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2014 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.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.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.activity.SubsonicActivity; +import github.nvllsvm.audinaut.activity.SubsonicFragmentActivity; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.PlayerState; +import github.nvllsvm.audinaut.provider.AudinautWidgetProvider; +import github.nvllsvm.audinaut.service.DownloadFile; +import github.nvllsvm.audinaut.service.DownloadService; +import github.nvllsvm.audinaut.view.UpdateView; + +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; + + private static boolean playShowing = false; + private static boolean downloadShowing = false; + private static boolean downloadForeground = false; + private static boolean persistentPlayingShowing = false; + + private final static Pair NOTIFICATION_TEXT_COLORS = new Pair(); + + 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; + } + 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); + notification.bigContentView = expandedContentView; + notification.priority = Notification.PRIORITY_HIGH; + } + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + notification.visibility = Notification.VISIBILITY_PUBLIC; + + if(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_HEADS_UP_NOTIFICATION, false) && !UpdateView.hasActiveActivity()) { + notification.vibrate = new long[0]; + } + } + + RemoteViews smallContentView = new RemoteViews(context.getPackageName(), R.layout.notification); + setupViews(smallContentView, context, song, false, playing); + notification.contentView = smallContentView; + + Intent notificationIntent = new Intent(context, SubsonicFragmentActivity.class); + notificationIntent.putExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD, true); + notificationIntent.addFlags(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; + persistentPlayingShowing = true; + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + downloadService.stopForeground(false); + notificationManager.notify(NOTIFICATION_ID_PLAYING, notification); + } + } + }); + } + + // Update widget + AudinautWidgetProvider.notifyInstances(context, downloadService, playing); + } + + private static void setupViews(RemoteViews rv, Context context, MusicDirectory.Entry song, boolean expanded, boolean playing) { + // 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 { + imageLoader.setNowPlayingSmall(bitmap); + 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_start); + + rv.setImageViewResource(R.id.control_previous, R.drawable.notification_backward); + rv.setImageViewResource(R.id.control_next, R.drawable.notification_forward); + } else { + rv.setImageViewResource(R.id.control_previous, playing ? R.drawable.notification_pause : R.drawable.notification_start); + rv.setImageViewResource(R.id.control_pause, R.drawable.notification_forward); + rv.setImageViewResource(R.id.control_next, R.drawable.notification_close); + } + } else { + // Necessary for switching back since it appears to re-use the same layout + rv.setImageViewResource(R.id.control_previous, R.drawable.notification_backward); + rv.setImageViewResource(R.id.control_next, R.drawable.notification_forward); + } + + // Create actions for media buttons + PendingIntent pendingIntent; + int previous = 0, pause = 0, next = 0, close = 0, rewind = 0, fastForward = 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(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_UP, KeyEvent.KEYCODE_MEDIA_PREVIOUS)); + pendingIntent = PendingIntent.getService(context, 0, prevIntent, 0); + rv.setOnClickPendingIntent(previous, pendingIntent); + } + if(rewind > 0) { + Intent rewindIntent = new Intent("KEYCODE_MEDIA_REWIND"); + rewindIntent.setComponent(new ComponentName(context, DownloadService.class)); + rewindIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_REWIND)); + pendingIntent = PendingIntent.getService(context, 0, rewindIntent, 0); + rv.setOnClickPendingIntent(rewind, 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_UP, 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_UP, 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_UP, KeyEvent.KEYCODE_MEDIA_NEXT)); + pendingIntent = PendingIntent.getService(context, 0, nextIntent, 0); + rv.setOnClickPendingIntent(next, pendingIntent); + } + if(fastForward > 0) { + Intent fastForwardIntent = new Intent("KEYCODE_MEDIA_FAST_FORWARD"); + fastForwardIntent.setComponent(new ComponentName(context, DownloadService.class)); + fastForwardIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD)); + pendingIntent = PendingIntent.getService(context, 0, fastForwardIntent, 0); + rv.setOnClickPendingIntent(fastForward, 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_UP, 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); + + if(persistentPlayingShowing) { + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.cancel(NOTIFICATION_ID_PLAYING); + persistentPlayingShowing = false; + } + } + }); + + // Get downloadNotification in foreground if playing + if(downloadShowing) { + showDownloadingNotification(context, downloadService, handler, downloadService.getCurrentDownloading(), downloadService.getBackgroundDownloads().size()); + } + + // Update widget + AudinautWidgetProvider.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_VIEW, true); + notificationIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + builder.setContentIntent(PendingIntent.getActivity(context, 2, 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); + } + }); + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/Pair.java b/app/src/main/java/github/nvllsvm/audinaut/util/Pair.java new file mode 100644 index 0000000..5d45533 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/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 . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.util; + +import java.io.Serializable; + +/** + * @author Sindre Mehus + */ +public class Pair 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/nvllsvm/audinaut/util/ProgressListener.java b/app/src/main/java/github/nvllsvm/audinaut/util/ProgressListener.java new file mode 100644 index 0000000..aed50d2 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/ProgressListener.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.util; + +/** + * @author Sindre Mehus + */ +public interface ProgressListener { + void updateProgress(String message); + void updateProgress(int messageId); + void updateCache(int changeCode); +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/SettingsBackupAgent.java b/app/src/main/java/github/nvllsvm/audinaut/util/SettingsBackupAgent.java new file mode 100644 index 0000000..bb31699 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/SettingsBackupAgent.java @@ -0,0 +1,44 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus +*/ +package github.nvllsvm.audinaut.util; + +import android.app.backup.BackupAgentHelper; +import android.app.backup.BackupDataInput; +import android.app.backup.SharedPreferencesBackupHelper; +import android.os.ParcelFileDescriptor; + +import java.io.IOError; +import java.io.IOException; + +import github.nvllsvm.audinaut.util.Constants; + +public class SettingsBackupAgent extends BackupAgentHelper { + @Override + public void onCreate() { + super.onCreate(); + SharedPreferencesBackupHelper helper = new SharedPreferencesBackupHelper(this, Constants.PREFERENCES_FILE_NAME); + addHelper("mypreferences", helper); + } + + @Override + public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) throws IOException{ + super.onRestore(data, appVersionCode, newState); + Util.getPreferences(this).edit().remove(Constants.PREFERENCES_KEY_CACHE_LOCATION).apply(); + } + } diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/ShufflePlayBuffer.java b/app/src/main/java/github/nvllsvm/audinaut/util/ShufflePlayBuffer.java new file mode 100644 index 0000000..284218f --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/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 . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.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.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.service.DownloadService; +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.service.MusicServiceFactory; +import github.nvllsvm.audinaut.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 buffer = new ArrayList(); + 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 = Math.max(1, 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 get(int size) { + clearBufferIfnecessary(); + // Make sure fetcher is running if needed + restart(); + + List result = new ArrayList(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)) { + 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/nvllsvm/audinaut/util/SilentBackgroundTask.java b/app/src/main/java/github/nvllsvm/audinaut/util/SilentBackgroundTask.java new file mode 100644 index 0000000..53e9a89 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/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 . + + Copyright 2010 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.util; + +import android.content.Context; + +/** + * @author Sindre Mehus + */ +public abstract class SilentBackgroundTask extends BackgroundTask { + 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/nvllsvm/audinaut/util/SilentServiceTask.java b/app/src/main/java/github/nvllsvm/audinaut/util/SilentServiceTask.java new file mode 100644 index 0000000..05c79f0 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/SilentServiceTask.java @@ -0,0 +1,41 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.util; + +import android.content.Context; + +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.service.MusicServiceFactory; + +public abstract class SilentServiceTask extends SilentBackgroundTask { + protected MusicService musicService; + + public SilentServiceTask(Context context) { + super(context); + } + + @Override + protected T doInBackground() throws Throwable { + musicService = MusicServiceFactory.getMusicService(getContext()); + return doInBackground(musicService); + } + + protected abstract T doInBackground(MusicService musicService) throws Throwable; +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/SimpleServiceBinder.java b/app/src/main/java/github/nvllsvm/audinaut/util/SimpleServiceBinder.java new file mode 100644 index 0000000..eb5fd6d --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/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 . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.util; + +import android.os.Binder; + +/** + * @author Sindre Mehus + */ +public class SimpleServiceBinder 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/nvllsvm/audinaut/util/SongDBHandler.java b/app/src/main/java/github/nvllsvm/audinaut/util/SongDBHandler.java new file mode 100644 index 0000000..203effd --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/SongDBHandler.java @@ -0,0 +1,260 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.util; + +import android.content.ContentValues; +import android.content.Context; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +import java.util.ArrayList; +import java.util.List; + +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.service.DownloadFile; + +public class SongDBHandler extends SQLiteOpenHelper { + private static final String TAG = SongDBHandler.class.getSimpleName(); + private static SongDBHandler dbHandler; + + private static final int DATABASE_VERSION = 2; + public static final String DATABASE_NAME = "SongsDB"; + + public static final String TABLE_SONGS = "RegisteredSongs"; + public static final String SONGS_ID = "id"; + public static final String SONGS_SERVER_KEY = "serverKey"; + public static final String SONGS_SERVER_ID = "serverId"; + public static final String SONGS_COMPLETE_PATH = "completePath"; + public static final String SONGS_LAST_PLAYED = "lastPlayed"; + public static final String SONGS_LAST_COMPLETED = "lastCompleted"; + + private Context context; + + private SongDBHandler(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + this.context = context; + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL("CREATE TABLE " + TABLE_SONGS + " ( " + + SONGS_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + + SONGS_SERVER_KEY + " INTEGER NOT NULL, " + + SONGS_SERVER_ID + " TEXT NOT NULL, " + + SONGS_COMPLETE_PATH + " TEXT NOT NULL, " + + SONGS_LAST_PLAYED + " INTEGER, " + + SONGS_LAST_COMPLETED + " INTEGER, " + + "UNIQUE(" + SONGS_SERVER_KEY + ", " + SONGS_SERVER_ID + "))"); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + db.execSQL("DROP TABLE IF EXISTS " + TABLE_SONGS); + this.onCreate(db); + } + + public synchronized void addSong(DownloadFile downloadFile) { + addSong(Util.getMostRecentActiveServer(context), downloadFile); + } + public synchronized void addSong(int instance, DownloadFile downloadFile) { + SQLiteDatabase db = this.getWritableDatabase(); + addSong(db, instance, downloadFile); + db.close(); + } + protected synchronized void addSong(SQLiteDatabase db, DownloadFile downloadFile) { + addSong(db, Util.getMostRecentActiveServer(context), downloadFile); + } + protected synchronized void addSong(SQLiteDatabase db, int instance, DownloadFile downloadFile) { + addSong(db, instance, downloadFile.getSong().getId(), downloadFile.getSaveFile().getAbsolutePath()); + } + + protected synchronized void addSong(SQLiteDatabase db, String id, String absolutePath) { + addSong(db, Util.getMostRecentActiveServer(context), id, absolutePath); + } + protected synchronized void addSong(SQLiteDatabase db, int instance, String id, String absolutePath) { + addSongImpl(db, Util.getRestUrlHash(context, instance), id, absolutePath); + } + protected synchronized void addSongImpl(SQLiteDatabase db, int serverKey, String id, String absolutePath) { + ContentValues values = new ContentValues(); + values.put(SONGS_SERVER_KEY, serverKey); + values.put(SONGS_SERVER_ID, id); + values.put(SONGS_COMPLETE_PATH, absolutePath); + + db.insertWithOnConflict(TABLE_SONGS, null, values, SQLiteDatabase.CONFLICT_IGNORE); + } + + public synchronized void addSongs(int instance, List entries) { + SQLiteDatabase db = this.getWritableDatabase(); + + List> pairs = new ArrayList<>(); + for(MusicDirectory.Entry entry: entries) { + pairs.add(new Pair<>(entry.getId(), FileUtil.getSongFile(context, entry).getAbsolutePath())); + } + addSongs(db, instance, pairs); + + db.close(); + } + public synchronized void addSongs(SQLiteDatabase db, int instance, List> entries) { + addSongsImpl(db, Util.getRestUrlHash(context, instance), entries); + } + protected synchronized void addSongsImpl(SQLiteDatabase db, int serverKey, List> entries) { + db.beginTransaction(); + try { + for (Pair entry : entries) { + ContentValues values = new ContentValues(); + values.put(SONGS_SERVER_KEY, serverKey); + values.put(SONGS_SERVER_ID, entry.getFirst()); + values.put(SONGS_COMPLETE_PATH, entry.getSecond()); + // Util.sleepQuietly(10000); + + db.insertWithOnConflict(TABLE_SONGS, null, values, SQLiteDatabase.CONFLICT_IGNORE); + } + + db.setTransactionSuccessful(); + } catch(Exception e) {} + + db.endTransaction(); + } + + public synchronized void setSongPlayed(DownloadFile downloadFile, boolean submission) { + // TODO: In case of offline want to update all matches + Pair pair = getOnlineSongId(downloadFile); + if(pair == null) { + return; + } + int serverKey = pair.getFirst(); + String id = pair.getSecond(); + + // Open and make sure song is in db + SQLiteDatabase db = this.getWritableDatabase(); + addSongImpl(db, serverKey, id, downloadFile.getSaveFile().getAbsolutePath()); + + // Update song's last played + ContentValues values = new ContentValues(); + values.put(submission ? SONGS_LAST_COMPLETED : SONGS_LAST_PLAYED, System.currentTimeMillis()); + db.update(TABLE_SONGS, values, SONGS_SERVER_KEY + " = ? AND " + SONGS_SERVER_ID + " = ?", new String[]{Integer.toString(serverKey), id}); + db.close(); + } + + public boolean hasBeenPlayed(MusicDirectory.Entry entry) { + Long[] lastPlayed = getLastPlayed(entry); + return lastPlayed != null && lastPlayed[0] != null && lastPlayed[0] > 0; + } + public boolean hasBeenCompleted(MusicDirectory.Entry entry) { + Long[] lastPlayed = getLastPlayed(entry); + return lastPlayed != null && lastPlayed[1] != null && lastPlayed[1] > 0; + } + public synchronized Long[] getLastPlayed(MusicDirectory.Entry entry) { + return getLastPlayed(getOnlineSongId(entry)); + } + protected synchronized Long[] getLastPlayed(Pair pair) { + if(pair == null) { + return null; + } else { + return getLastPlayed(pair.getFirst(), pair.getSecond()); + } + } + public synchronized Long[] getLastPlayed(int serverKey, String id) { + SQLiteDatabase db = this.getReadableDatabase(); + + String[] columns = {SONGS_LAST_PLAYED, SONGS_LAST_COMPLETED}; + Cursor cursor = db.query(TABLE_SONGS, columns, SONGS_SERVER_KEY + " = ? AND " + SONGS_SERVER_ID + " = ?", new String[]{Integer.toString(serverKey), id}, null, null, null, null); + + try { + cursor.moveToFirst(); + + Long[] dates = new Long[2]; + dates[0] = cursor.getLong(0); + dates[1] = cursor.getLong(1); + return dates; + } catch(Exception e) { + return null; + } + finally { + db.close(); + } + } + + public synchronized Pair getOnlineSongId(MusicDirectory.Entry entry) { + return getOnlineSongId(Util.getRestUrlHash(context), entry.getId(), FileUtil.getSongFile(context, entry).getAbsolutePath(), Util.isOffline(context) ? false : true); + } + public synchronized Pair getOnlineSongId(DownloadFile downloadFile) { + return getOnlineSongId(Util.getRestUrlHash(context), downloadFile.getSong().getId(), downloadFile.getSaveFile().getAbsolutePath(), Util.isOffline(context) ? false : true); + } + + public synchronized Pair getOnlineSongId(int serverKey, MusicDirectory.Entry entry) { + return getOnlineSongId(serverKey, new DownloadFile(context, entry, true)); + } + public synchronized Pair getOnlineSongId(int serverKey, DownloadFile downloadFile) { + return getOnlineSongId(serverKey, downloadFile.getSong().getId(), downloadFile.getSaveFile().getAbsolutePath(), true); + } + public synchronized Pair getOnlineSongId(int serverKey, String id, String savePath, boolean requireServerKey) { + SharedPreferences prefs = Util.getPreferences(context); + String cacheLocn = prefs.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, null); + if(cacheLocn != null && id.indexOf(cacheLocn) != -1) { + if(requireServerKey) { + return getIdFromPath(serverKey, savePath); + } else { + return getIdFromPath(savePath); + } + } else { + return new Pair<>(serverKey, id); + } + } + + public synchronized Pair getIdFromPath(String path) { + SQLiteDatabase db = this.getReadableDatabase(); + + String[] columns = {SONGS_SERVER_KEY, SONGS_SERVER_ID}; + Cursor cursor = db.query(TABLE_SONGS, columns, SONGS_COMPLETE_PATH + " = ?", new String[] { path }, null, null, SONGS_LAST_PLAYED + " DESC", null); + + try { + cursor.moveToFirst(); + return new Pair(cursor.getInt(0), cursor.getString(1)); + } catch(Exception e) { + return null; + } + finally { + db.close(); + } + } + public synchronized Pair getIdFromPath(int serverKey, String path) { + SQLiteDatabase db = this.getReadableDatabase(); + + String[] columns = {SONGS_SERVER_KEY, SONGS_SERVER_ID}; + Cursor cursor = db.query(TABLE_SONGS, columns, SONGS_SERVER_KEY + " = ? AND " + SONGS_COMPLETE_PATH + " = ?", new String[] {Integer.toString(serverKey), path }, null, null, null, null); + + try { + cursor.moveToFirst(); + return new Pair(cursor.getInt(0), cursor.getString(1)); + } catch(Exception e) { + return null; + } + finally { + db.close(); + } + } + + public static SongDBHandler getHandler(Context context) { + if(dbHandler == null) { + dbHandler = new SongDBHandler(context); + } + + return dbHandler; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/SyncUtil.java b/app/src/main/java/github/nvllsvm/audinaut/util/SyncUtil.java new file mode 100644 index 0000000..e3d9873 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/SyncUtil.java @@ -0,0 +1,156 @@ +package github.nvllsvm.audinaut.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.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.activity.SubsonicFragmentActivity; + +/** + * Created by Scott on 11/24/13. + */ +public final class SyncUtil { + private static String TAG = SyncUtil.class.getSimpleName(); + private static ArrayList syncedPlaylists; + 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; + 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 getSyncedPlaylists(Context context) { + return getSyncedPlaylists(context, Util.getActiveServer(context)); + } + public static ArrayList getSyncedPlaylists(Context context, int instance) { + String syncFile = getPlaylistSyncFile(context, instance); + ArrayList playlists = FileUtil.deserializeCompressed(context, syncFile, ArrayList.class); + if(playlists == null) { + playlists = new ArrayList(); + + // Try to convert old style into new style + ArrayList 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 playlists) { + FileUtil.serializeCompressed(context, playlists, getPlaylistSyncFile(context, instance)); + } + public static void addSyncedPlaylist(Context context, String playlistId) { + String playlistFile = getPlaylistSyncFile(context); + ArrayList 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 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"; + } + + // Most Recently Added + public static ArrayList getSyncedMostRecent(Context context, int instance) { + ArrayList list = FileUtil.deserialize(context, getMostRecentSyncFile(context, instance), ArrayList.class); + if(list == null) { + list = new ArrayList(); + } + 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 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 synced; + + protected SyncSet() { + + } + public SyncSet(String id) { + this.id = id; + } + public SyncSet(String id, List 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/nvllsvm/audinaut/util/TabBackgroundTask.java b/app/src/main/java/github/nvllsvm/audinaut/util/TabBackgroundTask.java new file mode 100644 index 0000000..bcba00e --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/TabBackgroundTask.java @@ -0,0 +1,51 @@ +package github.nvllsvm.audinaut.util; + +import github.nvllsvm.audinaut.fragments.SubsonicFragment; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public abstract class TabBackgroundTask extends BackgroundTask { + + 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/nvllsvm/audinaut/util/ThemeUtil.java b/app/src/main/java/github/nvllsvm/audinaut/util/ThemeUtil.java new file mode 100644 index 0000000..173dfca --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/ThemeUtil.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 . + Copyright 2016 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.util; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.res.Configuration; + +import java.util.Locale; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.activity.SettingsActivity; +import github.nvllsvm.audinaut.activity.SubsonicFragmentActivity; + +public final class ThemeUtil { + public static final String THEME_DARK = "dark"; + public static final String THEME_BLACK = "black"; + public static final String THEME_LIGHT = "light"; + public static final String THEME_DAY_NIGHT = "day/night"; + public static final String THEME_DAY_BLACK_NIGHT = "day/black"; + + public static String getTheme(Context context) { + SharedPreferences prefs = Util.getPreferences(context); + String theme = prefs.getString(Constants.PREFERENCES_KEY_THEME, null); + + if(THEME_DAY_NIGHT.equals(theme)) { + int currentNightMode = context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; + if(currentNightMode == Configuration.UI_MODE_NIGHT_YES) { + theme = THEME_DARK; + } else { + theme = THEME_LIGHT; + } + } else if(THEME_DAY_BLACK_NIGHT.equals(theme)) { + int currentNightMode = context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; + if(currentNightMode == Configuration.UI_MODE_NIGHT_YES) { + theme = THEME_BLACK; + } else { + theme = THEME_LIGHT; + } + } + + return theme; + } + public static int getThemeRes(Context context) { + return getThemeRes(context, getTheme(context)); + } + public static int getThemeRes(Context context, String theme) { + if(context instanceof SubsonicFragmentActivity || context instanceof SettingsActivity) { + if(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_COLOR_ACTION_BAR, true)) { + if (THEME_DARK.equals(theme)) { + return R.style.Theme_Audinaut_Dark_No_Actionbar; + } else if (THEME_BLACK.equals(theme)) { + return R.style.Theme_Audinaut_Black_No_Actionbar; + } else { + return R.style.Theme_Audinaut_Light_No_Actionbar; + } + } else { + if (THEME_DARK.equals(theme)) { + return R.style.Theme_Audinaut_Dark_No_Color; + } else if (THEME_BLACK.equals(theme)) { + return R.style.Theme_Audinaut_Black_No_Color; + } else { + return R.style.Theme_Audinaut_Light_No_Color; + } + } + } else { + if (THEME_DARK.equals(theme)) { + return R.style.Theme_Audinaut_Dark; + } else if (THEME_BLACK.equals(theme)) { + return R.style.Theme_Audinaut_Black; + } else { + return R.style.Theme_Audinaut_Light; + } + } + } + public static void setTheme(Context context, String theme) { + SharedPreferences.Editor editor = Util.getPreferences(context).edit(); + editor.putString(Constants.PREFERENCES_KEY_THEME, theme); + editor.commit(); + } + + public static void applyTheme(Context context, String theme) { + context.setTheme(getThemeRes(context, theme)); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/TimeLimitedCache.java b/app/src/main/java/github/nvllsvm/audinaut/util/TimeLimitedCache.java new file mode 100644 index 0000000..072b15b --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/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 . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.util; + +import java.lang.ref.SoftReference; +import java.util.concurrent.TimeUnit; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public class TimeLimitedCache { + + private SoftReference 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(value); + expires = System.currentTimeMillis() + timeUnit.toMillis(ttl); + } + + public void clear() { + expires = 0L; + value = null; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/UpdateHelper.java b/app/src/main/java/github/nvllsvm/audinaut/util/UpdateHelper.java new file mode 100644 index 0000000..212d442 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/UpdateHelper.java @@ -0,0 +1,92 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.util; + +import android.app.Activity; +import android.content.Context; +import android.content.DialogInterface; +import android.support.v7.app.AlertDialog; +import android.util.Log; +import android.view.View; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.Artist; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.MusicDirectory.Entry; +import github.nvllsvm.audinaut.fragments.SubsonicFragment; +import github.nvllsvm.audinaut.service.DownloadFile; +import github.nvllsvm.audinaut.service.DownloadService; +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.service.MusicServiceFactory; +import github.nvllsvm.audinaut.service.OfflineException; +import github.nvllsvm.audinaut.view.UpdateView; + +public final class UpdateHelper { + private static final String TAG = UpdateHelper.class.getSimpleName(); + + public static abstract class EntryInstanceUpdater { + private Entry entry; + protected int metadataUpdate = DownloadService.METADATA_UPDATED_ALL; + + public EntryInstanceUpdater(Entry entry) { + this.entry = entry; + } + public EntryInstanceUpdater(Entry entry, int metadataUpdate) { + this.entry = entry; + this.metadataUpdate = metadataUpdate; + } + + public abstract void update(Entry found); + + public void execute() { + DownloadService downloadService = DownloadService.getInstance(); + if(downloadService != null && !entry.isDirectory()) { + boolean serializeChanges = false; + List downloadFiles = downloadService.getDownloads(); + DownloadFile currentPlaying = downloadService.getCurrentPlaying(); + + for(DownloadFile file: downloadFiles) { + Entry check = file.getSong(); + if(entry.getId().equals(check.getId())) { + update(check); + serializeChanges = true; + + if(currentPlaying != null && currentPlaying.getSong() != null && currentPlaying.getSong().getId().equals(entry.getId())) { + downloadService.onMetadataUpdate(metadataUpdate); + } + } + } + + if(serializeChanges) { + downloadService.serializeQueue(); + } + } + + Entry find = UpdateView.findEntry(entry); + if(find != null) { + update(find); + } + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/UserUtil.java b/app/src/main/java/github/nvllsvm/audinaut/util/UserUtil.java new file mode 100644 index 0000000..ece11de --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/UserUtil.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 . + Copyright 2014 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.util; + +import android.app.Activity; +import android.support.v7.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.WindowManager; +import android.widget.TextView; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.adapter.SectionAdapter; +import github.nvllsvm.audinaut.domain.User; +import github.nvllsvm.audinaut.fragments.SubsonicFragment; +import github.nvllsvm.audinaut.service.DownloadService; +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.service.MusicServiceFactory; +import github.nvllsvm.audinaut.service.OfflineException; +import github.nvllsvm.audinaut.adapter.SettingsAdapter; +import github.nvllsvm.audinaut.view.UpdateView; + +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 int instanceHash = -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); + final int instanceHash = (instance == 0) ? 0 : Util.getRestUrl(context, null).hashCode(); + if(UserUtil.instance == instance && UserUtil.instanceHash == instanceHash && currentUser != null) { + return; + } else { + UserUtil.instance = instance; + UserUtil.instanceHash = instanceHash; + } + + new SilentBackgroundTask(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(); + + return null; + } + + @Override + protected void done(Void result) { + if(context instanceof AppCompatActivity) { + ((AppCompatActivity) 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)); + } + +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/Util.java b/app/src/main/java/github/nvllsvm/audinaut/util/Util.java new file mode 100644 index 0000000..4f684c4 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/Util.java @@ -0,0 +1,1389 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) 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 . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.util; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.graphics.Color; +import android.support.annotation.StringRes; +import android.support.v7.app.AlertDialog; +import android.content.ClipboardManager; +import android.content.ClipData; +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.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.util.SparseArray; +import android.view.View; +import android.view.Gravity; +import android.view.Window; +import android.view.WindowManager; +import android.widget.AdapterView; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.activity.SettingsActivity; +import github.nvllsvm.audinaut.activity.SubsonicFragmentActivity; +import github.nvllsvm.audinaut.adapter.DetailsAdapter; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.PlayerState; +import github.nvllsvm.audinaut.domain.RepeatMode; +import github.nvllsvm.audinaut.receiver.MediaButtonIntentReceiver; +import github.nvllsvm.audinaut.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.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.Random; +import java.util.TimeZone; + +/** + * @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 SimpleDateFormat DATE_FORMAT_NO_TIME = new SimpleDateFormat("MMM d, yyyy"); + private static int CURRENT_YEAR = new Date().getYear(); + + public static final String EVENT_META_CHANGED = "github.nvllsvm.audinaut.EVENT_META_CHANGED"; + public static final String EVENT_PLAYSTATE_CHANGED = "github.nvllsvm.audinaut.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 static Map> tokens = new HashMap<>(); + private static SparseArray> tokens = new SparseArray<>(); + private static Random random; + + 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 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); + // Don't allow the SERVER_INSTANCE to ever be 0 + return prefs.getBoolean(Constants.PREFERENCES_KEY_OFFLINE, false) ? 0 : Math.max(1, prefs.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1)); + } + public static int getMostRecentActiveServer(Context context) { + SharedPreferences prefs = getPreferences(context); + return Math.max(1, prefs.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1)); + } + + public static int getServerCount(Context context) { + SharedPreferences prefs = getPreferences(context); + 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 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 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 boolean isBatchMode(Context context) { + return Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_BATCH_MODE, false); + } + public static void setBatchMode(Context context, boolean batchMode) { + Util.getPreferences(context).edit().putBoolean(Constants.PREFERENCES_KEY_BATCH_MODE, batchMode).commit(); + } + + 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, ""); + if(!SSID.isEmpty()) { + 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); + + builder.append(serverUrl); + if (builder.charAt(builder.length() - 1) != '/') { + builder.append("/"); + } + builder.append("rest/"); + builder.append(method).append(".view"); + builder.append("?u=").append(username); + int hash = (username + password).hashCode(); + Pair values = tokens.get(hash); + if(values == null) { + String salt = new BigInteger(130, getRandom()).toString(32); + String token = md5Hex(password + salt); + values = new Pair<>(salt, token); + tokens.put(hash, values); + } + + builder.append("&s=").append(values.getFirst()); + builder.append("&t=").append(values.getSecond()); + + builder.append("&v=").append(Constants.REST_PROTOCOL_VERSION_SUBSONIC); + builder.append("&c=").append(Constants.REST_CLIENT_ID); + + return builder.toString(); + } + public static int getRestUrlHash(Context context) { + return getRestUrlHash(context, Util.getMostRecentActiveServer(context)); + } + public static int getRestUrlHash(Context context, int instance) { + StringBuilder builder = new StringBuilder(); + + SharedPreferences prefs = Util.getPreferences(context); + builder.append(prefs.getString(Constants.PREFERENCES_KEY_SERVER_URL + instance, null)); + builder.append(prefs.getString(Constants.PREFERENCES_KEY_USERNAME + instance, null)); + + return builder.toString().hashCode(); + } + + public static String getBlockTokenUsePref(Context context, int instance) { + return Constants.CACHE_BLOCK_TOKEN_USE + Util.getRestUrl(context, null, instance, false); + } + public static boolean getBlockTokenUse(Context context, int instance) { + return getPreferences(context).getBoolean(getBlockTokenUsePref(context, instance), false); + } + public static void setBlockTokenUse(Context context, int instance, boolean block) { + SharedPreferences.Editor editor = getPreferences(context).edit(); + editor.putBoolean(getBlockTokenUsePref(context, instance), block); + editor.commit(); + } + + public static boolean isTagBrowsing(Context context) { + return isTagBrowsing(context, Util.getActiveServer(context)); + } + public static boolean isTagBrowsing(Context context, int instance) { + return true; + } + + 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) { + return "Library"; + } + + 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 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 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); + } + + public static String getSongPressAction(Context context) { + return getPreferences(context).getString(Constants.PREFERENCES_KEY_SONG_PRESS_ACTION, "all"); + } + + /** + * Get the contents of an InputStream as a byte[]. + *

    + * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + * + * @param input the InputStream 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: + *

      + *
    • format(918) returns "918 B".
    • + *
    • format(98765) returns "96 KB".
    • + *
    • format(1238476) returns "1.2 MB".
    • + *
    + * 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: + *
      + *
    • format(918) returns "918 B".
    • + *
    • format(98765) returns "96 KB".
    • + *
    • format(1238476) returns "1.2 MB".
    • + *
    + * 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(Context context, String dateString) { + return formatDate(context, dateString, true); + } + public static String formatDate(Context context, String dateString, boolean includeTime) { + if(dateString == null) { + return ""; + } + + try { + dateString = dateString.replace(' ', 'T'); + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH); + dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + + return formatDate(dateFormat.parse(dateString), includeTime); + } catch(ParseException e) { + Log.e(TAG, "Failed to parse date string", e); + return dateString; + } + } + public static String formatDate(Date date) { + return formatDate(date, true); + } + public static String formatDate(Date date, boolean includeTime) { + if(date == null) { + return "Never"; + } else { + if(includeTime) { + if (date.getYear() != CURRENT_YEAR) { + return DATE_FORMAT_LONG.format(date); + } else { + return DATE_FORMAT_SHORT.format(date); + } + } else { + return DATE_FORMAT_NO_TIME.format(date); + } + } + } + public static String formatDate(long millis) { + return formatDate(new Date(millis)); + } + + public static String formatBoolean(Context context, boolean value) { + return context.getResources().getString(value ? R.string.common_true : R.string.common_false); + } + + public static boolean equals(Object object1, Object object2) { + if (object1 == object2) { + return true; + } + if (object1 == null || object2 == null) { + return false; + } + return object1.equals(object2); + + } + + /** + * 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()); + } + + public static boolean isAllowedToDownload(Context context) { + return isNetworkConnected(context, true) && !isOffline(context); + } + public 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); + } + + public static void showDialog(Context context, int icon, int titleId, int messageId) { + showDialog(context, icon, titleId, messageId, true); + } + public static void showDialog(Context context, int icon, int titleId, String message) { + showDialog(context, icon, titleId, message, true); + } + public static void showDialog(Context context, int icon, String title, String message) { + showDialog(context, icon, title, message, true); + } + public 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); + } + public static void showDialog(Context context, int icon, int titleId, String message, boolean linkify) { + showDialog(context, icon, context.getResources().getString(titleId), message, linkify); + } + public 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 showDetailsDialog(Context context, @StringRes int title, List headers, List details) { + List headerStrings = new ArrayList<>(); + for(@StringRes Integer res: headers) { + headerStrings.add(context.getResources().getString(res)); + } + showDetailsDialog(context, context.getResources().getString(title), headerStrings, details); + } + public static void showDetailsDialog(Context context, String title, List headers, final List details) { + ListView listView = new ListView(context); + listView.setAdapter(new DetailsAdapter(context, R.layout.details_item, headers, details)); + listView.setDivider(null); + listView.setScrollbarFadingEnabled(false); + + // Let the user long-click on a row to copy its value to the clipboard + final Context contextRef = context; + listView.setOnItemLongClickListener(new ListView.OnItemLongClickListener() { + @Override + public boolean onItemLongClick(AdapterView parent, View view, int pos, long id) { + TextView nameView = (TextView) view.findViewById(R.id.detail_name); + TextView detailsView = (TextView) view.findViewById(R.id.detail_value); + if(nameView == null || detailsView == null) { + return false; + } + + CharSequence name = nameView.getText(); + CharSequence value = detailsView.getText(); + + ClipboardManager clipboard = (ClipboardManager) contextRef.getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText(name, value); + clipboard.setPrimaryClip(clip); + + toast(contextRef, "Copied " + name + " to clipboard"); + + return true; + } + }); + + new AlertDialog.Builder(context) + // .setIcon(android.R.drawable.ic_dialog_info) + .setTitle(title) + .setView(listView) + .setPositiveButton(R.string.common_close, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int i) { + dialog.dismiss(); + } + }) + .show(); + } + + 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 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 constructor = BitmapDrawable.class.getConstructor(Resources.class, Bitmap.class); + return constructor.newInstance(context.getResources(), bitmap); + } catch (Throwable x) { + return new BitmapDrawable(bitmap); + } + } + + 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)) { + 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(); + } + if(lowerFocus) { + lowerFocus = false; + downloadService.setVolume(1.0f); + } + } else if(focusChange == AudioManager.AUDIOFOCUS_LOSS) { + 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; + } + } + + /** + *

    Broadcasts the given song info as the new song being played.

    + */ + public static void broadcastNewTrackInfo(Context context, MusicDirectory.Entry song) { + try { + 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); + } catch(Exception e) { + Log.e(TAG, "Failed to broadcastNewTrackInfo", e); + } + } + + /** + *

    Broadcasts the given player state as the one being set.

    + */ + public static void broadcastPlaybackStatusChange(Context context, MusicDirectory.Entry song, PlayerState state) { + try { + 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); + } catch(Exception e) { + Log.e(TAG, "Failed to broadcastPlaybackStatusChange", e); + } + } + + 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()); + intent.putExtra("package","github.nvllsvm.audinaut"); + } 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", ""); + intent.putExtra("package","github.nvllsvm.audinaut"); + } + } + + 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); + } + + public static Random getRandom() { + if(random == null) { + random = new SecureRandom(); + } + + return random; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/tags/Bastp.java b/app/src/main/java/github/nvllsvm/audinaut/util/tags/Bastp.java new file mode 100644 index 0000000..cb06ced --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/tags/Bastp.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2013 Adrian Ulrich + * + * 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 . + */ + + +package github.nvllsvm.audinaut.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/nvllsvm/audinaut/util/tags/BastpUtil.java b/app/src/main/java/github/nvllsvm/audinaut/util/tags/BastpUtil.java new file mode 100644 index 0000000..a76d824 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/tags/BastpUtil.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2013 Adrian Ulrich + * + * 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 . + */ + +package github.nvllsvm.audinaut.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 + */ + 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 { + public RGLruCache(int size) { + super(size); + } + } + +} + diff --git a/app/src/main/java/github/nvllsvm/audinaut/util/tags/Common.java b/app/src/main/java/github/nvllsvm/audinaut/util/tags/Common.java new file mode 100644 index 0000000..e8d3e79 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/tags/Common.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2013 Adrian Ulrich + * + * 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 . + */ + + +package github.nvllsvm.audinaut.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 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/nvllsvm/audinaut/util/tags/FlacFile.java b/app/src/main/java/github/nvllsvm/audinaut/util/tags/FlacFile.java new file mode 100644 index 0000000..a3e2341 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/tags/FlacFile.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2013 Adrian Ulrich + * + * 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 . + */ + +package github.nvllsvm.audinaut.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/nvllsvm/audinaut/util/tags/ID3v2File.java b/app/src/main/java/github/nvllsvm/audinaut/util/tags/ID3v2File.java new file mode 100644 index 0000000..1a77d37 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/tags/ID3v2File.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2013 Adrian Ulrich + * + * 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 . + */ + +package github.nvllsvm.audinaut.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(); + 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 parts = new ArrayList(); + 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; + } else { + break; + } + } else { + break; + } + + 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/nvllsvm/audinaut/util/tags/LameHeader.java b/app/src/main/java/github/nvllsvm/audinaut/util/tags/LameHeader.java new file mode 100644 index 0000000..340e7e4 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/tags/LameHeader.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2013 Adrian Ulrich + * + * 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 . + */ + +package github.nvllsvm.audinaut.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/nvllsvm/audinaut/util/tags/OggFile.java b/app/src/main/java/github/nvllsvm/audinaut/util/tags/OggFile.java new file mode 100644 index 0000000..176af00 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/util/tags/OggFile.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2013 Adrian Ulrich + * + * 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 . + */ + +package github.nvllsvm.audinaut.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 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/nvllsvm/audinaut/view/AlbumListCountView.java b/app/src/main/java/github/nvllsvm/audinaut/view/AlbumListCountView.java new file mode 100644 index 0000000..02172d0 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/view/AlbumListCountView.java @@ -0,0 +1,130 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.view; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; + +import java.util.ArrayList; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.service.MusicService; +import github.nvllsvm.audinaut.service.MusicServiceFactory; +import github.nvllsvm.audinaut.util.Constants; +import github.nvllsvm.audinaut.util.FileUtil; +import github.nvllsvm.audinaut.util.Util; + +public class AlbumListCountView extends UpdateView2 { + private final String TAG = AlbumListCountView.class.getSimpleName(); + + private TextView titleView; + private TextView countView; + private int startCount; + private int count = 0; + + public AlbumListCountView(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(Integer albumListString, Void dummy) { + titleView.setText(albumListString); + + SharedPreferences prefs = Util.getPreferences(context); + startCount = prefs.getInt(Constants.PREFERENCES_KEY_RECENT_COUNT + Util.getActiveServer(context), 0); + count = startCount; + update(); + } + + @Override + protected void updateBackground() { + try { + String recentAddedFile = Util.getCacheName(context, "recent_count"); + ArrayList recents = FileUtil.deserialize(context, recentAddedFile, ArrayList.class); + if (recents == null) { + recents = new ArrayList(); + } + + MusicService musicService = MusicServiceFactory.getMusicService(context); + MusicDirectory recentlyAdded = musicService.getAlbumList("newest", 20, 0, false, 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 + 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) { + // 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(); + } + } catch(Exception e) { + Log.w(TAG, "Failed to refresh most recent count", e); + } + } + + @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); + } + } + + @Override + public void onClick() { + SharedPreferences.Editor editor = Util.getPreferences(context).edit(); + editor.putInt(Constants.PREFERENCES_KEY_RECENT_COUNT + Util.getActiveServer(context), 0); + editor.commit(); + + count = 0; + update(); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/view/AlbumView.java b/app/src/main/java/github/nvllsvm/audinaut/view/AlbumView.java new file mode 100644 index 0000000..9c120a2 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/view/AlbumView.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 . + + Copyright 2009 (C) Sindre Mehus + */ + +package github.nvllsvm.audinaut.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.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.util.FileUtil; +import github.nvllsvm.audinaut.util.ImageLoader; +import github.nvllsvm.audinaut.util.Util; + +import java.io.File; + +public class AlbumView extends UpdateView2 { + private static final String TAG = AlbumView.class.getSimpleName(); + + private File file; + private TextView titleView; + private TextView artistView; + private boolean showArtist = true; + private String coverArtId; + + public AlbumView(Context context, boolean cell) { + super(context); + + if(cell) { + LayoutInflater.from(context).inflate(R.layout.album_cell_item, this, true); + } else { + LayoutInflater.from(context).inflate(R.layout.album_list_item, this, true); + } + + coverArtView = findViewById(R.id.album_coverart); + titleView = (TextView) findViewById(R.id.album_title); + artistView = (TextView) findViewById(R.id.album_artist); + + moreButton = (ImageView) findViewById(R.id.item_more); + + checkable = true; + } + + public void setShowArtist(boolean showArtist) { + this.showArtist = showArtist; + } + + protected void setObjectImpl(MusicDirectory.Entry album, ImageLoader imageLoader) { + 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); + onUpdateImageView(); + file = null; + } + + public void onUpdateImageView() { + imageTask = item2.loadImage(coverArtView, item, false, true); + coverArtId = item.getCoverArt(); + } + + @Override + protected void updateBackground() { + if(file == null) { + file = FileUtil.getAlbumDirectory(context, item); + } + + exists = file.exists(); + } + + @Override + public void update() { + super.update(); + + if(!Util.equals(item.getCoverArt(), coverArtId)) { + onUpdateImageView(); + } + } + + public MusicDirectory.Entry getEntry() { + return item; + } + + public File getFile() { + return file; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/view/ArtistEntryView.java b/app/src/main/java/github/nvllsvm/audinaut/view/ArtistEntryView.java new file mode 100644 index 0000000..7b34f05 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/view/ArtistEntryView.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.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.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.util.FileUtil; + +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 File file; + private TextView titleView; + + public ArtistEntryView(Context context) { + super(context); + LayoutInflater.from(context).inflate(R.layout.basic_list_item, this, true); + + titleView = (TextView) findViewById(R.id.item_name); + moreButton = (ImageView) findViewById(R.id.item_more); + moreButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + v.showContextMenu(); + } + }); + } + + protected void setObjectImpl(MusicDirectory.Entry artist) { + titleView.setText(artist.getTitle()); + file = FileUtil.getArtistDirectory(context, artist); + } + + @Override + protected void updateBackground() { + exists = file.exists(); + } + + public File getFile() { + return file; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/view/ArtistView.java b/app/src/main/java/github/nvllsvm/audinaut/view/ArtistView.java new file mode 100644 index 0000000..afb242c --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/view/ArtistView.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.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.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.Artist; +import github.nvllsvm.audinaut.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 File file; + private TextView titleView; + + public ArtistView(Context context) { + super(context); + LayoutInflater.from(context).inflate(R.layout.basic_list_item, this, true); + + titleView = (TextView) findViewById(R.id.item_name); + moreButton = (ImageView) findViewById(R.id.item_more); + moreButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + v.showContextMenu(); + } + }); + } + + protected void setObjectImpl(Artist artist) { + titleView.setText(artist.getName()); + file = FileUtil.getArtistDirectory(context, artist); + } + + @Override + protected void updateBackground() { + exists = file.exists(); + } + + public File getFile() { + return file; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/view/AutoRepeatButton.java b/app/src/main/java/github/nvllsvm/audinaut/view/AutoRepeatButton.java new file mode 100644 index 0000000..2442393 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/view/AutoRepeatButton.java @@ -0,0 +1,86 @@ +package github.nvllsvm.audinaut.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/nvllsvm/audinaut/view/BasicHeaderView.java b/app/src/main/java/github/nvllsvm/audinaut/view/BasicHeaderView.java new file mode 100644 index 0000000..a104a53 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/view/BasicHeaderView.java @@ -0,0 +1,40 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.view; + +import android.content.Context; +import android.view.LayoutInflater; +import android.widget.TextView; + +import github.nvllsvm.audinaut.R; + +public class BasicHeaderView extends UpdateView { + TextView nameView; + + public BasicHeaderView(Context context) { + this(context, R.layout.basic_header); + } + public BasicHeaderView(Context context, int layout) { + super(context, false); + + LayoutInflater.from(context).inflate(layout, this, true); + nameView = (TextView) findViewById(R.id.item_name); + } + + protected void setObjectImpl(String string) { + nameView.setText(string); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/view/BasicListView.java b/app/src/main/java/github/nvllsvm/audinaut/view/BasicListView.java new file mode 100644 index 0000000..84221e6 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/view/BasicListView.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 . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.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.nvllsvm.audinaut.R; + +public class BasicListView extends UpdateView { + private TextView titleView; + + public BasicListView(Context context) { + super(context, false); + LayoutInflater.from(context).inflate(R.layout.basic_list_item, this, true); + + titleView = (TextView) findViewById(R.id.item_name); + moreButton = (ImageView) findViewById(R.id.item_more); + moreButton.setVisibility(View.GONE); + } + + protected void setObjectImpl(String string) { + titleView.setText(string); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/view/CacheLocationPreference.java b/app/src/main/java/github/nvllsvm/audinaut/view/CacheLocationPreference.java new file mode 100644 index 0000000..52e47e4 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/view/CacheLocationPreference.java @@ -0,0 +1,146 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ +package github.nvllsvm.audinaut.view; + +import android.content.Context; +import android.os.Build; +import android.os.Environment; +import android.preference.DialogPreference; +import android.preference.EditTextPreference; +import android.support.v4.content.ContextCompat; +import android.util.AttributeSet; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TextView; + +import java.io.File; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.util.FileUtil; + +public class CacheLocationPreference extends EditTextPreference { + private static final String TAG = CacheLocationPreference.class.getSimpleName(); + private Context context; + + public CacheLocationPreference(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + this.context = context; + } + public CacheLocationPreference(Context context, AttributeSet attrs) { + super(context, attrs); + this.context = context; + } + public CacheLocationPreference(Context context) { + super(context); + this.context = context; + } + + @Override + protected void onBindDialogView(View view) { + super.onBindDialogView(view); + + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + view.setLayoutParams(new ViewGroup.LayoutParams(android.view.ViewGroup.LayoutParams.WRAP_CONTENT, android.view.ViewGroup.LayoutParams.WRAP_CONTENT)); + + final EditText editText = (EditText) view.findViewById(android.R.id.edit); + ViewGroup vg = (ViewGroup) editText.getParent(); + + LinearLayout cacheButtonsWrapper = (LinearLayout) LayoutInflater.from(context).inflate(R.layout.cache_location_buttons, vg, true); + Button internalLocation = (Button) cacheButtonsWrapper.findViewById(R.id.location_internal); + Button externalLocation = (Button) cacheButtonsWrapper.findViewById(R.id.location_external); + + File[] dirs; + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + dirs = context.getExternalMediaDirs(); + } else { + dirs = ContextCompat.getExternalFilesDirs(context, null); + } + + // Past 5.0 we can query directly for SD Card + File internalDir = null, externalDir = null; + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + for(int i = 0; i < dirs.length; i++) { + try { + if (dirs[i] != null) { + if(Environment.isExternalStorageRemovable(dirs[i])) { + if(externalDir != null) { + externalDir = dirs[i]; + } + } else { + internalDir = dirs[i]; + } + + if(internalDir != null && externalDir != null) { + break; + } + } + } catch (Exception e) { + Log.e(TAG, "Failed to check if is external", e); + } + } + } + + // Before 5.0, we have to guess. Most of the time the SD card is last + if(externalDir == null) { + for (int i = dirs.length - 1; i >= 0; i--) { + if (dirs[i] != null) { + externalDir = dirs[i]; + break; + } + } + } + if(internalDir == null) { + for (int i = 0; i < dirs.length; i++) { + if (dirs[i] != null) { + internalDir = dirs[i]; + break; + } + } + } + final File finalInternalDir = new File(internalDir, "music"); + final File finalExternalDir = new File(externalDir, "music"); + + final EditText editTextBox = (EditText)view.findViewById(android.R.id.edit); + if(finalInternalDir != null && (finalInternalDir.exists() || finalInternalDir.mkdirs())) { + internalLocation.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + String path = finalInternalDir.getPath(); + editTextBox.setText(path); + } + }); + } else { + internalLocation.setEnabled(false); + } + + if(finalExternalDir != null && !finalInternalDir.equals(finalExternalDir) && (finalExternalDir.exists() || finalExternalDir.mkdirs())) { + externalLocation.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + String path = finalExternalDir.getPath(); + editTextBox.setText(path); + } + }); + } else { + externalLocation.setEnabled(false); + } + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/view/CardView.java b/app/src/main/java/github/nvllsvm/audinaut/view/CardView.java new file mode 100644 index 0000000..20cd126 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/view/CardView.java @@ -0,0 +1,67 @@ +package github.nvllsvm.audinaut.view; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Path; +import android.graphics.RectF; +import android.os.Build; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.FrameLayout; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.util.DrawableTint; + +public class CardView extends FrameLayout{ + private static final String TAG = CardView.class.getSimpleName(); + + public CardView(Context context) { + super(context); + init(context); + } + + public CardView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public CardView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public CardView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(context); + } + + @Override + public void onDraw(Canvas canvas) { + try { + Path clipPath = new Path(); + float roundedDp = getResources().getDimension(R.dimen.Card_Radius); + clipPath.addRoundRect(new RectF(canvas.getClipBounds()), roundedDp, roundedDp, Path.Direction.CW); + canvas.clipPath(clipPath); + } catch(Exception e) { + Log.e(TAG, "Failed to clip path on canvas", e); + } + super.onDraw(canvas); + } + + private void init(Context context) { + setClipChildren(true); + setBackgroundResource(DrawableTint.getDrawableRes(context, R.attr.cardBackgroundDrawable)); + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + setElevation(getResources().getInteger(R.integer.Card_Elevation)); + } + + // clipPath is not supported with Hardware Acceleration before API 18 + // http://stackoverflow.com/questions/8895677/work-around-canvas-clippath-that-is-not-supported-in-android-any-more/8895894#8895894 + if(Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2 && isHardwareAccelerated()) { + setLayerType(View.LAYER_TYPE_SOFTWARE, null); + } + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/view/ErrorDialog.java b/app/src/main/java/github/nvllsvm/audinaut/view/ErrorDialog.java new file mode 100644 index 0000000..96931d7 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/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 . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.view; + +import android.app.Activity; +import android.support.v7.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.activity.SubsonicFragmentActivity; +import github.nvllsvm.audinaut.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/nvllsvm/audinaut/view/FadeOutAnimation.java b/app/src/main/java/github/nvllsvm/audinaut/view/FadeOutAnimation.java new file mode 100644 index 0000000..6f9f507 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/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 . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.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/nvllsvm/audinaut/view/FastScroller.java b/app/src/main/java/github/nvllsvm/audinaut/view/FastScroller.java new file mode 100644 index 0000000..115e435 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/view/FastScroller.java @@ -0,0 +1,335 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.view; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.RecyclerView.AdapterDataObserver; +import android.util.AttributeSet; +import android.util.Log; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.TextView; + +import github.nvllsvm.audinaut.R; + +import static android.support.v7.widget.RecyclerView.OnScrollListener; + +public class FastScroller extends LinearLayout { + private static final String TAG = FastScroller.class.getSimpleName(); + private static final int BUBBLE_ANIMATION_DURATION = 100; + private static final int TRACK_SNAP_RANGE = 5; + + private TextView bubble; + private View handle; + private RecyclerView recyclerView; + private final ScrollListener scrollListener = new ScrollListener(); + private int height; + private int visibleRange = -1; + private RecyclerView.Adapter adapter; + private AdapterDataObserver adapterObserver; + private boolean visibleBubble = true; + private boolean hasScrolled = false; + + private ObjectAnimator currentAnimator = null; + + public FastScroller(final Context context,final AttributeSet attrs,final int defStyleAttr) { + super(context,attrs,defStyleAttr); + initialise(context); + } + + public FastScroller(final Context context) { + super(context); + initialise(context); + } + + public FastScroller(final Context context,final AttributeSet attrs) { + super(context, attrs); + initialise(context); + } + + private void initialise(Context context) { + setOrientation(HORIZONTAL); + setClipChildren(false); + LayoutInflater inflater = LayoutInflater.from(context); + inflater.inflate(R.layout.fast_scroller,this,true); + bubble = (TextView)findViewById(R.id.fastscroller_bubble); + handle = findViewById(R.id.fastscroller_handle); + bubble.setVisibility(INVISIBLE); + setVisibility(GONE); + } + + @Override + protected void onSizeChanged(int w,int h,int oldw,int oldh) { + super.onSizeChanged(w,h,oldw,oldh); + height = h; + visibleRange = -1; + } + + @Override + public boolean onTouchEvent(@NonNull MotionEvent event) { + final int action = event.getAction(); + switch(action) + { + case MotionEvent.ACTION_DOWN: + if(event.getX() < (handle.getX() - 30)) { + return false; + } + + if(currentAnimator != null) + currentAnimator.cancel(); + if(bubble.getVisibility() == INVISIBLE) { + if(visibleBubble) { + showBubble(); + } + } else if(!visibleBubble) { + hideBubble(); + } + handle.setSelected(true); + case MotionEvent.ACTION_MOVE: + setRecyclerViewPosition(event.getY()); + return true; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + handle.setSelected(false); + hideBubble(); + return true; + } + return super.onTouchEvent(event); + } + + public void attachRecyclerView(RecyclerView recyclerView) { + this.recyclerView = recyclerView; + recyclerView.addOnScrollListener(scrollListener); + registerAdapter(); + visibleRange = -1; + } + public void detachRecyclerView() { + recyclerView.removeOnScrollListener(scrollListener); + recyclerView.setVerticalScrollBarEnabled(true); + unregisterAdapter(); + recyclerView = null; + setVisibility(View.GONE); + } + public boolean isAttached() { + return recyclerView != null; + } + + private void setRecyclerViewPosition(float y) { + if(recyclerView != null) { + if(recyclerView.getChildCount() == 0) { + return; + } + + int itemCount = recyclerView.getAdapter().getItemCount(); + float proportion = getValueInRange(0, 1f, y / (float) height); + + float targetPosFloat = getValueInRange(0, itemCount - 1, proportion * (float)itemCount); + int targetPos = (int) targetPosFloat; + + // Immediately make sure that the target is visible + LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager(); + // layoutManager.scrollToPositionWithOffset(targetPos, 0); + View firstVisibleView = recyclerView.getChildAt(0); + + // Calculate how far through this position we are + int columns = Math.round(recyclerView.getWidth() / firstVisibleView.getWidth()); + int firstVisiblePosition = recyclerView.getChildPosition(firstVisibleView); + int remainder = (targetPos - firstVisiblePosition) % columns; + float offsetPercentage = (targetPosFloat - targetPos + remainder) / columns; + if(offsetPercentage < 0) { + offsetPercentage = 1 + offsetPercentage; + } + int firstVisibleHeight = firstVisibleView.getHeight(); + if(columns > 1) { + firstVisibleHeight += (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, GridSpacingDecoration.SPACING, firstVisibleView.getResources().getDisplayMetrics()); + } + int offset = (int) (offsetPercentage * firstVisibleHeight); + + layoutManager.scrollToPositionWithOffset(targetPos, -offset); + onUpdateScroll(1, 1); + + try { + String bubbleText = null; + RecyclerView.Adapter adapter = recyclerView.getAdapter(); + if(adapter instanceof BubbleTextGetter) { + bubbleText = ((BubbleTextGetter) adapter).getTextToShowInBubble(targetPos); + } + + if(bubbleText == null) { + visibleBubble = false; + bubble.setVisibility(View.INVISIBLE); + } else { + bubble.setText(bubbleText); + bubble.setVisibility(View.VISIBLE); + visibleBubble = true; + } + } catch(Exception e) { + Log.e(TAG, "Error getting text for bubble", e); + } + } + } + + private float getValueInRange(float min, float max, float value) { + float minimum = Math.max(min, value); + return Math.min(minimum,max); + } + + private void setBubbleAndHandlePosition(float y) { + int bubbleHeight = bubble.getHeight(); + int handleHeight = handle.getHeight(); + handle.setY(getValueInRange(0,height-handleHeight,(int)(y-handleHeight/2))); + bubble.setY(getValueInRange(0, height - bubbleHeight - handleHeight / 2, (int) (y - bubbleHeight))); + } + + private void showBubble() { + bubble.setVisibility(VISIBLE); + if(currentAnimator != null) + currentAnimator.cancel(); + currentAnimator = ObjectAnimator.ofFloat(bubble,"alpha",0f,1f).setDuration(BUBBLE_ANIMATION_DURATION); + currentAnimator.start(); + } + + private void hideBubble() { + if(currentAnimator != null) + currentAnimator.cancel(); + currentAnimator = ObjectAnimator.ofFloat(bubble,"alpha",1f,0f).setDuration(BUBBLE_ANIMATION_DURATION); + currentAnimator.addListener(new AnimatorListenerAdapter(){ + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + bubble.setVisibility(INVISIBLE); + currentAnimator = null; + } + + @Override + public void onAnimationCancel(Animator animation) { + super.onAnimationCancel(animation); + bubble.setVisibility(INVISIBLE); + currentAnimator = null; + } + }); + currentAnimator.start(); + } + + private void registerAdapter() { + RecyclerView.Adapter newAdapter = recyclerView.getAdapter(); + if(newAdapter != adapter) { + unregisterAdapter(); + } + + if(newAdapter != null) { + adapterObserver = new AdapterDataObserver() { + @Override + public void onChanged() { + visibleRange = -1; + } + + @Override + public void onItemRangeChanged(int positionStart, int itemCount) { + visibleRange = -1; + } + + @Override + public void onItemRangeInserted(int positionStart, int itemCount) { + visibleRange = -1; + } + + @Override + public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { + visibleRange = -1; + } + + @Override + public void onItemRangeRemoved(int positionStart, int itemCount) { + visibleRange = -1; + } + }; + newAdapter.registerAdapterDataObserver(adapterObserver); + adapter = newAdapter; + } + } + private void unregisterAdapter() { + if(adapter != null) { + adapter.unregisterAdapterDataObserver(adapterObserver); + adapter = null; + adapterObserver = null; + } + } + + private class ScrollListener extends OnScrollListener { + @Override + public void onScrolled(RecyclerView rv,int dx,int dy) { + onUpdateScroll(dx, dy); + } + } + + private void onUpdateScroll(int dx, int dy) { + if(recyclerView.getWidth() == 0) { + return; + } + registerAdapter(); + + View firstVisibleView = recyclerView.getChildAt(0); + if(firstVisibleView == null) { + return; + } + int firstVisiblePosition = recyclerView.getChildPosition(firstVisibleView); + + int itemCount = recyclerView.getAdapter().getItemCount(); + int columns = Math.round(recyclerView.getWidth() / firstVisibleView.getWidth()); + if(visibleRange == -1) { + visibleRange = recyclerView.getChildCount(); + } + + // Add the percentage of the item the user has scrolled past already + float pastFirst = -firstVisibleView.getY() / firstVisibleView.getHeight() * columns; + float position = firstVisiblePosition + pastFirst; + + // Scale this so as we move down the visible range gets added to position from 0 -> visible range + float scaledVisibleRange = position / (float) (itemCount - visibleRange) * visibleRange; + position += scaledVisibleRange; + + float proportion = position / itemCount; + setBubbleAndHandlePosition(height * proportion); + + if((visibleRange * 2) < itemCount) { + if (!hasScrolled && (dx > 0 || dy > 0)) { + setVisibility(View.VISIBLE); + hasScrolled = true; + recyclerView.setVerticalScrollBarEnabled(false); + } + } else if(hasScrolled) { + setVisibility(View.GONE); + hasScrolled = false; + recyclerView.setVerticalScrollBarEnabled(true); + } + } + + public interface BubbleTextGetter { + String getTextToShowInBubble(int position); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/view/GenreView.java b/app/src/main/java/github/nvllsvm/audinaut/view/GenreView.java new file mode 100644 index 0000000..c76b260 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/view/GenreView.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.view; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.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(Genre genre) { + 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/nvllsvm/audinaut/view/GridSpacingDecoration.java b/app/src/main/java/github/nvllsvm/audinaut/view/GridSpacingDecoration.java new file mode 100644 index 0000000..0713760 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/view/GridSpacingDecoration.java @@ -0,0 +1,133 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.view; + +import android.graphics.Rect; +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.util.TypedValue; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.LinearLayout; + +import static android.widget.LinearLayout.*; + +public class GridSpacingDecoration extends RecyclerView.ItemDecoration { + private static final String TAG = GridSpacingDecoration.class.getSimpleName(); + public static final int SPACING = 10; + + @Override + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { + super.getItemOffsets(outRect, view, parent, state); + + int spacing = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, SPACING, view.getResources().getDisplayMetrics()); + int halfSpacing = spacing / 2; + + int childCount = parent.getChildCount(); + int childIndex = parent.getChildPosition(view); + // Not an actual child (ie: during delete event) + if(childIndex == -1) { + return; + } + int spanCount = getTotalSpan(view, parent); + int spanIndex = childIndex % spanCount; + + // If we can, use the SpanSizeLookup since headers screw up the index calculation + RecyclerView.LayoutManager layoutManager = parent.getLayoutManager(); + if(layoutManager instanceof GridLayoutManager) { + GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager; + GridLayoutManager.SpanSizeLookup spanSizeLookup = gridLayoutManager.getSpanSizeLookup(); + if(spanSizeLookup != null) { + spanIndex = spanSizeLookup.getSpanIndex(childIndex, spanCount); + } + } + int spanSize = getSpanSize(parent, childIndex); + + /* INVALID SPAN */ + if (spanCount < 1 || spanSize > 1) return; + + int margins = 0; + if(view instanceof UpdateView) { + View firstChild = ((ViewGroup) view).getChildAt(0); + ViewGroup.LayoutParams layoutParams = firstChild.getLayoutParams(); + if (layoutParams instanceof LinearLayout.LayoutParams) { + margins = ((LinearLayout.LayoutParams) layoutParams).bottomMargin; + } else if (layoutParams instanceof FrameLayout.LayoutParams) { + margins = ((FrameLayout.LayoutParams) layoutParams).bottomMargin; + } + } + int doubleMargins = margins * 2; + + outRect.top = halfSpacing - margins; + outRect.bottom = halfSpacing - margins; + outRect.left = halfSpacing - margins; + outRect.right = halfSpacing - margins; + + if (isTopEdge(childIndex, spanIndex, spanCount)) { + outRect.top = spacing - doubleMargins; + } + + if (isLeftEdge(spanIndex, spanCount)) { + outRect.left = spacing - doubleMargins; + } + + if (isRightEdge(spanIndex, spanCount)) { + outRect.right = spacing - doubleMargins; + } + + if (isBottomEdge(childIndex, childCount, spanCount)) { + outRect.bottom = spacing - doubleMargins; + } + } + + protected int getTotalSpan(View view, RecyclerView parent) { + RecyclerView.LayoutManager mgr = parent.getLayoutManager(); + if (mgr instanceof GridLayoutManager) { + return ((GridLayoutManager) mgr).getSpanCount(); + } + + return -1; + } + protected int getSpanSize(RecyclerView parent, int childIndex) { + RecyclerView.LayoutManager mgr = parent.getLayoutManager(); + if (mgr instanceof GridLayoutManager) { + GridLayoutManager.SpanSizeLookup lookup = ((GridLayoutManager) mgr).getSpanSizeLookup(); + if(lookup != null) { + return lookup.getSpanSize(childIndex); + } + } + + return 1; + } + + protected boolean isLeftEdge(int spanIndex, int spanCount) { + return spanIndex == 0; + } + + protected boolean isRightEdge(int spanIndex, int spanCount) { + return spanIndex == spanCount - 1; + } + + protected boolean isTopEdge(int childIndex, int spanIndex, int spanCount) { + return childIndex < spanCount && childIndex == spanIndex; + } + + protected boolean isBottomEdge(int childIndex, int childCount, int spanCount) { + return childIndex >= childCount - spanCount; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/view/MyLeadingMarginSpan2.java b/app/src/main/java/github/nvllsvm/audinaut/view/MyLeadingMarginSpan2.java new file mode 100644 index 0000000..3588d97 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/view/MyLeadingMarginSpan2.java @@ -0,0 +1,34 @@ +package github.nvllsvm.audinaut.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/nvllsvm/audinaut/view/PlaylistSongView.java b/app/src/main/java/github/nvllsvm/audinaut/view/PlaylistSongView.java new file mode 100644 index 0000000..5de25ea --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/view/PlaylistSongView.java @@ -0,0 +1,95 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.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.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.domain.Playlist; +import github.nvllsvm.audinaut.util.FileUtil; +import github.nvllsvm.audinaut.util.Util; + +public class PlaylistSongView extends UpdateView2> { + private static final String TAG = PlaylistSongView.class.getSimpleName(); + + private TextView titleView; + private TextView countView; + private int count = 0; + + 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(Playlist playlist, List songs) { + 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(item.getId())) { + MusicDirectory cache = FileUtil.deserialize(context, Util.getCacheName(context, "playlist", item.getId()), MusicDirectory.class); + if(cache != null) { + // Try to find song instances in the given playlists + for(MusicDirectory.Entry song: item2) { + 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/nvllsvm/audinaut/view/PlaylistView.java b/app/src/main/java/github/nvllsvm/audinaut/view/PlaylistView.java new file mode 100644 index 0000000..3ce430c --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/view/PlaylistView.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.view; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.Playlist; +import github.nvllsvm.audinaut.util.ImageLoader; +import github.nvllsvm.audinaut.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 TextView titleView; + private ImageLoader imageLoader; + + public PlaylistView(Context context, ImageLoader imageLoader, boolean largeCell) { + super(context); + LayoutInflater.from(context).inflate(largeCell ? R.layout.basic_cell_item : R.layout.basic_art_item, this, true); + + coverArtView = findViewById(R.id.item_art); + titleView = (TextView) findViewById(R.id.item_name); + moreButton = (ImageView) findViewById(R.id.item_more); + + this.imageLoader = imageLoader; + } + + protected void setObjectImpl(Playlist playlist) { + titleView.setText(playlist.getName()); + imageTask = imageLoader.loadImage(coverArtView, playlist, false, true); + } + + public void onUpdateImageView() { + imageTask = imageLoader.loadImage(coverArtView, item, false, true); + } + + @Override + protected void updateBackground() { + pinned = SyncUtil.isSyncedPlaylist(context, item.getId()); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/view/RecyclingImageView.java b/app/src/main/java/github/nvllsvm/audinaut/view/RecyclingImageView.java new file mode 100644 index 0000000..227635d --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/view/RecyclingImageView.java @@ -0,0 +1,121 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2015 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.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 { + private boolean invalidated = false; + private OnInvalidated onInvalidated; + + 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); + setInvalidated(true); + } + } 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); + setInvalidated(true); + } 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); + } + + @Override + public void setImageDrawable(Drawable drawable) { + super.setImageDrawable(drawable); + setInvalidated(false); + } + + 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; + } + } + + public void setInvalidated(boolean invalidated) { + this.invalidated = invalidated; + + if(invalidated && onInvalidated != null) { + onInvalidated.onInvalidated(this); + } + } + public boolean isInvalidated() { + return invalidated; + } + + public void setOnInvalidated(OnInvalidated onInvalidated) { + this.onInvalidated = onInvalidated; + } + + public interface OnInvalidated { + void onInvalidated(RecyclingImageView imageView); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/view/SeekBarPreference.java b/app/src/main/java/github/nvllsvm/audinaut/view/SeekBarPreference.java new file mode 100644 index 0000000..a45c36a --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/view/SeekBarPreference.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2012 Christopher Eby + * + * 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.nvllsvm.audinaut.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.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.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/nvllsvm/audinaut/view/SettingView.java b/app/src/main/java/github/nvllsvm/audinaut/view/SettingView.java new file mode 100644 index 0000000..0298123 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/view/SettingView.java @@ -0,0 +1,88 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2014 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.view; + +import android.content.Context; +import android.view.LayoutInflater; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.TextView; + +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.User; +import github.nvllsvm.audinaut.domain.User.MusicFolderSetting; + +import static github.nvllsvm.audinaut.domain.User.Setting; + +public class SettingView extends UpdateView2 { + private final TextView titleView; + private final CheckBox checkBox; + + public SettingView(Context context) { + super(context, false); + this.context = context; + LayoutInflater.from(context).inflate(R.layout.basic_choice_item, this, true); + + titleView = (TextView) findViewById(R.id.item_name); + checkBox = (CheckBox) findViewById(R.id.item_checkbox); + checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if(item != null) { + item.setValue(isChecked); + } + } + }); + checkBox.setClickable(false); + } + + protected void setObjectImpl(Setting setting, Boolean isEditable) { + // Can't edit non-role parts + String name = setting.getName(); + if(name.indexOf("Role") == -1 && !(setting instanceof MusicFolderSetting)) { + item2 = false; + } + + int res = -1; + if(setting instanceof MusicFolderSetting) { + titleView.setText(((MusicFolderSetting) setting).getLabel()); + } else { + // Last resort to display the raw value + titleView.setText(name); + } + + if(res != -1) { + titleView.setText(res); + } + + if(setting.getValue()) { + checkBox.setChecked(setting.getValue()); + } else { + checkBox.setChecked(false); + } + + checkBox.setEnabled(item2); + } + + @Override + public boolean isCheckable() { + return item2; + } + + public void setChecked(boolean checked) { + checkBox.setChecked(checked); + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/view/SongView.java b/app/src/main/java/github/nvllsvm/audinaut/view/SongView.java new file mode 100644 index 0000000..eaf4bc5 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/view/SongView.java @@ -0,0 +1,239 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.view; + +import android.content.Context; +import android.graphics.Color; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.*; +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.service.DownloadService; +import github.nvllsvm.audinaut.service.DownloadFile; +import github.nvllsvm.audinaut.util.DrawableTint; +import github.nvllsvm.audinaut.util.SongDBHandler; +import github.nvllsvm.audinaut.util.ThemeUtil; +import github.nvllsvm.audinaut.util.Util; + +import java.io.File; + +/** + * Used to display songs in a {@code ListView}. + * + * @author Sindre Mehus + */ +public class SongView extends UpdateView2 { + private static final String TAG = SongView.class.getSimpleName(); + + private TextView trackTextView; + private TextView titleTextView; + private TextView playingTextView; + private TextView artistTextView; + private TextView durationTextView; + private TextView statusTextView; + private ImageView statusImageView; + private ImageView playedButton; + 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 isPlayed = false; + private boolean isPlayedShown = false; + private boolean showAlbum = false; + + public SongView(Context context) { + super(context); + LayoutInflater.from(context).inflate(R.layout.song_list_item, this, true); + + trackTextView = (TextView) findViewById(R.id.song_track); + 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); + playedButton = (ImageButton) findViewById(R.id.song_played); + moreButton = (ImageView) findViewById(R.id.item_more); + bottomRowView = findViewById(R.id.song_bottom); + } + + public void setObjectImpl(MusicDirectory.Entry song, Boolean checkable) { + this.checkable = checkable; + + StringBuilder artist = new StringBuilder(40); + + if(showAlbum) { + artist.append(song.getAlbum()); + } else { + artist.append(song.getArtist()); + } + + durationTextView.setText(Util.formatDuration(song.getDuration())); + bottomRowView.setVisibility(View.VISIBLE); + + String title = song.getTitle(); + Integer track = song.getTrack(); + TextView newPlayingTextView; + if(track != null && Util.getDisplayTrack(context)) { + trackTextView.setText(String.format("%02d", track)); + trackTextView.setVisibility(View.VISIBLE); + newPlayingTextView = trackTextView; + } else { + trackTextView.setVisibility(View.GONE); + newPlayingTextView = titleTextView; + } + + if(newPlayingTextView != playingTextView || playingTextView == null) { + if(playing) { + playingTextView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); + playing = false; + } + + playingTextView = newPlayingTextView; + } + + titleTextView.setText(title); + artistTextView.setText(artist); + + this.setBackgroundColor(0x00000000); + + 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(item); + revision = newRevision; + } + + isWorkDone = downloadFile.isWorkDone(); + isSaved = downloadFile.isSaved(); + partialFile = downloadFile.getPartialFile(); + partialFileExists = partialFile.exists(); + + // Check if needs to load metadata: check against all fields that we know are null in offline mode + if(item.getBitRate() == null && item.getDuration() == null && item.getDiscNumber() == null && isWorkDone) { + item.loadMetadata(downloadFile.getCompleteFile()); + loaded = true; + } + } + + @Override + protected void update() { + if(loaded) { + setObjectImpl(item, item2); + } + if (downloadService == null || downloadFile == null) { + return; + } + + 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) { + moreButton.setImageResource(DrawableTint.getDrawableRes(context, R.attr.download_none)); + 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 = Util.equals(downloadService.getCurrentPlaying(), downloadFile); + if (playing) { + if(!this.playing) { + this.playing = playing; + playingTextView.setCompoundDrawablesWithIntrinsicBounds(DrawableTint.getDrawableRes(context, R.attr.playing), 0, 0, 0); + } + } else { + if(this.playing) { + this.playing = playing; + playingTextView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); + } + } + + if(isPlayed) { + if(!isPlayedShown) { + if(playedButton.getDrawable() == null) { + playedButton.setImageDrawable(DrawableTint.getTintedDrawable(context, R.drawable.ic_toggle_played)); + } + + playedButton.setVisibility(View.VISIBLE); + isPlayedShown = true; + } + } else { + if(isPlayedShown) { + playedButton.setVisibility(View.GONE); + isPlayedShown = false; + } + } + } + + public MusicDirectory.Entry getEntry() { + return item; + } + + public void setShowAlbum(boolean showAlbum) { + this.showAlbum = showAlbum; + } +} diff --git a/app/src/main/java/github/nvllsvm/audinaut/view/SquareImageView.java b/app/src/main/java/github/nvllsvm/audinaut/view/SquareImageView.java new file mode 100644 index 0000000..8b46ebe --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/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 . + Copyright 2014 (C) Scott Jackson +*/ + +package github.nvllsvm.audinaut.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/nvllsvm/audinaut/view/UpdateView.java b/app/src/main/java/github/nvllsvm/audinaut/view/UpdateView.java new file mode 100644 index 0000000..f1d4882 --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/view/UpdateView.java @@ -0,0 +1,310 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package github.nvllsvm.audinaut.view; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.os.Looper; +import android.support.v7.widget.RecyclerView; +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 java.util.ArrayList; +import java.util.List; +import java.util.WeakHashMap; + +import github.nvllsvm.audinaut.domain.MusicDirectory; +import github.nvllsvm.audinaut.R; +import github.nvllsvm.audinaut.util.DrawableTint; +import github.nvllsvm.audinaut.util.SilentBackgroundTask; + +public abstract class UpdateView extends LinearLayout { + private static final String TAG = UpdateView.class.getSimpleName(); + private static final WeakHashMap INSTANCES = new WeakHashMap(); + + protected static Handler backgroundHandler; + protected static Handler uiHandler; + private static Runnable updateRunnable; + private static int activeActivities = 0; + + protected Context context; + protected T item; + protected ImageView moreButton; + protected View coverArtView; + + protected boolean exists = false; + protected boolean pinned = false; + protected boolean shaded = false; + protected SilentBackgroundTask imageTask = null; + protected Drawable startBackgroundDrawable; + + protected final boolean autoUpdate; + protected boolean checkable; + + 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(T obj) { + if(item == obj) { + return; + } + + item = obj; + if(imageTask != null) { + imageTask.cancel(); + imageTask = null; + } + if(coverArtView != null && coverArtView instanceof ImageView) { + ((ImageView) coverArtView).setImageDrawable(null); + } + setObjectImpl(obj); + updateBackground(); + update(); + } + public void setObject(T obj1, Object obj2) { + setObject(obj1, null); + } + protected abstract void setObjectImpl(T obj); + + 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 views = new ArrayList(); + 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 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 boolean hasActiveActivity() { + return activeActivities > 0; + } + + 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 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) { + moreButton.setImageResource(DrawableTint.getDrawableRes(context, R.attr.download_none)); + shaded = false; + } + } + } + + if(coverArtView != null && coverArtView instanceof RecyclingImageView) { + RecyclingImageView recyclingImageView = (RecyclingImageView) coverArtView; + if(recyclingImageView.isInvalidated()) { + onUpdateImageView(); + } + } + } + + public boolean isCheckable() { + return checkable; + } + public void setChecked(boolean checked) { + View child = getChildAt(0); + if (checked && startBackgroundDrawable == null) { + startBackgroundDrawable = child.getBackground(); + child.setBackgroundColor(DrawableTint.getColorRes(context, R.attr.colorPrimary)); + } else if (!checked && startBackgroundDrawable != null) { + child.setBackgroundDrawable(startBackgroundDrawable); + startBackgroundDrawable = null; + } + } + + public void onClick() { + + } + + public void onUpdateImageView() { + + } + + public static class UpdateViewHolder extends RecyclerView.ViewHolder { + private UpdateView updateView; + private View view; + private T item; + + public UpdateViewHolder(UpdateView itemView) { + super(itemView); + + this.updateView = itemView; + this.view = itemView; + } + + // Different is so that call is not ambiguous + public UpdateViewHolder(View view, boolean different) { + super(view); + this.view = view; + } + + public UpdateView getUpdateView() { + return updateView; + } + public View getView() { + return view; + } + public void setItem(T item) { + this.item = item; + } + public T getItem() { + return item; + } + } +} + diff --git a/app/src/main/java/github/nvllsvm/audinaut/view/UpdateView2.java b/app/src/main/java/github/nvllsvm/audinaut/view/UpdateView2.java new file mode 100644 index 0000000..1ec507e --- /dev/null +++ b/app/src/main/java/github/nvllsvm/audinaut/view/UpdateView2.java @@ -0,0 +1,55 @@ +package github.nvllsvm.audinaut.view; + +import android.content.Context; +import android.widget.ImageView; + +public abstract class UpdateView2 extends UpdateView { + protected T2 item2; + + public UpdateView2(Context context) { + super(context); + } + + public UpdateView2(Context context, boolean autoUpdate) { + super(context, autoUpdate); + } + + public final void setObject(T1 obj1) { + setObject(obj1, null); + } + @Override + public void setObject(T1 obj1, Object obj2) { + if(item == obj1 && item2 == obj2) { + return; + } + + item = obj1; + item2 = (T2) obj2; + if(imageTask != null) { + imageTask.cancel(); + imageTask = null; + } + if(coverArtView != null && coverArtView instanceof ImageView) { + ((ImageView) coverArtView).setImageDrawable(null); + } + + setObjectImpl(item, item2); + backgroundHandler.post(new Runnable() { + @Override + public void run() { + updateBackground(); + uiHandler.post(new Runnable() { + @Override + public void run() { + update(); + } + }); + } + }); + } + + protected final void setObjectImpl(T1 obj1) { + setObjectImpl(obj1, null); + } + protected abstract void setObjectImpl(T1 obj1, T2 obj2); +} 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 0000000..3c11332 --- /dev/null +++ b/app/src/main/res/anim/enter_from_left.xml @@ -0,0 +1,12 @@ + + + + + + + \ 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 0000000..568a0c0 --- /dev/null +++ b/app/src/main/res/anim/enter_from_right.xml @@ -0,0 +1,12 @@ + + + + + + + \ 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 0000000..2cb8feb --- /dev/null +++ b/app/src/main/res/anim/exit_to_left.xml @@ -0,0 +1,12 @@ + + + + + + + \ 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 0000000..a3fa5ba --- /dev/null +++ b/app/src/main/res/anim/exit_to_right.xml @@ -0,0 +1,12 @@ + + + + + + + \ 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 0000000..c41db06 --- /dev/null +++ b/app/src/main/res/anim/fade_in.xml @@ -0,0 +1,5 @@ + + \ 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 0000000..d615f2a --- /dev/null +++ b/app/src/main/res/anim/fade_out.xml @@ -0,0 +1,5 @@ + + \ 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 0000000..6ab9a04 --- /dev/null +++ b/app/src/main/res/anim/push_down_in.xml @@ -0,0 +1,22 @@ + + + + + + + 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 0000000..ce36458 --- /dev/null +++ b/app/src/main/res/anim/push_down_out.xml @@ -0,0 +1,22 @@ + + + + + + + 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 0000000..6ef582c --- /dev/null +++ b/app/src/main/res/anim/push_up_in.xml @@ -0,0 +1,22 @@ + + + + + + + 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 0000000..2b267d5 --- /dev/null +++ b/app/src/main/res/anim/push_up_out.xml @@ -0,0 +1,22 @@ + + + + + + + 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 0000000000000000000000000000000000000000..a312638d7c7532f6b375aee7c99993a0c7a0e4c0 GIT binary patch literal 400 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%&M7z!>i7;uunK>+Ox*Ud)aXZ4Y&? zT~*SO(EY-3> zxMAZA!4F11mbA}*UgB{4@lPiPMn9%08Jag%^eH{iKgY<#q0qpfS>f40J zM#)Pfw5={~y_NOCA-(K;TT_3_gh}FbJ-LZi0AWanKwH$e+_@~&=4fx>FVdQ&MBb@0Pdu$!vFvP literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..df74111c1f7e4be395356e80bcc276fc0d67d7a1 GIT binary patch literal 421 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 U-`A4=y}*ElskKSH@%6a>1J-mi82|tP literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..083379bee5a3b27b6c1350be4630eba9e73ea274 GIT binary patch literal 2847 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 U-`A4=y}*ElskKSH@%6a>1J-mi82|tP literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..af8748fa2bf808c5fb75557cf13a48d87c695848 GIT binary patch literal 349 zcmeAS@N?(olHy`uVBq!ia0vp^O(4v{0wfi=FFsgXEIEGZ*dV6gnCsUvR%LU7w zekO*3IS$6ro8^UFO^iiYgq0885pp@_z?bX5^DUQk{_#IIegE0sy&pe8Eq2{X+ry4N z3Jwhn536r2(e%6E+4P=)k%@&v;11_Imiu7V8V}C~kl1ZUA)vwq5~?84u0SM?Kn^=l ziKDs)NTbMN6`(3%6?9c|6*vWe`V*`Xsu&y%y_y;s7@Lx&05vK&Y>)-(5>SYkI0@nk zg&h!s>~)U)Hr#xOslDfx$;@eP5&QNZ*4({`<@TIk%88SFJ)6?Sc78cpac-B&A+708 riuL+7>$`tAVY6Lsq4r++*-u!DW5q4zEIYsi3^oQ&S3j3^P61_ho*wZw$;Zz`(#T!+;K}lv^LhDg*E2|3Vjf1fhY?|nnA(@5JmtuC^5Yf)hQ5-92R5{F9jHh zk`l{_Xc-nIMWdvM6y)TPN{Q(J+z1k8h_WI~EXF%=RU%Bq0CRAySdK@_P$@vm5j9NP z5Y~?I354K6IR}92LBgm&wF*QnhgC9=lPE|j$fB$r<>Co-GA<;E6{HN};5p_vT$Mm- z+zM19hj@vS#O8Wjq$Y*ZrcpW%AtWhLjRFy5Ad4_IN}EdSJ&eIiY26A~5Gx3=9D_(R zrAuRt8MHoK3Np0L!kzVj(6+9h;BPF&GzGkpt)BNwrI`JX%sLohdCu8a3OL_mfDsJ zU|J=rW{qAUCR>RL5Me}UE2B>XaUIS(Xq}tXxX92v+GG&d%OMsb3?QLbAS%Y-VGZfD z){Sv-gvyDA6j>V~IF*PBp)Dw5gDEq@SOHuM(-w@klj=lDncQTwMW@kq7UX`Y{cVzjbFqU<`f(j2v@^~*pLVGf@@hKw#7x@23@89Z;4 z)Ejo3w&=cFRa?00@RGvM>oXcC^$Mu@+*b`vZ}n|$jb7F;@K&|zewF&@&WhJ`VPQRF z*EP=BSaag_9uxX%RPdd%wSC1b*Cy?Aq(A$M{qdz?TSlhZ+du3McKsf28cTepWWDL% z&M{Sc`9*B}Q9332C3@z-n%TBb3csIem_OVZmFn?7@b0YxeVPmVHvayhaB;()H6_tg z(-CdWzV?ed=Oujn(cQ21l&_3$XxqDe-h2JPKvRU54t_WJYuwSAHsG82rPBorx3xwm z&(D2?+VD8={EAV3c6fQ+>x0BeML6~Ki2OPI8v87`+nRZsU3AWASuI{ra;~h1s?0A> z`nw;myyN@pa>qAsj3i7Y(M-V|Bq(UgOmMk={;4P&@Yhf_mqRQ98^2 zDqP9ktwVoo+_|kQzvx(D;ihMrqKia7&g;Ba*Xo=d^6=uNF5=$I*H#(73@_?k=a?N% z-xl>tNnXHbAOCG|=A*p5H^PpM2Fn^>>wA7;)fZK@%L!)x-pdjHIc=iPL{@E&yn5w* zQCzdN_TwII=_Y4E>XVmNo4$PFq2>Flh94R^mU_Q8;?S?f|GID~psX>NDq049Uf@`| zt?84b+4%7>-wWT)tn*sxB&RIh1>8Wl+m$h(&@gxuQ`6S4_yGJpX>-s zmKH@Q_pD0(U>8~BA?p#>U2H>j%gv6-<7aLTk9CfH5Y?|aep&v?UHi&G z>dJi^{!}ca8Yf7%qvx@cpGl2v{@dyY&mVfA;>~|V`3>cx-oAz>LO(;4<;(GQdWyFE EAJY*W;Q#;t literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..8b6680bf57059989e1d7d727df21c58b6ce3d69d GIT binary patch literal 905 zcmV;419tq0P)?o6Ip$0F98$$0Q(>e(xG%p3IxD*_!b^oF2SFGozM&U=?^E6c7GHZj;D~tqg=}G^@2`r`1_%QXLyTu6>0Z`lV(@QtlgnxJe?`! z_w(RdIR*Fw@nrER8xJi=e>9P-J~gautd$!6O(5@vI_-;1O2%iy;F@ z4OsV6fraUhC9}41oK?WfYK0$fSnb}a9uUFkiD9@~tNXW%$FR10mrwy4?U^uUw-PeD6yg8U znQ4MO$quZnnLiOKz)wkd!KolSh#5P#k)ulJ$4j2^CPT zBs@YWrrj4w7S9SLN8L@RfG!}37)yi=BUNPDFhvU#pzPvIQTRP!_}wylhC`;%Qw352 zCmwkP><$}mczdT?me3LCQ9ddYBo}~5#bZ3j2o+GZ6elaQYcY=kyo3q}22u#YN+cCv zJe$p>0Y`5=1EkfF5!l?`%1$-q`z*~yKV~%5<&kHEFoQ!9<9DxUf3b+}D<=rM? z?H`C<91dT{4T_B(uzLcPid|0o07*12r14zNqU$5WiL`m)ae2XG@2?Uo;9VdIX2apS z$8=~6k*LiZ4a#Q~WvuP0=jmzGne2s!x zN&y!`tgY+*@d=~H+Q)Mq?jafW%6TrNhrUJY{!}{iL6k}=;byr{=f0nRV_wO)2vf~4 zJgxNk=x!hf8iQ{cF`4~v49G!#VnmcwA9R{x&&Tbi$+7f5CG@SDd(CG{cXaSR>-FF4}u{A=K<@R4K(9E7Kq!yEhS;AL0~o{$I4&{n=+(Dm?ywb~WG6P!1;OXk;vd$@?2>@cRI`04g literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3cd9f7a06f8336e1331e67947414955ef09eb5d4 GIT binary patch literal 167 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0DJWm(Lkch)?FDvpLP~d5O$d$%s zuITu|f??LYti3Fp%jX{O@mZI2^7jXm%+K8dg0;HK=iIS7&e(czf$a~TfHS*Z9ctO7 z!yb0MZb!kt`~$_VxjXJGXs>-@Hm6TVDJZFvr+1;DN!zgt5>n2}N>!!ICu+R?->%81 Q0ou&q>FVdQ&MBb@01S~ng8%>k literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2b93e326ab2321dc640e4a38622f87df6ec29830 GIT binary patch literal 907 zcmV;619bd}P)R&+qP}nHrKXo+qP}nMv`kIc5?QtrW8!~bktSPHNH2V z9(tInQh!=X+sJgnW|~hu=y$q3T?T)gdQmAskeOQ2t*IFNWm-y5AVlNn-ee4Z9Zeu4 zK!7^ad5IeSJt`w4*gkql_3-s5Oo-(w(=+%u{~!Ec+yZq<+oN$=w_|FSw8h`WP4RyI zpSZ&Gq@D;jeW+^q&V+d0%l`w8kQ4EiHDm`v^h5;dXQRW{C+M-oyBm5Je^&cF9T6YO z4*wa233@m~ZruT{ut(%R;iCIRhrfla1igG=zwQ8kI3Rk*-b?3+1k59dSnOJ(J79rp zmCU14JH5kyOb~Ug={eNqBTrtS2+2f0e-qO-tec;c7m)mo~=;>_&uY~ z5hLOSyh}*qfB@YPD_~;!251^Z8~y_FWvqd)SmEA)cx$D5BW^LjVl-e?qyWfzE%!&1 z1k0g!=9c0%W=W^MA z)sX^n2%@Gs7D@+9u+NnZm>DTx5kb^V=9jS5y9c7{fX|FOEpNyMbcz(vgCOJlLk}H0 zm~U0y2VE+L9JAiDT^n|ld$V?c->j?$zA3DMiw{3)ka4(fj*81-vW11;dk!aAxh{YUf!oI+~|M!cE#vlhVDN-6^SiR=qKKyHGpyDV>N z0SaDyX4562yK{dMj6At}NUtR7JlVapl8_){ z=&D4YsXT?gro9A3meb3rI6MCVO_AJ^w9+7YHXY8`pHJ^nJDNdzsg&I0phDVC6R9b^ hKxZBq!!Q7&000a>Q|o<201N;C002ovPDHLkV1l$bqzwQ7 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e8e2351c0515ae31da8b08dc03cdaa44ebc11bf5 GIT binary patch literal 263 zcmV+i0r>ujP) zArgZy5Jh386NP6$aEprG1Q7)?eQIVZaOMZM_^0V3fS(cQP zbP!>30NE zZBxO7Fi~ig=^p~fsc5LkwEzVpSH?(p+6BZkEdf3OY@ReWAg89Geidc<+usAM4ZK?# zco(^WWAHm8$VVNd_{|XYzZL@)qn<&=>t!X!O)ZKoh%=`Ys*{qAq6cgfOlw^_rm+A3 N002ovPDHLkV1kBQVkQ6p literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..13290f06bac8be802512d0c831329f40519af7b7 GIT binary patch literal 274 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0Do1QL?ArXg@6C_v{Cy4Y2JUFmu z{o4ah&X0SUdY4JCUOK@w;p4H(y=jbwx@#IF`I}5OG9}ua;mT_NtHa}A8~=h!%2INN z)DEd?>F-Rj_p{&dK9aP2$rWTdBgLY3cB7cd>4HldGx@x^B}`jNkC?VdN@S)q@+=RW z`jKU^mex+|J6$tBVi9a}? zrNQAXD&lgr>xu@)S|MJktg{Rr4vx}BjwU(UlD=kq3o^6){pU7TZV9yIay#h3$k1d| VVRB#(OB2wO44$rjF6*2UngCZsUW@<$ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3933f35af53ba1254f990186cfdad729914dbfd8 GIT binary patch literal 138 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0DKu;IPkch)?uR3xu2naX?UY}`w zcJ7=rGuu)e&Afy^-G7s_&&7#>p~uJPbVs~{?$kggmVcaE(y#ijX|z~-`dnSOn?7%a lyx{dI42{b3?HKlGF&{Yeb7AKO9#f#<44$rjF6*2UngGX%EtmiR literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..bf583d0fe2c87762823a18e8534f479f7722b593 GIT binary patch literal 145 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0D2u~Nskch)?uO8%O5a4kLoE@=n zm-F(C6Ej_73WPkBAK9Bv?-x*HU`W&K>RkFlOyKe&*B*w(dZxdTz0Sql8FA0~=f}mr sKWA}ga`l0~9Xpn>aK!xl!uTLm)XF$1cdIE6KhO>aPgg&ebxsLQ0OMaUFaQ7m literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..eec654a9c1c16c1a6c9154f1bf131d8f010b901c GIT binary patch literal 407 zcmV;I0cie-P) zF>b;@5Jd+H4q|HLGf>b(0!1m2pmc%2U0fiFK*JI6830!xgj`xl<-OBOg^OsM>@EnU z@ZV~+Gw;hYws%AtHEQ%fM4OP9l#GIcjFgy=_G923Cgd1pC_84oSg6m!^z=|V(g4J7{>PPy?Pm3 zEn7{m7IEaAHHX^so}Ix$P5$Dgig@Kyjak?k?9}9u2muKRfe3GE%+Ai>sHA4J2&!Em zLZ(I885BzDhX_fvOGH>JQH8(3`jH0O!yiO`l<44JqO`WjU6w5AiZD`RQagiKO)eN# z5ks|%hS<&^to@MotXQ$HJr8Yf;N0t1kz3!=OuY=IF2C!GGydW<05EVi7`WbPb0_@V z=hEL=l<<_?qNsAH(&q&002ovPDHLkV1lhF BzOn!S literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3a7001a1fe622dfbe1d2e3b572595e97cee6e2aa GIT binary patch literal 411 zcmV;M0c8G(P)0jbiGcV_e}3O@ID;A?;Oq%v9hxIr-%_-RdkdhE*>5(MWW#YE}5ccOt}aEzc$v4EpmfL@A0b7Gh;dmwlnFb_bkaROiub*XA1ehs6rcKX zts3VUA7xC7GP@*5ut%8|V-%kkHkLgNaqgIy;*Rj}7#a6Yed%}uIl*N(*QP^UPQ$${ z_PM2vqO`f?U^Q zu}{KK5XPTiG!X|0L>*;xFea`B2Q?0upg5XS|FfZsQ6!Bqqys|Aon5URlxvxd+oqXm$mk8n@ycYp+Kgf3|SpQT}J(_wp+E>IZ zMcxohHo`QTr&Ks%CRnDz?cflaQlS(_2ttFTZ`9;K3`z2!C3Z@&K+{dGJy@qN*N%a+ z|0e_I$H+|jjLb^Nk@;=tH~wJg|HIny4~y3qqmJ#q7+xKV;v(1VuXpbt(VqruQ;`th?=7 zwbPEUhn=_I_naT!3}cZ+7MYsG((!bh#paHi+X4g|>ezrbwCPy~8zH|j7x3dvehEG3 zyMNdw7kUogw2T7NLZah}S^Wj-@ERD|6zZsFQ(*8Kc7A-Hyi#5c z1y3L!0^&J;T1Za%3rVX8O+o$vwmOAoRwX=la_e4)G70WT3SLnR6r9KaSj9hzG6p3X zD9VLO<;c`>XB_@`53aa1Nr5*q-3k>27c%yf!QtlwbmtC~%!OGEe*f-U&vXLD zDBYqj0{#c;T2Ywp%JCv7BxTVe1S7CT{A+HH7Xf){BB*vSg1R_ts1Sf5zNrUNOcFB^ zg=|WNfO?z+gf{|oLpn~1*@qtLDg>yLO7)>lVm>oL2lrvX9$nwh@a1U2=TYjgk?;49NvdEO|3ljJ4O9<@V Q0{{R307*qoM6N<$f}T|V%m4rY literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_action_rating_bad.png b/app/src/main/res/drawable-hdpi/ic_action_rating_bad.png new file mode 100644 index 0000000000000000000000000000000000000000..8bcaeeb92e73e8b5b3e8ee5c10ddacd65aa192c1 GIT binary patch literal 440 zcmV;p0Z0CcP)A~6vgq|Z5t$^Sri(9t06(rlGG3kgkTYv8Y&@BwIx7JO$8rm$OglgCaFM>AV5G6 zSPTgy)K+W|Y{L(+uVu{c4zq6z%o+D`a{e!8=FW|1)Tq%vEK^RqsWakP(N5mQ!s%jJ zz&T%y(~|*0r*PIO7eC{RGFqt$SO9%nL+W{I_fOnk1I%~`-f9hT%z8cvU`Y=>7e{zH zlK|GiD~D?VIO;vPG14^wVAU^tcDxpVX`jHYVJ~=6zIQspvM#{tnBViW1Ly5(fHQVR0E z_(R?#z0!Hk_9;=)@c73 z|J(ha{C^z=+=!!o@F%A(X8O;A0rmeM!eacC|NPWy0C-|Ce!_oo>a{TPF^r!dI1pf&(D6A@9%!9acR-x%7Ap=QFw q$OrqWnlQ0gVMbHqN5Lo+*^GO7$+W;QmMG()#*u7+o{UjHO)W4ABhGj-Ezl7+ z?4hXz2Cl>!Po4$L#3fL&EilwKno3Ixn2j@uH=Ya&^wo@}(9{A$Pg0C0$AXYi3kEt4 zjFpWikYq(5sWlp-p}ujRaWgtJYQk? zQmuty7>rTn7g5o81}5VoGz${6*{Ik;G88rHLvs6cqXcVSsy4w)bZF>$a2@T0Y&?}+ z56*txh^VEy>!GzoKh+Z^dOk3oK;49i84sE?HGUL~f`J48?=$qTO1V0~00000NkvXX Hu0mjfY#WPu literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..fac2c93c6ebedb72d702ea3c94abd985d5df9892 GIT binary patch literal 290 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0Dm!2+;ArXh)UfalbFhJty$E1lB z>`GyV(H@60UgT_zC^a#8r}t4SSLq{DS@-u16AUIiUdA!8YkjzX%bm+>Gym;hdU9`H za(epr>&~opS5B)pwEy|){r%xy?X%lA9^Tz5?v(yMcvB5uw#T!XOtB}#W-v_@2ykOq zWl_|q>A(_jsEwP4QAy#SV{aqh0ahig36I!KS=<=J1MK;IZZw={J9T3jQ_~f7si=sD zDr|e!oO>RXR99y`)t>zq=TFwIn|BS;uIyo(cBkgyORmYeam>6~am>s>oD9UJG95o> myh!vvbKGU}tnHhBKReeoo8xX-a*8R?(+r-jelF{r5}E+*Yj`pM literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_action_rating_good.png b/app/src/main/res/drawable-hdpi/ic_action_rating_good.png new file mode 100644 index 0000000000000000000000000000000000000000..0e2b7ee291bf6676cdf3a17565b476703b16571a GIT binary patch literal 430 zcmV;f0a5;mP)tfdO4&8f#}!f81Izm2B}r{N{|=E~^9X=$i+1aDG=vo4G92`r9o ztlo9Q$s&x|1rJ6gm^%w6BEmgQ{W$Q*#lh{S)w^ptBEmE9S=OmoGKfQW1`Fyv#r2*i z&$|^L4Q8dBpq?A$gq;GFqWL zX3s(1yEe4IRI%&jykK4t_6zO*pMuPX-1fiB6ultN-DnXq^L^rJucvOiQ2i8CRQ%8U Y1vyMrr-Qrl4gdfE07*qoM6N<$f@#UbJpcdz literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ea8e3591a3b06f7dea601453a83410504923f024 GIT binary patch literal 345 zcmV-f0jB~J^y8B>H4$4xL~Gk!aR!x^QfONQ6aVR!GHe$Q?M9+@W0-F9t_At zjs^Z$j6VyE?v4M~VZh1%?fx_3H_qaJIviSnMRKuPfD7*buZ!P;0wl!+7$U^LL|R)= zFlqrIm_S<#j{Y~Itpz>*S!ij&vH$jj@-1Z+%p~A`iYXbC7F+!2ibo#|ap}_P-&q6(mND1{7oY r@9;kbNgN$U{+Au?ag2gdFuVZ(YkQ#^6bklZ00000NkvXXu0mjfGlitV literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5fd1fe5ea4779cfcba4b1703394d5e9066ac53ed GIT binary patch literal 364 zcmV-y0h9iTP)6auFp1_1%&sRf3yM3w%S78nH*m42oh>5*xHffZ2}6lt@OX@NG6 zQKIoQ0>(EPs1jL0V%BItz-(Y(?2Qmdhlb92Vxv8dQ7{UIHvj+w`}C#3G=?<*0000< KMNUMnLSTa9dx=m0 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..47d66eba43ca4acc15e57cebd90ed1d10698c39f GIT binary patch literal 294 zcmV+>0oneEP)`IQn9UCqoq`c?ZsKF1P5>cjeBTFD9F>vvY27#7i8Xalg)d@ z&i=_N<#7BbWU0#PR~%_e>H$uaCG`M9Sv_=Z>oY8gQn9UH-02UbCH1hD)x%U)4;@>2 zk2%jM*$ViWa$!SFr)v=0RzoSJ81V^v^6vvdoW16xwEfu4TyBnj-u$p$7Sv-A6a?tmr^gl5w}9xxr(t#1Rqr33mh;4#zc57bRB s$k>gh6dpdK$o}IRP;h@74#$6U1rJ>a$V;TG3;+NC07*qoM6N<$g4<|#UH||9 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..448611ca884240c54d8279327a68d77627d5f04c GIT binary patch literal 241 zcmVl<6iO1uKFyzLWDuK6gG6((w3dU1T-HR&JFcXW*T74%LI<}G{BJ3sDH4~&BTPuPkl{BN4Z2}c3i*Z rfs!R%{f3TMv1P-YzW+XZJn`iNFP_sxU(Rr600000NkvXXu0mjfxocoI literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..eecb370a715982eaefc9239d5d5e0cd48bc8b6d3 GIT binary patch literal 401 zcmV;C0dD?@P) zv1$TA6h%jDEChvEq*`kWOAA2@V;Vm}i^NXYBD)`8nKXj=1=|H7wo$iO12&TPO()H2 zL>zQ#+5@kzvxs(t)33uNa7Pj>NjHhXwM z*ig5JFEKm73&M<&IjV?HNO(|fAHfSk+dRM{L6C!LPZQ9y70}fLbZrH+GyyG>v-j}u zsPly&>B^&!HaR+ikGMNOz$YOgVZ}Fsq^pE}y8n1(iwQM{Fi7_^1g!SzD_+Q03{Xlb znv97NV#YKty;4V;Y;z?1bpSc?5fi3-FlEAsfDUD&Mk}Qh1^NtrTT7M( zZ^W8cAm)_@*~_1&#ye{beCsu1YM0^LEKk6rgrU88eydc}5a{b-b vIsA74{%)e4XW#tm>>0V}eoLL4oQ(AY9=zxd4~USw00000NkvXXu0mjf(ypqK literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..cb6f71f4242ec4fb36f345d0333dd6739a7dc79d GIT binary patch literal 425 zcmV;a0apHrP)`odCr&vL;7FK2*7`NRT+4GfK=KH^xfoJ=wvVrANlyHk5NpL!f&^ep}`eTa0D_;bh9bQmj z&drbFTfA4)CJo|QF!Cwn;vxCSY96WbBsYkQ8|ZCbu-i==sC>KHFoz|rZeJ+i*9w*} zXSSgUB|543U375vBegA>SdVdPOKVXG4>VC)^;^sspKy0#e`VZ}&&KtNG@knM;!JAP zRT$!?ntZ|y424^yfP73xbOxT7LB2^{-r&2wR;1+$cjv4pRHNRLzoilq62kZbN!c(p TXARq~00000NkvXXu0mjfOZUR^ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3672cd158592c9f48291ddfd764289876c4b2c7b GIT binary patch literal 455 zcmV;&0XY7NP) zJ4*vW6orq$N021O#=jsIBED9OG-B)|2x15#zEZ4^#zq7S(L&TJR+3*JSXo*JMU8QO z(`VrtGg;Ji?R3uVz8%iFJ9B4Q$;rvd$%#H@DN)2DY4Ic;MM})j`y2ay&S+pTG&myL z;#rOZ{=}&r{j=`RFl}{7N|H!Xv-k-;@d9RpF5j!kyd<$e)8daz#|tP~HU>jklJG;k z7%!k0GTM?P$|1fIFTm^Aq8H{QiF}8BgQ<7{NscX}YH-MDE-ZdXU)(v$QVS0Bg$ssS ze5e)Cq{hH+ZbFI$e9GiWTRcskGCujYxB9Q&p$uab*rY;*4Hg+^uU;OvA`1~ovP5y=LLw-kY@wTK9WL6n6G1!YnKE$z4SX+LO;ErV9t+d}?iAG$b!8O+%4Lpo&1 zhoCd($MEE|-Y9$M8X{J_L<0sN4d9!dCy$D|Drm~g(h41c^LfziJdU$TR|c%ZGiP9p z^L3FYC07P)h=LHk=B=y!kpXC8i*l6nDAYA^*&?=z9fwrU9CAd3z9av8^ts%lbcpSX zIZz$?By2S=Y+2~|nhkDo3DE%CnL6t_MpQe`szAop0nMs*^&L_%MhZ~HCN`l;&qzFZ z@bfhrq?ZOy3K&CmQ*BQ+N!u`hsc^|3a7W^b*Zfod9KJX|g#m5n5{hR{am+vuM&dD9 zF@k5%xR{qVY$=SOhndwT9$LiVui};%Ud(sw-Ene5T3lhlu6MP(5uc}rd``E*RS#Kp zOJC5&TJlC*@j2WqyjP9o2CcOX>iniF3kI!!Zjeim%6gGG<_R5%Ji7-82?>ea!#Aq) VGvHR9>mUFC002ovPDHLkV1k2)(Eb1b literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c0c844899a14675400fac35f9a3a3b7bc145554c GIT binary patch literal 234 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0Djh-%!ArXh)UbE#oPm}> zmN#`i9qq~0`;O1!2{w=vthG*XjH|epa3b;+W8KQnY|Z)XEK3_57+FOEKt#p)US{EC zGCRY0m&Dw7a7jRL;mH}g;g_-+*ww>I?BaH>vHbjp-=ky3#b3KFruq3Kara0}xh4JR fm3j2#S9iISK3)rvJQY6y=y(QCS3j3^P6XDNDxI@Uu7bxL%fwnK*?5M}Ws^Pl4m^SdsQ zBuQP*WpILH^>D%<*YbKWK#BsUW(uUizzRTwvi7`zL8+7#ptp_A8yLN_0#w`-P_Y04 z2q1s}0toP%C!f_cn&E<1%K+!NW3PR3h=|BpP4s~uoVNqK^%cy$|JOxdt@-vv?12m^ yLh`CMHlCjA6EaI697n?@d&`mb<(Dl<-R=WaZ=&enunyY*0000?#df?gup_SPx4vFl@Zdc$u4L>o1@K89ZJ6T-G@yGywoY CYDk~} literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..72475958798cb57b0e388af04a6fee66713ce542 GIT binary patch literal 244 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0D-JULvArXh)PTRH1ees`2XKSCqR$xtCcpHIO`WHVcbmKmqQ${Q*NJRh{?e{y_a zuM<_&A;~0k^_Jlut`B>onEiLJJAEhHerCAmM{h>{Jd?@>kJTTS|7K8f532$)h3uKO sER$#4d+WFVdQ&MBb@04PXe^#A|> literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d2207f125a67b5dd34dd6d7cf101c86adcd0b89e GIT binary patch literal 256 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0D#ArXg@6C_v{Cx|>?IJU-U z&JxGuut&Vl%{ffh80M+*N?ns^k>U_YZA|RAsPk!oqlV$L%@U!Lvl%bkZ(-(=n059^ zRKgvd! zKTAS!6ow6=3?(8Q0x74vHg$&vpN5W1AGGy#Ip9d{X6#|VbLweb6P|kn+ye|CacHP$Dmb%4cect3dLoo zx~>%uj!T`a*2z>o`igH$izjmIE$i!OWMs62`T|=k=uKLOZ|DF3002ovPDHLkV1iz1 BmNNhV literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e715f199d3e41caa7363c702df7cdd25a08256fd GIT binary patch literal 365 zcmV-z0h0cSP)5@jS7rf zjiwT`ro%WuN1VI}VYKGelQGmXvM_Qt2sKDCE;ep7o`}_d-3IXn1~Qx!Ct?+LeQ6^- z0|%o(!(^jU<91*mAxD0xiM25obuE~NVnG?H7LX7`#_{@wL8}S?SZV*-{5;c#&HG4}8;c6pVsVFa!Yr(_Jktk6y-y00000 LNkvXXu0mjf!rhC* literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b3743708d845c08def0ac03a02bfb591973e743f GIT binary patch literal 239 zcmV&1jieb#`pt47@@hF z&|gy!9J~AuF<7JWD@?gds$j5qJH4c8pUndSfh002ovPDHLkV1k~6WE=nh literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4fd07172cc3c7d7cea6ec049d1590371f840280a GIT binary patch literal 255 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0DW1cRKArXg@6C_v{Cy4Y&ELflR zq?_yO)fHKc7fwWGTqwWN6v*w?#M-v#*u4!osWClyi~%fa&vP}rrm^12X`AO}Y#7tW zmo8P6wRxhv$wihmGcrYd0}WPgkal~pF=<(13zv1P;le|Gm&F~f2j|$B?6U1mOOUWX zd|`4{r-;biW0M0eT~O&-G;M+YikDSB@fQw!)!4V7_4UMBN8{`LOgaY+OigfPZ)IE8 ze3>cP{V}JDllvx}36Cxbs`Ggj_NuK2G~jY$bY)<$inxBVJFU8hdGj#2$d7e&u_;mB77*Qvd&x}X?fBQwVSN8&4$l&Sf=d#Wz Gp$PzymQK|G literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2fc874e675fd5fdce2287dfd9bbafb9639080d04 GIT binary patch literal 222 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0DrJgR1ArXg@6C_v{Cy4Yg6*Zdm z_gQF}h@>Vgob)g#<{0BK-($i73j_BDE?_QQWAMRzR;CHJu$2GuKG8F7p*MQkBON#X zG@5hl><*m++`oI0dVXeRGIF$CyutL?Dmm;{)CHZxmRSc^ur3zsi89DyjMXVfFkGW} zo$0XDqE|UFQ|%lrE+3eGBCR`6A#HQ3LXHvNghz`OJapyA`rKm{?tV~1f;EMiVa+v} V?(5I%EP)PY@O1TaS?83{1OS6WQrG|h literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..986b0d5486e8a500b6a332749997412b3d4f286c GIT binary patch literal 309 zcmV-50m}Y~P)s0Wt^F?#6;=Gd5l^t8!UFsj zWFRT6L=rEhmIax#wt(hAL~0}-MUpv2OeB+(%&{UId z0cnLDSr$Nx>z%|D*JN6NQQ^$RRpC%z!6+C7qhJ(_g24m;r)0|x3Az}f00000NkvXX Hu0mjfZxM!R literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c7c66034ef9394e7705b3fa18db314c693a179cb GIT binary patch literal 323 zcmV-J0lfZ+P)i+!&1W`_!e%%a&NND(*nDFnqp3J809s+JOp)<2oJOT622L^RGM zu}%U(5QeG5VhamSC=9JU0xhq=3s_2GLyqzcK7c(f)g@PLXrP05m4tZD^w|*M2r)C8 zT}@#BY5&Z(GqZO)jYd>dltA2K1krR$-eN+56+})-q6cJDib(ZLELVKIIH zpS&^ti2#5Px@SES00w)o{v&|CV~fOqXU8F5JYdI?G8|yefO{T!A_)PQ^Kj(z4%jiM z`Md*Gw8H{QdNtpx{~3&qTwPZF0UL(g?9;zH|E_<%rdwvbkexlf*Dty92Pjp5Lhxjs z0H%B?029d@RtMZM6;`n(qooFf*yH{Pu%IJa7>$09Xp&R0Wg%J^EmmJcs=NJqKtf6` mJ-bp literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..91860c7f5ea53eba523fc61e1801883aa71b00c7 GIT binary patch literal 370 zcmV-&0ge8NP) zu};H442C;Uq)12&NT^HT0Z38L{>6$ISwIY=Ta>kHr!El;*MTmSEK$kRGE}HUs)(K5 zfmG%9YWefozVr2ZojP?AC_avYW<$SKa140DudvNVxk*f@22jeC+)Sbd@Ja)qTwN$l zc&=chzzF~)GJ5_tAApb1aF>q=5CyPX*M9`iRm&1d0sPRi$p;Tu;|+y30On&d;mrl# zo6rLQb2|SqoX!F3G1ac1bHMWY)GGi5E}I_mJ$pcpTd&cOm+q%%MYnfHoA8*ney)EV z(~@U+qI6gGPH*yF3#Sf1p#r=JR%RVwGX7K`CW0MS2gD~P6Q1IoQVi4p5BehjFXA)F zhcOD&iJ(t8S15TQ`7px5s?T5&-Sq1Lp-Cv0=HKE3n?gxD;B6Q>bvht@0ozCh^f(^U QWdHyG07*qoM6N<$f@?6E{{R30 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..68d07bc685afbca3291dc6d5f466d60876549eb8 GIT binary patch literal 428 zcmV;d0aN~oP) zF-t;G9K|0d7!Fo`fF?l%9bDVQ$-uU73(G+1Euy;);TJH2qNTCbq@k%q1bKQs)2~I# z$nf50E%nZSyXV~V;|-7dcbq_h{)z~bq`(O^YMf9YN%-f=BOKCWgqCK+6^BGNFAtI9 zhRLV$#Z0&%M`$yE%XnTl=AITVE-mhtPA~kIZ*$HRAqKc)*N7kcX0(ZYo7thw4Dm$9wmwP^A-dn!JC5TJql*wdqIQ5HLcFlI zHW}vyA&Pc@4nj1IvQ32O*wn{)L<~t8rBe(M{qtnj1Vp3 zd>bK-%>ic!(J;<85aQGvP(_HkalVca)&B=ro~oZr00;oT38?r2RLlWKz5qw&fL$J} z0T1ljJUMxutpLyDZJ+%#m-Km^Px*^qe)sxZlJ>VY#@V0G&We>~;l(#x9|Hvn6zC1m WAjR6?jVe|E0000 z!EXUk5XM&p3HIEBs7N;Hf$Z*^AsTz(B1C8<#BmRtRiXzsC#}SV5TSo?zOd3|$)wN&s>0Sy|? zQx~zp>Y*a$z6fhq0QyGkySfpBJ{eeJ(Sy96#0mMcF?_Zsg0_pmiJlac*Fugv=K@&o zygZ#U%zAWDeJvyeF!OZxvwQVYr={Y|?KSgafG+uJ^YlWS09^rjOJ%Q)|2f|Abh8A` z9mM$ZY4?#yyYqts&X4E@U))*Br1>wXK!>)u2im6o0yltF=!F)yp%zVCg&P1hdZEc} zs7WJH`*(n&ta<5?015a-fb(DM0f1HhUsKBqAY<|G6Hyom14gN+$VLG$6#`5VgtrRT zficdu3FQ+&d0!i|j__8oT^lVmeY;IPa=Nm7)-3hP^-+cl88SWqXt5u>>?HHM00000 LNkvXXu0mjfFdfp1 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..514ee644009e582b33e264cb19ccbabfe21f4622 GIT binary patch literal 434 zcmV;j0ZsmiP)|SXsVNJaLF4V?C39_u?yt~{mF9*0#F8}elOua!9!2l0e!5#wyO^LTn8V5m(BBp|i zv=BI?WdSlv$J-^plxcF9n9%`pv~h8x1DLo7#DoII^CSZL_za=~bf{4xPnlXU3p4_J zDkJHp6haA5XX;m8(+lXa7Rt+f=>&9G4CQ%=8KD61DTMM8eZ6 z%}YW-6veGjkf^M%XCYbkh559(=UOd_cF`tXl#dE3Y!|e0FS&5>$f)!QrSsReNc(W( zjEoTFJ-f@y`3*C3=Z?=ejA8uUur10Y1B3A%<(13@Kk+Nl#=UnZ=>T`^K|(^(l~MEb z9FDp2VGYSHPH4f9^~QwQ#tk0Z<}Y;1`>AO|<~y@E;``3J@hlvQ8M00WXcL$0wyhf| zio?Xbsr??$>}87l)>$_SNcct#nTrq#oqM987rN#tB>CygqLOe#NV&(dKN{4N(&h|f82>ch Y0li1kxu>+gSO5S307*qoM6N<$f-v&ppa1{> literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b76ee54b6ce43efde867aeadab5510e8bdbcca81 GIT binary patch literal 451 zcmV;!0X+VRP)=j6Wk6s16c0tNB~6`HiTz67;~!4!)%TWm}W+WC>+F~UJk)|LT{ zJm_Ee4v>56lUjEAK6?bgWr9u{l@b~~OvY66Q18*t>7pKeQ7Pw}2J@ zb^YY4-}9WEzRDXWJv2(FXy{CEefoZO`Whc0kLE51^@>7+0owq%!!U>Jf19*1FtORN t#=^unV=xo6xu!)k#idZ7K!F0u@B;w8%;*qV002ovPDHLkV1h4H%<=#L literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d94921c87d861678579227d250557bc6d65787a7 GIT binary patch literal 494 zcmV4qH5KW0i5k&<(H2uJXJ&0&`W+Od#5&RETp#;tKb! zwZb;fL_lAAhRr{6vG}Nx3QbQccY7EZ~AvEYx=<>onJ@c zy<^U>k@hLQIxjh0N4+0Cd&D;|&IqPR&X&yat6NgtT>2A~d(zMPF7I974>)l$1!Kui zpe0^2ebi6>1XreuQk+jk*7HHs|160vsyX58{<7!9d_@-(3y%u- zCt&QRFW-3XgIzN%#e?VPpbJm+cC{($EV+pOcHynwHi!+x_O@hCfz!pOpR-t|qe!D0 z3i2n}z*koEu4$xc4BGlT7r8)*ekhDwKi!5Vqv7lt*JO@R&v&dIn%+pV9uB~SLNli6 z_Jzax!h52}m(cW;6pW5vnz)WYJ=v4N_OVMJI<>pVHk9YZZ_5mqd_iN7ep}86ray{_ kYK~AwxnKAsv6ruhfCb9m69wAee&8^*woq7R8H3Ou>BV|sA>Il&$v$QF#G?* z5}O{JdvIjY1M9=`8rf2hwTx;G95{BM`}+4e9$JM=%O1|_`X?@sc!b4E$HKdX_1KS; zOgZ+&Vm(m?SqE3JE*9hA=Kd#O@{m7O?2cl_LD^F}GY)gTe~DWM4f;HOt0 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7758d210e0366a8c2605ef696b8c0440bbb35a91 GIT binary patch literal 256 zcmV+b0ssDqP)e5cR?DJ~&i2#b}2(SyMVMeTovbN_ez{sG~< z`~2)185S5jGWbJHV`gB-qTnavUnpzi+5NaoVZm*|Ult}VyuN4u4`uwrV+zx7Peq0y zgpD}A2A?bo0sGyEa3o#}XdVZn7K~ajYQd-lqZSMY3*hYrvQ-eU;NMe177$WF$OoJL zzriQ}{|)0N3Y(6ak{sHMoH&gC{^RQP=cDcKQ7{UIKL7yMOD#M#K;Ys40000 zI}XAy42FTFy;TOp0t*8YdzK6gz|vlV1MnDFI08(J6%v*3nLdUJs`6}TDgny>ua-YK zjx9GDUS3{*D*_Z6^VVHjpQVpUkrF;IqG=CMCDRMSiGei&jUa58yzu&pNElioFbKk= zM9c+YSA{h|I5Dzl_XhysK(}6iV7?BpemP=`>Hu4Nz#jk-W*7`Jk~#oeRJIIgq1d?w z9E*#9-h+i+39uk2M1-u|0`ArZQ@4QeUCh`mU`}}RM02-*ly)Ja2X4qeaLlH*k!hgY6HE>4Q8(=oF~S*7>#^7=8I0eccd;u&?tg8%>k M07*qoM6N<$f~K8$O#lD@ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5efe601c28d0273e0c8f36fd00defc033a2addff GIT binary patch literal 335 zcmV-V0kHmwP) zJ#NB45QR;FFJp<#ytfh*NKsl#Mv8y}Za|GCiX0$msEmS`)284rjv=u2T1wEq*N*1f z*`L{oqM)FlhZVbGY>Mobd;O?WD?R7jLhtYfuQd>2?o6*x?Bv-QVN)m+Z!)=Vu1<)8 zFOCRgP^e69#~cxkRX76_JEya#0Z?pu{dNJ0?5_d1`t6Bv*%;vL0C)gE!G(-u#082L z0DKp7Xa*fI{O}CmCkCa6!G(t22(V=Ldqfg5_X?=Cji;$!z_{}3jXiHs693pDzrBcC zqO^YN`H-PpbGWY}i8 zJxc>Y6h%i#qlw}of`yBf*x89TsYC<|3nAKx0Xr)}6cI@y1XC=Q!Nz7|C6Xqj@B|-5AKxZH9|-ZtC5IePqGo{&^6tsm=L6B9%p5~@Hpd1}2vKL&4X{cZ z(P90|yc?MzM2IR`my=ULi1JT+V~k;$XGEKo_yC^=#3OSN_)~;9jt^Kvh|7NKH^vz9 zyd$dd0lNrs_{R=!8Ho$nLx_XO_!JGq9l5xGErcjV#^-rKgk<6Z7I{O2Q7dE{Au91} zFv<<0%|_qogzbc)OQRJL;)$6)J$`}^O?(%?G&O_>S?WEkOmIRA(W2mb8!ga4h-Xg8 z<1@uP+cr7kn#tHD?rEpGwR`8D1~2xj#Wk z!Ak-`6vkIPWMU#h3S}T9SZUkNdn*MJ*r~)*hY*QM2uZM$1s<|P=s^UF3QhfMdo#P6 zQEP`C_td_*&4ceZ^WOV*R#Ck0!i)b9J~gK0;229!gbPRwK0Vw}{Lws^eZay2iX z+rT$&IK}S}u)BpLE->$ecqANMcie;25_{x>sORmnnGiXe1Bx<6D~dM1uvHt6pNm{y zjY$A9QR9^4+U@VdnhBBokoJdKLGojSn|xBJ)vshLmEMF1wf4yPOwev?KkT^0Gdn@o zl>5K|_W0z8HN+OYQ?8m`kwj$Uc>n7~=xc@ju!(L+Ba#6Gy@E}A!VYV*IxC9S`k`v# zYSIX6aLE(jYMx$!hKXoM_rg91Q8S;=3I^RmG%SQmw^017VijsZz4+yppoy5kp<6#@ tek#<2U9Sb*0`OiOx+w337he2__yk&)rRVB5mT3S0002ovPDHLkV1h{3{_g+) literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..1d4e52d874a5d1cf9330c8ca1bf4dc09e16681eb GIT binary patch literal 453 zcmV;$0XqJPP) zzfQtX6vi(Jld!;qBqSXuj!ZJ}2r}!?U}Dff#M$ryJV3!j!#jYmV8RH4@~_YIa}e;> zw&WIC2GeuKbNYR^UwglEgoq-FD3pUZNh#yO6Fl)QWixn!9+s57py87YYy<<6!4l}q zYy>m(VGapW{gon%9f0ZD(cRS-6#ZL8xC9|f6!J$7{Wsln(b5}nj=X54Kf~o$s1Y4Hj6AGj zrfL<3nq5oL>_WF|A%x&o5@s3rz>!8~1C0w_Tt?=aE(J6ij1L%@LXhW*%$}iN+b%9- v4jB3;SX)lKc!jx*^TWT7P$LeiHq$^uN>!cgEEMLGxLPQU<7DhM15 z;Tud?lro_NtfH-5QP)Z3Z^>?d48rf5kGMuJ_ZtI_Uf=aKdtU_l#K} z<`KpFkz@Z&eIHu4%~->s-9J4vsJX+%;j0@uqchm}Sj8crRXl2T$|RuK#c$Vy5b519 zm~-TT18&q74z^U^#~n)kQKer=lTKs2`vdO$`kRHME4oZ3@S-!NvwLirkczG7T)x zZ+aTKUD9hq^hwKo?{cU2Ip;g?cfNa-a^=d^pM@X)sI0xDFsV|-B!yQVut^(jJ#Es` z_F-zay+%_bEDOl?*=saeS-_qHuq$crqu>B+$(gTm4;1m)T&?^yqi>@_fQ!0-}H z$N_#jWJ3<|Q%sLVVrn;9erTg*@r_~v2T5~H56vqjR=%zWyn@r1Np@+W(Y$a%ln4h@ zsBplv93;+}bzR?cXGh?cxa2fj8!~V%MZb?2XMX_DB`JI88OI(nvVdi}13O5_0s>t1 z0m=lVJ0n5Ewr{XBisp+^piG$pG50^im8*Z$2iemaXc$W$nE(I)07*qoM6N<$g5;6J AE&u=k literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..04e631b05e3607160ceb5be9ea42b72e75d3f936 GIT binary patch literal 478 zcmV<40U`d0P)pwc#sNP=L-Px_IBRcE2Y0w5ue7-^xtX-f+{YLiZ+uQgWhkYTGTfA#ntcBe2IuU$ zUH4Ap@JH|lrGlt{nRI%A8}!Vi^y*l^{gnRR+76-l%Inlb4?Sv@*Q5W82oe4fUrUG6 Ud2IFGaR2}S07*qoM6N<$f}5M(%>V!Z literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4195867c49fbbdf1fd3c7e0a75fb11bd66970bd4 GIT binary patch literal 405 zcmV;G0c!q zv2MaJ6h+;%kw_#DnX(}MgZ=|w5_N1*RY=qli2(^#egmmfC-#PKIvsFLNN}S#?&j;! z*1Pxl`Mzf%JUl!+hJbrQOY&emy=A_q= zNk4*+B^4(sGGi7ATWbO#2*?!03Te2e6_quC5X71yZ&M)2)vhgp5CoLBv(V}*O19Pn zLLe#HS=7`QND|i81VSKn7V~qtuhdIb?hk7MoyCF2w))zc(GX8d<|=&9RPU;71dVr9 zI1S%&M)@D%FQqkldsz5fhmS9p=3afNsxjro*)_EvCQ}X#>m@`k)StuO@x*t%fsnGX zZxy#}74fVnawI7``(|gb6%ntBqGUh3UEi>k&ceaKG1!WTH|v2z$f9#(rtIx!a`(_r z*$o)_>f5sXW8iNFeOo#|UXg>3mxqUk$1i>W!yN^L&n3ny00000NkvXXu0mjfB{j8{ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..39f5478197a925595f997e2d927eec170797f121 GIT binary patch literal 423 zcmV;Y0a*TtP)h{ zjc0t+OWgh4G6EA$Sl|N-Os2AMZOr8f2r)1zDIjBX{F)%cLY{zdtELFkAqAshTIDE9 zKnO7^je{&2^#x>@%M%brMLNu)RWDLTT^rBR1YNDsp)7Y-eM#!t@GVWyWO0l9q5AfW zw^6V(qtT%ZKV21782$fBr0a(Nr!$n$mUi0PO5qz@)!oT>ih9V(zs!yYr@HPU^;JLv-}+wyM_i;^#h9(Q1@CPG2^`EcNuN|6Fi2{`;K9aWAXI2ory5Vi~+A zxVN}z4)j95ox4EQUWCf`lT2(BNsNhGKx$mXEm7m44|<{B((+Qrj(}}-nK*OXw==W( z$0S}B8|v#_*~IWY=%0c4&;xzLpu1494=FP!(jvw!|yH^`(PZ#V_wb}F0Z#j%w_VvV9)@i z-q!EG;&X)jMwogl?7Bncy|!HVivUnAfqpy0nEVg`$|xU{95mLyR(wC<@=BM>qY^2x z!^TXkvF+P`&o%Bmut}R9SLs)tf z!qlhv5(B=13~lyGdJ(OlF6tR_xY)jw&-Ny~VF9i8(L9LNtxx}5xQHMu?+Q`ekfd_+ za(G)jn=Ti3MiB$9qKqV%+BzqDpt8+na;GBk8_8)~PljbJz*Y1H5njnh?4(M>$&H^A zi-u$`UjbJU>s>5Is5o3J`Zg1y3;5zJy%h-;CxSpg z5FK#2+@kV1sdzYKNp=G#@B(+v#EnwkN?2Zm%U28;!D}{Bw`9soYTNw=FWC+J-~m3P z@#+wLq1~L6s)KEnArOr7jBoAWGk&{M>D~2@E#VVX^G47Ay^t+p*t6w9ss%g(VwTtg zt~~H=G5Na9!V$uYA+EInf2aav&w8MjOHAEllh}qYri<7 z<#QzGPsq}Mp_l{dLbuUoF-P0q3gb#%euMjWnA){mq_plkiY&hw3Z`7vM;K_rowHRi z1-?SP(eY*jc(B#&o4i=Fq^%NnxtkJcEvKP;V7Vz#L>mcSlFR2p+CLr=W{0X>P+K>( zksK}`k7^^q4(t`*Mlv-1HhU^PcZ|kgCt>1y)Jp)6Q29uqjX&6heZ)0$<8Sh}YNwm) z){jF>A7Gh2a%4CphGI`C4v9ez(lx7R7{i*TR$41*98*YALwB?!U_U9&>L2T14PffNx z1+tB{Ju%+))M)E7<1J5&wmvZe3Ie6J0!5yH$Su!+4CAd&je*L5YJlu*KxH5W#z19T zKx%;6f#OD6Kq5eMKq6oS(hfERtQ~3&LIkD^q8Nn)83;r`>!3zqW$fPE`5PDlAtgb6 z!3>N{%q;92JiL7Ta_YKz`o^YamR8mdPHrCFKE8hbVUaNjdBv4Awe<~6tzF#{CQq9& zZ~n?n+qUoAf8fZe(`PQ;ymjyXgJ&;Zzj^=Ra_h$;adN6O}FjXEL{4rPx@}*u`iq1V`>iZp6Ok@Z^j| z1uNb5%SZijIlPZacWUIMKXxW>Cq9v1@Ul@wzI~a6=RO8s)=4$ov!YafGMZHWPuj=p ztNU!>ahs1brprEaonCrc^ZbXr9cPa7K07S-K5+8S1!pVIdYtWf`#g_cKw-4U@`Sk7Er$28Ja_^aM`*7yeOJ9x1 zHxH)z8A|T5jGehq*P2_szw;GCyhMm_zdARcZ}i&u<<&m|^=tR8%h|48e%Lf)b!(Zr z?y>3bKg;!0{QRA2{r2VK2yy2Z$NHAL_pM$m(>BdEWbxJQi)(up$lsMyw3C06=(6|0 zpABEn8tG`{@`#3CuZ_64Kj>NM{kz6GmHR%=yte$0+?tPng4=#_N0cUp#b+H@^=p!6 ztrF8|C1JLoSM_vWuakN*KjH3VM?2=%Mr=RT)?`aPv5j~x`NYxT)1>Kpp4|NO1ss1Z ZKX`NW7cNn8?N9?{GEY}Omvv4FO#ll7+cp3I literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_toggle_played.png b/app/src/main/res/drawable-hdpi/ic_toggle_played.png new file mode 100644 index 0000000000000000000000000000000000000000..cec4c0bee5e8bb963e470662ea37bc0010c9795d GIT binary patch literal 501 zcmV4qH5H&q`P)~Z%NIf(jl+d_4n<~b0PR*en#en_*Jy;J?!S)~)yj7}#{@QLg*1GFQ zlPDBnUm)O^yf@CwzA+M!kdTnLCMbFWtB{90R`o*1VZdCqRz;}tfxer{T{U*ckP?J&ECi@#x)bhsEtc4iULkCSy3kS_Jj5K}^bI=Z2 z#YSS+$kT#Vj7Tns8);;G%IwS5r2hg>-RRwj8m rV`ZLP`p*|y`bYn^NJvOXTot|m0B2%~w+fc400000NkvXXu0mjf;&S3% literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_toggle_star.png b/app/src/main/res/drawable-hdpi/ic_toggle_star.png new file mode 100644 index 0000000000000000000000000000000000000000..c30725f5e0c9087e6c4966e546534edaa37e2ea6 GIT binary patch literal 491 zcmVJP9@M-^NSTpC?(iYBoJ#ig~ymZpYiC>#Vu z5JxeDZ0OTs;^`&tJ@1CV?|8r8eLmd1-{anC+9v-jBuIEGs8I0|zsDz^?0F|J0WiJ7 z>+}HVp?f2^;5QfE2p(sGrWg1_=5y%2;Myj*b{ijNY!i$LyDKQLn1Y*lKkX$!8^3#k zQ=D?Dit}L*Bgr{gN?6qBItsefuqct`oFp*}e5;^ zG%=i>VVkNe_$u3fzx}JV@mDKXLeschYYi?%rs$ZV5`M_B=%B{wNaH)iW%j~8&rcvFfRrJ`zOk7qzgsog_1?dE)-gLv!I*J+(mDMXb_~AUW}SS zLkBry{Xqx{afU?@5~H?8tu)6R)kP`;r?rn4Jv%#d?|ZiQ@V*zqeplx?&-eHJp7Z-X z&-XbeiWcJ`3ym5pE6}aGlJslbamSj<3FN_>m7=e99gM23jKE8#V_#(i`eT7hm7sr0 zGM+BKz>JvM`}ozo{ypz@E_BXJ)ngxvX?c z)M)Um7rp8&C!KW-eEqGI-?Of;! zKjCUfx@>hSa+M1K`G+SsY zbd{5V;Q3So>CAkDY6Mei3(nT~4{XhDa&2I!==_hMBfG%Ipeu1_d2-?E1fOOX7zZu! zc()P1+8vJ%fWPM1yG7~iLlf)6lKdogn?CEKD2i+d&_Zq>Ga%f{^9+ zH$5#9UUzk$Uw1)p@4MX{&ojI~zV9^64LANcIOf<%e3%9e!p;dY2m~3Y@H3PV2+GVj zBiQ2|f#988X9NYrFbhuLqjWw69isLN4nOe%!GYcQ01rCB0|9#l3Ep&qHxhQ@yKqtJJ3_iv&23vgP_GeOUyGh4sM$iS#n%+N0lah1J*42G^ujOH94}R*#2HnmjYVl z%TLSfhQY7k18#m?hD>wHt1|wT9Me{H@R6abgzu8UCsS$Y#y2H zurlrhY*3NLSJ+S)vIs>fe36Li9ET`b;!A|o7sUj(mV#R*)CECmmVz2Vb-~(~C2iiG ztLlQJRFRYFf>SBMi7MX5y~#T0n)H!RO|VE?2kMb$nPu|ybkEx?stL9XC)lK>X&RG; z6K+#YkR3VV&-LR(h>MX9%c==3hZn_1cJLeC`q|-ec$vIZ!O!uGpicVxK_W<+I)W!= z#}~wDamIq>cMF`+B0j!gotWGKf`=F}_wUb*e;glP=m4N+=lX*H0000=G`P)r3aFqI>cXAXO;xD4vnU9@5bMH4DI#j`uXUkP zlRk2LNd%#Dx8XZCotbkQ!{}GP{pucq7Yotff9D4e35o59@zy9`{TARWYcyH`7azVi z_^{jv@!lYneRv5;Wp6M7^xW|B6fe>SYC!?iXoDA*r{;!xnG{EOR6vo-e2zOj^Lq=n z&Ea=EJ_>$#;~!v%b!jK5)MESWva6(xMdp4HrA zGmVQ-feSOO?-T<#3l%ss2DJHZiXTFMpa(alEr4vmq}w}DRzD^^l_m>M{|ye5rf6Sx zhLX5!V+XA^a;atzE#=Gyw)vGxR>sOu39tSwcINf|N2~-AY?Fr!!!US=HUkOAdIE)J z#M6;~oy@d2ZcZ2#^k(SfJ3?-;$|EgaHG);X{fZ}XqJhs%uG~YSDz-W=lA97+s6ygi zO!R(#2Q@m(PKQ00ou)(7x&(bk0YSE6fF#H!yZ`mquYX*hltHd#BD}0%00000NkvXX Hu0mjfna}$7 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e15db3540452bfafe506311a71c445e06ca9a51d GIT binary patch literal 6450 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..160dcfc41f241fb8750d5ec49ce3376f7f8f1d83 GIT binary patch literal 415 zcmV;Q0bu@#P) z%}YW-6vdy5tX!noqD_T@P@ukM&e3WSZK7QSEt4RKgo;{O3o93qsVxkV$h?1TyRzVY z%zMw>J7@np%-lQoj%CG)6)RS>DkfE?yjYl>0(yIayZRiD?BPv6sSCZwbYyU*amW{p zArl^>t&dbD0=&xM;SYMkx@G4>;wvTgo$wF{`>?|lHOsCW9s*&nm<`HLR8G9`5D05l z!jPPsQ&pVe9v{8}hA0@8m5qNrKo$+6iq~3;YR=f002ov JPDHLkV1ka$*QEx%A+Xr`U9=?8q%SYoTr8+uwZ{52g z;jz5s@iwtLJJ!F|t(-M~OQg+;m6dAQ`@1t{2((NTbexe9^xk>5+n)aX^$|PID%Jiz m@MzXI>u$AR5ul~2w)&ErXBRkMm0Jn)0E4HipUXO@geCwkBwlv_ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3a256ffdfb321a2a2552105e020ce9b2eb6ff6f7 GIT binary patch literal 260 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0D)1EGlArXh)PIlyKHV|-)?PL+p zYk734*1@Rr!6EJ$!5!ir3%_vKHk70m-%<E#bMwEvA6?$NaFKdNCn+5zt!zopr0LeFIR{#J2 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/main_select_tabs_dark.png b/app/src/main/res/drawable-hdpi/main_select_tabs_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..2775c142387a59624353526d5a0915448ce78957 GIT binary patch literal 230 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0DwVp1HArXh)PT9!SY#`z)@7KjK z_g8&nsn}haFZ^B$FYFhLS@d(+<%NM+-peI4zRc}utbW%jAm|z>;`-Ct!%*T<1yl3U zq(@uj3oVXmXd6V#v3Fm8?(UdJ`EX3Hj1O>U+#AJ+hBP(j& zKHimdKI;Vst00opbiK|@2377u&l*3m6q_A}k+9-zh3o-XpF&UihvMVU~G z^J))$o~=812EE;(woJUoL*s^ZcD#+C&V@Ff5qtYBVr$_Qpeh_c?RPCkRr%dfMD^bO ty#>`G@_wR4`t7Vp1AZF}!!Qi9Rx^*bMkc7)or-~kWLDm zlBv60bORtGoU%#c5B;tSfNCjI4L}kUc&Ice$^bIWAIVgezvaReZ%1HbCo&FZrU>s`r{TTv_);chmss9W@Bo zPjgH`?XXAf)M^7ytp%tbISb!Q^Uzfh^M zZY`Cli2upS#WK5kqQjDMVa||e`y3PucX%jFnCYc*;YJ6`rp2p07VMhrpm5K+ljRb3 zyn;aHTNQy5XKN-pRO&W5tf>~^yz)(m!^O&dqC?#yP8O%N%`8kaKRN?7dngFx#k(*q zed(+q@MeyRfCvA3rN&oYOpPi3b%ZqZRRuaq`jr~5+_-+9jVbVl&`0x`^*%qvd;f=g zkhlIB71x z2l$=uh<`iJx8%n}DSMG8$7lUXxc0Y$X&TU*U;V2Y?UXlj#63|_0VNJkS3j3^P6$8)MK0xRAui3|c{0!5hSwGE0Yi6>x+^CoCvt)6Zx^|mL%}$mhs-H_(IIkGx zT5W4svB@!iLBo#pjYnDI^BSY}^|NxBoY=BnL}0tlB~g_C^UFFPQr$Dm-wAsC5mJ|1 z%&uvlGSz>okoiXM&L!PDckP_$xj<<59KS!#lfO(i`sn}H^YfX1`*Noqk&Z6U+%7gb z?_HSY#rb*FwjUYYFDWlO(q1!PcC(*&(xg8NZo@+x81DBzF$FlNy1g;IwhI`=44$rj JF6*2UngAU^-pv32 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/media_fastforward_light.png b/app/src/main/res/drawable-hdpi/media_fastforward_light.png new file mode 100644 index 0000000000000000000000000000000000000000..2935e61ef9dda222b43c304572f11f9b981c1f11 GIT binary patch literal 557 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%&M7!1%?}#WAGf*4vrq^_vYPj>ikC zxVV%}RJDi@;C_Frk3(GVQ16k%-H|eHTDoOZ*kuHG5;wk#U%l4F=iB4l@B3z{|E!x= zJh^t)x8HWT%|KHTU{a&(`lT_)yLQ$Trk>f%u)f>+;N4?wpJO&@ov#pk5M=V{=$93< z8>IfKZEI#+&$LW$Q*a*BD!WQgpC4iq{upjgG@oqG^s9ac=jMH^D*2x-9ojLwVU^Ww zZnnQ%FLsF-_c3K@c5bR(c2rg2=jK~en*ErnG%eGkYM9d0rmNp&DOvWB+3-oY!}ZYj zHTn7pukP4gegrgju58);OR+3VRxD%y09VRz19I{8HtD;bnz{z^Qf91hjw0Ivp`Ld@CWE-eRA!=%T;2V%EH9 z&Rb_=j^F9H;%8p7{qh!mlUg}V?YjOcNBF-kSs&UFpR@PEz9^O2owuJX^*wa`^$~ds r4SPeu`X|c&&UpP>D2<2~28O7gOdTfAD@~0(OhCe(u6{1-oD!M<e{A|Vat3uB3mwXl1C7=14)qK%yT{Y-%>|FL}de?2N3s2USGxO4)dF|4? p_}ce?QZRuKLI@#*5JCt8;0?*jxPQ8L-lPBk002ovPDHLkV1n@?$5#LV literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..fb819e1e8d45082335bba4b3b72730bf345a7980 GIT binary patch literal 486 zcmV@P)!!(xrpWg1ER;5Jv}53mx1FT1A`lYaQH*inW%! z2k(bnA+k@8L3CWnS0Em}r<8V@0 zfXHx(qOt%%>!!zRv%&%(XwAx^v;b&#)v~kzh-aK`7L)}DqP4xDEkJv$Hi|RC0wA(* zwYQ)wKoG4^8}^9HSAfXuVz;0yKrq}^)};kN>!kJ4w6FjOTD!6-EdW|491Q+RMGQfs6Fpmo#zWI|W~v^Q&}Yr+B_sWlvY-_wX( z0JPU|@?C0;UH}Af^K}8FAwYWyN2_zf0w9R%&r++B0IieO`_qz?0EjG{mF8U_&sWH% z{3G_?=>Iy%#G{0Cm=llF;suS}3zwAQMZI=gr5Db_vbPrVYVKmo+v`5y_|=GMxP_eD zhkX47ijIKxm{onyExl@>z3lPxlo_Rf^um+&%9;4mpZ40N_~Pr}1xjQBj^j9v<2a7v cIL-)o2SW|G+x7QZ(GAIbJI94rSl~O2ic(GrNN1yW( z>*i0BK88=4Wa4M*(EF6>S|(e@#u`STHV}AFyR>-H7DnggB2ymy-`N2Y@^tlcS?83{ F1OS&IF7*Ha literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/media_repeat_all_dark.png b/app/src/main/res/drawable-hdpi/media_repeat_all_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..a26a72942ee69eeca32124b76d5ffa2d7f2d380b GIT binary patch literal 876 zcmV-y1C#uTP)bNxAg)5n7$?>jr|k8x)AedGPU$DMiJ?`P)Sd7tNbEz8Kr$jHdZ$jHdZ$jInF z6xV6c+x`z;>TTcF7T}+KT>%d4>j@y(*AYOnuOEQJzHR{AWLC1T7XY_dblBGkz}3D! z04eO-ydp7a^y-RwKZJiXjk?46vz{{GnI8%>;52cVX}Cj!_XW86^( zq23e12Hl)f1)-Yd6gJmcV>czlIOVGU6S^qmDpfpZkR_Mjm$;|RMn)k|8Be<6H+MBk zCCH&EDPLOPhPntYsRWn#;P_uma8XqR*IbA*ykJYZu2L zy89>L^p_zOQf6vnGezX6?+Cd>m~z|~ry?_zRHe&#z$DRBA680;GMRM8B9)md1}m9& zIh9#qgifj{CrmM6%BiN45msDJjx%T9vMh@-sX}m$EfS7%0NZh}QI`3Vyoi2wJRGy` z(8nb;x(Zp;Nn6`-#te02sni+?(Vld8muk$?u4E6C0wR23^$_c;3=^S1?KV{j^>p)v z7|U#8v&k|sKGRM8X);$MBO@atBO@atBO@at)wBl?RfFbse+Z8N0000KeG#-)DcU|&utLSC6~tTXB~ka+ z_Mx?%lbf2$=A3fA^O&&vedo;1`DVs2G&D3cG&D3cG&D3cH2yA`XYj4BLB_(Sn4PY(mjvfJs$5%T*vm5#?MQS6cWrFpOCcMfdqaztRHD%Jq@@pHhYeQAEW~-Z~4Sw<-#F$4=m*f&%POex0c(z~*It zm0vdO9c;T>F{3nLn@q(SIUF{HR!NY&Avyv6q zk|Z~C9W7PAc5s20Amug(gN?Mq1kAgAZ$l< z(7`Q?-CYoT&2@|lViK5ruD2D;1xoSluJyDbAYVhH_brJOY&haE)@P)b;@5J1tjgk8}@sUoFF3Jyd=NkfAxdVCcQkR~0CQD_5_?KAx}7BLQ$H9M( z5}1(-tU0FvpySF~<)uo_2erUD(cA$#T9tia^yp=m1|ce-Q`p)f13LD$0TsDrK>YDX zfHMo;CawkN1mlkYHHFxd)EPe#pkc=>#V3FapkbqUun`C7jvoQAQ9f1*11j}Xu_!?6 z%@YZ*O)$P3AcRC$Dsq5yN5LcC{W=;1guoNe{dx+X=XpjW=fG#b4oUW8-w;y+$XHE( z^vMBalwxP%B!H6EFn}ek;H`up)w8KjU|t zm{Uw#!+&HNfCuAuG@L~Kk;Ml*9A9oJ0Ah}>0@z3WFQx#+~Gj^lD>tDR{ zDzIMk_TkMP!QM$~L@;?291?1qccZ@qn}D8R84v;)m1#h~FIWbIK+e`Q!1FxM^E}V< cJkOil4=brHkE%VXaR2}S07*qoM6N<$f(aVUOaK4? literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6a7f51aeba9a87cd4bf90c9cec23cf6ed4a39fd1 GIT binary patch literal 500 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD3?#3*wSy$m0(?STfwU1A02vMr4u*z?+S=L> zK?4H=AlDcSfJz{2b8~Yb8>m1=Mg}MY(W|eo4`c%ssH>~%>FL20Yieo&#WgfEUMFsw z1Jt8b666=mFsqw|MO1=urhn`-Z`+6Yb;AAYi*7P7FiLy6 zIEGX(zP-M?=!gPOYvM_zi3!(LHh!-=`SfqRk>gT*&p~>er^~wD2jB#MpYhbi% zXjFJyqj3t%%u^L?)PQG1RD$K;PqesA@oQc)MpOY))Fwa`UxU2Y%Q^zukXPM&kB+bCw@=0u}}m6Pj3J7?`)*>HYufPu0tB&de{b zvHi<)06I{CQM{pnJE37q1LGtnHji_>9g8%&7QGO^JCUz}qh{h&V4yO1y85}Sb4q9e E0774?G5`Po literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/media_repeat_single_dark.png b/app/src/main/res/drawable-hdpi/media_repeat_single_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..568cf18796ba6749ad1c3f78099f87b2e90ecb5d GIT binary patch literal 570 zcmV-A0>%A_P){r zBj-6Igq&YmdVB5-0*l3Bu~;k?i^XEG>|u!L5^4s-q$G+7#H3^-x&b*E9o>MO52b_| zKr3Mmpp#Gn$P#D}IjAO+IY7=Qmm&{UA|6N`)P!jXkduk53zbXnwj>B%0lC1^<{6MP z&F5VU#6`$SgEMPrQ3fPW1j z;*Q6n?rK?+{|IpOR=dvtLb|Ij{mugjNq(FMb{>$BDF)CXTQiY0Y0;v^iQmz5+x9=4 zZmY-wX6>G~7roI0qM+&bJV5%kTw6@tcry+qKt<1j~1 zyJZs6Qw<0LLWZgV#k!yw5ClYAs|Hvs7K_DVu~;k?i)HJ41E}o<{~c3sF#rGn07*qo IM6N<$f+PO(1poj5 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/media_repeat_single_light.png b/app/src/main/res/drawable-hdpi/media_repeat_single_light.png new file mode 100644 index 0000000000000000000000000000000000000000..d3b75bc05d34add7c6579aebf5a7414102648de7 GIT binary patch literal 600 zcmV-e0;m0nP)As=6bJAGZxO0cq|ib-YmutCqd175ixdPMTow8$96EI9;-HllswMffy(Csm)YQhy zOJ05dH!AvmCU@suG>t-`P$(1%g+ifFDCZa^+M15I{}$uZS|2gN0dPwaPy=jk@g{D7 zEtW_LKLE6Z4FD-&8vsqnXzcVdvgT|9a0t~7%%sZ1i~Q|QF4?}d4RB^8kafX-=@l0Q zAy$A5u(ZVtu=x-*z#Pc|ju!yXegH7980G6|RWbNHf%XG{X^W}4l0fap1hCA3Tpl&7 z084ZMH#W)vuenpVu! z^IjEmySGJS^=hr3TEKDPj|3n90sc3D@v(UIijOsh4xs8^?O6kK(LQ|X(+|+)S{w4k zsRzjREyMucbkCMUzSzi?3QpZ$#=A9**uGdQJywAQtlLKk(5szw`v?KLJ9KTVfMEN! zIWw{?7Oi~G$Up!)?Yn21GbPW+008^iS6A@^Vr?G+z=yFu7X!G3m^s1?h?f8~jb_Y& ze=8Fts1o%hC>d6~y|`U~HgA9wEDH!~xI5JCtcgb+dqA%qbB6U=kKtIX9+4&BCY zb4tis#_A0&>AMSGV~0D!Yyv#zfB~U10T$`SXwcuj z0hVI!T$uv2IF1c(BtV<9gxWF$nB^@uiSbheXz+pi#M?HxdL` zN&Q|FD`4C=Vgv}-jv3S>VE+j9D*Om}@Dh8_Msxa*9Xl2njuJOd8YKo!1u8$xjf~TS zCydiQr{lB-KQvzaQhM-HpUYaM<8-a!#bPU`)#6Xb0+nCx=CEF0aJ*jk;DE!6Bc|b% ztnwjW)ZeRn%o+`RRX*qzLI@#*5JCtcgb+gb%s(z?B}OMdxsLz<002ovPDHLkV1g;J B^?CpR literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/media_rewind_light.png b/app/src/main/res/drawable-hdpi/media_rewind_light.png new file mode 100644 index 0000000000000000000000000000000000000000..7a6d6aebfab0e6a7ae17aca7fd19f627c003b4de GIT binary patch literal 582 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%&M7z{Kn6;uunK>+MWi@52EiZP8Am zI!isK2L-Tqi#9L&J}0V6@2%1U}_E)O!yuD5)@ z8E@hL?(EL^jS@g(;oy(@v`I^2=Y5@}mwu%2(kD%Qy_KnFD!q*h=Ssa=)nD9uf5lT@ z1@pjLPt~?g;VhW-?PYh=J(e#zbGL5pV&R#aZ^gPpyhA+v#lq=f3JW@Uv%Z`zR}b*G zXjl8sgIzb^+=syOS3ATXWbTqU!=`iK$k%w*6Wkdwi_?RxEExSRh?$>#dYV(mZPyoB1u?;YpS4?dFDv2ido`+-GWg zVEy}3E{jQ&e9jI=ZXMO5eDxMn^HmMzFFd?(e{KL%l3R?Gc)n@^N1^uAZ=MQ%q8~NF z_eC}c@6j|a+6VFn+qCL%rmAWEPdD3f1l(Jge}nVLN~iDZE-8y$=nAbX4X=w=u-7ZQ z({twptIF@T)&rlWT&{Y!xuN8~gxUhp)y`&F0tL3MM=s7^c422y^63;PJ)4;4(8mIp=w0 S^cLPLAW=_OKbLh*2~7ZM;R1UA literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4039ee550002f668fc05b605c78bba343bcfb7e2 GIT binary patch literal 364 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%&M7z-a90;uunK>+P)FyoVeFSSNaL zbgpdRd(kMifJrE80lU`%#^+1;IJPZwXJxBDw@>!9FT3)?hwsYFrIULMk4XRtfs)Jf zbT7PEYaFd0QuXS97{}IKS>9qCt#MYHSe@kWZdK-Paq9KUX$*ML*qPn#knrf1rbFjz ze@3U6{~S;Hv-unE6uwoRV7YED>!xF+6C951;1f9^eqE4h=OZgGh2R)>?v&2gER8aS zb2S3&*Ris^I<~dL;p`4EVW2%B9odeJR)xAc8){;@xpic(N3u-od7Z=Lee^2R>OY(d z?UO3pF4m`fSj_UvfZg72vA)R*ZPhHPgg&e IbxsLQ0Fnrk6aWAK literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..068c4ba327c1396034b9fad733167fcfce00973c GIT binary patch literal 369 zcmV-%0gnEOP)+y@0ii7w`lgz&fR!Ak1k!fttikcAoxsg9ph=c6O>t2qAhiTj{e_Fd=(vd=rTlbuTQJ$r?D48z@APr6&HISUWl10 zNSIQ^jlHR-@4pb4iiEwSXw^goDf_AlTJ{wc)a(lt^z5_7?2O)=d!3|6Q&-?y+Q#V7 zus5+y4XKK9tjf_NW8YsOW6Q2FR*8zyWp`WGrn;`pwlu!b5UO>lIID00000NkvXXu0mjfJzsfl literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/notification_close_light.png b/app/src/main/res/drawable-hdpi/notification_close_light.png new file mode 100644 index 0000000000000000000000000000000000000000..7bd2d51e0e38f8dfa185415510eeb4babbbc4512 GIT binary patch literal 268 zcmV+n0rUQeP) zK@Nl<3`JpO!Vy@z(T!40!U%yr%L>~z)!MEY+V#pPq`?p9=j;69=Ry5{x1bKW3@Ya z&wa_?A0|C|&mA;+tjf8#ciFA$+LG(q=C148FPz)2%y)_Q=+ZYW{~9q2!-yBYlZI8G S2(n)Q0000GO0y9rmuZVso6p95?Ts^6m6~{d}N@7(8A5T-G@yGywoq CJ7Ssu literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/playing_light.png b/app/src/main/res/drawable-hdpi/playing_light.png new file mode 100644 index 0000000000000000000000000000000000000000..48203a18601a4e6a917361bea72b3484d3502514 GIT binary patch literal 266 zcmV+l0rmcgP) z%?d$L6orqUNEyh)fHFbp%01R*BoCmJD47_`@RpgOxl6=gJ*_EoYjAVE7x-%JbM{`& z1pr*n)QU6PFVTK%`jtno>XzCxalY4*~J8!>Ik^u)YKY?cz64 zT_^xSJH}0^0JJ+6t%^_p;)%p}T|p-xXgBNz4WR%8?TlGe5(+?jVUNC00D`!Vr=4%ULL)JdiCz7})`)J}-VaH)Bqw89HaHJ|k0wldT1B8LpR!q02;LTTKfqdz?EG+!oy(}!^!dF>X*qeD+qVMt?xg2E7SZiz+=IKyi6yoEs z;qqyRJ!`fI3B>qb6%vr)ohl@7=KnL-i88HqxyjvXqm|5-a!zu1^|22RfVDNPHb6Mw<&;$TdlT=>- literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..437ff7be9f2db5c1e9da4bb42ff69b4c69d90343 GIT binary patch literal 436 zcmV;l0ZaagP)2LqUu30HBvb@Jv^F>d{eYu1w7Ikd4GxVB1+_KwGdAQ<;TX{%jS(a?)E+e( zEn?7+)zzSUT8@|Nx$3!ghah~%?>*XLc-TZQ;ori@iz zY|&*O6X2D``efK1W|D7X(+N>c(=-N1QZT`0)EGY%v3LQGb4HGvv&j(P_m`P=P2Qlv?f=BXwSaqAb`R-hu}ExB5e7jTCN{lIvcuWO&ASfGA>D)R~3e*8FMC9Tq z&vkyzxGJ{RQi?2DvU~ur-e2q z05Gja^_kMNYaVU5Sw|P%sh#76OhdIyfk$%WC{Qx0t2TRxheZ(utBOptdaEV4mmoL9 e@U?IJKi3bO4go=%)aK{_00006P)u()L72iAF>ubl4>s;UKBu!ErH?DCSsyJ;SZTb*RN$D$yI^rRvBC0^5l>$=v)<1y{ zd;{@;$0zvEAR$o|%0nt241yM-3REGXib#Esrgj`VH*w;7cW0S+lRInwyyu*mjm?q0 zn!7u5cIG!{&YW{*W=o@^qd^dqGGu;c6WOPjlgwi|tjiN0!PU~5*Ck2Pxmrr^kk@s8 zuDO%VM&vPRtu?*^IhceBya*cUU&MonA%eg6+p%9UjoF0ERj_%4HO8BW2oDqH+#H#} zJeXm_fo8`wjyOs2;#dgZ2=y#Via=)*NiHLnJXjSc@AoQJh&B;ZUg$QUh>ynb7-S06 zr6=KV6OLlB;plHjw~uowxC?75vDZ>xq9J6+uwcnAN@++Cy1Kdu96Sc zoP;Gd`hzIrmF^^YOZ{DN4rPA}8W|FKP@?Iw1Zv)O_P1Y)TOEi6@f7n2=RFj+gd{A; zingD3gUy#KYbB8jxfSK4bMTIb$$@aQD5EHNI|TB%O?mq~PCl5>o})zuiJcNjl=<;T zYcu~?huq%qv+Cr5Q-C{{T&6@z2(F*=BDduga$$9!M_@tT2RW$alE;)tC`REl=+QAs zSe}NG9fo+57A{P4xFZMtT~2m4!I{M!bhIq)BkyOnF|0tblNBM|SQcT$%yMxih9Krw zM2FLsEGN{wU4d+g8Y3YG`jOnG^b9S?`BtypDQ z_zE>5#aa}A4nH9y3A+TF0|{qq&vi6d)!{AFbUWov1OXE<^$FjLSNWQYP}B^0xZu2% z&JII@k7Q?1@wHv#FNaSeA4s%TP&szK{{kzGHNwi8cXDExBeC*}86@J2q>2QO5Gmmx zMgBrRg;p%MF)of{lj!T~+kN{d4ouzq*yB%r^||NwjO|5qTs1W$m`P5tL0DHK+6n6% zxG4jf&KZW`V0CD8&)9vRx&P@eOrQMvi{JXr%cov?`Q$fFJ@f2W4jq2LKRjKq03=Y(4^?GBZ-qP@;i^=-4}e zHKL3S!*P(Ma=F~s*Iyep)TobKm)6>Fy|=f&D-3TS8r&$N&5vv&1D4F|C`IQQI22Bm z;kqpc+~E;(6ujRu zAaMubPNg+5|2!4P8D=f<1Ss^sP^1(cU1Y5va_;6eAk0z0hX3h=h?Ntf?YD@Nt2*Qk zs2kQm0C$S=W>6kva;=EOI(G`0%E84NODdfYccPPx{|jK$x}l7DL=2VOy{4baBcge5 z>tvHi6D~fCX%94biS}6HbM?6)vH27P)QpWt@hD4XI7R3U`n~{bMDza-x7Ir0R?4A} zn^wULR-zqkMg)rwQgbMqM6d}u8j0Q*B}bW7!m7A^&MqvmCw?7Np1FGf*2Yjfy?_jo0qi_=c{5TeHRCoR`>Tqz|g z5Net5tA(i?#<5<$cCFc5xG*<6n@a1&3rm-q?RKlf(7vdY0mvlNOUmkIn3NV|xv)!g zcgV)$(}V^AIp40%G^yXpIgCUqf=Cop&jiz&}xj00@{kZz#|=)N~3;f%CaQ>GFYP z4p}@xzSu@On?exfShJ#yX)>qQ_OF<0jzXss%ULrJqsaTBY)Qdu=HB5ep-LK52nmbv((g>XX?yXAvkob^AZ z$*#!cq=VJQM=@^&hf9XD)*vD-A!bBe(r_hI?Q;K>Xfv0#ILA8nR?rA~b|$p?pY;1Z zOc6ktjdcJ?*2v{>vnXrQ1M)dgq^(RF3djnx)~eh>lkQX!9xS9glU-WQ0*y%HOOXVK zTanh9F3zh*C+CHVbx5$jRkm|Vh?92oSr`i^v;Uk9Z@LOHLND49CJBcUTH6V31j$D4 zJW%GPTW6RHgD|`x%iTyN*@No~wHQK>q;SRk6kE< zMG&MC?W7rmmK-hDF!FhEMuuS+j_sW|^2D>#C%*Zmli!|x?$q?xPEDV9@!=!StOt!q~0GJq9bau!{0&rr?p1@>!!=)^5>6zxqjQYfmm`_ zNLWK`yF|AzJC)d-c6p*roZv*^Wrsy}H>rm0ae4H>BS-sdJEK?!QsM)X@^B_w7Of7x zm&CDG-pn+Xz;JzJeB!PH_Z+(W;GsLlC#yp>d@nlz?znB&?!BKfDEqG{Zj2oQ#jQjW zo1+;mysrG_N&3KG)gZfjY_hv|FpA^!p(&tBfl0>tW(#$a$F`F&G_n7l<(6~kI*QtU zpPs}=^6-5KU{MFY=`HKn6Iqm%QU^bMc=EvEc3d)dms{;@@fd2EHxFk|H4kswB{#P_ z(`<8bfi`5FAQozs5cYx!)v+baj8Dtu?$14SeCzf-%WVTG4~+!8uC7pZmD6ccPG(12G6h`zb@uYbI0BwNVSoE&)T zkFR!6rmbnxE#y(gQl;+hZdyq3CO#s{PVBpT>cOYu*iDnZfA-&hedCw-Mnq)))cuoF z4<}4EGepjwnfdkAr6$eI;|s735>4BKe87c_(NwoKJnYkV5MGR`OklLit8k;or<6a~#JoIq9&&WZ@StUd6EFNCo zcU}f^epiPeMQU99Y7zMK$x~y;CjUy#gTdh0quc#n_mB3tU&Cx~Hs8Kp>27zDL8OrU z*6;0n?zGq6zgc~9@A>lG_Qr?ETpm|G+r8W9G+wSe zeQ+~p=}COT386R`4kX-r$ufp=QCf+P+m2YTIYq5j6TCFslBSM(yncM?>a`!{ovT}| z&HJ~m_j_OEZTS*HJ8Pr%?K^%FXaNhUTh6(~rJ2)bEe)>I`WQ2?yS*hT%hCk6YkYMT zXQyRRmZi~oC2k|}vaFrIbY*I0PJt7Rb*=Gg_3@pdH7o9fglzvkadUYpfmR6Wi0!yz zIXp2tKRbWn@X_&jY}m1rFn4$R8|&{HFP2-)wRl||N7lhN*@10gLeUCMf?KXA52372 zOx7o+j*KPTJKZmxR`XN48C9(F<Z*#zL%_YAVqpt<(JGl^C5 z33NuvSuI3{57@d; z^zb0UO+?KF&Y|lD&66`ITmWw6EC7n#1Mp>ODM44OZoLONDB9-N9I>20Y!?`UPYm+s z>+J2y0eCKOVSrff93ar`5{8~r#36gnM))@&~y`q5D^G&ewhuhdKug8ZdR>YXHrOyO}a)$`R|J9sJi@(L5v(Xh)6z zH(iE<=ER-2`|Fp3=D7W#lUnExnnU-;%K388e0bmCOda6*f%Z2LPL_lAAF%_&9qVZH dx5J|u%}+189^G!y{(b-e002ovPDHLkV1hWevW@@% literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f5d008ced4c45b3993b8aceeeaef970002c84b25 GIT binary patch literal 36093 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%0AnioWg^Lh{aR6UI$I$0?@*H#e`}i?Du7`12 zDLz1#%}&{*bPd>&^$7&hPx)m^O42cb&|}i3iV5&@hN;8-T>`4x+mD~V|CoP0+@F$U zGhl|b)4HA;T1BcNUVmP@#nV)dYV?%~Zy~V}@oGdoAc=?>bj4@oUBKRw@*#A?1CXQl z2N>VX*$YL)ex%$as1LUGt;1X(`FOuMz$#YM50ac;=Q+*I{DK&8%KDWYvrd(ae%b1G zn=mSw0S`&FdN_Y@d)~-OK$XiK)62v0yk%808|T)afoh)TsfeazS7UF5uOuxz=owPV zaj{F*r6YQaivt6+d^!iK`J`Ooc(w#m#gpm)P(yg0bd8Wl9T-eL>QdhW!En{6O3ss= zF8?cGRU&JW(^-yZ96DqRUoimQAJXw~dsmEr=_Q#23s&Vq39B9-A4S$`U@R{QqF(CR z$4VeT5mFDNrJ1SSREejv)tX^KioZJQ{e6W-PsxNhxBW~r_-^>nfBrdeX5o}*J~8* zs^xl8Vy@ooXmdWM9=;6#UqR3{D1Krxq`*BGZj=LAQGgFU04KG>ZrflWejEzVC0#rG z9(bbKL7u~_vhp*{`Sdzn9Qb4vk@a-Q7Qef6+>{#1_v-sa^Zv7Mr6VAts~gfgabc;L9x`Gc>xppoF20c@bs7g=%M(o z-`{1BI^->@gjDw_*y`gC$subeNxli2zG? z<>Kjo9+Rvm$*)O@R&|mDS_y%2Fz{XGSW0h-8SpN7TH#~o{Ajhl)EGZbq!>$pqxq(rC$-)BMpT;!y7zN-v&ba2MTg28RG+?2?th1ZSobB+ z>h|taa%(>;VN$p*&W;&%LN3>+mz$nst47MygD?bbFihs=H$@oR1<0S=XX(KFeI)D5@xGw7Q$vngI>N zT7luvdy6Yb_@KB_2vSf20ety{YlA~tyocNVZJu6KCh>lWI&Eu^;jm(XWWxLD;ssYU zqSLxcQIE2lgp@t2>`ze$0Y4X}rNezft?aXY_wiHuI5Sq9N{!=DBv+Tx;Capd@ZrO> zC=Zb!F%U{7fReI_`f|;$ zcBU<$m*h4;o)}C|tt{zq-nyEKJS$pHet>I2tqeSm*=aoi)5qIXdsY#9V%!k8(&}#>Q-{8Db>t0ArwKf<9aB6>EeII$?R)v8we&CdqJf_CL?c(nF@My}eJ-zlmf$nE`Y1 zswZy1`=W0pmx@$QvaDIkn+$;R-1CNu5F5i)@AjaFdOdN8M)RfLvJ0>Z@Rc{gdlv-a z+<@`X#GX*MbSzSQU5jO-=T7-;#X!{!dL%Yxt1C!WE5#02(k&*JisPYp_T?R`Ok8{T zTu9b$6L)R00OtHvg@5UxI0qKu+JtT;hPbda$ro9&o}+;KOm@0?fIpM~x3Tzd?`3xV zghM!}0tj>Cnd9RpZL#A7QX;DW5&&jfU3GJT=h{!W2?ty-l!txhg27=-q#T>EZOYX$ zZEf6s>B9&KO%=9g;tnjH0aM19YPiF(;;QwduT+_?hw0PMUtE{z2plZbxbnY4Ge7R~k z6hbl2qnhGvs@zuJO0?D$4_%0B!+3p+mscFhfeGFK8hrr_sLMfd#cR^N?tcC6|MIK< z^AFi)z4%y20j;we$Ftm&e;w3HO;(Yo!jNaOJpFoC{; zUL*}whrbhuf8uNuwW4A}0|?>5PtW7VbUcQhCY-E6wt`nWPRBEmVEL_ZVodY&;ls=J zXQit;>r&&kr=X7iQ$ODxbK}}*@3j<}=eg-5f*dd*UJe@iV??~cuw5|y`TOUcJC*Vo z@lQY}MGT(`c_Toa8<));$S-T5$jJWdQ#z*BEwBr^bnR+LnGx90BG#I|0fmn`|H(AOUw9kUuHX zL7CBaJ7A{&T8o+!+wzhRU^$cufAab^m6~DX+eN7kOB5BkC|$sEcoAS@0JKl;TU_z> zB+?Y@Hh0pQtse?_gdLY)&)4_|#&}*%Kg*#~5k^_X3)qJMV&=Ee@^pw7bOrzI$2S`f zU9kLYHf%oTc>mFGg9E>c5_zK))2kGDW8t>9e8(SbMO+VDao0pHqJ6@jQU-K^iowvu z1w|_ZTO7!+)tu#o8n7ZKekY16SP2tLyIu?k+p%w3B#R-MEaA!Y0&wS=1aaV%3nndIkKFPLX zQ}+r$OE!E7bu#8)c#B`<<8g~;oY7AK^kUS46u@{yu->;iaXRkmwnV*IrLG~OA&(*U|5p(f%`-9X346np5j9W z=mmsqCT+q(qPQ3-5WGvC9phV8elscg$9=+waG(`-bw4!X3yYgVhxoXhd=4RQ zhp4^Q$OMWR{AId3+VO5k$1(^iav8E_N&m^sT5|-Lp9&Y0RmlM6 zptcd9Y?-smLpnsIy3#{1c1BugU`_;%l&{p48>t4wE~cSO?~vd%N-Yq*}fpil;%`M+tMcrRcCkg2oDT+3Qck zB*?#SZ9A7dY24eUBBIDXq05)-RNt2FSPx@eRJhE;;oFqi764d`Z~$Ybr!}=f?0-^T zz=#HPF>Ry%h-|ol()^}S^zIzG>{=dSIlyZ}^SdJ^#11dM4$CJne(H3tFaZj`9d?n+ zm^)GLQcbN}-OmJ)^w$39ecQk2Ij?P*?sQpmy{bQN_BoVFaygO2OrVNwBrc-3U>Y)P;j{FM$^5kio*o3`x>)D1CcH_8)ZhQrfdrC*x~<$1SV-oWS$Y zQE!Ncu6X4s9w27FPRF~}#G4=hsCZs0ehAfLLxANFz=J=4af(QO33#DeX{+8W%`ZXF z1=Ne+rN(`|jqmwlm>ySTlf|#?##YN4pyqQh;^_rIH{6pcmjYYNt*!Y{$Dkg|VKrD$ z;S`7kf-98b-5dAqIJjrd>#Z-EyGG#GN~CO&pP35x+rMTLm+{_Aub3fAKAUfzgj z9?lLtRxZ)l3_G~W*GsKfh zDPFDS15wKpr9@fWqh| zX8>tHmcQC&@~yQcNwvPl+X>^n)kMUTgVE4Ag){3GSzZAN6~Ka-1LI*vuM6Rmqb%<* zZ=a<{7VzA!_f0hvbKyiVAsQ8!4XL+aqsM%-suWM=do`v)-k`b_w>Kqck)CDZ2G$Cui#f#te4pG<&aTZUoJ3O zK$?&RdE+6q`hYGVXknBps6TF)GCN$13%^wcB#Q9>_EHRH64U}J&hVb_=?{)7tHo+{ z9;vYZ1giWLUxhdUqm4MD$pY>)|K8|kF@B1h3VTgqd?T$E@Z1jr{P8SxPV0ssn{h7J z&*fzh8v+ZY;xN*xmF=FR6xo)}N<_S!3FGxG-p_~;#p^sGGDiU^nBuElP&ilp?nkE+ z^@`LLP|E0u!^XfcJ_(z3vTl>IKYkC{FLdcLw-tQsp3Vs^@JvI_sU~^F?)|xHIaJ)`u`{7c z3Pt$fA;~>wrtfISAdU?LJOBnMZjjU=h^6S|9?3ctOMO$%cFc@=QHR(D5#cRTt_^!r zAB3W;#KUc@pHA{qz8=%;M$NIaitG8@=bMD5cyhMZN5D$ zy{l4-D~=GlcIDtU7UU(GQBW7%#luXPUZwN5!caD#`+5wP^C(uj*B+XApY^4VrO(J# zD;h4BdI#WBQK|cqBt<${dUGvDB*4H{@B207o~Z527)WbTDYgtS%1n*b3EKbVYws6@^YLek07+gghy3cBpt z+5f@?BFqBdaIT#W?e|m!j3;c};rl}zykX+w_#}$8g|pi9CChx*u~OiV7Y<#{kX)6n zwbtKwT%w-EB50g|Em9Ny5deCH0T>4iT{Qta*n734^HJ=7n{ex4@MYUjSkpD9 zB29n7sdzcAf1x=Skqx(u-99d{9vpC8k|IiV0Hr&`g4d#zJ-rayy6uH{M6l8_7!JzA zsrNxYOD?Bpy#fNc5J7&*sub4?0TXc`miw8Qcor3@4i_Eyy~+_1$*Z4@+Tz+uP+)+# zR$eNc2V~g*DwHahBLUx&(}%!dOrM7~V=Ne;0@aXPgaa7E>G~wUk)=2Pi_NC(93>i+ z-&u3scr$ig@_TvW7ATxPzF#5U2o%7!O_3G%ei;lr6jvVGuOPa$*Hw zAel1qIaGNdNlIm+4fX>UwHqyY898RzGhgiQ@YECMc|qUB-%@m z*X4?7Onxs811f+)10%*`-hTY5&Bw(F#zi_E<_kpB!)L|sU>a3Eu@NT{KPhv+&2p&l zL_rcS@4Y}EkR&%o0ZB8Zgj@Vr;Y9GTjXEb(eikDJt9I9qRQd z#fBb`z-_Q1a(&QcP}dw&7Jhl|g~+`vxSGX%5ji^|$kz;=K3W@>-mwT86j zO@j-xw@f-BBh>i%5avLRTm0(a)b94WxGc~L#IcGqaVW*WMb)Y2m*8Wp&0<^(=AN%D z^fUBMFXD_unb@mErug)d9VETAif*3e6Xj1g%IP*jKylA8bMqDCm5=picK=H0b`RcW znAxw*FE1S|QI|R(9&=C~A_eTiRJgIq^Nm;7E1*4GMC_pJYF4fNBf`R?i>&<})Sh%# z@3T+xD%=l+D|Q`5#a8gO+NI|LSyIhV+W>5;6;)Gb>w(F>vGKOPXsCX6wy7Q3rZ%ss ziUDUJD9l^q%L{-syP?I011-Sp7RJN@Z~|P^>K^L=Ostx`+nj%-2W5ffkT?yhaRRE% zw)s#&qoygP8~auJq)7|lvdP&XT%-|<*X8IKJ2cPz$k{BoIDS%iOq>I+An=L$CKZ2u zEDBAYa!5K@YGw6aynbKW58G1;rSG?Cv3&lK&GeMDyeY{tuR`t-puFJ=TKp<)fwfCj zm|UWC4YVlW%AqJL%~|(M`P`PcRSDy%Wc2EKQ+4-4(%BC4mBnHgPe~JxQm<<~^>jY< z=DSx5auq5*X&>+Q&K(Omn(}KkmP7184zLQY@^!}|oP3=lG)GGAQ3m@$wuTHDGGwuT zTGtD6{YFH9zUZ3Y1k4h+K~1yRzf`wCBG6j%dlIZX;4e&9ShwGTNp56nd(pA}h=_qT zkMEqt+YCWWg~>2^$(c{hR7Bb6cF%9*MYs}ARNoQ5qTIExAxrz4^tx@wr0pIen>QZY z(DZdpM~AyO#T8B76CQK|%X0$ zL&U)FH(+7)f8QiMbp|-`WR>~1<9b@x^~q@?;n1{!WL~h(VQd<_z3E%l(+D#vaF@uL z4~#Q1?2ML)OS~;H{x6Wnu1T-=yFBNJS_gD_+P)HQiO4D)_0ewS-Z-DGX+h(Sb0>f; zUK=`t!2Z3=^~Ryn$?ao&yif__)%i_VC%I4aDqi>NjWp8*Qgi}w`aZaE?zQ9~?WGk> z%9n5l6pMWTjSohSby*M*=5we+1&{o#XHCPIdCLHXffW&_Tpn*ru=-m=d#l#%6ab zaDC?GSL8c4_NMB1Dt9U3NwLFoYh3=@-rBPsusSI{8h4Zq15(5UgyI=Q=0Uzm97hk!C0^^|Ei8mQ9yrkv0OPrp_4eBW+(62)f#j=f zXTH_~K8olGcmI1%@$T3AR|TpD$tfnMJ#A}+d;PtU{3(!lml(SF%0Gip^I%&%rwR8l zL;L?}Z7{Mc@q>|5w5CIPSPsik8z17iluP;PXkO~=yME}h$WOcf6&PR*(7m{yf`O6H zkCKYFz)Si}5jVGDm9f%g^ji)uYiP%;4!9!rxB7A2vz*=>KfS8S=$d1ik~OgOHAMdKwpgBe@fNyyHxT zE)fB_m8EYB^DAJl++6GQcp}o52AXT+q;X0Awqh4BDDkP_7p+R3&lY3+ZfLx1VBS@< zT5LGWP95uugoNuyIyI~{J>@2m>h;FKUfg`#4KN9HfJ2v*H`}Th^nZs$g?wom(ADT( z?j}TjmYWc*A7DFFJ$#(5ajMfVs-ni^}snSDPSsakhQ zkFuEoAPTxT@N<(RX`Gx51jQwag`L|2EqY#0VJrl8NZ{)aEc*R_T zX<)DHsM+23frvamXhpv(fv)&P#{PLFeu@@FPr@1)Bxm&Wr*At0TsiatJh_RbzX10z z*vVlm$|bcj9d3lL=}Bu_r(Q3KdYkkKuZ7+CnMA~8%MD;wy=ww| z4uOhq23=Q%TOtBjYD1x`$b1E!D*Mc5W9B{^&pPUQyE|MotKJ4gaV%8M#@@d8J1BjY zLpi^Q%P^xtpkg(uYw{jnfI>qFH8@MGh=ga%;vOr+df8qY-bzlkDMSafj^cCZjaVF?EQ{@newxEPhnNMl8bJMNz>j*CAXgU`guO(#q5 z+N_25{e__(nOUiA$Cu>C?;ILeY0@|s8&7b9L;o`VWBVn{gKW+8#C0q+$zEl?kw zerxo)Ee1XPbg=YRBce9pU-_h=N@`A7OMB7ZX~+#;XLS^oEiD`PBd{vG40qDmYX+}) zy=_sz!vMyNTITKZoS_7J0AH{HhK0p!#uFncv#pUJx2JSx{VK2VvxHCw)j{oE0{gjt z-$EaQUJK8rJ)cFyBAvXNVTtl+-g6&! zL=tdnopq(H(n0tH@AC8O+b$>7-@1pVw`_Y20XU893R2C08yp8qLHY8&^Pcn6Um^nh zAOOO}saynKoqN6@7>MTs)cxr_;+o4TbZ>aLw_ZM;$)@&@`^)Rqhv*>?yupTdzaAa5 zg&2Eupas6;$Pc3S+1^eVYejSe-NitU;glI1UG%dYO35YK)?oH(PT1k(5u<0-%0)nK zN^NFZUZ`l{Uwc)DJTVv z$g*`C$-&4R;cjmjjLDyz{<-VqhCAlsn3bHa=Q< z^K?lE^5qxV9nGs!y*R4_p zC|=aLU+FVb{yZZeo#0zYq%ZizM|s&bm%}Z&;9RxcHgb7g?n(1T$-VB>IYqn zc+izCDC}#;^Lr^JnIW+$LjjL>kEe<1$nRCFOL+mE{1%-L;}@nHXfAKRYv(Ek{(FS< zNMA$2)h#2Y^(l&H*x|k*Gk3r7*BN7Xr@2_)^>Ft>4qe?3@KX0*O8s|6As>G$JLA0M zWtGW^!!TZ=mbfT`AvY8maLjvgneYb_Pp%!(XWj=ugM_ZjK(Q)ZELgd-lTu`#IZXin zmbVC~CbT9intnM`7j0fD#oDCko9JjEWJT`Qvbt7LcsI1f9T=`c$s*a_nDWEe>I&o`oJ5n zj0GEGWAi2UEia{_`OFWIBI~+-B`c+XP>GFbbP-lefBy4?F1U#C1#{;%>Scqm3m6!I zwN1D0!D4n}QJdvneB7wmabNW5<#Eu}j3gp`>t6v~h30$+qWt(e(UJTYqs-!yG4i1A{irG4wr z6+adx3rx(|cb11CD`oIlU}!TRCg=PmF&3q;J}1!Js)ud$S`ImW4ND+fXu2$isRFjn zX=8~;I37lAkTPbE21B*L0{qCW0E{=nAqrfP_6>9ow9iqKgFSzEul3gz=&aYu#IYHf z>ilo|=F}Ehuj$tB{*|+EMJf=w_|<1HJHGpV!4hxp<{dD|J!ah7a`3CKMFxhwWm!30 zP0h)@ysSX3b+0@7u{Xa>b4;S!y20t>bOmX@9N7zEZv-lR?7!|Igl6c)5*idAmwr zZl-i-l1?(2z%W19_xArk-`TSdb~w;!j#;dN?^^0JW!Ym;Bk+=O94FjY9EDL7hb-oapO=L9q*(%25=f*!8-U=; zLI}!+GA@ibj*+Uqd;w%f_#)Oo!+Th6T1?01XP;2D6g9-`ekOjfwwor z3)$crIjMZJGNHoqc*=x&oTmc7v}Mb(R#sN_Z`}Ow;YXi-@%6Xg|MlDN|Mu;l{`T#k z|DHVm^Si(N=Qn@(`!|33``6$9?W^zq_VxGw@zr<#@%49q{qozteD>wHkDh*d_wK>Y z&ThS4Hw>Ce9fe35C-1ls$#?|fs8yBL3x8-VPmhUPHs6++z;ihWn!@lV3FMe-6UBw# zS+N=TJEwQ1m&Ex^=QvJsh+p#pKteP!vyg`Jii9APn5J2+)tZe~dv&E&Z!{XsW@}}2 zb!}yJ4er%yEovA9UZzEeB*Xwr663nikl^DhK88TXIE&*b3M|I^4nI){HZRL75VmdlbV5K09UpL{@7_KP$8-*6mGccF0}Fmq$#w zz*tc!E&a3zO$czS(QK`)t#52?Z*A>v@9ef$*G;=d4GYSM3gJRTOo$Nv3R#I|7H5}a zGFg&-l%!%%v)O1jo2%QqH^s+c-P+z;+uUk2S1h|$p(ddQ7nLxQLh_V3<6H<>HIa|(pTXDfe+UqTmn4)zKbBoz z+q%}++`dt@Z9&X1j3(SNaV0pGQfAv%V(5CCGKfyqK$m)NWdt9PgU~4h7$PnrRx+u; zb*y-K@k1(>Zy=Pf-M$GerC<+*3P&iqc-?t-u6vu#158Z5UsA1Svwc4=YE{o!rMeh`(GLt!9wF*yy zhYIzGg_rCpz%6t{vDwVsROW|UeE#BlDeenjUK3d&J_Q;gZ#otZr*oiWme*-72<4pa zkupSL_y<>pVSqMkV{>!&+WzfZcXsw~t*&hWVns1`!?-ui!o=oKVnE!U#?9mh=LSg4Vlf0Me!_4Pfz#wR`@I+4J zu1xZ_Y?q#b@Q&BaLnFvFe}k)8{^q-61|ij2b$xC9gL@Bd-24FGt5$0RZaID&F)p}Z z@Df*X_kUlk_pVR55-`3~%Ou-tJpAO*Lbu4Pi0WmO6XGrA^$$R)f20g06U6kWf2WgJ z%u6w=*<~N|V4S}wK<t3t$6)XN$rs<=I(XDx-xIXzg>2w>j^_no z$QYASkkri!-q6fZ9)KoqR%U+`aZc(p(CTj!uiL(uv&hPUm+z^UwiR2;ia=vj9yYoga`#8-$R2F-tzzv7E2LS z7jH^2H-OxT3Bp97C5M^zztlksB|sOjT(>_sc=+V$>c*~N)h@bj6i1A4AqAPEj4=du zizHi?4Vj>Xa{6aH$qJ~o^_X@fcpdY5Y0l%T%8!s2AL;x{O8=0=NufzNFNv6l#*_up zt=hFackg}v^>?3r{r$C@_YJ%41$^Xsei$(><}62yEY41;KrV*}*#|U@ScT;~c=5cK zdeX$T86Tk|UqGQmaaJw_cmT`qQHDU11bZ!(%w!+a>-n*X=Y&v*j&bwm?N2`c`o;%$ z?fOcmKk^)paRy~5p>vy{lU#|65t;kdAQs*_11}s>aOx^0b9#_~&kcrIP(+W}uxF7r z4394O?JMrI7>wVe_g?1iHBpX~Ok0LaU|3+QmMb&C%?V-3;V-pUHl8=<%YT8b|)e8U7yNffIlWJMsf`g(=jwJd9AXXlGAzq#|^F|D?S zBi|1q&UksHY;ut^10YmddjWaq4O$ zPNbNQXEr^=QmO|Sj0J>Rc?*7CcoS+V7gQi7gs9gW5KRO8y4E)L`$OLgLdL{Gsepjg zx*IF_->u+eLybxyf|!Udr{K|yHp-u>-5ov+SaIX{_VJ*Cmn-5yF7r2jR{+xp@bLEW zUwrv31TYLEF&K`>WvZ7F16HzkSI*`ICxd#NfGV1t`}nT)XC(BG6s0~7BtF&hl*7Q_ z6Kv}5xFd7#Z%?EgjK}uQ?wy1CH$J#ObYsW&#zvG1B0wpLwLwzhFClmbM5!C|J=f5s z+hS7qvve$1$mZuOE=}+yJPY`!qS`Wf&g~+brg7))!R><&rCIaCC=8>y0%g3!SPR=l zr2%%AXK(Q^%p7}BAhtsESCAzue;>aSN@R)fEpmwvb5zcR6q>1op*Xc7-wmaOH7%T6W$TtCJGL2Wqc2&Sp}KN zaDC-*%1m$NzL+kI<+>{b2|UiJ7jNRLBUS?e?Zr)IO{LfW>qhd(;@A-af60}CgCupb z<}MB15&69&YCicpIp2WnW`5Vwi^;T`DK%RwkDh$Gy0H_pitqX|hv8)w97^ndspW_8 z)pbsGkB{z;>@F8;L)}8_?*WeA*Agf05<)P>Sqv1xJLgOaK`5~-tI=w%t#5#j?X~^= zTeoiAy>oDI?>;>5-o1DG_MN@G>$`iqo10s$RLW7!!iaY;aOVl!ju9 zoBU0Dykn?k+t$X$=0}e{Hmgn74}}mV2d=z?Se6R9R2@)=GIN6kCc-R2fi(ourV&s0 z{9Y5M>Ornm;R3#W(5$4Eq=cHLWtaxQRlQ!T*6LNeW>;%fc-HIHTCHkTE!&3Arezz3 z0oAz>fKNf-Nx`EqavjGH;NgXV9|l1LuP|ghHVl*CGJMS#!15(0&h)>svbwi_edpSZ z)6Ot+OfEHmN;CSqDv ztJ!X^thHCyTJUFWy}i2LYPPIu12ApO(?bjJkfcJP!K7wpgCP=mp%9Eq$z#AS-*rX< zc#ehxXV~A|+O_SfkdsG>Pim%r=O*(cA;d7~&hFm+joW@KqA2E^=Sr2{;!vg^^YDO& z3M4w>NomXYM)c24sh<5(uav^Ktg3x{mAn#EBm@_RVN|R3+Qudb{WdpuR@OIGRyOL* zHZ^V2w8spSVTwd=B{&DvknjN>Jb6#iuy_XaOkQWFaIx;IAxW$i%q;Mg1AC z488Elf)Rvv-7WqK&nn^PQq1V!+s9YQCmBfyYE`S%W~;Tby1u!!1sbuKF*6Ww9J znaib2P8^wM-{OA_2ttnEjHf&P4U>jy~9_zZaURL>h+Z6wwX>>J(&fM$8de zO9)9Rv21%|ef`??+xs_e?d{*J)|#eO6U1~w9>+Z9OmG2DnZ+U`?*YR_g_BBldJ*-> zq4I7X1uks`CI6_3v@9F)Pg=ER7{>V}T>xJ%A~@qLPMLlMT!;$yWQEf#L39WcVefzd z00i(L87w7HrgHdHeBL4Quw(+sck&;XzY`b@uCK4}UfaKU>(1W(?X|7lY8{SL?)ib^ zIE-;915Hyx#+CDOLHW)ALh@HBNtE1BO2oF#HorTiv|g`+*wD19K@gFv2`mZ`c(6E* zqDXKiaaI*?;CiPCUD*irhc5kN1S8N(S3YdwV;agR1hBUcG>jMzN_H=VfW8dV0DFmR z`!~jpSqBfcc5Z~M;`+hs{srTSc(5W<(3%TFfGfS7{t`2QuJ+0*_$hNqqL@_{kO&FK zJ`+J4$5M!CIL0DO?GaLF?Sj>1K(6+v$Ddk;X_|x(d`1jB2ZnDDUvf68zI==Z{JbzN zr1s@tHF53Q^^YDu{rK~5_HP~7jaA2IuIC3q#Dqxst$DyL0FinBv#VK^^IXs>(GIXmiJ9LZ$A5Sd4G_xeY7foI6nBNN^Wf+8yG zxJHPQYW^)MS2ALs zfHV)-V_0^*-Y|@6%8D%#bY-5}EQ%NlgyhP#P5MLE61sp)4Bs#_pD;w%*dBEQu=krS zLI`2o_SW|Hor4FDKmHsyFEV$Ip>KvJDInvStN_~T9NbOiEo*fU9H2h&4m;~ zP}t=~B7kegSRA-43RK}oUTdYHHN5`GStf{!J#p7&fl3DN(xg7hd+`Vr7$vGpL7A;m zVpw<=;txK2eE;E7Nvi`V^nzg=bCIs$Tv}|)In?+N%d)Dq8sn04sbCJOMZH4GIEtKM zFYp~ojTrz{c4+3lDug#3lv^c;aE}*3QDk%a8eA$O?PcznCvvORe*EO=XJ3DR|KX>0 zbKMW+(Dlb$m*6=M<0X7echgPN1OO9Kas|Uzzh6S|FbrI0;Cl`sln_<+Z^h+hLZvxU z<`uf~=#|?Zl>mm9UgG)Xe1kI(7ofMG7`otnb#?9F;Qn`i`uo9ykFEOJ@o8r;8pUx; zDV?Q(!;(lshGCeNokYf3JoJ*4IAZRo@4KToiZaiM9JiE`5;ZxZYsA@tKCcjGsdDHl zo`v}yO>Ptfmztno^ziaYo~G&>JAvEvo71NmhS6@dpMLuJ!$+U2ZQtk(T-Og-9Fw%J zl=F@TiP^@y0T?w1g^`Oi!`VWK4Y{m@f!8@Zg!dFv@^MMhcZ84-!Ed3N7`zbjy#rf0 zQqNQUxl{#Pi(u?@Hc(8Rx`6BY?2E4+KKX2Y=f=>BMqU_%kpwEuFZuK}XToG5g~$%+ zg&|R?FwQ;C?Ohy2fjGSiMay#AQG3-%r ziGBF!>C?}?F{`WnkvDREN@=A8gjmDe$KrFRa2MeE4TbZ)1I00m;+Oy}nytK$PQTlK zeG>R1!8xo@L1Z-q@RY~JQ@t~s`awcTifUAU@vHha)k_{yJxZ-!fB5L}{STk4ZR`z4 zzUu{yi8=9!LIjzg`kH5VviEn{pj6QX;1vV`Tx8>40ODLmVchRr^gE}V@dCus7`jS% zj_HrDW&z`Z7QW$&;iKZ>IW}#@o^Lt+Mx$~4`c1Ho-`KwHg?!++sSb=(N-U_7^OCee zCf-h=?-1ZAX< zl_2o@-HWrMUp;4-GG4e-xVY9#{53N|`gi6LPQ&9A-{6d6ZRB;)>Ook4r5%)4B2hF= z^X|ceTX#RSn`R};QN7;vg{dqiIn4$anL&-4Z4_RFJ{fNo7qLaKwglZcSTPk{TmybJN4#@W!GcI zO6!~~D_jWAbx#j}J$d<~=MLajPNs*|d8@@O{Vm`W4*)Oh&7)U7fQyKdmn8}z>}qvuclXhg zPpMILgD{TcMT?%NGv#5GvH0xt$QgAil2J;_x+y^zfQkIc%by0FlQ@c0VUM=4Yk@R@ zF8y0JfRMs8{h2WQyntpXqQp-}3L&<)ckg}p=cg<-I^wz0CdZq=G%Op=gNX(Gdr_d91NhrgU2{uKBQNl1?q7SBTAN0>1j_-6HI zx$sg-a|&ClNqM}>NLB!mqI1?3_7W)R@#d}Dd;2#XHwt4egiP$P7m6f-*IYU6an8?A zk6Wu7_a1F9MmR4|>@F!}81dfgv(v+$PY-{LqcAJS^}#Z zo$V5VW;{)3gc?Sp)!M)L!P@5T!0{Moasd;MRER9t7ab#~bUPQPM=t|!1UG0|j1Q$G zK^PDFJrEe4A3t+PeJQxk%oaXyrB={YR*P!`2w^xkYuR9qo6*fKQN74iN?EPecJ{74 z{^ZNn>Q=Wu;#@3Ns$N-f#}ORPMuX0*SgvA2#Jnl*{~3Zgjw2~VtJSJC+O}QejF%t| z?>HP(w{v#%+sVrxoZ)N91xX0OyXb{j3ZO!JmTLz1Z2;l%1`jp}1fACDgBXlT0FzR- zS61&o_~_cr148ZL$j#5C^cM5(sN3%)dav9v>Q6$5z;)ZJYt80Lg-|KwJSR<-VHo!= zPR?HaeDv(!fjbaFkR$^t5(di7g=Em8v%m@OSp9x-E0@J~Mg7()cj9g|TXzl~7>4bI zk&trX(#%&R7hG~aUkNI&-lH%)J3Bf){1pt!t5s`GotH%tgxB}{-s|I|7eAc6`o$ad zIAfK!cG_4%*Yad8mK@GNh&L}bdw6yb$>ibtGEEbr8iI#%5DV9jIOpS%WU*}JdlzSe z?kVV3RPbJSZ)h2YA?L9{$@<1NHB9(U2r);SC?#QBc&-Cn7q}#!AN}eKI>{8ql!I+8 z%rOvLQE8zIis>w_G=OHNveF7U@PonQdcHhnNYH{1Vr6CJ`iyUA60pM07r^WjK>z6g$J=#qn>)z;zw|5(b{E zNJ40SFW|Za*?tiUJgwzPn&)9LfDovFz&Tx6F?{3rDj1XN?cKdww-5Gj-5ZX)D2lV@ z7A99O=Mq9WfWKGZ4GEz+qlx+8!-Nnqi(O}6nI@RwKm;jS5mHLcxn9Pk@B4$!IT&c1 zzWNn@LqwtfyXOMQLzm97L40{c72TOFd|iA~boOJC3C*c}PD+TV%$O+@cO*D(wc72K zwTKCxp462^BPj%rqkyrfAcwNXX7nVXI1D=%XU~55_p28_I)m2+k(Ol~f@Xk%=DSLV5&pJW zVdSr11<524{>vCSZ@%TCUSzFSv+E5%N?nu}j)V{_4p|&kI4?|(^oD@cg>iNB#n5i@ znHWuGUy<|ElO%Vgf9Ju|`pO0+W`$C1u=a96;+Tb@?+gc>)8`i_&j;PJC=B5nN{tM+ z6mT1}jvuHnOoqbcg=-}%V*sg=;N^>{HNn^4#G}|xnULFe@9pm2B4#}dA|V7>FdyYO zirvu&%mmy~k1Q%x?^J}W)@r6@rJRe9lFS~hvVoy83<#Nvn$S_(uaF^fSWcli95-+ub<7r*?odv?SE&oGE(*_0YH5BdZk zL4>425YKVC7pKQBe}4JPKVJRzFMzI5|8*34>6w^8m#(ygF0DN6^y0lzU`G^Fkn037 zjDqe8Ge04x4EF)MZr{1TwYwh+%DG4uc9Fz}72>&0_x$AI^m!O~gqCFTDG)?q=!}Mt zk2nrI;DHRowoD7|6Ouar!qqf+uB3~^0l5-KEDU^iG_Lw&=w#zfMzDV)X5XfW)*c7|OM$BFdcq-myz1Ty4% z6cCc%c`Z2xEjCte0bA?#JI5Et&ySz|0212%>l4o%3N8|KyM(Rj$?)?^N4z0>3HUAO zOF=KH;~$zxZw4$H7Z*?u^2jBcRh<5mfuof%$3bO!Yhi2-Wuk`RjQNfOIXD=L3-!zh z&{D}FaL!pAMN#MiBi8G#ZEmlwZ?!kJYxQ=m(X7^*iRlWI6xl4Ld~3uaG6BJ)0!{Id z^C${}!1n`x=sCk-@4|C>zB9gNEa03au3Rb0gSRdoflyIsEz6vwxqw_%XqP42RH0ZtC@3 zf5*ZN$QkwA(J=CzDDnf}1tfz~PzQzs@6ZTBPKf8O@IX?2qm0?~^7a`u->-e6 zsql9YqL{vQpQ$(#I)5rr{;oY42& z;lQd^ZM$aIs-{`B?0RiXO}J?oW`z>dv=U3>1i++_f{$OEje%?IRu+VTABKV85sN~& zh{KTcNI(H7nZOkxzDPBp;xE7~gzk`b;Ipyuf#^3ISHx)oQKU7^4^97O5E2B-D_X_83B> z0LJ0TIN;Uz5k`?eZZ;{C;uXA6s74XOj2UKUolN+4b+Lplgc1=UFhocWRR*-h71lrd zCBD<*dC8PG|MaWx9zOZ9y>-*|JkA)DD802|%;lihJ3ajA=(m45X+}yyu)eYCso2Zn z4^m&|7D#4S+2`a=X51rX`YF4Uw3vQKQ4)bzNWmb!5%t^yeww;*oXS_sRLWaqB@u-Y zb(OM)_)haxgp|JT`M$GZQc4J00M?6(N)&|7umcgz1K$z3H7o&iA?6~b_+Pd1Es(2k z(xuDRFa4+>wvdiKwzJfs=6|Csp$my#cw-p#0EQ{Azfcjt%5z4}uy2`^kcF_;;9NSx zUjO145_)mQmSUXTVLRO{o;!SVLXjTC*XD|)m@wm-w%4{M-ZuK&^-~1 z!R;BOk6I!b^reX}m!jU)_lt+F609CAhs*>>!+$Ze%ZrwQ9EqaH84WH@UrE7D%S@|Q zN=HJ`Jju%V$kiJAOH5^=l>T4F6ROPT}vv?d~EWogV%SGDCkf;38G>y&HNh@d>GI#UmDe3K)-ww1pBRQ9?{p_ldlDf2ndU zrNU+v%zY+Vp5r*`zP{+59sBN}TD2)L$|5^VL-7jV@ge;@q(K?>FBs?Pyq7+tliFct z*D|3wnu3NXQoaLCTZ}M(w9*5s&!U2tC8{SGMUib<^+t1ZXP+^~xg@2qGGtu1j@vyy zI{fv&$7xWakW!;)@r%Emha6PJBQJr~gQB!1 zDP7;#BBn(s6+*!6ITsiT2eSSj|9bTNU&H=6;}NOA<9!N#^B7PNfxaMZSh0XEU%ud` zHhhBupC_Ej$%2GJg}Mt8>5A$Mvl@?0f5RT)yUxYg@!_){c;s6Ktq?lTW*U_^X3l7M zb_kh#e+K*bIEp0PC1e^A+u*$*NJLTEutEXd8_NG*6|un6``ky6e93_!MSbQs~a1z>GS3O z1*2tLMnOF2o&n2s^z5JRu*W#dq94s6dM)8f^U>lpb2$7~uj0}lx>7=*SIGekA80^5 z{HMvEqR@vux2x4=vrVWW#5|x^0<yq(mc|6wK><}+tyt%WTZR`Qa>jhm0RXE=-e|T>%Sxn(LNA+mE}}33M=tP0K7R2(y^BNF z=`+Sig{bA0mM5>CijbwHB|a^Ns-4A`r*p??YAdnT)xJ3cu+eaEa`@|SKmOzB#gD_@ zh2XJe+lFDNMg@|xTz(J^yPdPc-;SRB&++qrcTS!;gH9X;Iw*CPfjMTA{Ng=!v^)Vc zH!Q@LC#Gp47=ki+Hy2B6<~(M!U6o3o!LRVahL_%_N5klXBrHf5st) zhwnnqY-IU#O+pCHnQ54u z2d>9`Nx=n!{-P)hK}h)88C@q7_Lj;;%-C_sq*l!w^zpEk%4tleQ(N zg^`hFRi>Y)3=dkW7;e-?;L!`8;9qlzqH=Mf3K+{yKtd@s%zC}vY_2rh8*3X|?bYo@ zd(E(_wrv@PS)o*>$dy*fdduKUrm&S>%ZfBdg0mP9CJH=v zH0XEw-P3x#U2Cp@VSS^uQmr>kvqntQGHuhc4bw>Aj>6j|TBSfG1zhr&amL~}4x<1X zK-wh!k8T;zA&U8qHR#*;=hNRt&3dSL?M#vjIp}tr~_&4fx(76)G26kkWHq`mkIGOG0IH*BoV`ZiZe8!t|-ONSu8>uo+n@;#(_5vh>?LdZk1VzZg`S z5x(WhfXUV!UgnSR!3PIZS15=6 zXk6{Czcr?kj>AoH-rA9l5&)u#ck{U{y5;3&{Q0O_qOz(_ePsGYAO z+cq)Iy2|(QQPgH1k}InOOaNW^xx|uw*MEHD*N-ggnEtc&e>)con8QSRlX+P4T%0_2(DS56Rc@w*U z1@rCChnk-RJ?IS*{ucX36s+vhxW7)=#!*~Z&dalxb`OE|chfiIY}_KY3`{F@mXX z(QX)kL0~Mo|d8}?UU^bdXmBX7=LedNRgH^vNONHa&n`NvQjR8B#r zTM&Lr(a&4TUJzIoeiHIqW7=b%YB;kBjWNz=I3qg!^)~gsjQOtzs-s~5n~@*XrRU zF*wQ0FbrRoTW!rZ9y|t4{u_sh-6N3QE9x`P&wbBRR@VoFyQ$n-w(Pv? z%jDY~)7uvm_qV3=Y*iZbBM;i!n1NO|2-KmbiJQJQQovgUN?p5xVg1?5U&q7)zj>N( zQ$IE*u%o=^6FHm^Cp~W#^K56Y{I8=W1{xBWHmkB;f4n|OBt|m(^~s#a)fE@e=e|W> zg|1_A88+a(?A3p%)z`o<21BwzFR{(zsBkGP2))9tQi{^Pe5#s%4&`Osr=aMC9g3O% zdzH*ssd+*Cg}9mznAgE)d-?an{57=l@H)ICbQS-Ug8tqXiG5syHmJ7{2b~gq!B__N zI$=}I&u9Lmh5k-~L7gk<+ED9~*wFT7==G3@#N#*IX2G*VSVQeule)B7sH<{M!C-r> zZiK17Rw6!ZZ%;*kHLnQ&wANk$>e(n#Fq?(uh;03J-4s~YJHyXj>UNbU4^k%j{1s#I z@@B6W2}lVK+MB!D3pv?D$lQ4ReuId6uKEUVQ_*+7=Yp3Nifx)8m}6WsbUY_;o3d0l zLY3j+=ki04NcJL+Vu?PKaV&+Ha+OGzd}m*b8>C?tsg^*rrf?7X>Yns zkq+;E;ja0 z7Rn*oi?hNO00ZihGiAlH3shg$C6q&hcGt81Rwi1W3R^XufRKy3`OsL-IjicTZQwoHiYO(bGpHAbWWtve76&Ln3gIqew1YO0RZvwwT z25G@t?2LAivq!_i02H}QsM()%~OZqsd^!Qw0wLbj~ zvIA?*mv`GtNUskJ+i3>x#3OV?y}SOm@O@|ZAs5CB%=((UN9!5A+E$!I=_ykPvy0~k z&1d`Syh*tE-&`MTtSc@p@oAXzQGlk*n4|yFTYviuSAtlEV0G_@Na2by=?qJ4*tZpO|s$=eWg;;L;GZLygmRZsQH<1 z7N?B-@RXacCW(iU9mdjAzS%RM8o;SC-?BD~r0J!u$kO{`dClaG=ufa)=|~yJf`am6 z06>a6ruy~oa^)#Q*~`xm{pCdEl^R7(x+AX#fKGb1udk|>3+N6%v*y{0{V+dmOAedX zE+haTuW5}Qro~X-YY+BPfzHrW zcmOZzYrfRBRt%fEog?--(-hM}kxTnTRQ8znE&;GhnQ2vN$;FvxZUKS!DxjQ&ru%ol zaK(P=_0lEEJGdLOp}9r2^!U)b%eQ$LbpsHDDSHV}5I^pdxXq=6Ah zXI0aVvX_O!ATSl&&e8hdFkGVjwHf8IcXASb^`l=(;Wz0n5Pi+i! z`S8B)OC29K50CX1V)h57G#S#MMhVC`TdVtMA2N{DrbUJbqP0XfwmssvJx7#*z#7LX z>zqXURwnZ*jXJR8dxe_!x!RXpQCXYcMM1u(3gr-Is$Fe!6=&k0WXpDY#t8TrN@JDtzqX|)At?HDwvk{LV4Ub z6QDZ#LspJ$;JppE0TZd95BW7%KYPhX62kg(5+-_gQeD9WlIlrz-5Hj2=6PrP#Bk-%XQw!_z&%cC#s+6Ovxir-ln0 zl3=adx<66!)mAIRGux8aW6=8slX=(EeX3UJCH*n&V(?>wv1zBIrN|>u#|Nv+tT4Y9 zvM_K{wAAIoS7JOH8FvzGK^-rpqn%-J&q;)uP_){MUKv-(*5SKNa%up-gjLDVnkoz% zYpu*F7jikMbX5W%uTJAV$hzlO4%G*YXs5*88E$Zp%9d;zph@_-dY^8f%?$i(i&klr zL!G8ay=9twgI(0pG)>g~=L=9faOqM$&J+AX(|Da3mwL_yc+a~o2%rXlK!Ux~0{mGd z^(WNj5BSLUJ4~jI*w{aZkSbKFEcp$QILRBGUx6Frg>#)jVQar_)?pr$?dH`Z!JQ>f z&rBpKYKHmgIX+^izF{xonR3+;{Ue9H^1CMSF=0;eZgSUn%o}Q-YZpg}4~YaHE^?h4 zYLi451U!5oGErQ$5B2aptpZ&^y1@1-VX^|5E6VVnx*rquw>B*tdc(c@3%OE-9X%La z)z-e#`Raln>l zO`=66N_XAj;O4wNJ*#Bapm2#H^)SjIo*mmLa$8D6thTNaVlWnX;jsgsm3i6B=?$<1 zGg0T z5X`sJ^b8ox}=SdDCFzf>#6TDtN5Vc8%?YqUbGg z1n$HO$8jb{>Mf~r9Hcypp!a7cM1AECV%xqK99HSo-EbENVjIL}ee+u}CdF}5c1^S~ z7-5%)W=QLU0&VcNJP3QSkQvfkwFKt4+GPDRHh~Rt1HxD+hb_$`Ts#0^TT8n@2Zp`+ z^Ge$oUI0)92{DGfps|Jf1%TpbJxL4v8C(N)6aEBK3;c%8=ytKe3$b*0Jyv( zsGuA|Z2s{8fPqDX7P&S+)XbP33bIFrv(oIkgrOH5~3Z~ zu#q274naXJ6@R78!6iV>z!tSXsI7&^JV0+$vy3{NU_ey(1_P8sq?q_4<4W#LS2idjM8B%8IPT+fy~DI z>ij8u5NGm02?QvI(3KqTlU75At%;BI0P4#7@@-)CsuCD9yc__^AyDyW^?T8ist(0T zxp9%jI!mu}K~---T;_z*2BqVcRRwpJ3IWO?wwiw2Qi2Ow`e0zQ4i3I=&HPNVn?@ZB zM;Nn3N+815dcN{CoZD} z0N7pt3<9vQ+pw8lAFS^&1hBmT&>*DM00vdL_a-EO?FCr%;{SUVUS~jET1;|l0DKMs zPWkgt-)V$(9D~Gc43czApVa6o04Rq5X!`SkHE5<^atHusU(Rh|9uHStzzhHY04Ca|o_GKN0000000000000000000000000 Z@DCKUQ}(v7y|VxS002ovPDHLkV1m=}4qN~L literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..0f90dfca9d729e3c94d2ee639e06283d2528bb59 GIT binary patch literal 7445 zcmV+w9qQtVP)riv9-p6|A7Au%Jn(Ne9$)l#4(}#1 zw-@m?aOAtrTqWDj=krG+D8@u*{woGV|NFt;Ju!%s&1B)vU4wgG``6m_tWgg>WuD%VDiA0Q?$irUy zYmj+y^V=X{M+7DgupQ*J zF(`K;e+CnGvR(-S9VY6*QqG0Z_c^LTrf0%kc7XitL<}W|1WoHxbEGEjj|jvZ;F3mI z5SS=2Clf#WbFFVSvf67MuOzxi89gWLpWj{x0}QR>L2pL)7R_gI{-65p9Lf~e!TjES ziR2->JF&`(g2^_i=+qBO)msDHL$*g0=KxsZme_n+f?%V2vOfnPD^s7sJ9glGt)&6@ z2k4}}!G*jM>Q1@@C@Vjf3;>jbCSn(O`g0);Ip(1W4FPMqJx5u>1s{53W56VVt z#aLe!Yw#&Clx;6!U`VS+LzpvXzkJL;Ht-Py5YgoeXjh4!9GQHU8$j^w9ROp$qy`z0 z0~H4;M45wu|4tR5A;VdS7*mk1^tyj@$27k9CrSs*ymuV0EsZB-+u*Fk<()h`*9xZw?~)*^T- z`aQURG?w?u#H1pErf;T-wM8T;BA4iogFK^<{!n*qzi}O9e0TPE*SogY_O9)95{D)d zfksJ40zrYMln@#e6savJB!ntZLE8v{1ibN|@W2xif``HbmrAV=^bLXfP>BZ~P*oy$ zfW+6tm$kj;%wc@a_N?vi?)>)qzWHuL#rn|n(pp<8m7L8{FA~wZ zM;ejYyL3_v7&jM&BsSG13(i_VNeaFGg43ftc1Oo#DBO@b+kIbAtbN2F;=WpJ= zbLW-UniG=}e^a^|sH_i#z-t=+q+n9a>3$`+e64^Hu42$F&0znPN@a9x?AWp6=gxoX z>b2|NxbyA1-}~YBfAr(;-2K5hZF4N1PH%Pj}fRU2a}mBCuAHr{A797&mKHJg*8W8*`^ zBMCl|L4MfEx`rRNwHe@&e)zv1>XN4V{*JQ�|g)C_G@n0_$W@$nLvKJ|NO_&>IaS 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 T(K)}=00000NkvXXu0mjf0B0VX literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f4640d3d16640ba276e50bf170f876a443947824 GIT binary patch literal 309 zcmV-50m}Y~P)Ti9#nDod?jM5e11&NW?oRwoqQT^t=9 zf&vZK4k-a-ad?M-f}(HKTj$V2QECe?`?SEyL!c)mV0Z}Br3Bip<4;he1d`ah2uOYj zqk@xd{2D3Q=YJJD%lIkM@~@y6%IIK#Ch`dL=bjKk2>BTWPybJ;7R&CeUs#1X$7k^E*s8WTK7*;Iv%HwuUua6be$>26 zs$luC2L&p7iy9NpGFTs2QQ9Dz5Yu^=Q7!IEkpn{*`L~|2E9`&Q*A)?^$=%A5Jh1;wy}!M=NEgMZM@4mo7lE(+r}=oZQHhU^Ns5_-PIZ|{^9hT z^Qvz5RHHLSVg{yR7&@U!T7{OyI6Q&axQ&4*ks84TF#$gzE`3H{%-wGq9;42j{mh7AFs~#p(9G-J*O`CFJX{glde`jwa%zEIwt?JU z{*~vY?!u$76VLwg=Oda=y-$40&#*EF#JBWh%w1ykJ=>}U=GhhSEj^`%=rIeS&3Bxv zU|+SsI=c(}tIj(J-z%X_x14U_IJLlf+Y1;ZmfkWvxbcRY85*w^Sm0>rwm*u~ZFrFB0Mml}p{@d*j6yOLVrF@{>52TH0Z-{6MpEbZOY48MSUcBi5S_zHEuQ^PA% zb$Tvrjc=(K{svLEC*#I=lnUXOQQhmiK?y9xUk|}QF%AV%;|-}6<~cmBzr-YzON)2J q+-Qn!n28lwhH2=6C~{`0x9$J7)|a{YS=cfF0000k4U_MR?|As)w*6C_v<%l~=)M z+429`jTa`?+`j(Lp1URcL<#GihN~&n%r9C^9bQ-zG+Ib;FWzVXBoZk4UZk{fVAs)w*6C_v<%V%uP+#I+d z_Wp$(CwF+>pOACAcXm&jb5A!=Rgw~JYD@<);T3K0RVnMDt-U} literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4dd6583c7081dc40416fd254e5691149e5f5cd8c GIT binary patch literal 610 zcmV-o0-gPdP))N($+qQX(ZQHhnwY5$%)7^i^`SQH+OjWzPLb;n-{ynfW$%jx#*3?36_()16 zM9!EgHj0QgzKUqHBq`axit|^&6UFI zQ6NDIml{%I;D^BGs47?B0zuA$w6~C&Cutu<_K6Kr;T=d%P_iT8H1f~!r0b$PWhyl= zSKtz%1PXvQ=MS$&NhMgoBY%Mrgc2+OYLUCZ`cfz`J$Hf21QGQE`fzXmi2U;=<2(2_ zH3c^1F7TM3q+Re>*k!vxYOJ!I(3bxrSm1E(0#68X8U^%+pEd+a%$w&~DW=IEbM%0F9Qd6lb;w^$GKTR+^G=6Fe z?2@}5XiO0V5#KT`@jU&rns67Yp>$U?h%yOE&LrQ#fy9fV;c?Vn^uBN^Wf9~oj@g9y z(d$IQ6R5x1JL91gMv%fY9y$b%R|<{9 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d2616a4cf4d23052bb11fc2065b3b643e99d0b28 GIT binary patch literal 187 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE0wix1Z>k4UO`a}}As)xyPB!FfFc4s2?T(qL z#eZ76rRJ>OPZy?rnK>>O^`)UWk3I9Z;FRWDp2$igKp-MV07wNj^# zn&8K;49y9iDmqOUbroHd7VSU8+i=*FK_KkfsZ6dW!|{rt#kYZz5?!rPHnL#j{;r8;OXk;vd$@?2>^y5MsWZD literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..9fcbe509f05efa103c087ed5738a52f3a10fed08 GIT binary patch literal 189 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE0wix1Z>k4UEuJopAs)w*6C_v`Y?jw ly4EI<_`-ePw?G4+V~SRo?dA75E&_B9gQu&X%Q~loCIJ2uK6U^A literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..078d385d2916cf58053c42f402b367adf0ff9ce6 GIT binary patch literal 140 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE0wix1Z>k4U!JaOTAs)w*6C_v{Hz@o#|8M?W zqqoHk6Ka(I75}-<(;}mMo+05vp~J!h|8*TS7@ltymi{z*VZrJDlO1-jo>NzI(=cmD n((hTutd*|TwfVD#gdqcie@U;eLe|D8pz#czu6{1-oD!Mk4U!JaOTAs)w*6C_v{Hz*jSnWioE z*c~`CP?FUxW3%LT>FwE@du^C|BDdaLFo8XXfxpK;q@c-3ZV4~fa%ROj?jbz^ISfbR nBz)OJ&iRO>KMyoWVqmxzqW^WzyDOnU;~6|%{an^LB{Ts5c77{$ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..db4c5eecbccd3f3a53936be1f5dc39e40c90df73 GIT binary patch literal 284 zcmV+%0ptFOP)kdg0002vNklC-(k$ z7W(5Pvc?sPNDoMbapb^?kcv_0sUA=Z;~tNK0*^}QwI0w5APY}k-<7J8-!>}Fl#P7hepS>qi`(_8d88RI8>+cYFbK=S!< ie)&Ur%a``AFV_V+m_ABe9n$&$0000kdg0002eNkl&^z&}YMjJ_SEuhJDS<4>-rb*$=SqNr8j_d2*isa^zj=S(Z#BiE9j8B#8-2 zvM$7eEuq9T2F4Oab}Zah=wWF~lo+vR&yYx>jiu)XlsRE&ND?8TB+Ji z)QE_vGvLmVR+68nu)^}dV0d6zQ%!mW0UhStxG|?o;CD&^2M(U)-vHA5OP+-zkdg0003rNklF~l}6wg_0DCkdg0003;Nkl%uw%OT(xwNJya}Y$x$-fW}5yT(_0uc|{U+u-3^drfm(p&fT z9?ZNqv$IQdu9KHV)={{?!jvP#l1hr0quGS|=I9=;IDiPkg9cyk>I5`mj0000ujP)kdg0002aNklkdg0002DNkl42!2N#{MFs$ku>8N^|7Ik3 z48?#o|2YU}BxYbxBH$_{?T7!nk(w)6kX&%;zZdD*S`*23FLKHSABwaC4ag(g@lXST qDX<-E0PFu23bccbU>dCk4Udp%toLp+WrCrGd!7L=BPa&yo`i&YF)sMI;kMI^3-jnYY~RnXO^Opl6xDrcofVEii?_Kx|8ocJFP$3mIBo zL3vlwl;=#IvE`<~g%yl9>zIRsHme#>;!cx@oG@o??^A(olNZWLC`}W|WM6h#VUsL> tX`?Lbwr0leL?hK(2alf4;B}N?U{G~<_|Jb^vkuTB44$rjF6*2UngA0jR)zoo literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5a560b3eebd491ad06bf57a96256548988c5a423 GIT binary patch literal 197 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE0wix1Z>k4UJ)SO(As)w*6C_v<3;yB%^Z%2* zTE)lv@;cnCJGK`8tN$!-UEw%~@#OzPrbj$G940dUfBWzM@-q#lB9b5C-~G=&#gp;> z|9m6nxS*^9{~666ur6i$@!0f{>dTw2OeTH1|KHi;>bZaa)hB)9xb)V1v4{1AhMcY~ x%)B$aCzdT> literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_action_rating_good.png b/app/src/main/res/drawable-mdpi/ic_action_rating_good.png new file mode 100644 index 0000000000000000000000000000000000000000..5d3390ce1d2de45c3afba3e8a04fd8113bac519d GIT binary patch literal 260 zcmV+f0sH=mP)kdg0002XNkl#8a5g)?VyAf~A#pn!qktN#bXbRGVG0 znFR~UUkyC|pMiNy#DTxSxmzdc`p6Qqb~-QZbf)S0lN?v6`lGIuAKiWR;YQC~;$vSu zldts%_sSH;)!K##8R$erc(qNjBd|W#+-^`iYZy3c8@R^2P2dd=$pL5h)=v-2@Lc!$ zI|ELA;UYb7hxhvZD+LTn^PW#kngt5gEF3orj4Knlo5L6XFZl(f17?22y``rB0000< KMNUMnLSTZH9dL30 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..074e4b3b5a079f58eb47f37b5d70142e00ed5f39 GIT binary patch literal 232 zcmVP)kdg00025Nklx`)D?S2b!PvOqrvKcO830r>zy_@S z&q0|1Ig|zBq5sO18Bm5j?uj=*hcW}!|7WDkfCya47uf(#pgq{NZ~ZSqgaJUF=l>m8 iwD0`yHCjUqO8@}kFDD~-sOO~s0000k4UJ3L(+Lp+WrCrGd!VRY*%k&cv5 zGu{xe;BLmIi#pEMpSGSin8n)SC~||ZO>9c${8SzHeUBXrw3v-MZ}IvHY-3K`YbMQP zWFUBD-PyD|ElXryFg-3)iO5{h>N>g2;L5p^Qilr6_%d!RJK#8xQNffsyP;6nKtr?0 zQNgt_!{%f_ml89N^2TGfAK6}L2BkC<8uvYF%@DVE=&|nv+lmd+=8re*KRjbT2eX94 pmovS)Hb`9X4clqJ#b(dIAer^>-|^3$mwk4UJ)SO(As)w*6C_x_F#K2kr~mW3 zQ|Gt)|NsAAzuf2d|Nq4&FUzrA{rA6KEcMp^|K>}5exLvUW=R&m+oga1|7%`)TmMtu zyyD}1`Q2(74rz~#nu3`B@$X>N*EC%)*ND09|MbOY8ccUQnis+RM#K04)4MC~a~Mw^ xk^I2a{_XmI%TD71@~Z{)|FidreLO70z+mpDS?pdkdjZg044$rjF6*2Ung9pGR-^y` literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b707921e13337347fc614ba51e5a5401ffbff059 GIT binary patch literal 158 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE0wix1Z>k4Ush%#5As)w*6C_xb8UNUKvzY&X zF0qL%K!$BG+l0ge^$Y+1{9pdh{%^f@Bpbul^NyYMlXy?KU-jF?Xsv1bz+`LehN+hf zPAFYvNSqQe<7<4wmlB0P_V54mB}BaWG5>db!IlWwZww5^i_ebP0 Hl+XkK4MaO! literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7a68e9fe61ae7e6f363e8b9f3218cb136f57105f GIT binary patch literal 466 zcmV;@0WJQCP)kdg0004*NklY?;930B?AKbLs2l|ZAOc2F(?EnA(07*qo IM6N<$g1bY_V*mgE literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4e5dc1222dc8fdbd7752b0028466dc33debc297e GIT binary patch literal 501 zcmVkdg0005JNkl$Ae^MROpv6}^+^zlmZNc8#@(bz*yjlHk~@(G@^`YSNFjYJ zv5)|QYk<6C3DAeM+(f)*Jd|~SFL+)^{6$EM&-l5n!>W+kdg0002!NklMINcP$rf@Vn|#QrRbFyp=+KD{3d^<9 nA5}Xygu|WeK;Zv>vtLy&^x|Oi-7Vk)00000NkvXXu0mjf?3jRq literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e1c1200922943031651c03be75e2123ceb96d8dd GIT binary patch literal 307 zcmV-30nGl1P)kdg0002`Nkl_nGc--!EU@ z{V;8>|cia zz|%W6c?P}C{fj{|PR6|q$eboL#RFX7iU*p~gv_Qc3WIk7d~WIJ3w8ot?|swYjFiHkdg0003KNkl-Onc`@QhDD zZaC)1<^ZV)Jk|;Vn)V0AH+Ox!Koy1&KnssMhp*~J)|^)ZT!k59USi0ToV2~u)pyZ^GbN* zM~O2&4bN#+3wnFLzVVktz>DFMUbSGrvbJC)(Pm+I$)H-$Cu31(36Cek8U1}LwClYd ed50S{`qei{SaP#W#JJr60000kdg0003yNkl+C^Gov36yJ-QD+E#wt}RSDk4`tkH_HGNsO?S z98*Gh^*XRCGk`r~{RVC%?|PjR1K!2}H)WOnQZJH&EZUsk4US)MMAAs)w*6C_v<3;!wqvH$!3 zNS%WB_3lYqjQ_{Cgj#dx{XFk;gQ4&L>_VvxKkbjNU|h`BBjvD*(fa@NKiB`Q|Gb~) zgwoB1-v68bcON`@k4US)MMAAs)w*6C_v<3rk2ye3d+V zi0Sti-p-5XQ$kuptvSMs4Fs>OKbv-^?=-{XvfOQf%x=6ArVS;`yLu1tihL2_Rc_Ro z#G7HtXCTnf`I1YDwaKN^k4Uv7RoDAs)w*6C_vYpB5nG*m6uABz%{=fwklBq_;a6 y#DolnE`i(woNXCwW;}sr3tpVOc&Tg#6T>3aCtg+?PwD_IV(@hJb6Mw<&;$VVeKd;z literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2ebb97d65ad70295abd40c29ac98cc653b10c956 GIT binary patch literal 194 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE0wix1Z>k4Uot`d^As)w*6C_x#u>FtxS1iz= z^J9zjzxaRk#*91u>{rx#d*=W2fA-!iF@1+c9iHc^ooYDy-!dbY;hcfeE!G`L4H?V@ zM-Oy-Wpev6=?!b+|7Z_WhN};w|IJ@g?xn&o{ZIO`BMp21%ND8%cQC5|SDzRy)yTt; t_5Zv}C*zZBcBupZ?q_calbFT8;J~E$`qnw``9Mc8c)I$ztaD0e0su6QO<4c{ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3ede325059dfdb8d26e4d3a778060a4e5a334061 GIT binary patch literal 190 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE0wix1Z>k4Ut)4E9As)w*6C_we*w5U``Ju>c zEO*yU>)6&~Z&?!7Hfrhp^=UJ8+sxtoI7(*Sg*4+l^-~RH3*0ku7|t0eU1NwbblAeU zBProY6{BsH=XWOllWYkVbyj&F0e2tmEHkdg0002ANklnP4kZRa;KKhA|3xS<00g)FPx>!S zi2)$E_kY!YWnv6q|Gx^W0T8(Cf9!u=LPm)EH~jDMKOSh~#Q!V)AI30b{r~j;a>ROw z>A%E({r_(N6aQEI?+3c=Ak>ik|MUN=k!nC6k^u+)7vMC2C@;vk4U>pWc?Lp+WrCrGd!=FZqG`Aq6o z=I4NfhQi$!brkrnh%Pz6VDQGQ=R)w3BMb&6>r9G_o-GFnnS3!3TX=*);Ei#Y;S{Aq z49Q`O+19W%EKFRxZ?=fIqVf;kkgsQZY#Pj-NnMxpbu&M*k4Ud7dtgAs)w*6C_v(_&r+~k8;DU|C=Yh7knh!e^q&jgk4U1)eUBAs)w*6C_vn`3X}dD zd^>&CoK3saXDZ3KuyD7Tv>}^`lk4Ug`O^sAs)w*6C_v<^Z$ANPoJ}0 z=>JU5t9c*mFaK9AsQ6JY=IQn){g?g!|B)gUujk4Ug`O^sAs)w*6C_v<^J^4ja2#H+ zO@LXr^A_(`p4&ZY$_pCpGu;fNrc0l7YBcU)_PDVAY}%c^GzPO9%kw>EGH&i!Ransz z!Q69u>rFRa3Dbr$HUk66aG_KO)pIso^X6`JJZ$EytHdqxCZI8S%MIB!28L4mPbZB9 SIK6kdg00027Nkle#(M!^6902yUb97R?y;s5{u07*qoM6N<$g6ddd{r~^~ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2d57bb063732371e2ab9dd2ce12513c57c184ef6 GIT binary patch literal 237 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE0wix1Z>k4UTRdGHLp+WrCrGd^P7q*9u2Ysj zEcH0kEx6}Zug#Yoo>$gsb6hdwX6)5ARrS2E?rhqO_BFCRt0iW1njJX7ocCE}@kxVi z`)0QpSX~iIN;s6}AQ@7r}lOoW%ot`_HDU9_zwP~C+kUExu`TdvvTvJQ!voShoX2Rv_z kR|lzGY6vvoV&h<7DBIq)Ber654AA`yp00i_>zopr0Q5#yYXATM literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d37d26fe6190e693be54a4b16c01f6abb4765017 GIT binary patch literal 266 zcmV+l0rmcgP)kdg0002dNkl~n-z z<6l8Qq{+~wyr4vBQj*z$hNmH=brnA)9W5>GFj7SGfQ}sr35f+DLcJ%yfOaTf1h{O$ z2z-ub0X;Q0f8at+%$}_u(34Nzi!)FYj}O}mcyJhIQr|N;ld&P?S}fKuKT& zjs~Kk(0!p1@a;koii%j#kGL~^In_9A6pLx$9sI{-ou>Nl|9bk;D(3r87XaRcZy Q-2eap07*qoM6N<$f{F`iUH||9 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ea5c055368d7b8583a9bfccd2f05d2f9f1e2fa9c GIT binary patch literal 274 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE0wix1Z>k4To-U3d9>?EKvE^?v5NMM&P*KZS zr>Bx+;>)%7_TOl}mZPn%5)CdNuY0*J4!qSYWPYza=iSNLX&WQ!Cm-HhXI$=3qM~@v z=dlT&Cxb}AW$9p}?)eL(R<&C@ToW_m3OJm$JxGn=PO#~!Dy94u+ zn;jlAoEiDpTm(JOa_(iER>f3vH{NA&tk1_qe>KbLh*2~7Y-er!(w literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..bc30803e8a33c82c715b8b75cafe9f8a3e199fbc GIT binary patch literal 290 zcmV+-0p0$IP)kdg0002#Nkl5Nntmw)Arjv_6G#(0 zw>yXPFD}E}O694fRCRPze^;GY+dDhkTO0ofV`wWEzf5jx_-K9XB088EsH+>8IfyRC zkJLv_qP6ag+w~$k8I{#*cA}NWx1?bu+Nq@hQ_)fLZq{@ZP1AslXkM7kMVmAr6N^`6 zBFfT$+*Q9Y?Yqj;fGFq{raeLYbHL|3;y(fR0B@y$*EC>R3RtEAZ3~$o2seHZ1et|) o`H#@j)4P36ACA`ZPW)57174*kdg0003DNklX>7UwE~_iHTL*Cjbf?26=86z(W5V@Udm| z3h=52*CGJl+)Z2;0c7n2^jq;tQdn5nCxC^8l{8-UTZJ#es2K)f@LZ>NWz?*`iGMl| Xi0gog6t59_00000NkvXXu0mjfQD%to literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..33bae5d55faa6147ae4108ab81922253a5d9847f GIT binary patch literal 309 zcmV-50m}Y~P)kdg0002|Nkl=E1;=+~XGMUe8(n5v$+{j7UsO_z#} z)uG3VJAxw%ekto!?sG=u=txgyx7d&p6DbsvvcZxaKV(M(PoV}oLL50jvWSI?r{1Fm zxEzSy0eD1+A^=f91po<&IUr_E6Q2opGr%y*CnkV1ZkGF!i{t=mFRh%ITw$8!bF(+N zFr|jeh*%e36Fj$^lwBD}CnOyL!W;#3l+za{NA~xR`glA5$Ic`rqPY?T00000NkvXX Hu0mjfQ+j}k literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c1939a236b51a06af1aee77c9125eee9c5b7c939 GIT binary patch literal 339 zcmV-Z0j&OsP)kdg0003RNklT6jc?UoeBZp}KsJ#ON(*VF*-ueRyEUwPg5K}I_0Cc;1&{Gcp zZ)Eg0R>S>;)Btq<|0{a{U}&~0Vc~a(In8umM*0p#OMvKc`a9**^(#5Gc1S(ul(QP8 ldhG1JGIXm`e?G+i_y%Om)XWvpa?Jn$002ovPDHLkV1gcplyCq5 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b9bcca6e5b43047d2d00e7f3d19cd5e9d753b8eb GIT binary patch literal 292 zcmV+<0o(qGP)kdg0002%Nkl0i_#1>h$Ey0000kdg00032NklsLsAf;MJpG9V&d<)`I?S$yS%5@ix0ZznRY& zE$bgY9}Um(9INpX8@ilOiX1tmoP@3sEIPcvn#=QprRW&oO0z^ku@s{gY$^Qluxp^t z3#FL_gBN`ZU93^)ahg(8ub*&9vF4zO*};#b=EGI}OQFSgM?;@h;W@~;9JOxqXI$C4 zdGHE~usL1Y-nRi;<#RPykAtEM{BHiFQ01pJb6Htk4UnVv3=As)w*6C_v<3;z-Stwoz_d+t{2Mwk4UMV>B>As)w*6C_v<3rlchaAb45 z)|4uhKHGFC&(?Lqj7A2*TU&4TNcXBq`k4UlRRAi=io#Hi@zB5202Q84Wpn?Q^6 z*#>6^k8O;<|GPI|b}*UXI*l>0;9|iio(E0cMHAB*_+zHD+c4WaTom_2vfxMkuli60 zANEZhW(<2xEI3A8_E-fA(In-G>Dj81{a?-r-rk?={eM44$rjF6*2UngC6` BOi%y- literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3614cd0f68dc73c4e3479d30174a3821ff8f6954 GIT binary patch literal 219 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE0wix1Z>k4Ui#%N%Lp+WrCrGd!=9jo7`Ay>N zA;#nmLxv@_T^Cb$ILr=Rn7M#4;jNa`p%(&44IfRK+!&_%UK2Q`s!5 zsOs61qlZ$I<838vC6(hN@>C8ne7SU>V%Nu#5W&N8pSVRflsWIdm{cyo#K35H(lm9! SiVr~7GI+ZBxvX literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7919a795d5459cbbf0c22700fdd8f7b6fc60b6cd GIT binary patch literal 323 zcmV-J0lfZ+P)kdg0003BNkll+$s=CXt3s*ntTyXE={gc_#&d9hQ=BjE>4OKBKZgJc}<_DAnx>B zTq637UpU|Aa5(4L+WODmjEatXV`~$SE^L=8-_e%~xlg&Ua$~p5>wRIWudU(KwYhMY zF6hWj9sD3|6X7gXa4+-=tS8*23dVBn0&7}&RY?=%J~aw#-^frcO)!x=EwCdCZ>b$T z3fBeJ6$Ys@xU@3=IR#f{){avJ+irx3o~AvAx@N+YYWgPZx)Z|E$U<0q_TfRj%sQXD z^=jzNz_F?aA=4^*Qnl~NtILF}R2KzZOD4Q*qNsZjGAElTGVhw3d^INS002ovPDHLkV1iZKk1YTI literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..9eda3c2534c4613bdd5466e7af49a70553166f43 GIT binary patch literal 358 zcmV-s0h#`ZP)kdg0003kNklCWVbwfyF{MApSA;X;XznNimB} z2%hr5IdkURJL7p*{8OZ8nXy@8C1y&~OC?gvjp<`iEITYJM;TPI2~IbCR5qqZrJnc8 zVV6D1&{kdg0002tNklCB<$$b3&e(U zv*f#j#C7xm`5V#ooI$m~+&O$kqd7b1tXg17Q98IniHM+BmC%A|-6bZRY;t<807*qoM6N<$f;TOA)&Kwi literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a99f86df328fb28c1aec0b9ae24246c54a15ed13 GIT binary patch literal 293 zcmV+=0owkFP)kdg0002&Nklr-`6nZl-gN2!my^%yGn&{y1grSyE3(R(?cCZmJR~R=N zX%dJBm~F=KD1n;*cNGI|4WQ5q6bB+^Gykdg0002?Nkl2hW|qk9+=exL4#{$1EyT^l*)kgoKcqPB5bQIU}7QGlEQmUy>Wa$`-yNU}o@w z7dyPgn1Uq%JyKGhssA6orq7C;H*Q5l@bLCS4Tdb3^Ge8uEg#(2U3vJc+b`~^+D5i8 zcurF@u!9A=Ej-w_P=@%#hAp3#A^sWTfHgVq+=+# zt9uQ~>7=?mrbaN;2?j>c*9lyFA|ia;^Y3ta^$h^__5nTa355Uv002ovPDHLkV1gWO BhV=jd literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e9fd691b9a07fd646f288c24faa125b20f43d491 GIT binary patch literal 310 zcmV-60m=S}P)kdg0002}NklRnF|IYPHS$qmC(8iCWTROSXqZfv0mc(x1{hBuL%TMc@pQNW6G_yp zz+vcO)CtoL^g<*_+VvETD?!dSbTJGuY%^>#2$kX@TC;(GL8$RWpySg_Wg#&^L=0H- z8pav*8}%8+8u=JaHkxTvXedQmbj2ZjYus#N?Z84}kO6InX`gP+M@EEEYyibxKng@( zP#~I+8i=UTXcT5V2^fv(#&Tc*Mj#{=z!Q?Qp{G$b5PBH1lAM$cT#=Kqu{U|80X%jL z+sFzJqgJ>ZTFEmY3}%2qD0xxBY-n#>WL#uu&%iudISf7k0N(Ro=6($Y0ssI207*qo IM6N<$f{2ZB^8f$< literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..de814cd1806101bbca64293be7caff3ce297fa52 GIT binary patch literal 263 zcmV+i0r>ujP)kdg0002aNkl_3g$ z%9BvIldo@=k1JZ_!Y5G}42C!PPIg)>-3p?j$6_%QdkiEU7EL+xmgqa zmm6b^KDS623X5hqzse6()GBX^sb+{3Q%KHtbfla&K9uyphjQ63+6TlA_+iOqjb{J= N002ovPDHLkV1iSzYq$Ua literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e833c0cf3c0ab29deea313e428dfbffef30d5b45 GIT binary patch literal 294 zcmV+>0oneEP)kdg0002(NklmuxkiIptcaEEE+ISFw8aQLlraUGpaY541&hJKqK(mZkTH{%cv0D2m=M9 zR-pJSqsbb=1Y7{Lt_o<;IiJ27Ht&!G5RNBHJF4T!upQxe167g@L3X^6 zicvSr9lC-<1tO~BwRwzkAW6$e#kh&Agrv=5Tn7>{?jRv4qlUb;1jqKyac%5ut)R9U)^6(kb6dm#K|V9 zpt$5cLPaa}zBgEo%b;Q(;t4vza@g>UkYUMg8ZfTky3B3O2*|Dx12^9HGJhTVwD|v7 z#D~IYEEjC(c7X*VvEUp@EXStO!3)=XH*nC(_S!#*V4x|{c!pkaKEKb>I ziPfHz4l0%g69PuaO$ch!5(5d9$C5VQsZU=Q@Ptr8i{bZ^2>}CGz|@#ODMxHMRxKV@ zir<6(1vWs(!+JKvA(p-a8yvs|y=X9m4vH_U9ea__OF}qpW%o7gI}rny1Vxkb9!l^z zjRKY1y&|F&*oJo!JLiJ*Dsritu-O5JCwFBJ&KG zkcT9M-s3+ZHnz$6v>wM(|}sP`jo58*=D=LBG@9wJ^fpeiz12t>YnPV?SF* z=;li0T7R2s=fV)BT?+W}99Z?Radw`I*s{XGYr}8*YALwC#!E)@P>Mo||rYYO?jI z$@a&_TY!Qfw(*vy##^76Y<*&~?Wys$CqTCGwx>p0fe6F^%K{Z3qi0|)goJ80-U`$R zQv)&(Cpz^g2;_R(k|4ie21X`k7B+THaRo&c4NX0L3p;xkS1)g0|J?k-vfAd3&b~>r z<}BTL{KUzNm#$vBedq4;m#;p5`TFhquRnkPX$I;qU|?W0@pN$vskrs_8gmt^p+xJ$ z?AzON=l{)L-Tz8$?wl)s@3Yo4Ur?>HS9Do$K{8K&`r8e`95n$(5i=hiTqE?SBQ2>V z-Hr2iT4uAjnd<9jg6m~;SD)m)x4|=__o76;6l?4rE)lI7V}s^HIu7pK0jDls*qc3zCU*^GQS+> zn0eK&x_9!$Ur*T9-d-YS>UFHz-J;;lQ5kjTwYD<1wmO}gwNsTxXA|F~dB;TE*72UN z_Kp9|=DdB|`K94(3)kekZBxiOd@DnE$}%%6*3?D4^BgQQ7b|#%EOO%bXnlwAnqghL Tmz#+YC>eRW`njxgN@xNAq1Z6n literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_toggle_played.png b/app/src/main/res/drawable-mdpi/ic_toggle_played.png new file mode 100644 index 0000000000000000000000000000000000000000..4fa676a9b367ec6951a9b035cc0fee483fc308aa GIT binary patch literal 299 zcmV+`0o4A9P)kdg0002;NklEF_99fll65JDd03 z&JSR~fG=>zG{=n>cmZx`?vC4$XB2uV-K*oJ6uL7lGEZ>J#fx|&Ns?~Q8yC3c$q%}3 z^Qm;jJvwZ$g%0k^7@vImoBN0_iXMSb4+0|V5@ai!&v00Hd9N)Dh^MLUR#g%_8cx xk^?|uqkQMARyv(g?qQjFS?2+NhX<6=qS-*MUu*yX002ovPDHLkV1h+Zd+Y!J literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_toggle_star.png b/app/src/main/res/drawable-mdpi/ic_toggle_star.png new file mode 100644 index 0000000000000000000000000000000000000000..fbdd76468482b28264743651fb3a28be17785d8a GIT binary patch literal 341 zcmV-b0jmCqP)kdg0003TNklzwqELvRD6~pS(W3MKhzC$|*rP$#&)93W zQTWO`b517t<|_5$AwD<- z!CtUbkdg0004_Nklwc?-HFt zg}o{aA-JTBO#?M*pVuh6oa8ajf#WsWFgTxV8pctyudcamOe6!$V>Mon8jV*^u#gO} ziun*6mQ_}EQ+EjF@HE*cDq`G>!r$2|m~jT*9c&Ibg~%(gW!khY@KS`a=73CmYRRIm z5JJ#pQ7KlhH3vKd>qctS5$oVlbHG=5la13RWl~kdg0003bNklh%7`g*ws=3#V0vtsGiAg}Sn$X`7W|!i9>Rh&x9p7Y z9XF(-uhC?s#aA?wUZRMbh`SVZ9Vj!7#mAIQnplWAg-gL~3UVwW1q*U!f(Boa0-uJN z;6C;ZI%a~;0J3HsSbdn_S!Qb9hE%CCR5Lmws~6Pq5sp0JsJ%;`IU@Mf^@0ww6j>?Jqpue@iPekqR~L>G**4dLoDehl00000NkvXXu0mjfN41sM literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_toggle_star_outline_light.png b/app/src/main/res/drawable-mdpi/ic_toggle_star_outline_light.png new file mode 100644 index 0000000000000000000000000000000000000000..62d446b9a194517b900f8c0da86b7d507154252d GIT binary patch literal 387 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 LNkvXXu0mjfC**g` literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b0a909a1bd50912afc285e2d6ebfa08161570341 GIT binary patch literal 262 zcmV+h0r~!kP)kdg0002ZNkltYhOL-s&?%R~IZk;8&NbxRr_v1XdPW!)8Sw z6>PuGpo=3#<3{x4xXCF47YG72k|9hK@VIwIX@ZNgKoDUk1z`?>;E)tNatUrpfm2j4 zEjppWIfr17b`DL}N^r=8O8Q->GiHy=Ra%c7{j3*Z(2~~pf3u~`6XgLlD$UKr+yDRo M07*qoM6N<$f|}T9_W%F@ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a9890dd4077e91c3fd4a7909cb5168a0ffd531ef GIT binary patch literal 279 zcmV+y0qFjTP)kdg0002qNklN1Qa>Oa=~yJw~$-(74ywk%5V5{RSz> znqkl&l}PO-I@q*>potD4z03vk4U#hxyXAs)w*6C_v{Cs;K9f79an zA#?j*`{(<$x;nH1j!dgy-t~X}-~Hl=ECIjc7I>I{WaIcVpW(^|u_T~CltTWYdd46d zMq{VX`>*}yRmok4U6`n4RAs)w*6C_v{Cs;5ihaFO? zXqY%JaOOUfO&$BT1kRi&_wif0#95{1(i7!zjgL(yL0L`0pCL0nK~Y&vVV%PHvJ~YL*ZDqjME1!8En;C} Zcv~rb``7ac(}8Ya@O1TaS?83{1OSt0JtF`B literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/main_select_tabs_dark.png b/app/src/main/res/drawable-mdpi/main_select_tabs_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..682f201b1a6252fd51ebf3aabe21539f5f98393b GIT binary patch literal 161 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE0wix1Z>k4U8J;eVAs)xy_HX2EFyLXio5Im{ zQbRUIryr)_C zu)6E9pKfA;ikH<~17vo;PCm5EWrf6V?VE4CWbkzL Kb6Mw<&;$T&);uEs literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/main_select_tabs_light.png b/app/src/main/res/drawable-mdpi/main_select_tabs_light.png new file mode 100644 index 0000000000000000000000000000000000000000..05ed913a673a511f2b199956cb80a470009e7555 GIT binary patch literal 167 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE0wix1Z>k4Ud7dtgAs)xy_ABx=IPkD&|6rW& zAUGpSMDZbO=$@A;p7q@p&CS+#C(h2#;YziuALcna| Q0MKRzPgg&ebxsLQ09d|2vH$=8 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..be797b55da7a36bf85f83fbf58efda4a7d6846c0 GIT binary patch literal 397 zcmV;80doF{P)9F%9C?r4!JW&4@6bhgjE@)8h3kn0!CS8_(4Dt_9 znKQ1a{1W6FphbGD4=A}l0dgF1OKo7y_XkjwYmO=X(R6$Pw96y&Il7(?fW|qe#YC>Q zTRcD;bXm#W_lpIn#3?sS4>5QY3D7KU_J^CM3Iynod*+57y@~;7l1rNXmHz(#qJM(y z5I(UN?GPIH4Gk0`6Q$TdGcwVR4Gbg(Mlusase!TF#KpwG)y%}@)WG#TTTr~P8W9kE rMPpz|HNu)M)RO=SkN^pg0MP;ON>9i^9Zx7m00000NkvXXu0mjf@lKt$ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ea543c110b7d434966c50d87c35f02bc28b1a8fc GIT binary patch literal 412 zcmV;N0b~A&P)0OD{u zlvfY{I05wbAQP?6d;0*#sSC8Y6b^C_nQR>HSklYuNqM^!hSgjP%w3 zQ8l;`!|##){{Wo-f_xGNkD#1QLITT>LLj0LiYaVH6t-gm2?>Rij6zaMAuXqHFd=X> zBXKw-a6Hco6f(n>LxA%V4S^!n8f7|L7Xui;00uC?Ip70bKjuN+iM~((0000R#Lcn-plc8?~>?0S< zMhJLLm=pwz2i6&k5HN085ClAjT5tLlFkaYc+vQIJruC+O0na&8f`HVUp9%QVTg{~# z0-j4|A_P3gOb7zTJ*&U0WQ%}t!=fPIIbtkAz$>}UupnSuF&81=Ib~W9Xz5KC0>&Ln zg20!(=|;eKWtUMwpwXK@B9OcVXPG5T{tc54BK;^{>;5QCzKl|@4Q540$;T;_WBdgACVWR*5002ovPDHLkV1f`mkm>*c literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/media_fastforward_light.png b/app/src/main/res/drawable-mdpi/media_fastforward_light.png new file mode 100644 index 0000000000000000000000000000000000000000..b1e057123f16eed2bc2e53924fa94d115fa06fa3 GIT binary patch literal 366 zcmV-!0g?WRP)8@=m4%$g zPU0;CI&Xc12X=Bdn`NHod7kHaUPa-q*w#FU(_TMoBml^VOu~r*dX8mryWdOz05QkK zU?U13D9>`30zgoP-Q73<#9KJ4^(ejoAf|ax00`nifS^2u zb{wFao3#KS=HasZ(twH$*w! zm&7z~c+giHtk4pEF1GQ*QC?jjQ{6A7@xw_`-(UruozD-5a3nh27Ud|edNYGbvB$EQ z^-z!A?PU%TA5Tow6gb2_eVs$Zh4k`f)+^c(h9>e0|L|UQP-#yxl|1<}LeJyMBp*Gm ztD9o-e4bqM$@P2s?axt}fD_76l^&`6FF`KteaOCjiA4O8l65;lVd3fO=d#Wzp$Pz< C8-kYr literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e9dd01ce1dc0494daeb7e9605fe2dc740ab4ec4f GIT binary patch literal 326 zcmV-M0lEH(P)*uoo0WA)p|FV9fnmODho(IYs7d zy~}~!nVp$Zk|arz0&={zC#3@l$g*zswpjt|sX6>9H2+t?`iS#b zCj~6)Q4K5Lc;7!QE1-ZZ$6d3F3dnlVaakq>ta)4wHc0`?F|9*XK#rM4gVn_qV6R}V z3Hb=wFhmm(or=-V2>y=Yf`lx|;Gz^Q%i+cZZq3-{lx@!g9Z;|%8hA*BkLi*mNs@)( Y6D?<=W%UTb>i_@%07*qoM6N<$g56w-8vpmdKI;Vst0NtAT+^O+Z{Fqo-gfu=_}u&6J{4LR#sLK#C>K>rk|!x z*17Jqdn*m^kwxl-!z!QF}^rHvhspsuoT;6hHl<7-!eCb^-IN=9d ztWsiJclu|X^`-Y;KY7D#odC0(4+U^>`NbSJN4W(Ke+D4Fa91aSe;Zawk!POIzSXIb z(TUIw`VG0qM-Rk-&Q-Xy5RvCVJ*hG=iel7Ph`7Tyg|MWz9JhiiA!)t~z=_5$VOTrn zA*X`yL3c}T1or|e0&vVN-80c#;Qa@51>w1=f#s_Jd@=>_S^)M;0h9!w88uK+u8<9e z#{zI36`+HIgTo_J04)hr4Cz-CzgAR$JqffJ(r+=QkC1fDQGQ~cJW0=)dUmt;eD zWmRK-5&bVXmQe3Pc)eGpI2rwqUXx&_tU+mjqwryC* zWi?{#Fm;9P6g$d#Ij*ug!LDcA%ne=qnzG!DaU>(lp$gogOe(-8McFg7AI7x&9)y}_ z7H+e_D+**tlA*vn8@^e&7IS);^|l9&AUzy<19!Jc>uIs$EO2zi(d>u?t%p?7tWdus zd7Twa@7@P7Mv)3t8g%K=r9qVnMaB|VR#sM4R{x>Dd({)Yk=N&k00000NkvXXu0mjf Dp`#a| literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/media_repeat_all_light.png b/app/src/main/res/drawable-mdpi/media_repeat_all_light.png new file mode 100644 index 0000000000000000000000000000000000000000..18627bb7205609728014f6229c71393c21966a4f GIT binary patch literal 681 zcmV;a0#^NrP)W_y7I#gI6{LVS@Y9!yX}vOt8uOmHS<-q6w|fU2C@Z%ndGriw`@^GyzvzO# z;b&_dbY+@zc*x_`jqrUJeaAO*vm6Onh0z4iK?i=|OO8buECF@t_dq5-F5cxxp+5`O zx|uXJtB4=`_n}59o)lU^zu}8}_|8Y-h+`Ip3z4bek^981F3n6rjBW^#aR$`0cN$CIEa@MKs!hB>-qS*UPs$M8JB*if|yxvKcG`&4>$WiR&wDgP|-Va23`2wy8^9>J}agBEaOv0X?Kj z=#0Q=RJl1|1Xee`TcI#vFNe)Dm#cC~Snt z+lDK}S|WPW*>tC~87*@i zQcJ46M)e`XY29AqwfBBZfsPfsN)7B_4?EPbt5^YDnNlPqBqSsx{sVsiu#i5HKuvJy P00000NkvXXu0mjf(bYeU literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/media_repeat_off_dark.png b/app/src/main/res/drawable-mdpi/media_repeat_off_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..33bdeb92aa53e00b5cd5301426d61b7eda9eb044 GIT binary patch literal 344 zcmV-e0jK_nP)!#q&f$s%%;Qo0$8y#SzE4n@jM&AVJ)eB1*K+*haz#toaB_OV|wl4&%vAJA%@Zf3$ z2!S1Y_U!y`&$IZYJ#*p0+3u@Z=dkjSEmoACiiShTl0vwCmjWMTq=N#SWdM3hYmKWSq6cfWpZ@7i7vs8lMI zN`;p>o`eCYpiC6NmQ+h3v;d7HLI=>$1BYNZmPT&?SQd=E+LB}5af3uKpNOUypb+x_ zYC6jTI9Lg6m23~2-j{i&X95k5a*yB+ud0000(GEbX;?r>9gu*fqzOo90S!kn0@kUPEkJ4%usl&XRowz) z2;GoQEB}e`1k`N80y4QNIFN8Nq7n`t&9jgLlvt@Cg%m`LS-l*H;*=-vQ=RcySi>4uEi~sDfb=SVhq>)5MgUmAh7rKH z&rWR^53B_qUiIAjAID?>#{IF$eSiqSMejC@yFTv%5`faZy<+*j3kU#;KP)FekF2Dx&Q;Qk}XmDK6tSUgAJzfmu3j(ZgWz1C(pv};@XhDE&9xb`@0&MbNEuRx$ znUjfa=R?5KPlEvK+|10C5wP~NAV8mIM{aR}8U36Ppv4(q&fFgq2;PE)H{tCs2n0Wd zQCvjnRlFbYDvsbWisCtrU?7QNB+G`#D<@10alL)?MLI@#*5dVfBE!`ul T8j*0900000NkvXXu0mjf85@^g literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/media_rewind_light.png b/app/src/main/res/drawable-mdpi/media_rewind_light.png new file mode 100644 index 0000000000000000000000000000000000000000..7d60b0bf953204551c9371c6f6ff8e4dfa7e57e2 GIT binary patch literal 385 zcmV-{0e=38P)Y5QcLQgn(cmV4-P55J@0+^G>=T99Rj#(!xRui;ym05z-_@ihz~k2j+gQMK~|# z?Pc{ca;wZ!zwA6P`!QRoD2k#eilQjR@wz>neJ2>y=}x?QvliT9Nf-7cwRpjA!sYpx zr-%^Q1i~u%cts>D;L$mr5!nSg`+UF32?_*t3|5QU1bo6noD&nE7B7#tMzRsB4F;PNx(m(@uplMfxLcZ1-dQkl)X=Kzo;Nu(aj= zqCn{__~|C(@h=FJehi(sh#a_zU3iRKc#a(yNSqkSK8Mnm(uPtO#`3t!WqwQ?_?jt- fq9}@@{2M+1aW}wH39o8q00000NkvXXu0mjfSg)hL literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..84e417507153471371b17a1e6ba37e92667be9b1 GIT binary patch literal 294 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I0wfs{c7_7Ux1KJJAsP4H&QRoX3KVeNuaIHb zaZ%}pub6p)(N3nxNAot!h-o~UB<|zpYEUa+SMGZ}$?)WVo4pH)!wM4a&9*j<6k~A| zV98JLyslmF^i0dFV22J{=JgFHW?nAg;wVh|^EGJ?M@N&X?D7Ty^E8t=OipiJRI?tL zE1C-waCeJiRNVGu57VPVQ+5G0GCKn`MqVi5;;2>G=Ed3345RR{}Bgbm|iPysdFw?-n1>LSR(TA4VwuO?nbWzvgA&$?d~i!HP&B#?dzSI o1smpNoN?FsK2wjQN#RGOT*|NYidur)4Iuw}y85}Sb4q9e0Etp`^Z)<= literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..53380694a888f1e159147e8c31d38edef4482ec5 GIT binary patch literal 308 zcmV-40n7f0P)hgWX^;3I>DOV6bX2sbDySd%xCXFv%UO4|Kj?@WJfP zo0%vw48t&tH@Ky5O9lKDXE%u|N&$k{`>s}i$T08Ml>!8DN=IKW#)|-vV?Ayu1qk{Z zX1*pA071WVu~Y!`cdU}8PyhsnN7s`IfS}*@J5mAAT`-NRLIDuOVUTDAhzyIkp%fta zk=QCEK;-EraC{dO2p?M|1j5F5Nr7;&Ra)Q~-~THhE`jtBO247F5lLIIIE<8WEFMUt zBbjt46_4f8#YDTBiI-F9dM-XtNKZ7{BbEG2XBdWI{5%g>&ICnT$`~jB0000@jRu?6^qxB}__|Nk$&IsYz@#Z(gH z7tC;WvRkn!kSFfx;uxY4oSd*g{lR~J7I`@jRu?6^qxB_V-BctW|y$66Srjj7P zV1`*~p4*dvJaJDK#}JL+22WQ%mvv4F FO#oHa9GL(B literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/notification_close_dark.png b/app/src/main/res/drawable-mdpi/notification_close_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..55e2bf21d8d231667a880445da3a64d07ca9b49c GIT binary patch literal 173 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE0wix1Z>k4UC7v#hAs)w*6C_v{H<&%x|K?nhM=RqBE3QdKzw+^DbgsQ7g4< zHpgzJRh(B1F7-56%r-fm{E=g2-||_EB2|rt9F`bJY5I9gQBReeGt)C|%L++W6DEc{ X-j$0J?wK6~+RxzW>gTe~DWM4f1Ux}& literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/notification_close_light.png b/app/src/main/res/drawable-mdpi/notification_close_light.png new file mode 100644 index 0000000000000000000000000000000000000000..780add4503dd43d62ea4f8a1bb2359724d961a62 GIT binary patch literal 205 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE0wix1Z>k4UQ#@T9Lp+WrCrGd^ZZJC#aYWDTZRZ2}zIDTC@#ZIT&muH`KNY z@H!k9Z`$(u{((X)<5A@MbCue9fH3q7zIDAFm#&r_~N38 z8T|osCMbAHnm$=5r>LhX-Sy~{Purr!u4gqC2r@8Ceq_J+^V-+NK=(0ty85}Sb4q9e E03ZrWr~m)} literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/playing_dark.png b/app/src/main/res/drawable-mdpi/playing_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..b5b90e089c20c0e963ee9a967349886a37068d1c GIT binary patch literal 188 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE0wix1Z>k4U&7LlfAs)xyURllCoFLJfm~pA= z{#C^dJOQycO}pA!7-uY$PLO0M5u6|;lfG=bbpcCb0b9kS_foFueNuUQc33zpSpL-W z4MWm0wL%6(D>*rZx{y1v4jmJlRx@*aRQ$~F@u_bC!^R5}@;G>&_;H2_tDpLsY$AR# mD)sY>HN|D}r)KVb$}gJ7v_WR~^EjY$7(8A5T-G@yGywq0|3onW literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/playing_light.png b/app/src/main/res/drawable-mdpi/playing_light.png new file mode 100644 index 0000000000000000000000000000000000000000..55a0070a76802b6196dc8e9c3524feec99e7d640 GIT binary patch literal 186 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE0wix1Z>k4Ujh-%!As)w*6C_v^SMss>Iyj=3~GW@_gw527@=oUy>6R z9$^shxjuc_nTANWShjUqZ>C5XoX%KQIC0?%2B`~c-@A2+Yj7Q@<61QFP>V}s&@vt4 jz$tSz^#a@u1~4gTe~DWM4fR4hO5 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..fc0e806798e3adf3e74b7a05187ce4a4b5767f2a GIT binary patch literal 191 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_+iHcuDF5R21WuPn?xlqhiQVSRH( z(;>ycxeV>;%sSEr49pKC7jEj4osbjI>XJ08VPWNrhwg`ds{efVZg0LsoF@Bcws&&- z&T=)_TTf0vTjqwE`l{O5#ScKBk&v<~PZ22WQ%mvv4FO#mK^N$~&x literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..cbc0b33a28f8412c75bde372264c74125038b13f GIT binary patch literal 376 zcmV-;0f+vHP)KsRs_C{@^Nz| zy~4)-i4!i#S`}z3DasG0mYj<)+}1hR*_$DStgf25kR`6*2k5wRh%fjI-Z>l5*s>kp zT_4_m^-s}(mtl*9yB?W61P{FR(Vm`lMcIfNaZ}+d*98j4d2}Uwaz4aUlF^2X8rYKg zhi1H132*Or@|q@Ep13I|>xQxxwk#ag)@9H13`f_R)VN8eEqS3Wu + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/notification_close.xml b/app/src/main/res/drawable-v21/notification_close.xml new file mode 100644 index 0000000..4a93427 --- /dev/null +++ b/app/src/main/res/drawable-v21/notification_close.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/notification_fastforward.xml b/app/src/main/res/drawable-v21/notification_fastforward.xml new file mode 100644 index 0000000..d0ab76a --- /dev/null +++ b/app/src/main/res/drawable-v21/notification_fastforward.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/notification_forward.xml b/app/src/main/res/drawable-v21/notification_forward.xml new file mode 100644 index 0000000..0d3c93d --- /dev/null +++ b/app/src/main/res/drawable-v21/notification_forward.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/notification_pause.xml b/app/src/main/res/drawable-v21/notification_pause.xml new file mode 100644 index 0000000..330260f --- /dev/null +++ b/app/src/main/res/drawable-v21/notification_pause.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/notification_rewind.xml b/app/src/main/res/drawable-v21/notification_rewind.xml new file mode 100644 index 0000000..25a16a0 --- /dev/null +++ b/app/src/main/res/drawable-v21/notification_rewind.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/notification_start.xml b/app/src/main/res/drawable-v21/notification_start.xml new file mode 100644 index 0000000..75e23c0 --- /dev/null +++ b/app/src/main/res/drawable-v21/notification_start.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file 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 0000000000000000000000000000000000000000..3b98cba1948627030b23ecc18f08ed9a0a4ae1cf GIT binary patch literal 498 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H0wgodS2{2-Fz)npaSW-r_4bCNwo9Ny`^VXe z8^d(Yw6XHCaVtuDZ#0^<=7yoRFULvt4GJgSf^$?(UUKtPQcXUV{_A!`#+|fxou@0# zf3|*I{cg|sJ4au#Fak|R0zZ5rV~_6JB>MhM-?uyOSx&sv{Wf)VMY`6lVipAkMh*uC zrX#WnYmeRt-<8(TyW#m8xqbWCJbsq`Qh`Wb@nldn75wdWt+?Tr1*cF015}q^6HL3n z1M~VbQ~37pyq3LhaqQ(&+2QTCt}lCLlKfhwBCPa(+}juH9(9KMGp%qvUDdIr{U}4$ zwYn#^Z&(AIR`XR$@EELKbe!?ZO6ldcXILYO?>y#b?$P>x)>Qt}fBS#488*6{wWxPJ z#+370?!k%g4C?#XH7wX)Y!pp6U-(D8^aj_4M-mU5IHMVlg_Lnk*k1Pd@|xoIkKIz+ z%3n?`b-m)@YSt{t#?u$r)YyV2N{>@`O TKc!v2LBZ|m>gTe~DWM4f7QWLc literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..13dd7b2a869fbea3f5d9b6eeb06d19503882eae0 GIT binary patch literal 528 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H0wgodS2{2-Fkbg`aSW-r_4d|zZ)r!710OTj zJc-bm(H!9E>t%Sg^nP{NUWrFb)r&LV=G$=bvN<-{iirB#c3kR;kp3T~FBip|CcfLS z_V?d2mFY9z`%OypNV}5zgQ>m?_Kw~`o}Io1-<95zj%UWStKSg1ZLK$y%wC>W^tfk4httl z7mEUex+6$Ckip1d;8DzdYv1FF>+j_@CtqB&=4IQqwW{(<*Lm;rYr3B|`@UN0-M;m) zAFb6HSGmS+o$vXqK=Odw+NmXc{ft~6Z$Bxv;8}3GY~EuAqjl|>&kDp9&VJkfp5gMR zXL*Oi^6cuZf95l#IKIj(pSYRfm*sQD6z%uc3F^-o*Gx7$a9VvIQ;nzOgD*#u{h8wmZG^lNQC%kEn|{LT4cGw;9qJ6YrYQn^o#Y9&{#em%~5 znZ7N5{*q@^ufN~n?q9V0V)0tHrFV2?5uyIUKbA?#G2UZZcy1I()YH|^Wt~$(697~S B-Od02 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..58620b7b8067396fe1ca6271634dad1d003b1271 GIT binary patch literal 1181 zcmV;O1Y-M%P)ERM*rU@RvyA5h~ejem;%y_Cmwof&)I!Jb`nH zwVk5UeOE}1xdNx;8~$+2g7Ss4tb-awn~k9e%O%Hcl5YXNg7PWVd#Mgtsz}q3D%EG1 z?Qt`>SsMOSY=iPG(qyDv5t`^CSYHXFqy_i|su}KdG#f2B_Pw^@kHcmt-#DXm z{NXyBwOE_Uw#Oe}TY$Hryra8wMs+AD+ec&um)I0w5R`AxmgCJq&HcdkcYY`>{Mjh+ z9X{4}vJO&^WRHb5zcs700=(ioydHT8P%xUO?QfoADnL`OV{ex^frQr*oo3no?rWw3 z%nGupB|6Vmu~rjRwD}mN#ad6)XG)K}dXdA-KS&<-KkEeT+6F@;gHc`6TipXejkMP` z8YT&1IRTE+K`G)4{lw70^;F3Hzew`;_0Ls+D+3ANW1;lSx|sY-mgOqIJzhJy0K<{_ z5|_!~&};>GE^LiW)3*|fq@Ww{X-*%3yub)1e8Zu-VY+w%UEs6;^Z{v;%ih_0F4o8M zR)81WuIdEg>kpQ8cBwP672v+GjJWak(_9Z>hi5CmwNB>zRujYpU}$m~es!+L;IYWK zSmx5n#rMC%0t_{c!V92+#Q3X2i8(AlBU1s|1{%K2B>DL3z5wr;3h-`VPl0yR{&QK= z*}E@PQF=m=Z-Glp1-J;RP%KUV7l_B&f3FNf6CCR*{34rIV&e)Lg1XMrm-@e_xt@!% zya<_|^Oj&4?WzEkZFhdCd@#8�>h!96#JR$|k!l&`${Y`@Q@{Y(I+Zf)Zh=N|^sF zUzb@uCgUA`U*E&k?5~ZJd>E;n$6H<5_dEZdkrd!O>=k{|Cb&i+1YdBynSn#3pU0}$ zt^5}0H$bu(-oHol`8s?YmYeV2-#=xsdehxMtl@hWvO(erWK0VkUQ`O+*m$=Y=UYUT zH^c;IJl<}q(*RBzpP&FoV04zv3}FvnGw3~n^P$R!UM}_uUWOG=AvXX=x!Nsy5$1;c z%pWaJCchm_oN v|0X>bY21ZJ@gQzR0w*8z>*xwb!6*O#U$vQ1^QfC900000NkvXXu0mjf{Ha2| literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..826361e61dc11746679c7fdb3dc62e99b700d560 GIT binary patch literal 193 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I0wfs{c7_7U4o?@ykc@k8r=I3)Fc4rZ?l4_p z?m0C{>QiuQ#kOV7wUn|LIUk+TPLRLJ`&b63zD81Xm9Fc$kB)(hRkfZ-?$59_R$rgc zFTeg{wbjoptXaPOOnXnw7p+*oz;{p6V~*#y1&4#E`-m=^!?NHHDFZq2fB%rPhXq{G3ZgAXj?2`njxgN@xNA DNh4Ai literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..171c20999adb1b45afbf3c709f45b633d77b697b GIT binary patch literal 1144 zcmV-;1c&>HP)#slb3Pgt$`Y- z$$npQJj{ssuq>9ue3-f%n;jo3;S@ZL-;jnzqX%E(ejJR2#@0E!F78GL6w%+f1dI0d z#wW(1_!IKv18jc*;4sudG5HGX_J!t`!Iw~pJd7Dj)%+1iK_ym)b>nP&GCTqG zAvv5HNAuI-Lnw^JDmKyOZMW;)#HXsr)T%w-seoy4rGoT-U;8^#qqllL)`hJ%X`#P2 zbP3`{jPI-Y8Sy<7#%fhNsXO1Ju+@`KXx-{PTtc`Hv8 zSvEf|o`qc0Wt-$$jk%U6+l3RY0yq_N#riGR$^_Y{8^$%GHoqEjkgG(Hl56q1QTGlt zn9jBN3GovY#T&gJ%C-1ii(TaHt^u43MYVXvrg|^&nX*An#a5o_)8w~Ws$z4C00OKU zw)yGN1z9S#_*rWog$>|bsEh&-)-SgCDbQYq0lZOc0Q*8^7(jseiV5JYG7UgDb4<<8 zihLOd@Y9$A*bOS<0EF2J1#m|{2w;yw0sPbt0=S{jRoz@a2;lY61+XyG4+5wfT>$Gr zKFsykT6(JcH5FU@tmgS^9iPVoB90+`Et0H?QHC968M`N~)V*a@<*T;(>(DKD#HDbk`98(Ri2)2IU2 z0J3mM<5{wZJu zwU%@L-9m@Le?V5&soq`B^}MOhe7C|@Z;;fRy&q}o+C!{2fVUP3-~lKG&PRU+4k#4B z{*Z6e{jq{ZtQr4YDXswKD-^&ykZ-g6xvEAiz7|LGe-(2{>@&zG=60z9xU8s=A7#7^ zSfbdQA_>T&;m#4#K_-soKMQkX&E{A;iF!_}R*V~CtdDs^74dMX}r)hBQT7Rt94PVp%Cc?*%C5F#wa19vFnu zMN0kD$P~B?2`DDD*e~4iz5gI)#--?jJgLCp_FH+|Ag0E?c-LsWQ38)*GfXhHJwrGv z_Qsv~qR6GA3Gd(vY=tSx?P=24u{_qn=GYjkV;M|4POLRh1N9G_W^me6&tL}t0000< KMNUMnLSTYe&przP literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..fe7eee7d2976c7383b78294eb5a5823a11253a8f GIT binary patch literal 285 zcmV+&0pk9NP)P z004d@ab+#*#AR!*N;R4)nIy>CG{2b|WzAy#+^x)oL7L8 z26>#vX<#`l0)(#{eKpk_?99FZqG%uh0!|>{`|n0KpmqihMl=NQ2;g7UxR+J_b)EfS zSx&ZUbFx*$S0n4udV;fk6$G{Un;OxpHRng(|%#` p0S0Csg9m&4H@`W)QpNL{HnO9=n~ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..0ccc11a8e4b7b003191f90d6bf0231b4af9d8fb1 GIT binary patch literal 202 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I0wfs{c7_7UiJmTwAsP4HPB-LhP~c%GRJA$q zq1Eq;zg_bOPW5tUKHkjt?as`bd9$`=&6+i9*8ZcRj6=9+;Q?d913kIqDgO@j2?m&bg8>>p zC<7&2VEQfj98-!a@sqE^!9+?v$AqHSIq3`VVw2C|p}<97MW9R{B0!aczC!bSrin2s zru2EuumAiDc@z2+I!5#%I!NgYI5wsaajZ*U#4C${R}}RGoU_n}IM++MQ#JupjRZ|O zf5wQ0x`08f(Y=9>w-}KF^#PD|>7$0tD0T}6c$UVi4Fc`%;_j}HU5VeuwWP(NngCbW z*(1^vlSUo4gnIOr!;`R$t0tf&?5su+8T1$LePqb9FKp9N6VMkfstt;8R(wt*Ni-e% zY68ZxuFzfabt9iIX!XJ^pts`dN&nkeO@J>Pg7lX}brMZVQSS8B1hj<{YL_Ci7%tw2 z;iAN@uuYqD83xg;&*2Prgfrx#DHw1r!y`#rxeO&N#b?MPZgHe~5|pLZ5%xY(@g_Zu zr17e!!=(`xO~-peoNarkz8jvhZpWuO)d1&#M5jVPlTAQV?@YL(Pz{PJ#U0&qvBM(3 zF{z0?V|vf1PNurpv@|oVk!dxU6sIip@!yu`3Amt0z!gmbE~yf6O_zv^N^91vS+mA( Y@59N6tC9}k6951J07*qoM6N<$f)c|LZ2$lO literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..0a1bc51a9917902dce30355da5631fcde1f5a7eb GIT binary patch literal 521 zcmV+k0`~ohP)pC8|DvDof!3lzjGrPzn#=t|7o(cmUFPD1ZVefC4B$P<9a#I5@<< z3P218hXfHj012|VxJ-ZY7~Puu3@$EN5_SM$oFE9RCUVgb;LPOL5QLMLHTy2YCW5eK zzB4@d>>rpn`7DC4Y3~#)BM4XYo0e?f2T-8X%MNgjAS~Mh7^jL*W5q<$Z@QvWhD>h7 zYthE-0Yq6x5GwSTSPQf4a>*sT%+hS)JyZ~c4Wc%$;xP9JLZ05bXe%jFwAMv>*+UTS z8L?drw!OvPdB6Q8$&NQ`JBXjKXM{3SwE6OFOmXAQ$Po{sA`W`XD^$oc$0%LIiPOax zbL73f7YYmpRg@hLsF{;r;haMbIj^gXHI8tCe1;-S;J)`3aKWUd`u`zBHyH|)Us;za zvPdet2RTZb1rMUk0%@za{>vxgX*1kFcU=J#;1>g^R4SE9^-uH&!{P)DW|V(W6V39#^(MN31a! zr}}BC{IDpvq=AlxN`V3@8afRwEgaWWn2t;R411QX4=Is$i+b@}!Vm(LyE)8~|;qwI5O3r1M=v(orB9RFu%W722QMZPRUQ@H5J zmUO&qC;&O>l$+sF&Y=)5xo~&@P!vjHhl3DliWM34(xCyQWprE)4nQBoGgl5z0E$9G z?4pPF06h|bn%Jct96(cKJS`FcO=Qab;KkN23+|yt0-(~v#H1Cla1)!GobwrOBs23`H5}fB~ zb`VkA&@&K8&c_(yoS)g@QCxw@GZb5RCs>t_aluP&#u17=laY=87OQ;0#H2Rxib%=G zI~_YnJmD>;+>0e0D+*azkd%GPq?woBZz2{XUL~1_?6b%vnWC`3k$X|OIig zAZzK@g}J1sulM%+?)m(k!&oY*q>@T1|FcwA)SQ~Dt}I&Z3sTU8j&_DU>|-B$+8K1v z)N5*P{>lr~#2(t-AAN-*dNnuYwK8<*(NDPx;uo~lE%Dkj`XPOPstO)p1Vb3Y2p*rc4n=^E==e%5I%4-LBERzNGc6*ACr&3eA`F3c6RLg((CX2Mv|54rE@ zld=&$S@AQsf_9K7@$>0`foAKL|Cf{-l{;-!--m!PwZz#t5hhGV`!P5j}Uyi(+85DC&S zxDdIvdIN2Tmfqnpy%to6f=ltFGjN|fgRTi#RzN8ZZbsAG5%={ww9$qSJS!44qu_Sz zVYRpTkuEAPg|uVFv^*yy&C1Xt8!dawr_7E$Alupuew5o-L z)AH%TM6HB0zPQ0;YBakE9?h^Wb~O8YIPEQXIQ1LPp{2+m?G|^7ASp7m<~W}71qC0| z{=tIcIjm^haZNLLkMQ`qb6ryTuj$|v+SJ^sE?`afEEbiiq>@T1sfd+7cVD1kC$Ama P00000NkvXXu0mjf_N99( literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_action_rating_bad.png b/app/src/main/res/drawable-xhdpi/ic_action_rating_bad.png new file mode 100644 index 0000000000000000000000000000000000000000..0d92ad3529d28985b3b3222a687457e52e5a8359 GIT binary patch literal 425 zcmV;a0apHrP)EhM^&=DrVSiMR5l9hA zE+~LYoPeaj3ug;V(ff1d)p4hY3E<2<|HNyzhzmTpjWzIEFD~%G$))FdSpnR*XTSN*1i15v+=YW);z@1ACbp+O&NrN7nlmI3!Gu07TaN;v? zaAgG0a|1Zn5;)LT5vX2BO#mZpfhEXEhot#mjmFKX%wjN2D@`b$lWIK?ZdL1v(8pG>p8uM?VuQ4UcfMg5hS3E-j!{|= TNU%j600000NkvXXu0mjf&G@+X literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a211c0a35bfbc6d1761f46d6a04ee34a4c225d3c GIT binary patch literal 410 zcmV;L0cHM)P)J+kIiTl1V+h}Oe=^(?5sSrQu~;lWhB{}&BqPitr>ytitI`{I+N0`%U*>is z`1{SOC=ucpGY<$8!tDXdy<KIVeg&=|SzUH72dt zlC;fs&4A9EV_4p|ZubdKGVFR1}i(c^(m)heh1*&&^tyc;f_T^c6 z9K`(F=E0sZ31H!I68oSIRmK9(+#sFYH@%()wXWkG1jl-=DKxkwD0Ctnx zn*^|^#tN{qsAF#oU@HsftN>1lURVKWPtV>E08Qy_Kyv^)_|6M(uWN4vz`p^SoB$S_ zvI5w7tg-@h4-|m^2f)4w)d-&P0(@ogGz3D`&JW#;xJCakx`Dgs!sng zbI}DyJ?tAW_v_6XUB|A!=N41w7UPzHJaAL6-yDkzKb6@lwYJO{ey{-BY~csWL_F6U zsm)%g^~vt%T8iO^&oz4E3O@wcU<<$1F}wJ|6n^WX%Hai5_-(CDkjE1KPXst%v=4g+ g`F|=BiT;c}0g~-_9&QGYzyJUM07*qoM6N<$g0_ggBLDyZ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..85e960fccc5ccbd3ff577d6f1a427097f47dff35 GIT binary patch literal 308 zcmV-40n7f0P)G44tJLa0iw`GSiYn39&%u`|u|EPXBXG z^8PEHlq5-#B*~XB;fbE_(9uvIz5GO}e#;OxhlH4b6+>wP0~oU#9MDAuJW@bd0HcBP z4-c4wF@TCUZ@>(^0V``j(E^4BEWsGSMcD$z1 zjCzS1t6t*Hs+X8r^%4~?M!h6)#j8;-NnG-5)F(-j{4w?-5sV2jTl9PY0000D5`1hS%^fIu*VK=cEUl?XnfA_5sv84*!|Kp+qmNJK>h zBH|)=*#ZHwTejSR*`t!ozP)EMFUgyT1OkCT;6H#Na;&hxkQ1Mx!Yga^nDGE`!i@(m zx$(e+8xQok@sFJNC7$&tW5j2Ou+-xzCq6cJASJ|$V5Da?VnVzKF7*8NH{SWJcoEF? z%JuFAigvsRR_Lbm&>x6Cjo*hIwS&UP+LplJ&DsfUDSs%ix&ChL%vu0D5$N12*>ny? zKm-X}AYu#j*aFk$L1YO$XxW_e6pYO_BaT4cj4duj-UKi(o1Ah4uC(OIX;$zhfEa5n znYs0?FM%`jB|P8=+-bp$>zCh)0FKNC#~gu7m*2b3J{|jJ!g zCjk6ofE(s?0pgth4y6HX>uyAy{9A4r059z60wgTy0vx!Udj0_(+0X?zVNw@h&28}T z4UjlqUU&!CcOAX_0$efe3t*Lp0st-d;nN1NOyV3r%6$Ul0I1M*1~@Emdu9VLh822F z@ja?V=6@@IF|2dOQ?BqK=WK@i)1aWBprD|i^#+~Y9fHjoEoA@z002ovPDHLkV1m(( BxPbrw literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3edad0c60d7e077117df31deaf35049e1cedf4a1 GIT binary patch literal 433 zcmV;i0Z#sjP)3-XH{5#J1%O+ewFFQvrvf-M-hAo+ zU%F2gD+vA zBcDZrh5p>wcp||XabW|!k6d^>kk&r)}U#+rX9XIq)jS4$TZq z9GV$e$-Yx9!t1bntqgpMcW;{KzjPO1||;846Nk9wVIdF&5(R* z23pxF*MK=0cr4j~Ij>?r@Lv>S$x+_>KVm=xPec1F@s7JpdBZRa!}w=>0ng=E0zloc Q#{d8T07*qoM6N<$f*o|AivR!s literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c1e317427905a93d7ac5035425eb0c490e1920f1 GIT binary patch literal 268 zcmV+n0rUQeP)=ypF2l#Ipc4pYY zKnNj(5Iv!Fo&#eENZ{WDqCZ0kNI(J-kU*Hggq)HyH8m&Jfd!aR@OI5V`}mZ*XZ*JI zxA(>y3y@IwevEJr@$w6368b`Wv9R@?lXKaw{Ao`ufT?( zbt}$1t@z3$#4L0TqZ?_c^&_L;^%MSN%QWN$l(3|vqM@OpV8KWqRYC|ML?6C_LmV-# S9~5T*0000q}HY6nEQ&Yz=#DwJEH9&>})aYKb(%4`m=~v{@R0QB(?+2^GOoS&%EG*-|D5OISks z!5&I4C3|fat?u7+ntSf--MJ5EXYbv;eYrE>o^xjAoZp?tnKQ?N0;f2|DNZp}U=K9X zTJk^4*TE7bX?+hqyUka?FU7o6Z`x~qJ-kv(|A8Om?;?2Y1TesSdjnMBZ_v)wPn-V$ zYl!3>p)T2F-VgH(fIgIk2{mdLfFaf9t3g?aQ1jAUJmHD6GMQIgs0nRuNbif7c z<~NfyX92{h+y~2qt6g>6I(San7c8F?7*v^0!VRV66XDu8BS*1+oi+3OU_}F?gdzD9zd_od=g5P*4u;|u%m`qbS%PN5uYgOVqo192c!JzUG4mUAN6x5@q#(c%d# zH7K2++*RbN!~g^Z%uBUhbwh5_?SVxi{Vl_UR_t?33_wT_z(;x7;UIF`6_>SxQjbtx z&VcL?1JET1APKvf87oD?u?#rP}F z#y2W$!9Gp7bj6PqS1bpsg&HlpP)k(h6N34>j1oE>-DxjCxChAj^LHJ8*Xfuc(xueP z887F!_QuQ{kc`1N`nJpg$(%t3p$9HgI)KX&GZ&D#VAueFHYA(m>S)F>*UZL*KKPn3 zD2;M8L~umC`KZ{)Go-_E>Ds-+8STc%ATA$Ivk)RW0H>6;7yO8qI-wzJFC+ajsGnvn zoU!+^a6%k44I1~PewO;=%Op3uka1UXSTYj>))lDv1=&3MuNWOY50 rte(oj3AdE4-{So9!YNL1ina0&1tx*nHE;>x00000NkvXXu0mjftL(+& literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..dc2af735944442fb0fdd18a615c94d0da4f4b7e4 GIT binary patch literal 1031 zcmV+i1o-=jP)4w4AXZh9D7$5aJaSj5LA}kPsC}AVEbD5RU><{#s|= z9k8{5@|*td2TD8yfTsDkqh$03`;E1DseLZ`?LZNo`9j}^JYpZN z#=9JVFbLe2EB~6H@ejm&w9)5)vBQ{}U<$P|a)8O(c@p|-W@7qfm_%k=kgc-n4UcqdaYudFX?#6(t8#i zg{ycy`D*}>pMvZYdQ19+d#ABr%A9`X9N z7xPm#0elz5F5F?G=w&%%wMtncdCTVG^&BMXw`T#vHUXR@8@R++T%aLmo~lpQ`v~|; z9M)WiHIM88cw!eoqFV~n%6&*yD4S&oKqml5fkLim?+!EM9H@Y$tYlM)4@p>cm%T^oVEr?fjWAHe;vbon&KZFD zD5$0pc8s~`hNy~ax`IZ$W7*m8l*%?>4SwKaAdX;C1b?-7dHYjq0n;<>$4S=k6?K+l4t-1|2>AC~Ca)!$}W50Tk6Otve zOjb=JfG#1CgO6&q5s%_g@trzhmM8LOr6MwG0zsFO;Wf*GwhQ(=&bJ^7;-9A`rSp~m zpr$}5|Dwz!GVmOiUAt(~VtX-U1{ujc^Xv+w^#D+>0$`P=UZz=f7$i31tS&aFL0g)M zw?Yn|dUb{%WLX<$a~G%RrbS(}!yK}y+LQJ|XmHxQCe?shlgg?)P-rT~?~QFbG4=MUQ5EEtAT=ttJZ{y}9RE<` zkV65l2_aRpdc6Q1o`1VO*4}=}|L28IKKbN~{sHk>s=JhWz`+0j002ovPDHLkV1iLe B-NXO@ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..36a1d13a1f364574545e3994e774900a3d101b44 GIT binary patch literal 513 zcmV+c0{;DpP)N}671%~$mO`}Y2V8amfmaZ%S{NC$m+At_Xj+OQE(*PX0Rsh@@?6u? zB2%-OshK%5MV$9to{#fBGjqO~Pc=V{MQc_Y<`h$7{)7=cNlyMxc00000NkvXXu0mjf DsP*2{ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..eac24f7aa53b1dc0c1c960415d547e7d608ec8b5 GIT binary patch literal 548 zcmV+<0^9wGP)8QGjKHFju(Pr;t};qP)-ma5xA0&kO2B?UBqe$ZTZ-? z8nf@6+THAXGrO~$lraej2?+@aiT?skfdnKp3TmgBcwLcd+JV0BdYV*g!5xgZXmHW4a1$F9Xol)j$no_Mzozpq1GV)Buaniy5o44BZ-ah<^y@!Y7||h;LsD4t{9O%*XJ~H3N4n zCY*LR+9pS-ZdtVPE&I%(KJ5D%2H4pVmKW#qr?P(N%fVa6z$^>KDLi1GCj6_|hX>l;Y#>8h@#_d+#53Z?K0i%`KDHJ}Y`YG92&74eEvqy%apb4&$(2aH^6 zWyIUmLNE<#%*)6y=A(JylYI69`3(bZSr+aEujtD-b%YFbF5^Cx=R zwU;7@mliUg>E~hB&2BPjl63+W=6R35efN1b^JZp*FfuYSGBPqUI$vUznKStevy-yU z@|<<*)UYX&?c?*5v8hq#JyFj`k z^bT5$#SmZYiSU&hy7$63OH^pn!r^ZJc#)ulLyI;QmWWU0JcwbD8~k$O1f^*oWLK!J?54ai^FBFn6@O_M{uv(FZ9cuKL?i2EFHNRw?=S*G~UqezNJ zIGifC8HwaKRUU9P&{5)7fS=;y+8!WT1K1WQSKI{r}0!9)7|ePq;Sz#$yk) z4pxPmUgt%AD7YQIK@GD{b2OH{L~_6_ulu!V`OyMQF&C}8 r{tF}rJTYaCbR#1pBO@cD|4@GbLA5F^kslN{00000NkvXXu0mjfHj@{Q literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..69b4f65e3b1ecb76c753f33dbb75d4a8dacf8bf6 GIT binary patch literal 658 zcmV;D0&V??P)jU);*)dwH@Xwo<8H3&8~k%&Tz#HdXgV@%qV zzt%mEpT|gLx~EHb9fR=k1jOhmAJE<`oF#7-}u3bA1xVpJWlcnP(QyFz!*scIu4wih2Hki3E%SkRWe sMK}LeLYcrX$dN7~At50lA@LvL4>x%m5Un6NhyVZp07*qoM6N<$f}#U3T>t<8 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c623d7e907cfaa97b2c81eabda7eafbe000b7c8e GIT binary patch literal 265 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I0wfs{c7_7U3!W~HAsP4HUO&sl7%0>Baq=6F zuIL1*rG?t3{33&Nrc7ylqNbxNZ#uJxS?5TRNwIJL)Tx(d@O8heJ@+mCeSURMpqJ*- zkVPvtKJuIM;cjH$cFXsLJ~scV7ttzn){3}+F+VlD2wbVmz z#T(eOe^qdTzph6;n%lc9m?|}qvE|jyi-1)H-8IL!R0OgSsbd( zc|i_9fDg|bl>19pMy>OG@g=!|Wz)sKRgOoe26-J_slj~H>w~nz2|IJ?najDYymc)< zyxrYk%o|^C6A;+{gTe~DWM4fovL%_ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c3ac36677116c41b32730903c86c8ba32d99a00d GIT binary patch literal 248 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I0wfs{c7_7U{hlt4AsP4HUN_`xGLUF{C~i8( zkw0NaV}&^1;`YX~3y)uzyy4*i7Cs9BQ#PkMPidKb+m(HPFL}$Pv2;q%sj#NS%Z&S^ z+#ePHww+q}`TB|)AwB#2`xhVnVSKllVQcEk6D$=QX0n>E*%$Szlv|;Jfr$e|++cXU zlyTxcAnT9%f~5*iJshQHS^jyiZMm_w{(j*ynVF&-1**z&XF5;rXk{~bBBPeR?1@Pt t!~J%>Z$Vvhg- literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2a757a64d7aa7f42098b63d58d6fa627a9288e8a GIT binary patch literal 284 zcmV+%0ptFOP)DHsp}3xa1bV3{+$G!{t{nPjq~000093F zc~@&gwE6R$r!2xSPUi*xMb4~v6S?5wndv15)g@W> zU32`A>cjkryBNZ^MES9>J}UX}EuXRR;dNG* vws>&?$$#DrWlo3dA6aUNRaXODtNfU)|3rw;^S9+$KpumqtDnm{r-UW|)>n86 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5def9100d3c2ed26519eba1511da3f2bee25a2e7 GIT binary patch literal 405 zcmV;G0c!qR+e&E2}IWx1loT4a-qA0O2%@H-GBn8?;T(U!zaVY_V!SKp1B}oB; zA)>=NGjak3LysHklqg6E*!9dl6-fcRZrSoYkO2aQPeKAJ-Uc#Iz^*4+EO^wmRftK(7?#o>zEd-6C78RutZCn)uLQ*N{E+{H0GUwB_P;jI8 z=gl-cx@W$?fje_%-g6v55ClOGMv__N%UqG876qt<9`3M@v=g!=prD}m-94tHMFA)% z^teOYykt*6L2;Mw`H+%M#F9XN(F>ieuUZrstOx8G9>@d%#oyHQ${Q=j1~N%tu%79H z5=M^clmh*I(KL{(Sp=XABx^x7T=&{|!_T;6;gL#i$_bmxX2e}a z+TF|*QN=a2@Hr}3e4dG06+lIsz;Faq!U<|zM;ou>M?g*9^FM;&CanC;Ce&~YJwM0L zdWd|bct^v@587qaqXr#VyN+!>NbEhy`gyVUD7E)2_nDXAewyjZOVB>g_2(r(zi0$O d5Cq{L@e4VZ4(4LtnNk1%002ovPDHLkV1gRu!`c7< literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e08f62420eca81680b424add577e3b8ca34e7693 GIT binary patch literal 237 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I0wfs{c7_7UEuJopAsP4HPCv-oY#_jr`|+tG z`=6lc)(d&H7Ic|BXwSa&M|e)c>zbeoXG9MeXfII#>3!kw^pAi810##Tj@1o|CKbJ1 zc=fbbR~T3EW~Z=AInjyP0Vi&qxwkKw%+iXev0p2li~`=##@t?FZF8@mba4k`f{K5z~frQhcY1DdJZ4|C@?T`I505PYX?kx zKA|e(mBobesgoSbP0l+XkKPWxbd literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2b2e796d1c29c44258fc3e3d997f7d59e09a1b82 GIT binary patch literal 252 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I0wfs{c7_7U!=5gVAsP4H-Z11k zC#Y4%A&7hDv`P0wJ_>wma#Bz?XzKOdWqbKE)5SHDQkDPY^BKfj?mnimbV^XxIl+k& z`Omd2{r^?dRJZ@&&duNV?t9So*Fsf$e#7AtRo>^%NjI`S4{TVv{0pl`h75z==O-?5 zLJ1qU*Td^k^MI3 yjr#KEKYw>QXtLjb$A@;36Zch33G!MQ`JHLO^DU1G{Y^PQzVdYSb6Mw<&;$Udd}i4I literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..fc83844aa6fba1e33decf1ba1015b64f27120da1 GIT binary patch literal 266 zcmV+l0rmcgP)7Ro88k2WA+4*@{z1orAnr(qGoI@bh}Et~}wK2iiX<)McUL;-?1y003AuFA6TchN$01 QXaE2J07*qoM6N<$f^oKPoB#j- literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..da58df5c0e42fefe21021e45573c21cc4730bbbc GIT binary patch literal 398 zcmV;90df9`P)Y5Qa}9TG#|ZP*D7cq!7}@(oV1vOlM_dX_4~$2dOO-0yZ{Q2@)h?^ekeboUp&? zvCsr>FPB?$yBCmorkq>emz&wy@jO8g1VIo4K}dxP=QQaN^1&n5)Y)P-KXIRDlok*8 zRPvBNq!SY#M31Ul@p(=Y#lIuuj2U+ZYP_WwP`prcCBI2KRbHV8D7rClX(X>u_{`at zU*&y7#k+L)9MOyil#Qaqf_eRQnJ!1{u)ztfSgucC6fH_-@&y{2M3d!Vc7+?`^633i zHZ!oqt#&lh>+HKWu*SXii+3aWMb`$_wfUF3fvJ~Z+D(}G8om<7qyqxlhSxCjAgad3 z_+JN6woy!xyXZGLKSovzcpk^}Cwq=<8JKt>v1GttCEH6lWzWEvrSu|sDYa_g*INGc svzA{`XM5aQZry+&2!bF8g7DAz0!;zgUzBnKKL7v#07*qoM6N<$f|il5;s5{u literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..71834fa33f6b7fee2f134dd3ad9fe7913b618c21 GIT binary patch literal 416 zcmV;R0bl-!P)G6RF!=&xm+zN(Kjtw;5ClOG1VIo}+26)9Y(f_Ue1}K45{=z0HkXn(FQYG>;ZqKL zK%bXWkas<}SH(xsaEz_USs!~^(Gqz* z{dqW7B%1E>s5(~QS~opvy?8|}{lgsIC_hWJ4&1T^+7a&>cX@f+f4Q02w2CL=1W2+CAWo(?9Q)M*w8|?cD-OMoQ;}b-;pNyMQH8W6Xt1 zz?NZS%)sp;oN-bc5ioHJD7pW+XcF+i&hu&?hC=n*a+_@VyqoRu-X;kB^U!yL$E%tKvEBH6ab50 zBywNq&%kWhYZL(H%&GhM9g`VY2T=D32S%F^U#+u454X!FW&w)u{%g@J!$0ti;W51UHF|c`IBSe4crCVx8nZ@uvS8N6MUHb1i&XHfU)2b{!0dY`NwJL=_>=5;3Bvo#^?lywx691z>`Y9EL?Y4niU7+*h_Fmx)X}S?$a6)7S6-=bMV=I^ zqlgZX<`K8gcpy#a|H6YLxu@AT-o>TK9Z7=!5kE)nk8mF%$DHl>C0@EB-YAeE#ynF@ zF;9#P1>U+Usw~=!-{jV7UE`GP-j_^}ZBD6qb=Pd#n|POA+h=x}>{~g>F3(==rKJh! z#ed^4JHYPX@ZCYM5x>KiYIn}kfb->CIlkC2jSq99nw41_*tJGkxo!xX0c2F8Pu7R> zZSbj_X_NRFimJ`U(B2E>EHa}H5Le5pL1Jicf`(eQaeaUj^%Q4X?#??nHeY~;TJ9s` z#prD+ds^=O&gP{L&`=+jEiLz!`gqm#0oq$oov4;OO1-@u=>xb{09*kWdw`x{t6l&E zKmY__3;;f?0KA)@h|B;CPDEK+#sDp{`Tz@*i~&k4nEaZ=crpTbB4+w`IL!eLRqFoe zTiaDb6^8?+ZC%K%5cS6|M-YFkjQsLWBoc{4B9Wec0CfeVZOrdIsQ>@~07*qoM6N<$ Ef{%6Y*Z=?k literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..39e4a6f81be2736dab95e95a3e65dc4be77f928f GIT binary patch literal 563 zcmV-30?hr1P)M{JEH-r^%%-~rEg#se<+h_|pb z0_%8`D!64QVBAn89vzyw87UC1xf5vbu)`f(708SX!9F#^hkuy+5PX=LQEYF=@YJ`! z8`bS9CChV0$Ox6^C}~%zj<+8XEjy;r-iqVWpZtZ7+5BB(Gr}ex^NSzXwBpJR9?txg zJr;AuL}2JdF^5M#_Ds=%^zFaU!1UkU0S*4@A*d*S23>bLyY|0*wPSY81L#^A*={#m zY`T-ty|wah=oPwmvx?2^tla6ZdTaT}tqugfh7S+CBI7@Wy0`zeK~wqEL$4cC;_FX& z*XlLDF=%dngJ0ciYo~+(_`o}UCqmi1qqr}B0e3~RX;1nx_L>SFUFYzsKcd=uo zg*r&ueR!cQUV*MVK`9}UT}q(swj=QhI4S_x(E#HP@O!9=*8mzo189IT0C-md@GkvE zBnKezMpW40PicV9!j55^I^KqcTxa%SeN}eouq9v&_cUE0&#!DpoIo) z8KD^4kv`>CONE002ovPDHLkV1k>Y B3bOzJ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..8b5f3f1bea2d8458597532e2b8788dece55baf84 GIT binary patch literal 531 zcmV+u0_^>XP)U=-%d7g95drni6mX?;*{}SzuH1A@W9sG^3ZexK%-cfw9Ds4W@ z3Magy%8`<@ZYIb&g^IO8Q6Nr>gn19kWcjMHHaKPm-|t)dm|&ZWD)UdSSSKQ3-pV`& zyqYFG#Tva5^&TfyQ|~+*gkbAa}b{R<$j_fPF;CC6n|8%9LrKP3iPTw8X V!~XAAz8L@j002ovPDHLkV1iYA@TLF& literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ca304073d37577b31453f61cd2d9b98d5ac3668c GIT binary patch literal 580 zcmV-K0=xZ*P)CIoisCXJ(>qr13)h*+?G&cz)Fbx{ zuK2jSK!Xh|)~G!z;(nx?!|6jJ-UZ?CI<-qD79?W7PMwIsJncBw4dkDchYiPIAdmD| zOL&B@O-{eWbr@!d_re_PHOHGC>`gnx5^q|u59y_Wx(p|n*2fsqZEg@&>9#T1sEo&G zPW5rzJc;M^`VMMTa?U6jfZm!BH?M?abYo9T3lJ$+3D00py4vx=)Bb@)dJb;l?T>1M zrhd7x8i$-G;MSa7oCt2_Y&DROlbn(@0n#sqbDM6vixVRA zy~0xhLJ|;KCsYE$5J08Ij}le^^nlxF+9^sB;ubw}3n*B6Q@5LhCr$ybInvT_CuP0R zj(~l?=eWfs;LQTxp@Uok?pg!j4INFcB7(Q#d-3(}yAxB1#{;fo_E9RP zreTibjQnn>Q9iWgG!I&Ur2;g_P!@9SnK&U6h`yE!db~a3_;sbAprD}87QO*s`8UZ- SzMe1u0000B!3=PY}=#%%&%9;ABh;UVvD}bO@ip0fF>0@ zR;jYXE|)}f@$6-P6O)hW;J=u$LE9evdP4jYYQ>-UgP6WYNqg1%oD`uUogZ;xK>vY& zK`vY1P_iGJh|fL`xwIu^v7LyHLH7H(9ZXsh>ilKrFQwn(A?})chM81gAb|oS@#pO+ zeIY0@k}yp}@$+#=d_oCQF%~~L(iVbErV_$SA%40-;L#;o8t6AYtr$xtPW&NKZ^s*7eZ5frIez|g{*32 zbHQXGOzy#steRzhs7fIW#8-+MW7+gk9(SR~fG-BwmpGREZ*zJh^Ulb+dvVu%FaG6J z<2a^2<)bYck2V_%I={m>ZrN)-*`x0gamhD!sIrPjg(d+P3D>yJc>Gd0WOAHZ%U&tA zxq6lSmLJw?ukkrOviv!n`=0kXBV@{ikWqo(^TamR@Y$q}pZdmgaBy&NaB%n+@CTN! V961()3{n69002ovPDHLkV1kNy5R3o- literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..8defd5d154bf5c83ba0ea37c8e52d907b8ea9270 GIT binary patch literal 636 zcmV-?0)zdDP)d98X6iJ|1WALSXli24J_Wtn7WiBhnv_%pZhdGA0MGX zPAQiO`PngbXhiQT#t3emqg2T+cA?ELE6L^H;QTN(dOYA?pOMo$h13$;DbWM)a7M&W z;LH=r49>;^RFRGOu`OPw30_nyI{O zDdZF;B}k zf35h}^ftAoS8BylftzwGFOv8R_7o!#H50pX8#|)-$v7lf=u0W{QN>^A5t}B$*{8vH z3zg+d;%}G;qf(`CZNK|nuB+@JXZNwViSh=u_;ocsU8$H6SkgOu)!O-pZECGjLDh=M zmIvrYuzsH#JePO^e3>INe*m z`QTzOxw80-2kFtf6jF~oH~~wT!r(mFiR^`CQTMfaUCC5vQUPO0000LWGL)kY87R(3>Q?BR~=t}#}MVmi;n>icNl&%Hpq O89ZJ6T-G@yGywp!F^7Ht literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..de35e04b951f83486680030ece8de6f6dd5b2f79 GIT binary patch literal 268 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+ru!ECYN(T!AzMXlrZh=;)Z5ni?A$ z8yOiH8X5u_5CCKYMSyG|31_IX1sei&3YP@=1v4-*@rcXHsd&a`7T4DI_Dz{St9YNw z2B37lr;B4q#jUqzt|l@Ea4}b z?J1_p221sMmoQuW3NoCt;8N&U*{0_O;SAyXnRdM3;#8U7z{F{1G^6Aq%Q_$N)CXL> ndKrv2oaU7zJ!5$9!|{<{_OC^9^~=1SAU}D!`njxgN@xNAm%31A literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..19486f33b68f41a9bca8aadec0d3fdb9d1844156 GIT binary patch literal 350 zcmV-k0iphhP)b4hC5-Ll!S!E(Ff5Kcb=hdp@U9@5kf*5>_D16)8{}3NUOHo>*>J% zo%PQ>U++n8dfVv;A%qa(R~QlVMc$|~B6^|Pe>QA>sDy~C5CZX8?GL-~6hNWa{ERJ| zS`~T_#Zp@y8P=-MgD4i-Fl>|$HwbS5-$4Wr$VrIW70Aij7RV*wE8txckbngKPhiKA zCA)S6_RN_uV8DbqduxF=+)2{$ZYi+hzLb4nWhW5PEoHmB+6i1&rf=*7`jzSa1qryy zw6haK# w72E%yIIa&yzQ1VFvzw>HUhRbtLWqmt8|$+#I=I?I@&Et;07*qoM6N<$g6Jci9RL6T literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..abc05ef675aa7e4f0bcc17e3698dcc2055ba1c2b GIT binary patch literal 376 zcmV-;0f+vHP)vu< z+SBfu2;PWtK=+W&r`JUAMwAcQ&8J=R12)22z_$?r1kR1PRG`w8UjKjL&~{bc2R{GMM*X6zRfl~~p%Y}YHA{O000WnSe4K@bGtA@~NG W7#uY8N+_QI0000V38u7)s1jK;a{}62 z;j>Ae#At#~vce8Gh|qIKl>*bI!$0FA?Y`iTPm2muCd0qP|0iIFCf6ZB`=rgC-seGv zxn+wYE{iO&Op&iNm4sd9%+$WDLj0!Cx9V~#S0DIfLcoej^%qhD8MD-s#H{|o+fk=a zMY0yym#lBh=oR3~!MhD7Kg4?@1>BR;N6@_Gd%sLGR_G8@TCb0}F=?bd_> zEw%YVMEJU7w8@z8ft4Q9@p?e=57m0zj3Jp)cb!16M~N)+ydXn~A3eV4oE*=MSX9#N zDo-Q`xZ#B39xo&iTp~E*2#=%*33Ggow4{PIb>49%4XS6#8@hbwzAt!(DjCNi#gY2W zOcor68FH*s!>2)m1GXuX?+zXbJT)&Z)(lBdZJ8n!o_LaYJzol*GY<4*!CWDp6p&9I2frIMH68D z+8&rie!6Y51DE;sxRZI`y!XC0-!5YY1_lNO1_pl(T5(dMh$@=sQAgaP%1_|1(0rmV zyZcn8N64|~nU@H(qB;6z1YfB$5nMgg7DX1;7~c7S>yeOMYT;p&5LK+0zSCc$r@}np zjAxxxCZ37AJM@Krr`CYPrM;%5_9zTa`HL|A*50LC^}dHw4e{N5FV*zTicZx=1SO9m zPX;bLDto7&9-cAA_BN=ZDE!!34YvJKec{p5MDj<8ZYu&WNC!$l6xGIOek*aMif|Mx zftuPj>$@_ilu`J1M`FJgKH+HPDfy8nBg#`(|LB8jz5pKD$}}M+JQeJ}iuY zxQB$Kj=STx(oXbeBGm%Dm{9$PCX|qn;ylnM)zN`YGw0=2!(_co@7rwc{0V<$BDz`;0{RH5z3_a5~G9LQ;n=#7Erq4XT^hWld9}jF*YlWZ5(EG+H@ASqE>WV@!7DOfBjO>RtO&pJII2@LcE1+7>DyK z#0(0i5aWg52Mfr-P$7DB%$62ch~B5q^ZJ1?B@6t3ju#eQVTl46F$kc=hEM_Xz}gocTHk?0u>L*eNv-DiWDVk ze0s=oYkC4j+G94=A}Q^cHRizD6sIQ;qu_c6Ec@-o#B#j{1!8{7Zqsq|lP;=pI*4YV zGR-0f^ra(lJZNgzGb>2zF$7KURrikTrEN}wjQW0x(In-#%NuW!@>P{%r4N$ad_KtxgDkI+K1vOP<4B+74vopSXJfLdv@tOZ z^4w!h*qR-&{MVXQuoSdrGtivQKzlxm1B%E;G?5Rf;+#i1ri*+~siC2vp`j8#r?JyQ T&%G1900000NkvXXu0mjfTL}H} literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..0cfd4270bfe936e2818dc2ecb2d7a8064edde386 GIT binary patch literal 624 zcmV-$0+0QPP)8L~-w# z+^u&Jb_RsNcr!cm%{M!%X-bqRQKIC33eD(wQE4VY_j(o&;96AL*RzQQ0A=dqyQtKM za$*56Mz5{_UKvX)0NixCAL(XtCFpns0G&UBoGSyTo*6g||IY|wuFw@6vU+*$2Y^j{ z@C@JqclFYjRe7$q4o7rluEccTDA2{|<3sBVHyQ;byzzAYtv5B`RvX-UID+OyqY#g9 znj+EzoqJkQGlu)xf^|EN%OJIGYYWlOe;n8SC1OGxj7?MX-H}n0m)&MR0YEVnE66MX<`bexw{gx+Peig>4rhjSWIhK=DB6*M{E2 zYUbp&Atxe~LPQvih{C8?%z~MdPQ|S5A|U55YNM%_^b&_&J9rpzLYjOSjWdq%1$xjE zkuPl=%e;`VrT=*)`@EFWU8Ud!E!30Pr^36j*sMAOz*>2loFjGC4C@70Xe z;ZVYAHU-P+6s+e{_&|~R6HV%mRC*TeRG;Zmf2dTVM2Qk5GUWsOL*8=-ok-9C0000< KMNUMnLSTaDUmeE) literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6aac13fc0347dcd4064523552a14f7c93b4b9f97 GIT binary patch literal 568 zcmV-80>}M{P)6vkiBSRo4H7b+FBMbS~AAczG)aO)uGB4#i+h=U)XgM%MvLDW}G9aMCX7=pAq z*=|Cui9=%U-}G~lw>Rd-?Vh0A=Z+bk-zON1Tb#MvQ2 zt+ahBF_d;)Dim2Fw#}Ou=f-HRQ2ekQL;AJsXNhs1p8&dTn)hgZFT)X^JdZL)@VVv+ zpApLKnKv`f@4)_?QEGzUD)~qYmG;cb7$W&UW~^DPGgVBJAVrD`rU+Bc7k+A0U#87w zmAly|7ue<4Wrf?o{FrZ4*tD-=zi7PDOj2h%;nUjJ>*pVHG)VNfgwg(5Sy>l|E6q%3#mGN7 zmaON*^E{6(w#gb1?)WNoEjx<*4_k7{NE!hWr3OgK0c;upHsk;X4bRiR49Wq7SStWn zvG_VNAXGD>#WBka)zUK}#4wBa_$)FkG7+4doSdBAxSjw!w}b4T*Qe6}0000hBeSf_J2@0Oga8F~M$9lvX zewQRAnRBah$ac;}@rPG++=;!?neGuhMYm4p(uvrm+Zs825pUDBM*6WHN#Dj^Sb~*> ze$5V-m+vaG?G8SOL56&TYbE_X%+iY>r`to&U&JIzrHiomgaoTx&p z2o6(QI!1Y`_yl}Z3OHaB;6UqoN<|0w;?c&vW&vVX<95`Fa4)Sl z*s((E;a=_`VT&Ye$+4YYQhk#2?q1gzRP?Uf%#t?8!f7fV4fv^|>hiy)w)`-GR zIRf@Z6tt2gU}Hp?WO^e63Dg=<{hHngIRf=Y+|mb^P?SI^P|VDV*g}>-CiNcQ(wd2M zVR8tFv+(V*gdB<4V|h|u3(Ex67lARvf3!&b5An03Igj!d*b7>znO?!rbCGD z@O~#1hb)IFMjJjW#mFNXtQ;G|b50>C#~!bo2W&`;VZ+z2h9u%Vd`tFY#5hf9!H99% za%0$tw&kq{6OU-j(JVRnvKF)&PNPzIp)+7No+mwUdBvT_bL$6+Da)Q7C=?2XLZMLf Zi*Mj*NDco+F24W(002ovPDHLkV1g7pyC47n literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f93ed76c0da7755bd25e7392d0a6d9c98ee3f940 GIT binary patch literal 470 zcmV;{0V)28P)H%MR!NJt7<@j|l^ zG80O|Py~p>wk3sjBlJi>0@g;ze_v_^OTfkmUHmMJ)QJEjU@)TgRGkP&0`fO5A4gtMP0O+ZGC`#gTh<){qHwXOc#UwA{W?{QhAf{t&P)Q2k&W@d=9G zBECDS_}mkz&D7MB_;UMWlO3^3rbCGDgm29~B1rtjAem1o|F~_|-i+;sYscj2xuo#> z)SK~Feg-69E<2QpuRkN$o}U3nGP|;)88OmOsv7}E;J%tKRRA5!@2vTG=8t$aQ<)EJ z!S!-#({NShgXchgJvWQ5X5iXNJ73SOKTu5lMNc0n5C{YUfk5<&AFP%+=YXzPUH||9 M07*qoM6N<$f_lWxH~;_u literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..44e6a3f9790df9de82932715bfe1e36102eef2b6 GIT binary patch literal 1591 zcmZ8idpJ~S7$05SNhy)s5>3{1&M+*Ma&ICTltJXOkw%u#s8BG}WoZ*jlpXZ$Kd*9#tyS&eH{y535E)H_ihooUJn4FWN zts8hdLa*db@C*QKqG2$xN!N4kc3=zJ+=SiVv~Jl1dV9Ys8$L>7T9mO28)hu4=TNi- zpmE%|>b*rh%dC&(D6*xARsuc27ObHZk>M6!!~~b$a)YwInTkBYP{?P?fBS@55iT;> z21Vxop>0n=rbzxjlW(=()EWr*9QcU~Zo0W(U}8IV?G~4il97|&qp(kL{{f|gDynMg znp(QYj_c_g7#bk}q`8IF3F|X9ws!UoPG`~QoLyWmU_3mr-afvU{rs;4g@%Pk-HeXA zeJ25*n3Vhr;c-f8T6#uiRxT;;89Be8u=sgtS$RceRdr46i^i7LS8W}C_Vf=74!wCx zA7P9!-;Gba|H$G@P0xH<;Qq6?^m&=bU-=^Vy1KS5+>pY+LtwC-SDkE6yWbk?VC;-S zqLp_~Dl%!Om^2)d7Ql?@^J})We1joh<}XO+yGf6Z=W??p4L%1P&3bL3-tOjz3-s1| z{9MN-lFW{FOsAfTxyV0MFKzEgRBEM`#FcKKbL+EUooMXkS7{Nr2M$HE`d#%XZJRf=wjJ$A91?r;G7n+xfI8p z#hXY6&K2|xuE>*KVsdna>q0fsCuJW4o-Nj(wUf7>^Xl+@3pX z>37{7*pqI`am_|zS>;dtN@vI9PcY%R8*vC31^_QDhtDL3KffN4%5S=x0W_85W$mMg=+(`z8jkS!F&}fto-C!%_-HUdF?jsz$ zP^hdXHy*DV6Z`0|O#7q+`WIB+Ge+(5WP?MuWZImHmin5S7Tvu>S=RJ;W?0tGy$Fr< ztULRMm5Bvi?Mvb*9~ifMP?t0*Bz=Xx^m6$>hJK^_h9>Et1McgfKSm36WMVw3Jm@F2 rcSlrIOf>Ka@a18~C!;yuNaChgF_#=>#PWO#{Q;cpTx=`P_&@j;xLiQ| literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f560d2c8ed8b5fa4316dc3b9e901ca0731a8eae7 GIT binary patch literal 1172 zcmeAS@N?(olHy`uVBq!ia0vp^D?pfo8Av)$4Q*p!V6+bK32_B-85;hFuKI6$=8X2L ztHzt3>Fs=By!)>1mWP%*9vg0Z4&<6{ePz7kf#JqS`kNn{?tE&n`LXfV$41*98*YAL zxb+#3ZL;~X;r1s++n$hu|N<}O;ie8tLDYu0Yr zb@1@9<0ns@Id|#u^;@^^+`Iqa;iIR|p1*wm>C4w|-+%o2{pa7Rqr!a*3``3=T^vIy zZoR#gnj{^~)0Wtp$di}`1a8KgZEyW&dzbrrb}NbH#Qkg zDnG^5=Q+px7}p}XNoHrf+B|PbX-6t&K40@>&Jx>AliVkXC94`vnwF}XIq6uI&!l5r z_f9*{SIjI76FPi*@)c=2JEy1nr<|xidssYu=6t0eh3>^UH)Yra7Aec}ocz)+`;>jB zM6b4vbWFtkjEIhx@rJuLY}s1K`_C%qvmGC|&6jBv&#(BKwn!$c+sp5l>6iSXR=IDk z@KRsnw|^`JK|#l{qO6?v>^k`vyB|U;g^D$&ez1*^TXUYTbm!{(KFO~S9Qx{Zv7AG6N#>|ge7=Hlft({;a>m|VW=a;WFv;RPqB%{+M6 zvH8fAuW#8SA`{kc?^D^LdslR)($U>Z&59yUf6=}DsBBm3oX3%Tn{y9&=M}Y8Up&6; zlkScab!UYcx6Q3zH1kcU@t-q&6>{qorA>3iZz>*L>$b`Fxv2GvRg>QY?wy(L{?vT+ z3AU};YSw3BR&HHby!Aw%_PVf`sQs}?j7#5EtvWO3q|zD7RUYnhT94dX$+R_H=h>QR zS9~`v@C>^4;Y5?1v`6UvlTBypg0>!BROWHjwtJUyD5uy|g-}besmh@`JPWx5v=|n0 kxN0~Fb_Fnkf(Z=%Gt2ETO6fhb<|Byf>FVdQ&MBb@0Oh$uHUIzs literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_toggle_played.png b/app/src/main/res/drawable-xhdpi/ic_toggle_played.png new file mode 100644 index 0000000000000000000000000000000000000000..e3c26fc8520e667c28603dc920ff6430ca725906 GIT binary patch literal 620 zcmV-y0+aoTP)Ud$nd^p+x6PtB>392%2t4{jn0A?~m3 zA#MroL|lEg72fxbZ+5>oZ@!r~V;CA58X6iJ8X8-N9XpRBI_FFFd4dQLPuS;6I>(Wn zH+D8jopG#Mi+Xg2NM3YDy;{q$jLZhu?Y&xyZ(gH*x%j5mviFpd7qWExeBH|~p3re2 ztBBq$;({LqnkOD(gb|PNBpl%p7j97jJsq$wkcTwz8r70xIaamgHE4iPID}6JYtr*a zen|XyO5OVYyk&i#x|k+x@4dzv^!R{Z39rW7HcZjnAn84~7bIDR z+rljblv^e3@_a31dN=mIkaat_LWmGoZs(8UP3j6-xVH0Y`AzRkl3pBpK1Bb8uT%8- zFxG-3M>;d!q)RlaAyoP_T92q!NDPKf9g z_=J^@dIh$N1h#d?x05Rp$kDD(U&fetpPbnfs8o?bQliA0%OgBhDHCq<4KH~&IE=dTifPQTm#zoSI0t76jOr34$v zh{k7V^x6et?1;th5L0PHZ&~{D?)5{SCEloa!$}@}#XmCXw;_*ltsH;Nn8cl+T9)q1 zuB<`}N1o%4Xpy`byF3eY$-V-q+|ttc^3F78Q;W%XMqQrhu@Q;-Y^VPb4jtWK#Gqmsi1m=FX%-Sh8tp|<8G(>QYA_$lR-)}gL<>YH zwt;C27C{yf>LBFup`sexe|?zS^}W&N&GLFfG5=5VuIHTp-#h1>bM74>f(YW@27?$B zSOI(36G-|p!1WVdz`mE8<;r+^KrFuOgB6Rt8Q!(0J28Mf5W^6_6eFY-o)%{8>l zk(TfLeEoDm<7t&)SL)W|ILBF+$zZ9pY-gl-0#>RqBwkbNGpUzdF0(T;7_1u58uJ9? z%8;+s$|RoYI(=#9n8i!EcG5fnr81;V2q758B~5|uEV>t*`$N85d2SYce^uSm2xq&d z)ViX9ZG^K{J~c7G903beUrSX-5GQKMAcI=nS_!LF4_$1IfNYi5&ALI`=Irte#HRa8 zllp>=nIoX6o6Rm}oX%z(#obt$ae+rKzPN<7liqc-CX96KE|%ZCo1?lHFx%xol25Fg zF|I(BEGOoeaUI=>7bF|nC-Xw%vDbeAzfU|>toLj@CzSQ-4(*Qr0zLyRoc8YagMOT* z1^Dd$eJGSM%O~GilmWy26Ocld@4p8SNtS7IVGuzC@!#SXirnCqn@)qj00000NkvXX Hu0mjfO;E=^ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_toggle_star_outline_dark.png b/app/src/main/res/drawable-xhdpi/ic_toggle_star_outline_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..f6f82347e0e89c5286c5a5038134460111104898 GIT binary patch literal 674 zcmV;T0$u%yP)6{;6vrb*8p+}yQiq~c$Q05p4wewwMU#vnWOxA~Ly%ypU?Hmn2SdovLIO&MmLSqc z0+}iaWH3Y#Blk1?x`^fBQ{R2>=DiN@oDcB-!9C}H&b@bG!4E(D@WVk6phSs)cK}Ir zbdufyXbc#jc_km=0)gOyh&KRPo)8G0$a*3FiV9+s6+-R{UU)gUhrlnJ^Akm6 zvgwBW5>-=qfuPFL%Ly+NBgGmT_frAx(O4rzjAi>mId;fX>$C0y zO{MZpGLrL>RW3~o*G>+(WYtbXCa5uREI*(|!v5NkcGwdg(hgS@H13?#y>kn;-xD%* zKj$DaHhMz7?&tJGLrT73tL|qa6r=zO>etY!6hKw+ib|X-DS#{DY5}$-Eoq>5J>y8a z3yxIEhd7l6I8}IIE9WXq+~KPe-cvzkTZC|GYztn%OzoQ{NUN(1v&LopvU2Kk0Z{Z zp07*qo IM6N<$f=LT2zyJUM literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_toggle_star_outline_light.png b/app/src/main/res/drawable-xhdpi/ic_toggle_star_outline_light.png new file mode 100644 index 0000000000000000000000000000000000000000..ecf0d437bc94d685909a7236ffed728210e1a770 GIT binary patch literal 747 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..03708c8565c35b3771b2ac4f3cf9017e40e3b85b GIT binary patch literal 491 zcmV?}l5Xf0R;43QKTTPWJ3NF#^}79!eMC}<(5AYQtlDHN1= z34&ye^GwHL6`RDJoy;i0eBT3{GtBNkGYt(kHa0dkHa7n^G!vzt7*Se&8y#khd3HD> z&o$@lvPgo~-+}j$M0Re$vR%$EA8Nv%((lP>v#smb%MEwPB4GOrV`~`&39{3BmMg?yC1@4Rr zxPSW*3f^Q+fj{1Y5$+8N?8{7oWpf+AjEukt1w#TS^vInDQ%3L=sr7#F zv@or36RzD7%Yko4#M$Fj&(B9%@x@3lDYiM~ilRaboUqDdz$;mmH*q%u3@bE7m*yv$ hjg5_sjg5`|d;qxziX{3-Urqo3002ovPDHLkV1lD!%W(hz literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..21b6a1491377eca8d0b123a75143f4de8fc3b710 GIT binary patch literal 537 zcmV+!0_OdRP)KEWa%=HAD`eB#D?lEd zTFO+sPA!E_-ugD!#S*OnENKUdjjR6r>~G083qtjAl=pk|=T>E8o{Vv5HQr_#Yq!{_yt01d1^?xqP zK&>;N%hZ6V0E|0;u(^S%P@b7bP!?$b@6B)EQK*4(XN#~3A2d^-vk}Zsktcx$(lni5 zM_d48jlzIB33)-yIHL{H&BKro0~MF`Rj9285%o2t00000NkvXXu0mjfgmn7f literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..bb9a236740bba43c152e2fd8d99bccd0189b2e61 GIT binary patch literal 281 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I0wfs{c7_7U2c9mDAsP4HUN+=vb`WWK7%s6v zq)D;Y+~eWVZGI;tix>|clCBWAYy3AeMYwae$vVdw=e;5-^3x-X1wr7Z@VONG^C@$J zd-q>>q_JIA4*#QD? ZZVLmvv4FO#s)*cH95} literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a6b11358600cf3baa74bc88c6596b28a7372a495 GIT binary patch literal 287 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I0wfs{c7_7UXPz#OAsP4HPPXN0F%W2N)d|~l zLhM^ptV2pr1;6)A?uhQDt;-AicL)ACzN1$-vLaq9u|V#=w+uVT2nL1^{wJm@*YR=M z9(9%pa-I3bPq}JJplG|yYvxrion=$i#aiO-zg%g>r^mm*>+j=#C94cv1Xf90d%gJ7 zY36_>vosF{3O4`AtaTQ2=CpF!?Q$bhVD(qs)4%2&iC4<$s#x=6d*}Hd0^41eoHq78m`*k0Q e{e-xYp+Gf$&-L^>Hvac{K_Z^6elF{r5}E)4_;Ymt literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/main_select_tabs_dark.png b/app/src/main/res/drawable-xhdpi/main_select_tabs_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..d4dbb69f3bc9cc388073af31943350935c7b3833 GIT binary patch literal 266 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I0wfs{c7_7Ui=HlyAsP4HPBY|cc3@!f-|~gK z&c$P*+5`P{Vl_@5tjjXjvUm&LH_=ooN?&(HIPth2>vc))UO^`g#TEglJJLdvwB_d~ zZ+von@7~m;LvcxsEz>$%TRY8uu@t@e_2Yia4#lva-Ni?E9-fGokl4mq=v48^dfuKN zE<6uUzSrzEY-6uoA~5gIyd>**3*mFz8z;wCUH95G*}3J(d$l>zP30c@%G;$M)&6Oi zWvF5=k+j6F&cW@l%%uO~Ny`+iA8UK8JoKqtQ7h>03b1oH6!VUV+b$OP-`T$WEy$0a Lu6{1-oD!MDfZVWiBBp&a0BD?y>aixm0PJfnz9LI2= ZcMs#?J86QQg;|$Dd{0+Dmvv4FO#pc~bu<6~ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..11e1e4fcdaa3fcbcbbbb12580adaf17a98c9a6b2 GIT binary patch literal 802 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H0wgodS2{2-FunA2aSW-r_4dwrzhFm^;~(Ga zvT84A68z)7#ZWP;=U4)dl(PrVf^MfNVT+l9GPS2ped9Cb$dMgiOlzd)zuY02%`6hl zbz<)4*}28f&;4BT);9g!85N8CP6ieQ21X7C1}1?9Y|MN>adad2JgPf_mvmSU|iGBl&be$ zGGGsb*)b`9%a8pHNvs<(ukF0&=3vMaF-_90aMjv|B<36QURg13P>lOL>9Q<~h;T#i zhp8ve9g`4jxV!w{@)M=(3C&X?9_2g_cVkRgdTnLi7Y4HfO4486e*ZOv`G$q*vU0JV zdJJo(TWZC;?K9ZQFzvDO=O?@V@+}oQu<1;|?bRoGr4G17rZ)a-u54PucR+1r@$(0l zmWMD#v}taWusiX`(3QdLjL_0=U3XqEe4GCYXo+))?fc3+FUB`qqR+m?KUpSqplZf5+s87V)@2OWPP29M zG2- zN54;rc#%0HwaZW4)j59O@}ti6^WuA2-!d9iJsc=k2&_#{SOSukCmBJ*(S5)AHx>E&i~l)~@I9cSbH?L?4=TTiN7LhK>B9 zP1UQ`G*7uBxAg0@^t}E{T&$CyAHH(v#-}Tj&bRXjLXu8 zMCP9QbEgaF`H+$zzhDMNCT12EHg*n9ZXR9%Az@K*328Y6B{g+TJ$)k+GYeaLM`t$= zU;n_M;E>R;h{&ku`1r)6Rl@|xPZhNkAW_Rj9!2@@wznL2IyjG41oO5)Bk zFfg`yx;TbZ+dbLs{hkJa0E?FjEKgSH3NTGt47Bf$EeGSxqyAGG zF8Ot_CAhB_2-x$_k#ojQR>g#jzp|5xHiiz%u3hZVpCfZ-1cX@B0Fnj`{t{ zU@SFMn0)^~Q;c0p*w01WJ8U}EoJ!@l+28Y~bj^Q-!k?aLQ_F%s{ES@p-*Drk$7T;c zJ-?!L#k%1!V{3257iX>G$yV00{`O_%M!c-+&d+ONJOQ&X8{AN4f?HzshS8Uwv zU-v$;vh6{(!BMV?HQ-V>L$xzkInYmo*Rz38uFoX}$}?p?xj50u(HUHx3v IIVCg!05LKTeEw;ewJcM^o#xgB=QHW?YK|!U6J2@NJ6UHll2xUX~lleM3@LshN%pH+mYLq|Ig z3!8^9|1%MXJx^q2Gc$K=k9m{x=op&-@4HR<1`e|-6y^gBNY^lE5XiTf8_clr0LyKl zDp$1|6a5(!gl4{1vCNTfH-NxCBM~s5saPlZj+j-qNpL;4}%L>C= zzt)FID%_QFuYO{cq1mvaxH-3x(Pge?*>hnBlgc}{)^0q?@KV1}V?M*nYZXg>Sw%Pl zttu^h3Y4AsG$4UNabB43o23j+XN-lU9L^Lk%;wg}Wf8bP^WXQ)wbG17xcGN{*>)j} zK{2k_2Iy*wIl&Ac8&`i}XA|(g^XK-FINk~W4)n1Bed-VLY5JS6SbC{WieXJPda{{5L#VOCE&n}Dxc zE$a{a1LchN>_0p&yk}avhj-qmDhD-fr}R^?f>}nV6MoEkkl?ra&{XMV|6i{vc=(&C z^*xX1Uh@e4`?BkvZ?TrCw@WZw{+ZErzJTVRo`8$Xk8s(mNBr=-xbws2L&ZPmi0b{> xQgQJ+5{Y!O3|_T#%X z;=<>L#2l2~c6X-A+pyYAi)sV4H8iGl@z-i~y=ZP)q@>-Yk+nB;?*3~k(MuntmX)1< zmow}A>EHbiXHW3-JEy`4G#eEh;JUKsmiFuw>pj%JiGIGg<;;u~4eraPnoTY}$Z=uk zzbUcn@}~(W2=AX6x%0?B?t;g8yZi$h{Lj2nb$zMHdsX>D8s_eZzskGW8K_<6rfd(x{}cSX;}853 zc_2K0wJ;;+hR3m1xn?IgH*kqvemSxCAxFXEZ$}s{8SCzx?`u&}Fwft7IaPeaf36c> ze}C!}<=XJLdtX`3>;(+7GxGQ@W-bh9n9Z)6YgM1!%JgUXMxUgoOlz7{Yd@Z;a%9x| zSe2Y@!1U(Fx_2etGT-%c(-0KbAUpr;oBiLvJ!Rd&)HQ4Ui!B$XGHma%4`pX%{c*&r ztn)6=Nn5STD<;oj2zR(R{ogc;HB5h)uHDdAPq6$L;K1}Ix_YWv{r$^d)eg*+I(vno zymK$thw5EBPrfZ;ieYp;-}{!!t)c#jdgQbt)-}6~JKXxY>P|TErBKkkE&Z zxG!Ig67BmPr|@a^!WHk?-p&@NRyg%SdY|_N< zoZl7)hs@_$9&GX|?%|2uM;HH#S9oXdru08uQZ{8$_V?zm3m@LDA8eA4%VT7k01><)!2MZu0nEaH13 z3B0r`^xB@tUlX36d;94cv+y%z<7>{r>U)ah zW*f`T*&;W0tK2*@`T6GY3pSje^PY?Mv#Qp!?2@OE ZHP=@NMn>0qN&=G$gQu&X%Q~loCIGvP`w0L5 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f9b522e3c9667095aff9bd4497373c61f2751805 GIT binary patch literal 635 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H0wgodS2{2-Fgba;IEGZ*dOIi9J0wx0)n7Ap zhJdSzilShb&sod&x0LR>xGCIo`QrJi^-G~(Y>W6JbIFAPEgZ^=jnDns8rxCsHQDNx zWwz~S=Cq0#mYcu-UblDi8HwZ`!((vb8ONpLw?6EwZ(IH#aQ(dfmKHyiebZ_WF*%rY zn;$hzWn|fCRO%efaM}FIW^SNpNs^^}u$Y6u?4M^M>-qobGxnIxyUO0m+VEn@`lBm0 zF*JSDwp|x1B*xIRQLcOmYr{)*Ip6Qx+zcC|b(YOO#l*2}>lANphQ$;8zphKvYHYYS z?Ubq@gWKYYxZPSh3@+^6%kNuE_hfKfbLIF}R)cK@)8F)=o2SUBl|SKWWrBOgPP1ZLEJ zIwDqZynK?%+~l(9b6%hL*QlN5e|o3;Hd&y4RPefdfr_VoQQNuruou6|L2{n1elF{r G5}E+cct~vk literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..19f6b054cc88b30ff22777f6d1835848feb7a3e5 GIT binary patch literal 192 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?I3?vN&YJLDI(Ey(iS0Jset*xS>Vr*<|XlMu| zjf{-4CfHs9O0bs%`2{nuv1ipb&$7JT@fau+?CIhdQW5v|#zMgZ3L-8CJA6!xO&S(X zy5Lp!U-ihx&?JEwwV#fN6&x?0q%t?TZ2FwnC;l~Rr}>}W>Ap=Cs2>%)E?=PHsbAD~ UEmdKI;Vst01q8HOaK4? literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/media_repeat_all_dark.png b/app/src/main/res/drawable-xhdpi/media_repeat_all_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..0c8ff22e73f9c97b45fa080448bc58be9fc1eb7f GIT binary patch literal 1215 zcmV;w1VHAxnxTttq(O#P2k0(&5_gx zL_(WT;zPA21x%nyg0&(QP?Y_fK79ZbX6E+pqKv41Sjc7ze*Q0`d%gR|Mo8{B8)KDevH4 zEElMx!o|72TpsZ^e=$f8-_yo^DwEaydj-0$mgkJ~3t#Y70!Br_!5>uS=;ISMy4wBa z(eN7|TPuN9rq(E6S;A3w$G~EF!Vm1QRl!tQ6!_ev!A=Fu1r@Q`dWC&IUE z=XDNnf=>FF4mk2DUy@3mf>p1=)N+dJJk4|LJYU2 z945mx+EIuw724mdNm4NHrbO!lE5@d(-e)pe=5ps!ik!sVx0eukpOq+B3$-Cl|x5adsO9s?Tle*EIKU8_qe*ZZD4T zfibOZlI*KIuSt<*(wJTRz86<$=eeQD&&aW_#53(9$1~#>1npjYVS~1F^YXfNkty;l z8M=&P$;anX%oF6;LRa3r;3>LkVq3tkx6wp*K#F}ATfDf#W;&P)$Pd3qj&rooNSYd| zsUb}xEu144_?6il9c=dOfHrO6VlK1H6nW%K?4JAu6nwDoox?B~hG7_nVHk#C7=~dOhG7_n zVcrAQkjQB~{p`oIiL)9{A68LnD}Xgbn<%vv;J@kXDS$bQTgTJaQ2=Y`#PszO;6(82 zD1ezh_3P^l=6Tl_-brQfe`1KLMBlvX@z%%&u5CBcj;Fl;-(w%I# z5z}$)|0}>N+QD^N#*+9RCq*jT9jWQlv-;C_j3d~lUtk&%5r~RL!C!YALK(?8uj}-W zqk(=S46O)Oo?e|%fMXG?tiCbuHch^UAJdmYDnVPKK%&Gy&Eaq^1ZXHh6<^ZpCM0-i z7oxck?q)C2hcqH`I16RkueTKNt59x^4J0`AC=3H|gUVhJK{YIfQi8e&|2dc2xYKCLAG1xJa-O(#K=NFq5qxjfoV!P$dvE z(eR0y^aSAs9w`$1VRVIAf~^)!(et?IaF1*wyqL*W8zjUS)s(%U9BSk2I2SZ;i^u4Z zYMhzLP`ifaFmAPSE?}nBGKs!5!i|}1sZB0MWkrDH2%jmqJ{NwW+9@!gl>jiHS|-{x zTG|ywfbTRD0PiUR{H&P(Fr{RH72TG=HDw>z)J%YhPn8JR>-5HSS;S8czb#8^NIfs; zbW+;I>dBEA^f?D40ww@FSF&=-_s8ec1<~_PFM+40Z^*v(o|E9yPA`EKB_t<(zTRZO zwm~c@H#Fz-_2!i8l{&oy?kG1@@mYY1+GW;Z0j#`oL;LxDUyGr?16B-W233>7&vSgc zpdOj~z3nz(QmAPV9{Xle#6U4b@b>^1A~>u5D%QA9%WYh>MX?wJmuAFqTMvDb`k{J2 z7L3lJ7%eFR9MFg#LgRrVz*0aQL3JlUNsrooYfe*KTKRyKsG~i!;rHn+*y=)~jnSUq z7*)3%XZXAA?BftA+;v>xR)B1Z^d;v@5%zk!ENiz80o=-743tv?Q;ySHi7SORoZ)}| zfurD(cmz{H^QyWWWsyWlOCo;8;E&_4E}ccC03MBtxJ#f)T04tHB2u*G{7!rQtg`x* z3NvnPZMbI_-f2^e*#+El?Jmw3K}F2CWCDqAa3Oc4wQtH@!3BKd)+ib5)|hkngI8BL z%tNBdgqnbP*I1G$@m|0i^rYBf}+ZKd2-7yTqFbu;m48t%C!!QiPFbop{{sCs(zi{B}BZdF~002ovPDHLk FV1o3-Yp?(S literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/media_repeat_off_dark.png b/app/src/main/res/drawable-xhdpi/media_repeat_off_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..6922a0680cdf105fe4704ac9d84f7811534bad13 GIT binary patch literal 578 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H3?#oinD`S&Wd`_!xB}__bOC>)*Omb-Q!WYe z3uZXGnS))G>C{Q)!Xw#UNzqMPB#*Axq0L_}mJzge<77s*ISTuur9Lt+Fs6FCIEGZ* zdVBL~F0+FK%R^;jW9tj`{SV)4_P5GrvKFnmb^pzSImPEJpU){SUonR%dY?~%gVb3z z9ffbl3>3bp87REs5^)G$z_5#n^@6BI!#W41S+Nf{hHQ;K(myM9q0Q1&Vt-duJzh5X z+H&m#hFC_X7hDVuZzCP*XG9*@A{}sGkMt}?v)kuE}Avlq7vbP#z^G%I~4TbuP8a(xl7~aY&HhgPmxFye&QO~j9FOc(`;nx4fN7udk z-!$d+oAtlCcm3yP$=FmM$_r5>wxQoZf&aITLdkLig_7q64&f_)?02xWXSw=>OOt)k zyE)7+Kwdup^g27(>kW)0tb7Z!1AyLSYkBukjsX~Epn$7ponR)i!F+EKFED->JYD@< J);T3K0RWr$BkBME literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/media_repeat_off_light.png b/app/src/main/res/drawable-xhdpi/media_repeat_off_light.png new file mode 100644 index 0000000000000000000000000000000000000000..bf8f38504fb55070b26863034d7af8e2db5323b3 GIT binary patch literal 567 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H3?#oinD`S&B?tI~xB_V-BO_yDV{L71AgQmf z4;O(jfU*V#28M=)Kv@VwLqh||28!$H=|NF! zZ*Q_^y)lq*P28$pZSz(2viGFF>x~i~Tsp|a%YMjK`^OIZjztgV6rZzvKBqW;MGRB) zK0gD6wcoYH1lnbJIu5fiFIwloxI&x5A=Z)M6&F)LxWIzfqWipp-5IT77uGD_c5Fpk z>~+uEmqiU4)-f=aure&j(@D6KBDP^IBdbi-3pR_n|8-y0yxO#1Na5+Tqg(7*F8t$h z_#MpfmY;EpJ@bWsA`ZWu8Qz{}*kaFm;h(^RKxVELs~ebp9cO+wzxcb%f_eX+&r6Iv zXBi$RU%dX)^?7;BdabNJ*L|46ewR8i?p9pXkl6t=&z)nzU(JSZ%NcIXXUeGOTJV># z;hXq_2(i*PKr6C*xTRTN?5MA1`f^jA5hz{IZJ?0v#2gY4Z|F~!UTIpE2 z)9F;>3OS1gppQ#fc@}6FFvK=8z2K5?2!Ft^i;4AuC`-et{&EIrXuT7^St0z@X0PBe QVAL>py85}Sb4q9e03j&LF8}}l literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/media_repeat_single_dark.png b/app/src/main/res/drawable-xhdpi/media_repeat_single_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..f3fac9a29b2833ae2118a237f899cd18d5d8f70f GIT binary patch literal 710 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H0wgodS2{2-F!g%6IEGZ*dOO=N`?iBbn||5K zEvZ=^ZBAYkFofS zM{@wjjLB>ZRT_*ryqP4L0|XC+4IDX*RAjY!C^-7X#(k7(?K>`8~tv`unH;MN% zSUY~$mSLW%%)!WdgTpC+QLITI;(*NdGfZ&>y)z;mnixO#C!{6(lw4gqe|b42)&7>?brFx$ur%V1i_H-EyWiruEO6go=0X-_0=R zAzN~f;W2ZU-g)z9#{B!nRM6nRO|iFmOUq!>x^i?>;UL`TqL8{1<qjSlYP@-WOcI~d1xfMfGA;Um&>9vxnE@9~5E;u|vlemFLhP+tGsdaUo(nz5{C9oUgAG@IGo`UCnRmDTZ(s7ISGA1N zOnc7W_iYsqxc$e3=~BY(e3t{c7k)GDY~_ElK3mVIkfVT)S>+1Q3k;H58%!N5h< z{bJMFB=7P(zxNu#mmkklG+#c?BZLq_2qA&QTpe<RqKvz0?fi0k?0JI1r0bS4{!~|d#VL||65vBy-6=6mIS`i!qtQFyOU2EA*W5Xf9 zQW1K8tN+kert|C&V5bPqh~_gkicp{c8$~EofQ2FyETAjbzykC+xb*A50>Y+O0savT zZwx%XL%>t8fAn#66YR^A(eS$kXlu0#b0^G1_=^)@B>cq)Fckh`1Q-f`5dutwKW_o% z!k@PQbK!rFfN)Q*&x5h95t@fM8R9I{&4eTN&fK0BHprZcBS90oI{_x<1IC@#us5f&4A` zHM;6=i3CLLKHMHpu$gdK)!!OkBL_VR+5zY+*;4w%J?0 zG&d+k`PlbqNqh|O4|25;(NwY(6MX5pvEqWW{!$LPFB)MiA0N3oil{U-Em#oNEVcGY ze$^fw?>lO<@6J8>PUo{-{ejcF&$rw?o1feck!}RME#|oy$*J zub$a#k~#J9r|g_f-pA)LWZikfJ~xQt0gL~O3merNloOLq|J@q@ygJ}o7eiLe)8H8| zwlSntNQ7-wYIy0fXgAY}qKW1Ir<#g5OfBBiqq0k@!K(a+ux7SOgR`yhU6BKs_uj3{ zQfXj2U#+%Fy}|R^jhB&Tiy71+9(&Jsxz4iTz-GHpW3C4h{YgTxLJqB>`;JZV)otL? zuIckFacB5^=V^G(%_&S42aX*$GEF;s|CZ+y(^VR{yiJ5vb}2Oc&AH$gV78Ir*v@$d z8!H%1lJcu1SuQ*(7;fiQ6_Emaf?F^4D2@_Z7)oc zW0H6(^uBj>8E3+j&wDmax^e39w({GDZk2N;TuMsfW4ac+Z#J*khAhLt{S0@RzldC@ zZn($tHC`c|bGgkw^A(i`4Ah;{y^IC9xXwq;=R7wxP0eD<$`3A`%MRTVzi@8CH@3yQ z59dZR9lHClER%(0_fPA4^Y}Tw^Zc<@6cYLNyC)#v=JVjBhr9Qlf6mfe*nVE!wfU5B zPPD!Do>(Qud-KKkwSNEE>#!rsuHI0HC!pqEn#P~)58j6-V@ozp925EfGlZ|5{^g=5 SqZu#_GkCiCxvX%h_wy>i}+vdMs z0e`%1X^Vz?gvQLCYpL$LHGFMG{uM4RuBDG$*SiSr4G0KPS=909d-&P)UzKi7*fRU> z+`BK|9jTlz|KU(&+0B`j#oI!dfaYU@23yB-7kTe?7G=k*n*SyG&a&rL%9V|`|J3TN zD%alQc>Bsl^#f}=UVmw}N>+5(@ukBGBw#P4BBg&|?TONV*R zt7L!t9x=v89|{(oWSVnKXs@i6{b~mHU!RT^EZWGlXVsp=^<`7$dosv#d7FeZ*qqKU z6P@qbQ1CM{Vo@X09?AON{cY3c2Qm2n&i-O)bJ2n^Y2LgZ4ZEccM<0JlC|MNAB(u>c zUg=c0_=AqreaZn+uwb5 zpYxQzPC{zeIyG+-hlT=W`)W-Vj&Qpr`%emSiyip=yz+udKiiE8jB%lP7q=Dt6lB_?m4Cti>k%iGj>@{J zi+*r0esZ~WxWhQT=p>jU`r4^V8>wXIES_9n0@NUOo6`)B`s`Gi#G47 zZnCkqvps10itF__H^{d@ki;la6vdAF<; zFWtR-(^mP)Z@C*bs@LpwZ`i2ch(2*H+D7x%(RYy#gI1>e-BR&?OTzpAOb3HE|LUKZ t$9(iveOuGzrnB|EjF?FaDB_^^hxzf-J)Rntwx>Y~JYD@<);T3K0RYd_RR910 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..0ee9c0d7513b168de1210ea444de315825c4b14b GIT binary patch literal 579 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H0wgodS2{2-FmZXhIEGZ*dOK&o_tip?wy7pk zK0HoMYV3+FM>2{urg%8I9MLQg6q1sWiD@YiSj-`EaOH&!j;wcGSSDV1IVrs6|FYu` zndfosm|85J zH9xi5A);_Z%Jv@95yQ(irD6(4#QwU?e#phR=t%lJOKyQj{K;akA4D-e((m3=1eE0r zwkZTs{&7{@0_inN??yHVNbh;txS1jFQM7Dcn>2^OqX`qOcFW{zICS_{?Pcvyw!8RD z$D!js&!-cS4I0Mr>=PgBvTzh02G`JGRLMM{%cI-}E?OYWODGZdc6S*U#6T3q3X z+~U@~GOLvwru>Y%Fgvee8@= zOFy*o0S$U%B*S=<$&#g*_nG39LFSlK9 z!UnyGhWe8>=}k7)pRz@7s;U08ZFz>% literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..cd267084336af4429c08f6fc6b0acfa02f354cb0 GIT binary patch literal 599 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H0wgodS2{2-Fv)qkIEGZ*dOIgt+a*xsIKNt| z(j;-8qUnCm-gJAacxsxe@_$m$^yPTk*x6Dcc=D~I-sjA>dzbCWI8{1l=KufO_Z?hc z`^_r*{oXJE0VfW{76efvG<$3A1*S7QErVw+;EQxPXg70ihCoEak-ak;W7yv_)yT}K z%VEruec^h$N9RDRU9ZW7;~xSWy4^PmZBybdNX-8#a>PnQ^?}Ait|>E0gx|`@#V>VO!Iz8g&k=W!{~;yMeW%McY>D3In$QXL}YyRKt3i zvoWg}BO2B;?+J2bifEXltABh~(VuKarH7aNj+t3bb8ZOfn;odWp&{w%iZts~GuSicHTg9E5qz}X zpj&a;VH<&m6D9avD|OCw3&!<^HZwdq9r)pO;R|-58~!N=zopr0NbY_CIA2c literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/notification_close_dark.png b/app/src/main/res/drawable-xhdpi/notification_close_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..7da5f03e59100f275d2ac727fb65e08843fab874 GIT binary patch literal 289 zcmV++0p9+JP)x0gt}mxQnzVpf`W#j?#zKF%-*A%qY@{0lh7>evz2 zy8Oh(T$dKd%*NEV{7NL?tD-%D$l9z092=2HM;-3||UHr1tUBoXL(M|lU5xvCE z8qrVutPwrMe@Fx77MLzV<85eu5Sbpu#+Q-#b!{J;Ln)5^tFe4UtP)_q z^XL>183`?_J^>*Xym8|c5IOHOxbO)Gk<#JLDInH-@WiE0U>C+WZEk%6LaZ2+7)AjB zp%TL=C?KZvcyJ1cj8C2q{%q|bAjF(k>P`WXGv>u9AjI;3rFnV+IbY5Qte4PShBI#? zvbN$C18Qga9GeG{lun5Uf80Z$&-Z!u>@P1VIo4|CBA8;lI&! S71nD20000XK%8?zuwxT|Aim&iZ_OwGp{~xko0fb6f-vPEZ#V^nH(ZwewCocQgc^5;$2;pj z0YRK$++r}k7l5D^gV9hxc&JXxmQz5S^BQ^KZL7He1mU?F<(^YOsCko*oC1RIy1>-@ zeF8Oaogr|3LUS5ckKRJ$EXAB*-0%L&c5EIw0cJ jP}om2K@bE%@JIOsyTyzbO2emG00000NkvXXu0mjf16z$l literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a9260147408460c3eeea9c8b0d5f1f50b485825c GIT binary patch literal 308 zcmV-40n7f0P)%7zW@EGg1)4IRaOAn}m*_MQCetY7Lqi+QQY%%_-<00w-~A4#7S88B&UZtHGhs zLGHkw1Nbg@zxVlji{jsekyaLXwJ>my1mM|)y(9n+PVFQC7;9$%J`6RJ0KB zO-NKx6vzFTvQjaksjw6&sVG7t6N|uE76cX)=paSfPDFAgqy#N;5rs^+5QJ*grWllh zh!zDyWKkJK1rzjv_%Rk^Y4bs2zc$y~oAJFj_d2~rG5@=~=l=iqoO91PHzy~HEdCEr zz+O(#%~i23(T<1WzeVrmIY0QxFa97wlref(nHhBn%{;~qK{{ExRC+hJiIWZq9j|;nFnB=ZfDAHE3agj);J?^ zK!&N$sb!;oFQsTe}^LT zBUMZTBWxj8v88xZ+6F*?_2%Y9JQ9^T%5LRQ9Z8XxrP?Z{Lk6?YRFkhLv7d2y?u*6M zDByC6qaltedr>3jMkjgZ={jMOst8`yHcTl&;#@S}%;mVnXo=9F>_s`PR$G-!*vzbJ znb%<>=ft`xI^$a^oGJM|L`3Utb4I{LKm=xzI%gzmgb(n7GG`=wA`fsc{Sc&*VcKx% z2yEt~d_kjlY;E`P5{Tf_`bbjGcPSC0hl<5FB%e)m$zB@gv|gB6_-d$xPu$=nyI3xE zchbxt?+m}y>)MlydV(ZS!5m#nOZG<2@*ypKfJ2(!#w3vy^xBZm(6JDlIU&iBwwz7kL<#%4 zYU6wFBVL-a3OFz@vLF);ejFQY<*f_u=2;!M5_A2{6$b51JDIPyEn~hj_w_8kxYs`q zoe@bmlfxna)CklJ)XO4}pjfkDpUmq6TgyMP{D|*cjA<-Tae-791Cv4n1BU_w6GsD( zdEh<&7RBCa>z!T8uT4KFbM~gmr4u)0PC8W0X`8low(q^muXs{T&VPQ~zq;nEi(ZOT z!!))NyXKsl>|?sOouQQF#6KJDJI)QO7%$CtvlD%<=&+UH<=JMhYK{O^hbp6>LVm_m z%qgjE|9%};KCp*P;@tUDKq)B&Y2(Pla!gZLJ;L>_ZE9y|Wi)$w_z~X*)&A&xQk&n@ z?^!Jf=%EEX}*4Ud;amI-}byNuzt&t{ban{3vk{>i_TKkIFc zkc9X2$Z4!MCcmG0{J*NRTBKA$>bp?B0%dRCO*RMLapg1nrd#E2J}a_Xbw`Zm0ZZXE zQVGRz;{E?Kugy;Q%FFVdQ I&MBb@03+Q(Q~&?~ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..de26b6d0b631e82880f8c205a300aff53328bdfc GIT binary patch literal 812 zcmeAS@N?(olHy`uVBq!ia0vp^2SAvE1xWt5x}=AJf$6iSi(^Q|t+%)CW{ZSN9C+w* z)I?F-P;04>{Q+0)?DFSxHU|c9Tx{UbYP#TXw7+3ecUbjeu|Q_0kUws&y}zzY?G-#| zde?aKyJI%zcg~-=XMalhvlKQqp!t~Kf%>=K&ni~^4=Y@8_v^A5)o1K@FWeEf|8n2! z`mNafLd)+nm^c&~7*GfY`Hrn$7X1&~b1Ac7%a%)qzfBx+Gm5JJuidcqF~k3LB|g&Y zr)G#R*pXw#!~xU_)Xt#*G+~2O-GYqc5Y8-fV~*dF)-6|FTC%KAamZMDbWbryfR#h7&ADaMcRm+bkkargV^`!e8Kx;* z28J@{rdzi&s4`t?n|AzvT*Ew85BF1j@eM+Z-~P|4ey;2w%E)&4rq(${hqQ*;{YRf? zTRGg26x+4;QO@tr$0uKPiQBt-;cKykol7Z|)zLO^WTBmaXEtV3Rg0 z(mL>Et}ExH%(NNz1oA_T{vH2Xc=^r4$p=2!URV(M>@NG!AL~jQuAfbqG&9TjWzDJ#{Z_I<93?owQnB2`><0iDtUjvf!boFyt=akR{07i3R Ai~s-t literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..aed0d672a8c8c784136a09dc8f72c2f2336f5226 GIT binary patch literal 1716 zcmV;l221&gP)6KnCUL`GN?0{2o3tqr?D2uvig>LAAc4&+W_ymvQavYMT7%uQ#a4|kacdUlc zGzazYBu+_F-Z=EVaX(sOB}Bw#;8$FN?UEe)IjE)|x|T_J8wVvR_#4mz!Xc|r2FGS| z@E4;mgu~XN3=Ykv;EzH>2#+koOW48Z;J3w-SPJ2(K`8Pu{Qc1cA`7g>(>{fNHl{#i znY!4=_3$6ZT8Jz)9w)fk-hU2}0JBlzQuy1W3`Bw~#bvI%^D96k&>CDBcL9EcNU#++ zE3WWghbT$S#R;~O(|&Hhv0plf{3-)X7ow((@OMOC2-oG7?-7K(MLEBA5{0!#nEZPmi@E?G zLAb7{{#Y5ZnxJVaX+F)w-@OufQH5XN%PhBmcQwI=l3TXBsV1p6wvS@G-xtCMg>^^E zV8hAzy^APrJjujsc-nLU?t}0}ag)h1)bMhC>ujP3^Rd6F@OQ#=2%qGZ>k)SmzBN^V z#~^$o`+OofJ0w?J&cq@QWIbr_=2iSpn$K>DV*=F8C;H0nYF#e8%jI-8)4WzEqI; z!rkz&1$fn)@M)VFvqv62SiFX@T!C?x}8p3&u(dCTSio8mFbzbl7VU`}2dok9p z*vDG@BHNY}I%a;j>bzu$yH~MU?`~ zjq5NKGf(3Mkmq|`@s&yeM#Q$O)o3F7uO01esN467N&%W>)7GLqR^p1=RSNKB?EAY7 z#u@B@ySY?=vsDW4aQug=%uUc}(p(9@6nm-^;DT7guhQRmM=U2_ahs^RA+v|o4`Nu@ zN=3hHzG!b*80LO(Xz$Fki zFizjXL8#&XnN_Jw^MhE5K9Fl%Qlv9TFX6A@|?oEdYdgIJswb)f*iWO6vSa^%Me zf9A9S zbMK7Krv>M!$f)HFB?@c@iEnR4V{A6=nOx{5j8&a;JgjUGf{wjObkgm;8pi>>v&O2fDNo&Vk#k^8gRZY*|= z>YhPWAymoPYA`%hYw!J6ip!&Z+{k#f70zLQHaT_WY&03Zv~l0KVyxcsI?u%5u$9L6 z-b+2Pqpdq9KDD)z4g5Pv7T{>x_YN%!QIcANOXIk6?oMc%@bF7H1Bo2w7rER#OCA^t)4%afFHF)E9~FOS`P zdzRQ9sDM(>Cs=p*{wTvsG0wyBeRUk{e!}XLwwJH~!f}0ZuDfaK&l%puJO_jCj~kNm zOyS+|1co~B_}cooILS}eJ{vz^rp5NXH(p58le862;+=(e(cbWV>!zS49>x)A+Uhgw zQ*bk0MFn(3e~iOSEXG1<3I?J%zQyCX1c#()rdN8US9+zFi`O5tr{U9)XvG}>0000< KMNUMnLSTY~kUQ4t z$e~)*4CNi_(ycv07s|Shg#O9iwM}Yy@DcXsI~vQ~3;wt%12r;KEb93@Q?K^do6RS7 z-1;ehb4hINwwYI}er0g*)=Ft|M_!szj$Q$E7xTw vL}n@lr*!#D)HGs%@z^wsKuk%HWAa}xy)c_@dtmjhRUodXtDnm{r-UW|#u{s{ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a675574e65424c690807c9de7c4a5fc89486f35a GIT binary patch literal 269 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%xcgu6VjQhE&{od)+bXP=G+g!v?tp z9JZ~q)1I;Day2a%bmGwf=iL_|2kP`dg*h7BzU!?SD~K)R4PWb=!XXy8rjiX6tS`eD}bIHk*HBBKVBXWRf40J`bJ{~AUcZ=u;I&oeIuxzW?r&t;uc GLK6Vz!f%rR literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..07a2bf48a18d0b4cec0e44f54babf78c9f4fa359 GIT binary patch literal 1701 zcmV;W23q-vP)&7kV^hS}vyn|?o)rNv|+7%YYc(~or1_1yR`X#!aYl7v%BdhB{?{J*q`pb#q! zqZe~?Xb?!A*r;YO zHUA5WAmlRB=>gT7UzHq$Tq=ZKQT6@4D?u;!oOmC-@5N9B2PL%n^mH*1edXwFFI)y5rl(VtR)VPamt){F`YQK`$Rg@gu$fTcDdFGEI=;f)-|88Hoqm%YK!1XsS|;~_QPn1$Jb0!*fbaagF71jKgDV<;h zi^m3|D$AX1D^btLZPIVDNodvCo0s6q(V03?3z z7J5pLiBX|u0DkKcXaFS%W&nQ6q6Y#AV3!#{z^eP({ErD{00GxP(3OWC_a%U(W&pY?>dUEzY%>7e4U_x+Zw7Ek?&#MH zz)5#%3814HfMGt+62Npb0K??d62M-93hod25YsXj!{3^hy%GT^lXCWPBz}(()nfB8XBZt`TI_e`4nffktZyAdw)-i1cZY zRT2hjJ+uUHh#*S0)Zvg-T#X|-fJRyZSU?bk3-Y|v2Spbj6MSs-t_koTbtj0_IH?;% zSL@_{GLC}2&=NpJf@u6OMzSjnMpw4m0*?RvKO< zS11kVd8Dc&U@e&308YP3RP~ZC0sI(@0dSiGRhxg(*NIuTQ-=8p(9Ix!Cg+Yd1JKPE z{sizFK{XGBd=hLQ!pHobo_-UB5fl?_hoLP3Ss~Mapc;;5FE{v7vH4DVE|383p=5$e zVx5V&xFVb}Dh4o4k7cT@2`V{a55dK?*LFm;=DX=#Jpy<@wL?;F%7=?%sK#CA15l7}FU_Kp2&CAL?k z`N!#YqxK9MPmrxdTzy4i8`o2|QLH$R1*7rl4raDptH>5MbQqavBjx6@BDcFw(x zmJ?*)HpDnn)f3vt{Ln2SbMNT8Xgxu+JTX;qrcQ5BFGDRAc2K{M))7Q{F6=va;PI}0 z;WYvY=XvDc*5u+>!V-c=$L-*mH{KjZ5M^A(Yz$1BD6aWZZ``0gh{|Lo z7_AyVd`reP5Y_wx^hU7UN$DLCC-2@m`G=?x@JPBV*zP5E4~-%ZAxNcAv0%TI;ZJme z5JWsQkscS&B4LBJ29!cj+;;j(SaT$d8Cp{kK`}e&Uun&eFm9+u`w0>`XeRw|J>OaT z3+hL)0^i@aQoUSzC+z@c;(bo7X{FBl)+P)FT!$P4T(^fG z2@?2YdC*Is=lqI{hxwe}4DXv9TQH$lFj=|y;hgAYJJtK=PVnUb8UO}nTU~bkyYoqD zW!B%VsY?%>{nqt0J@sd}Sk_ndz`yQ7cQ^k(<@NmDdHW|THhxNxXDy2U9sm8azUP+L z)nOHVYt1}s?RG_GGxje&`)e!D;!}%%75-lxXfE_2UV@QDz=5H0KX=4&y{TWmu-L?_ zf&_q~0pdHtE!c|0Zrep1mP6_WtX(1*)5>E$lTa z{_j2%w43vu&BQ~FuP)|>M3`N>ui3ltd+gJ-w^ZUz&imVTv5aq)jr(oG7eQxQ3t=AH Y!F=OP+^v7NHGM%Mp00i_>zopr0HQmP>;M1& literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d417b4a967993811b36adc8f32435756ecb4c313 GIT binary patch literal 358 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%&M7z^Lo#;uunK>+LK&!#EA6{5~(W-2AJTNg^1#87TyB+p-~Cd?9GkZ* z8m8C&DZa0tvO2cziSWxC+m2WfvS7wpDi=r8#yHjFUWvAXJ y&9!)+^8VlbYigUnxkp7$F4?%sMh)Uih6Z^{+f&?2XC5d933$5txvX~)y>7_WU?AXnQ073A zx8j4p`j2OBxgxSjOwjbqAFB!J%9^Y|wG0xc%w&H*|9bthQv(Ad6AOocf&++Q_v6;C z8+TZ){5et`zx_H`E>GQM*R7bmlm0&@aza&sl^+mZzl{lG6i+HUcgWXeO3X|GAg-sY KpUXO@geCx-RXDH! literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d74ec1bdfb61636c0a6f02cf216dcb029f812f86 GIT binary patch literal 188 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%xcgnmt_{Ln>~)y}p;LK|!S9;%+uy z!7X3d%&y-4wx!pMMencX^E;6rE<5%B)iN;5GAgfJb?4u%=bn2R7@1f&1QZ+^K#V^h zwwKywTCnFNR~}7YUVVK}g*Q-kg^hyJ*QGa)eSS7=<(@;%Y7n(hP5yswF@cQCIK`;N VJn7si%k6?7uBWS?%Q~loCICC|K}G-o literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_artist.png b/app/src/main/res/drawable-xxhdpi/ic_action_artist.png new file mode 100644 index 0000000000000000000000000000000000000000..c1fd3b0264aae2fcc6e176be7454c2fe3ff7913b GIT binary patch literal 666 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..1d7d9990daef5dd52596a8219c9f0c843ffa32a4 GIT binary patch literal 548 zcmV+<0^9wGP)Z}kgNuVxdrWghNwTU3gYP#9X z+j;mta~1i1c4y}8?ilM7%*Z+vor(h+k3sIwOE4s^#S7Jz%!0`3}qvKD|l{qac!@Qt;A z@=U}h6TnsY;4QFgCO(+}Jg^qn-!c`SNI>@!D6v2xX=Vc@6M$25@o5Aq+y&~~1==DY z0wN#+aSGsz6tZ{))FpF)zCzWYKtRyY!<1?mQ>f82C0FneyGHK76=iF6J<4$R{?z14 zSl2<0{sBfcia85g;BXT$e#_movWFkBZuBPm^mdSl@(CCN%vgGc!rh4wzp1j;9s(U)fq3*27W9h~@qL^b>wn73I?8;VX1^C>`R(J(8 znU$@HP4tyn*_yZm0kg6-w-wc6GkJz@ZsRYX49eD&eJDHZ=P)Q+zbNG4eX#RvJ6tJk mz%$dUF9?Dl2!bF8%f=5^$CkgKmpK9e00007_i literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c67c9ede18128c3c12500663c35fe4f374bac6e4 GIT binary patch literal 562 zcmV-20?qx2P)FPiODppLY8AyKcpN>AhtM+$3=JXv8ZTfbB1fAwVUT4$}ZCN~2(j^j9v<2a7vIF9pwV3j*OVT---!eef+^6guHKwkbBoBTbe z9Q_jmtn!>PoPJHAHv>hqgH6l|oy8nBe*YyQ^8EX7g>kVs8)4yd0zXI$k_prVcf54shq%gPryf{61U#sda~AFrCgy0m=N1X$o&bb-_psL9Kh zpoqA@SyHk>oJKRn0_jyK!vT#*fJ1mpvp`x2WI3Rv6JUWWOeA`|N&>Z1^0j8Pj*RJA zp;QB^SD|bNv{VA=22?A7j0d!wfi+}Kw+g*Tytk&TP&Wfie16LB#HQZ|+<+VC$pEbz zXj?bXv~FO}`Y&+>N0>|Y^j7FkoIq$}6=%r|Tqw+Fc)}vK(LgJKf&J_n$=QfM`I@XM zBoN6$=}+EbHMH_Guvl9Da=6cM)YvFho*Ry3xyh`sZCF0QC4KpY@)Z{EzOI2>ukJ^M zE{%4-BY%)@hUEiX`3w1ycVOOZ_d5w3m@QAoz>R770AEeZ2l#ASKEPem@&S(U!LWP) z;1nMX%Lf2X^3Je)0N?}<4a;{N$8j9Tah(6eJ7shRMLAPY>Hq)$07*qoM6N<$f(MQH A2LJ#7 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_rating_good.png b/app/src/main/res/drawable-xxhdpi/ic_action_rating_good.png new file mode 100644 index 0000000000000000000000000000000000000000..d9167960fe286eb846823a10798c22a662416589 GIT binary patch literal 776 zcmV+j1NZ!iP)2)5G8%uNKa3GrDiBJ<5TZpvM4%AVCWxX8I#to=MJ-&@=f2JLnHklrg+FM z9y=|=tkD#n4$4J%;Iur|6rK)>M9Alp(|T4@c*;aX*dA!FQ57B$5l%X-KUtwDJafIb zot{or;Smub$xEm2{PGi6OJ}<9h={O_L8q@_`3am$A081A_VR_y`A9RRfTIa6VBO=-9ug!fKunhL#))Ecnc#n{~<< z2_^6$Qh|R|sQ=H#dP&4AknxCh++aenfCx#hsumETR=Yri!caXm<}` z^a)h*-rM~{ECNTEbV@dAyO0%g&neg)+}mmpsA0_6oL{Cuo4|Fa+$i-P*W)Mz*bo@z zdFaQ1kqcZ3471Pc6*2b$Z03zq?1hIDq=*HY_~AT7IP7^(l4}8qxa*Yaqe7cNm2>=+5|2H251yfCBQm*oiYRd&fOvrAVDMJPMH(h z1d?21=5LXARK_MiU6!{nU!C*Nn;u_?%2?n|<}VRYz!j&$cMkbHd(Ko~f?L#Pl>SQE z`Q=o2O)^e_@Er4d`o9!`VUN@Aqa=EPW{(1=MWPodrPo;mvuB2GHpL;3M+2`s2=uVq z`|FrXd+0zODOwl`G5H}{sZjJbsbLs~VHk#C7=~dOVfh1cT?bfDV>g8W00009rU^ffSu3e;pql;UI3a+BKO8e2(DQ7$-22}1+?(cQfSWLh z#=L{=98w~{O^Bhj;P6Aa`-c2Y9>j};59DyELICpx$@7mi<(y9;j&Q0(_`tgJMa)qo zP`c}WbXb}R-i=N%4gTn zX_Uj5EF=6JbxI0I7V-R@$pZjiWs_DYg>G$I?h9}f<0yBtxFahdkU|{7C|b%s{Wpip zDo!u3BIZ7f_|Oc`Fif9 zX;-(vlvSq zd-|zcV5*Xn-4pQWC};ssRA4apw-RZ=@DFJ_c!6a>fu%|>5D=gld=?7-h>|rB05F6v z66u41$}bZ6r=HUM3cYxNHax!90vGs(Wh_z#hG7_nVHk#C7=~f|FZ=~wHGwj-WIP`L O0000+q4bEF^$B1oj5eVb_0zT8x1U!)|3qF4#%*R}`=r-`P# z&iK-K&Q|1o<{s{G?$xpsMNt$*Q4~cPe4J^wWXECS^9+?qsO~I+K7GC?AtB+KJ%{-G zs~_=6NVp0XJ~>9upYcgZ=oBdadV(K3@rCME3^~4VV|xgsdHmv&a1CSyzVv>lZZS5_ z>NoMlJs9}-cD%Fi`?k;m%-Hg+7S!1CX`pMgvE{83xP8C7vU||+Nz$ueW6QGxweiMQ zcNO8rmS+z-8pyFlFXT{QMbXl?NAaOW*?hh%V4j0ToaI6^K-ziY{=2F0cyaqwvDkrg@i;4i->4tqfJPH#HXi) zC?vNrbC(%1U%kh@w{K=|k0CM)!!QiPFbqRJ$fJf8&GOp-9z!~3Lg7oCmB@O#*Me=ul5qB8QmTvh2%xjhp+;F}v;FnJ| z8hu%a1RL;>FS&&ii|3mS-SYbgHOmK>3n<92_EYJAAY=IwTNokg@TVxxz%KadB0?#n zcE=UcqW)Q&2{;A=UCO1lI}O@&{Rx_Q*w*}7luI$*b=sNI{{S1?6P3`Pz5aR3C9h$k z=jL=L5Na~vU}0|M|SKlUt$8Fq8nq9EH8lzH`XAW!$5C32PmI`7Nh)oI?vv~38R61H{k6k zj{yflG?+$8O>hrOye2g9kekCo3wi!ap_hTL6iSHD{ipOEuCa$48HQmPhG7_nVHk!n aJA42?Yvq?7S}Dx{0000+Ma)tV01Jtq*k^ zT%t4|`N>#th^!3$9w4I;B$MGTD!8j7&r!tu;t`h3%PzBqeAwG_uK9mC^LK8wXR(`m zIw0UhTvn64EzoEnP+)ZUwEqZ%4J98~O&?_;xV=PwJ6ZNB9@mJbQJU@k*ee`9A3f#$T>&WmzHr?ZFpTpU20)Grb97 zmkDN{vXXh$6~<6*hqo*iTN(aXFn-fzkXAns&3$0C@B`IfdOu8mYx~DfeD74eefrD1 zC$}^7i@h&DSRc4{;`2(2FD~0Y#V*$gvETD6`u*3dPZ$-x-#=iRz4-Oqi`um>G^d(A taxkuay=Jvk=&lzFX0}NRLcqoU{}^<#pE^&UaqbPsU!JahF6*2UngFzgm?!`M literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..9fad1f6975540d735287638cee238aefa38b1cff GIT binary patch literal 1476 zcmV;#1v~nQP)n2YbMT%;%+69 z;NWPt)%@4AGru%waV0$``I1_NFR@}K%&){PEr8dhKF8!i?Lp=&v|)6HLJ)kVH9v*L zQlBnxaQuN;9%Xitl(EQ{KWx;@J{%k)D3yA>MWAXu$FtKUXMMEhKZe_e*yn_+-^;{( z)}!1K{6tUmMGK(QU_M8;tKV|sZtyI52R(5Q715dh45o|#wzzuMkfU{Ymc39o(Z;CE zpM#%`=5wsdaY>Oyr08-j)tt>BjDgT>_h%1B$2KREBtF{}8jQ(_f zmP5-53?PMd6!I3m- z`A!56PYT|1A}|j#jK!Mhr=3#RV8UM^V|C5nkaS>x6-KTi0bhoQQ96Hz}CP3QgR_h6jy4Dnl_7C zhHyW06K{WT00#mCaMdBOg?RNY>DAKjdJ+A!2wr>Wa3arwmAg$l4ueg^Ym}UG5Tj14 ziBb$xpEsmzBh48M4&ZbM037QSQ<~`d1A5<5!h!+nvrBN+x+_dU09+0QK=>{;xZMM5 z=(bp0t^Z_>oQuP7tuRB!;@1r8YVFhP``n;F73&6yST$XSBfN+`v* zns4Af^f&740ow?nT%2t%9lPvjb`uh~pY!1B4rV)HkMa%VqsIyR0hHn!bHWw?bFv<31;hK{te)P!}PU=3Ux7mc%7U0 zD%#io+DRXle)=E}|FsG5qmqDMU_HpmZaSOcx}Evpgrao44|fhesnfCxFv~Ij4@|6w z`VK6~ZGAq|ER9V7bp#7Arfb8V+A1&mPrwaMefRrMVgdS$o_xQ&^tta~Iv<&cDEN}mBI%g-t;dWvn54`kZl?abL)tbPl6Ty z^t~m}!R*nr-nXtC^5_sJupVtj?*8Bf-umcmjSZsFW$G6^joL#-RRCTy<-Sw4r`Pm-@nII> zH+V1q@@l($K_kVEn*et6XY3aG)ek|pn?Bm))>Ds2(Q*aaoI{PY zvO&Uga3!p-&A@kV({&Kw1vj;Vk)OGQ4rk#4EU7Bzzr*@twUw_jR21NMZV`Ua``-u| zj|~DS@TL^~8KJTYj3}4FgIoGHyTZ3D5b_P1H6#*-qy127Z2Ck$0xWS#jBSeYo`C&B zW)wTXgo)VCkjx?gQcYZk6p9<>4J2GypH=D2!Qwqk-o-7`z7+eRQh7o{rH+vqhZnx>(jVHci1en2nH<`@L z5c^s19as&u19Nr(2)}!e8o?lXycP)I?WIBaW8^)&4CYt3oujNP=W-;;2bC%oFda& zT}QJ~xnRAuX_tbRx2he?S)!jL?4zFuso*w^V&Rline2ZJr^RRX=xa+N%WR%Hx8ytY8S2Nk!lDyElE<3wuEoNYzI#$@xW;^ zo+uHmd07RnSMvDi8l%?O^ui5k3R_~2s!?bvzk;?{D+TU^PG_jW106=orckHWW-ACF zC@qqsZ_&vZ@za*0!LmA8_0q0po1VLiASmwM_pxa0_?k#Gw=A?>*!N#vXY;R$F7!*X zo7oSdIbL<8k>G&C2N|@K-w@!Js7`2-=!J_Rbv$Aswf1gbzIr>h1l zp4WepjFM?nq6Qcj*dhM1$Aaf&N#QD8QHdTS4d^#-*KNSJK88)e&(UjeNvQ=gZRj5_ z7oNb7Ukw{+P-+8pguk=GUpez`gKCe2Q}x1n=?`wuGhhjkYBKLO&{kw4Q6T%}V=cVf zbG`U_RgoRw2Db;y)nl18fNH`|qRZdtl~NDrXePxGH?JTy`cFFggk4%Llpe4LzU2g% zQ0fV`9|7%vJ5qZ9=X{F(2rHtDHb#sXF=E7s5hI3<{{XhM>gRQtqrCtC002ovPDHLk FV1jY~_Xq$0 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c563ff56f83a54084cf83cff5aeeba94fe68f972 GIT binary patch literal 589 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 Ce1#1F literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..54da8106599bb488041eaaf5c600d2ffde973d5a GIT binary patch literal 356 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%&M7z^Lu%;uunK>+LOHZI?ue_K$~k zBsdc-xsC44veN(m`^v#L>5B0VW}8zq6}uIe8gS2IoB8~5l#=S4%atcfZD0R>_wTLz zd1WOqSoA-v_>ljjYt9ZY2@AiC*Isg5- z=_#QF6X!qSy5PI)xwYljy&LbG%fDi5fB)pxWsE?_;R6m!8J(>|jAOPc+lKOgka$}^ z=R%|0D&`&h*Y@0+zHpXVZa|&G_4t2zm%^^-Y97dYe7CoCzR&GN->&Ej9*8Ru-Cn<2 z`Oe1E=lAdJVw$t{+5cVYGdIRHGDx$`EzoYQwX_^nMV-+VioYkgkO!36@2 ZerL06ZZ5L4Tweilou{jx%Q~loCIC|flScpm literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..cb5e919a6bf3c742bf0d27b71aca45ff6528f24d GIT binary patch literal 334 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%&M7z$ohJ;uunK>+MZnzJmcGZ4dc5 z4UNBS=6}=5D^bd+(>7N^pfiHi`S2>va22PZ+^c8bE`5Ld$co%^hgUKR2|~csb!|+! zBCkFj6`WA$DEls{Q|B`GC$s2M#M4Oye0U>!tVpFycNBmHn{3VY8T2Mn>HE ze+MuBE5Dz;>f7TteQ8!p;vVxREDbz!|3PW*)^D-`nd|Bvl`iZceHA;OxR~~Q~Z>r!tYBd(HhHZ_nExpc6EV()K^@)MB@tAy?@FMvd`1i&t;uc GLK6T8po5VB literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7137a47facfdc1f7fa83b9d9dac9305eaa82fe78 GIT binary patch literal 372 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD3?#3*wSy#L1AIbUf%Jcxfk3U_Pk|Q5mjw9* zGcdAn@bK~rNLbsudH98dMa9O?Td-o~nr+*6?Am?i>;w+W4xrNIo-U3d6}R4AK01}z zQGn&5Oj_EP`)6$RJ+H0}KlF$1h~@_m7Z;a(hU{#Yewu7coABw^wFms#uj>Zwe+ z9L3l9bIUe|1NQj>S?o$|b%_j2`f>>e#4E1zTSzg>^e+C%vnTMt|L8OP5(y9DXD^nv z;CjJU7H)3fc7dlPl;N=hcl9I(-n$N*cO97TI&gzYAR8zGWCIaMN=ZrS*Gum9oe~1# T$7lEhoy_3r>gTe~DWM4fL}9)u literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5d2c41ad29508feb632dea31dafa18a83d7f7602 GIT binary patch literal 340 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%&M7z$oqM;uunK>+P-6d6j1^Jed#6p9bI#)9|IbX%vzHkQiYh9Bz_0#WS07EE z!X?WDy2&Ng!{>i5=_=fBty?`P&TS z9q^A$37ia+1A&@-cl=(i?sRBy&QJMO|0resx0Q|p2fQ->w=?Xh%)hisvu@=lp$T2R zpBVGBk|Z7+Q)*-U=ySwKLDZ1P;Mwe~DPXgKI>FVdQ&MBb@0Q}jG%>V!Z literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..be6572be8b5939019c16b66b42d19779e168c429 GIT binary patch literal 681 zcmV;a0#^NrP)hBq8J-}wXY<6X{luVpEUq9}@@D2k#eit@igfD}Ch zBnub}Ke<7RWC4Srz*~Zn1q_A~v&19|*s(%C`y~t5F+xPPfWeUGlxzWm;R{LG0(OR) zASzkFjx-l&mMmb$O?Jx`uuEP@vVa}a#AOQ@44)a~fMfwX9@D{A$pQvLhSRbI>>g^! z^~s{40tUl(#_5zSV8l`Raf~k!fPC zrCFK@(@la0OpxLe=X3Y4YmB!(1duiacu+D6w93-`eeJCgL!?++~s}7RXSn zD~&!S%64Zzw~2#)6tZOjg^UnKIl*Nf@|uhTtwf1M`W!nn4mWd#8NyP#(I4C<$R?TX z^c)FVWezlXUbs5mH&h_UBX&t0>Q_1ENu%5lfk~pCb@>fgEw))+Qis5MuE|~QF8Ssb zvj%}%?2~%IG0zp>y{g!tK#`YpS$<>#0y#!4|5EXy`>f}CoSFs3IcfQye+p!{L} zA%QYWBrHk%C9vq5+uy1M3JeH*t@T@AhCZS18s~)J8OVVWL8%gg~M}LlGos z3+LB*(R$&9fkO9V%sl50oF{vBKF%D=QWQl|6h%=KMNt&xe1_p+vGYQcGFfhD@LD2&B1BHTYfqH~0 za#FBBL!-p#Zx<}k__^;oCq)a?Q=Z);SfHV?;N?UMz`*b~Oz}Cv0*&?DiCae{3m6zw z(pO~*)KlK0z&neU3K$svLB-U7V1dS(cd`LBNInS|7;L-a0Z8~LU|_h)@10(s73^4Q zz#HK)?=hG7yDt)-oNQoWw!hturETQ#fQp#ouh?mg2CZU>&W93cGYKz;0>49*I2~H9vT}w<6MrEfZcWdVrnQo>Q2%NF46j4IUBFsD`D=p%|6bZ*;958dHe+B zCXUc9Hc<%e2-z_(3Yg<9Y;%=s*lo6MrQtv$5%r}<-AT;y2VO#jtA`1r?n_SI_ILAx zCq^#N@Sj9Rs{#_4-IKn=N%salhj0J?#s6($*>gTIWp*-W;q#Jm4EoF#gO`}76>f!nB2?V zmB>snn-}1T)ETJ*jzupQxmOkY9rEF=bIpn(`w-Z4XRPS5*Pz=Q3H=`DpaN8k{FMHm z0u{RN`<1Lu0y|hi-jc*$0?Uz^{x&PXRk|newN_n#7myeFUQSUIMNt$*Q4~c{l&JC> XazT&(mz3V#00000NkvXXu0mjfB574~ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6fe35de1986bd77801d114a28df4912dc660fdfd GIT binary patch literal 317 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%&M7z{uk1;uunK>+KCku0sX_tq+%E z3%=KAifWmpClY>i^3JV~W*+PH5}$TVO-=a6{+NWgxlbC80d)g`gmV8&X9gyL1_l-d z21X7C2$MtN@+rqWyTeuUd^~SlhJz|4#1TP3!Vnvb^J?16S+%=en*j zOW*Qo)i$?>pZ)~oZhiGpeU-0!E^A11y4T08w{AJ@`{%LMl=aQOjIA%%=)TXo934IH z>NB5{N5pp7FqG`q?Osu@z3+S%(6J7?7_QsTQ9Z8=@kfm+KC+u0sX_Z4XOU zRv;g;#F$oD{jIE>SaxY#m{zSdYrYtkd(|Otf9>3@>+{ODzFedG zKI?LHblufuJGbqTyW}9x&v12_^2Pn4_tVW3fld|BZLt3Tu;-@@6WA{YuFLud8*Es= RCg?GU@9FC2vd$@?2>|-%j(q?C literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..951b9c4e20f58d324bf8ae143648b23042dbc436 GIT binary patch literal 293 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%xcg-gvq=hE&{odut=#!2pT2hg{tX zce+nzh;C1K!lu%bcSvF429M-~EM1|_rIY_${}Sojd+p-9$$QS%GvA-RC0N=OXao>A z=$+kiJ+xmx@8$g;^R{hbdLG?$uJze$AMN?i__@!U&oukTvScZfPqzKD?cV3UKaCCe z-riue{c_0N3CuprBIa&j_Axg-%vX^qRMzlvo!bq@WsHn00uBr~hyZ^F5zW=9b+LODu0sYA4G&i| z?^mC|$MwkX)xQ#v?k7KW8MCF z`L*fyS{M9ub(mLt`-JC(FBS@aSycA6U1vOBY|**uI^!$3zQ24MKIv%1v1?er5^!K( zWWhx|W#L$ux0K}$*KXCxhmDw&BAmdKI;Vst0BiVpR{#J2 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7436e5a9d5ebe030b2db7559fc58f99fbbcf98ed GIT binary patch literal 581 zcmV-L0=oT)P)19er65K~(ZXP2OsmnT#Rx81xDvNP;4g3`7%?lMS_siHs6~i^nu&{qLK#;B zYC@Wx-}Jbd3CcOgJNM|lXW)Il?RUR~P|YZ?T-R&%IdR{XBoX;T!dv`S{}BW-(Ft zMtz|-rua8K>)utrB2r+h<@grM*I44e<203k@A#c;1_)SI^A>m> z6yIVg=PU3rSm0-#@o#bDi8jYG3Jfqp#nW{{Eug~mnRdpi@#LS*q58q@vTN_E7EtkA z?+}Oa2`o9rE;j9c9iM<}2X1;u;BkBc+pP?xJ3fJGy9>CMPn7fH7>OiofCi zPMW1ae+q>|`YxE7oxs(6TFg|y7;dm|!I$Yq9kGyTVzvUt7{eg71OkPG)3O&Zh91g% zb4FibjIPA9@(`&1^e``Z&szP)_Jd8{@Pbl{Q}PoKLI@#*5JCtcgb+dqAp$r9vsF1o Tg!P#u00000NkvXXu0mjf7*`I* literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c2090863b0977f5eb89feec6e4105477f1d0033d GIT binary patch literal 614 zcmV-s0-61ZP)eXX5JCtcgb+dqA%qY@NI{57aZ_%fjbAV@dQWYk zB^&km0z_Y(lvQ+KEY+4(aWmlf%X7Mm6Ki|}gA;uiKMeHUV;#Sp;ZI8su^wD}Ijc8E z34bQ>ZDBf~`0-uGy0_IJAO*cQc637kA6$ZB{3-Eo>mQ{gS4Ucm)y zODLL3eELywHu2ZQscEf1f1^?9Lj1Bm3QxdskgYs9cM9|ZJjj|e`qpGK>fDrhU_EL|Qt8ZokLa6OjtAE@5oisy%=1dBOfxhGJnxVk;d|J#@KnQ(H zeHd*o=%IQ)O2>=^gb<=WkN)KX7w4p41ca#FUU{ar?4Pl)EG9?om5ac?(M_((kLaA< z*!Hob-rS->P)_2Ne>~dTgQxkFn=bU7l84R((83g=00k&Ofhw8EQb;3%08w-xfni?)87#~9I{y*3IMsen0(Y2` zx10^`rKcKL$qnCE5vSzDcNa=~Z=)IS0)cmd#sb@!ZVv)&1;%0?1ZE11#XShbHP3qk zjbXyaM7Eme`*qa$5a`ftZOwi9nnI_a#auV`bq7bdlzZ($FGevH_S3gtPA8EB)btkI zk@*$~ybD~I#`c6)Yk_^k@D=OD9Qr6vV8<|tp0R75LXt%iPbT4CV@9$SmT(h3mG6owkh<12iG+qiZ$m& zEC;yq-i79I<61yIF^i@DSPLZ{J=W%Mj*I66@d82!A%qY@2&q240PO8*L!^_rL;wH) M07*qoM6N<$g8c3ITL1t6 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b7ac96432de66d1a9160647e0d2d5eeaa456384d GIT binary patch literal 519 zcmV+i0{H!jP)L?Ns0&O8$eg6Izk@zlA9=wjHRA7Wvb$oXr#c*%6bdb7Ku z-_N@b@58*EA3Ki-000000002inoKPx7nF%P`#|ZTHi9u!?bwpv-{66sh^B9}(T4ke zHlFnF&uZ=cA$GQMt*%Z)2>jL`Is(@(~Yzr8>;TItwDlp;k{BT5o z$IFfbffW&`NzIBPr<>= zFMwT_1YS7*|3i3!0)a1ai%3jB^<=I=i3zkDjtlq|#|5~w!ZW~S2LxJ&5duvYR-wy? z0_r-k4>b4Yk5#Yoha)@jf&zL@;OFk&&O+*Ob}gz`mrFkl$LX#L!cC&i7B|(!GR2e1 z`#@sv0}x2B0KdB~aBVYvH4^HYh$x{`ivpAIMCcu6u>g7YiilsuS`8uui1TV>Mc`iW zeJo0Vh&+9+ZzN|z-G*m7dBLZ=Uj)(*1Rwx`Z|ieevO{s&rx>Szjj%lh8R53d*aav& za$JCj`;H6vS$=R@fQY=HW9wUpaY=0z{Gxmi0000000022BVQNV3YUCDLO{*2O=s{2MgL-HcJp>!HAbz1DiZ>OJUPV#SqC(L_@z{eRLPfO>}mDteGHLjnewV@f+rkl#p>;3m|ml}yS~y-M=RCuja=5XU#RX{{I(`VwOcsRm+m_HsLOtv`Yw7Nu|gwpgk{`&6hR8bnaETvF0TttpMM z$zSV>czc($T$7#63G>56=wVr| z!;;LzT)Hg4Dpa8jog>G#dB=~eg;7&gB3XJvb-Wj%(u+7u6Fs&$wtU!VtyLxp)1?CVOsw4P#&9b z%NmQkbCS-@(B5goszLeg)ldmkAb0ZC>u!ajfFbz{lh6!I{na|#Pn@X<`8l|Xvlq;? z&W%9c_NONLKSOXo5UXtB)V&tiynETx10^GgJL*nwOZ54#sM(*N9yDI@A~uwWJH+zg zdZ22Tv~;vZH74!JJfhYte$keePpXC6qh$J>^J@2Z9BpMery8)*boIZYOjOmFsZn)j zIj^YXC$Xg%K%KFRmfzd375z*kPu5kM6{!0UjU@5+{>87+80eTX zpua`10XDz}*g&id96e0NVFPS{4X^<=zy{a=8(;%$fDQB-P3jk0)CP!NY&|n&K)xU` zVCRjkfGuc%EfWUN6I7H^fp0WK2ChsBIv|HXC=Fo4&I?*(2H#K_!1CZ9&ZBPUO{~B! zHo_xPJMOv)>R6GlWAYOq!!QiPFbu;m48t%C^Iz}>u~S0uU;n9_00000NkvXXu0mjf D&4pP` literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..dc7cb683ce1c015b44fb187098b995b5b8ef3ba3 GIT binary patch literal 649 zcmV;40(Sk0P)DwRs5QiUT1m}Q+* znC2&#<0l5gXlT~6JfMm>j#nX>PqT2sdLu4}V7-UORH(<;H_Xbm-pMUqQj4{2s4&ft zyh8 z1>Rb$8|Im!M{b=N;*oXA?DB?7lIQzXmaOBx&uz)pdwIexhq2aw@RTfFawp9guZb+< zzQA2Fa>u=&hpbvBjm!AJebVmRu{AH|8?o*ffi180e^;??clHY$c?URR%H7h9yxTVz zzB+0>NH=wpQG#)B>Rp*601_>fiM&ez6hHwKAnvIf)EW^cz=;hP1yBG5@Co2E6hHwK zAaQ^?5s`I!Q8>UJ#p9fN*1c(=0N=UM{K?HtDxm-@#y^^iybl3j%f4&I1$N{Ev^=6C zXE{_A15j?8za|@CvTc4zHo&!i_{E`QfZ?;X%JKkmp#xkGeF_!Z<`-oz;`uh&UDC^_ zVZ3Gbu*2RoA%MM>x%(kBb0004=O~BLPFKnQl{d;tSRXWH@?0UyAS2`*kG*~qpFU>& z@?Y#%on4Z0_}6rnD7id2`Q+XHrqdgC^s?dEzU$kKq?q)y7OIpIyWhxpGKq)O`;97< jN~Kb%R4SE91Jo%+qBAxiici2iS;6S@&||z3Kl(-)TK6B5*xQ7 z*z8~Hp+wtsCsCN$zJd3B*J1bj8Q#8q^TYF4EEbE!VzF2(mOEO%qMn4YS9H?63h^!e zf)rAJ#WCxmqV7W%(m|+~4wVp9Y$h??7S~e%LnjD`>`4fe6Q}+O2*qUi1RQ%h%h#c z0Q-5`KdEBggfaVp%I+tvM!&vpuHGlP+fN}?-wd@LwbI#c5)+GYboyO}YT8)`hfINc%7=Qun1N1S$M7O<29iSr{e>$7H+P!I- z0Qdu!hL_y91Z_tE@MHF)qgmB-0bpOhYj$JSWGYz#7=1)1s^&JqtN>thY?l+01_;*1 zjSnNz0QK5GbehM60o45ewaVlGMAdPC#x>VdC>%Flg|z&aV`ex^z*Th4Gv2!5+ zU1OIV#apRz3;_h ju~;k?i^XEGSVr|5z)TxdX?-@f00000NkvXXu0mjf_8m|v literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..cdc926ca6fc9bc2de5ff4478a8b1f209e669fc80 GIT binary patch literal 799 zcmV+)1K|9LP)B5QUY`*%=_dPFX^nA|UHlFi+-aWt0d6g0bK@bE%5ClOG1VIplP#{h} zvn-G$N1mVL$nuU^t`m1%dMim@u}#@?yex?lvXT02)(~IhN@I-+X5IhAVpo8TGjU_y?+To{wWo(2CT%)!O`mxJ+IrwI?3FCKz*B9bxzJ+2)`fev99Rga zfOE5Iq56-_N*soaaHmsep=|B@HcEYJVn$>yD+DH30 zaZ^ShWdd7l=8tA?QrSmqZNcTo_+Xsw&PH=4-feB8Az11&!f__LTZxgNsjHMyT&yb1 zu3WhLmjA>0=xkAbFNLZL`B>wKkJa337wK#q`OQY<*k1VIo4K@bE% d5Cq}B;vahaJcK_JU={!X002ovPDHLkV1kFBhHU@< literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..0c730c8aa0945f02ca6e5dc905d8f18d31b5be87 GIT binary patch literal 850 zcmV-Y1FigtP)i7r5ng! zm5Bhz^hrO&BAz=T(95{1jC_Q?-7mRFk>kpQuHE}Qn?E%|^%lw&`Hh9EBh@b0^8Qc# zN)Ra_%&KcKPvUw2mVBb;!?TZR^?m?b@(5bc)K^1l*(^Oz$$#j=gKmX5GFHMcP)f}E z{(H+~`Ot2XQD8S24%e%a-5xA~IC$`TcJye6aa=3L3o zd7eKm%u|(eb;rejokHTS_vsVKAX^isUAlK-ESSFHOh~EvOL=#&6=(*(`;0LL+rk1; zp#32_2{3kxVP`3R-Ww|q7Z zAH~~UGMoqBPHFRR9>x$XhFlBnRf6aIhAv}_B`#T0voa6vZso&w>8-QGGmJys`XH~& zqpzFL?R(d5XwxI39w#iBr;|SKC?Zaa8auG<9E|fbd{fN(K^4Qm(fq z`R?E2i6nn4_B&Cq_n?k7sA3Ub=_$F_6@nlLf*=TjAP9mW c2;zU@58v;fro`(>^8f$<07*qoM6N<$g1PXU+5i9m literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d8e060d62e9f1a638eafaf58667a7fa10de246a4 GIT binary patch literal 326 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%&M7z{u+LOju0sYA?GNLZ zaQu@z;wT>O9~AC*@GNtMM!NXHqrOLqEjk`BzEZm7*lMvu?4p_g^Ly*IS#|x4f>E6v z5U}XXkGto-nS?(-%sMme*_Z7Mx34{V{3HL+m6UqU4W?_a{C#xDCwVeE>y5T+tfxb- z?!TjOeQ!c(j^6(BlcuC>pR;dUmEzW*wAEco{0t0>I{tItoV#4{(x>^pbKc$Is%QGs z!vu6R4se8ZN7YP4e)b#Hj53E9&EgpR&RgA;+pKnL@F literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e78aa483032b88683251aa460460c5169cd714ba GIT binary patch literal 328 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD3?#3*wSy!=0(?STfiwiDsHi9?D9Fgj7#kaF zYinCuTLT$}hK4}45f~U47yw0pOrRiy0TBmsfefGsTyTAL_yVAzG9^KN!3@kS>>Qk2 zJiPp(61GmR#iivHon75MeG?~5o^_&1VLnh@yQhm|NX4zUXImE@Vh}iRVfF6n{Qs8w zwZjf7<~O+fV`5<7$o{*$^U0DwQRXI%b0&nooOW!I^3A*bf#njr<=3rw_#)@tS$>HJ z9Cvw&c;+@TGPChW7$h8M05fV{lwQ&}exf8u@pwmRkk)a7s<{R;ibS^0eet_UMfqg) j-DI8M;CqY=411UsoY~v-`{6AsPyCNDy>H*l-J81sA%qY@2qAbrfpH z68jldKmrnwfCMBU0SWvcy-+i}<{@wRiLstGE^<61fwv61sb#O|;e%}RNxS6zJH_J1>1tv>~SW|(i%!ucH z3w+3o_~^I5#>|Kfehci%jM(9~z=_O=qkapV%#1kgc?E2L+OQpLLUBz6VumfPK^$V8 z@2$j6?%KA*r@z(iu{VTn^avxiZ{=Tg=4=lmWp7}}ciAJ)54wwd;x^+;_fV{ZK6d9h zd$?m&2uv2ZCG-tw%{^94X6ek+k$Na9 zS^@W)3w)^|{jTQIFLLSc5@%_vq!J6f<0+$g(icJqA%qY@2q8qU`~{^Na;=7I7$g7y N002ovPDHLkV1i^K8lwOJ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..fe8bfde9bd9ddce9f92c2e3d5220a278b6fe59e3 GIT binary patch literal 508 zcmV!{*WjtN$n6FB#ff6gk1NpZ7+q1?UwCK zGx+_y^FH2Z_szbUWf=ni000000002MRA3b=;*4H|7fHBcpT!hbzdj=#A}akecuk#x z6@B@B4Cqi%{eJU!Qo>cFAkHNTc+|J|6bKA0-2rtckMWJ9U@Z*Yv2HYZzJ{}9xPWk- zBeMnkwNN}TT0rt>Qz9U9w5bx1HrkX4_>E+`fCm8xKtQU1{D1%iAOHafKmY;|FkGNv zm-!OSm*;XBr$lwL1a9p7=+*WHzcxgGTAQh{pC*%ZWrDyniwESm*Is~DgK`vBG#8+Y zuq^yUYXQC!Dp1o}AfFBuSkPMF2VXT~S_@Xtp)7muq^qs;fq*Q^?r?>hGsd| zTc97@M0}0jbf3f>W__Q7+9qt~n&UTfg;~)sWIIiFxy;B!h!qy8W72`f6}L|5LxKdg zssHZ>nu-1rdh+A)3Mu literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6b30493cf14d2a6a3c8b35f268853b49099ab1d0 GIT binary patch literal 826 zcmV-A1I7G_P)-gJfIOKKdUud_EZmn~IL(!3m+=UkG4?GB7bRn>Xnp$6!9Sj1Ni~n6svCCI+*GuLgHPMt-L?lCQ;N-!!;eG)Wl9Sq9o;vQyUd zACNFGVa8S+{3(M!En#5R5Rj{bKVtB|Nf=l@-t*MO@3n^DzfWZ2<)u2efTn>kc?MdO zZXhddVC9d_fq$O_JU3o{r4IhEdAO0zfoFB_C#7@X>hGMbIAgJgBn)(05=LXPZ;0=O zf`C_sh>bYa9-56MpiDt(JKb*`OyZme@>Z!!>P$uh>(($Di^#ievF0Vu#2r@6XP>)~ zbHxNwqoo_wM+kOzY-@V$okEegk}ONveB`D(S*OTt7G158_`^mV2g za<^0t!eY|q!Z`|FMq3OEp|{Z%<8d7suqbF2Q|Coj8@ugBNnO_ZfEunyQ4Q z`-Ei+s@mIM4R_3ap-K2eX8+=hTDIhN_>=ff6Fx^3zG~CZ=;r%=awGU%IeS22zB{4i z6Nni6MdfrIhY19LqY7x3IJ;Jlgc|<&SqcGbYT4?;0hI)dn-meSqkI7;`uY64a{dQ_ z06c2-Jbkqw)2a&+RAj{gYJIkVssKB70y3$X3lM2y0eJj3V}PI%z^;+&>n@@b38|miz z{;dRTQLlvt1AC#WyV6d?(;;{wzxfx88T!?hNMj|ib8E1Ir**{Q(9gRvtmEezDYI8PUGJ=QUYE@5U}Np3R`ogk^$@X{5*c{ z{f7qkuM=lB>AO>QCoFp8930@!4#wA^+^V`F0q4H^C*4x;|BQ&?{48#2tMpUcgj}Fy z8`O5I9(d5mCM|Oi31JIc*qwXL<#Ac^1^1y5juakuLVoqffvSYBBWj0*wPk$x-h)7^(Ii->M;vu zUuipuo%w2M!%ED8*jL)>u{Hm?w8heS7=~dOhG7_nVHk$Fwfq7cm3(x3#|%p&m?1LQs+(3?-V(FffQ1CMAqWda&?8!br*& zAu~-YFB#~V?%(wF(1unC_JKs0#z4lt`9M2;V2m}IwKp+qZ1i}VT zga{{g{15VXV+D*Gd)+s1Lm;1_)qMlcga%%?Z(u@bVA6H@1w#1@425nRI3hJr=C*-q zseu}|4P1~KxaxvT?tz3BseyJrD{}0}?*l_3$ONh}Cp9pSlYyK6F&GAI$iI#?Ffc4* z4D}hmpTR`>iY$KP6KXT=yo?V>fq`KSjkfdrG`bPP9we2dXGQ{1l;Z)uWb>Jp-xVn4 zS7`YR3_k;Z?J+4oDlF7=#UXPBhAhZu7&ayUv=#Xb4ChS9&%>xS17pb7Z=lJVdUrO8 z^Y~<_rZgxz>=cO+qj8w z6d$>1uu_R3#=U>s_~g{WK~`^0d?8vTrNjzKq$~%qEGZ@0P-I5_u#SlW1{GM|X}5rK zOeq$^uZ89GfFjFR%|!)%glI*EP|8_6gP|Il0-XjHLbZb{xR01G^K?kpzJFY)a~Yw3 zOS8{jwBt6OhP3}TUg;id%~&0)d_$h@ZQNqjKpTw~11y}iX5hT3vv|ac{P*UHiGAsJ zS!Uy~$zrZ2#LFUirvIm|VyhM=ed#WNN<@`Qgv}Vh0mSgcm!4{v)Meb+fQj_dT({HG^kWr$i292*H! zl)s;9_Zbc}=67WCDNy_s`0J=jof!)gYO~ZzoEr^3KUya*nf(A z)qwcr(2u(6e#JDpFUS~5`ZDnZcJDr9IR=-G8re&3exkc`gW%KpCZK_@}1nvNwY1m zCV0)d3u}8qxv21n_a4SVbn;$A2HxefopQ=-;r0$9quhc9!?M#1Ba!W^{hn2zw) z2VWUafO8@K(0BsjFIxOc$9y!Nabv-YT>vdCEG#T6EG#T6EG+&*`~?^FSpn6RH+uj8 N002ovPDHLkV1m-<*eL)2 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..9bde62421f18489bdae6bac70b9b21a85c958855 GIT binary patch literal 784 zcmV+r1MmEaP)BRZ_)&m=&qXaPOo<2)A+dz)gESE;GMFJp zte8amVj9Pqm^Z$U&k@-I-Jt?^WDCSY1*T;Sq(cSLvIYFmk(Diw2o;!>9REDuLIv(ht^YBS z78A&)P3GQb*ebkFQeym@{IrbkQztF_S>BtKo-cvnztbWsd=ukF$Q0)&U^kD9Zzk9) zDSQRDjEFzzr*Q3x7Dk!n6LZY)m{wW${&dg^OPL$Tl~$$$jw;QKU%GjA?R?3Xn`_K*pz-;1PJ zHa?B9MOOUkP2!s)#|_G*J)Th}EaQ)eT9DQ)J62mU+NBoA9*g)F-9AYrJ;_)c#&fa- zGOi1}3l*4=EzoCC{ktR!9I`5-abi*h>=&|8kqepUQA^fR;TP9MzWJiT*z2#({-7e; zEK#AJ7e>%GR1x7g3&uAIfs=_S?c5_thAc_?iLD)IDgt`{iwv;Ca|h}eHKtjXE~nlF z(?-hpCFa>jfjbIOIt_(oQf~bYwG0F_wr;a+?TR8!FvJ++40Dnai4x6BZqizea$!Rg z;4{~VN|fjbcq<>@WK|zytIX*%Xx&`@D71e^J z`B2PQVEjoZ;;%?t-q6WN)9twhMKmzP6jMB;kz!9#6h%=KMNt$*afm-_RfJjb)^B?N O0000i^XEGSS%LHNolWbuQgP9`YfK~H#~UQ zlP`N4jS^4iXo)KDsuxw{a;nJ``JLGKSNy>xx17rc$akXH@sq!Q66d+=56R`@<-?;D z5n`<$-T#vh51!+&HXw&|2JnRU3FLJKXq9&cDrpUrc%Oi$y~>7%5Y{=%zlk3@1M(@? z{GXsgfB^E4X4(5-GLU$W3LKk?q``-PZ=`LQ6Y>eg=kwx4P zp(_{gTKQ$6r-PCB{sw1Qk@$B0=w@eLcaCf~>eua4)sUxlSQBgPe(pjCBL zL5z8u{FhXd^XllR2oF0n$Z2p7r{Wkvo=R|$m!s7?o2JF4fSD5wnCn;o@ zVOhw8NH(J!uomyq1D0>TGHpt)zaRa+NjAoD+G?eDiqH>ERD@(!ZYjUyIR;Z$X5=#} zz(tt~G$s=V15JN}&+gyFcvpikhjengjwVwnKn$|qgZBTBu~;k?i^XEGSS%KU_ydF? VF0}9OHH-iN002ovPDHLkV1o51i#q@S literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..933b3a34462171dce09be327ef6d132c073cd28c GIT binary patch literal 705 zcmV;y0zUnTP)?Su8TO~N69gF z6aEn~F>mQwTziYp#D&L|yYORTPUu)%f0Mb#1UO7Bct*?WLHgw{fkA4)1)qp4?$YC{ zuLO2D))pPmv$)RzQ(raEWvDGXs$b4Tyz-QQQgk?Dky`izVXZranN4t+fKs%WX|?d3 z?Hb|h8{j$trHHh0j(KOMHX?=v;k!&gDY}_*9#)rg&Q+sCIlD|iDY|^r_Oe24w0Wb2 zKcSaUo7)7GBIHsFs8nrCw6m{ir_|;;0j1bxY^FBaOtjx?@;Cg1+T15#sx~^AhvzhX z76(kdB#^7N$CcU|=Onq$dtd!QT5VHqlxl0idA_@C-fq{gwlDvm>;i3_*YmbpwN16~ zv)UTlG(xUw;gR<65P)ljA6vu5RO`3K9v4Bv94v{*zN^zd)JpwD zO+AE40|;@&G17-1%p4t{EIA`=gyb<*}2gBm)oSsET;)ErD z#Z&mEoMnO9XhVKbPJP^N#cHcKfGC^grE;FRYLjE-*MDf^f3;N~fOAl%k)GPZ(;q9( zYka%?SZxde=&H?cq>|@2`Kw56ECFPxO};477LC$D(gXR0sXkEDHgW2?+M3f@cAA-d z-L711!H+nzv$aKMCB0p#+9t{Er?o{RlkeAxLg+tFCQp{OOuk*2$kx^xmHzi>Yh6x1 zM_blYtSzh2)0VwxYs=E@uZgyFt1XMQrB`h!xnC2{ti25y+Vbegv>|DOw!FK~J+p=+ zx3+4F(=Vvny4%)jE7&it+V&ocslD3rwWw8XdE%uS0-ZCo7MG~DJo(mw=I&C}mMf20 z16V<|<%&0K0j#Ln^2BSF09I9PMcQHsKzZ{RR$I5)VhJGFTfW-5(-zYm==Ybawqk9u zy@f>2yh$jY#@b@^K+iYp^K`exl&*5dF9!es000000000001a*c_R*V0$XY(H00000 LNkvXXu0mjfLNW8-h56B3h>@1&;vo}O_xGb{Uk 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_toggle_played.png b/app/src/main/res/drawable-xxhdpi/ic_toggle_played.png new file mode 100644 index 0000000000000000000000000000000000000000..7a5c5be7a8f30b81cd52c8f209b094c891aae49c GIT binary patch literal 1047 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_toggle_star_outline_dark.png b/app/src/main/res/drawable-xxhdpi/ic_toggle_star_outline_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..64427ecfe6568a879306617d59ad2ac87bbf7757 GIT binary patch literal 991 zcmV<510ei~P)depo*FON^!0&)EX6FH1CCOR# zlWCi0U+|7}X8~mMI|N{g0%rj{4zVu~ymXTNZHw#+1UXKz@1ia&Koc%!0BoQqEI@$G z&HyN3CM>`#XPg1>HmrStpv?*PcSW=>5ain*;6+q`ZoAvx!BA9yNp{;CpfY3!v^av? zw(+irB)X#77YM$v(!KzNhV})5!}bOEY6_swuJ(&e*%t^(>{E65D8x@WH8k?gG(bO1 z)NlnqMfeg{t>GdC59wsE2`@f;lu*h|>gb?@Nz-=C#0(vDP{%DwIgSq>oAHuCIv!G3 z9*g~YnrWepPP!Rn*kE~-9kYxuNDrTAr-l2}FC9SGE0zvW!KiKQk5duL3^-&P06*?n z6H`AyORWb9a3G$QD2ET00`$hc`6GpQ5h|?2uS6e6m5~oJXtV_20U5GVnwv|??O%^^ z;zrykpJhUs`Dw~Xmc6lXlV?zE$%;`5rGEPJAhz=*;r9CyTKJP?P_)Mzt!z#77J3a& z6K4M{>r_+$9`1&mzQ~CQ8d#&Mka2T)LH|Ggq;-uIYOAr6j7VH}%B(0T3kXQDKfpe# zYT`Xo0QOo})R#&DIBy-mJt+WBtg^p~b}0azthNTgD{T^ISp$$OsW&uf(Xj{Tq~(Ef zYXBZf19)Z)zz=BvLsr<&lDZelRu51v6`;y)r;$s}1*+|@Gj$phWQ;LWy}fbPtIh$# zCTzW+hM)B^aKGO#qvk-x1G+!P!3GiJG<7N92jvPf;{!4a9gUxZ0LiboxQFG63T+W5o~FI9|%eLhzm zKxNpD+D_BYZwY&+t6CjE-y-wntXk3tsg&_~;m=Q%{mcb7LTgEjkZTY#yh44;L6!GEkrKP2%rKRJLahU41D3@wNZ} N002ovPDHLkV1nz<%O?N; literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_toggle_star_outline_light.png b/app/src/main/res/drawable-xxhdpi/ic_toggle_star_outline_light.png new file mode 100644 index 0000000000000000000000000000000000000000..94def1b7eb972c77fb3ee2b76dfff270ffac7f1a GIT binary patch literal 1065 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..130e3da47ff5e8b2df5e0b3a951e981254e1df82 GIT binary patch literal 652 zcmV;70(1R|P)Og#kJGFZ1V;V;NFppC{!w ziTyvHlRVO5e=f24a|N#ylr1lt+HdMz;gZwBrNvbCV=a5`x)N7X+m8!+)|GvcQyT&J zESPZNzo&7^Ms_5-H!pWk@A;SLO^()OEO>3xd{aMi=|)oI>IIXI?o#GEw2_VVzbX%A zSgspDK!2YyEGNBhHS*5;c5US@MSxhe;F*pDaFYzlWRAN$P+A7wRo=#N;mPtWqR z-=Zp4tV~D~*RwC(wiWSJ#=NeSv!=l{#cqfP={}zu{L5{ChUU{8U7wuWkM+d#ntn}% z{YXxwX!iW?=?D;OwBPx-oKh8&03Y-OkZ%ST9zIm`1|s3)0U}MJryU+Z){+Om4M21M?jopj1{B;4t|pH>@hay2&)UrKpHamM+Cx3dU*kgRIeq`nayd{!gQE z&hz{cB>|?5FEzmn4FQI)m@ifOWYw|#GYZq~g@)W-E$O+9-+*hb`VlSJ*>82xf|CGq zX*WZy9cf&)b2Wd``1ZSPFWJLzC68U5?8bG>S_%Jv(2&|F?#UNf~;t~=1%ca_(p8r3b}E^oujD^+`?suQI_-FB44eI@A5$8inaMWMrgQV%M=?A>i!C!CLv`|RR|_{!+r>vW z8k=42IOm)$l$qh_xj3o2u!Nqv=1siq7eOV literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e6ec9a95e466e50eb74b1bf3e72832132917a71b GIT binary patch literal 435 zcmV;k0ZjghP)vnyyb`2TX( zG;q?MYv0blh1NLrK#`j1}tH>&S;*$Aw z_P)CA*D~_9@U9Pk|N4IUGB$CAO(aeC;ca2K`YhPSPxDMivA_10ZFJ>^-WMHQ?ytCc zdVbS69l$keBN_OO|AW`PD-`Cj8SXRXo6!NsrSD1sUlYb#0@vEN2?u%Hwd3l<#OEvfgJ9*0Wn-9^mSJNW;y zHFxk{^1gYX#sB~S000000000nQKl^I0~ESWx%|FLl^UelJcUYGSyk#J*GS5AD*Nl| zsFl_Mj-2wXtZq^&|5o$h0CU}^Q+^qgZ)H{LF8WzeOBXdye|7buo`qm^ttR^Lez}VH zEvWE{ooQct!CQzm+SK=Yf2JK{jM1(>y;&H<*gS<-{z&=q?v{qmz1X6yruSd8?X1Z4 zW(H6wkbk#i0x=l?ZjJ6=Q#`WbJV~jTR7FD#>#MblQ<^PQH zV?@!WW3QNzJG!-da=m)HF4uQ}6Ysm9H`>+qW}&;z7CO+E8(R&JFj8IzJpcdz literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/main_select_tabs_light.png b/app/src/main/res/drawable-xxhdpi/main_select_tabs_light.png new file mode 100644 index 0000000000000000000000000000000000000000..3f31efab06604d2674f4382da03a025e73f9de95 GIT binary patch literal 428 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%&M7z*y|*;uunK>+Lmr@52rf$3K>B zQR{xNU%WnyKH~Xqo6YGYcFO!{2TE8tM=P{ z55#nfkUbm(M?a{@6x9B=^-3tu&F=dRt-kUq>a=C3#=Ge7`$h@_Fafv)O-s+vxNK z2u82onqX(x_18Y?v&akMsaKP;MJu;XbVK=Y4RRnA}+gW?1MjG^FoiEET)o TY`wSUIY`jc)z4*}Q$iB}bKk#- literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..0ea774fbb785e309d267fef80a736c4e415f3a62 GIT binary patch literal 941 zcmeAS@N?(olHy`uVBq!ia0vp^2SAvE8Azrw%`pX14+DHcT!HleVGY)-V)_j9q<2Y> zUoZnBGYcy_2PYRd4Wqm_yTgSvH zQ)kaxvUJ(X^&2*B+O~7gz5@pjA31vbdK!D}& znHrgAGR~XMT=-vqAya2s>uKH4zYE0HQ8vlmCB624xxx8#0v z_j-!Xh9B?3{f{w4ho_#5Ncg=^IlXc5^u8WezC7+p>kmBpIXgwC;K!9In-Bbx2g<*E zoDb9%6P}`DQ2%MKc5>sEUq85XYV^F1G2Ipywd9uByCqde;Hc{{rofmvtc!L?aO=#O zsv6&TbaH!F4{K1#2T_lU2O|=67d3A@@XI&&9Mf-p-Je`8g)dJ=D6F;H$E0Z`!TrV! zs4T+bx1uC>PQ1udpm9ed6hy_78|VI>)x#QQ_vg*&h=l(O6@PLqd6A$q0cezi2vGLt zV{@Pb|DIkwhc)S|p%~Df@B~YcJ)cs2&oS-ZUJAD76vU{A+8$Q3N1NK#ANcbTs7+x} z^JWJZ_T$+`i-FG0u`hY5EwI|(o#TUW(LdqMJ#4@Gd3hNFn+(Kr?nEAc`QSx% z^rZ5oI-ZNJY+jqRcjM7(N&BQMIa4k+G4u7!SXOZ2xS=-t-NWnMV!l1t)%(A7xlPm# z&L^(z+0$cc=Dxd{tXE(B*Uq(j+7V$9IY!^_l4AT1E4p6%moTZA@5(Lb@>!+!#iGLz zY;4Isi6)$f6D7_+wpY-+@&C1sfQ+5_a?JxXwmo>T?t!kr#YM`7I+F_LI&YlO#iyJt kDP{8}%VauAv@`r?e1D?$=k~iN{(_Q%r>mdKI;Vst0P8Xi2><{9 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..303616f52029c831cd692ed78fa898b25f335ac2 GIT binary patch literal 910 zcmeAS@N?(olHy`uVBq!ia0vp^2SAvE8Azrw%`pX1mjZl3T!Az)Fg7;U*49>0QBhD( zkdcuAa`p7|fFcG420#+X*3r??*VhL!OifK8vPMQmKv|%muCA_$i3v~;C<5dH1%V78 zfXD(FhK7b<0T2OE24nzbfdI$=%0dKzB#;dO$SQ#%KrUPas0;|;B5?Nl*f<}cm)uH% z{DK)6nV4Bv+1NQaxwv@*gv7+9Wn|?PRaDh9baeF$t!$lK-F*B4gTkYt6H-#sa`OvH z%4=%tn_F7jI=g%OCrq3?W!m(avsN~xd}m-_QuTCk45_&Fb`I;h#fB0`x&A32dpeMsA)pv86z_S^WaK&D08HBm?)9M@(Wb=GA)~U~-=QU6FgrRiG0VGlwtO z@4~*p;s3en6HLOt{Vu6_W28YPn15Ag1_j86XsQzce$UWtSgO~=;MFHL1 z5eaKV#5Wwc5ojILxarrHMGf|fpXT34-C|JsXQqTIPw{?<<@^^4cNvs_o7)nrIe&7* ztGi*2`8hkc-iYb-VCv0y$k^`g^Z&tw^T}cSyXtFAKW%Eip8G?5-mHsFY8P~@;&y*| z^m_l^sy9#b*WbUM@p0-8mZRjscSt3SK0?Xx3BVaPi<#YBKOp5#~`> zDSor3{_^9aX)L8_b3SkUR(&q+{Cjx|asKqRo6p5nuU)C>IY|XKTH^PZyG*_IQ}mpK z$&Al-9I{^H_v4B37mM?D%q-h2Pk-iQY6K7@!F|*Wv`1WD- z6GkSH8J%jz4h6IQ+hb*UL>5%9Zf0ceNhmcaXvka3*T!M+WQL7PL+jOplee;SRK+~< zy{5v#@$V^5bTp52~W$4ba&DT9pv7hB?QbYT;dD|rx+zc+v(Ai$TRq8>b;m)TF2X*b9-MqzY zvieD`CNNcOx!-Um|G>;!`x%RUtl58PueeltpyxG1_@RH|PwE~2Nx%BcFrWR0%bD~; zuJbwT<9-`Il)Jw}TS>p+cT*7a4r4t-hxv=&cYJTF3;2C6pxj-Z?Ze-OD-8P-CrcbC zKN7@z=gE$I&u6SBYWyp08*FFVknXyK?Szf)(~7stm!2_xUFlfKXk(~t=4FD~~hy}xPs^79o{RdEdO`8{|hR5u?h zd260g(e9Jga3*?6TGoEolhr^wx7hruntZRBagVo=p~I;cuX9&%Gw$)XFm-s=d+YMO k6}q^y5io}>@!QF+`dC9c*6u|YFo!dEy85}Sb4q9e01s`;ApigX literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/media_fastforward_light.png b/app/src/main/res/drawable-xxhdpi/media_fastforward_light.png new file mode 100644 index 0000000000000000000000000000000000000000..f0e01323acf7e830ff6c8f1e20d50f039ca04da5 GIT binary patch literal 1100 zcmeAS@N?(olHy`uVBq!ia0vp^2SAvE1xWt5x}=AJfkoKU#WAGf*4w#1vxOZ+8XoJ0 z2I|~ncXicWdGei)&Y}*l)%!yPvu(AyGHe%icm#6Ag@lAm=?csJ7nN?dHDI=x?Wf(Go5LlQ6fG0KW9f> z^`e+BaSfSjYSYWSIW{cxP!nG2-H@9fvb}?0q0baM#?Zf^{BPa}2q;YI4AMBT#XigA z=c@(t7{1Tcn9KC0-tgnA1^XJJ)IP1SitupwWLW9QXnfVNaO-=f9ES?=Ilfx*I$8>= z_)hKKBQ8+eb$svKcdRaNC)-W_t+jI_!({%rRY8A}C#!N8{8f3@h@86nS6dr@`;=O)>Gi(_~n*4w}IjJ?e$wv zRoG|5H@szkIQiM>B^E3atoIL}u3G$_=}r3&iFsLhZ1!@Dff6#>^1$E>{NR1}rYGJ( zd!|M|&_s##Urbg_nk;akM(xx}hRgd_a(9QbpX4|ZQ#xTrL)%ryA||6ySrv^1jy`@t z4sENNQ&?RLE4Rd~na(dH7Qo&7ytMr}Q&#t@s)T<`eawuJH=ez4X?V&0dAp^R;Dmq9 z0eLJ}ZdbkYaoEN0xKsGU|7*qNOlf?MUzZhCPxodlPTmyFXeQixJ$T37(;FLBc&&S@ zQ>1rbTlN=*lX3=cD)|len|kmih&)M`U7-59q3-@`2Cc+rO*42?&8}?z+Msy5)l$Kh}Q0DHHb|89E1Qjk1t0mDV> z$5%qnzwdc`%_Wd=()YG!8AtA~V#qruDstfN#Ys%dwmfaT)a-nuAG*$?DJab)6~y&)^>bP0l+XkK*x$+H literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..28d3723042e50e0bfc45b7fa1ad50b0bda7f7bab GIT binary patch literal 924 zcmeAS@N?(olHy`uVBq!ia0vp^2SAvE1xWt5x}=AJfjQaJ#WAGf*4w-G*};htZ6B4j z{w;kQva2JwyV+Y%{q+O(4G*p@>~n1|IW3?bD-!OwykRnzUoT&4gNW+X<|EII@A7So zR%x3v=eOdlT$;D_!SWr8^Sl0AzWjUHdH$DxaE1?z zEq+(7@-oygEZq2O*K%%#9SlwjlVvkR8RQ*SgoI}^{9tfOHO_Z^%gE8n_9{adD6CPk z#C9#i17?oiFEP;|`Gx$uI2(i&zQpmD2m<9ZR;Wj@A7B^wwbcC9HA{vV2HqW_Tjdjs zR^7N-39@J5x63l2PnZOL&3v{(Nxs3OxN)+$LlgI$I}HEy3V;ss$#X3a-YNQ&Q$T9f z^0y*lGj}qn7_T}Zsn8kddXFJ}ao1{)rOLKfO+lswe~samVK`?eJ44KZVMe|y!wh#( z1|R9r9FPlJ?_D!r#gOppXV7&fc81Ro+;DUIqEe97`&G+-@8W6TRuK8MaZeyC!!sx@ z)(1O7%wqfMI|~^N{{LL#epR=C!Qg%ugF(L*LqthPAt%Fg%ZrbXaxg@EsR>V?$jZkM zT4NQF(8%0S3B*6cSRAINFX&sx{J?hobLmiq1UV>a4(mB7hI{=Pdm~vGbiPm7 zoaYKm9P1Sre)!MG)?m0=Cs2Ki-Ox>t=P|>IdYRf27Pq!fs66U)tbE_5NhzB1H5uxs z9g3Jn++#90xqs#Y?vjk!w^zfk+W2=fqi=vV@1hE!G`v7X~v40y`m3FDuL#u$;Txo zu|80>b7P20PGx=Ing?X0X0Sd;o#)oD@z*WS7t0^Co@2Peen9(dFN3LW1jCQ(YnLx$ zZZTl=?Y$5d$nb#C zFVdQ&MBb@0Dv!K$N&HU literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c76e11685ba49616a9c0b1bf7de6237ce9bb386c GIT binary patch literal 907 zcmeAS@N?(olHy`uVBq!ia0vp^2SAvE8Azrw%`pX1mjZl3T!FNak&&^nvA(`OkO5&E z8X9VAYa19CK)66PK(>yK4qR46MFq$PY5__C^#Vzt79apB0|H%LU7(H8lm2KrT={ zP#maUvot#kXoOoykY6yvtX{R5-Ew|BR{hf%1;oRn%GswFvNSW*=DMnCOjG1YEpeKd zA~v}*nk%2xHccq7g}J`Qhnr2MKu@RDZ31V4Y>;l7w9FO7g@p_ZOiG?Ejv*Dd-p*m| zi*^)Y`5&E@mZsJ|v-rQZt8G%G$h}Fc^8(rezaMz*f((95oV@W}zOhZn-;(;STQg#k zxUXFKqZz%xjqm=K156M9+$`Sa@b;Yl2G*?aDLpy@a_{y}WIc52qH6Slb9Qw~+%3zC z3mT(hB2&4qy!fFLyCp|)w907|k=^Fd#TThC z=lz<^tXU-wQ~7l-EMkpb;L5$lVbM_`prOaU-()J?W^i0=#Y7R=O%Bn$H<)yzfJQv| z(5EA?uI$=Prmtn$O}Y~n9Tv-QZPyJ5{=7M-QL6ImFV+YJ8T~UGS);Z%aBs;tDCTid zk-KH#LNCSFtclI`iz5Dkkb6+U|x@UnX&|P85n;a^3{C5!J z*j{a%)9Avz%>m>zQQ0jHUFTSUUK3Zl!L)VuRS(7|{wM4=o__K1=$ybq2H#iP)VLjg z`INbA;m=*Bi7VanR`1yO^jcDM?FOb!KDmO5BVXS0|FlW_y1;t%`G&Y}A9nSM&x;M0IwQ>6Xy><0vhUP2$18P{F7_W(sudV5Nk$hp|VST%ff3A%4mn`35 zF^$nu;_yWkzV^(E2N^vN2!`@;7W>>j{QP}v1J|2>MI{9hOCDPV$JFgKx7u`V;X(HP gw4BWy$lwn{d8mtCecWbsVESY5boFyt=akR{00SIRtN;K2 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2dd83dbc3909821592562f838b20c2ccf8183189 GIT binary patch literal 208 zcmeAS@N?(olHy`uVBq!ia0vp^2SAvG8AvYpRA>UE_yc@GT!Hj|B+&14F9RsVS`y?J z%-|3@^K4n1Z6{D5+tbA{q$2L^t%bY{iVQ3V^&bjLIRr3p@+!R8ulC1Mq$9HSq)Ea3 z*PfG9JSVw*Qx7P**xxYqDod1}8c;V8;LaY``~D!!TNpnpg<2}>@!bLG@^tlcS?83{ F1OUTcJ#+v7 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a31cace6b8c03d4c63ddbdf4120e26378626b9a9 GIT binary patch literal 208 zcmeAS@N?(olHy`uVBq!ia0vp^2SAvG8AvYpRA>UE_yc@GT!FL!7#JHH8yOkRnehuG z#99*M7tCPbP&_M4NO?a{AluW$F{C2y?X88p42lda2lXEcOF0BEaPlg=*su1-QlulY z_M}O{{nwt8R6HlSeNzu8y4c?^^(sr0o*Ga$5#Y`q*Zck;&082hD}`Dr>+#(I>GE{- Kb6Mw<&;$UI>@!^e literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/media_repeat_all_dark.png b/app/src/main/res/drawable-xxhdpi/media_repeat_all_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..70866c83ff71fab1e9389988a2c632a70d9d2d41 GIT binary patch literal 1877 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/media_repeat_off_dark.png b/app/src/main/res/drawable-xxhdpi/media_repeat_off_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..a8a2043c1670f73ee3fe057afb9f4e31b9f62d7c GIT binary patch literal 798 zcmeAS@N?(olHy`uVBq!ia0vp^2SAvE8Azrw%`pX169RlfT!Hle!33r;NPYmi$)F_2 zFPPyh&pIySWh`N3C-(6jQk!{l#n$6uwoDuBas(3>aPD4esnDL#?8cUTjH9oJxl_A$ z`3mU{1_mZxPZ!6Kid%1Qv#v^Uy`||5@+yB1LyI}n3!ZM8?5C2&I5OR8* zo0oLX^7))%B;u`&d0F)Ln>QQYdRZuh0qGSj%t=!V9Ilwk2=vQ8V2Cvm6POy)P;}fbPSy-|u)M)X(YL41O*psS$#?IQx$)J`R+ioWSIyb_>tiN+jzdTh<1I#M zfvFJ~cYaF4v-8Xu|0Xjo7vC*iUT9VOk7>o*wT-i{{8`9w zR-Ws}EorF**Y(vLzI(I2sGG@fc0SjIKTry&U_Pe?P*FscGIPoP5THpcb29z~IDBhk zi}`kpO@oWKZ{nx>CFcF_4b1HuM?x}f8_3DWXqYvc-}h0;rwr16NT*A zJTG=~nJ8#qXoz8zyztrLLc^K{rjq>`4q+1*XW5HR5CuAwpS9*siA=_UvxNnWRev8b zZEE7V@ORbf&xYqGavEg*d-W#eOmPa+nVJ88EMox40X?y|E)b}65z{_VnG50?4sZ33 zvSqLX1EK*KbRG?53%a6@(u$ALA^eHxIX5)`xV%;)8(9pStEG!~es7(8A5 KT-G@yGywo?9H@Z+ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/media_repeat_off_light.png b/app/src/main/res/drawable-xxhdpi/media_repeat_off_light.png new file mode 100644 index 0000000000000000000000000000000000000000..0a96bd4f035e14fd67468caf7e0027024e5c7d4c GIT binary patch literal 782 zcmeAS@N?(olHy`uVBq!ia0vp^2SAvE8Azrw%`pX1?EyX^u0Yzz$Vf&;#@N_cMMXtl zU*FKs5GV-b0s)W>l!dTOO-&&Tpa=vQ7#IK<5XI{1>OeM7fws0bkc5~6)b8Nm03;y* zq6?w`s25^2P!Py&I(#Y+$k#0i@(X5|#o5fRR?F--EwZ0$l5Fwx`anH_7RCG@V^yyW!ifMET~&#C51Hh0yp znzQxS$4vGdhma!1Ta3~IQzIJ67}$@LTReCt#=GL_hJ}pY`2h{j`q?u6U1l&BzrA<& zj+u7#j902mGbYcnmss%AnbE$gnd!?PR@N7F3mNX}i-3e?*+VI*1wV}$mjXq+^Aj50 zP3OAsXJLc!S$VD#_bxQNzPEsZl~p3)z~_loYE0|o`>q}?y{!!P@AgXj!1vPob?&P( z^B6qHY-aniCw?JgWVX?Q=Z*^)BOBSCI8I=+^=IZi!KLBwy_+qCRdT`4V8%#Cpfdvw zy#ATFfU)Y2OT)bVI~vv~FqiCq^{VK={}ay`P3l9_ng6+)F)sMopKGL`eS!h#mJ6T7 zMHd|RXZ@pNu|Pkd!78SU_X6JrhPMpB;G4kk*5PG+it?{tYkgPlulbW-wC0z(hQrsT zD;PK*FvM)!Ko(>KQ-_x1)_feX9~@V8#mw6r1gZT12^?L_ql0Xx~&v@C6K zQeJf+%)wDKf?LDHsc~(C)6RFBm8LOq)n0YJb|i@5?Tr{P4o%Q&&@vEr5S_ujfa?sa z!K|6w2`0wk0)-}*dA|yEmCE{x3aph~y?pzx838<{QA!G7)_S@>O_cpaEkp&DbKT+U z2x9GHJ+gx7I+IdQ+BsH{s$PcE6-#GxYvWcxTSmtnbO#Iu&nv!^q%~zyg zO>D08k#&91lm7;@%4C^d>^kEVJF7%pA?Ta6$OpYUt(DnivV=J^AWWOFy`}c^c zZSBc_ub8$heE!Sq>z9vzm~NG72Z=K7TI#=4od5ZI)wjEZ*Gy`-t$H)x!e4=bk;8$3 zDNU$g!DPcigXRX_+I?SEz5mtPPy5vR}}! zGFhCd=eQn=iNb@KmlxMdFvg~GYzW=3>`bocksp$;c5od!!x*<(;oM4&<)H$vVlL=} zCIHR!EcCyg6w6z&M*h#$8Ec!|IX1j!wPXs)JdPv~H{ttFv=D^mc)Z9Yl+i!E=K_CNd%q{`FP&t;ucLK6Tn CczyK% literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/media_repeat_single_light.png b/app/src/main/res/drawable-xxhdpi/media_repeat_single_light.png new file mode 100644 index 0000000000000000000000000000000000000000..c02ab24c4e89a99d73f7eaf34043f2ab342a6a3f GIT binary patch literal 962 zcmeAS@N?(olHy`uVBq!ia0vp^2SAvE8Azrw%`pX1djotzT!FNak&%pyjIpt?ii(Q9 zzP_QMAy5#=1p*)&Nb2b507+9*Q=m$aC>TK007W1GsM5f|04M_!FmiBk01CnZP)b`{ z8^~2xR|m3z76DZPxp4JBSs)u?0z?+#HeFp^pfaF1(0m|(2triW7B7DUQQltd1kb4+scfBD+0yAO zVS-Xkp$bfsXJ$k-EZoe%z*Oq#;uunK>+S88~&sic7cOLl2T(I^Dl)iA@Q$`>y_zGLgoFj~hW}VDVal-5^ zmum|d)FZ_NiY~EkU=D3~$HxGq7_27%d4UX1O$!~4MsNAuth6?tbyd|Qc7Au)sB?`L zC#QzR+m$WgN?_l`0H!uElqqm6P=C}=t$wM&X5M8x=EpZQnP=>mYxo(?mXpK3{_Z!o zuRoa=)UAJ@Hgms5!_RIexubqk2i{IsYxumH>%pHarWyOS8h&O&sBWei^~DWGA3i(v z{2u?gpO!O!nzKjjFs=P}{mZXE|F^It{Hj)r;dt<88(YFJafTW7!3{@Qm(|-P0M)Tt z)R}RX=qVgs=Xl`r^hP}k^^Mtr2BteD9M~ilaNd}^?5_R49go>B$5;Pe7kc2)MIKq* zvi)DbeBAE8)|Z`OlCLk%k9x_321YgsAko}hX(hqI^lwLWoINv-!GjzH-j0_W)OkI2 zuIK5hH4EL3?VS_n*uZeSeC{ zf<02S!9nJLC)w}>j2h*?qysL#C$~$=+07MXw++%%;QkX&X7{RVGZYk i8CgtdiDpi*JmW4$qx>C}9ruB0nZeW5&t;ucLK6TtLSsDu literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/media_rewind_dark.png b/app/src/main/res/drawable-xxhdpi/media_rewind_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..6fcaa1574263b4bccf2ef36174ba7b466b465ee7 GIT binary patch literal 1110 zcmaKsjWgSK0LH)Z3kj)rw<03fddnr_qR5LwND;|4lngg-sUF(%l9p>pFu$7Xp>@%! zp5WH#teAP(*`b^8W2VHn)Y7$2XjVfh2`v-dWJfF{B@I2*_;T%&FdlLYFDK{)Q z>eR^p#iCDn@MqIK0MG<(a1cL#erqu?gU@kzzUzI5Z&}H9CttgCHJBW=UP%gTJbRuk zw+M>VenEOdDtUq}Y3xaHmshdRp=}H;8{=BaVj}O^9Lcsb-iy0GEzhQDGSvz#Q~z)O zZI(hf;ir{)d3if?AABTDaNHPofXgqSu>M&(PNu%Xo4K|4y+k=u>vLB-RaD8fKPre| zHQ_b!D?1F5*71hbet@DXVwG93<38Y4&fp^YJ|u8=lWy_HJwYfFNzScfrGU?=uj-jK zdYo+f$Q26{lm*Q>Nhrl;krM_XyNcc?tu+|rH@~a22l3Cw+dOLY#xnm^;qT_B)Fm+` zAZ|&z%Q2U2)>g-YLR$JL(FerbC}oLbvc=#>m4!r13ra2)5ur9%#+C02z#NJ-epf9&9pxTQ;m`T?d((UPDd12#sizB;`R6L}IAD znc&WxzfRIqhU#im)lMq`Z*O1-65zbPr_d>Ilw3FoY*Sgq`{2=*euoegzED^ZKLkJmCPrl2lH(5vTsR`D7Gs_f(p)$( zI7FWH1OY1=AQuu*XngL516b&@``Oq8gVYJGgeOAS`J+c?;J3#%nTpnQaJyjH3blj! zwJ(vo_6s^PNEgolqm0q+3i7NI411kSq0sfavvgSIbrOZ*)6_F=AV!~e0j1!)B<%;pp6e~*xRMC22NUcdFQ!E z;NR1wu>!o8mb02?Ah;OPwm9#P(i9owI6~p60Y(g54uLwth?aIj z3C2gsRUZrwg)KU$kf_4{G$eK_oXZ<#>MHsk9d?w3Kh_S&MlAchJVa>9A#G)8a!jf- zZ5P~BZLg2CL4)RD{k}UmsY|Jfo&xPSG#&Ha1gn#|J@^#s*yC` zY%pMMI?5olYQJAbkv={+;X*qpd&dF!Q5(?`|N~xfIjO z!`A9d47Q(>3D{wx>ZMkN za4Lgdn#VOm$jr5@AEKqpPkn_qRYk+)v6!V6_m8hKO^oZUA>jw2$5K!K0&qjZgFD!Q GlK%jGjO_AwWXw; zhYE{_+ghTj&HJuV3B;;bFnXhWiZx z0LG*cV#HqeezE~_FVzzdiU2@*kcj?~S7j?Ri&xD2EuL*WD2|d94u&t&@(bIO$&8wU zKQvKjrqe%KK{g0-y)A=&CLThR%4g%SeUtxvP!3_719qX6K}_!1lG({WhiQI z{WC(XoI8ToGQHZixG_II{kVk@+WSppYRVhv^l;%A&N@|7&6^NBEx$lx`2iQj%NJA{ z!yVor)4$KFxL^b&M3H>>kqlS3q@k8tWwQzx&E=N6PhzHqKs}w_GPX5#ig+oS9Fg92%0cEncjfpRb9f=+_b53k&COVV-? z++np9m2n6(H+wGJ^@D@Xv(7_h?c*0U8>p7fIlo7-f=8Tfz~H~WB`5Q2Z#%$4PesBA zc^P^>CDN!VZ|4{Nd*#1n3+VC_=uw_To_NH~9}8W*p=}hajwR}?-_o=yJ@M{q+lD%K5O92*>H2n}#;u)r^|_&gF1G(FaiqBkWQ(h)J<4a)lc? zz6Uf9nTHYFTmm)^B)zGi5G*f)=zedbw=~n! z0o{way_U1+^`PNJ;9NO8aJx(56OigU)U2ou&ssCup0R7iOI^Ej0>~_) zf^{(~)9o=v?c1wdf{!P7R#qvC*}FO@mL${Rk~f<-s$1my++BOhs1??_2vL6PK}A*| z=l&1X6RP}v7LmgqETDZA=y>RzJ6`G#Mh=xO!5IO^nlbJkZZh;F$2vuLWa)}<#7Xw* zRoikygO14<+J)L(=CY;6`=2zGbOwmKf^wZZ><0^T2D;*tl2o=r+OxDV-n_$*!+=;rBC()Du#l7SEgQ7JT5;sdgLFiOU+M zzOS4%d)X!74Vylh_7~qxsbUJ-_p~%`hOa+R$@D#EOh3QmjQBY>d}p>3&=hU^&q~{U zSE?sG4Zrui&+;qFnl*n;+`eP9dS1iSspXX^QNBPm`BN^hYgjsG8>`0hQpQs+86Y?_ zwxP(%p`j>Be8C((fdzA}0i~H)P?Ug_f|S9OU{Qu{3ZKA+YD-zggy%(8E(i8KE$L~9 zJ^fOH@&Bop!c4o??>+U>fw6k(<%LY&G?yo{z6tVk=f1IU4xf0zzoIGz#^-v=+XV`q z6;(+vrt2%^%S0b!EjqMwIm7kKqUUN^D_lGu-U)6v9%8#Uaa+OTbt#(_uI7BQ^uUd^QQejV3_efu;H$Xkf8YRqwE z><(oOn OLDHVCelF{r5}E+UXBi>@ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..fe8e5499ece82b251be595e03cf6d9eb28712577 GIT binary patch literal 710 zcmeAS@N?(olHy`uVBq!ia0vp^2SAvE8Azrw%`pX1jR8I(u0Yzz$jI2(7((jl=>bJd zO-+GZLqkJ-eSH%X6CeYq0HOw{Kt)9b!gX+P04lS#w$|3x1~PyufqH>bK=m3L8bA>P z0|Oul0YElH3TOgc9H?@l;#3=;HtmuizhH)0<<+%{jIuMzrLBXDjrz>lJ9JsPdD&XM zn5X#(Pj*e1k}WVPmRmI~)tN6)b{7K!<4I2!$B>F!Z_Xd(JsiNmdT@VYTAJUKf7~UU zcJ`Uen>8n?ctTOupWUj_i?6;*VSn+J{U&1u-(hBhc`fWS&ON)y7@-q>lkwW(+%kr; z-_B(Xk6+G_Z8%*zS@yxdiZu2IrN2|yAAINKKd@U`Zb8y^zD@=P2L`rd4`0Mj;MlAm z(Oll`{+03X3X27DSJ{s&{?$+sA}`SYiuqAst%J=f{tlqz#}#%9m6DVEiC&%J6S7+kyHk>~k_EwzwI8%rNB2c+Tg+z_C!FL+ygpyuaLW z9gGhT&lXu+U;cifP>SA*E$qGOPk+1b%@>uA&i;Kz_UGPr2Ob_N={H{OT>i-~k-u>E z^?RB{YG1Z*d+_?ii;W+)|M*)T62La4;mQV)hfU>L&8r@>y$|s;=Mva+*`O_R(xbW0 zDtS~~-Bd{e_Fzopr0Ia(vQvd(} literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4e6b0bd3e71d535fb55f03e3b2ca32b29c6cac84 GIT binary patch literal 130 zcmeAS@N?(olHy`uVBq!ia0vp^2SAvS8AxUb{c{0QYymzYu0Yzz$Y{BK?*Sl-sU*lR zm|<3$=k_EZPt()IF(ktM?MXvM1_l-e1KuzBD;c-5$lTt|@gU|~kQ0Yui-1!{J>wk7 Ymx~#bKJ1To18QRMboFyt=akR{0H6pXDF6Tf literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/notification_close_dark.png b/app/src/main/res/drawable-xxhdpi/notification_close_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..479a90da280fcd4370aa48885c39b32de60b6d4d GIT binary patch literal 590 zcmV-U0F&ga&N zHz7BlC!-W0gb+dqA%qY@2qEO(feY&qx{~EzVTF$67!j_e%D=@9F+MOf9-)s2G4{BV zDgPS@5+s=6(rAPMrs)V9Ox$mtUHan*63h{_9$|>b%m@dJowhDkh%-N*;H_OVqKh#1 ze2g`EE~n7JQY+xIc>(C2EWq0NX@ntWxe^k*VASx*P%D4YehZ#s3IU$9DTMXyT=3M~ z><~E56yCM9e8!;W;GAmZM`GotyZK8is8Vf+v~O?Gm;N)+!$NE461An;%CEk3`AR8; z(aEkZP+LY@f1g*;&7<`~c+!|bg|>cA9$(5&*SA7#h1v>~UzDrO3vHRJ%^PhQtIZ#6 z8LG`EZ5gV~Cv6$2%`a^ks?8^DwN6{csZFj6!on@|=H=<%AZ;0?Ho46cRvx1bQ=2~` zo13IIsqzccR{PW@H-VD0)i$+Bt$sz?YM$EU(m+kxYL?pk(N;m)s!wgcXsa}BRi`$8 zv{jL|>@FnTXsae|nO{h{(w529qKw+iF9|(So6RMmxZ2RQy(BDDo6{E@H`v>yElq7l z_ixGWv9Y@(%&2XP2Z=VG;9z%2n5yk4Qod43VQhOzhz^#{H&4D9Ji0iSjL>V+-OmW0wjX`-&~p3X3lm;@jN_w2Hft8oZ#*-p4zB zs(2Xd!}0#}wBgsAM$9SH?A$Ma-YOJOKUjd*=(ka)#UpPDj;ELH)1DlTGv!a^7eUYz z?7csQ;kf)bA3nts*(LBd-@>!MmNf1)OD@hkzkHghmA~E1^qyJILbcH>^X@M`Q~oV+ z)8MIpoR6vPM_b3@KM8*?IYSZe|<)*FbsZEWeX{%yt(^~z4v~_xFQ%eJ7Y3sDq zmW;OY(pGtDOGR5p(^g4pOGR6SX)CND8ADrTX-nRajHE3|Yf(vU@+M&-Y71%-s;jLY z+9b?VTh!xhulN6AlB6wPZM=^DFInI6XIPVPr#AkwLxp2(`?Lyc5^mMTYnAeiF^0Oq xO~RU;N8ddDVbmW100000000000000GpKto%0TB}g3o-xz002ovPDHLkV1m|^B0B&8 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/playing_dark.png b/app/src/main/res/drawable-xxhdpi/playing_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..5af5088d277f59b8c381a1da4afaa4eba80c6604 GIT binary patch literal 514 zcmV+d0{#7oP) zE$%4;f*@r{@_RT11Q~6fGolL!g1@Y21A^c?4X!E!f-YZ~Rt5yYFY4S-1_T*fJX8kc zgK3GI%77r_3r`u<1q8tn9~oB$1i>B+t}6qAlr^T50YPxUV#Nm2fPkRKXC8A!7my>~ zY6F6x#k@A)gp~CP>>$n!$UnQxmpr*%5D*-)#$9DVKDoXqZE`IH5FE4417$#PLYL(d zPMHb=iWUkuRv$u5#&oCAO&dC z-l1J+IsHj_IlWC?c}w}egbB02P5l$yP5mEUSM02G06>=w?)Yf2B*5xWm&gSlukY}z z!gpqU06pI6UT|cznN6zRnSkHC;&zf~0=l%7uUYqbDf9^Ezo!P(y$$ON$Y`^m``9Dp z1Cxnf)aY=;H|{5zQRAofle4kJIp>^n&N=6tb8hhd0c;&D5!j!-H2?qr07*qoM6N<$ Ef^zlUIRF3v literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/playing_light.png b/app/src/main/res/drawable-xxhdpi/playing_light.png new file mode 100644 index 0000000000000000000000000000000000000000..6b5e382e7ff0b1578495fe80b69e6120e1c4c5da GIT binary patch literal 547 zcmV+;0^I$HP)WYOjP)Z6G%i=}0~YX;4Sf`W`!lNeeN`>f@!Xy&nd+3p~6#CGg lAP9mW2!bF8f*=UN@BuOddj&wRCl3Gs002ovPDHLkV1jcO?2`Zh literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b6bd07e3023d9095d60b4243b8f16c05c983ce96 GIT binary patch literal 424 zcmV;Z0ayNsP)lPcmbu@Oq3^3XERC46DSsw$tIhWWH4b-3?#WEd<^FS zTinMIQ~%5FT2)nv{~gv1WPxrxC}>Xt!IcG#Ngy~fp*jh~haG)MAh=V|nFNAs zOPZ2EaBM6K#G5TcNg#N!p(_bg{722JPXfWY8MR3u*f*R7;>o(6BoN$N(V7H;Q&T0& zdVd1(Vb5quP=5r$yDhy*Ah@-xISB-3<}@UM;J`>0h*z7slR)rnO-B+4F3hS+0>Pna zHAx^o?dneg!GnUfBoJI$(vk#%6XUafo~>c<$BI--(mSoslEphcCiQAK`O8x++jMzzvW!-lAFOxVCGYNQu6 z^b8vdM-9%SRtr(fm#Fnr*a9zV#Tm5(j#`t49TG&HT0|YAgq`c89W+%UKfVBX@H>DM S-H+-30000o^Li$GowMhwCJ!3g3}EPT!ue&(bwD%r<3yhl6__zvbF7V{m)`2!pM zxT~9HPEpP>;t13mW-1o8QpewT(`}mA%QDQmK@mfTa;$XXa6__F$zmpI zfisazPPiPE9V^9Tvyd1j3C})EEXm~XEmnHmj_w?p8tfAD*)3UdFID7`%#;!4M3Bs9 zRJlMoIHtkDF^3Ya-F@XCZEPb|$<`A_Dy6hY7Gq~G^VIK=nF1~W4jNcR>?meNl8co- z0qGw$5~Uu=#0u)DWhD`#oEc08RT2xl%tso8O2T=SjM3uUz4bDv6p$Po=PCc@1Hrt` zSq6ouRLFh)MG7UCCgIplG1uQ&lZmy$v32l;{}Z`|R10?Ws~AxI0~F4e3<$oqlqZET zn-Kj?If{76M_1DBF(WunoZdi@Y@-g%#O@Sy@TAcksLoO?(+ zctQnI0FUuC{lHZU2vbrfAC#n<$Q)dSnw*#k#8OBjTbZiFw|<~kV$5=~vtK3B44x)h z$)juObTei-^>UOYDv-t#sKl68kAQQSH&`HyL`?c;D|1QXHDT=ImJ_I!MgGt4vbpR8 z>Z6G1{sxNXh!6nkcuUiE%9w(EfwO>ye)f~(?>%~sN@0&~P~rnq_L9j+bKo}e##WVM z#=?9p8Zl<}2z3H9ldmYGxxS@{T6XfH78QLa=Ohv7DjQg!e60lWEZ=htuycrX)g3e@ z@d3>eq#lm(J~4`BNAWI)oDJjwPLrj&osNkTx?J+S7D}=3xaXZBh$u2xN24Tnu(5_v zEn@fvApqQKc($;M<)jkNO!4mq)0smGIc(w*cO-NEVzWjq24+)ClT4n}M-4kDB$s!b zBcC#=x$ahHw6jsejt3@kIN{b9R;mH&`9y<8o=a4EneEimJ8B#o2Y8#wdW6a;EMhYk z6z`OSE^5eUmY(rDf;4{QA~s3Llq&&Su~I^k5B{4G$~@Na6X$8hMz`>H=oZ~vp@B1$ zlZ{FDzhy2~mBcD`Vij(@2CNj3#UnmNF=WV)AwxdZf3hM8Q%i!ZG5`Po07*qoM6N<$ Eg0W~SZ2$lO literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/action_toggle_list_dark.png b/app/src/main/res/drawable-xxxhdpi/action_toggle_list_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..674c41b9891bff4acdcc90b43c184a70690c7a1d GIT binary patch literal 1140 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K588}#g)VJz{w+sv{)}AhoAr-gY-aX$fUMh3^ z*Iv+}m}6)O*0g{S6od72$4K1xM=M9WtvsagmKJAm>Zf?u6itS5+ApQ53^C3tVjf z*g5>3sk7ps;O+Q5$_*zYBfHsMa{Sja6>xr#!>()VmDB7lYsz=bWrW!cGY;yiszuZJ zJ0hlD3ypt&@X>6&wP|k8QZ<@q&U|uz&WVXKTK_~2EM2qq0JGAzXI#t~=TGHEPMU3G zzxK?jFWmJtcIuzqe{cUYOWxR>!Sw39dF`KEru7zHS(awdyi@h$&pDzFvhDWP{OPIh z&h+rvTWo#o&#{32_Q_S^R8;LT!{Jp{knP;(~Ebk3$C`5{||qx_uwl}+}E#h z=fcgxQYJEZ1@L@7`nq1>`_oy^SRM!!Y-HWJ*z>gLfumw+9t^%_SJph`dXQw!vEb%9 zafi*kAX?O6^WV$OwOd#NVxL!S=)cUgV%>hP`-fM~5ppP%)vo1lFAib28v1Yl2iptH zCs+(Je;WsVlKUo_4peWSy5q{*`De_wH;RVMIk|0)ej$7HW_O0G-(2~o?fCk>JD6$3 zH_HbO`?Nob6}WE=({9*UGxg%6ZF{t>Sp#ZsZ{9lP?Yz^|kFH=?RWB%U^0sBnp| zy8r4OevCcy4{XeNb4fq<*3CJ-uB!9pBP714s+VWS?R~VqbKdFn%B8FNCyF{OX9;_< zQ)h1z@0@e&m8&h{ZhoF4SniYHX_oR+rTw|q*GJAH>U{%8B= z#E*qX@4u9IpXyhfIq&Q$&YH|W9(JdGAALWyes%U1;f@5qu;&+b-c(%uZ#MtQ>!c;} z*Hjuh-8LAU-el1YOj|ME_wTowsQgoP_h#$r-^o9iX4J3+8|!l$nQu0pkSTfgrOerd lZ{8pOZwx9VaD#?*KlzWGTerL9<;LY8Ri3VXF6*2UngE{n_TT^j literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/action_toggle_list_light.png b/app/src/main/res/drawable-xxxhdpi/action_toggle_list_light.png new file mode 100644 index 0000000000000000000000000000000000000000..566bea80f43d8f7cff6489a7967dee4e900d6867 GIT binary patch literal 1067 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K588}#g)VJz{w+sx-pFCY0Ln>~)z5CxgHdW@p zM^SI{8D|8OH-x3cZVGx|yU$|E$w${R4Bd*?ZC$YFC|767#x43TjRzN8?bsLT_dEOd z$~w+vFKa*9eBTrIy!`#g;(K!Qi{E{$dLaO`f(TIX;8O3Nu>aHY-h8QTGdOWwdJ)5? zs9&4^7`qf-x%gEutz$DPXz@or_hncZw!wma%_1xXR`4#H7{=WWM#cGPZo&qSwF_q%O1L-4VTSpKEr0-S2)#YJI&$ zUC1vRl?Jx%<(K)HGB(EP>=zPrm>jOPo@2pXMjcKdu}8b1jQztthFh`+HZk0)m8@7@ z!>}>^T(4-}KmP4Y;tifZo5#3L-2RXB?wyKf9oI6(Y0DmXv;Dk%-nrhZbG7T(B2MyB7wV-& z_7^ioT>jgT{PUTDy_C{_v2!!CLdA}y*~{K|c;Cl6k0Ct$*~}kj9hh$Ty`N*5H7`_; z?Q#F__}|C*|NWDQeC3^>R=an9wEe@gE0do^Y~1YsZ0p`Q!6#=g%$G7ee5&ct-7sU_((BdYmup__xWUYT4`l4z&#=f^C`yUp Q%VCf@Pgg&ebxsLQ0F`jCHvj+t literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/download_none_dark.png b/app/src/main/res/drawable-xxxhdpi/download_none_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..188af249cf751a5cc008be168a0007b02e1e87f7 GIT binary patch literal 396 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H0wgodS2{2-Fa~?NIEGZ*dVBM{mP?|<@j_lD z_rBI0JWDmWxy6;O|0{LR%t>?BoVY2aaGld;1Mz}&FVmeD8d{4z|66lT)W-k$`8}WK z&SV6d2Lk{6sv^DST7_0U<+Y#xcFq$;-&r=#baqE-Wd3QDd20H0diIpMRYk}4|1d*O^Ka(qFF5t~3aic1ci)&^ z+4#pgOy0Uq|H0BXtRnw8z7$!MGB%$n{cF8p>c#bs_owcEBkwfbbq+^Rszu?S**epU zq$19^9<$H;w>s(A{|bS!qB*-wGK=P4KGb(u-h0MbuWOqpUOl(}Cu72)gC61+SW6gV z9WbfJO?`)VHRv%o9K&wqd8{5N^#-MN1@@c@ki zfgjFOug=^v@4EEXHIwdr2#$WeP~=!5VEJvcBA;NzjG&? z`H_CV{eR7&%wns~v+*)LQvZ`VcX7(J>@eE%vz_md_r&S{&c?s_WA^WO@f_bNRsY;{ z<9ebS-rn95_lotz>yLSNn7-I|ai_*N{9Yoi@Kbq0=%IHvWG|dm^ZqB9vG(Y__45xt z5A*ott|6ydsGM=-(90977V-0ZpO{^k!`J?I|GtH1@7sB%{W<7(gJbTaPk*gs*4_HO zHZAD-!^TdF>v4CYvd#hR=V!3jciYvVw}ATv6IN>FMOFrn8Mx(lITV#JLV}0khkX8* Wm!IE;S1c+6iFmsDxvXX literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/downloading_dark.png b/app/src/main/res/drawable-xxxhdpi/downloading_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..96bea87ccad29f7c032575362ccd09d0d5d34431 GIT binary patch literal 499 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H0wgodS2{2-Fz)hnaSW-r^>&t_A5)-+YoUqP zg(eRNj~h+Brw!G_#LU7jYnW{2(~V^mlvY}*G~>*yb-jl5U)a;sO!kzY%dY=??vD42 zlldk<1ChYC7QU%jYnEOA+F!DqU)S`~Mt{bY_oixJclvI3cs~2%)g1N@`B{Hx9m@Z4 z_+wB@{lo8}EgnB=MHQv$^j#yQ>wdf55C9Ppb*Ei5Wb02`GMh*uCCV>V9 zmMOvuY`2z5`9yc>Gs@a#NuAmJYadI*Y96p8lDZ!v7cK@*y&hf7B=pskCv5@qJ_SXD zDGCQRGN`^ODI_et{$i&zx2DAF&DXBpT52S1nsIGk zOl{KB=?j*&pJFmHetlh|Y3hgTI!#MIT-S0+EBZUhb%Vq|F4qlG|1P&u6|DgbpwrG0? zVU?{dXB~x;JfybnEl(&Yyl~`*g~bL1=l{)q=UmthEopdo-D*a}tZTP~#LiXD+kEe- z%^v;6*{>~tW+DOUM``+RW+w&&@7j0Q-2d6-b7w!lZ1iGGyk9e8Y2>V#vM+f47p0bn zK2-m+nrVvYL-RkY7cb?Q`Qf#eli8nBT@$41bX_%g_Ww~5X^(dl{KvchiP99l{b!V> z@bAB%6vDNCkJ5~4kMu)7S+&8s)c6ez|Q|)b`ZSNkxn(V9?&oXzHY>1zOPTJpB zt^&gUzIG{S{mB+p6x&(1Q>lZe{-;t0Z~aT9Bag2C-_7ypaC{7l=tF%Sr!`rBO9Vq& w?U!?G-mw0|W&bXLyJfHVkb<9q;XvCDHmgTEGk4~eX@Mj>UHx3vIVCg!0L^0CuK)l5 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_add_dark.png b/app/src/main/res/drawable-xxxhdpi/ic_action_add_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..12f250abef575ed68d0746e39e4952dcf3856677 GIT binary patch literal 315 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H0wgodS2{2-Ffw_%IEGZ*dVAYH>yU#)dtzq) zg=_W~w0C^m6uYZb^H%Q_=8%KNe|PyV5@1`^bLMl+>&PdriZeacgn>qY!Gr%#Rpn=v zr_H*)jcLzQ?c3+p80TvrFu5ILReWMO0~3cr16IPn*zs`fzm1V;_qhwstjeg(ea;d7 z`%m)b)A{-bH1E5<&bD^n@yqjmb=L3GU?aB3KCpc)!}Uyk`90RT#X3>@PcGUgz0mdG zon3-jk@nS}#IBGz1J}B}9KF z)yz~nAIDbu*M9x|*G1(sZ|EFr<_dW7be&E7*+NrM2L?tK0c^wqbE)5d`k#GP=DW_g z=+~^Rk@N3ZIu+0Ve{s#XkJZ8p`kZHfJvmqT&+etW=UtJL1R2uc$lkI2#+2jF)mN>^ z*Lxuny87$wpDH`W7yf+mbLV%ZD`|f}?R{<^@zopr0IP0%o&W#< literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_artist.png b/app/src/main/res/drawable-xxxhdpi/ic_action_artist.png new file mode 100644 index 0000000000000000000000000000000000000000..31274163a4941eb9c91e951a51221ee93f3ffb55 GIT binary patch literal 1043 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H0wgodS2{2-FyHcYaSW-r^>+5(Y!P>vcJq%- z={`#g)2p^j61wQICdkEW>b?bS%Z@Cny_n_csn$5(5wy24I zY;_6t%`FnwpLNaT-u*8>n$7OKIh|@AG&k1%{GT1h#h+{Q%F5Ept`)WjIB_V#iH{}= zX6Zy`%|CuY`i86ibjg$n+J8_G(*SJ+lPux;n(0K5Z{x%DF&jYOU zrGE8(KVBxgYSFaSPvdhg%6d9%_vigTB}RVPxov?D76~2*I{)>W2h+W;Q}SoM6sx?( zw9i!P#=D*K5-m!89KOf*PUuGN{D!q|%>5PXmfw*7)p4f$p0ZJiWTn*`;SD!39`MF} zcoK1JZ2*&7=Eo^(jgQS(G}S$$=lKC!v)@mC%;=OnJU^k^|K#@G%}cMpox}6iq|Zif z=`Hp1eUffXfp%XtO|P3Cdb2V3!0DbTCPim%Pn2yJkv_@V7QbNb6#tcOZoRXn-_%q% z`~8^Vjrl9M1WKD^CcC|u{KV3AZ~i|K_uhS_lcb`i+dvANWXUNgW#TzScoqDovSsz>6l5_c6W}N%F?Bd_uj%0{4?|QyM4!`H?Pa)4UbVin)kb$Ez9EF%o}wUvzb4v zdCnj1yMKcL-)!a6k{@dLE(_*9Nwd5C^LBAdRc_3;_G>%Y)+xE3`^3oY#CHE&rvFcw zwI6RPtWKC~s9E*pkKFQp-!tYfWyjO-j(z7kE$U#-VEfdK`fFVdQ&MBb@0Me1| A!To1SlrN;3b39E-_1M$r{*idYu6houx!!XEA??B@^i(^37^Jc;Yh-q6NYQ@xeb%1~VQd55*>rv7HS;|w;) zY0Aj5(B~#j@hncSy=+Ubum6|wYfW&T_14W6i&@7RBFb6yjFR+O`%jc>wV$tS>EBPk zGN!nb7U2;-ZYl;h&LvjcCSwgPj47nFd+rg)=9JQ6AL1jMW6J}|S=Qold`N|~LZ^8{ zy(6;rF(L&^qkQS5`lY;-zRD20or%`SqKY9&UWiY;oBqElre(cz!77UgC?wwXXc`R@Ym5t?a#nz7|9rByP!k124U zfW4A$?oU2I#Yf+V|GhF7*s_)$iTZlC)z6W#Jricao>T}h4E)lbbHHUgz+uT0wU`W_ zQXs%EI3vmb&Q106r7Z7r*1JkTv9t_&S?L}Dm4t<@$vJb*FJ%H2aZK2LjZx#C|357V zA7-=Z9Yjh57zP{pTZlm0T(Se?NhdsQW<0q~1Z2@7jG_mhI|P(TPH3y?K2j#YFesNC z@n&}jI4Owjw6uv%nE=D!zA%zg?hx=;5L@RN0lUOwRd+&QF%mEFi~ygs8(HBz0bfgp z*+rfakSi^M9nKTbAc*`WexBcP0&=(@jOMuW1Y8!p?^%nJ*9HO%gL)xHbj^7Jx`Zvz zVNVIzEu`eSosR;8LQbyQQv$XzJUc<=Gy$;#j}j$pEh%Xm0b3ZHHJ(9-56v_IA_1?h z9Z=auzy@A;O~3{l2-s+OJnMvVjEr0Y<6Nb}S~7ip*nt0LL36ShWp4?P-Jqr5lS0MG ze>Z-!Q6-;`QMgtFO5hqRxc;2PUg3WS2r6Y!G|3O8BqsgQ$GNz@dtxtM%rhk7A7 zcisKX&{dYE&|4|(j7ev{efX2H2rQvh_~(fX*$xYf;l854;uckNB{{v>4(CZH^eNZs zP(Xup@IB2iZHWuw@CoVps4E3@Qbo`nTY4?wuA&U)72n2R4D1jq(!5EEKD5on?&6+w z@;lBgs?{a)()qMYNzrma>^sVB>pP#Lig^&`qFi%CnM78(I|SrOi=fp?A!OYCi@7Oy z_eq-MP?@vR`DC#>1lVviHiv}e?4A`S^~wRRheuH!HnR~9I4-qT z3X~%3w=)hgW1xOl6t3$GB~5W&JwWKEs;a5^5)Qa&!&G&QsP#eD>LhDld*;i%BxfDX>o~?d?8i--YNFj zGd-!~sq$J*2Zcs@fVK1}ug#6qjL*7PAd5o2S6-Kzrk_$HeURYdWbXpFI60Hk3&y?e z`;r#6nCh*JCPHyvAm~V|uOAZ}-%jvo!Xyvbg^%3$PhzlyTzu@HJ;CMaDITQF+E;jl zxvtNT(#6j-5}3I@zv;vE`F)(=3J){*`uvQ)t`bsJ>IW1e3^T|eAwmo?$S{#4o?3Z{ z-*dm8lr^}k^xCPl2%UUpWCqBlb^$-4@*!_nuh_a+Y0qe&$de7P4atk6^eIk%lWWsN z7~qKO$u65oD5RQZ3r}eU=fP82G5)=nFE}0dm66%)DJLnXI0Njx1u9nH=Q!7S&I<-( zT8JL5a}0l4z7XS(r&#@eGc+_bG&D3cG&D3cG&D3cG&D3cG&D3coX5Y$v#0(QzY6EPtyZ7FOa7f`mY4uQSDu|`^6kD|-3R)sc8YNxa$A;tI~=sHRZsvHaTIt!fM=;W?J;Xshs}dXZY$iDNPL zbQo2-jjMGH{qvU9e+BJ`&xi457&Ed|#C^-dFJl?^=>uks^my|TUG4fHY}gt&*C>a> zv6dq>c;yrr^py787(SMl={XW@C`8tNPOIuW= zB5vX)6{$#Dv|Go&^7{3LwhgIvyahWohp+|D^}8^M!`PqfP4+5XiDfe#}a77zRL{sg`scJpY$#ggo1%sAq^`rlzv^C)HlV|DQ zYUT{z!<>4>N^jJIGq_pJeCm(;IvZ^GgO@RD{<20ErnE~eWWCQs6mQa66;fZ*)yDez zFU-Cc?cyw*cd%`p80s{B#*(Q>T1!M5X6>6ze%gba8h6F44nWRKQ|ONyLLN~BABZmp zJNn(^S=WTsImqC)$vjvL-Xb(NQ3KaYY-GNPi`paZ%8u3u9hH%(yCkSX6pvt>Ew2Pzl z4ux8kwu7dgv7!=iSlDy6wV^=#eOMC2?tHylUp$OYD1Xp!*~(r{Cvk?S0A9qyz7wDi z^0par^$S#r;QFV3zO^Q7`#R|*PrZ@$KiIXwPk$@2#|Z(9!2s`bQqJdn0LBFAld&kD z91y@5ObOQDdq2%z8z1GRS9IP_0`v~SON021Zcp8y8fD4GzJvwQ>H>C>~O-}yTVW6%Z zp&4hm;N0X9afd4xgAmy%!wx*e{x1bN1969`l|P|(o~siN!x+62587d_c0wFQxbS@0 zmjpWgPy`G&dQC)47sFe^>WhJrd`4OzK~Z|22zkFf zZuPCS(S$e;s0eq&d7#F4vfO)*FYE$BTHK7CM98Y9tpguaT6fx`X;CWUE&hDcubeR# zj>c-^+G!0cWYW4Qa05IVV+{K_0kaPow=;u2mq|uK_&KDHb2_r54>G?H4+}XOYsxwI zpQCi)B5lEa@t0IPpPNiMUvtQXXspEqiTanDrkDvW{3Xx?3C5IDf^aFpfL~8n<9vE1 z<(&v}d;7q!kRSfqpIun*5b{&@Z2F{5V4}kfnqbPToJCE!YnH~F!7!~yn`J?lR#OBXnwQL2OP#E_)Xw3oY3bBrLljXe zZAR9toP}PxBaPHJu9}tQz*0KQ8UuZuv}RT|2Ro3Vhy$w-N{98IOv|OpkW;!?X#Yw2O^&X)}ejZJMbDz)k(vu zc7uBK4%W<$AG%vL1%H4AdUas_J~qnJOIV-@7-+~FV!W{w$+L%zNk~XYNJvOXNJvOX mNJvOXNJvOXNJvO{kN*J^m^%8lOnNu@!$##y-FTZ60chSL9xz78)LXR9SVWK(6l^T1nJK+0T$v*PPJ z*;x#E0m1^6_dS>@y48Fc%sG4y>}F5#WH8Tg6rQld(U-yB5XgyG$oW81jq?GUvN25in)cX}3DSyvRynHS;Ykz75kBt0l|{mANo4(s5%T7ss_s#*=O* z6+A981>{bs_!%ktKP}VDZO12RHIWeMhkL%r&v4XYX8b+%zK~oS`{CE8;y;+|xpkep zp0`3uZmpEP{HjGCKWEi-|M9+_D`h|bj_af6$Gd}{xR=W*+55Zo&Od))rb^wO9*sX1 zN9yJz{a5;yEc|=o(K?Zh@>%v*fFkx~8|4F^sJp&?zCKpf;pn#Mo7{a4Lc@NJevSR& V(<|O|z5vE6gQu&X%Q~loCIArV33~tl literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_rating_bad_dark.png b/app/src/main/res/drawable-xxxhdpi/ic_action_rating_bad_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..c3849e51e16eede27596da57e6dadf909f709e9a GIT binary patch literal 763 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H0wgodS2{2-Fdg!AaSW-r^>)r#?~p)=_WL;t z9Cb}qg$|i*({19)5zd{&#by5XH-owF>;?QUm}mK}F1Wbp*oh{Eh4}{^bXU1X-TdS0 zKYMmh+D)EwJJ)~yH*d3?-v0Eu?{|vN-?2_};!tc6Kqfxk4@hVz_W9KO%BnZtCUMGL zkucjh{rvwwUw(h%C0Q>${l|{Hi`mE3y}nq8J4`ebnhjlf3OElb!LC5C&E+9kbX2O5xMX5A-+0%`>;Ho2vD=)(aA8#;yGZv2k>OJ{rx5$OVr*AO%sYfvU6m@7c=X`7}Qecx= zlh35_$zof>6mf@oCXE8amn<**4wS{I_PzHxu=2@ATh0Y@#W%9@JQuj%&%ot#M66(g z=kNd0&JK%o1CBEVjTHJB7!W6re9iTlJ$rWdXbmvjTBGhMpRSS92jEnd{~-oe4T zJem8&sZ7o{ZU>^+r!a6%fS`tJq8p~NZ)7bg(BIZzcJA30=@-w+emMC3KG;{*a>}yo zfn@D8^OC~J?=3l3=+uXp>9fx5`D7~2xIL`CScRXn`A*`jnY)hbeC9p=|4`s(7TGV? zZNCL%MjYVXcEhbb?w)DQzw%qh+8f#8r_Lxa_`klp!I+n`;>3cjY*VDQ>gR9eDz%Ti zW%bnDc(HxK&$DMNBzOEtON-%*{`V&BSKy6L>D>#iC!Wp^JN|m>+VFo%H4E%Aer}e0 zU}N7r?f#h_rR|47RNg!`-Fmfz=d=Fn2WH;P2Z|^so}SMs!5jV0=fKkqPtUVVIW)~) zc*A%7{c2n4ei}2*?P1mZKdH(2Op8wa*`yyV@=VkAyB$<(n&VLws4?Gn>ybz z&)XpzJr1M@ZkUwir1pK@6po{<-nwFEoK`$y7u~h-!J?x97Z!yry`&|Yv-RgzzkOLj z&A#nt_No`(OTU(Aaehy+{kbzo3V{}&f`p1YNxO>XZ~K~mSK_Rr`ev;k-f$OhJMBIMPRoSC=#FD$?rFx=hz0$*t z#f@tjcCGv7EMBl6uTb>Cuk!zeH<-Q@rY^N-&S42je-ZoQsk!ln$Qfyx4)+ZM_vt6> zu9>-%;p@=}4R`-F&EU|`pP~8xep3X;3Du0?Ee)S|f2l<}6f@;;h)wXn$bOMYtD8;C zp@((HmgEgJbxdBNi=P$?2Xw67mBHwy?$E{*^Y_9Fh7-OAlDh1q^yW`zs0ZmZFb}NM zk7_t5tKiM3ch!|!j(3gV1z(Fn_|HzL8N(#exOTI29R~6d*`p6Vp21OKlm9J6q~hqZ)*R zzw4AT@62gm`?dLJNy`=QYRQ27my?obUz$?ow*L)>#otwHR5y8UT~mKcAfV3f$7wb12et*k3Csd!MpRcZ%`M@{y>l8h!$9?vB^?X(T(eOV^{*0%k&OcyR0texV~w%=26T|CeP?*k^e zp1k$Z2c~ZR7q7JG;d`AXn~e|Ob3}+q|2wDh!z1U1HB(m7;rp@de08%F6#nFH^4zOu z@<-kLpzmIFqd)cHhnHr(pM5Op(EZg1v`a)gxa#$vvc@;=`0#z{f}oJqpAOY$xfLKM bL9?IC7t-ekWcf@|0ttJ%`njxgN@xNAxA|E< literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_rating_bad_selected.png b/app/src/main/res/drawable-xxxhdpi/ic_action_rating_bad_selected.png new file mode 100644 index 0000000000000000000000000000000000000000..ee1c17df57fb9a1a3cc9057fbdf957cd404096a0 GIT binary patch literal 615 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H0wgodS2{2-FzI=^IEGZ*dV9w{J2+9I?PK(c zW>Z%6Se+f=1s6^#2C+}~bCNABz0$A1@iHKG=|YPa>;;)@ADK%|M9%SzEh)J4NWmqj zdME3VtYicA{PVT{_c>IoQ~!U@*>6b)3aCGzb$icq>;I=GFvi}hui2A)#k!_jUB-Z0 z*-A%Du0gCeE%W;d7FE_8Dc7u6*&nbip2;rp_g`FqU&Hh(3!^g_^bFRh9$0XNfq&)k z4JR0M3|D9}q_XW`zsIQcnjz-d1#1WK9_9^a#NRVWC&)Cg+EuVeu)k-pzQ(xYB>#H` z{R0*Z&$F!<#iSoFO0X|DYW<#}{GheNeJcj-(s>NtJR4Y#F-APwT;9MZ!M@@@OGH#d z=?A_I$+8J@>IXJ4{=8~!V8c`mo$wcLW(!~RL!`xQ1B z9{<@s%l_OZ>+KBHT8!N#&%QX_2-D2CoEq@C{So78tCto>Z?9y|dG+M+r7P!W*?IeN z6r`W}SUo3L_(R32kE`2y^grIJ_&w=}`s26WF>odb z&I7f1+L+$k_htJ08)0IPz313{(px<55bob55tDp1Wmp*4z+GB5_?Z-K_4u4eb q%ziT)D4fu1e(bTk3k)#UF)7S?xuY!of*mjcF?hQAxvX&6?nQt7E|>gP>--*h^tVmv zZ@0bvU+aY5Z|?ZMv-`W`(cjA}=C@bO*FRZzUfp-n8+H+?i*|fb-5K)BS@*d9o~V}n zZR2H^vv&8k*}MGC5WC>=dx7Snzi;LDdE7p07iYqFf7<(Rfj`T2lUpC>$*a$QAvk~c zUH+Q1nmB=X-zNSDIG+6HVYi+g{>%=W@5UTs&W&WyJMbVmp7X{Eju{h9O<oYSuxBBsMJ7mE<^qe!tR})hPR_RxGF-X;fCcxRvjLLogWt}KV}35H#p4SiHlxb We3t7<%y(dPGI+ZBxvX&W0cSxc{+kKyl zm*$+{IGiD^mwm^tQ6ED1~{v)^U!g06tkIN1G zT8)@})dOj#^`Snlfl{?SnwP$P8Ps^@5J^$&B`=8QpJ_Kqre7+N?_jvJ7-p4&av6r#@ zkGB`?UiYwf`NvA=7Y?!A#k=85xNCb1c*%bxidp1QZF=l+p^yN}na z-nWeJ&}DjiC%1!P@u9CwHUSHdd}iY`s56-M-y!zQpRcp`C5QXQ%bUj+)i0Yeqd`mA z^1hF{+JTBWpMN_PF{#~n7kwb#zMsXT0g5byJ{_+m zF3)Fd-e>OgRKj}S6qY$3bQ7lCoyQuo({Xk>W4xtQ`uX`?4c7zX{rcd#WYHA4vEsr>F9Gl$|zQB@0A0jVb<+rR8;L=lIiMnyzQtijI$J+n968Sb*!jldx5goDr%>DF! W(rk?n57q)xFoUP7pUXO@geCyhJ5wS6 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_rating_good_light.png b/app/src/main/res/drawable-xxxhdpi/ic_action_rating_good_light.png new file mode 100644 index 0000000000000000000000000000000000000000..f31fab3286584bfee29ec09dcd8702805fa3b1bf GIT binary patch literal 800 zcmV+*1K<3KP)IBZLq_2qA?P7|yZzz9L*(26qwSCO z$(-zWba)D->IP#BBJX(Be{d$Zq%*>gPxMtcG8q zsQ78uX*B#$sBGtNm<>M^Dx1hz4S%)D7mgVXf34o_v+eNzhrd>j?S{X46TE6S{MDV{ zQM=(+sH_yud*h?>G$7-dyTaZ^^5*=cBtMO z>5psu&-u@d3cwatu~Kgpm;7}8WGwx6V4~snn<&}FohYUVnO=Fm7XqNN<7Xmxp^_Vr zw|gT1itm29Pe>#00ed#cFB-upmcl3;5&j zp#Tb?01BW03ZMWx0{o)!2chE2+<@%_wEAp8V%y_0kE;0xEts+hbUOe1!y=^Cu(VeUSA}=n^P7XIOwNXL?NwC|nQrt}h7zpISTv zNK8Nlj{<)1I(`8Gc7TZqSPptHB7OnI@t_ADVizE5fgXSo5l{(u1580c8_pHREx^wP z|K^dH1q8kU#xEei2sE)vpyXV10>%%^hlEF<%?rp4_% ziEM@&1@s$Sl+>JC4<=12Nj3c>q%tY{LS#(R16Gd~$1_xm2U~c56}DAAdpe(?;*iSgPu)L0^{`%hX}%|+tMr#W z=d+0A_X~RUfC5h{mj72hZ&RDyU-|2r+UGB8U7x*-ethOKd)&pD*PecV$gy90S@}=a zKS#r&rq_4uk4&3q7jT&6^Or{JlFxqkty1Rg+jXD)BQHgoy> zX=e>m3hq7nq%Gm;+`zl#6_@PE_|4GB&oDh;R^Cv4rik4l?Y_Xt zLUs-7ox5}#8DrjXsA(KvZrjP6BYjvlVT$#ErMrb?PjYNHA^Ske_6}oM%)Hm!_71Gf zcNo9jo%fd4wxL`3fD5aIS(~*(pS8oypMPB&**kg?`98dO!C_#?n3TMOxu=l*LvelL z0mf-Nn5SHjPGg7{_gW_^uz!AMD7=K$DWZ8LerejF!r=}~FbtlqelF{r5}E*;ehEeZ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_song.png b/app/src/main/res/drawable-xxxhdpi/ic_action_song.png new file mode 100644 index 0000000000000000000000000000000000000000..5d836e26bb979d63ec317a12fad6c5ba13e1b347 GIT binary patch literal 527 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H0wgodS2{2-FkbU?aSW-r_4bypw`ibD!^h2A z9P)c6xjL!-U~CCpk(^ zOh>EqdG~9Y{vW>caL*k970*df^pY#U)t22y;s67iK?5UC0s}LMdBV`)wYf902$Cd} zxy1UJfO+K+uKAXW7CoBazbA3lB34F?8PApN^(8i^*#BlYbLsJne+#C^SMuC{Xu7;` zd%a~({q)>ZCr+RL!d%ck_r^nG4u|$$tH;ab6BPeFJ+HvfpKM}EDNvisn4TT9j*M;-ru4Cec=r4vYQxb=-8{+8T>@Y~EGY#-Dw94MZa*AS=7xa|_} zhHD-TM@yK`m>ATZY%XE$`MkKSp*C;1^aH8)*?DchY93Uby8Texbo*iTGfQjbOityk zWw5ncIiI7%Q*!=J`!p&}{%WnCu)-o`^3NQ*n&~bQ&*aWWq5F#pzt;Bi2bB0OqoL;(Uqp3K`H*kjuezAa)lm%w^Ko8r)UA~$# z{75OjJky+GLs;uK^K8xXOX=ruIPd${%)1gW&pWn-t$vJASLK)To`x{huj8es`VJ1S zX$;SZ-_D$80srWtJS><0gy;GW4qrnuDAM7pzJo(N4E3W-`xY=wg)KEoh_Wd;u8(Z> zUEjgsj6L7u3nr4|`pBM)1oYV=poX#3kLL2Ybqr-BVBC_Cz_)<9<;Qs*2^u9p0t#6` zt;aRMN&yXpEufs?j0AjSeL)K_hI<(a=q>PJ&=^in<^Kr$N#zgw0oep1Q)^q(it_Cm-UU{i|%_>?4 zJ)dcm%~ss&*$`k1TbT58o~eLlfVmQ23#FA1~Bm&sON#fjLhy-IvNif7+u5*gL zArn9;5o$O@JJ0yzYBo7bKbL5smP$$sS^!aw)4@YNxLgRQE{wj=#}!Vv|0&)o0W@)g z5x)DVp8Cq%6eHYXS6~ZdAF`@ANMB}4sx*1bVXEyfUrQTLd~AWb+a~SjEOj>5KfnOL zb5edOGYoRn%G-e|F1UQkXPUoUw(e%2iHCV9zm!MpwyA!U0siGxz&yj$+fu(G2hV(W z<3hxS`sKs}sqf%$(}LG0*vHqv1Wa-~==!Cc&-2}%`XsnUS+D{sxgS`42ZyI@3|7Df zmwyM8tGuDsvPDv!^C{r>2qi!QBtQZrpr{3`_F7%O@`_l{Q{fnm0j9pl{`Gn+a literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_add_person_light.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_add_person_light.png new file mode 100644 index 0000000000000000000000000000000000000000..26782495e02ca41fa32cd8eaa95a8cfe52f1ec55 GIT binary patch literal 990 zcmV<410np0P)doD*3#-+66Ge_!u*ZYL-O00000000000000000000004l7)0POvdW?_} ziuELds=cMMQ`9=c8J?DTUgEsW^E6HJIWyK0;ZF_lBPsAVF0x%xMgA@Yo}#F)!^e8) zf~=GU-&V3lm!iEsjc>)}*>2&tYQY@w<-F(YwqgITDyH+JhkO>_h{$V4;s4azQv-e) z{QdIWReYOm&eEW7cKq};*>El3CrzpjzYPC^>-aX?{K+SqBHyekzRfmg{P6vUWUX2O zvSxJYk|SV*&h9TQ#|z+@s=t4mZMvY(_xM~2rR8`6Ow}Y{Ruci^ef*~Uqd9zIJ1y2E z;GHHrLA3&`{_XE`y%P*V00cC#013P70d@$`q$j9)5$xclS_H^@>1=KRiXPWA0;ZdN z8&q_7$!gUCz8ImV7f?$xWKC#r=Ko!(A$nK-;VQkVa*rTk{#exl`1!;^ z4bJ?V^?v)6M#WcE?h)GMx~#j7zrnXXI0o+=>TavDR?4{+@Y?FvVy@RifKqaRzq*RQ z;?W6kuLLNS8m2|3*W#R~53hoe?_onOdx9+hw+=vh-#9|1;)O}@!9QgGM^mp8>9X_juK$oWt4S_w!E z@jY4QFI7Y@?+;gGnI4=xtj$>XKk@F=FwNH1QdOv@W64;L_TeQ==XuWk6{y2&(go|H zCpU#oNey$BzwJf%t@=(mIKY1m9zq09d{s3QY@a!*J z3FwY$^E7`V$WJ_rZ?m1AGxvEaAlfVQUJ9U`gf(lD8eAVdevS|VAOHd&00LTBz&{_W zYgD~7A|4-wjR*~TSIL1E`_~Hq000000000000000000000Kjhi0$>^+-ad_5#sB~S M07*qoM6N<$f^2We`2YX_ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_admin_dark.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_admin_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..a7a645f587af27a633d1262f682632f8e0f8bc4d GIT binary patch literal 1172 zcmV;F1Z(?=P)V=cY&1&mu^(Ko}ZL~ZK3M^5!nlo#ohZi|u&RVsVu{g|= zFtx2FXB;9SazH4AC%ylsPcND-?uT>V7w2%isPE_Z`g>p3?|aVm^t-N-5+Q^TLI@#* z5JCtcgb+dqA%qY@2qDBup^7+hwyaoRO`JHJ*JA2BXd}fXGGs`Tq>CL!`Z(_}z$h6q zj50zSJJ(L~4fNo6Vu_F09x?f2Eckrk1GaIO{pq*xGdFy?@8FQ;RW{^Qm(jv_fchqB zUa>v>WBd`&{eNPfZps4H>q#&hqCaeN`p<==@8B@QE>O zTe)uVg--hnNEvkB;necwka9jU(hu8~evC^-37lQNK_^d8;nY2*?spa!HF}^j@{~ElXyi*pK{eu0TNwCLAf8M@T;E2&O zc-Utd%o=>5&Bl$;QsnMmp~<6fGS~<$EuTSbvEakV3G}hv=OBDCvi`WORiLp*-Cy)C zj1c!NgZLeWY^@G)fcenw&+}BE{!y-l>K8~@TNk2?Lm?I{(ivP6qLkJU{R~IF!^bWQ zp5Sth^q-+3R3AJLe&<>1)_t3? zL!9RalJwHS{@mqI*Z*K2Z}J&~q`1fs{j{@p?cEJ*q>cvasb%Ae^cB=l&o=6)ED=Ho zA=bd z?H@`#N7T$`tcCE;zExGs!65)5)%o zFP8E&SA6upk>+a>?6%@eNQ~XQ6|oNBjxmm~i&*eqo5ylqmXC9YMmF8KasCipBqLM+ za$k0z<*2tncDt4RA?5Z32IyvQ(T~X7M<17s%9z1%oheRpe}K&|b2UW&7n7W#v3SYX z(jBpQW%gg_qS`yIDdn4ph0^0iD3wyi^NbWc`CD<37H`3W`iKuF94^oj{m{LV^uyoq z9_FbDyDxcKB3gra$VrdF~6bE>ed&^fL4R6k9=a0#PV64 z4SZ^#U*e6`NWanW{x_c(=WdVrCM(Vq4;S8m*NmR9rX~_K=nieym@Sah}jK#to6{{^+aum;o>aRzX3a9r)1zCf8Q@cp3- zkO4A42FL&zAOrr#25e!N>0*YbC}P(BkI^1un3O;p*j~)Yv;D1Bza$bu2qAE3cLvZ`szN^Ln0000D60=-|9NkBL!G5mo{sg0yN4TYi~9iF(ZcX8FU?2C{7zJ@tbNA^UcFX+!dkOaBmw{ke^Yh5jPGx(HYwUhnR(78(vS^!n%F37Y zw#Pdf&~zb)D82G@-y%!??!KY*w$p|mdlbx1v=l%-D7W?fyHc#{nRfPRP1TXi2bJoh=((CY656>H$WH# zB?sp1`*0 z>GY_X@<@l;mr_!Uh^fpRy_Czy%Na4^44m4NY|6HXC|U<2$);-TjmA_2t$mlXE!EjG z48t(Y$Kl*?Fxiv{B|@plU?d(+ggP$0ly$;T+?DkBtpiu^r+7~K&c$7G|}6h z2$gq)dMZAauhCe#qx0fNTj%|sVI0rBGXRwzyYYf1%AfPilt;RKpPtcOv#ah`Itc{D*|cG7UHr9%%JfI6)RInOTKPcNKWn@SLz6W7n+)2sz*LsB#U`N!_Ob z`tuas|EGNyXs~yjZ7Ewh=C%}uO6_YE>xYy|7^-gv$k}&wT%hk&HdT4+6x>gDsJ;K5 zsIA>-bBM1JaW@4-;ZL>-g~CkM*-fyPK@HKn&3;B`*k;2O%S!8AA z%;uU35Dz0?PXXwlWd6k~yf(N7RJ7JqfNV=0cL0klvh4k#V>gx``uv}%!!o@y@z!FEt``dq7^Ob?&s1>8q|(TAf4=f@ z9rt=^g=MD{g|}S2t9n?&Rg1TVTbCmsZRy7PKS2poH}3};l+-12v#tE}rPDmF^P*%Q z%+zA{`B!zluy19}=?hE@{8J*AeyFUg?XG?@m*O zd-Kd!&iVNBq=<^c+0SZIgYD+|Ff!%T*4|rEbm{fvOmzbWV`RX+N4#!R>#3((K2!nY OkipZ{&t;ucLK6V(^T|B` literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_bookmark_light.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_bookmark_light.png new file mode 100644 index 0000000000000000000000000000000000000000..a47931daa3b3cebf298f35da5cdbd4aba6d0423d GIT binary patch literal 518 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H0wgodS2{2-FrM{vaSW-r_4b;tH&dWU!^3PY zJua!PjM+-AD<01+-XecNNvTInaEsOo-+*qH(>%AfzF?jbWn#L2o3H!~Js+u^w|?LI zJN0ul_!_wXz?f=ZySq;W7 zdeh5Z|E?~w_F|~y5d->zG9bfrA;2nV@|^uj%P&paUeCHksg zukB_&pPfE0T}+k}XfPDStXZ{m+pbeK@v&R4UYhmxrkxO{%f%EZE7omB^!mi+tH)U};q_beWq&}BJ`@X&GOo^ z$awEG#ng@d^75hAA6?I%Ub1L;ld_BSg7x0H{6Al?c5+fH|u8Q85iHuN4y3*F7K8qb(+Cv zkhQ$r^i7L_^n>+t@BN*c`=P}^QsKYf`&YkP43_@@(vMqUVF-*XrhANnm*o8HIU3wR NqMojPF6*2UngA1fvj+eG literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_chat_dark.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_chat_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..7a4f30062d272057dbdb928b9db18da4e7ae8f53 GIT binary patch literal 464 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H0wgodS2{2-Fi!V$aSW-r_4c-*7qg>C`$xeR z7ZD|nNlvVax$7P{Uh0^evADHEU<3COp*;<~t!0Wz8QLtS8`6K>)^K^GcgSdS{`cDZ za}w(Iq@>?aVNpVWU;eLFWGCc?ZT@K+`_fUE`E}s+S4;2KM5~|bS!#ee?dMzdg>r!-UtMYwzFCyR+G5xc!s6uuW|lW879| zpu-^`K+xgKVhboo!jAn&9e0A9!nbG!za|ET1Hmw5JGcwPAE#wR+)($%-|J(B9{RK--CYbs>#(7)cNhiqs-rADeVE(n1c}~_*{R1)EIsaK@i#>1^ zf7SLt_<`T?S1V^#% literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_chat_light.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_chat_light.png new file mode 100644 index 0000000000000000000000000000000000000000..51443f5c47b0f8ce3ab9daa3e723ae26988338c0 GIT binary patch literal 487 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H0wgodS2{2-Fs}1-aSW-r_4c;C_hAQ#hL6G< zwYoeUU0)>}bXD?wm-m6WM_lp+Jl*ORSgZ-6a_(mzN5BjF)aHI5~UI`QrHx z>h}2RshGBPAi%%ibiFLDh|f!N>avf{{bhY!c*DEDm3OpCJ_jn_EMK`MmH&Dg_pN`& zw%C4Ne~IDFg;>!q$s2YvZJ)pE%6?UM*O(vERP+87=dNR8|DZmJaZ>k%-}6hhPh!lm z|4^zBSz>#V;oVI=MxZAkUSN|#v}K&$81H?8 z?SuZ@ZCMQE32I-Mc;1#W*3T8?{*cnYc$uLV!AkoFL;{S(qrVNRHIt-$R?Zq5Uf9PktVEkt>qX_SR4u;sn|JfaGfB4T9VE)IR x!G!O>Im6t;|5+E5{II_jw_&5A5(2DBd4K1vVlr0|8+qlaYWBlF3+r@5vOA0DqGyQUN|DQ^W%NOQx6v_>)Yr3GgMEVin*+G9@Cw zDxyNJPFva(TWw??| z)UcB}4snFzbkWTtCRs>Kf~(0y9W7ktKF^q9mU$NW#d0c>DU+d|CR(}7HG1f0ka51U zl$vDPq@s%Ln97R|m-p3Ux;Wr{rIw8!5_QB5tosH2`n3f!QNQRXVx3MF3C=9vJpG}6rZOs zCWa6irYF2Hu)SQFq%-dGZl;-AU6mNy9saCLfyUAK{3HLB}M|86tE#G zm;SXrC_d`FdE3*v79+?GSBaaBrIZ$<^2EC^n4 zCbBEw*K;iE;z-`=$*O>pU7;$a3=8149CDN1@MJ3Ag#Qun=UGi*GIb=Nox)`5P(b>U zsS5$=N~Z1wq$HWb2uMXTg%)6XGDRf7++>PUfRV`*tpEd)DJB8tBvWhxOi89#1(=Xb zNeIw7nUWEpaWW++K+j}KT!3!Lq(*>7$s|Q;$i5u#u?h`3BvZ~G5hthw_jtj78oZTF z*-Kc4Hp$dGa<*KU9h|=64hI#wOHZgFd&!*;nkG}ZASM3>J&+f&$xYgoef};7nM@|j zk|GT`6YiXUldH7ED_p0~QOhu8%9OKDOWyCNA@3SZ1r>foujNye3&MY9;*=?8UnS4G z!^>QkFTBI8Fz1UL%9=NB?%1_wVo*xq&ljojeRtIK*?Xw-MGm;h&%JSz2k<@8H+-&K zkfJ?60o|s@QsbQtnz6;JJZYMMoIT)-1=CS10cXsZ5A9y|Y`H}bc~v%ei<|tFzjfV} z$6ROE@7MJ}Jazqy>jPAR$R#dvyda{X3dcbR7=ASBJa~Z_vDy6kUhsF3NhSy*lig zfA7vYmlU4yszzdb#4TDENBL%lp=jb`ajyF_8s$xb0`$zAq`n%{EN_w(pf+z*3Q(9g z>IJCH8`T1o=Z!J}y5x--0a^Z;oNjrOpa5N`H%SQ4IB${=pmpBFD!_!iiA{hhc@vKS zgYqU~0p{gRlmfE+$4PVZCbR(4^CpadROC(f0#cGUT?j~5-gGD+eR}4cK`qY07*qoM6N<$f}$vb*Z=?k literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_download_dark.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_download_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..47aa8ad243646fc8bbc7f58a10c687f327dc19f1 GIT binary patch literal 390 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H0wgodS2{2-F#3ABIEGZ*dV9;-i#bq&^?|~~ z1YM7oH-2k$-Ug*fPFv@&VG~bB#=}{47ri?AXP%sUy0+$i>_OXNKI5Z0VnAa+fb;0@ z9m))h91aXj0u2l-3fP!kd+!--`)KuT(&Y2eZys&g!?7-T{?lLQ3xn@`-LDjJG;~i& z#jov!*WL?w>C_)noD%iV$5~_NAIY|Ws-Nj{PP|X&7jkT!Qol^`O2|K;VDwL6{)NXr zsmnO6ed=%0GVApG1dgrK|DRCY67WyC?Sj`&b-sm5Klw{IWj~d-Y!N$cpTx0tTKxlo zD82s;id*9T)wf-6{`p_RN&o5pR1Vvz|JxP6g#Bw)e6y?O*L|fIKYuY?IQILGlH8`h zieD<{CBgjWpvgGrc&tsD&G(IZ+6|KqvkE>w^;- zWlGAMnYk0Cit`^TZP~bKV-auIwWijo9bD_eHh!L)GO0lR{`d6{=2@}_`CJB?0|Gr7 zd%jsSayT$B2{bUUC@^4SPFUl*qvw3Byycfi-qlxgmWv;ZteK(OaJ*pNz0dcL&aIG| zx9tDd!$%+HZFTn8{O2>T;?+;*ja#;yw*M(`N&o*A=MxM6T;^4r|H?O>AzJ=)v5oV1+ujMe^-= z_-}#rb^8{(cl%;Fp?;gtF?nx=?E7cC3pU3s`>MEMYuvWgKi|Dr5c%U56C^Ae`tC8v WX6)S6zP)D!NW|0C&t;ucLK6T#7_lG# literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_library_dark.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_library_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..8c3d713932fcbf2656c0e449227a883ab798684c GIT binary patch literal 444 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H0wgodS2{2-FgAO-IEGZ*dVAYe-#JmD;oL(J-CTBgsHm;}knh7YNh;&W@n^{|J31qJ*jzXaLtR4W-Y=Qm)3)t-NPltsdHsra zPj22bXYrh*0z+S{*-iu}Y&t9XPye<4#&ewFceb0Bhn!qq|1;36a-r*$gL^j5{r1)U z#-q)PpVp_DUu{1ib#~pg{vQeITiXxl zRx~)8+r#*1W^vdI#p!n$1O@#=W}Yi*=uk5~wn;`oDarITV_f8kvWAYsYuGCiRBkiQ z)095Iz-G|E$dkapEI|r$!D5EWb-%3+C?8!C&;9;;X~W_PCeJdyy#2-d%=uH&4~v;s z=1=r`!OHcf^k=DAXJ%ls%@;P;zLn)aDy=$yE;`3JdExYkw7HkJ?LU9qe)^oiM8hv~ s%{{@3{u~cZYFelO3n^fb)qUr!Gx&aGFVdQ&MBb@0Bz#CEC2ui literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_library_light.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_library_light.png new file mode 100644 index 0000000000000000000000000000000000000000..5b35e18370ff4831baa5fb71e6db85f8c674a748 GIT binary patch literal 477 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H0wgodS2{2-FfQ?QaSW-r_4c-}H&dWU!$WP) zx+M~=-KP#Xa0s8O_S^cAe~Si3^Q28vS=X&{71_e2^=nb9>#Wb&%nMT+ANN?D{XHkX zAnw__XA;anGhyJLz{%sqonE{4Yx@41{Pu0vyH_(~UMAZs^KYBh&tCWQ=aM(CXO|o; zG@M^0@!>1Cq2&bq@YDZ&Y%lOTmTfV<@T+gzQ;iM$3oX>;o7c*JXixZV6JdXVUs20E zj+Nty^M#&!mETxdJ}ORG{`B5qhI=)h(>HCk%wDvR_rh%PXzjBtmJN$#bJzK`S~%oy zbCz?sA3puiUsj84f}a?Eg>i!^q*lz$DPXz@orF67$Z97)Vw{JCVJN_;F<+o#tKHh)OKDq90kH=LWXFbKQjQ;6=XRI^x)-;xxr87V4 z^wW=9>`!mcSv<+?@D=O%_V2&f-21)pdXZwN<}AUEDM7O~-n`hCHtllSAy_~H>4y8Y Y%#Sm#t=w|c#tJ0t>FVdQ&MBb@0CQi%#sB~S literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_password_dark.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_password_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..19725074fad91b3e1e0891d7e9b38fc43bf1da13 GIT binary patch literal 861 zcmV-j1ETziP)Zj~22zsrvS<_ZvTRYCHWEk?)J91`L!!(y z%Z70zQyuc9P!@U^EunO^W*n;?|9C+#~2Y25fKp)5fKp)5fKp)5fKp) z5fKYwB&g#KPZ;J09v-6%(nl9{B-j@1=J(M>A5(bij%6n4r;~#eMzQ%lJZ7Bv4VwQm z=9%C=<QnN?yu%eQ(qQNM~NwP#g9D;+C7(Qjn6c2T;MgyaFiFBM|u?!M2Ro_k{o7 z;6qEv(T%q@DCN!nFcG>H;fatjG+UdPrG4G0a6QveSPRtpXd_7zrT^UgtqwO)w={ql zgIO$x>m4tLr>uJmYd=5ed^w!OFZmKPOf$)hujV~G+NidSdOlgcW9{d6Z^AYk<(ss! zAEDA(dlw%ve@nzSMS~JRC2ySv@QoTDm4gw0F?9NVuKkT=ZuqJjm;e&I-ilXafeFAE z_B;G39OsD7s=*1s;U?!S=Y7`r!p z&WEJF(30IDsUQZB&oQ|ZSyu~Q0IOT2?M!CX{u~{YQ0}j+10H}e#!$vy8o9?)hF15E#u%cX>l~+g?K|U01|T9LA|fIp nA|fIpA|fIpA|fIpB7yS@;zx~-IsOMo00000NkvXXu0mjf`S69C literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_password_light.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_password_light.png new file mode 100644 index 0000000000000000000000000000000000000000..43402e853494d2531d6df7303c63181012db50f2 GIT binary patch literal 926 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H0wgodS2{2-FsFLDIEGZ*dOPc`zp$goQT9x& zN!x@xTvsP_vTo^qdv5PqXEtuhxJOLSgPp@v z3h!Nibo`g)dDdHMzmHfv(A}x9JTF#I)=s2;d4Jp`wzpRW|J#47nEcHB@IE2=_ePud ztejG(v18Jvo(1+-%4ZbrywLMwzgWo51is%6`+Y94$(rOd+)O#0f28Vpv)|{q>`VV$ zJ=094>8tO&w2aX#+}N_v&-?E)gXy=Y#z(WJg&l8NKD|74V*cc_@ja(!o!qYU=b-M- zl*zvNE)Lqv8H>W6?GoF6*|^H~e`xsA+bP9?$G&*h^WT}~Z}$H5@Id!V_F0b;Fs->AMl@~8gs#8x-og`$VG5Mur)%j3$ zQO5jTGTm#fKFa4W7Szo|z-#nsa)^ZE1zYLWg$GvCn+C)P2R0 z{fx3U%jbM4oO^3uu%C6tjEXxA3}?;f8EuZ+fAQemBG+Q(b+He&cTK;Ua+ufTbusgf zOFtr%*JtN72nOjSU2S1J%X^~rXK}Rfyu)H2s)e3>bbEZ+_||^5r^T&|*thj#h==_sAue;a=GxsRy&%IkXV_WFn z)@0>rG4_1ij{I}YcJA=7-gDH|{Za0w${06kcGZyS8{F&bH@_FTm9zNaWy_nNSKVBE z#=-W>){pyt?9qHB%3Sqg^6%V_ZyFZQ+?&uKP;~0$zA_1Ro97p*@+A^ne>{$i>8(7* zpm^+8tWT@-%hi46^TT-x)}K6?TDiUR&0|^SbB~xg^CcBr9Q?Na{$ji{?OooUv<88u z{L6l;%rP(Pu`z88By-7IzyaSz==ch zY15yDdi^_2x&A!VwI!yx>S{w&?t*_2xAy2KuZ@qazQdr{(owox@8pu7J#m+d)|O9+ vT%Nkqq5nr)kKA74Z%Y}mCU>CV2bp??O)cLd>nBUx1F7+J^>bP0l+XkKDmkaD literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_playlist_dark.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_playlist_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..6b9aed27996d157ff7db2d0448cd5ba3606fc13e GIT binary patch literal 723 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H0wgodS2{2-FwOFGaSW-r_4bZ`wy+~Z+ebm= z%+6(t6a={z3jJq&wQy4N4_+IMFB(cA6U1Dy`Qk*RHw2yW>@?SpbJ2+NTHVXo92k|H z_1)N3uB){}P0=?^&HBCZ^VAvfH_oP*cN%(WE}aqtC!}0tl#>%>z88N?uRO$lO{XW+ z>&c&}KNj0F1tMknEnl7fGFZeK@3a+2RN9wtk`t1@QBO;x5krw%Fh^P-xX6}V6-U8I45#Xx|;?@u}l^MmJZCzKlIM<+6JH~@75Wq|5#f*o#b41wUdSOeK;6=#Vn1cL}x2A8Tc1@d>3)qe8>;+vfJ z?6zTI(0)68-vx$Qccfl@W_`~n6~igL;M~#;jQfIKWX{|8V;)m@^(=My``r)D&U>~~ zF1#T&&fwl*o2ftEF=x$rX?2=)RpC7@{nC3}#>a&AKjQh&d{poG|7qLr3rSyCelYjh zzrBiTH@`p4Ka$sx6PVO?xL(%!tns$P3^%O5iJDuupOVlk>gBq+@BH5zcSQR@p6anU zu>At#JxkUfE~iWn%n5GTWVnuTQ*HeO%O5|^ZK-Zx;eKPmwBxwqbB1FRo_%dBFZuhY zWLwfh_2Rt@5{Fs%b>?%-ul(|S<0^Kb`~#cv%KWFkA9cB(R8`+e;#Jtj2@Smq-)j3M zwf~E5v9Dpy_;~SDJ?mTk>kOG;ckg{JtQWH3l~oY8XfPDXY~`JhbW^rbKEExq%aK_k xnI~9hv)|coAF`uO4rbmytbv?Tf>zzHW0w156V;w*rvyx_44$rjF6*2UngH0nE#UwF literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_playlist_light.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_playlist_light.png new file mode 100644 index 0000000000000000000000000000000000000000..19b4d489ab7bdd424de1d7aa7aba78fef5f58def GIT binary patch literal 786 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H0wgodS2{2-Fx~WYaSW-r^>)trY+*-{gWKx5LSidz+Yzh;rMk=1G$uxUOu!bc4lti)rS!^ zZ~xGKvQ@iPbKKp1S)RYL%~vciujI3Q68taWctXd;WhOiv97`qNL`>K}eafcJFaM8y z>M3O0lBie9*KGTk>B(eI#*&O0IfI#Y?J-L=IT{!^Bw`saJ?WZa?Rur!!F0Dr@LZ)= zOntjl85ji;LL3Sv^IMs7FbXg*G7D~~`}?QBtzl{F1UtrQ(+&Z}5XeA*@zMmV>pRp+ zGYd|eGKQYKoLsNy!TxfpYTc3DHBTlrB&FSw&c7WZ{W*BgQ&m|31>Rd>;cc6?m?_yl zXzk(6%q>)55!rqBuLt)8`I#>j*XiBXSGbWq|CfxgLhpl@0pG>fhzESPUCvtID`#xoc{|)U@4OmCAPrJq)rq zQLfjz_oKr3wHo`m|15i}_xS&fTkDUoeKD(Wl723J#OT`hFXlg4$2{%d`St3~k{Hchvtt(vh5wTKJuy<0PmzI1Az=&a3IF4)7sMwsJ*l?J zi)y&&92^sJYD2@BlKc8b;`{&a-}JqQJHYVPlVE4AxK-ZIST#I7ULF#Sm3L2Hx#Z!A z7=5NGr~E!md)64R@cRF4p}GPKrgW`6_p$ibs>cg8=4W2^|GMq&B7cXFMg~wU0nv~D z)%^92r}sCVJ7mV_tC{^X{6eIJm;&cL^WD#qYXmMxA7}lcd7M>4^o_^mOvaZMqGvQW zJWAB)OgNs&=%u;U#+lXX$*Ol7R_UDb)4O{~7L>@5z~4{o2_IcbU;biU4-)ls^>bP0 Hl+XkKplea; literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_podcast_dark.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_podcast_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..286f926c5970e2d8b6168571f32f3a3fc6bbb832 GIT binary patch literal 1042 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H0wgodS2{2-FyHiaaSW-r^>)tf>=1XEEOb|o~Hrw?aGtODk%lzk5Wz~zshSy&%9A@2R;bMJ%)05iVy^XPRk~?f3w-`P) zWNmw~thw@i@axT)4}Eej7e3aB|6KUgeE$TUdZWOS--?RDUoIZJe__h4;R{@@sW+gK0=;(C=c)Q+p?El@|(HF!|@yOaHjmeb}<(ZF`H&()m}X zUic@K=X>#(MWvgCC%dl6WWFYa1#N0}>HM)h6J|eBlQeSjzVyA^enPiq-K4}_H~fPC zT=nU4+g8|ApOhXjFD&R}?2lWTM?~f+d$-NXJi0PX&^r10g!VV-X-2GT9_OW;{BHRz zX_L6<+F6GUKYps2eB|5y{yBg8UnE_YB z->;79?*?a8&u;9I7Od{_^~~7cw^~qon$OA4I%zjf8J|%9_x^8gk7Vjyy|Y{8H(t26 zWuLa(hd-*FRln8-&)xg<=4!)c*=cVa#eRrh$nrgX`swZEfk!R1=bx8l+g5h)0_NS!$KexAGP2i!pr*u~B2u6^M(`}6B}xPP3s_*V8yWctoyaR-_k3Lo<*JU00)^EfBr%-#lrKqe>7 z2@G%)&vegXl2|;GiUUrh71hwb{P5mRp$8AYGcFfWeb7_Q7%illFuR)ZS${|k*NMuC zvrM5fI?wDMrHUAw)UOfQ(eC#8Ja-+-lSt_wijVAV-m&&L}ZQC`2NN` z5V^^+FT5>EYKFfWP)t{iZ~i5RT;!Yu&0`%eKg(R7xO2(*KI>3mUT5%h^>bP0l+XkK D#$V|D literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_podcast_light.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_podcast_light.png new file mode 100644 index 0000000000000000000000000000000000000000..ce195344d7f9e03995a6ed1743d76645add78e6f GIT binary patch literal 1122 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H0wgodS2{2-uxNU^IEGZ*dOPQR{$)3b5rnkB`FEeat|MVBo@me}4kZCZFAL_+0^zN-<9V zUN2C1I#8=PH16T-FCEV-`sQ6Mp7+>tzu)FZTH&^l|GwDr?zK=l|JgZeq1xUh3cntN zUs)u&>T-|87rEAdM;kA(-0kJI3OiOi>j`tM{GTI%fsZTiy)&!f(s+;?w_xVV$F(y1 z${XW8tlrM~@4xTC(9J*{8RYvr3KWpKrHt4>?1FF zPgYjV&G^B{`eoL<4;A(?e+%>82p;p!DssNJXl7C1yc20M#|ybK&sD0wR61MKZ2l;J zl1RZ~lf=qe_w)+oXJ5*nXr28fdhYGU*}?Z_%n&pG*?&Xy$`@_FQ}>R9FAY1h^|pYi zWt2y`QIB=SFO9j~rxo{jPM5Vlou+kR{>6j8?ceHs%PV02x%|6?ajaU+gJUSjrwwIl2rXGi~ozX9^~#y^ZfBI~hFHx!vL0 zqu2gZ?j4v`$JiLk#Kh6S07t=0_bP6f*|9h<;6z`xGW^}or>*ZXLoDF|Yv=7S&K;}k zSts~1@zl79S9u;?z;IXp#Tz4qb!DG>EBIO8oRI!Av5rwtUSC4!!ke?^UUTQ9ypWb- zX4>{*PBP!RvRRx}$_?gwxcdUxW~|8cV)*|!T;~V7mff-MD}OHAf66nAAwRfV?!{lv zI{hFe{qv`zLzb)G>9E^%P0F}0=F{s>+g3#W*l*NYQs^QxY4Jt(>r>OtSS&q!bYl5$ h6?s^WMbJMU{NR6*G%x0B{%aMGsHdx+%Q~loCIINq7k2;v literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_radio_dark.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_radio_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..f066ec8ac676275841b7702d561ee3f49f47d3c0 GIT binary patch literal 1001 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_refresh_light.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_refresh_light.png new file mode 100644 index 0000000000000000000000000000000000000000..039e2cc9ed23fc63a37588dd94c2c274832bd00d GIT binary patch literal 1309 zcmV+&1>*XNP)Zfj2ZrlzULh0Xo;TE&zz%qyr(AhbmHB3 zs9s|as6LvdCHjlDY2WJMe(JE}yR4nRgEdqf$2tL_M6O^aD zvxHCkv`#o7P>(oy{#PS6zPX_L08=yUOF5&VXC zElS#NIL)MWS^YL~A&E4VTK2X0amJ$RZ_@ZcHw0h#2TjVc-18Q`-uw;vcSb3DuUmm2c=5i0MNME=&Mer?8Ysg3jgXw zUa1V~Xe}gS8RAM~pyh5a#A!cg@7_ z1tZ`M-0q1RKQ<0FXpO0v_w_HD3Ch$p|)z|6uFown1Ir z)_!yJ3`Ve1{0GhPk>L(AZmfS!X9SzY4*-yk8eY8=oY{J@!w9yE9{_+Z(^lDCwvz9= zwGZ+NBiJu~0BFL@;pu^c61eW>dnaonI3Rui04>5#40n3y>RzCQ5gZXe000dcH{abe zzzIfhMEs?6uT^Zlc;L-Yxe*)^KL7+Omc~VO6THANBRD30fW!d*Q=Wj-a?O@Or!|68 z;s@X`=M43Km!`aQk~o#iQ>j{kNBwc0kSM$Txv`6z=#FwK_Zt{2Bfb{d>-jCyG>1bG{b;Ij^U5&0{A+s}O(H z7cL}w{d(FwsD^RPqNJVbvpL$J&-~f;j<|a9JReS8exn{{H38L`8sWt=g`Zb9wXVAO zboT!=aKHMF*Xh{_?-UBO#q0DiF;KT(n8ZV>Pfue8@6kNxILE8B%sI}{Jk4T;hPdCZ z0~Vfdk9vDLnvM>3YhCGRI;yw(v&{*DAP9mW2!bF8f*=TjAP9mW2!bF8f-m?F0#2{P T%r6)l00000NkvXXu0mjfgHDC; literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_remove_dark.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_remove_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..5c36b88d4450cd21e947969c1971dc482e282fac GIT binary patch literal 473 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H0wgodS2{2-FfQDuj(R}%LUd(^Ki9nN~pocm5itx>d+qc3sk2_a;m1J2RU8Aj+^T+n>6RG%FSEl{? zcIL~!vwRyi@@<(}{6}8)YDtU>le_f+si~r=GCd)GAO8sQl+L>C^0(*l%6q<27x*@O zwhB@;KCosPyG3TWZz}K2OA9MGU)m-(b zXJ%*zqYq1OP;D2>WL8(c=qk718X?aB@kvwUW)#QrbLEwC%hoC!b+raL_L`@QV@SoV zx93_5k0$UONcdeiW96AMck8#yO@DaaQuAfBgAh;^1H-R~wbj48BG&d^J6~JtTprMK z#owlO@^;G$j$*#^cHH>(MfTyjoKG%OI(vIJzZ8DCai6x&a{rvSoxL|>sv~t;8QCNb z#2Xc=TwB(6WR{gsHjlvp24);Y!+O8IBe&LBtvq^bpB3kkTZxPLP6=<^#^v|*^rO#V zXJX@y#=ndAPhl_*T^t{zHjyW(boTw9zdG0Py^H>71$L|)!`ay)8)p1F42oI?Pgg&e IbxsLQ0BC8W;Q#;t literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_save_dark.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_save_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..8038e362654ab6e4642eb89bfd9e97bb01ddd455 GIT binary patch literal 706 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H0wgodS2{2-Fm-ymIEGZ*dOPQAwy>i_dwBF# z`3&zIB@fqtBWy2MR(yQ0RN+?lg>LcY6&{a|dhLzmU&PA)bO zeaZi!VXK+{-}z0NzwXTX7Z|rWGvk+_VClQ!6HAZQUw`o>Hk-vyI{HtZ8IvmaMh#K1WLj5eqrsPb%J@xVYOZ4FkT*CLmD z*=932O{+M#i`{>g9u+V9=kps*cV5!}ss14K+V+E2 zS@@k;?SB5ys4*`4#!_EXBUyF+WW-5lonJrAcAWeiaZ;b-udP6(&--~R-DiCK?UK>F z^I5OMyqjVE9oEkSOr97o{Xd`k)trY+*-{!c_srjo?Y{M*Fv3gQ7tDIlH$=J-x6`&`s|}Lzuc${C>9pU585L72*qa z2yPKruu1TXz=92eC4vjq2$~3e*e*Cpr=k6l{|*K@Ck7S=Fj^tJpa#N1BYoN%=9T=M zz`ykL-WJXYjOIV51{Ymd(T*%Xdv}4BdC81uP4iyu9^d`I4@2L12r*gD_noJ?nC^_VXoZQST| zUgY#cb+^CT3xwZ%->!37D|TY-XRQadGhTeTX2U3>wrfKn(;VxCS`W4rG6~h2&4}g@ zww-VEUTy-f@ZFu6a~O&ayXdc*9nL5(WXJo-@k8r`^wZ(Y(+}NSe4xBRNM8DfflIaY zbtc`Wu!`$vmLC+(V(I$h$M!6*^XmEk*}G%DZ998U=ymYn{em{LUq5~);D2`63Z5Uc zqkq=NM$DMrzRCXIxert3a(m3)uJQQh(-OYUY@!EgX6f5|4^rWGwjA!(%KiW+m z`;)~Ts;y*uJhkf>{Y!)#uHIl>QCWNM`S<^l?;?$*o~@Aj&Ms1OOmpwOkdMEX=zQXv zzsi4p`Ke5A-oiSUqw9a{&HcamScKHWf4X&kYclr>mdKI;Vst09SKH0RR91 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_search_dark.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_search_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..70196b4e93b7581b7629bb950002fbc722407359 GIT binary patch literal 1251 zcmV<91RVQ`P)exwGl3wDTw^7t>(+6DCbpJ>`#brGnF5_( z8*`jvhw1LubCYFP?ysfC3Bu+&AE2FtYv(t{Gr9|X&QwmpBa2(wCGR{Q)bz<~Ct!$sLw3}{8wRsRZbO%vcyQLKJN z%DS!cuK?CPzyiBAWodmvonHm8{{A1+P!y|eT5XXij$LyEXx6sGWhcN#Oqn9UlI26E z%P$1(XtmqsB*AIl7R8KeLC{?kt351Qw^#mM28T2O&K9M>I;mN`gPtt5+L~e~{oN8f zpmhXZ@~itU9@`kx++U!@-vU%}+tw8AacL{SH(LC!a@~E0@oh=)g~Pet0aupx@zB;_ zKc@T+75w3F8eDKDpq^=)oe8!p|3j0*J+OaiEyzZ@gZpXrDidHUXSA|tt&P#g`+2>; zonyAipJACnWeQYlWzpJrL5$rw-wiWn+xle4aElsc4urE_hrB?TkiA<>fGV2!nK{R| zW(z-Feb*9na5swti~PxXz9T{hd-#|RKBJc*Vp{Xly8eGp1NGDzbAgfo5v`85uIMR} zBzQ!e1T&74UYB5!hdgk9)cP(&*iSN#%dxqxOX?>>QSZksZ%=|&Mm=1VTxN_;zSSg1 z@dNLfv?BDobU)27HA+#>r1q2N95pK;w((CMQU3o7@q&qsZ4b2$y8^QUK`E-~;WAJ2 za(|vH^iuiy-5oq|c*?9mP)aGPXyKG?I_uYB`)RROd~6}=_^fGx@YwK)Wgw&(Dl_*jfsp2m5eKN7 z6bR)v`7I*!GFZm$E2UJ~WPU*q1VIo4K@bE%5ClOG1VIo4K@fyu@Cr*TnM`v_k-Go@ N002ovPDHLkV1hgWSY-eJ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_search_light.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_search_light.png new file mode 100644 index 0000000000000000000000000000000000000000..e9c75b8f7b28cf34058c7f33db94f602f983aa93 GIT binary patch literal 1315 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_share_dark.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_share_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..df5bd1b984d2d036336b9f96deeb6e8384bf36f9 GIT binary patch literal 1243 zcmV<11SI>3P)pLTP>WW}4K}mwIGGWenYEtQgG#67ctM5Pv}q3W(3Tp* zg$fHb8&8I$m>l5%=Wza}PcL}z$l>Ok+x4Q~#r3&fa`WQ*-TiRg*LDAnF^ZxnilQir zq9}@@D2k#eilY1Wg>#fog<5e&w#j7Ov*E0(8+*H z^0I(VCj)+zC4VC!CY%h|AxC@xP0L(`Fp^^Rzw34RwOnLsS#fvy2tQl6Y0v*?;34ly&%|%N z@=Vgr^Yh~hX=jX?`~E(|Z9<-|3{%EI`j-_yLl@gwlQ1@mO7_ymuk_HvS&p)g&G@{W z=QVKIQtz4M9g3H$@{vOkB|JtUdEQQP%jmF7{0UC+xRDIyv@o`;_$k_XO>~u?96n=k z$!2!1s@Le@o*zDn*hV`&M3|FvEsx0@rS;jd`u9pw9N)9nhY>BKvf;>0B zhi~SH<*W~Lj9L%x=f+S^zhysPPFk~V z$-$Q@PVtPVkGmuT+MVQg$V+T{oeUV1XF$ZsfT%nJrrk={t9=O!$ur<5Cj)+#XF$O5 z7(7mS;sx3J1{5>woCpOemt(*hzHm;3qg-Q;n6ej7J14_)@eAc1ZfE=_*Wq#61YC!W zA%n&g7EBYiEQVo%E1qT3;>Z{1U?iai72yWwcv;3w*vcQ4mU)%1x7ABLH7zx_!xbHo?cok@Y8D93&OOq(a48fp5=7XcZ27sUKPEh zMH>of;s_x^e9vx*jG`!tq9}@@D2k#eilQirq9}?Z{{ldLUV<>?LuUX0002ovPDHLk FV1mDFTsr^& literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_share_light.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_share_light.png new file mode 100644 index 0000000000000000000000000000000000000000..1fa628bf41cd6249ef2127d6ff509064029b06d8 GIT binary patch literal 1332 zcmV-417(!{haCvq&7zQdo+M#2oPH7R987##?Ybn7IiwGDCR({kV z6aoY;f2}W4N`SkNhgaURpL=_6cJJAJzPtN8XBdJY2!bF8f*=TjAP9mW2!bH|9|={O z^=AFYA6!DyRbBKR&fzky;7V*6=iojBE{pBHZa+MvZOop0D2+o@YxuaEU#dEA^|bNt zpMUUkD(AiQyKoC_R|nX%1HFvRkAkb|`N7c6G!Qy&6j=QE&xUGc8U6Se4~ueujpGb6 z0oBraQRZhciyM&!e(6sYZGINB*vZZT@LbCQQ|ugoi#iT~B)=T6qvHTd^IHK~t!Iy4 z4p`H2z#=;pK(ks7_=%SUzK#jp)p3C8Vq?5n4ZUZJ-*ZynCKDkPtcrEhpPK0}i#9xA zA%#^dE$D52@189TF=%<=kY3auZ-98m^Tw-z*s_axtC%Vr{n2|~7d&BHq>Q6^yYv78 zXwjy-{)=_@m3h~89|T-Wo;{k6-psw$=BG*AqJtBrAK+U|c)BwjX~4(0;`m(;a1J|- z{4ZOgf^Zkc=q3IE$=D1`r~zs*NiVO;^0F<2rG_pj6Gcls7hQ!dD(Q-tbv*7ZH12{l1Og zSxp@JHY*o0e^v#}3I<&}HCW{AE>Gmr8@Owf861e$Xw2^39cQ3N;cIMUjp`_HaKY*8 zzkp9Xz1^LLCbdRa)9i#TB()r{#?AqS_y4n^h@As86@Y!oLx9%#g}{_n4)~d!1FX2- zHi*y6#{2@=`#GRiUkJgl*~rHOWjLfOh4GMH;~h3_uZ|nqYasXhVm9`%s0S`y4)4HN z^g0U{Ku=*8P=?Yj!LSa)!73g_QOadMuyKQK2di+Dww>RAq-ynkXCT)UeRH}gO6$i> zQ4F~@MKdtbe#`S!B9%jcZo@Ra!hJfxC3p@!5Xd(xScPGnb2;DfFO0PL4gQ2G)z`G? zvUR4cxVI;MMH0RSBly-i7%+jv`~r+&7=~G)T5-m;Tl|dqaP&w;!re1z^ZkSvddPiB zSjwcOY8a6Q9CA^Thcu6!CpCguVKu8hH3&~&Obt>$wQ$H7rB)Lr+_YkBhFXqZjNXK4 zOy%{U_Ay1j!lyj?3PgW7kIX4e58C|k8W?dsAo~N=a?p}>aNq5i9Q{r6E+-0d!7|IE zrYMOt;HYMs^+<6pa2T`Yf+0+=FXV(7HUpQYE6{U+!_vE)?u_}&di+xJ$g~a9%>F50 zHREr%r_*$2_cwu1B@Sub7*XuhGCk4?gD==Q0Kd_4z^FKYT?q7F@(@r;WAtW6f!_+y z`hS4+5y#Ha)1n=a@cNNU=QL;)sR(x&-5bp(ScN|pH+W~;Pd2OR_w(o&aq9HFL3)XyOEF72qRkJzIF1QS;P=!YuQ3Ec q5ClOG1VIo4K@bE%5ClOGH2Dt&UZ$Ekp2qP20000(&x{^bCXwz*f9 zE=_t6tmSoOmA3a%J;_xaJF`O!HyRyu`oYLy#wK2~1{*_99mbI&P zNgeN#{e8=_@BZ=K#h;(mPAxva^ZMr&0VfVcRAQox*h=fF%R<+M?ZRVg)4sW`|J0=W zOlHqYzuDzA-e2}4-aK|ae}i-X{4I0#XDgnr$!8a(){t2lpXP{+a&y z{~QF~6`lQ@<82_*QarD~a`w;m`|oc)cM>>PDfj91a)bJn5-bN7{yeSj*nL0mY`mRd zm{!I=9n*SImK9w;H(&W{Oq3}CR#alHSdZayy|d$d8Yq7b6=X4l<=(o zhUt0z5mV0oNf$mVSd+Cm{ofPci>&Vov(rw=P5WW-P-fw2@!ic6O=|zC$^V$0v*4KO zu50eeny2L6eXzTjk<;itCrV7A^z3=5_{Yr-+wF4=EalEed_K;)Fu(3HyNc1ZKaZOX z4E9Gd&dI4*+ze!$bPnP(-y`T~Q15f@VVk)UM;mLwy|w^C?o!+B$=g%>&g=)K5XWx&~eDmSg9{+ZK&h_W_n-| l^KZF)rnX|F!j3h6*kjlhEz5B7nFh=T44$rjF6*2UngB|iYjywt literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_menu_shuffle_light.png b/app/src/main/res/drawable-xxxhdpi/ic_menu_shuffle_light.png new file mode 100644 index 0000000000000000000000000000000000000000..5feeb31f3b7ac471d23726744f7a8eb524e58724 GIT binary patch literal 838 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_toggle_star.png b/app/src/main/res/drawable-xxxhdpi/ic_toggle_star.png new file mode 100644 index 0000000000000000000000000000000000000000..160e2f56acb0dc6de0eb34cf19e79a923a46803e GIT binary patch literal 1366 zcmV-c1*!UpP)BRwy zz$s|Vrh*V+9Hn_FiYQEqv4_mUyiwuh_?s8U@p3MEZ_i$P?Y-Ck{p_{=zq8N&&Ue|% z<55ylQc_Yv&QGzGvAo7@H9BGYxztq(vqJ_so! zmtsHcjv z6tRzNGFd~CrBfw%Nx)AgITTP#1$8viN(Y|^AVL8?(LpPX)KNh(1>}&4pM)siFU)5- z8Dx>q2}-$41I;|;-L$TrOd8@HPidxs%an40e6q-3IrH^b?kaggC;g0Jca70cCr_wk zksbto9^)iE#;^A-Sj|1h*uTeWBhP?XE-+$y^CMg!*63r<$4Q-8oNOB9B)-|jf}`}< z)_xC1qY(~w@WGb$yV((~e*~%gZ9DsqqFP%fg$K5>{~+qs1eS2aHui6@#Ecxk9$|4d zVNXL?JiwsT_6IpI%dn@R!PzbGbAP9Zba=DD5|`O(XS1k^Kq`+U410c$IFZQjQrW*n zg6O(XBGt1fc12UeRFh~;Y3!5)VR3|0mX%KNvESmbILLmJ!(K!PTX|)f{a0+2p&*{l zGfV70BVC^6=O4oD-;t_$N&>eW9`-bJhRzA<7z}Yv%J+@&awNhea5U7%5s5p7)7}O& zLkHz`av(OT_ zMgX}&>jAm)0XQ!dz6=)bdsL=WFFkgY^ChYGd2~rA@qS(|v_jiAtla(v=2%q-5rS zImA=KM;%ugV3j-o_GsSrDAl2lPb8UY9izE>^Wk z1r|}PX%MSTl9Y&4zC^t78(637n(O#2;*a5rBnK_9JHkdEvYk1)uR4eAJdAMV-EsgZ z58KK6(fFTMV)-d-x3NqP0Cz%8?_8h#(O6_~J>(=(lMMFbcpG$c&&iF>Lkb_cJP*3Y zTM5pcA}h$odMRN+R99I*NzgNBvaAPiX2#TBJ?l+bX+8BbCKJzC51?_%hF+6xUUbGs zHm|3w*Jynj)HUhG|Cwc`uegj~CjA(?EKZ3?o0!|FW38}N*YfMcJW`sq0QQa#s^^LQ zCh(FsKD58rS^$^mp^PL;)=r{~9xhp%231%)tw}l+5_XA{l$4Z|l$4Z|l$4Z|l$4~_ Yzq*&Wf9n_xc>n+a07*qoM6N<$f`i3;5&!@I literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_toggle_star_outline.png b/app/src/main/res/drawable-xxxhdpi/ic_toggle_star_outline.png new file mode 100644 index 0000000000000000000000000000000000000000..d1b9f4cb480d0896ab9fdc8812d4b86730435b46 GIT binary patch literal 2071 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 QivR!s07*qoM6N<$g6Iyl1poj5 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_toggle_star_outline_light.png b/app/src/main/res/drawable-xxxhdpi/ic_toggle_star_outline_light.png new file mode 100644 index 0000000000000000000000000000000000000000..28321f8b7a5412d4eb57e79e990049e5ba49caed GIT binary patch literal 1647 zcmV-#29WuQP)uDAt9gJ`k1&vV0*br4<#lQV>OpuvCOjOAyqPrt~9)JOcs2Q7%f)^*_&12nb$6TN=_Y&*xz*LqK2!FU#}OP5?6T9%A+T zh=w$y-;_;#`1i8_9OH+alV1DZZ%(eQzkuLrtu~$bd#LME09^YP(4FB)pZ)LS5v+V6 zzzSZ3E7C`RpYh=f`T_!T+?poz1N-?t$G#F^nI0R(=@9^#Uw=hkKwwIXqyc?@9$n|9 zcmi+?cdNc}y#QJ*kHuYo0m0*D(Der(w_Qk z+yqhMtc&S~_EAs5^%oG-cl>~R*FVH>wi19}>y(k{O8P!8UD}F11O%=IzH{gLSJE>} z0=$6Vjr4QyWBdf@t4*+EoDJl_I^lBEv;!6UT=d$Qo+YOdx}xL~yNeGK9$&nLhBtL>5~Mlr|_jdon1 z(_GEfQWZ+UQuy^6YDOD6c$kMVu^pP*lIArJ^DuRxjhgiu<7j=EjWo;ieco^dhsfks zbWjh5Fv`>0TT@#_iSZ0Z%^~9sI=IycbBHUoZ&T28m5pe!-_8YC*vs@ex^n@p(cgQP z{w!TdK@`x>ZPLh)o&FnYNln8w3rv6RG4e4UPh~~vF@ty_U*u0f|98?;YpP@!Rp?J_ zY0DG%_#ynMpEmI5hs+!DOTbaO&6PF{TTlr)KF#(?`@Kh}cF;y!JyK|##gw$GKSM3H z4_&fXi#V6{CwJ_bjgE8_0=cN?XOf)>%QS8Voio!t^aDR%o%598Tfu~O*x4zw%*Fx? zSZj3Hzxw7-q;MDdiT*sgEyG4Ie%-~v8tO_g@8QI^$TxS{7cv_$w_)hfxt;&fXp+Dh z17l(r0^+hciH3|U8O*^sks`mzKKt*l@vu42x@FFl|V*_@!a zTOM9xbj-?c=`}pE)pnwHTqzU(OuZ(NlgBZh(a%|*;Ncvc=V^{5au-RLs5!<40!1D#&^N!5uZxkE0KX* zI)MS6-7p>2B+13frtI}kqwo@5ppASf9$(1}mQo8n`yyBDmE?A@KX2`E#ATjFcc{*u z_#DVJj?#6WUUz#?jxbnvU^PfB@MARCTc}0~wFm}3zeX!bO~`=Ym^n!;;R**z6pwxg z=V_c{5R#0^(G4u1J5=Y~jx5jDYv?-9Q;!tR5cL=a7df1mW^xfU>XPCdFp77nHqY8w tlBJ-aprD|jprD|jprD|jprDYR_zztTzf|&%>+b*n002ovPDHLkV1khd7P$Zb literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/main_offline_dark.png b/app/src/main/res/drawable-xxxhdpi/main_offline_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..3a4a38d3522a6c02972c3bcf142de6d7f5bc7d5e GIT binary patch literal 958 zcmV;v13~AE4QQee*kr!TQBxX=Vk(C7%wOLa10UDLCH)@b( zg;1J>l$KiUs?6W?>q55=uv&L#&N<`0zvnM_o}HQV?)%O-jvxqvAP9mW2!bF8f*=Tj zAP9mW2!bF8W*~=rf&^JkF+qX^$juDn*=%Gl^|a7Nlqk=6LzE~FxJnZT*}RB@ppZJc z7-Wn%Y9389&M@6HQo7izFQJ@ke3{qwe2US^HVPJp`CP(8_?oo&gyM8?kUZP%ujW1< znOU&?S&h?S@pd4G?ewK+{g*}vTe%J>;xyxF+n+rVE>U8e{bk(#%UPd_rg%cgCi_7i zX)~YDQwnXdzm~_^?JE>fHrZf5pUX@c31FIbmixcdJ!HguLMO@gcL3GIj0O;++RygO z7%*yIp?F8Re+!=`ri}+M!wvHN8NgOP7`LxbjIqmaHQzp(>0-zg`wGQlzbyG8_S31c z;!9rMuK_l2hF2a~pIZZ-o&y9p#h{^bCpnF>%{%+0w7YQH|F2!r=vAdx#aoZf&*~vd zy*T!28TEyI#efGr0;>4#6Z?u7JIEu)P5TE7*ZB)oKM`6uLZvI4fePOF%6>xM7@>z_ z6q{~w-SMUUU;4^Xip&L2>*-fN+0NlJs*G$0iWV@{@mAwpFfdW#iY4}Eb)JA;`x_ay zDgZ@>mW?!M0Agn8Zkc*8Dof>4do&YLSp6zu++X{ zl$|;PxMytu(===NvqX=z0Vujzk$wQmV#U~!*#Ri(GChDu`T@kP4xl&v0D7$sU?Tkh z+U(v0=?8E&(*xL%>04l>b=`p9^&&NyhuZk4(nQD?RuvJ<(3_Vx|*Ut1IS{JMfT(D zH8CTsVbYRr;C0R8vAFUr%$`~B3qba{>}N@>vaEKb*Nf0gxQxqEG{nB(*_=IQOR873I!nrF}I2vEUc z&JiI>l$Q*-q<*4wa-9?G_QA7CvI!6(L{Jyp1cHGO41}mrK{OK1h!_whJgh{n5CxQQD*}J5 zGYG=N0t?$S)6Lee9`zQRH}g4t4Ti48T1c)3c>7b+O(N z@yGGRmm>GR-ykma#oiH&28rd@6w;!j<8NEAOeOLbKAqM&b=UyhH~J|<&goxKpk<(kuljA*^B57c1GFnD z|G+x7Rf;g+y|M=EL6?BT5`9tDfCGv)CVoOtSN`FVxg@C#u=ax1BsTyD63+pRAx%w4m~dhg)~Iu`5fXw zA!Hqh@2J@kuv_JV?i1mNqT@d+s3}rYh(8oGU>|#geLFN76x;zeAKVw#84d+-T)EcR znAm$ErD~-@VNKDKyCD!!sTC3dCoV`dUHZ2{Va;ZVTw>oR80Xscb2Q2rdXxOI6h1q!fT%(FOAV~@Qxg)YA* z3Fkrw&Y$@h+NFY*wrK@>J$qKC`StSJMtDj6htJ{XY1yTiv;J)o`lu6i8J<<9Xnr$j z2DN-Ba3{2UGZ@ZwX18Y;hG7_nVHk#C7=~dOhG7_nVHk#Cm}|ygh8T>?S`p)d00000 LNkvXXu0mjfoCx82 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/main_select_server_dark.png b/app/src/main/res/drawable-xxxhdpi/main_select_server_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..649e4c03ea35489a9498f19c73175526360788a5 GIT binary patch literal 488 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H0wgodS2{2-Fs}D>aSW-r_4e9c@52r<$38ZR z-v7W_Dj?(XUxHmwP~BW`rBa_2r=F|~i@2bOp!t7+_|+4C&z4J$%FYVftHQPS^!MWI zABE+W^R`R{*@Fr;EOpe)e&ny#n;m+1o$P-9*Sju9e$>9O!tcw1*sr@w|ESuYe&Ksu zO4}sUg`Mk=C-2&oul#%iN4>lvAEd8H^M3kp5_ zzvoyj@6HdRD_#ZYcfO4lS2X=|R%GX=V2L z-|m*J>sFG8%|7<{L$t`j7nfdth!#Eg9*IcTXj=bpyH3;jhubxp-aq`mlp~enKfkMn z_mABo3w<8?YdYOLG+)DM@05J|GS*Z(JJhfQ@%|mOmVA9L$9?*_10b%atDnm{r-UW| DI>ymR literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/main_select_server_light.png b/app/src/main/res/drawable-xxxhdpi/main_select_server_light.png new file mode 100644 index 0000000000000000000000000000000000000000..328bb64e7c4d9b59e8a4a6a98d91bf68fde5245f GIT binary patch literal 502 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H0wgodS2{2-Fz)qqaSW-r_4b;#H&dWY+egLS z3u6_UPK0)wC3ZZoZ{b?(x>`v&YGZKf zwC=^X%RSaDWq;0Im~FR4qDgD}V}ARMZ5O)j|77fsnfZfzf^zKp-3=1X9LMFW?k%))`t6VA84uHU0&U0Y^RCu26}Bf- z|KV(Z;Pi*HZNc=p9ro`8zO1S%QmtF87$SC9{&wW0?GybYW%eHwIK?0RdT)Qu`jfR^ zeuN#*H)wI#yQ)UL?yJ#@?fKPz7PkwWC{8^3^oKj&!qN-w%O9IdHog8ho9E!iAG~c3 zD*ilkE-(k0xkL8<3&kCB|L;3{xc_l(pD^zSJHMjuNB8y#`+odxn{ez$eXQfIety)j d1@am`{$|^;x%b}Xy4{;WlAf-9F6*2UngBz@;tBu& literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/main_select_tabs_dark.png b/app/src/main/res/drawable-xxxhdpi/main_select_tabs_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..093f950592f7e9e42f385dac517fea9baecd0b3f GIT binary patch literal 435 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H0wgodS2{2-FjjfGIEGZ*dV9^#`>+E;+e7If z5q42^_KWscTo1Y~d9;A9=~7_V`BfSc301j-u&cO zU2ZQU&_GnM-@CJ_aR0vm^NVqfLbBRdL&BpvKYu;{Wt~&&v0Xjum{g^-LJjRRQ=DfU zj}P(lab9sG-gYC;Lb3X@&2=sC(-kdADf_a_=7wU#qxmoWWa=_U69=ZvoX zWvu;;ybFE*+@E)!Ba0*VF@Fh1>?8gHj@ZZiIUIT)*-cok|7ezM@{4(#Z`$(te?4Pu z>bj5Se2J@^<<$%>95Ma3)cHio+vD30tnxd}WpzAX)@d2{+Ml1SFI;Xky8c5Ps8D(R zKOW7C4$0Sl)bk`h^8B+^@yp%E>vewod>6 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/main_select_tabs_light.png b/app/src/main/res/drawable-xxxhdpi/main_select_tabs_light.png new file mode 100644 index 0000000000000000000000000000000000000000..49af1f8623befd2df7aaedf0512523d8c16ae3cf GIT binary patch literal 456 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H0wgodS2{2-F!p=8IEGZ*dV6iR7qg>8+r!pg zE;%X3BIYvAO`rdV3LHHo(5j%cEJ$a~nkIoadpJFf46C<_N!>BsZOPO0?#yqW*=c6) zw|j~(0!>5#4e?^`TNkXK|K*9^9d`lsT*=i_rSEcl`gQ55xu@#Xg>miSj&r`G=)Alx z*>2Wik(s`3zL(TBfqjq9&r4{V&~IPeBylL_Pa^L^i+>4i0dn;M&Jhy(|2u1R#b#ek@;_C?|bj8(O0jff4n+S#P!*MJ*TeRR}?w6UikBk8*^)yM4IzkeJtik z{b*lX_uF`v?)Ojc?h8yiGQXbX*7sZv?IZI`w>-Ws5Y}`5y5bta{r%1n#&!4G9tiCB zcP{9;KU?ucYxbYjJdV5m|6`mnE$aG@>Aa4)MPaSt3GQy|KI%&~MHNSPZg)t&9CJ=9 z!v5bT#TBOE?a`q>Wmn7$-n#CiIIrVc%k|ghFO0AHz$e5N$o)SG1~deu{P)cP zMnhvskY6wZBNHnt(}9j zo4c2{kFTG9a7buaL{xNaTzpb`W>$7yVNr2;MP+qOLt}GGduLbwlxfpv&YC@c!J@@W zmMvegYW2De8@FuTwtdIWUAy<}+kfETp~FXx9y@;G;ov-gS~eTsqh2z+%zKjGMN)t1-t! zZ#c+s{8YFv+m0)dCXx?+?&4*$2+p6(n3p%3XTq9$W|9wnP4H#=u_4vyz}Ih02N^o0 zwap|RROH@EY1o}?Cb=NzdP;+JEbC-Ox%}G+4V!fzo+SowhIaA?7)#Q5_!gx6)suGUWWSxm@Y07_#-VB_Gy8?!cS_O@ z$Ft3DFkG6OYSB>ifnRx&T>c@(lC7+K3%>OGnl+T9itsHE4h8A@m)FU=pg6aH;pJH^ z8HaSX2MnkF+)7~f_#W<^!!UP^brQ43da(x#R+pLC52P1Q((?hD^Xa>sv_q9$coOrA z5Ox^``^Kgv_Q$$%7@qEv-Nd-$_v8YGROtr{KUQv0nJcBgfpN;O9p~95&o5{H2jY0Mf; uDJDSGzc$-i2!Qg4z>|F26IbP6*##t`B2QWZI0`4lc-{I98yj0Yd#0n4v&*{m zthd;%98YgwKQ50S6udDcbaR++izs5NI5H|WF8rmX;kauc)l5{`k|H+PeA^CmU3sHJ)lY`}ux^@aRZU_>O{~AN3tu zxot}QkH%hOw(8>dxCV#oS-CD$cagTv)vB3OyrF$DYtnmn)BeeCwjT?4arC4q314iR+2>5WtkF-noe`WrjXaK@IC04Q$fGfL|@FCA#nB=0_NPEm?(nNGtsF-at$oLE1N6S48T^J zM#j;ZPqMk%bDB}8{`DeBa1>{tF?}3nrewO|$%!$s#By7z5*wC7(bGyDYL$@n*l^WV z9s1+Ez(Jiq>V|jE`VJvgVJ7VTP91bCoo=G%W(>OF#WQ^7+^zMn=-85_5=&CN-Pwm) z4?)vNtq$GYn#C2$dS%p5F-eq<+S9EY{g^CX3Qs#}Q;%(U;);tNSb?E>X&BQtY+stu zgua`*GGoOcAYDbIyL^OSFg&g%sj$}u^%+{skVi7qGW8%4|M+4*p$%i)SWYOz_2V&p zM11t@Mkit6hjf}&;ZSS%(~O~}HRqy}iY_TGUfMm6zI|R)MzVTZY_rg`^Ko39eLfdS zQcCeTMMHz(mkueek5UX1HNETlu{^mQgBp9lo}MYi^}}&|0cFUz27%w%&M53y$=NTj zl#uG^*>hQF;Ws;Hp^BJC%bE_hL^3vkJo9f)pj-7jgQ+CN5BE^i=GHdsY;-&9O?TG@ ziliQC_=T(X3|3E~5*I6(&g$p$6v>)?f9M|aTHUwlSGM&YO0ClQ1Jl(bXqx!$*>ORIk8G) z|0O7kx(T$FJ%AacZ@x5EYO<(nN?9O)XO}hwTd%wi>-#yIhHSs4{r^_1X|mJb^;)eF PuSk&*+eBY&k(T@mjJ4Kf literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/media_fastforward_dark.png b/app/src/main/res/drawable-xxxhdpi/media_fastforward_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..f51569fb90190d189a13adf50820f81fa47dc32f GIT binary patch literal 1561 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 aIqn8SLv-qKE`|50AURK0KbLh*2~7ZhF9%}) literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/media_forward_dark.png b/app/src/main/res/drawable-xxxhdpi/media_forward_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..10496b1f7bced2b558e7f130ee41a712421ecfc8 GIT binary patch literal 1453 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K58911MRQ8&P5Fn>Bz$e5N$o)SG20;jf`8;j_ zMn-c%eOL3_R39Pc*UfiHEyT-e@LO5DMd#NMyiYdnuWHv4ianq935U*{_;fr)q>+Gq| zl32OiSgxQYY(uZe+`xqa?%O8LS|^se**UJxBXipHT}#-5HXSZ%7SpO>U|BhYOO4GaFR6ko{%VnRX0wmZ!S1buoe7|*_C+fdHWHc2BFfjl2PItheRiMIy&o`%SD|uuat_1G6fV96XZE)9Dr{Z%fFbMLAx4q+y&D;q z?DVu~FxswY)==}Z(5xZEu9?|`ThO9`PwycE*SYE&43`cl$vEsZv}j;D>I;;w^Vz`o zB%Tkbk^kI7hEMy%)0j0T_yM^Oa+-J-Tw4AbXw&B?Aah=wMZ=aUP0Sw2i53n2+rQsr zxOB%|&aC0l-vWlr^?d9T%*q}zEM3}vlOd>7{S4cL|J@qW4zq50ZeSF7@p%K|lJ#mp z3%EFgj)(S4)V($Fnp>vJH;08ayF+~!xfuWW{;zR77hCM6PP`0 zr450O{2|1*Am-X(wh90G4l$POVPX&XGZ|?5zQ-cc4qV@Sfu=VK$T(aDI&6znBl8VM zBZdQflLQ&cCiF0DXii~jkX2D;xH6{|B*O@m@j=KeY2}b{V4q&VVC})i093*Z3?2iZ z9%hC&3X%*7>?c(k#H1A_e_1e}Wxm!6^@C;8{u)KxC`w^?|8;>f^Onhp?rb{}>t(+2 zZQd_*d%cB`!-n~Pmu75b`=9va+(gDA-f#ZN=f(SY1Z-0N+r(MCl9~9B>BpYZZ;C4) zbtp4W*`F#uO!pi@*wkR{#{O_83=Fg6MC!ZZ)u>X6;FTK8V!Q+?l z^~uLCzkGSFfk|A!KsdF3#UHuSl!lsK+r^*Vr`Rt2s$S)_ddhsK{*_--%4H=JtdGvS z@+W(W?W$i5a-qC`4$r Tei0}PG-wccdD-S^-6^RJkOOWePkTC)y2vwbM<-K-yZ zvPwpBL3i!NN&j1GuU>my)x;y<(Z68*?ds1D3SVfOpP$RjeB}Nn{n)eWbCa1QX6iE^ zI8h_X;P&h^gMm>zTf@YkLJVilh%+Rl?PFlj{>;h{F;k!6z?m9GhO}qg3>%E%85(B( zWP&J31u8LyDLDmHQU+3D9M5oI_qPWTp!|_AVgFZ?vXE+MUI9@K(@lz%w4J`51aUlF L{an^LB{Ts5n^`*W literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/media_forward_light.png b/app/src/main/res/drawable-xxxhdpi/media_forward_light.png new file mode 100644 index 0000000000000000000000000000000000000000..e70dec650fb7fc1ab4d6008209c310b57a3f9594 GIT binary patch literal 1407 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K58911MRQ8&P5FjTbz$e5N$VCRm#>U#(+A1n4 zGBPq!Qc@7EwY9any1Kc!Igo2$V4$n33shrfW@cz;sHdl=qoV`lYHDig>+74EngZ1T z#evFzQbtBbKn748$Oh^HiUUP}T(}yD6i^T-0wjS7fB>ik&HxHR#7#_0fDE7%gbPsy zmx34w5ri0prVPjhibJG;${-A&nGgm<7DxgC#A=XIu#E+Hu`D<`j@sG_Q-uA!x8Xk=n) zW^Q3)YiIA^?BeR~;pye$9~c}G8Xg%L6&n{HpOBc8oRXHFnU$TJUszaDT2@(AT~k|I zSKrXo+|t_C-r3dN)7#fSanj@|Q>RUzF>}`ZGkaAS7?}M$T^vIyZoN6!2#WcJ1%FqH zuVg|>1T*$jJIq$$Xjb_1HUIXuTp-%K{L`=Mrm7u%R)?(JQuLSIns5L6licsC$8Ezm zFfQqz=wjATGJ}czLUy=`v_sUn7UmZ_+4vT`?*xi}3E*JA@VY=o+96MQHm|~7lLL%P zeuh{y?CW~SP$k`;#_ZAFTEMV(`(ef>VV`a?T=EV)z!qR3dz@{;Iq!!Iza+SBFce9v z<}kcmtCqm5v1!UNwh8M_GnqBmEq}mpDbG;GA&dV3!^1Q&X@{y6J!#A@s)hIz=7t_* zTry9?!Xd@HpI5>58fy+i>uXM+8H@B1m`{9l-^{q=uaiZC-7TOwhYqIz9e+~-=#P~W z77c#g4;i?uAKzr?k~soY_MFe6VT%wyd%^UrK<%5=C-N%HU2>T5%RSMvY!kK#KVaCE z*$A}Lq>)#lJopgXgnPe$S}!Ha$T;+$Envtr6yw1geqbLJpolkO;4Wtk@x-b$(1h#VQQ`6L>(3f`(MH z1KE`(zdQ@JGW=EfarbDQq>ZMj(ShSWJrf!Con}^M*4bdsSHES$exci121W;NtNix9 zc--g*zp|$rTg7@)W#%$sDPh7@UfM-7li1jPyy-GBI#6g>9aO9_&vsQ*zg%dk^_$4M%co^9 zGw)c;WNdW6L8m%y|1BOKftkk2%r@8DCNmz|n`3O`P`+g%gTe~DWM4fTm4kw literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/media_pause_light.png b/app/src/main/res/drawable-xxxhdpi/media_pause_light.png new file mode 100644 index 0000000000000000000000000000000000000000..ec0ce231e710ef1ef3bf17895af39a769be71258 GIT binary patch literal 281 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K58CaNs)Vi3hp+HJ5z$e5NNLyQ58yg!N7#L`S zfvKsfp`oD>7`&{n`3qFRRTAVE%)rYpAS59hnJ{bGvQ|Ez=mJj{$B>F!Z_gR>9#9Z* zz4)GqduvM{pTvs*R^NhXW_=wWmFMTmEkY-$ctX%=w>RgCPk$FnIJVE`dkx4X)B|&P kE0X=^t*@C5v+U9)`Th6)q<%UhE(5Z~)78&qol`;+0A;g3761SM literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/media_repeat_all_dark.png b/app/src/main/res/drawable-xxxhdpi/media_repeat_all_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..0bb7d115a36347e87fcbcec4744673067faba56c GIT binary patch literal 2624 zcmeHI`8(8$7yeAMScY)RBr39vZK%P;wT>kWAxn0qjH0nCWScS4+_GeblywY}-H1q} zaTS9s*+P_CBwJit*5<4GeeUn~54i6y?|a_ooL}DOoafxJu`=c773Bp0fZyEA*cJdl z2NDEugAa&M;bad0htAqq+M67dod57&0{=RJtrYQvgQaP@Zfj)+^j!U|C~ybE^H1{TL0E^KG z->(ZHD^>>?-+0^-p>UetOcZ_X3UjY%?K=Q)fz6FE_7UTYeP=;{AOJCdTPhfZ$oLW8 zP4Ns?oju8a$K>%*;=M8tiSJ!N@43*(_I~GMx< z{t3Rfg1-Q?ASbOZkDXOD zdYGB1<$I-FO;we@Z*gVujEd@VH!9jqM(c=-t!v2sy`Yw;TaGU30Ap61wJ|c1>a^Au z+;Q2_lO9t!?}_+Ds*iVM3UB^?9Lw%#%+2Rd5nb{%Rn^7*ouoKl))=M#rQ zh+k7#ZoyMhXvq?PjVn;9CgTRztnrJhm)KuE{M}zX)@6HopX}Efc+%B&w;ZX`tm_4) zcm=|R?cvW%FTd?DYHCVR#|eL^>M|tCWm!(X*NC6UIFj&=Ecq!%=PSml?Qf}Niu8OC zQa{x!Slu2r)Hh-DAT;HwMCzMx~k<7vh*Y-~-xG@EQ(vY!KieuJVP zoj)~P-F{JzLZiPvAL-y${420CAFnoDM120rIc;H2Yg#m=iT5!y8fdl>n4XBT{E=g0 z0%R;Iv(-(ZE*W~;%6OQHP-YWU;ZT0C0Di}{a~fqMM4B1mj`*A0}9JP`|p0 zQyY07TmDIvS$xF!N+3q4w4WwG8Qt-mvpyD>kH>i-%9HFLt@-=FBui28oY#YIPzq@A+hvu_AG&3v*#% zC|c|%3z#NPP%n9A(t78(D5U(}YSkocL~5Wtga%qAWfgV<^lDs-tZ5KwWOULinAM>m8{fK32!SA zvy}-5n$RtJny04u?H4rnEQTkClvX#xN0M?6qFvBzfz9?Wo4Hb?rlOZ5KKp>H)vDIy z1^w|7@6l7E%O8+S6E6u|$l7GSSHpaHOJJn$>+QSz&4tQn+N{tD+NkJ0gD-*_j83ijn%XWi#4G(J&yLQLLE-fi-q`0PeaYlUTX?>0e-V5HUEjbC3fM3@7dfAw z95O+b7$hbpUH#_De%_pxlQhQU|8qDu*hcQ zH~4%n}&Vw&!mP&u5cpnccCr-Nc(PtSrnB z?^e|kAV)3EL(xPNJ1k`6=DvkesSeFt-Yqk;*8)%(F*fzJPz3Xj)R$su+(}goRil;Z9pADnMyNl4~v-i*D&!Vx#t%AFLa<7sQLH?>DjN_)qd; zA?1yt59zMKLVZtZm2kI8OhCOxV&@5jC*NfUb*b4y$!io1clqs?irI(zG+@2S*S#F# zRrc@*VUva$MA!mntOn7LsKC}KxH{0Ev1^%b>D@d8TN*9C&GzdhN@ElUOoP$l97=#s z+vHcI>NPBXR826osZQ0140aL7 r{RUl4#Lr(QG99RUCs+8Nw|2lR3E1$r&#A8Cu|G|76D#8?Lmc^kto!j2 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/media_repeat_all_light.png b/app/src/main/res/drawable-xxxhdpi/media_repeat_all_light.png new file mode 100644 index 0000000000000000000000000000000000000000..92dea8375291edd8910e1b9a0199c2c6cdd74c20 GIT binary patch literal 2475 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 F{{bjclfD1| literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/media_rewind_dark.png b/app/src/main/res/drawable-xxxhdpi/media_rewind_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..e9f8df4259bd140742db474cab1bd84b623bd4c8 GIT binary patch literal 1587 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K58911MRQ8&P5Flr2fKP}kko$iW46+b-;NB$% zjF|S4AirP+MkZz!RyKAHPA+a9UOs*SArUcg327Nw1!Yw=4NWawJ$*xCQ*%pe8(T*w zXID3O4{zVV;E?c$sF>JYh(Cy1_tJ1 zo-U3d6}R3TGz6umLkho_n;ou805Z`)N&0oU4RW1_6nOq`%e@UkZ_lu7eS2_wo7IZR zIt#vTd0)%iz58S~kH$PvtA8u{W{19$`kS2}SJG#{={%#JL&|5{6ox~uuh%tl2W;tAHes#`l3ah?yEnsrF`i9wv;pXT42U$I4mpia8_`>pl zDP;|#5yQ*9YaU^-o9`+(`m+g*yx6Rvz`3SgXcOP^7+VZkY8{v8ah z?0F{`SWIo1LK+raQg&cfSdsBaHl|@sr#eu0>mCN6@TOn~Hid=HSz{V*%wq?NhQ76Q zWaZF`DRW{|*!W#^0n?GMc0h;BeC=1j_~_W235*^2R*WkcF8W&gonT1Gw*m&mi~=St zhJQb+8N?Opk8(;jd|6lxRJ-dPqbS4wL(UD{0XyEc@Hn{K7Tv+{?`n7hmxIH%EzBVd z|KBvacQ8bJU>0E1`N0^%pz~uPQ^W^l=7fX%;s=`KMH#s3RxpVDIRRAS%(~%$J1bC7 ztAW+t2q+i;)H0EA!-ICNgoFKDK*128>J+FEig*DTAKFOR}Tjegk*hN8c``XrB3|vxjq|0sXC9UWEdyGs=tGGl%v)HDr z3R{!$A?oh?1&kVDVQbgM{Ou4e<$cg0YPn%TNm6pI{l<;&JegY!x>rApO8r9gp!VBvTZSQM}pEKo5tVEMhEs6bFwGE9P#1)O+kV|`4dc>Wzt89QdA@gP?phWX z6KT!jvk*eo(NW=YgfNI0vS7g1_Df0yEb`-4D+4H9kReE=Qt)8}TOiY#kB<*M0#29E z4lUCfn6v}IG=&UPS}+Be(q#4))0Ey$J0=LtrgBrJ?~3Mhp~<0}=_T#Y(dI1a3(YYB z(uAJEIlwvf1^oI2+47im2*Vj>W`sG@!ji>eTUp!Ka^~CFa~=4OP74+a7P$zQEET!B ziI=-eyre$9ANU8zf`UUr!y{HjMXz2HvsSK%U$0C|O5UK}n7T=mmbQ7z)@|t@@7S4< zxhpF>=aWx!_vPj1?=L7UI#7J@P)TXok)!3uj%zC_PgI?(K6U!cXEiCem> zHl9xj4!O1ADX(qNRrjv0zp?tx^v@s3hDV(rzQrV@rq^AA77r%`wG*PmJtbCTm!3%y zrQ6yV8q~ZjNKZta%>}BsM*zL>kkFvro4*74`4lyee8|xgok~oDdiL!E_HxCW9OM=Y ztrL7?Pc`+kGqKQg8y!O%{1+}E`^VXEDZT`ABb7Hf0*2QAcPk00yzQ+)9b&({I4o5B z#gj!0tWN_19HRfW7xB+Pc2tig->oXucki-Dn_zSjAXgrGiefXNeN z48T3gpSX}jRC*v&fr>6p4(5hV?|LV~!Rp%~*-oqwezLc~Z1uWS)vD z-o>m(xl@6NCn&fRVlK!Z=c=gI?~_SFFMh;qMF%f}$+^rk;JJM~ibb9Bp2FPVoOyiA zM%`|s$C?aGThwGIA=K^1af%3Dj@bL{(DKQRm~z|&fx^}oITHYth&y9}l;bz?CPO3! zkP9{runFw7#tL(&KmdiaD(e2L!2Yf1oLj*%RfvAEqS)H&&&Ln6dF#~<7PTURWosu3 zL7f=4R^%${kPG$}P?Pr_s}0i`Xec%6#MwP>mafDCx`#NE!eIRHv$XMLo!hlGi-Hcm zlY~wCSB!WPoDWm&BF^3&jNFqrlj(M(*FopIDywRub@1_BtZWTc{NCcMY8Pohw`JvC zW|&;Po2sm7*7q1*TdnTOA;Eobyr4WZ`@gUEofjc|NpSf%1FB%Iq>T-BaV)U3I8%wUGz8D^Y9V? literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/media_start_dark.png b/app/src/main/res/drawable-xxxhdpi/media_start_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..ac20fff5751873e7f0d80733d58f963d9ef67772 GIT binary patch literal 1255 zcmah}Yfuwc6yDvk6+=PGG$|;cNMtMt2$eU;Ll6x(v>+HlFl+%S3XLFwG~uzeP=wNg zI4BCfP#6Y~*A{u0;9A-aNJ9_^BqAlsBk~AOdFa%kH+I^;?vK4^&-u>xedpe}oAagr zAzjKg3IsvAti#@c5Cnq|hP1W7vMR*X5TsSh^7af$d8!^cUR@Myko8ql=0L8mX^z{L zTy0y5?HhT3=?%Kcg&^;kCyzeV$^GqCU}vE-t5VA)r>4HpgK_lp&xWr(h zqby-W>TV#FgwCg`TvSBVW+3yvXAgZ}E0-cc;vUmo2w!~(a%71Z{f-a&h_6g6#n3RD zWyPM0Jh&ps(ZRQzgR|15I%iJOumM%;A$tI>b=@*s=ubd*{J$1wUPBF5l52|s3Fs0t z%ImUk6&q(+sV>|F`6M?ftD1u^RxHHui$dz4e)sPcVP;J{*eC7grtv{0S?QoQIICmb zwd32v0>FFjBN4rdhB5V;JL@@kKzN8zGAM@eE=B>Z_J%~cB}v~H|EZ=R<{g(V$6N-u zjDK;Q1>TMps%e<%Xmt5mAyBp?{GvPUyC_UH4O0nL}|8mk@HcB%hjRf6O>7a6_=z0)3pFFgq;ayaJFaF>QU ziQk(8xYPqb@?lz?>nF9ce6VHuqOsWoU^o5oSX&>Ma4nJOvDLBWGe)W$z=%?l(K2^H zDsW@Z1b{6oOIWyRUM;NT;t7MDzX!>X;>3`>xKp;4wWvp@KztOFv*G`!E!Di*bt+0q(`zOR5Ahvdg>A##==-`C=sX zs;o`UAUmAFE1OfEaSti&5r}7~nElXHSTz*RSy-W1-!!hc&_t;(Fyq_~2LNfbc$%s_ znl??vtY#X6Zib@riHwJA{7x%7BNQ#}_7n6o$lK2<_5rMNu`xCKS6ij=@$Fk%~3SLWv*u8Y(wi;lciG_L!I_$cvWTY)J%!D*dF!Ff-DymozUk~m3bY*;HGt-QQW7*JH}2twIH+N zlGB@m@hqeG1g|>Cef;B-pkyQ5tzeM(Zc}kICC6};qP*M_0Z%U7eI-EUSL_;Nsmjdl z0wLN`Iz$AXx{efgur0~dWZ!Bwl(?RYT4&dV{uhLRph}bZH>d|lT-x>y6N2}*hSS8F jZYG(B?s(si0@Pz(g!dven_$?gMuTP@^!KhkfSvybXmuZI literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/media_start_light.png b/app/src/main/res/drawable-xxxhdpi/media_start_light.png new file mode 100644 index 0000000000000000000000000000000000000000..bddff574b118f137f9da7e89dc06264df999f1e2 GIT binary patch literal 1331 zcmah}Yfuwc6uwK+1h5IMRiWeoSixeULNy5ifq-CTsu0C06^a1`8H5x9hG$?~MwALd zahgORJhbJ}it-R8!oV_AkcNk`6cG~yg$XD@3i2k>o3=mt&z;%XbHDSQZ@+WS%+2EZ zeoitrHwFNZeD-@C1^|H}0u1r+vyj5m0)Suf@$xv5IHXmp`A5RcGDkhKEDv2UTUT!K zKq_Rti_KqpUA@r3J!x0K!wkKm25Xcy>asVRLMyq4-!G51+tsJM>qtM>CyFJ!O1a!y zz&In4k)in89>br8r_LO(Vx##C_Xdg2HsZ+81TAz+8**Q;T2^0}geR8PW;(Zd32@T2 zK=y*2zYiHpez2Y`-$vwp#TpjL9>ajNs=4AT0OFUyv=C2NTKB0%a)GTik(U#GDM9F1 zr2^Mut3Rt619p$ZZXw4Z!~9Oeg24w05^rOh=b~-7n1Y7sl<61cutqLY&@v{#n0buG z!5QtM;aax+PR1Jk^mS|>0jK2Ef><*e8kDi}y$^wCsff;TIj=FRl9Mr{QvxjD<~9Ed3k%;*evoV7^hj zl>f04+Eu?#XX5NvQ?CMcebwp)My0W`)iopiG>}(S> zj%@V*mp4mlvxW&W1fSzZiuM{v_ne%uaxo$EW{p=TC4(t&ysi(unHE%R>{;IF97C{X zWC`5x8-e6oJZ9>F$Bwn(u^vl<5upPzlkuLxQpwux*G$t?Ir8q=OWFoyxR)cW}{n!M0bwB zU3_8KbdgkQ-+VW>2Oe7mZ4wH`)i`Lfgvo_#2W zvK6w?d!$r*1&uYs}8MrYafv1aONJZS+Qww<+7&w>>{?$v#DKHvee|1fs;fsVHBPO)XU)M>gTe~DWM4fkOCrV literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/media_stop_light.png b/app/src/main/res/drawable-xxxhdpi/media_stop_light.png new file mode 100644 index 0000000000000000000000000000000000000000..fbb16bffdd81baaa12e820317af0152c59fd3106 GIT binary patch literal 137 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K585o&?RN5XZRUpL{;1l8sq>YS>mh1N(0J4}$ zg8YIRW~F&cbmpw(J9p!A zo>xx76 zbu&8*xc>i`I*;Slg^SZYXLT3|{`>gaWTUL3?%S*nxwCp*XK?&~{&me>i|E|VvoBn} zJLTjxvBaz&-n?HI>^1q%a{E^JeXo|856$n!zsYZ$$x(c0|GaO3CmdSd9Lm>rn)YzJ zh~n)J(H#>?f6VTlQ2S$c*8`5pcY z4>JE01Z&#u9a#Q9q9pvgTc;9$^*TQMRKkVh(f3I_m_;MioX25B!Kc_=_f%fdldSdoxVW{%; zhviyMdxMrM3%;6W729!P%enbGInKWRGqXD(@cH>LmeXqCZAv%Hb{dHs+;wv5OpZdU zJ;g2=OSk@+q3`|OxFT|w#+8fzB})ITREo*6cIDjmC49Z%tQcXXJukIRJ{5Nje35>{ zKke6I|FpX0H9|iG4(>T=D)i9kW_-O*%VZ1LxUVJC{(M?3n>f{e4k-Pg0FUnS;H15K T{@Td6fW$pr{an^LB{Ts51%xHx literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/notification_close_light.png b/app/src/main/res/drawable-xxxhdpi/notification_close_light.png new file mode 100644 index 0000000000000000000000000000000000000000..e40b9d83520eade9871ea04d9f126ecded3ba0e7 GIT binary patch literal 648 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H0wgodS2{2-F!_7BIEGZ*dOJJbTiQ{?-Ta!} z4EEna@r&}C6hsc6gci|LidYu$Hrd)+*fOE<<08oe0DzNJKvq^;&XfWjZeNe z*!=yS7%R|7RIuhm+Qm~w`Py|R8-DLy`@vM^*$m5f?U(Ou`}KCZc%p#ZZuUc(8At#A z*gfm#rpsY_{{FD6TP+sZTTy>S+w93U`MyJME1tdHwD{Zo{^uXxXUu$PdFNzjz~A5I zS9=!Eod0O5m*BC7^JR>b_O#R&X^13lwyE6gAt|VK$lmnx&TYaAMfnat-rZ^@mZ-bqPm z2KW9o9BPO4HJWaI=lBw(hiRLT){gn`{!}2I~4ECQvNXBlV$otfANF4 zKZIQiWdDf>*0kF@vb=v7uh;bdgL=mUn?EO&e(=`M;@Ee1|C4j}CoTm9uR8EnO55Q1 zE#ngx12&8N+bWQl_~W`mOJ(^#8D3NEhyUx}3g)l4$@%}tx{%p^`oomPCrwok z75`aT^xOH=+@ASozQo(jeECjmA4iYtg#4mj*8r)1nS!S_+sNMYYT^ERTYfG__TPsp fsPPRn literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/playing_dark.png b/app/src/main/res/drawable-xxxhdpi/playing_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..d2fae59df171560d7c706128f2b5247c7c9e449d GIT binary patch literal 808 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H0wgodS2{2-FunJ5aSW-r_4ZD*cSxW_!^68L z&y_fE1qDykSaD#J3fJN#O*$%@TDrJgR&X!fwNd31_f)=+1VIgsg&vuc7HTf_X+P!m<_A(q&IlnSxavX!B#G=N#igNt_xD}36`ub+1cNH@Tv=qt}IT$wx zEN{0e`pU#{s64LFBxUlFN=6P&ww_A)q4(ukdtG&PLjf4RZA+D||=nvZx2=9xw@ z{_*b-`((n*aY*B8&J`X7j%O z90!c~taqQSy2y}xWzy>pD<``$`dmDwcl`S#J*EfT;+yj-lRFtExnwRp!0r5*%|meU zbgi|sm!wH7v{bphSd2kn>11I=>mcLw2A01jR$m`k2Q+ZJmHBw+h75yHN#WeJ^Og%I zw5WY+|MB3=8U{lvi{oPYXU;Nx=!`y>Tgk=qz+8VtK;Z%_-UB?%F^c+M*q{Al6nL!p zdrssf9tnk`8tFHlJehXgfkAGyrQsa5gdQXR{`dB! fMoz;(k=NoArzL;9BpwzB%mECZu6{1-oD!M<-&q``26vy$Y>}q0BYT5+_(>85(&hJdw)r_tv2#USXA|tzp5(tEUoWHgg z_98GdbyoJ7b3XeYe0S%W*_rc<=NV&+F~%5Uj4{R-V~jDz7-P(qwUv0gR0r@YsVCjw z?N%FrYA{ao+|hJcZ2GC~Mgbt4 zqakWd?3Loi0U!{};c;+Qa{%ESkA>}At~LM!g70`2HYe;NX;T4&^WhZrTso#U00a?d zcm(W~Xbm6`&SQ@Is6uT32qMm5lCCuE(;8s)lf_d$t~r1}cs9INb4YOj2!sZLT4xW2 zI08UuK4`me)X5v{@dSWCcnB#64~F;xK=6Y*_>5y$2(bnb&STVX367`@06~Or)SGT_ zW>JhifIzU!V|0fqoLCeK52n1RB7+lpHKoIdF4Ho6^V21(-1S`A{v=!ZXVFv>UmV>)m14PJTG*zwi z5a1k7(@m|5V1#dYQIy~PP6Xf#fACwBUI=lXr-O%0CzM_a^8&x6(196~LImI(e+t`+ z{<|;O4UswS51SqMBV3qUVmwBzPCY!0GXO858}*J2TE=)=d^k-G4${OEfEVz1z4tm^hw8ONw*$=XK}-svwh3~BF^BcU+E>m=7*$^ z#kk+1bx3N3zTiK|w)jc8cHM-}CeH`T6;>vazMCx1Xo8 zi;IeJevqoLx=Be%y1KezaE5bpb6Q(mWo2aO=;(ZWe5k0XlarHEQ&WV5gq)n5(b3R< zikZa2!`Ii=YinwZm7}mM<+X!%>aENC^R4N_ z3}Z5%&nJ_KvFZu0l%~@aaZOsV*=)AiY%I%KE|+U+yWQH9qhwR`cumnumPPN+D8jyK zPp9~g>81P9wg0B>{H6O?SMuI;DXA-YRoqT@`O^J|b=BdOx|ELl()}lN=Z|!44Oi+~ z_qv8bxG&wuy3|cq8fjYINiwevM5Mdj#l@d>fq89BwRf~m+H}_9^yfdF)%$OwrCGHL~|4o=Rr^aaLPO>M0~pfZwTByVo?C}m=3%M@?ub3 z;+488Z|AGq5^n$K4yhZk^k4uR3V{QPX-T^+V>F?7Ryrw@I z7Is&_#0&^o24pPkUK~!hXEi9RS0-t7+>OK?UYDHQeQ5(1-D%DI!=CvNEET2e7Tjgw z!DlI#y1TM$S3t$mlGaJ6T*0C0$`=RKowmBYyacSK;#v|{8QlSO!LyX6EV$~%o(l+X zXN4q;nh8$A*rqI!x<25OUQKZAL0(a%9!qmoSH3v(YLLS#c^~U8CGM!BD{(~VdF4en zVqUsSS?bU@?uab7=o)8MUUj9Ovt!~;&x+`ZK+|mxV-By%s|qirt%@skhaTN1;AkPv zQrFG$yb9Jet1Bw**R}6-qmr8@J+4R8HLtq8yMcVwG~Ai~fq`%rCWh(EGWl)4)h%2% zMjOek{_scR$K`?3ZHAj(O@~FuLgk8_)mLwS6kTn2C9m|Q{+f0+x0V-O75BOr%%OCJ z>xMb4-s+-@knO5`FOaxinETx-fBOQx!x`Uc5Z^q%Ye!wIeIPK zF<-j^w0)b<_2G9f4%8dz_VKE|79DIJAA-+c)X$$kJ6#30M|bh>T*R9=I&c6}djpCm zg$2rr6OMLOBt>F_+#LWMl!I$~mO&=3phE)i+lw5->Asl@bBP|FiNCbpeF$Yt{351y(z=1@sh8de<-M`uDTZFURan?SM9mFjps2p!xMFB zc>8%Nu9~X45?9{~&(ehu=!=(Co}eeQ{dZF|TSNBV4(~%={?Xe{{s{LS{~Zx5Mu>Ek z-JtN!ztz=o&kZMyEq=dnB#C#kFBwz_}Bcpdy1VEcbz9pW+);I#x&W^y8$vF8Uj&Go()SDG?miaDWoi?# z$brNU1Jcc^Fc?{vc$MX#`8q_~u`0T@Vymo$adk_-3|n2NS)$I1Y=c<^fckT<)wHxDbp0{hEsIS|h zj=|6C>YQO;IE}%Z12&7x-VKA%bv0h;YpgE1UyF$v4ei(Iv?W@JYpS>->!LV|+OQjl z5Gby?#Z?!f2$L|{&zxpsM&UlkMb{r+SLId4m$Hw?uZ3~n^y_%qPSwGcxbNfZB5xYf zoksfw;(Oh^&XAoe@MZ%(=VA2>-5(0C9A9OAx0-G%e$!oOxR-v6tUIgIsoesLcJIcm zZbQ0cn79Iwdm!COTpxItKSlSRSMzOk&6QHC`gJ_6HQU?d{uom?DFMV8HWi&iKda%; zX-&UG7y}i@gwD8p9acb@(67VDx_@cs4ww=*!Z4065(Ps51Q4wNI0NHpzyfNd!<^s$ zV|AZ5*=!z@cD8Lg&`$o>($|||XZf)ckBL3u<>M>903K`IPMs57O{)l3YD4!V4R440 zNS$4+Jg@i)@E_|cdvFQ2HBcvM`S|#LI73~-+XJuhHN4}Gj}__Sda(NVZ3NpQ+}2=l z3SZn_o2YZt<$ZEQAY8?L+^}tm z+i0jN(zU$oi?r@@s(ZUiyWwks1Y6dvYUFv_mQHk&>qM~nuG^+7E^za@l`i|5&u%wQ z;qFQ6wi4{MzDgv&R((FW7Pr!6-IN!;7WiQ`jr%gXQ`_oi8|7^)UCwKeujvbjq}$8r ze)*Obwief`L`l~x^4(=6gSh8u+I`DLw%)0Aw~zYfFowMz-tNypmw5pkOrO(VIH%Hh zn*7f6!DrhHZd0AK^&I~XCZ2;fOdqYF#ZXcta8KG9lR;Qlq;kr}$>>fS5WKO5b=Z;(B@Ey}mDpe0-6 zAn>@0Nw-lb!*CfLO4aQuH7?d24F28^!M^>vGuQP{Ny|4^-MsHpireA<%e2VQI_WOw zF9H$G7q1w?AM>kisk(Z2DEI8nu0Hp2B92$HWfxO4bNQB8E9CViU$FfedA=m9fGxJQ zx;01L?pD8i=+>8TSeAX=9ha(G*Q?DRchJ?tkI-Egypl)2&7XJume=q(hRxg79Cg?F zyozopL&<0NuI#g0m(}#~y@;^e+44f`jvStNStDNnyGOLJy}WeM-Rkp-7BZBtJ638^ zzAldM%P}5)yd2$hFO9D|FxwKYi|%3r17wEsb<01y2s<}}t1kNj7&*WaEo|L%ukxM$ zl_8h2E_lh;^k=mBbL*(7hZsI>PJZxwGm9smK z*6ISfi{nA5$W8Z}d?D-}(ZcrHQCF!$w1BRBsyKVy`V0s67o~2FIJi&BS(kVLjND|2 z7PfA>8+p-_x6S-jhV5*zEc?n(@@KWrMYNVf94(IVqS0sHP1pDmu%j*Ey6Ntg6D1wC zPl?G}sLJUex;Lfk?#-TG&7NQP%2^k_49vvrO0r}N-BWhReOEhGq)Wgg%&{!f7VB2J z?8|A&B37b38{PFrqrT$;`F{F)6RgX^PBHE2b=Av$zYoQEzhA!g&r28m6I|A$ z-dc*-OrQ4c!*vrCIGdkhH*dIocjU3?BcrTkm(jfPGVCcq@(OJHHgUCrui_1RT;bd8 z*#k3DPl5SDBHVUXw-K+B=-Tbce=|fFpqk(slgk=?sOGcGyvXiFOWV87scwyQ?J_=2 zim+n;8Q|oNYNngU=$z={Y!-#}igbr-weBZh$iKwPy2*a5jVW_dK`Xjx-){}om^Y(q zhz%>k(8f`=>loggu3g9QifKmIZx{=+GkJ7X_E?vB8CUYhufGFn6ojC-s~a!?0000< KMNUMnLSTZ8bFWAM literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..8d0ba26b7eeb77c5929a57024aa98e061d0289be GIT binary patch literal 5772 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 KMNUMnLSTX~7<&K! literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..00b0798647afc8884b763044af8bb7d081274443 GIT binary patch literal 5986 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

    R;Bc&RV%S()9UE1;61xIoKbkt88z}^LeT*d%xLd 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 YY6q6264d+S4FdjD6g3s9 + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/card_rounded_corners_dark.xml b/app/src/main/res/drawable/card_rounded_corners_dark.xml new file mode 100644 index 0000000..4db7d4b --- /dev/null +++ b/app/src/main/res/drawable/card_rounded_corners_dark.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/card_rounded_corners_light.xml b/app/src/main/res/drawable/card_rounded_corners_light.xml new file mode 100644 index 0000000..5475c3d --- /dev/null +++ b/app/src/main/res/drawable/card_rounded_corners_light.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/fast_scroller_bubble.xml b/app/src/main/res/drawable/fast_scroller_bubble.xml new file mode 100644 index 0000000..02dfee5 --- /dev/null +++ b/app/src/main/res/drawable/fast_scroller_bubble.xml @@ -0,0 +1,16 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/fast_scroller_handle.xml b/app/src/main/res/drawable/fast_scroller_handle.xml new file mode 100644 index 0000000..e1744ce --- /dev/null +++ b/app/src/main/res/drawable/fast_scroller_handle.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/notification_backward.xml b/app/src/main/res/drawable/notification_backward.xml new file mode 100644 index 0000000..f5fd965 --- /dev/null +++ b/app/src/main/res/drawable/notification_backward.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/app/src/main/res/drawable/notification_close.xml b/app/src/main/res/drawable/notification_close.xml new file mode 100644 index 0000000..67a5696 --- /dev/null +++ b/app/src/main/res/drawable/notification_close.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/app/src/main/res/drawable/notification_divider.xml b/app/src/main/res/drawable/notification_divider.xml new file mode 100644 index 0000000..95d50aa --- /dev/null +++ b/app/src/main/res/drawable/notification_divider.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/notification_fastforward.xml b/app/src/main/res/drawable/notification_fastforward.xml new file mode 100644 index 0000000..355c6a5 --- /dev/null +++ b/app/src/main/res/drawable/notification_fastforward.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/app/src/main/res/drawable/notification_forward.xml b/app/src/main/res/drawable/notification_forward.xml new file mode 100644 index 0000000..5dd1000 --- /dev/null +++ b/app/src/main/res/drawable/notification_forward.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/app/src/main/res/drawable/notification_pause.xml b/app/src/main/res/drawable/notification_pause.xml new file mode 100644 index 0000000..c71a997 --- /dev/null +++ b/app/src/main/res/drawable/notification_pause.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/app/src/main/res/drawable/notification_rewind.xml b/app/src/main/res/drawable/notification_rewind.xml new file mode 100644 index 0000000..ab7827a --- /dev/null +++ b/app/src/main/res/drawable/notification_rewind.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/app/src/main/res/drawable/notification_start.xml b/app/src/main/res/drawable/notification_start.xml new file mode 100644 index 0000000..b31b4f8 --- /dev/null +++ b/app/src/main/res/drawable/notification_start.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file 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 0000000..db1660e --- /dev/null +++ b/app/src/main/res/layout-land/download.xml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 0000000..3901710 --- /dev/null +++ b/app/src/main/res/layout-large-land/abstract_fragment_container.xml @@ -0,0 +1,21 @@ + + + + + + + + \ 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 0000000..efe2ece --- /dev/null +++ b/app/src/main/res/layout-large-land/download.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 0000000..322266e --- /dev/null +++ b/app/src/main/res/layout-port/download.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 0000000..56db143 --- /dev/null +++ b/app/src/main/res/layout/abstract_activity.xml @@ -0,0 +1,22 @@ + + + + + + + + + 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 0000000..a34509f --- /dev/null +++ b/app/src/main/res/layout/abstract_fragment_activity.xml @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 0000000..f13356c --- /dev/null +++ b/app/src/main/res/layout/abstract_fragment_container.xml @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/abstract_recycler_fragment.xml b/app/src/main/res/layout/abstract_recycler_fragment.xml new file mode 100644 index 0000000..f4f6043 --- /dev/null +++ b/app/src/main/res/layout/abstract_recycler_fragment.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + \ 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 0000000..f719a67 --- /dev/null +++ b/app/src/main/res/layout/actionbar_spinner.xml @@ -0,0 +1,8 @@ + + \ No newline at end of file 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 0000000..ff04f06 --- /dev/null +++ b/app/src/main/res/layout/album_cell_item.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/album_list_header.xml b/app/src/main/res/layout/album_list_header.xml new file mode 100644 index 0000000..e78d0ac --- /dev/null +++ b/app/src/main/res/layout/album_list_header.xml @@ -0,0 +1,29 @@ + + + + + + + \ 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 0000000..5e4498f --- /dev/null +++ b/app/src/main/res/layout/album_list_item.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/appwidget4x1.xml b/app/src/main/res/layout/appwidget4x1.xml new file mode 100644 index 0000000..5f2536d --- /dev/null +++ b/app/src/main/res/layout/appwidget4x1.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 0000000..ae61353 --- /dev/null +++ b/app/src/main/res/layout/appwidget4x2.xml @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 0000000..0ffb9d4 --- /dev/null +++ b/app/src/main/res/layout/appwidget4x3.xml @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 0000000..d0668c2 --- /dev/null +++ b/app/src/main/res/layout/appwidget4x4.xml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/basic_art_item.xml b/app/src/main/res/layout/basic_art_item.xml new file mode 100644 index 0000000..d29b93e --- /dev/null +++ b/app/src/main/res/layout/basic_art_item.xml @@ -0,0 +1,34 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/basic_cell_item.xml b/app/src/main/res/layout/basic_cell_item.xml new file mode 100644 index 0000000..dcbb90e --- /dev/null +++ b/app/src/main/res/layout/basic_cell_item.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/basic_choice_item.xml b/app/src/main/res/layout/basic_choice_item.xml new file mode 100644 index 0000000..e2dc220 --- /dev/null +++ b/app/src/main/res/layout/basic_choice_item.xml @@ -0,0 +1,27 @@ + + + + + + + \ 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 0000000..ce1aa80 --- /dev/null +++ b/app/src/main/res/layout/basic_count_item.xml @@ -0,0 +1,37 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/basic_header.xml b/app/src/main/res/layout/basic_header.xml new file mode 100644 index 0000000..b1f94b3 --- /dev/null +++ b/app/src/main/res/layout/basic_header.xml @@ -0,0 +1,13 @@ + + \ 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 0000000..1e7db68 --- /dev/null +++ b/app/src/main/res/layout/basic_list_item.xml @@ -0,0 +1,28 @@ + + + + + + + diff --git a/app/src/main/res/layout/cache_location_buttons.xml b/app/src/main/res/layout/cache_location_buttons.xml new file mode 100644 index 0000000..31e1264 --- /dev/null +++ b/app/src/main/res/layout/cache_location_buttons.xml @@ -0,0 +1,19 @@ + + + +