aboutsummaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/build.gradle33
-rw-r--r--app/libs/CWAC-AdapterWrapper.jarbin4841 -> 0 bytes
-rw-r--r--app/libs/CWAC-EndlessAdapter.jarbin5317 -> 0 bytes
-rw-r--r--app/proguard.cfg1
-rw-r--r--app/src/main/AndroidManifest.xml18
-rw-r--r--app/src/main/java/github/daneren2005/dsub/activity/SubsonicActivity.java119
-rw-r--r--app/src/main/java/github/daneren2005/dsub/activity/SubsonicFragmentActivity.java195
-rw-r--r--app/src/main/java/github/daneren2005/dsub/activity/VoiceQueryReceiverActivity.java16
-rw-r--r--app/src/main/java/github/daneren2005/dsub/adapter/ArtistAdapter.java41
-rw-r--r--app/src/main/java/github/daneren2005/dsub/adapter/DownloadFileAdapter.java25
-rw-r--r--app/src/main/java/github/daneren2005/dsub/adapter/EntryGridAdapter.java24
-rw-r--r--app/src/main/java/github/daneren2005/dsub/adapter/EntryInfiniteGridAdapter.java9
-rw-r--r--app/src/main/java/github/daneren2005/dsub/adapter/ExpandableSectionAdapter.java150
-rw-r--r--app/src/main/java/github/daneren2005/dsub/adapter/InternetRadioStationAdapter.java56
-rw-r--r--app/src/main/java/github/daneren2005/dsub/adapter/MainAdapter.java5
-rw-r--r--app/src/main/java/github/daneren2005/dsub/adapter/PodcastChannelAdapter.java67
-rw-r--r--app/src/main/java/github/daneren2005/dsub/adapter/SearchAdapter.java35
-rw-r--r--app/src/main/java/github/daneren2005/dsub/adapter/SectionAdapter.java9
-rw-r--r--app/src/main/java/github/daneren2005/dsub/adapter/SettingsAdapter.java92
-rw-r--r--app/src/main/java/github/daneren2005/dsub/adapter/SimilarArtistAdapter.java53
-rw-r--r--app/src/main/java/github/daneren2005/dsub/domain/Artist.java32
-rw-r--r--app/src/main/java/github/daneren2005/dsub/domain/Indexes.java7
-rw-r--r--app/src/main/java/github/daneren2005/dsub/domain/InternetRadioStation.java43
-rw-r--r--app/src/main/java/github/daneren2005/dsub/domain/MusicDirectory.java59
-rw-r--r--app/src/main/java/github/daneren2005/dsub/domain/MusicFolder.java57
-rw-r--r--app/src/main/java/github/daneren2005/dsub/domain/SearchCritera.java94
-rw-r--r--app/src/main/java/github/daneren2005/dsub/domain/ServerInfo.java30
-rw-r--r--app/src/main/java/github/daneren2005/dsub/domain/User.java45
-rw-r--r--app/src/main/java/github/daneren2005/dsub/domain/Version.java236
-rw-r--r--app/src/main/java/github/daneren2005/dsub/fragments/AdminFragment.java2
-rw-r--r--app/src/main/java/github/daneren2005/dsub/fragments/ChatFragment.java8
-rw-r--r--app/src/main/java/github/daneren2005/dsub/fragments/DownloadFragment.java8
-rw-r--r--app/src/main/java/github/daneren2005/dsub/fragments/EqualizerFragment.java8
-rw-r--r--app/src/main/java/github/daneren2005/dsub/fragments/MainFragment.java136
-rw-r--r--app/src/main/java/github/daneren2005/dsub/fragments/NowPlayingFragment.java327
-rw-r--r--app/src/main/java/github/daneren2005/dsub/fragments/SearchFragment.java122
-rw-r--r--app/src/main/java/github/daneren2005/dsub/fragments/SelectArtistFragment.java119
-rw-r--r--app/src/main/java/github/daneren2005/dsub/fragments/SelectBookmarkFragment.java38
-rw-r--r--app/src/main/java/github/daneren2005/dsub/fragments/SelectDirectoryFragment.java232
-rw-r--r--app/src/main/java/github/daneren2005/dsub/fragments/SelectInternetRadioStationFragment.java157
-rw-r--r--app/src/main/java/github/daneren2005/dsub/fragments/SelectPlaylistFragment.java38
-rw-r--r--app/src/main/java/github/daneren2005/dsub/fragments/SelectPodcastsFragment.java42
-rw-r--r--app/src/main/java/github/daneren2005/dsub/fragments/SelectRecyclerFragment.java6
-rw-r--r--app/src/main/java/github/daneren2005/dsub/fragments/SelectShareFragment.java33
-rw-r--r--app/src/main/java/github/daneren2005/dsub/fragments/SettingsFragment.java47
-rw-r--r--app/src/main/java/github/daneren2005/dsub/fragments/SimilarArtistFragment.java78
-rw-r--r--app/src/main/java/github/daneren2005/dsub/fragments/SubsonicFragment.java197
-rw-r--r--app/src/main/java/github/daneren2005/dsub/fragments/UserFragment.java4
-rw-r--r--app/src/main/java/github/daneren2005/dsub/provider/DLNARouteProvider.java17
-rw-r--r--app/src/main/java/github/daneren2005/dsub/provider/DSubSearchProvider.java4
-rw-r--r--app/src/main/java/github/daneren2005/dsub/provider/DSubWidgetProvider.java2
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/AutoMediaBrowserService.java314
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/CachedMusicService.java107
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/ChromeCastController.java93
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/DLNAController.java59
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/DownloadFile.java66
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/DownloadService.java432
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/DownloadServiceLifecycleSupport.java41
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/JukeboxController.java7
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/MusicService.java12
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/OfflineMusicService.java49
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/RESTMusicService.java977
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/RemoteController.java75
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/Scrobbler.java16
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/parser/AbstractParser.java36
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/parser/EntryListParser.java (renamed from app/src/main/java/github/daneren2005/dsub/service/parser/AlbumListParser.java)11
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/parser/ErrorParser.java2
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/parser/InternetRadioStationParser.java63
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/parser/ScanStatusParser.java18
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/parser/TopSongsParser.java58
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/parser/UserParser.java45
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/ssl/SSLSocketFactory.java553
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/ssl/TrustManagerDecorator.java65
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/ssl/TrustSelfSignedStrategy.java44
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/ssl/TrustStrategy.java57
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/sync/MostRecentSyncAdapter.java2
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/sync/PlaylistSyncAdapter.java9
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/sync/PodcastSyncAdapter.java10
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/sync/StarredSyncAdapter.java3
-rw-r--r--app/src/main/java/github/daneren2005/dsub/service/sync/SubsonicSyncAdapter.java78
-rw-r--r--app/src/main/java/github/daneren2005/dsub/updates/Updater.java7
-rw-r--r--app/src/main/java/github/daneren2005/dsub/updates/Updater403.java58
-rw-r--r--app/src/main/java/github/daneren2005/dsub/updates/UpdaterNoDLNA.java41
-rw-r--r--app/src/main/java/github/daneren2005/dsub/updates/UpdaterSongPress.java42
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/ArtistRadioBuffer.java3
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/BackgroundTask.java35
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/Constants.java16
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/DrawableTint.java14
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/DroppySpeedControl.java82
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/EnvironmentVariables.java21
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/ImageLoader.java79
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/MediaRouteManager.java23
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/Notifications.java154
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/ProgressListener.java2
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/SettingsBackupAgent.java7
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/SongDBHandler.java41
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/ThemeUtil.java112
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/UpdateHelper.java86
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/UserUtil.java58
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/Util.java307
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/compat/CastCompat.java8
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientBase.java2
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientICS.java2
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientJB.java13
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientLP.java169
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/tags/ID3v2File.java4
-rw-r--r--app/src/main/java/github/daneren2005/dsub/view/AlbumView.java14
-rw-r--r--app/src/main/java/github/daneren2005/dsub/view/CacheLocationPreference.java146
-rw-r--r--app/src/main/java/github/daneren2005/dsub/view/CardView.java67
-rw-r--r--app/src/main/java/github/daneren2005/dsub/view/FastScroller.java2
-rw-r--r--app/src/main/java/github/daneren2005/dsub/view/GridSpacingDecoration.java51
-rw-r--r--app/src/main/java/github/daneren2005/dsub/view/InternetRadioStationView.java39
-rw-r--r--app/src/main/java/github/daneren2005/dsub/view/MyViewFlipper.java53
-rw-r--r--app/src/main/java/github/daneren2005/dsub/view/SettingView.java9
-rw-r--r--app/src/main/java/github/daneren2005/dsub/view/SongView.java89
-rw-r--r--app/src/main/java/github/daneren2005/dsub/view/UpdateView.java4
-rw-r--r--app/src/main/java/github/daneren2005/dsub/view/compat/CustomMediaRouteChooserDialogFragment.java16
-rw-r--r--app/src/main/java/github/daneren2005/dsub/view/compat/CustomMediaRouteControllerDialogFragment.java16
-rw-r--r--app/src/main/java/github/daneren2005/dsub/view/compat/CustomMediaRouteDialogFactory.java17
-rw-r--r--app/src/main/res/drawable-hdpi/actionbar_button_normal.9.pngbin208 -> 0 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_action_playback_speed_dark.pngbin0 -> 1008 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_action_playback_speed_light.pngbin0 -> 1229 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_toggle_played.pngbin0 -> 1064 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/media_fastforward_dark.pngbin0 -> 1123 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/media_fastforward_light.pngbin0 -> 1309 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/media_rewind_dark.pngbin0 -> 1172 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/media_rewind_light.pngbin0 -> 1286 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/stat_notify_download.pngbin300 -> 0 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/toast_frame.9.pngbin2461 -> 0 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_action_playback_speed_dark.pngbin0 -> 625 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_action_playback_speed_light.pngbin0 -> 739 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_toggle_played.pngbin0 -> 579 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/media_fastforward_dark.pngbin0 -> 654 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/media_fastforward_light.pngbin0 -> 727 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/media_rewind_dark.pngbin0 -> 660 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/media_rewind_light.pngbin0 -> 707 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/stat_notify_download.pngbin234 -> 0 bytes
-rw-r--r--app/src/main/res/drawable-v21/notification_fastforward.xml4
-rw-r--r--app/src/main/res/drawable-v21/notification_rewind.xml4
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_action_playback_speed_dark.pngbin0 -> 1429 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_action_playback_speed_light.pngbin0 -> 1689 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_toggle_played.pngbin0 -> 1341 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/media_fastforward_dark.pngbin0 -> 1622 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/media_fastforward_light.pngbin0 -> 1847 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/media_rewind_dark.pngbin0 -> 1542 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/media_rewind_light.pngbin0 -> 1697 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/stat_notify_download.pngbin379 -> 0 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_action_playback_speed_dark.pngbin0 -> 2377 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_action_playback_speed_light.pngbin0 -> 2793 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_toggle_played.pngbin0 -> 2552 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/media_fastforward_dark.pngbin0 -> 2837 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/media_fastforward_light.pngbin0 -> 3139 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/media_rewind_dark.pngbin0 -> 3007 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/media_rewind_light.pngbin0 -> 3243 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/stat_notify_download.pngbin531 -> 0 bytes
-rw-r--r--app/src/main/res/drawable-xxxhdpi/ic_action_playback_speed_dark.pngbin0 -> 3618 bytes
-rw-r--r--app/src/main/res/drawable-xxxhdpi/ic_action_playback_speed_light.pngbin0 -> 4334 bytes
-rw-r--r--app/src/main/res/drawable-xxxhdpi/ic_toggle_played.pngbin0 -> 3715 bytes
-rw-r--r--app/src/main/res/drawable-xxxhdpi/media_fastforward_dark.pngbin0 -> 4784 bytes
-rw-r--r--app/src/main/res/drawable-xxxhdpi/media_fastforward_light.pngbin0 -> 5304 bytes
-rw-r--r--app/src/main/res/drawable-xxxhdpi/media_rewind_dark.pngbin0 -> 4993 bytes
-rw-r--r--app/src/main/res/drawable-xxxhdpi/media_rewind_light.pngbin0 -> 5407 bytes
-rw-r--r--app/src/main/res/drawable/card_rounded_corners_black.xml6
-rw-r--r--app/src/main/res/drawable/card_rounded_corners_dark.xml6
-rw-r--r--app/src/main/res/drawable/card_rounded_corners_light.xml6
-rw-r--r--app/src/main/res/drawable/drawer_header.jpgbin35612 -> 0 bytes
-rw-r--r--app/src/main/res/drawable/drawer_header_dark.pngbin0 -> 39431 bytes
-rw-r--r--app/src/main/res/drawable/drawer_header_holo.pngbin0 -> 43226 bytes
-rw-r--r--app/src/main/res/drawable/drawer_header_light.pngbin0 -> 45565 bytes
-rw-r--r--app/src/main/res/drawable/notification_fastforward.xml4
-rw-r--r--app/src/main/res/drawable/notification_rewind.xml4
-rw-r--r--app/src/main/res/layout-land/download.xml243
-rw-r--r--app/src/main/res/layout-large-land/download.xml231
-rw-r--r--app/src/main/res/layout-port/download.xml145
-rw-r--r--app/src/main/res/layout/abstract_fragment_activity.xml20
-rw-r--r--app/src/main/res/layout/abstract_recycler_fragment.xml5
-rw-r--r--app/src/main/res/layout/actionbar_spinner.xml18
-rw-r--r--app/src/main/res/layout/album_cell_item.xml142
-rw-r--r--app/src/main/res/layout/basic_cell_item.xml64
-rw-r--r--app/src/main/res/layout/cache_location_buttons.xml19
-rw-r--r--app/src/main/res/layout/change_email.xml2
-rw-r--r--app/src/main/res/layout/change_password.xml25
-rw-r--r--app/src/main/res/layout/confirm_password.xml2
-rw-r--r--app/src/main/res/layout/create_bookmark.xml2
-rw-r--r--app/src/main/res/layout/create_podcast.xml2
-rw-r--r--app/src/main/res/layout/create_user.xml6
-rw-r--r--app/src/main/res/layout/download_media_buttons.xml16
-rw-r--r--app/src/main/res/layout/download_playlist.xml4
-rw-r--r--app/src/main/res/layout/drawer_header.xml2
-rw-r--r--app/src/main/res/layout/expandable_header.xml (renamed from app/src/main/res/layout/newest_episode_header.xml)0
-rw-r--r--app/src/main/res/layout/fast_scroller.xml4
-rw-r--r--app/src/main/res/layout/home.xml23
-rw-r--r--app/src/main/res/layout/notification.xml1
-rw-r--r--app/src/main/res/layout/notification_expanded.xml1
-rw-r--r--app/src/main/res/layout/progress.xml20
-rw-r--r--app/src/main/res/layout/seekbar_preference.xml2
-rw-r--r--app/src/main/res/layout/select_album_header.xml2
-rw-r--r--app/src/main/res/layout/set_playback_speed.xml66
-rw-r--r--app/src/main/res/layout/settings_activity.xml2
-rw-r--r--app/src/main/res/layout/shuffle_dialog.xml6
-rw-r--r--app/src/main/res/layout/song_list_item.xml41
-rw-r--r--app/src/main/res/layout/start_timer.xml2
-rw-r--r--app/src/main/res/layout/update_playlist.xml6
-rw-r--r--app/src/main/res/layout/update_share.xml6
-rw-r--r--app/src/main/res/menu/abstract_top_menu.xml3
-rw-r--r--app/src/main/res/menu/drawer_menu.xml14
-rw-r--r--app/src/main/res/menu/drawer_navigation.xml4
-rw-r--r--app/src/main/res/menu/main.xml7
-rw-r--r--app/src/main/res/menu/multiselect_media.xml4
-rw-r--r--app/src/main/res/menu/multiselect_nowplaying.xml27
-rw-r--r--app/src/main/res/menu/multiselect_nowplaying_offline.xml9
-rw-r--r--app/src/main/res/menu/nowplaying.xml5
-rw-r--r--app/src/main/res/menu/nowplaying_context.xml4
-rw-r--r--app/src/main/res/menu/nowplaying_context_offline.xml2
-rw-r--r--app/src/main/res/menu/nowplaying_offline.xml5
-rw-r--r--app/src/main/res/menu/search.xml3
-rw-r--r--app/src/main/res/menu/select_album_context.xml9
-rw-r--r--app/src/main/res/menu/select_album_context_offline.xml6
-rw-r--r--app/src/main/res/menu/select_artist.xml13
-rw-r--r--app/src/main/res/menu/select_internet_radio_context.xml7
-rw-r--r--app/src/main/res/menu/select_podcasts.xml5
-rw-r--r--app/src/main/res/menu/select_song_context.xml24
-rw-r--r--app/src/main/res/menu/select_song_context_offline.xml19
-rw-r--r--app/src/main/res/menu/select_video_context.xml9
-rw-r--r--app/src/main/res/menu/similar_artists.xml4
-rw-r--r--app/src/main/res/values-de/strings.xml71
-rw-r--r--app/src/main/res/values-es/strings.xml49
-rw-r--r--app/src/main/res/values-fr/strings.xml171
-rw-r--r--app/src/main/res/values-hu/strings.xml100
-rw-r--r--app/src/main/res/values-large/dimens.xml3
-rw-r--r--app/src/main/res/values-large/integers.xml1
-rw-r--r--app/src/main/res/values-nl/strings.xml636
-rw-r--r--app/src/main/res/values-ru/strings.xml18
-rw-r--r--app/src/main/res/values-sv/strings.xml48
-rw-r--r--app/src/main/res/values-v21/themes.xml6
-rw-r--r--app/src/main/res/values/arrays.xml34
-rw-r--r--app/src/main/res/values/attrs.xml9
-rw-r--r--app/src/main/res/values/dimens.xml7
-rw-r--r--app/src/main/res/values/integers.xml1
-rw-r--r--app/src/main/res/values/strings.xml99
-rw-r--r--app/src/main/res/values/styles.xml4
-rw-r--r--app/src/main/res/values/themes.xml32
-rw-r--r--app/src/main/res/xml/changelog.xml148
-rw-r--r--app/src/main/res/xml/settings.xml5
-rw-r--r--app/src/main/res/xml/settings_appearance.xml8
-rw-r--r--app/src/main/res/xml/settings_cache.xml2
-rw-r--r--app/src/main/res/xml/settings_cast.xml41
-rw-r--r--app/src/main/res/xml/settings_drawer.xml18
-rw-r--r--app/src/main/res/xml/settings_playback.xml27
249 files changed, 7483 insertions, 3789 deletions
diff --git a/app/build.gradle b/app/build.gradle
index 12cd09fa..26afb5ea 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,18 +1,30 @@
apply plugin: 'com.android.application'
android {
- compileSdkVersion 22
- buildToolsVersion "22.0.0"
+ compileSdkVersion 24
+ buildToolsVersion '25.0.0'
defaultConfig {
applicationId "github.daneren2005.dsub"
minSdkVersion 14
- targetSdkVersion 22
+ targetSdkVersion 23
+ versionCode 195
+ versionName '5.3.5'
+ setProperty("archivesBaseName", "DSub $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
}
}
@@ -34,16 +46,17 @@ android {
dependencies {
compile project(':Server Proxy')
compile fileTree(include: ['*.jar'], dir: 'libs')
- compile 'com.android.support:support-v4:22.2.+'
- compile 'com.android.support:appcompat-v7:22.2.+'
- compile 'com.android.support:mediarouter-v7:22.2.+'
- compile 'com.android.support:recyclerview-v7:22.2.+'
- compile 'com.android.support:design:22.2.+'
+ compile 'com.android.support:support-v4:24.2.+'
+ compile 'com.android.support:appcompat-v7:24.2.+'
+ compile 'com.android.support:mediarouter-v7:24.2.+'
+ compile 'com.android.support:recyclerview-v7:24.2.+'
+ compile 'com.android.support:design:24.2.+'
compile 'com.google.android.gms:play-services-cast:8.1.0'
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.0.1'
- compile group: 'org.fourthline.cling', name: 'cling-support', version:'2.0.1'
+ compile 'com.shehabic.droppy:Droppy:0.5.1@aar'
+ 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/CWAC-AdapterWrapper.jar b/app/libs/CWAC-AdapterWrapper.jar
deleted file mode 100644
index 692fe4d3..00000000
--- a/app/libs/CWAC-AdapterWrapper.jar
+++ /dev/null
Binary files differ
diff --git a/app/libs/CWAC-EndlessAdapter.jar b/app/libs/CWAC-EndlessAdapter.jar
deleted file mode 100644
index ec20d936..00000000
--- a/app/libs/CWAC-EndlessAdapter.jar
+++ /dev/null
Binary files differ
diff --git a/app/proguard.cfg b/app/proguard.cfg
index 8e1a0a0a..a18ae91a 100644
--- a/app/proguard.cfg
+++ b/app/proguard.cfg
@@ -44,6 +44,7 @@
}
-keep class android.support.v7.app.MediaRouteButton { *; }
+-keep class android.support.v7.widget.SearchView { *; }
-dontwarn android.support.**
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index be941740..37643a21 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,20 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="github.daneren2005.dsub"
- android:installLocation="internalOnly"
- android:versionCode="167"
- android:versionName="5.1.2">
-
+ android:installLocation="internalOnly">
+
<instrumentation android:name="android.test.InstrumentationTestRunner"
android:targetPackage="github.daneren2005.dsub"
android:label="Tests" />
<uses-permission android:name="android.permission.INTERNET"/>
- <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
+ <uses-permission android:name="android.permission.READ_PHONE_STATE" android:maxSdkVersion="22"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
- <uses-permission android:name="android.permission.RECORD_AUDIO"/>
+ <uses-permission android:name="android.permission.RECORD_AUDIO" android:maxSdkVersion="22"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.BLUETOOTH"/>
@@ -25,13 +23,15 @@
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
+ <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
<uses-feature android:name="android.hardware.bluetooth" android:required="false" />
<uses-feature android:name="android.hardware.microphone" android:required="false" />
<uses-feature android:name="android.hardware.wifi" android:required="false" />
-
- <uses-sdk android:minSdkVersion="14" android:targetSdkVersion="22"/>
+ <uses-feature android:name="android.software.leanback" android:required="false"/>
+ <uses-feature android:name="android.hardware.location" android:required="false"/>
+ <uses-feature android:name="android.hardware.location.network" android:required="false"/>
<supports-screens android:anyDensity="true" android:xlargeScreens="true" android:largeScreens="true" android:normalScreens="true" android:smallScreens="true"/>
@@ -44,7 +44,7 @@
<activity android:name="github.daneren2005.dsub.activity.SubsonicFragmentActivity"
android:configChanges="orientation|keyboardHidden"
- android:launchMode="standard">
+ android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
diff --git a/app/src/main/java/github/daneren2005/dsub/activity/SubsonicActivity.java b/app/src/main/java/github/daneren2005/dsub/activity/SubsonicActivity.java
index a58f169b..a1c5ceef 100644
--- a/app/src/main/java/github/daneren2005/dsub/activity/SubsonicActivity.java
+++ b/app/src/main/java/github/daneren2005/dsub/activity/SubsonicActivity.java
@@ -32,13 +32,15 @@ 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.Fragment;
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;
@@ -67,7 +69,9 @@ import java.util.List;
import github.daneren2005.dsub.R;
import github.daneren2005.dsub.domain.ServerInfo;
+import github.daneren2005.dsub.fragments.AdminFragment;
import github.daneren2005.dsub.fragments.SubsonicFragment;
+import github.daneren2005.dsub.fragments.UserFragment;
import github.daneren2005.dsub.service.DownloadService;
import github.daneren2005.dsub.service.HeadphoneListenerService;
import github.daneren2005.dsub.service.MusicService;
@@ -76,10 +80,13 @@ import github.daneren2005.dsub.util.Constants;
import github.daneren2005.dsub.util.DrawableTint;
import github.daneren2005.dsub.util.ImageLoader;
import github.daneren2005.dsub.util.SilentBackgroundTask;
+import github.daneren2005.dsub.util.ThemeUtil;
import github.daneren2005.dsub.util.Util;
import github.daneren2005.dsub.view.UpdateView;
import github.daneren2005.dsub.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;
@@ -88,6 +95,8 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte
protected static boolean actionbarColored;
private static final int MENU_GROUP_SERVER = 10;
private static final int MENU_ITEM_SERVER_BASE = 100;
+ public static final int PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE = 1;
+ public static final int PERMISSIONS_REQUEST_LOCATION = 2;
private final List<Runnable> afterServiceAvailable = new ArrayList<>();
private boolean drawerIdle = true;
@@ -116,6 +125,10 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte
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);
@@ -150,6 +163,9 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte
case Constants.PREFERENCES_KEY_BOOKMARKS_ENABLED:
setDrawerItemVisible(R.id.drawer_bookmarks, false);
break;
+ case Constants.PREFERENCES_KEY_INTERNET_RADIO_ENABLED:
+ setDrawerItemVisible(R.id.drawer_internet_radio_stations, false);
+ break;
case Constants.PREFERENCES_KEY_SHARED_ENABLED:
setDrawerItemVisible(R.id.drawer_shares, false);
break;
@@ -164,6 +180,25 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte
};
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
@@ -189,40 +224,48 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte
}
protected void createCustomActionBarView() {
- View customActionbar = getLayoutInflater().inflate(R.layout.actionbar_spinner, null);
- actionBarSpinner = (Spinner)customActionbar.findViewById(R.id.spinner);
- if(Util.getThemeRes(this) == R.style.Theme_DSub_Light_No_Actionbar) {
- actionBarSpinner.setBackgroundResource(R.drawable.abc_spinner_mtrl_am_alpha);
+ 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_DSub_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(customActionbar);
+ getSupportActionBar().setCustomView(actionBarSpinner);
}
@Override
- protected void onResume() {
- super.onResume();
+ protected void onStart() {
+ super.onStart();
Util.registerMediaButtonEventReceiver(this);
// Make sure to update theme
SharedPreferences prefs = Util.getPreferences(this);
- if (theme != null && !theme.equals(Util.getTheme(this)) || fullScreen != prefs.getBoolean(Constants.PREFERENCES_KEY_FULL_SCREEN, false) || actionbarColored != prefs.getBoolean(Constants.PREFERENCES_KEY_COLOR_ACTION_BAR, true)) {
+ 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();
+ DrawableTint.clearCache();
+ return;
}
- populateTabs();
getImageLoader().onUIVisible();
UpdateView.addActiveActivity();
}
@Override
- protected void onPause() {
- super.onPause();
+ protected void onResume() {
+ super.onResume();
+
+ // If this is in onStart is causes crashes when rotating screen in offline mode
+ // Actual root cause of error is `drawerItemSelected(newFragment);` in the offline mode branch of code
+ populateTabs();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
UpdateView.removeActiveActivity();
}
@@ -281,6 +324,9 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte
case R.id.drawer_bookmarks:
drawerItemSelected("Bookmark");
return true;
+ case R.id.drawer_internet_radio_stations:
+ drawerItemSelected("Internet Radio");
+ return true;
case R.id.drawer_shares:
drawerItemSelected("Share");
return true;
@@ -538,7 +584,7 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte
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--) {
+ for(int i = top; i > position && i >= 0; i--) {
removeCurrent();
}
}
@@ -556,6 +602,7 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte
SharedPreferences prefs = Util.getPreferences(this);
boolean podcastsEnabled = prefs.getBoolean(Constants.PREFERENCES_KEY_PODCASTS_ENABLED, true);
boolean bookmarksEnabled = prefs.getBoolean(Constants.PREFERENCES_KEY_BOOKMARKS_ENABLED, true) && !Util.isOffline(this) && ServerInfo.canBookmark(this);
+ boolean internetRadioEnabled = prefs.getBoolean(Constants.PREFERENCES_KEY_INTERNET_RADIO_ENABLED, true) && !Util.isOffline(this) && ServerInfo.canInternetRadio(this);
boolean sharedEnabled = prefs.getBoolean(Constants.PREFERENCES_KEY_SHARED_ENABLED, true) && !Util.isOffline(this);
boolean chatEnabled = prefs.getBoolean(Constants.PREFERENCES_KEY_CHAT_ENABLED, true) && !Util.isOffline(this);
boolean adminEnabled = prefs.getBoolean(Constants.PREFERENCES_KEY_ADMIN_ENABLED, true) && !Util.isOffline(this);
@@ -585,6 +632,9 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte
if(!bookmarksEnabled) {
setDrawerItemVisible(R.id.drawer_bookmarks, false);
}
+ if(!internetRadioEnabled) {
+ setDrawerItemVisible(R.id.drawer_internet_radio_stations, false);
+ }
if(!sharedEnabled) {
setDrawerItemVisible(R.id.drawer_shares, false);
}
@@ -752,6 +802,11 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte
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);
}
@@ -819,7 +874,11 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte
removeCurrent();
}
- currentFragment.invalidate();
+ if(currentFragment instanceof UserFragment || currentFragment instanceof AdminFragment) {
+ restart(false);
+ } else {
+ currentFragment.invalidate();
+ }
populateTabs();
}
@@ -853,6 +912,7 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte
spinnerAdapter.notifyDataSetChanged();
actionBarSpinner.setSelection(spinnerAdapter.getCount() - 1);
if(!isTv()) {
+ getSupportActionBar().setDisplayShowTitleEnabled(false);
getSupportActionBar().setDisplayShowCustomEnabled(true);
}
@@ -862,6 +922,7 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
} else if(!isTv()) {
+ getSupportActionBar().setDisplayShowTitleEnabled(true);
getSupportActionBar().setTitle(currentFragment.getTitle());
getSupportActionBar().setDisplayShowCustomEnabled(false);
drawerToggle.setDrawerIndicatorEnabled(true);
@@ -869,22 +930,31 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte
}
protected void restart() {
- Intent intent = new Intent(this, ((Object) this).getClass());
- intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ restart(true);
+ }
+ protected void restart(boolean resumePosition) {
+ Intent intent = new Intent(this, this.getClass());
intent.putExtras(getIntent());
- intent.putExtra(Constants.FRAGMENT_POSITION, lastSelectedPosition);
+ 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 = Util.getTheme(this);
+ theme = ThemeUtil.getTheme(this);
if(theme != null && theme.indexOf("fullscreen") != -1) {
theme = theme.substring(0, theme.indexOf("_fullscreen"));
- Util.setTheme(this, theme);
+ ThemeUtil.setTheme(this, theme);
}
- Util.applyTheme(this, theme);
+ ThemeUtil.applyTheme(this, theme);
actionbarColored = Util.getPreferences(this).getBoolean(Constants.PREFERENCES_KEY_COLOR_ACTION_BAR, true);
}
private void applyFullscreen() {
@@ -1044,6 +1114,7 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte
UserUtil.seedCurrentUser(this);
this.updateDrawerHeader();
+ drawer.closeDrawers();
}
private void showOfflineSyncDialog(final int scrobbleCount, final int starsCount) {
@@ -1125,6 +1196,10 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte
}
public int getDrawerItemId(String fragmentType) {
+ if(fragmentType == null) {
+ return R.id.drawer_home;
+ }
+
switch(fragmentType) {
case "Home":
return R.id.drawer_home;
@@ -1136,6 +1211,8 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte
return R.id.drawer_podcasts;
case "Bookmark":
return R.id.drawer_bookmarks;
+ case "Internet Radio":
+ return R.id.drawer_internet_radio_stations;
case "Share":
return R.id.drawer_shares;
case "Chat":
diff --git a/app/src/main/java/github/daneren2005/dsub/activity/SubsonicFragmentActivity.java b/app/src/main/java/github/daneren2005/dsub/activity/SubsonicFragmentActivity.java
index 87a67e80..803e6f72 100644
--- a/app/src/main/java/github/daneren2005/dsub/activity/SubsonicFragmentActivity.java
+++ b/app/src/main/java/github/daneren2005/dsub/activity/SubsonicFragmentActivity.java
@@ -28,8 +28,8 @@ 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.provider.MediaStore;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.AlertDialog;
@@ -37,8 +37,6 @@ 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;
@@ -48,9 +46,6 @@ 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.daneren2005.dsub.R;
import github.daneren2005.dsub.domain.MusicDirectory;
@@ -66,6 +61,7 @@ import github.daneren2005.dsub.fragments.SearchFragment;
import github.daneren2005.dsub.fragments.SelectArtistFragment;
import github.daneren2005.dsub.fragments.SelectBookmarkFragment;
import github.daneren2005.dsub.fragments.SelectDirectoryFragment;
+import github.daneren2005.dsub.fragments.SelectInternetRadioStationFragment;
import github.daneren2005.dsub.fragments.SelectPlaylistFragment;
import github.daneren2005.dsub.fragments.SelectPodcastsFragment;
import github.daneren2005.dsub.fragments.SelectShareFragment;
@@ -76,6 +72,7 @@ import github.daneren2005.dsub.service.MusicService;
import github.daneren2005.dsub.service.MusicServiceFactory;
import github.daneren2005.dsub.updates.Updater;
import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.DrawableTint;
import github.daneren2005.dsub.util.FileUtil;
import github.daneren2005.dsub.util.SilentBackgroundTask;
import github.daneren2005.dsub.util.UserUtil;
@@ -93,6 +90,7 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo
private SlidingUpPanelLayout slideUpPanel;
private SlidingUpPanelLayout.PanelSlideListener panelSlideListener;
+ private boolean isPanelClosing = false;
private NowPlayingFragment nowPlayingFragment;
private SubsonicFragment secondaryFragment;
private Toolbar mainToolbar;
@@ -106,6 +104,10 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo
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) {
@@ -132,6 +134,7 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo
stopService(new Intent(this, DownloadService.class));
finish();
getImageLoader().clearCache();
+ DrawableTint.clearCache();
} else if(getIntent().hasExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD_VIEW)) {
getIntent().putExtra(Constants.INTENT_EXTRA_FRAGMENT_TYPE, "Download");
lastSelectedPosition = R.id.drawer_downloading;
@@ -153,8 +156,19 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo
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();
@@ -187,24 +201,34 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo
@Override
public void onPanelCollapsed(View panel) {
- bottomBar.setVisibility(View.VISIBLE);
- nowPlayingToolbar.setVisibility(View.GONE);
- nowPlayingFragment.setPrimaryFragment(false);
- setSupportActionBar(mainToolbar);
- recreateSpinner();
+ 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);
- nowPlayingFragment.setPrimaryFragment(true);
+
+ if(secondaryFragment == null) {
+ nowPlayingFragment.setPrimaryFragment(true);
+ } else {
+ secondaryFragment.setPrimaryFragment(true);
+ }
drawerToggle.setDrawerIndicatorEnabled(false);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
@@ -230,6 +254,8 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo
openNowPlaying();
}
}, 200);
+
+ getIntent().removeExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD);
}
bottomBar = findViewById(R.id.bottom_bar);
@@ -248,7 +274,25 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo
trans.commit();
}
- ImageButton previousButton = (ImageButton) findViewById(R.id.download_previous);
+ rewindButton = (ImageButton) findViewById(R.id.download_rewind);
+ rewindButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ new SilentBackgroundTask<Void>(SubsonicFragmentActivity.this) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ if (getDownloadService() == null) {
+ return null;
+ }
+
+ getDownloadService().rewind();
+ return null;
+ }
+ }.execute();
+ }
+ });
+
+ previousButton = (ImageButton) findViewById(R.id.download_previous);
previousButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
@@ -276,6 +320,8 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo
PlayerState state = getDownloadService().getPlayerState();
if(state == PlayerState.STARTED) {
getDownloadService().pause();
+ } else if(state == PlayerState.IDLE) {
+ getDownloadService().play();
} else {
getDownloadService().start();
}
@@ -286,7 +332,7 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo
}
});
- ImageButton nextButton = (ImageButton) findViewById(R.id.download_next);
+ nextButton = (ImageButton) findViewById(R.id.download_next);
nextButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
@@ -303,6 +349,24 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo
}.execute();
}
});
+
+ fastforwardButton = (ImageButton) findViewById(R.id.download_fastforward);
+ fastforwardButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ new SilentBackgroundTask<Void>(SubsonicFragmentActivity.this) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ if (getDownloadService() == null) {
+ return null;
+ }
+
+ getDownloadService().fastForward();
+ return null;
+ }
+ }.execute();
+ }
+ });
}
@Override
@@ -330,17 +394,19 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo
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);
- boolean requestsearch = intent.getBooleanExtra(Constants.INTENT_EXTRA_REQUEST_SEARCH, false);
+ String artist = intent.getStringExtra(MediaStore.EXTRA_MEDIA_ARTIST);
+ String album = intent.getStringExtra(MediaStore.EXTRA_MEDIA_ALBUM);
+ String title = intent.getStringExtra(MediaStore.EXTRA_MEDIA_TITLE);
if (query != null) {
- ((SearchFragment)currentFragment).search(query, autoplay);
- } else {
- if (requestsearch) {
- onSearchRequested();
- }
+ ((SearchFragment)currentFragment).search(query, autoplay, artist, album, title);
}
getIntent().removeExtra(Constants.INTENT_EXTRA_NAME_QUERY);
} else {
@@ -349,7 +415,14 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo
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) {
@@ -402,6 +475,9 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo
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
@@ -411,6 +487,19 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo
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);
}
@@ -472,7 +561,7 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo
@Override
public void replaceFragment(SubsonicFragment fragment, int tag, boolean replaceCurrent) {
- if(slideUpPanel != null && slideUpPanel.getPanelState() == SlidingUpPanelLayout.PanelState.EXPANDED) {
+ if(slideUpPanel != null && slideUpPanel.getPanelState() == SlidingUpPanelLayout.PanelState.EXPANDED && !isPanelClosing) {
secondaryFragment = fragment;
nowPlayingFragment.setPrimaryFragment(false);
secondaryFragment.setPrimaryFragment(true);
@@ -562,6 +651,7 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo
@Override
public void closeNowPlaying() {
slideUpPanel.setPanelState(SlidingUpPanelLayout.PanelState.COLLAPSED);
+ isPanelClosing = true;
}
private SubsonicFragment getNewFragment(String fragmentType) {
@@ -575,6 +665,8 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo
return new SelectPodcastsFragment();
} else if("Bookmark".equals(fragmentType)) {
return new SelectBookmarkFragment();
+ } else if("Internet Radio".equals(fragmentType)) {
+ return new SelectInternetRadioStationFragment();
} else if("Share".equals(fragmentType)) {
return new SelectShareFragment();
} else if("Admin".equals(fragmentType)) {
@@ -637,7 +729,7 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo
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_USERNAME + 1, "guest2");
editor.putString(Constants.PREFERENCES_KEY_PASSWORD + 1, "guest");
editor.putInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1);
editor.commit();
@@ -828,14 +920,20 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo
}
@Override
- public void onSongChanged(DownloadFile currentPlaying, int currentPlayingIndex) {
+ public void onSongChanged(DownloadFile currentPlaying, int currentPlayingIndex, boolean shouldFastForward) {
this.currentPlaying = currentPlaying;
MusicDirectory.Entry song = null;
if (currentPlaying != null) {
song = currentPlaying.getSong();
trackView.setText(song.getTitle());
- artistView.setText(song.getArtist());
+
+ 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);
@@ -851,13 +949,40 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo
}
getImageLoader().loadImage(coverArtView, song, false, height, false);
}
+
+ updateMediaButtons(shouldFastForward);
+ }
+
+ private void updateMediaButtons(boolean shouldFastForward) {
+ DownloadService downloadService = getDownloadService();
+ if(downloadService.isCurrentPlayingSingle()) {
+ previousButton.setVisibility(View.GONE);
+ nextButton.setVisibility(View.GONE);
+ rewindButton.setVisibility(View.GONE);
+ fastforwardButton.setVisibility(View.GONE);
+ } else {
+ if (shouldFastForward) {
+ 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);
+ }
+ }
}
@Override
- public void onSongsChanged(List<DownloadFile> songs, DownloadFile currentPlaying, int currentPlayingIndex) {
+ public void onSongsChanged(List<DownloadFile> songs, DownloadFile currentPlaying, int currentPlayingIndex, boolean shouldFastForward) {
if(this.currentPlaying != currentPlaying || this.currentPlaying == null) {
- onSongChanged(currentPlaying, currentPlayingIndex);
- onMetadataUpdate(currentPlaying != null ? currentPlaying.getSong() : null, DownloadService.METADATA_UPDATED_ALL);
+ onSongChanged(currentPlaying, currentPlayingIndex, shouldFastForward);
+ } else {
+ updateMediaButtons(shouldFastForward);
}
}
@@ -875,7 +1000,21 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo
}
@Override
- public void onMetadataUpdate(MusicDirectory.Entry entry, int fieldChange) {
+ 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/daneren2005/dsub/activity/VoiceQueryReceiverActivity.java b/app/src/main/java/github/daneren2005/dsub/activity/VoiceQueryReceiverActivity.java
index c0effe27..641b118f 100644
--- a/app/src/main/java/github/daneren2005/dsub/activity/VoiceQueryReceiverActivity.java
+++ b/app/src/main/java/github/daneren2005/dsub/activity/VoiceQueryReceiverActivity.java
@@ -55,6 +55,22 @@ public class VoiceQueryReceiverActivity extends Activity {
if(!GMS_SEARCH_ACTION.equals(getIntent().getAction())) {
intent.putExtra(Constants.INTENT_EXTRA_NAME_AUTOPLAY, true);
}
+
+ String artist = getIntent().getStringExtra(MediaStore.EXTRA_MEDIA_ARTIST);
+ if(artist != null) {
+ intent.putExtra(MediaStore.EXTRA_MEDIA_ARTIST, artist);
+ }
+
+ String album = getIntent().getStringExtra(MediaStore.EXTRA_MEDIA_ALBUM);
+ if(album != null) {
+ intent.putExtra(MediaStore.EXTRA_MEDIA_ALBUM, album);
+ }
+
+ String title = getIntent().getStringExtra(MediaStore.EXTRA_MEDIA_TITLE);
+ if(title != null) {
+ intent.putExtra(MediaStore.EXTRA_MEDIA_TITLE, title);
+ }
+
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);
diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/ArtistAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/ArtistAdapter.java
index 5feaa482..5ed79e82 100644
--- a/app/src/main/java/github/daneren2005/dsub/adapter/ArtistAdapter.java
+++ b/app/src/main/java/github/daneren2005/dsub/adapter/ArtistAdapter.java
@@ -23,27 +23,32 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
+import java.io.Serializable;
import java.util.List;
import github.daneren2005.dsub.R;
import github.daneren2005.dsub.domain.Artist;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.MusicDirectory.Entry;
import github.daneren2005.dsub.domain.MusicFolder;
import github.daneren2005.dsub.util.Util;
import github.daneren2005.dsub.view.ArtistView;
import github.daneren2005.dsub.view.FastScroller;
+import github.daneren2005.dsub.view.SongView;
import github.daneren2005.dsub.view.UpdateView;
-public class ArtistAdapter extends SectionAdapter<Artist> implements FastScroller.BubbleTextGetter {
+public class ArtistAdapter extends SectionAdapter<Serializable> implements FastScroller.BubbleTextGetter {
+ public static int VIEW_TYPE_SONG = 3;
public static int VIEW_TYPE_ARTIST = 4;
private List<MusicFolder> musicFolders;
private OnMusicFolderChanged onMusicFolderChanged;
- public ArtistAdapter(Context context, List<Artist> artists, OnItemClickedListener listener) {
+ public ArtistAdapter(Context context, List<Serializable> artists, OnItemClickedListener listener) {
this(context, artists, null, listener, null);
}
- public ArtistAdapter(Context context, List<Artist> artists, List<MusicFolder> musicFolders, OnItemClickedListener onItemClickedListener, OnMusicFolderChanged onMusicFolderChanged) {
+ public ArtistAdapter(Context context, List<Serializable> artists, List<MusicFolder> musicFolders, OnItemClickedListener onItemClickedListener, OnMusicFolderChanged onMusicFolderChanged) {
super(context, artists);
this.musicFolders = musicFolders;
this.onItemClickedListener = onItemClickedListener;
@@ -92,7 +97,7 @@ public class ArtistAdapter extends SectionAdapter<Artist> implements FastScrolle
return new UpdateView.UpdateViewHolder(header, false);
}
@Override
- public void onBindHeaderHolder(UpdateView.UpdateViewHolder holder, String header) {
+ 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);
@@ -110,17 +115,35 @@ public class ArtistAdapter extends SectionAdapter<Artist> implements FastScrolle
@Override
public UpdateView.UpdateViewHolder onCreateSectionViewHolder(ViewGroup parent, int viewType) {
- return new UpdateView.UpdateViewHolder(new ArtistView(context));
+ 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, Artist item, int viewType) {
- holder.getUpdateView().setObject(item);
+ 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 && !entry.isVideo());
+ }
}
@Override
- public int getItemViewType(Artist item) {
- return VIEW_TYPE_ARTIST;
+ public int getItemViewType(Serializable item) {
+ if(item instanceof Artist) {
+ return VIEW_TYPE_ARTIST;
+ } else {
+ return VIEW_TYPE_SONG;
+ }
}
@Override
diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/DownloadFileAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/DownloadFileAdapter.java
index d4613994..5b3dc289 100644
--- a/app/src/main/java/github/daneren2005/dsub/adapter/DownloadFileAdapter.java
+++ b/app/src/main/java/github/daneren2005/dsub/adapter/DownloadFileAdapter.java
@@ -16,13 +16,18 @@
package github.daneren2005.dsub.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.daneren2005.dsub.R;
import github.daneren2005.dsub.service.DownloadFile;
+import github.daneren2005.dsub.util.Util;
import github.daneren2005.dsub.view.FastScroller;
import github.daneren2005.dsub.view.SongView;
import github.daneren2005.dsub.view.UpdateView;
@@ -33,6 +38,7 @@ public class DownloadFileAdapter extends SectionAdapter<DownloadFile> implements
public DownloadFileAdapter(Context context, List<DownloadFile> entries, OnItemClickedListener onItemClickedListener) {
super(context, entries);
this.onItemClickedListener = onItemClickedListener;
+ this.checkable = true;
}
@Override
@@ -43,7 +49,7 @@ public class DownloadFileAdapter extends SectionAdapter<DownloadFile> implements
@Override
public void onBindViewHolder(UpdateView.UpdateViewHolder holder, DownloadFile item, int viewType) {
SongView songView = (SongView) holder.getUpdateView();
- songView.setObject(item.getSong(), false);
+ songView.setObject(item.getSong(), Util.isBatchMode(context));
songView.setDownloadFile(item);
}
@@ -56,4 +62,21 @@ public class DownloadFileAdapter extends SectionAdapter<DownloadFile> implements
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);
+ }
+
+ if(!selected.isEmpty()) {
+ MenuItem starItem = menu.findItem(R.id.menu_star);
+ if(starItem != null) {
+ boolean isStarred = selected.get(0).getSong().isStarred();
+ starItem.setTitle(isStarred ? R.string.common_unstar : R.string.common_star);
+ }
+ }
+ }
}
diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/EntryGridAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/EntryGridAdapter.java
index 38931482..e75a5104 100644
--- a/app/src/main/java/github/daneren2005/dsub/adapter/EntryGridAdapter.java
+++ b/app/src/main/java/github/daneren2005/dsub/adapter/EntryGridAdapter.java
@@ -16,13 +16,12 @@
package github.daneren2005.dsub.adapter;
import android.content.Context;
-import android.util.Log;
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 github.daneren2005.dsub.R;
@@ -45,8 +44,8 @@ public class EntryGridAdapter extends SectionAdapter<Entry> {
private ImageLoader imageLoader;
private boolean largeAlbums;
private boolean showArtist = false;
+ private boolean showAlbum = false;
private boolean removeFromPlaylist = false;
- private boolean removeStarred = true;
private View header;
public EntryGridAdapter(Context context, List<Entry> entries, ImageLoader imageLoader, boolean largeCell) {
@@ -89,6 +88,7 @@ public class EntryGridAdapter extends SectionAdapter<Entry> {
albumView.setObject(entry, imageLoader);
} else if(viewType == VIEW_TYPE_SONG) {
SongView songView = (SongView) view;
+ songView.setShowAlbum(showAlbum);
songView.setObject(entry, checkable && !entry.isVideo());
}
}
@@ -96,7 +96,7 @@ public class EntryGridAdapter extends SectionAdapter<Entry> {
public UpdateViewHolder onCreateHeaderHolder(ViewGroup parent) {
return new UpdateViewHolder(header, false);
}
- public void onBindHeaderHolder(UpdateViewHolder holder, String header) {
+ public void onBindHeaderHolder(UpdateViewHolder holder, String header, int sectionIndex) {
}
@@ -125,6 +125,10 @@ public class EntryGridAdapter extends SectionAdapter<Entry> {
this.showArtist = showArtist;
}
+ public void setShowAlbum(boolean showAlbum) {
+ this.showAlbum = showAlbum;
+ }
+
public void removeAt(int index) {
sections.get(0).remove(index);
if(header != null) {
@@ -136,9 +140,6 @@ public class EntryGridAdapter extends SectionAdapter<Entry> {
public void setRemoveFromPlaylist(boolean removeFromPlaylist) {
this.removeFromPlaylist = removeFromPlaylist;
}
- public void setRemoveStarred(boolean removeStarred) {
- this.removeStarred = removeStarred;
- }
@Override
public void onCreateActionModeMenu(Menu menu, MenuInflater menuInflater) {
@@ -151,8 +152,13 @@ public class EntryGridAdapter extends SectionAdapter<Entry> {
if(!removeFromPlaylist) {
menu.removeItem(R.id.menu_remove_playlist);
}
- if(removeStarred) {
- menu.removeItem(R.id.menu_unstar);
+
+ if(!selected.isEmpty()) {
+ MenuItem starItem = menu.findItem(R.id.menu_star);
+ if(starItem != null) {
+ boolean isStarred = selected.get(0).isStarred();
+ starItem.setTitle(isStarred ? R.string.common_unstar : R.string.common_star);
+ }
}
}
}
diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/EntryInfiniteGridAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/EntryInfiniteGridAdapter.java
index 2c4f75dc..6c1c14da 100644
--- a/app/src/main/java/github/daneren2005/dsub/adapter/EntryInfiniteGridAdapter.java
+++ b/app/src/main/java/github/daneren2005/dsub/adapter/EntryInfiniteGridAdapter.java
@@ -26,6 +26,7 @@ import github.daneren2005.dsub.R;
import github.daneren2005.dsub.domain.MusicDirectory;
import github.daneren2005.dsub.domain.MusicDirectory.Entry;
import github.daneren2005.dsub.domain.ServerInfo;
+import github.daneren2005.dsub.fragments.MainFragment;
import github.daneren2005.dsub.service.MusicService;
import github.daneren2005.dsub.service.MusicServiceFactory;
import github.daneren2005.dsub.util.ImageLoader;
@@ -88,6 +89,10 @@ public class EntryInfiniteGridAdapter extends EntryGridAdapter {
this.type = type;
this.extra = extra;
this.size = size;
+
+ if(super.getItemCount() < size) {
+ allLoaded = true;
+ }
}
public void loadMore() {
@@ -110,7 +115,7 @@ public class EntryInfiniteGridAdapter extends EntryGridAdapter {
appendCachedData(newData);
loading = false;
- if(newData.isEmpty()) {
+ if(newData.size() < size) {
allLoaded = true;
notifyDataSetChanged();
}
@@ -126,6 +131,8 @@ public class EntryInfiniteGridAdapter extends EntryGridAdapter {
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);
}
diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/ExpandableSectionAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/ExpandableSectionAdapter.java
new file mode 100644
index 00000000..6ebb34e3
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/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 <http://www.gnu.org/licenses/>.
+ Copyright 2015 (C) Scott Jackson
+*/
+
+package github.daneren2005.dsub.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.daneren2005.dsub.R;
+import github.daneren2005.dsub.util.DrawableTint;
+import github.daneren2005.dsub.view.BasicHeaderView;
+import github.daneren2005.dsub.view.UpdateView;
+
+public abstract class ExpandableSectionAdapter<T> extends SectionAdapter<T> {
+ 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<Integer> sectionsDefaultVisible;
+ protected List<List<T>> sectionsExtras;
+ protected int expandToggleRes;
+ protected int collapseToggleRes;
+
+ protected ExpandableSectionAdapter() {}
+ public ExpandableSectionAdapter(Context context, List<T> section) {
+ List<List<T>> sections = new ArrayList<>();
+ sections.add(section);
+
+ init(context, Arrays.asList("Section"), sections, Arrays.asList((Integer) null));
+ }
+ public ExpandableSectionAdapter(Context context, List<String> headers, List<List<T>> sections) {
+ init(context, headers, sections, null);
+ }
+ public ExpandableSectionAdapter(Context context, List<String> headers, List<List<T>> sections, List<Integer> sectionsDefaultVisible) {
+ init(context, headers, sections, sectionsDefaultVisible);
+ }
+ protected void init(Context context, List<String> headers, List<List<T>> fullSections, List<Integer> 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<T> fullSection: fullSections) {
+ List<T> 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<T> visibleSelection = sections.get(sectionIndex);
+ List<T> 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<T> visibleSelection = sections.get(sectionIndex);
+ List<T> 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/daneren2005/dsub/adapter/InternetRadioStationAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/InternetRadioStationAdapter.java
new file mode 100644
index 00000000..9d47d70c
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/adapter/InternetRadioStationAdapter.java
@@ -0,0 +1,56 @@
+/*
+ This file is part of Subsonic.
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+ Copyright 2016 (C) Scott Jackson
+*/
+package github.daneren2005.dsub.adapter;
+
+import android.content.Context;
+import android.view.ViewGroup;
+
+import java.util.List;
+
+import github.daneren2005.dsub.domain.InternetRadioStation;
+import github.daneren2005.dsub.view.FastScroller;
+import github.daneren2005.dsub.view.InternetRadioStationView;
+import github.daneren2005.dsub.view.UpdateView;
+
+public class InternetRadioStationAdapter extends SectionAdapter<InternetRadioStation> implements FastScroller.BubbleTextGetter {
+ public static int VIEW_TYPE_INTERNET_RADIO_STATION = 1;
+
+ public InternetRadioStationAdapter(Context context, List<InternetRadioStation> stations, OnItemClickedListener listener) {
+ super(context, stations);
+ this.onItemClickedListener = listener;
+ }
+
+ @Override
+ public UpdateView.UpdateViewHolder onCreateSectionViewHolder(ViewGroup parent, int viewType) {
+ return new UpdateView.UpdateViewHolder(new InternetRadioStationView(context));
+ }
+
+ @Override
+ public void onBindViewHolder(UpdateView.UpdateViewHolder holder, InternetRadioStation station, int viewType) {
+ holder.getUpdateView().setObject(station);
+ holder.setItem(station);
+ }
+
+ @Override
+ public int getItemViewType(InternetRadioStation station) {
+ return VIEW_TYPE_INTERNET_RADIO_STATION;
+ }
+
+ @Override
+ public String getTextToShowInBubble(int position) {
+ InternetRadioStation item = getItemForPosition(position);
+ return item.getTitle();
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/MainAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/MainAdapter.java
index 8f1f1c38..dd70aa99 100644
--- a/app/src/main/java/github/daneren2005/dsub/adapter/MainAdapter.java
+++ b/app/src/main/java/github/daneren2005/dsub/adapter/MainAdapter.java
@@ -77,7 +77,7 @@ public class MainAdapter extends SectionAdapter<Integer> {
return new UpdateView.UpdateViewHolder(new BasicHeaderView(context, R.layout.album_list_header));
}
@Override
- public void onBindHeaderHolder(UpdateView.UpdateViewHolder holder, String header) {
+ public void onBindHeaderHolder(UpdateView.UpdateViewHolder holder, String header, int sectionIndex) {
UpdateView view = holder.getUpdateView();
CheckBox checkBox = (CheckBox) view.findViewById(R.id.item_checkbox);
@@ -100,6 +100,9 @@ public class MainAdapter extends SectionAdapter<Integer> {
} else if("videos".equals(header)) {
display = context.getResources().getString(R.string.main_videos);
checkBox.setVisibility(View.GONE);
+ } else if("songs".equals(header)) {
+ display = context.getResources().getString(R.string.search_songs);
+ checkBox.setVisibility(View.GONE);
} else {
display = header;
checkBox.setVisibility(View.GONE);
diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/PodcastChannelAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/PodcastChannelAdapter.java
index a95abeda..f843a722 100644
--- a/app/src/main/java/github/daneren2005/dsub/adapter/PodcastChannelAdapter.java
+++ b/app/src/main/java/github/daneren2005/dsub/adapter/PodcastChannelAdapter.java
@@ -34,21 +34,17 @@ import github.daneren2005.dsub.view.SongView;
import github.daneren2005.dsub.view.UpdateView;
import java.io.Serializable;
+import java.util.Arrays;
import java.util.List;
-public class PodcastChannelAdapter extends SectionAdapter<Serializable> implements FastScroller.BubbleTextGetter {
+public class PodcastChannelAdapter extends ExpandableSectionAdapter<Serializable> implements FastScroller.BubbleTextGetter {
public static final int VIEW_TYPE_PODCAST_LEGACY = 1;
public static final int VIEW_TYPE_PODCAST_LINE = 2;
public static final int VIEW_TYPE_PODCAST_CELL = 3;
public static final int VIEW_TYPE_PODCAST_EPISODE = 4;
- public static final String EPISODE_HEADER = "episodes";
- public static final String CHANNEL_HEADER = "channels";
-
private ImageLoader imageLoader;
private boolean largeCell;
- private int selectToggleAttr = R.attr.select_server;
- private List<Serializable> extraEpisodes;
public PodcastChannelAdapter(Context context, List<Serializable> podcasts, ImageLoader imageLoader, OnItemClickedListener listener, boolean largeCell) {
super(context, podcasts);
@@ -56,9 +52,8 @@ public class PodcastChannelAdapter extends SectionAdapter<Serializable> implemen
this.onItemClickedListener = listener;
this.largeCell = largeCell;
}
- public PodcastChannelAdapter(Context context, List<String> headers, List<List<Serializable>> sections, List<Serializable> extraEpisodes, ImageLoader imageLoader, OnItemClickedListener listener, boolean largeCell) {
- super(context, headers, sections);
- this.extraEpisodes = extraEpisodes;
+ public PodcastChannelAdapter(Context context, List<String> headers, List<List<Serializable>> sections, ImageLoader imageLoader, OnItemClickedListener listener, boolean largeCell) {
+ super(context, headers, sections, Arrays.asList(3, null));
this.imageLoader = imageLoader;
this.onItemClickedListener = listener;
this.largeCell = largeCell;
@@ -126,58 +121,6 @@ public class PodcastChannelAdapter extends SectionAdapter<Serializable> implemen
}
menu.removeItem(R.id.menu_remove_playlist);
- menu.removeItem(R.id.menu_unstar);
- }
-
- @Override
- public UpdateView.UpdateViewHolder onCreateHeaderHolder(ViewGroup parent) {
- return new UpdateView.UpdateViewHolder(new BasicHeaderView(context, R.layout.newest_episode_header));
- }
-
- @Override
- public void onBindHeaderHolder(UpdateView.UpdateViewHolder holder, String header) {
- UpdateView view = holder.getUpdateView();
- ImageView toggleSelectionView = (ImageView) view.findViewById(R.id.item_select);
-
- String display;
- if(EPISODE_HEADER.equals(header)) {
- display = context.getResources().getString(R.string.main_albums_newest);
-
- if(extraEpisodes != null && !extraEpisodes.isEmpty()) {
- toggleSelectionView.setVisibility(View.VISIBLE);
- toggleSelectionView.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- // Update icon
- if (selectToggleAttr == R.attr.select_server) {
- selectToggleAttr = R.attr.select_tabs;
-
- // Update how many are displayed
- sections.get(0).addAll(extraEpisodes);
- notifyItemRangeInserted(4, extraEpisodes.size());
- } else {
- selectToggleAttr = R.attr.select_server;
-
- // Update how many are displayed
- sections.get(0).removeAll(extraEpisodes);
- notifyItemRangeRemoved(4, extraEpisodes.size());
- }
-
- ((ImageView) v).setImageResource(DrawableTint.getDrawableRes(context, selectToggleAttr));
-
- }
- });
- toggleSelectionView.setImageResource(DrawableTint.getDrawableRes(context, selectToggleAttr));
- } else {
- toggleSelectionView.setVisibility(View.GONE);
- }
- } else {
- display = context.getResources().getString(R.string.select_podcasts_channels);
- toggleSelectionView.setVisibility(View.GONE);
- }
-
- if(view != null) {
- view.setObject(display);
- }
+ menu.removeItem(R.id.menu_star);
}
}
diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/SearchAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/SearchAdapter.java
index 66f2db21..69e5d56d 100644
--- a/app/src/main/java/github/daneren2005/dsub/adapter/SearchAdapter.java
+++ b/app/src/main/java/github/daneren2005/dsub/adapter/SearchAdapter.java
@@ -19,7 +19,9 @@ 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;
@@ -28,10 +30,12 @@ import java.util.List;
import github.daneren2005.dsub.R;
import github.daneren2005.dsub.domain.MusicDirectory.Entry;
import github.daneren2005.dsub.domain.SearchResult;
+import github.daneren2005.dsub.util.DrawableTint;
import github.daneren2005.dsub.util.ImageLoader;
import github.daneren2005.dsub.util.Util;
import github.daneren2005.dsub.view.AlbumView;
import github.daneren2005.dsub.view.ArtistView;
+import github.daneren2005.dsub.view.BasicHeaderView;
import github.daneren2005.dsub.view.SongView;
import github.daneren2005.dsub.view.UpdateView;
@@ -40,32 +44,39 @@ import static github.daneren2005.dsub.adapter.EntryGridAdapter.VIEW_TYPE_ALBUM_C
import static github.daneren2005.dsub.adapter.EntryGridAdapter.VIEW_TYPE_ALBUM_LINE;
import static github.daneren2005.dsub.adapter.EntryGridAdapter.VIEW_TYPE_SONG;
-public class SearchAdapter extends SectionAdapter<Serializable> {
- private SearchResult searchResult;
+public class SearchAdapter extends ExpandableSectionAdapter<Serializable> {
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.context = context;
- this.searchResult = searchResult;
this.imageLoader = imageLoader;
this.largeAlbums = largeAlbums;
- this.sections = new ArrayList<>();
- this.headers = new ArrayList<>();
+ List<List<Serializable>> sections = new ArrayList<>();
+ List<String> headers = new ArrayList<>();
+ List<Integer> defaultVisible = new ArrayList<>();
Resources res = context.getResources();
if(!searchResult.getArtists().isEmpty()) {
- this.sections.add((List<Serializable>) (List<?>) searchResult.getArtists());
- this.headers.add(res.getString(R.string.search_artists));
+ sections.add((List<Serializable>) (List<?>) searchResult.getArtists());
+ headers.add(res.getString(R.string.search_artists));
+ defaultVisible.add(MAX_ARTISTS);
}
if(!searchResult.getAlbums().isEmpty()) {
- this.sections.add((List<Serializable>) (List<?>) searchResult.getAlbums());
- this.headers.add(res.getString(R.string.search_albums));
+ sections.add((List<Serializable>) (List<?>) searchResult.getAlbums());
+ headers.add(res.getString(R.string.search_albums));
+ defaultVisible.add(MAX_ALBUMS);
}
if(!searchResult.getSongs().isEmpty()) {
- this.sections.add((List<Serializable>) (List<?>) searchResult.getSongs());
- this.headers.add(res.getString(R.string.search_songs));
+ sections.add((List<Serializable>) (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;
}
diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/SectionAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/SectionAdapter.java
index d5f9a6ea..33bbb384 100644
--- a/app/src/main/java/github/daneren2005/dsub/adapter/SectionAdapter.java
+++ b/app/src/main/java/github/daneren2005/dsub/adapter/SectionAdapter.java
@@ -194,7 +194,7 @@ public abstract class SectionAdapter<T> extends RecyclerView.Adapter<UpdateViewH
for(List<T> section: sections) {
boolean validHeader = headers.get(subHeader) != null;
if(position == subPosition && validHeader) {
- onBindHeaderHolder(holder, headers.get(subHeader));
+ onBindHeaderHolder(holder, headers.get(subHeader), subHeader);
return;
}
@@ -289,7 +289,7 @@ public abstract class SectionAdapter<T> extends RecyclerView.Adapter<UpdateViewH
public UpdateViewHolder onCreateHeaderHolder(ViewGroup parent) {
return new UpdateViewHolder(new BasicHeaderView(context));
}
- public void onBindHeaderHolder(UpdateViewHolder holder, String header) {
+ public void onBindHeaderHolder(UpdateViewHolder holder, String header, int sectionIndex) {
UpdateView view = holder.getUpdateView();
if(view != null) {
view.setObject(header);
@@ -414,14 +414,15 @@ public abstract class SectionAdapter<T> extends RecyclerView.Adapter<UpdateViewH
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
currentActionMode = mode;
- onCreateActionModeMenu(menu, mode.getMenuInflater());
- MenuUtil.hideMenuItems(context, menu, updateView);
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();
diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/SettingsAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/SettingsAdapter.java
index 1cb9c34e..4e75a2f7 100644
--- a/app/src/main/java/github/daneren2005/dsub/adapter/SettingsAdapter.java
+++ b/app/src/main/java/github/daneren2005/dsub/adapter/SettingsAdapter.java
@@ -16,18 +16,20 @@
package github.daneren2005.dsub.adapter;
import android.content.Context;
-import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.ImageView;
import android.widget.TextView;
+import java.util.ArrayList;
import java.util.List;
import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.ServerInfo;
import github.daneren2005.dsub.domain.User;
import github.daneren2005.dsub.util.ImageLoader;
+import github.daneren2005.dsub.util.UserUtil;
+import github.daneren2005.dsub.view.BasicHeaderView;
import github.daneren2005.dsub.view.RecyclingImageView;
import github.daneren2005.dsub.view.SettingView;
import github.daneren2005.dsub.view.UpdateView;
@@ -37,56 +39,81 @@ import static github.daneren2005.dsub.domain.User.Setting;
public class SettingsAdapter extends SectionAdapter<Setting> {
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, ImageLoader imageLoader, boolean editable, OnItemClickedListener<Setting> onItemClickedListener) {
- super(context, user.getSettings(), imageLoader != null);
+ public SettingsAdapter(Context context, User user, List<String> headers, List<List<User.Setting>> settingSections, ImageLoader imageLoader, boolean editable, OnItemClickedListener<Setting> onItemClickedListener) {
+ super(context, headers, settingSections, imageLoader != null);
this.user = user;
this.imageLoader = imageLoader;
this.editable = editable;
this.onItemClickedListener = onItemClickedListener;
- List<Setting> settings = sections.get(0);
- for(Setting setting: settings) {
- if(setting.getValue()) {
- addSelected(setting);
+ for(List<Setting> 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 UpdateView.UpdateViewHolder onCreateHeaderHolder(ViewGroup parent) {
View header = LayoutInflater.from(context).inflate(R.layout.user_header, parent, false);
return new UpdateView.UpdateViewHolder(header, false);
}
- public void onBindHeaderHolder(UpdateView.UpdateViewHolder holder, String description) {
+ public void onBindHeaderHolder(UpdateView.UpdateViewHolder holder, String description, int sectionIndex) {
View header = holder.getView();
RecyclingImageView coverArtView = (RecyclingImageView) header.findViewById(R.id.user_avatar);
- imageLoader.loadAvatar(context, coverArtView, user.getUsername());
- coverArtView.setOnInvalidated(new RecyclingImageView.OnInvalidated() {
- @Override
- public void onInvalidated(RecyclingImageView imageView) {
- imageLoader.loadAvatar(context, imageView, user.getUsername());
+ if(coverArtView != null) {
+ imageLoader.loadAvatar(context, coverArtView, user.getUsername());
+ coverArtView.setOnInvalidated(new RecyclingImageView.OnInvalidated() {
+ @Override
+ public void onInvalidated(RecyclingImageView imageView) {
+ imageLoader.loadAvatar(context, imageView, user.getUsername());
+ }
+ });
+
+ TextView usernameView = (TextView) header.findViewById(R.id.user_username);
+ usernameView.setText(user.getUsername());
+
+ final TextView emailView = (TextView) header.findViewById(R.id.user_email);
+ if (user.getEmail() != null) {
+ emailView.setText(user.getEmail());
+ } else {
+ emailView.setVisibility(View.GONE);
}
- });
-
- TextView usernameView = (TextView) header.findViewById(R.id.user_username);
- usernameView.setText(user.getUsername());
-
- final TextView emailView = (TextView) header.findViewById(R.id.user_email);
- if(user.getEmail() != null) {
- emailView.setText(user.getEmail());
} else {
- emailView.setVisibility(View.GONE);
+ TextView nameView = (TextView) header.findViewById(R.id.item_name);
+ nameView.setText(description);
}
}
@Override
public UpdateView.UpdateViewHolder onCreateSectionViewHolder(ViewGroup parent, int viewType) {
- return new UpdateView.UpdateViewHolder(new SettingView(context));
+ if(viewType == VIEW_TYPE_SETTING_HEADER) {
+ return new UpdateView.UpdateViewHolder(new BasicHeaderView(context));
+ } else {
+ return new UpdateView.UpdateViewHolder(new SettingView(context));
+ }
}
@Override
@@ -105,4 +132,21 @@ public class SettingsAdapter extends SectionAdapter<Setting> {
updateView.setChecked(checked);
}
}
+
+ public static SettingsAdapter getSettingsAdapter(Context context, User user, ImageLoader imageLoader, OnItemClickedListener<Setting> onItemClickedListener) {
+ return getSettingsAdapter(context, user, imageLoader, UserUtil.isCurrentAdmin() && ServerInfo.checkServerVersion(context, "1.10"), onItemClickedListener);
+ }
+ public static SettingsAdapter getSettingsAdapter(Context context, User user, ImageLoader imageLoader, boolean isEditable, OnItemClickedListener<Setting> onItemClickedListener) {
+ List<String> headers = new ArrayList<>();
+ List<List<User.Setting>> settingsSections = new ArrayList<>();
+ headers.add(context.getResources().getString(R.string.admin_permissions));
+ settingsSections.add(user.getSettings());
+
+ if(user.getMusicFolderSettings() != null) {
+ headers.add(context.getResources().getString(R.string.admin_musicFolders));
+ settingsSections.add(user.getMusicFolderSettings());
+ }
+
+ return new SettingsAdapter(context, user, headers, settingsSections, imageLoader, isEditable, onItemClickedListener);
+ }
}
diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/SimilarArtistAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/SimilarArtistAdapter.java
new file mode 100644
index 00000000..2234d4cd
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/adapter/SimilarArtistAdapter.java
@@ -0,0 +1,53 @@
+/*
+ This file is part of Subsonic.
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+ Copyright 2016 (C) Scott Jackson
+*/
+
+package github.daneren2005.dsub.adapter;
+
+import android.content.Context;
+import android.view.ViewGroup;
+
+import java.util.List;
+
+import github.daneren2005.dsub.domain.Artist;
+import github.daneren2005.dsub.view.ArtistView;
+import github.daneren2005.dsub.view.UpdateView;
+
+public class SimilarArtistAdapter extends SectionAdapter<Artist> {
+ public static int VIEW_TYPE_ARTIST = 4;
+
+ public SimilarArtistAdapter(Context context, List<Artist> artists, OnItemClickedListener onItemClickedListener) {
+ super(context, artists);
+ this.onItemClickedListener = onItemClickedListener;
+ }
+ public SimilarArtistAdapter(Context context, List<String> headers, List<List<Artist>> sections, OnItemClickedListener onItemClickedListener) {
+ super(context, headers, sections);
+ this.onItemClickedListener = onItemClickedListener;
+ }
+
+ @Override
+ public UpdateView.UpdateViewHolder onCreateSectionViewHolder(ViewGroup parent, int viewType) {
+ return new UpdateView.UpdateViewHolder(new ArtistView(context));
+ }
+
+ @Override
+ public void onBindViewHolder(UpdateView.UpdateViewHolder holder, Artist item, int viewType) {
+ holder.getUpdateView().setObject(item);
+ }
+
+ @Override
+ public int getItemViewType(Artist item) {
+ return VIEW_TYPE_ARTIST;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/domain/Artist.java b/app/src/main/java/github/daneren2005/dsub/domain/Artist.java
index 56e8f92e..ff4d86ce 100644
--- a/app/src/main/java/github/daneren2005/dsub/domain/Artist.java
+++ b/app/src/main/java/github/daneren2005/dsub/domain/Artist.java
@@ -21,15 +21,19 @@ package github.daneren2005.dsub.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;
@@ -38,6 +42,14 @@ public class Artist implements Serializable {
private Integer rating;
private int closeness;
+ public Artist() {
+
+ }
+ public Artist(String id, String name) {
+ this.id = id;
+ this.name = name;
+ }
+
public String getId() {
return id;
}
@@ -109,30 +121,18 @@ public class Artist implements Serializable {
public static class ArtistComparator implements Comparator<Artist> {
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) {
- if("root".equals(lhsArtist.getId())) {
- return 1;
- } else if("root".equals(rhsArtist.getId())) {
- return -1;
- }
-
String lhs = lhsArtist.getName().toLowerCase();
String rhs = rhsArtist.getName().toLowerCase();
- char lhs1 = lhs.charAt(0);
- char rhs1 = rhs.charAt(0);
-
- if (Character.isDigit(lhs1) && !Character.isDigit(rhs1)) {
- return 1;
- } else if (Character.isDigit(rhs1) && !Character.isDigit(lhs1)) {
- return -1;
- }
-
for (String article : ignoredArticles) {
int index = lhs.indexOf(article.toLowerCase() + " ");
if (index == 0) {
@@ -144,7 +144,7 @@ public class Artist implements Serializable {
}
}
- return lhs.compareTo(rhs);
+ return collator.compare(lhs, rhs);
}
}
diff --git a/app/src/main/java/github/daneren2005/dsub/domain/Indexes.java b/app/src/main/java/github/daneren2005/dsub/domain/Indexes.java
index e15ccf9f..05e686ca 100644
--- a/app/src/main/java/github/daneren2005/dsub/domain/Indexes.java
+++ b/app/src/main/java/github/daneren2005/dsub/domain/Indexes.java
@@ -52,13 +52,6 @@ public class Indexes implements Serializable {
this.shortcuts = shortcuts;
this.artists = artists;
this.entries = entries;
- if(!entries.isEmpty()) {
- Artist root = new Artist();
- root.setId("root");
- root.setName("Root");
- root.setIndex("#");
- artists.add(root);
- }
}
public long getLastModified() {
diff --git a/app/src/main/java/github/daneren2005/dsub/domain/InternetRadioStation.java b/app/src/main/java/github/daneren2005/dsub/domain/InternetRadioStation.java
new file mode 100644
index 00000000..47d79b99
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/domain/InternetRadioStation.java
@@ -0,0 +1,43 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2016 (C) Scott Jackson
+*/
+
+package github.daneren2005.dsub.domain;
+
+public class InternetRadioStation extends MusicDirectory.Entry {
+ private String streamUrl;
+ private String homePageUrl;
+
+ public InternetRadioStation() {}
+
+ public String getStreamUrl() {
+ return streamUrl;
+ }
+
+ public void setStreamUrl(String streamUrl) {
+ this.streamUrl = streamUrl;
+ }
+
+ public String getHomePageUrl() {
+ return homePageUrl;
+ }
+
+ public void setHomePageUrl(String homePageUrl) {
+ this.homePageUrl = homePageUrl;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/domain/MusicDirectory.java b/app/src/main/java/github/daneren2005/dsub/domain/MusicDirectory.java
index 3c022cea..5f7b2412 100644
--- a/app/src/main/java/github/daneren2005/dsub/domain/MusicDirectory.java
+++ b/app/src/main/java/github/daneren2005/dsub/domain/MusicDirectory.java
@@ -24,6 +24,8 @@ 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;
@@ -31,8 +33,11 @@ import java.io.File;
import java.io.Serializable;
import java.util.Collections;
import java.util.Comparator;
+import java.util.Locale;
+import github.daneren2005.dsub.service.DownloadService;
import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.UpdateHelper;
import github.daneren2005.dsub.util.Util;
/**
@@ -135,13 +140,14 @@ public class MusicDirectory implements Serializable {
EntryComparator.sort(children, byYear);
}
- public synchronized void updateMetadata(MusicDirectory refreshedDirectory) {
+ public synchronized boolean updateMetadata(MusicDirectory refreshedDirectory) {
+ boolean metadataUpdated = false;
Iterator<Entry> it = children.iterator();
while(it.hasNext()) {
Entry entry = it.next();
int index = refreshedDirectory.children.indexOf(entry);
if(index != -1) {
- Entry refreshed = refreshedDirectory.children.get(index);
+ final Entry refreshed = refreshedDirectory.children.get(index);
entry.setTitle(refreshed.getTitle());
entry.setAlbum(refreshed.getAlbum());
@@ -155,8 +161,36 @@ public class MusicDirectory implements Serializable {
entry.setStarred(refreshed.isStarred());
entry.setRating(refreshed.getRating());
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.setStarred(refreshed.isStarred());
+ found.setRating(refreshed.getRating());
+ 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;
@@ -202,6 +236,7 @@ public class MusicDirectory implements Serializable {
private String album;
private String artist;
private Integer track;
+ private Integer customOrder;
private Integer year;
private String genre;
private String contentType;
@@ -275,6 +310,10 @@ public class MusicDirectory implements Serializable {
public void rebaseTitleOffPath() {
try {
String filename = getPath();
+ if(filename == null) {
+ return;
+ }
+
int index = filename.lastIndexOf('/');
if (index != -1) {
filename = filename.substring(index + 1);
@@ -386,6 +425,13 @@ public class MusicDirectory implements Serializable {
this.track = track;
}
+ public Integer getCustomOrder() {
+ return customOrder;
+ }
+ public void setCustomOrder(Integer customOrder) {
+ this.customOrder = customOrder;
+ }
+
public Integer getYear() {
return year;
}
@@ -586,9 +632,12 @@ public class MusicDirectory implements Serializable {
public static class EntryComparator implements Comparator<Entry> {
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) {
@@ -608,8 +657,8 @@ public class MusicDirectory implements Serializable {
return 1;
}
}
-
- return lhs.getAlbumDisplay().compareToIgnoreCase(rhs.getAlbumDisplay());
+
+ return collator.compare(lhs.getAlbumDisplay(), rhs.getAlbumDisplay());
}
Integer lhsDisc = lhs.getDiscNumber();
@@ -633,7 +682,7 @@ public class MusicDirectory implements Serializable {
return 1;
}
- return lhs.getTitle().compareToIgnoreCase(rhs.getTitle());
+ return collator.compare(lhs.getTitle(), rhs.getTitle());
}
public static void sort(List<Entry> entries) {
diff --git a/app/src/main/java/github/daneren2005/dsub/domain/MusicFolder.java b/app/src/main/java/github/daneren2005/dsub/domain/MusicFolder.java
index 99e86e23..37f76249 100644
--- a/app/src/main/java/github/daneren2005/dsub/domain/MusicFolder.java
+++ b/app/src/main/java/github/daneren2005/dsub/domain/MusicFolder.java
@@ -18,7 +18,12 @@
*/
package github.daneren2005.dsub.domain;
+import android.util.Log;
+
import java.io.Serializable;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
/**
* Represents a top level directory in which music or other media is stored.
@@ -27,23 +32,49 @@ import java.io.Serializable;
* @version $Id$
*/
public class MusicFolder implements Serializable {
-
- private String id;
- private String name;
+ 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 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 String getId() {
- return id;
- }
+ public static class MusicFolderComparator implements Comparator<MusicFolder> {
+ 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 String getName() {
- return name;
- }
+ public static void sort(List<MusicFolder> 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/daneren2005/dsub/domain/SearchCritera.java b/app/src/main/java/github/daneren2005/dsub/domain/SearchCritera.java
index 20d46aa0..ed2400ef 100644
--- a/app/src/main/java/github/daneren2005/dsub/domain/SearchCritera.java
+++ b/app/src/main/java/github/daneren2005/dsub/domain/SearchCritera.java
@@ -18,6 +18,8 @@
*/
package github.daneren2005.dsub.domain;
+import java.util.regex.Pattern;
+
/**
* The criteria for a music search.
*
@@ -25,31 +27,67 @@ package github.daneren2005.dsub.domain;
*/
public class SearchCritera {
- private final String query;
- private final int artistCount;
- private final int albumCount;
- private final int songCount;
-
- public SearchCritera(String query, int artistCount, int albumCount, int songCount) {
- this.query = query;
- this.artistCount = artistCount;
- this.albumCount = albumCount;
- this.songCount = songCount;
- }
-
- public String getQuery() {
- return query;
- }
-
- public int getArtistCount() {
- return artistCount;
- }
-
- public int getAlbumCount() {
- return albumCount;
- }
-
- public int getSongCount() {
- return songCount;
- }
-} \ No newline at end of file
+ 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/daneren2005/dsub/domain/ServerInfo.java b/app/src/main/java/github/daneren2005/dsub/domain/ServerInfo.java
index 73037c4a..5852210e 100644
--- a/app/src/main/java/github/daneren2005/dsub/domain/ServerInfo.java
+++ b/app/src/main/java/github/daneren2005/dsub/domain/ServerInfo.java
@@ -24,6 +24,7 @@ import java.io.Serializable;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
+import github.daneren2005.dsub.util.Constants;
import github.daneren2005.dsub.util.FileUtil;
import github.daneren2005.dsub.util.Util;
@@ -35,6 +36,7 @@ import github.daneren2005.dsub.util.Util;
public class ServerInfo implements Serializable {
public static final int TYPE_SUBSONIC = 1;
public static final int TYPE_MADSONIC = 2;
+ public static final int TYPE_AMPACHE = 3;
private static final Map<Integer, ServerInfo> SERVERS = new ConcurrentHashMap<Integer, ServerInfo>();
private boolean isLicenseValid;
@@ -189,13 +191,20 @@ public class ServerInfo implements Serializable {
public static boolean isMadsonic6(Context context, int instance) {
return getServerType(context, instance) == TYPE_MADSONIC && checkServerVersion(context, "2.0", instance);
}
+
+ public static boolean isAmpache(Context context) {
+ return isAmpache(context, Util.getActiveServer(context));
+ }
+ public static boolean isAmpache(Context context, int instance) {
+ return getServerType(context, instance) == TYPE_AMPACHE;
+ }
private static String getCacheName(Context context, int instance) {
return "server-" + Util.getRestUrl(context, null, instance, false).hashCode() + ".ser";
}
public static boolean hasArtistInfo(Context context) {
- if(isStockSubsonic(context) && ServerInfo.checkServerVersion(context, "1.11")) {
+ if(!isMadsonic(context) && ServerInfo.checkServerVersion(context, "1.11")) {
return true;
} else if(isMadsonic(context)) {
return checkServerVersion(context, "2.0");
@@ -207,6 +216,9 @@ public class ServerInfo implements Serializable {
public static boolean canBookmark(Context context) {
return checkServerVersion(context, "1.9");
}
+ public static boolean canInternetRadio(Context context) {
+ return checkServerVersion(context, "1.9");
+ }
public static boolean canSavePlayQueue(Context context) {
return ServerInfo.checkServerVersion(context, "1.12") && (!ServerInfo.isMadsonic(context) || checkServerVersion(context, "2.0"));
@@ -223,8 +235,15 @@ public class ServerInfo implements Serializable {
return canUseToken(context, Util.getActiveServer(context));
}
public static boolean canUseToken(Context context, int instance) {
- return false; /*isStockSubsonic(context, instance) && checkServerVersion(context, "1.13", instance) ||
- isMadsonic(context, instance) && checkServerVersion(context, "2.0", instance);*/
+ if(isStockSubsonic(context, instance) && checkServerVersion(context, "1.14", instance)) {
+ if(Util.getBlockTokenUse(context, instance)) {
+ return false;
+ } else {
+ return true;
+ }
+ } else {
+ return false;
+ }
}
public static boolean hasSimilarArtists(Context context) {
return !ServerInfo.isMadsonic(context) || ServerInfo.checkServerVersion(context, "2.0");
@@ -232,4 +251,9 @@ public class ServerInfo implements Serializable {
public static boolean hasNewestPodcastEpisodes(Context context) {
return ServerInfo.checkServerVersion(context, "1.13");
}
+
+ public static boolean canRescanServer(Context context) {
+ return ServerInfo.isMadsonic(context) ||
+ (ServerInfo.isStockSubsonic(context) && ServerInfo.checkServerVersion(context, "1.15"));
+ }
}
diff --git a/app/src/main/java/github/daneren2005/dsub/domain/User.java b/app/src/main/java/github/daneren2005/dsub/domain/User.java
index 797a1271..5307828a 100644
--- a/app/src/main/java/github/daneren2005/dsub/domain/User.java
+++ b/app/src/main/java/github/daneren2005/dsub/domain/User.java
@@ -15,6 +15,8 @@
package github.daneren2005.dsub.domain;
+import android.util.Pair;
+
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
@@ -31,8 +33,9 @@ public class User implements Serializable {
public static final String STREAM = "streamRole";
public static final String JUKEBOX = "jukeboxRole";
public static final String SHARE = "shareRole";
+ public static final String VIDEO_CONVERSION = "videoConversionRole";
public static final String LASTFM = "lastFMRole";
- public static final List<String> ROLES = new ArrayList<String>();
+ public static final List<String> ROLES = new ArrayList<>();
static {
ROLES.add(ADMIN);
@@ -45,6 +48,7 @@ public class User implements Serializable {
ROLES.add(PODCAST);
ROLES.add(JUKEBOX);
ROLES.add(SHARE);
+ ROLES.add(VIDEO_CONVERSION);
}
private String username;
@@ -52,6 +56,7 @@ public class User implements Serializable {
private String email;
private List<Setting> settings = new ArrayList<Setting>();
+ private List<Setting> musicFolders;
public User() {
@@ -92,9 +97,27 @@ public class User implements Serializable {
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<Setting> getMusicFolderSettings() {
+ return musicFolders;
+ }
+
public static class Setting implements Serializable {
- String name;
- Boolean value;
+ private String name;
+ private Boolean value;
public Setting() {
@@ -114,4 +137,20 @@ public class User implements Serializable {
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/daneren2005/dsub/domain/Version.java b/app/src/main/java/github/daneren2005/dsub/domain/Version.java
index 97246ecf..9df0dbb4 100644
--- a/app/src/main/java/github/daneren2005/dsub/domain/Version.java
+++ b/app/src/main/java/github/daneren2005/dsub/domain/Version.java
@@ -27,41 +27,41 @@ import java.io.Serializable;
* @version $Revision: 1.3 $ $Date: 2006/01/20 21:25:16 $
*/
public class Version implements Comparable<Version>, Serializable {
- private int major;
- private int minor;
- private int beta;
- private int bugfix;
+ 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;
- }
-
+ /**
+ * 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:
@@ -90,96 +90,98 @@ public class Version implements Comparable<Version>, Serializable {
return "4.9";
case 11:
return "5.1";
- case 12:
- return "5.2";
- case 13:
- return "5.3";
+ 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;
- }
+ /**
+ * Return whether this object is equal to another.
+ * @param o Object to compare to.
+ * @return Whether this object is equals to another.
+ */
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ final Version version = (Version) o;
+
+ if (beta != version.beta) return false;
+ if (bugfix != version.bugfix) return false;
+ if (major != version.major) return false;
+ return minor == version.minor;
+ }
+
+ /**
+ * Returns a hash code for this object.
+ * @return A hash code for this object.
+ */
+ public int hashCode() {
+ int result;
+ result = major;
+ result = 29 * result + minor;
+ result = 29 * result + beta;
+ result = 29 * result + bugfix;
+ return result;
+ }
+
+ /**
+ * Returns a string representation of the form "1.27", "1.27.2" or "1.27.beta3".
+ * @return A string representation of the form "1.27", "1.27.2" or "1.27.beta3".
+ */
+ public String toString() {
+ StringBuffer buf = new StringBuffer();
+ buf.append(major).append('.').append(minor);
+ if (beta != 0) {
+ buf.append(".beta").append(beta);
+ } else if (bugfix != 0) {
+ buf.append('.').append(bugfix);
+ }
+
+ return buf.toString();
+ }
+
+ /**
+ * Compares this object with the specified object for order.
+ * @param version The object to compare to.
+ * @return A negative integer, zero, or a positive integer as this object is less than, equal to, or
+ * greater than the specified object.
+ */
+ @Override
+ public int compareTo(Version version) {
+ if (major < version.major) {
+ return -1;
+ } else if (major > version.major) {
+ return 1;
+ }
+
+ if (minor < version.minor) {
+ return -1;
+ } else if (minor > version.minor) {
+ return 1;
+ }
+
+ if (bugfix < version.bugfix) {
+ return -1;
+ } else if (bugfix > version.bugfix) {
+ return 1;
+ }
+
+ int thisBeta = beta == 0 ? Integer.MAX_VALUE : beta;
+ int otherBeta = version.beta == 0 ? Integer.MAX_VALUE : version.beta;
+
+ if (thisBeta < otherBeta) {
+ return -1;
+ } else if (thisBeta > otherBeta) {
+ return 1;
+ }
+
+ return 0;
+ }
} \ No newline at end of file
diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/AdminFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/AdminFragment.java
index 630acf2c..552712f7 100644
--- a/app/src/main/java/github/daneren2005/dsub/fragments/AdminFragment.java
+++ b/app/src/main/java/github/daneren2005/dsub/fragments/AdminFragment.java
@@ -47,7 +47,7 @@ public class AdminFragment extends SelectRecyclerFragment<User> {
switch (item.getItemId()) {
case R.id.menu_add_user:
- UserUtil.addNewUser(context, this);
+ UserUtil.addNewUser(context, this, (objects.size() > 0) ? objects.get(0) : null);
break;
}
diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/ChatFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/ChatFragment.java
index febf22de..3208ffb7 100644
--- a/app/src/main/java/github/daneren2005/dsub/fragments/ChatFragment.java
+++ b/app/src/main/java/github/daneren2005/dsub/fragments/ChatFragment.java
@@ -135,8 +135,8 @@ public class ChatFragment extends SubsonicFragment {
}
@Override
- public void onResume() {
- super.onResume();
+ public void onStart() {
+ super.onStart();
final Handler handler = new Handler();
Runnable runnable = new Runnable() {
@@ -164,8 +164,8 @@ public class ChatFragment extends SubsonicFragment {
}
@Override
- public void onPause() {
- super.onPause();
+ public void onStop() {
+ super.onStop();
if(executorService != null) {
executorService.shutdown();
executorService = null;
diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/DownloadFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/DownloadFragment.java
index 7594a99e..9e8f8c5b 100644
--- a/app/src/main/java/github/daneren2005/dsub/fragments/DownloadFragment.java
+++ b/app/src/main/java/github/daneren2005/dsub/fragments/DownloadFragment.java
@@ -65,8 +65,8 @@ public class DownloadFragment extends SelectRecyclerFragment<DownloadFile> imple
}
@Override
- public void onResume() {
- super.onResume();
+ public void onStart() {
+ super.onStart();
final Handler handler = new Handler();
Runnable runnable = new Runnable() {
@@ -86,8 +86,8 @@ public class DownloadFragment extends SelectRecyclerFragment<DownloadFile> imple
}
@Override
- public void onPause() {
- super.onPause();
+ public void onStop() {
+ super.onStop();
executorService.shutdown();
}
diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/EqualizerFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/EqualizerFragment.java
index 9ee98cb4..948c59cf 100644
--- a/app/src/main/java/github/daneren2005/dsub/fragments/EqualizerFragment.java
+++ b/app/src/main/java/github/daneren2005/dsub/fragments/EqualizerFragment.java
@@ -110,8 +110,8 @@ public class EqualizerFragment extends SubsonicFragment {
}
@Override
- public void onPause() {
- super.onPause();
+ public void onStop() {
+ super.onStop();
try {
equalizerController.saveSettings();
@@ -125,8 +125,8 @@ public class EqualizerFragment extends SubsonicFragment {
}
@Override
- public void onResume() {
- super.onResume();
+ public void onStart() {
+ super.onStart();
equalizerController = DownloadService.getInstance().getEqualizerController();
equalizer = equalizerController.getEqualizer();
bass = equalizerController.getBassBoost();
diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/MainFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/MainFragment.java
index 5daf3d7a..82e50b76 100644
--- a/app/src/main/java/github/daneren2005/dsub/fragments/MainFragment.java
+++ b/app/src/main/java/github/daneren2005/dsub/fragments/MainFragment.java
@@ -1,11 +1,12 @@
package github.daneren2005.dsub.fragments;
-import android.content.res.Resources;
-import android.os.Environment;
import android.content.Intent;
-import android.content.SharedPreferences;
+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;
@@ -18,6 +19,7 @@ import github.daneren2005.dsub.adapter.MainAdapter;
import github.daneren2005.dsub.adapter.SectionAdapter;
import github.daneren2005.dsub.domain.ServerInfo;
import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.EnvironmentVariables;
import github.daneren2005.dsub.util.FileUtil;
import github.daneren2005.dsub.util.LoadingTask;
import github.daneren2005.dsub.util.ProgressListener;
@@ -28,13 +30,28 @@ import github.daneren2005.dsub.service.MusicServiceFactory;
import github.daneren2005.dsub.view.ChangeLog;
import github.daneren2005.dsub.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<Integer> {
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();
@@ -47,10 +64,11 @@ public class MainFragment extends SelectRecyclerFragment<Integer> {
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) {
menuInflater.inflate(R.menu.main, menu);
+ onFinishSetupOptionsMenu(menu);
try {
- if (!ServerInfo.isMadsonic(context) || !UserUtil.isCurrentAdmin()) {
- menu.setGroupVisible(R.id.madsonic, false);
+ if (!ServerInfo.canRescanServer(context) || !UserUtil.isCurrentAdmin()) {
+ menu.setGroupVisible(R.id.rescan_server, false);
}
} catch(Exception e) {
Log.w(TAG, "Error on setting madsonic invisible", e);
@@ -113,6 +131,22 @@ public class MainFragment extends SelectRecyclerFragment<Integer> {
sections.add(albums);
headers.add("albums");
+ if(ServerInfo.isMadsonic6(context)) {
+ List<Integer> songs = new ArrayList<>();
+
+ songs.add(R.string.main_songs_newest);
+ if(ServerInfo.checkServerVersion(context, "2.0.1")) {
+ songs.add(R.string.main_songs_top_played);
+ }
+ songs.add(R.string.main_songs_recent);
+ if(ServerInfo.checkServerVersion(context, "2.0.1")) {
+ songs.add(R.string.main_songs_frequent);
+ }
+
+ sections.add(songs);
+ headers.add("songs");
+ }
+
if(ServerInfo.checkServerVersion(context, "1.8")) {
List<Integer> videos = Arrays.asList(R.string.main_videos);
sections.add(videos);
@@ -237,10 +271,10 @@ public class MainFragment extends SelectRecyclerFragment<Integer> {
private void getLogs() {
try {
- final String version = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName;
- new LoadingTask<File>(context) {
+ final PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
+ new LoadingTask<String>(context) {
@Override
- protected File doInBackground() throws Throwable {
+ protected String doInBackground() throws Throwable {
updateProgress("Gathering Logs");
File logcat = new File(Environment.getExternalStorageDirectory(), "dsub-logcat.txt");
Util.delete(logcat);
@@ -258,30 +292,94 @@ public class MainFragment extends SelectRecyclerFragment<Integer> {
logcatProc = Runtime.getRuntime().exec(progs.toArray(new String[progs.size()]));
logcatProc.waitFor();
- } catch(Exception e) {
- Util.toast(context, "Failed to gather logs");
} finally {
if(logcatProc != null) {
logcatProc.destroy();
}
}
- return logcat;
+ 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(), "dsub-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(File logcat) {
+ 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", "dsub.android@gmail.com", null));
- email.putExtra(Intent.EXTRA_SUBJECT, "DSub " + version + " Error Logs");
+ email.putExtra(Intent.EXTRA_SUBJECT, "DSub " + packageInfo.versionName + " Error Logs");
email.putExtra(Intent.EXTRA_TEXT, "Describe the problem here\n\n\n" + footer);
- Uri attachment = Uri.fromFile(logcat);
- email.putExtra(Intent.EXTRA_STREAM, attachment);
startActivity(email);
}
}.execute();
@@ -310,6 +408,14 @@ public class MainFragment extends SelectRecyclerFragment<Integer> {
showAlbumList("alphabeticalByName");
} else if(item == R.string.main_videos) {
showVideos();
+ } 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);
}
}
diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/NowPlayingFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/NowPlayingFragment.java
index c557a174..10623b4e 100644
--- a/app/src/main/java/github/daneren2005/dsub/fragments/NowPlayingFragment.java
+++ b/app/src/main/java/github/daneren2005/dsub/fragments/NowPlayingFragment.java
@@ -51,15 +51,18 @@ 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 com.shehabic.droppy.DroppyClickCallbackInterface;
+import com.shehabic.droppy.DroppyMenuPopup;
+import com.shehabic.droppy.animations.DroppyFadeInAnimation;
import github.daneren2005.dsub.R;
import github.daneren2005.dsub.activity.SubsonicFragmentActivity;
import github.daneren2005.dsub.adapter.SectionAdapter;
import github.daneren2005.dsub.audiofx.EqualizerController;
import github.daneren2005.dsub.domain.Bookmark;
-import github.daneren2005.dsub.domain.MusicDirectory;
import github.daneren2005.dsub.domain.PlayerState;
import github.daneren2005.dsub.domain.RepeatMode;
import github.daneren2005.dsub.domain.ServerInfo;
@@ -73,6 +76,7 @@ import github.daneren2005.dsub.service.ServerTooOldException;
import github.daneren2005.dsub.util.Constants;
import github.daneren2005.dsub.util.SilentBackgroundTask;
import github.daneren2005.dsub.adapter.DownloadFileAdapter;
+import github.daneren2005.dsub.view.compat.CustomMediaRouteDialogFactory;
import github.daneren2005.dsub.view.FadeOutAnimation;
import github.daneren2005.dsub.view.FastScroller;
import github.daneren2005.dsub.view.UpdateView;
@@ -88,7 +92,6 @@ import java.util.concurrent.ScheduledFuture;
public class NowPlayingFragment extends SubsonicFragment implements OnGestureListener, SectionAdapter.OnItemClickedListener<DownloadFile>, OnSongChangedListener {
private static final String TAG = NowPlayingFragment.class.getSimpleName();
private static final int PERCENTAGE_OF_SCREEN_FOR_SWIPE = 10;
- private static final int INCREMENT_TIME = 5000;
private static final int ACTION_PREVIOUS = 1;
private static final int ACTION_NEXT = 2;
@@ -106,6 +109,8 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis
private SeekBar progressBar;
private AutoRepeatButton previousButton;
private AutoRepeatButton nextButton;
+ private AutoRepeatButton rewindButton;
+ private AutoRepeatButton fastforwardButton;
private View pauseButton;
private View stopButton;
private View startButton;
@@ -115,6 +120,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis
private ImageButton bookmarkButton;
private ImageButton rateBadButton;
private ImageButton rateGoodButton;
+ private ImageButton playbackSpeedButton;
private ScheduledExecutorService executorService;
private DownloadFile currentPlaying;
@@ -129,6 +135,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis
private int lastY = 0;
private int currentPlayingSize = 0;
private MenuItem timerMenu;
+ private DroppySpeedControl speed;
/**
* Called when the activity is first created.
@@ -172,6 +179,8 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis
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);
@@ -179,6 +188,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis
bookmarkButton = (ImageButton) rootView.findViewById(R.id.download_bookmark);
rateBadButton = (ImageButton) rootView.findViewById(R.id.download_rating_bad);
rateGoodButton = (ImageButton) rootView.findViewById(R.id.download_rating_good);
+ playbackSpeedButton = (ImageButton) rootView.findViewById(R.id.download_playback_speed);
toggleListButton =rootView.findViewById(R.id.download_toggle_list);
playlistView = (RecyclerView)rootView.findViewById(R.id.download_list);
@@ -194,6 +204,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis
@Override
public void onClick(View v) {
getDownloadService().toggleStarred();
+ setControlsVisible(true);
}
});
} else {
@@ -212,6 +223,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis
bookmarkButton.setOnTouchListener(touchListener);
rateBadButton.setOnTouchListener(touchListener);
rateGoodButton.setOnTouchListener(touchListener);
+ playbackSpeedButton.setOnTouchListener(touchListener);
emptyTextView.setOnTouchListener(touchListener);
albumArtImageView.setOnTouchListener(new View.OnTouchListener() {
@Override
@@ -239,7 +251,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis
});
previousButton.setOnRepeatListener(new Runnable() {
public void run() {
- changeProgress(-INCREMENT_TIME);
+ changeProgress(true);
}
});
@@ -259,10 +271,35 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis
});
nextButton.setOnRepeatListener(new Runnable() {
public void run() {
- changeProgress(INCREMENT_TIME);
+ 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) {
@@ -330,6 +367,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis
@Override
public void onClick(View view) {
createBookmark();
+ setControlsVisible(true);
}
});
@@ -341,6 +379,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis
return;
}
downloadService.toggleRating(1);
+ setControlsVisible(true);
}
});
rateGoodButton.setOnClickListener(new View.OnClickListener() {
@@ -351,9 +390,16 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis
return;
}
downloadService.toggleRating(5);
+ setControlsVisible(true);
}
});
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ setPlaybackSpeed();
+ } else {
+ playbackSpeedButton.setVisibility(View.GONE);
+ }
+
toggleListButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
@@ -405,11 +451,6 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis
}
});
- if(Build.MODEL.equals("Nexus 4") || Build.MODEL.equals("GT-I9100")) {
- View slider = rootView.findViewById(R.id.download_slider);
- slider.setPadding(0, 0, 0, 0);
- }
-
return rootView;
}
@@ -438,7 +479,8 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis
}
boolean equalizerAvailable = downloadService != null && downloadService.getEqualizerAvailable();
- if(equalizerAvailable && !downloadService.isRemoteEnabled()) {
+ boolean isRemoteEnabled = downloadService != null && downloadService.isRemoteEnabled();
+ if(equalizerAvailable && !isRemoteEnabled) {
SharedPreferences prefs = Util.getPreferences(context);
boolean equalizerOn = prefs.getBoolean(Constants.PREFERENCES_EQUALIZER_ON, false);
if (equalizerOn && downloadService != null) {
@@ -450,12 +492,32 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis
menu.removeItem(R.id.menu_equalizer);
}
+ if(Build.VERSION.SDK_INT < Build.VERSION_CODES.M || isRemoteEnabled) {
+ playbackSpeedButton.setVisibility(View.GONE);
+ } else {
+ playbackSpeedButton.setVisibility(View.VISIBLE);
+ }
+
if(downloadService != null) {
MenuItem mediaRouteItem = menu.findItem(R.id.menu_mediaroute);
if(mediaRouteItem != null) {
MediaRouteButton mediaRouteButton = (MediaRouteButton) MenuItemCompat.getActionView(mediaRouteItem);
+ mediaRouteButton.setDialogFactory(new CustomMediaRouteDialogFactory());
mediaRouteButton.setRouteSelector(downloadService.getRemoteSelector());
}
+
+ if(downloadService.isCurrentPlayingSingle()) {
+ if(!Util.isOffline(context)) {
+ menu.removeItem(R.id.menu_save_playlist);
+ }
+
+ menu.removeItem(R.id.menu_batch_mode);
+ menu.removeItem(R.id.menu_remove_played);
+ }
+ }
+
+ if(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_BATCH_MODE, false)) {
+ menu.findItem(R.id.menu_batch_mode).setChecked(true);
}
}
@@ -474,7 +536,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis
menuInflater.inflate(R.menu.nowplaying_context_offline, menu);
} else {
menuInflater.inflate(R.menu.nowplaying_context, menu);
- menu.findItem(R.id.menu_star).setTitle(downloadFile.getSong().isStarred() ? R.string.common_unstar : R.string.common_star);
+ menu.findItem(R.id.song_menu_star).setTitle(downloadFile.getSong().isStarred() ? R.string.common_unstar : R.string.common_star);
}
if (downloadFile.getSong().getParent() == null) {
@@ -620,9 +682,6 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis
}
createNewPlaylist(entries, true);
return true;
- case R.id.menu_star:
- UpdateHelper.toggleStarred(context, song.getSong());
- return true;
case R.id.menu_rate:
UpdateHelper.setRating(context, song.getSong());
return true;
@@ -634,11 +693,6 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis
startTimer();
}
return true;
- case R.id.menu_add_playlist:
- songs = new ArrayList<Entry>(1);
- songs.add(song.getSong());
- addToPlaylist(songs);
- return true;
case R.id.menu_info:
displaySongInfo(song.getSong());
return true;
@@ -663,14 +717,25 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis
// Any failed condition will get here
Util.toast(context, "Failed to start equalizer. Try restarting.");
return true;
- } default:
+ }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();
+ public void onStart() {
+ super.onStart();
if(this.primaryFragment) {
onResumeHandlers();
} else {
@@ -678,13 +743,13 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis
}
}
private void onResumeHandlers() {
- final Handler handler = new Handler();
executorService = Executors.newSingleThreadScheduledExecutor();
setControlsVisible(true);
final DownloadService downloadService = getDownloadService();
if (downloadService == null || downloadService.getCurrentPlaying() == null || startFlipped) {
playlistFlipper.setDisplayedChild(1);
+ startFlipped = false;
}
if (downloadService != null && downloadService.getKeepScreenOn()) {
context.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
@@ -701,19 +766,20 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis
context.runWhenServiceAvailable(new Runnable() {
@Override
public void run() {
- if(primaryFragment) {
+ if (primaryFragment) {
DownloadService downloadService = getDownloadService();
downloadService.startRemoteScan();
downloadService.addOnSongChangedListener(NowPlayingFragment.this, true);
}
updateRepeatButton();
+ updateTitle();
}
});
}
@Override
- public void onPause() {
- super.onPause();
+ public void onStop() {
+ super.onStop();
onPauseHandlers();
}
private void onPauseHandlers() {
@@ -780,6 +846,11 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis
}
private void setControlsVisible(boolean visible) {
+ DownloadService downloadService = getDownloadService();
+ if(downloadService != null && downloadService.isCurrentPlayingSingle()) {
+ return;
+ }
+
try {
long duration = 1700L;
FadeOutAnimation.createAndStart(rootView.findViewById(R.id.download_overlay_buttons), !visible, duration);
@@ -899,10 +970,14 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis
private int getMinutes(int progress) {
if(progress < 30) {
return progress + 1;
- } else if(progress < 61) {
+ } 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 - 61) * 15 + getMinutes(60);
+ return (progress - 80) * 150 + getMinutes(80);
}
}
@@ -938,31 +1013,22 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis
}
}
- private void changeProgress(final int ms) {
+ private void changeProgress(final boolean rewind) {
final DownloadService downloadService = getDownloadService();
if(downloadService == null) {
return;
}
new SilentBackgroundTask<Void>(context) {
- boolean isJukeboxEnabled;
- int msPlayed;
- Integer duration;
- PlayerState playerState;
int seekTo;
@Override
protected Void doInBackground() throws Throwable {
- msPlayed = Math.max(0, downloadService.getPlayerPosition());
- duration = downloadService.getPlayerDuration();
- playerState = getDownloadService().getPlayerState();
- int msTotal = duration == null ? 0 : duration;
- if(msPlayed + ms > msTotal) {
- seekTo = msTotal;
+ if(rewind) {
+ seekTo = downloadService.rewind();
} else {
- seekTo = msPlayed + ms;
+ seekTo = downloadService.fastForward();
}
- downloadService.seekTo(seekTo);
return null;
}
@@ -1156,13 +1222,55 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis
}
@Override
- public void onSongChanged(DownloadFile currentPlaying, int currentPlayingIndex) {
+ public void onSongChanged(DownloadFile currentPlaying, int currentPlayingIndex, boolean shouldFastForward) {
this.currentPlaying = currentPlaying;
+ setupSubtitle(currentPlayingIndex);
+
+ updateMediaButton(shouldFastForward);
+ updateTitle();
+ setPlaybackSpeed();
+ }
+
+ private void updateMediaButton(boolean shouldFastForward) {
+ DownloadService downloadService = getDownloadService();
+ if(downloadService.isCurrentPlayingSingle()) {
+ previousButton.setVisibility(View.GONE);
+ nextButton.setVisibility(View.GONE);
+ rewindButton.setVisibility(View.GONE);
+ fastforwardButton.setVisibility(View.GONE);
+ } else {
+ if (downloadService.shouldFastForward()) {
+ 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);
+ }
+ }
+ }
+
+ private void setupSubtitle(int currentPlayingIndex) {
if (currentPlaying != null) {
Entry song = currentPlaying.getSong();
songTitleTextView.setText(song.getTitle());
getImageLoader().loadImage(albumArtImageView, song, true, true);
- setSubtitle(context.getResources().getString(R.string.download_playing_out_of, currentPlayingIndex + 1, currentPlayingSize));
+
+ DownloadService downloadService = getDownloadService();
+ if(downloadService.isCurrentPlayingSingle()) {
+ setSubtitle(null);
+ } else if(downloadService.isShufflePlayEnabled()) {
+ setSubtitle(context.getResources().getString(R.string.download_playerstate_playing_shuffle));
+ } else if(downloadService.isArtistRadio()) {
+ setSubtitle(context.getResources().getString(R.string.download_playerstate_playing_artist_radio));
+ } 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);
@@ -1171,7 +1279,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis
}
@Override
- public void onSongsChanged(List<DownloadFile> songs, DownloadFile currentPlaying, int currentPlayingIndex) {
+ public void onSongsChanged(List<DownloadFile> songs, DownloadFile currentPlaying, int currentPlayingIndex, boolean shouldFastForward) {
currentPlayingSize = songs.size();
DownloadService downloadService = getDownloadService();
@@ -1199,11 +1307,22 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis
scrollWhenLoaded = false;
}
- setSubtitle(context.getResources().getString(R.string.download_playing_out_of, currentPlayingIndex + 1, currentPlayingSize));
if(this.currentPlaying != currentPlaying) {
- onSongChanged(currentPlaying, currentPlayingIndex);
+ onSongChanged(currentPlaying, currentPlayingIndex, shouldFastForward);
onMetadataUpdate(currentPlaying != null ? currentPlaying.getSong() : null, DownloadService.METADATA_UPDATED_ALL);
+ } else {
+ updateMediaButton(shouldFastForward);
+ setupSubtitle(currentPlayingIndex);
+ }
+
+ if(downloadService.isCurrentPlayingSingle()) {
+ toggleListButton.setVisibility(View.GONE);
+ repeatButton.setVisibility(View.GONE);
+ } else {
+ toggleListButton.setVisibility(View.VISIBLE);
+ repeatButton.setVisibility(View.VISIBLE);
}
+ setPlaybackSpeed();
}
@Override
@@ -1258,11 +1377,16 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis
break;
default:
if(currentPlaying != null) {
- String artist = "";
- if(currentPlaying.getSong().getArtist() != null) {
- artist = currentPlaying.getSong().getArtist() + " - ";
+ 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);
}
- statusTextView.setText(artist + currentPlaying.getSong().getAlbum());
} else {
statusTextView.setText(null);
}
@@ -1334,6 +1458,10 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis
}
bookmarkButton.setImageResource(bookmark);
}
+
+ if(song != null && albumArtImageView != null && fieldChange == DownloadService.METADATA_UPDATED_COVER_ART) {
+ getImageLoader().loadImage(albumArtImageView, song, true, true);
+ }
}
public void updateRepeatButton() {
@@ -1352,4 +1480,103 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis
break;
}
}
+ private void updateTitle() {
+ DownloadService downloadService = getDownloadService();
+ float playbackSpeed = downloadService.getPlaybackSpeed();
+
+ String title = context.getResources().getString(R.string.button_bar_now_playing);
+ int stringRes = -1;
+ if(playbackSpeed == 0.5f) {
+ stringRes = R.string.download_playback_speed_half;
+ } else if(playbackSpeed == 1.5f) {
+ stringRes = R.string.download_playback_speed_one_half;
+ } else if(playbackSpeed == 2.0f) {
+ stringRes = R.string.download_playback_speed_double;
+ } else if(playbackSpeed == 3.0f) {
+ stringRes = R.string.download_playback_speed_tripple;
+ }
+
+ String playbackSpeedText = null;
+ if(stringRes != -1) {
+ playbackSpeedText = context.getResources().getString(stringRes);
+ } else if(Math.abs(playbackSpeed - 1.0) > 0.01) {
+ playbackSpeedText = Float.toString(playbackSpeed) + "x";
+ }
+
+ if(playbackSpeedText != null) {
+ title += " (" + playbackSpeedText + ")";
+ }
+ setTitle(title);
+ }
+
+ @Override
+ protected List<Entry> getSelectedEntries() {
+ List<DownloadFile> selected = getCurrentAdapter().getSelected();
+ List<Entry> entries = new ArrayList<>();
+
+ for(DownloadFile downloadFile: selected) {
+ if(downloadFile.getSong() != null) {
+ entries.add(downloadFile.getSong());
+ }
+ }
+
+ return entries;
+ }
+
+ private void setPlaybackSpeed() {
+ if (playbackSpeedButton.getVisibility() == View.GONE)
+ return;
+ speed = new DroppySpeedControl(R.layout.set_playback_speed);
+ DroppyMenuPopup.Builder builder = new DroppyMenuPopup.Builder(context,playbackSpeedButton);
+ speed.setClickable(true);
+ float playbackSpeed;
+
+ playbackSpeed = getDownloadService() != null ? getDownloadService().getPlaybackSpeed() : 1.0f;
+
+ final DroppyMenuPopup popup = builder.triggerOnAnchorClick(true).addMenuItem(speed).setPopupAnimation(new DroppyFadeInAnimation()).build();
+ speed.setOnSeekBarChangeListener(context, new DroppyClickCallbackInterface() {
+ @Override
+ public void call(View v, int id) {
+ SeekBar playbackSpeedBar = (SeekBar) v;
+ int playbackSpeed = playbackSpeedBar.getProgress() +5 ;
+ setPlaybackSpeed(playbackSpeed/10f);
+ }
+ },R.id.playback_speed_bar,R.id.playback_speed_label,playbackSpeed);
+ speed.setOnClicks(context,
+ new DroppyClickCallbackInterface() {
+ @Override
+ public void call(View v, int id) {
+ float playbackSpeed = 1.0f;
+ switch (id) {
+ case R.id.playback_speed_one_half:
+ playbackSpeed = 1.5f;
+ break;
+ case R.id.playback_speed_double:
+ playbackSpeed = 2.0f;
+ break;
+ case R.id.playback_speed_triple:
+ playbackSpeed = 3.0f;
+ break;
+ default:
+ break;
+ }
+ setPlaybackSpeed(playbackSpeed);
+ speed.updateSeekBar(playbackSpeed);
+ popup.dismiss(true);
+ }
+ }
+ ,R.id.playback_speed_normal,R.id.playback_speed_one_half,R.id.playback_speed_double,
+ R.id.playback_speed_triple);
+ speed.updateSeekBar(playbackSpeed);
+
+ }
+ private void setPlaybackSpeed(float playbackSpeed) {
+ DownloadService downloadService = getDownloadService();
+ if (downloadService == null) {
+ return;
+ }
+
+ downloadService.setPlaybackSpeed(playbackSpeed);
+ updateTitle();
+ }
}
diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SearchFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SearchFragment.java
index d21b82e0..dfff45cd 100644
--- a/app/src/main/java/github/daneren2005/dsub/fragments/SearchFragment.java
+++ b/app/src/main/java/github/daneren2005/dsub/fragments/SearchFragment.java
@@ -3,10 +3,14 @@ package github.daneren2005.dsub.fragments;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
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;
@@ -25,6 +29,7 @@ import github.daneren2005.dsub.adapter.SearchAdapter;
import github.daneren2005.dsub.adapter.SectionAdapter;
import github.daneren2005.dsub.domain.Artist;
import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.MusicDirectory.Entry;
import github.daneren2005.dsub.domain.SearchCritera;
import github.daneren2005.dsub.domain.SearchResult;
import github.daneren2005.dsub.service.MusicService;
@@ -39,9 +44,9 @@ import github.daneren2005.dsub.view.UpdateView;
public class SearchFragment extends SubsonicFragment implements SectionAdapter.OnItemClickedListener<Serializable> {
private static final String TAG = SearchFragment.class.getSimpleName();
- private static final int MAX_ARTISTS = 10;
- private static final int MAX_ALBUMS = 10;
- private static final int MAX_SONGS = 25;
+ 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;
@@ -108,13 +113,13 @@ public class SearchFragment extends SubsonicFragment implements SectionAdapter.O
}
@Override
- public GridLayoutManager.SpanSizeLookup getSpanSizeLookup() {
+ 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 getRecyclerColumnCount();
+ return gridLayoutManager.getSpanCount();
} else {
return 1;
}
@@ -125,24 +130,13 @@ public class SearchFragment extends SubsonicFragment implements SectionAdapter.O
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) {
menuInflater.inflate(R.menu.search, menu);
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case R.id.menu_search:
- context.startSearch(currentQuery, false, null, false);
- return true;
- }
-
- return super.onOptionsItemSelected(item);
-
+ onFinishSetupOptionsMenu(menu);
}
@Override
public void onCreateContextMenu(Menu menu, MenuInflater menuInflater, UpdateView<Serializable> updateView, Serializable item) {
onCreateContextMenuSupport(menu, menuInflater, updateView, item);
- if(item instanceof MusicDirectory.Entry && !((MusicDirectory.Entry) item).isVideo() && !Util.isOffline(context)) {
+ if(item instanceof Entry && !((Entry) item).isVideo() && !Util.isOffline(context)) {
menu.removeItem(R.id.song_menu_remove_playlist);
}
recreateContextMenu(menu);
@@ -162,8 +156,8 @@ public class SearchFragment extends SubsonicFragment implements SectionAdapter.O
public void onItemClicked(UpdateView<Serializable> updateView, Serializable item) {
if (item instanceof Artist) {
onArtistSelected((Artist) item, false);
- } else if (item instanceof MusicDirectory.Entry) {
- MusicDirectory.Entry entry = (MusicDirectory.Entry) item;
+ } else if (item instanceof Entry) {
+ Entry entry = (Entry) item;
if (entry.isDirectory()) {
onAlbumSelected(entry, false);
} else if (entry.isVideo()) {
@@ -175,19 +169,24 @@ public class SearchFragment extends SubsonicFragment implements SectionAdapter.O
}
@Override
- protected List<MusicDirectory.Entry> getSelectedEntries() {
+ protected List<Entry> getSelectedEntries() {
List<Serializable> selected = adapter.getSelected();
- List<MusicDirectory.Entry> selectedMedia = new ArrayList<>();
+ List<Entry> selectedMedia = new ArrayList<>();
for(Serializable ser: selected) {
- if(ser instanceof MusicDirectory.Entry) {
- selectedMedia.add((MusicDirectory.Entry) ser);
+ if(ser instanceof Entry) {
+ selectedMedia.add((Entry) ser);
}
}
return selectedMedia;
}
- public void search(final String query, final boolean autoplay) {
+ @Override
+ protected boolean isShowArtistEnabled() {
+ return true;
+ }
+
+ public void search(final String query, final boolean autoplay, final String artist, final String album, final String title) {
if(skipSearch) {
skipSearch = false;
return;
@@ -207,12 +206,20 @@ public class SearchFragment extends SubsonicFragment implements SectionAdapter.O
searchResult = result;
recyclerView.setAdapter(adapter = new SearchAdapter(context, searchResult, getImageLoader(), largeAlbums, SearchFragment.this));
if (autoplay) {
- autoplay(query);
+ autoplay(query, artist, album, title);
}
}
};
task.execute();
+
+ if(searchItem != null) {
+ MenuItemCompat.collapseActionView(searchItem);
+ }
+ }
+
+ protected String getCurrentQuery() {
+ return currentQuery;
}
private void onArtistSelected(Artist artist, boolean autoplay) {
@@ -229,7 +236,7 @@ public class SearchFragment extends SubsonicFragment implements SectionAdapter.O
replaceFragment(fragment);
}
- private void onAlbumSelected(MusicDirectory.Entry album, boolean autoplay) {
+ private void onAlbumSelected(Entry album, boolean autoplay) {
SubsonicFragment fragment = new SelectDirectoryFragment();
Bundle args = new Bundle();
args.putString(Constants.INTENT_EXTRA_NAME_ID, album.getId());
@@ -242,7 +249,7 @@ public class SearchFragment extends SubsonicFragment implements SectionAdapter.O
replaceFragment(fragment);
}
- private void onSongSelected(MusicDirectory.Entry song, boolean save, boolean append, boolean autoplay, boolean playNext) {
+ private void onSongSelected(Entry song, boolean save, boolean append, boolean autoplay, boolean playNext) {
DownloadService downloadService = getDownloadService();
if (downloadService != null) {
if (!append) {
@@ -257,7 +264,7 @@ public class SearchFragment extends SubsonicFragment implements SectionAdapter.O
}
}
- private void onVideoSelected(MusicDirectory.Entry entry) {
+ private void onVideoSelected(Entry entry) {
int maxBitrate = Util.getMaxVideoBitrate(context);
Intent intent = new Intent(Intent.ACTION_VIEW);
@@ -265,6 +272,55 @@ public class SearchFragment extends SubsonicFragment implements SectionAdapter.O
startActivity(intent);
}
+ private void autoplay(String query, String artistQuery, String albumQuery, String titleQuery) {
+ Log.i(TAG, "Query: '" + query + "' ( Artist: '" + artistQuery + "', Album: '" + albumQuery + "', Title: '" + titleQuery + "')");
+
+ if(titleQuery != null && !searchResult.getSongs().isEmpty()) {
+ titleQuery = titleQuery.toLowerCase();
+
+ TreeMap<Integer, Entry> tree = new TreeMap<>();
+ for(Entry song: searchResult.getSongs()) {
+ tree.put(Util.getStringDistance(song.getTitle().toLowerCase(), titleQuery), song);
+ }
+
+ Map.Entry<Integer, Entry> entry = tree.firstEntry();
+ if(entry.getKey() <= MIN_CLOSENESS) {
+ onSongSelected(entry.getValue(), false, false, true, false);
+ } else {
+ autoplay(query);
+ }
+ } else if(albumQuery != null && !searchResult.getAlbums().isEmpty()) {
+ albumQuery = albumQuery.toLowerCase();
+
+ TreeMap<Integer, Entry> tree = new TreeMap<>();
+ for(Entry album: searchResult.getAlbums()) {
+ tree.put(Util.getStringDistance(album.getTitle().toLowerCase(), albumQuery), album);
+ }
+
+ Map.Entry<Integer, Entry> entry = tree.firstEntry();
+ if(entry.getKey() <= MIN_CLOSENESS) {
+ onAlbumSelected(entry.getValue(), true);
+ } else {
+ autoplay(query);
+ }
+ } else if(artistQuery != null && !searchResult.getArtists().isEmpty()) {
+ artistQuery = artistQuery.toLowerCase();
+
+ TreeMap<Integer, Artist> tree = new TreeMap<>();
+ for(Artist artist: searchResult.getArtists()) {
+ tree.put(Util.getStringDistance(artist.getName().toLowerCase(), artistQuery), artist);
+ }
+ Map.Entry<Integer, Artist> entry = tree.firstEntry();
+ if(entry.getKey() <= MIN_CLOSENESS) {
+ onArtistSelected(entry.getValue(), true);
+ } else {
+ autoplay(query);
+ }
+ } else {
+ autoplay(query);
+ }
+ }
+
private void autoplay(String query) {
query = query.toLowerCase();
@@ -273,12 +329,12 @@ public class SearchFragment extends SubsonicFragment implements SectionAdapter.O
artist = searchResult.getArtists().get(0);
artist.setCloseness(Util.getStringDistance(artist.getName().toLowerCase(), query));
}
- MusicDirectory.Entry album = null;
+ Entry album = null;
if(!searchResult.getAlbums().isEmpty()) {
album = searchResult.getAlbums().get(0);
album.setCloseness(Util.getStringDistance(album.getTitle().toLowerCase(), query));
}
- MusicDirectory.Entry song = null;
+ Entry song = null;
if(!searchResult.getSongs().isEmpty()) {
song = searchResult.getSongs().get(0);
song.setCloseness(Util.getStringDistance(song.getTitle().toLowerCase(), query));
@@ -286,10 +342,10 @@ public class SearchFragment extends SubsonicFragment implements SectionAdapter.O
if(artist != null && (artist.getCloseness() <= MIN_CLOSENESS ||
(album == null || artist.getCloseness() <= album.getCloseness()) &&
- (song == null || artist.getCloseness() <= song.getCloseness()))) {
+ (song == null || artist.getCloseness() <= song.getCloseness()))) {
onArtistSelected(artist, true);
} else if(album != null && (album.getCloseness() <= MIN_CLOSENESS ||
- song == null || album.getCloseness() <= song.getCloseness())) {
+ 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/daneren2005/dsub/fragments/SelectArtistFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SelectArtistFragment.java
index 3df0a9a9..e971bfb6 100644
--- a/app/src/main/java/github/daneren2005/dsub/fragments/SelectArtistFragment.java
+++ b/app/src/main/java/github/daneren2005/dsub/fragments/SelectArtistFragment.java
@@ -17,6 +17,7 @@ import github.daneren2005.dsub.adapter.SectionAdapter;
import github.daneren2005.dsub.domain.Artist;
import github.daneren2005.dsub.domain.Indexes;
import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.MusicDirectory.Entry;
import github.daneren2005.dsub.domain.MusicFolder;
import github.daneren2005.dsub.domain.ServerInfo;
import github.daneren2005.dsub.service.MusicService;
@@ -29,12 +30,11 @@ import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
-public class SelectArtistFragment extends SelectRecyclerFragment<Artist> implements ArtistAdapter.OnMusicFolderChanged {
+public class SelectArtistFragment extends SelectRecyclerFragment<Serializable> implements ArtistAdapter.OnMusicFolderChanged {
private static final String TAG = SelectArtistFragment.class.getSimpleName();
- private static final int MENU_GROUP_MUSIC_FOLDER = 10;
private List<MusicFolder> musicFolders = null;
- private List<MusicDirectory.Entry> entries;
+ private List<Entry> entries;
private String groupId;
private String groupName;
@@ -63,12 +63,14 @@ public class SelectArtistFragment extends SelectRecyclerFragment<Artist> impleme
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
Bundle args = getArguments();
if(args != null) {
- groupId = args.getString(Constants.INTENT_EXTRA_NAME_ID);
- groupName = args.getString(Constants.INTENT_EXTRA_NAME_NAME);
+ if(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();
+ if (groupName != null) {
+ setTitle(groupName);
+ context.invalidateOptionsMenu();
+ }
}
}
@@ -78,47 +80,56 @@ public class SelectArtistFragment extends SelectRecyclerFragment<Artist> impleme
}
@Override
- public void onCreateContextMenu(Menu menu, MenuInflater menuInflater, UpdateView<Artist> updateView, Artist item) {
+ public void onCreateContextMenu(Menu menu, MenuInflater menuInflater, UpdateView<Serializable> updateView, Serializable item) {
onCreateContextMenuSupport(menu, menuInflater, updateView, item);
recreateContextMenu(menu);
}
@Override
- public boolean onContextItemSelected(MenuItem menuItem, UpdateView<Artist> updateView, Artist item) {
+ public boolean onContextItemSelected(MenuItem menuItem, UpdateView<Serializable> updateView, Serializable item) {
return onContextItemSelected(menuItem, item);
}
@Override
- public void onItemClicked(UpdateView<Artist> updateView, Artist artist) {
+ public void onItemClicked(UpdateView<Serializable> updateView, Serializable item) {
SubsonicFragment fragment;
- if((Util.isFirstLevelArtist(context) || Util.isOffline(context) || Util.isTagBrowsing(context)) || "root".equals(artist.getId()) || groupId != null) {
- fragment = new SelectDirectoryFragment();
- Bundle args = new Bundle();
- args.putString(Constants.INTENT_EXTRA_NAME_ID, artist.getId());
- args.putString(Constants.INTENT_EXTRA_NAME_NAME, artist.getName());
-
- if ("root".equals(artist.getId())) {
- args.putSerializable(Constants.FRAGMENT_LIST, (Serializable) entries);
- }
- if(ServerInfo.checkServerVersion(context, "1.13") && !Util.isOffline(context)) {
- args.putSerializable(Constants.INTENT_EXTRA_NAME_DIRECTORY, new MusicDirectory.Entry(artist));
+ 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 (ServerInfo.checkServerVersion(context, "1.13") && !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 (ServerInfo.checkServerVersion(context, "1.13") && !Util.isOffline(context)) {
+ args.putSerializable(Constants.INTENT_EXTRA_NAME_DIRECTORY, new Entry(artist));
+ }
+
+ fragment.setArguments(args);
}
- args.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, true);
- fragment.setArguments(args);
+ replaceFragment(fragment);
} 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());
- if(ServerInfo.checkServerVersion(context, "1.13") && !Util.isOffline(context)) {
- args.putSerializable(Constants.INTENT_EXTRA_NAME_DIRECTORY, new MusicDirectory.Entry(artist));
+ Entry entry = (Entry) item;
+ if (entry.isVideo()) {
+ playVideo(entry);
+ } else {
+ onSongPress(entries, entry);
}
-
- fragment.setArguments(args);
}
-
- replaceFragment(fragment);
}
@Override
@@ -155,15 +166,15 @@ public class SelectArtistFragment extends SelectRecyclerFragment<Artist> impleme
}
@Override
- public SectionAdapter getAdapter(List<Artist> objects) {
+ public SectionAdapter getAdapter(List<Serializable> objects) {
return new ArtistAdapter(context, objects, musicFolders, this, this);
}
@Override
- public List<Artist> getObjects(MusicService musicService, boolean refresh, ProgressListener listener) throws Exception {
- List<Artist> artists;
+ public List<Serializable> getObjects(MusicService musicService, boolean refresh, ProgressListener listener) throws Exception {
+ List<Serializable> items;
if(groupId == null) {
- if (!Util.isOffline(context) && !Util.isTagBrowsing(context)) {
+ if (!Util.isOffline(context) && (!Util.isTagBrowsing(context) || ServerInfo.checkServerVersion(context, "1.14"))) {
musicFolders = musicService.getMusicFolders(refresh, context, listener);
// Hide folders option if there is only one
@@ -178,14 +189,16 @@ public class SelectArtistFragment extends SelectRecyclerFragment<Artist> impleme
Indexes indexes = musicService.getIndexes(musicFolderId, refresh, context, listener);
indexes.sortChildren(context);
- artists = new ArrayList<>(indexes.getShortcuts().size() + indexes.getArtists().size());
- artists.addAll(indexes.getShortcuts());
- artists.addAll(indexes.getArtists());
+ items = new ArrayList<>(indexes.getShortcuts().size() + indexes.getArtists().size());
+ items.addAll(indexes.getShortcuts());
+ items.addAll(indexes.getArtists());
entries = indexes.getEntries();
+ items.addAll(entries);
} else {
- artists = new ArrayList<>();
+ List<Artist> artists = new ArrayList<>();
+ items = new ArrayList<>();
MusicDirectory dir = musicService.getMusicDirectory(groupId, groupName, refresh, context, listener);
- for(MusicDirectory.Entry entry: dir.getChildren(true, false)) {
+ for(Entry entry: dir.getChildren(true, false)) {
Artist artist = new Artist();
artist.setId(entry.getId());
artist.setName(entry.getTitle());
@@ -193,21 +206,17 @@ public class SelectArtistFragment extends SelectRecyclerFragment<Artist> impleme
artists.add(artist);
}
- entries = new ArrayList<>();
- entries.addAll(dir.getChildren(false, true));
- if(!entries.isEmpty()) {
- Artist root = new Artist();
- root.setId("root");
- root.setName("Root");
- root.setIndex("#");
- artists.add(root);
- }
-
- Indexes indexes = new Indexes(0, artists, new ArrayList<Artist>());
+ Indexes indexes = new Indexes(0, new ArrayList<Artist>(), artists);
indexes.sortChildren(context);
+ items.addAll(indexes.getArtists());
+
+ entries = dir.getChildren(false, true);
+ for(Entry entry: entries) {
+ items.add(entry);
+ }
}
- return artists;
+ return items;
}
@Override
diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SelectBookmarkFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SelectBookmarkFragment.java
index 5f3ca38b..c320f3c1 100644
--- a/app/src/main/java/github/daneren2005/dsub/fragments/SelectBookmarkFragment.java
+++ b/app/src/main/java/github/daneren2005/dsub/fragments/SelectBookmarkFragment.java
@@ -18,6 +18,7 @@
*/
package github.daneren2005.dsub.fragments;
+import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
@@ -28,6 +29,7 @@ import github.daneren2005.dsub.domain.Bookmark;
import github.daneren2005.dsub.domain.MusicDirectory;
import github.daneren2005.dsub.service.DownloadService;
import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.util.Constants;
import github.daneren2005.dsub.util.MenuUtil;
import github.daneren2005.dsub.util.ProgressListener;
import github.daneren2005.dsub.util.SilentBackgroundTask;
@@ -89,19 +91,29 @@ public class SelectBookmarkFragment extends SelectRecyclerFragment<MusicDirector
return;
}
- new SilentBackgroundTask<Void>(context) {
- @Override
- protected Void doInBackground() throws Throwable {
- downloadService.clear();
- downloadService.download(Arrays.asList(bookmark), false, true, false, false, 0, bookmark.getBookmark().getPosition());
- return null;
- }
-
- @Override
- protected void done(Void result) {
- context.openNowPlaying();
- }
- }.execute();
+ boolean allowPlayAll = ((!Util.isTagBrowsing(context) && bookmark.getParent() != null) || (Util.isTagBrowsing(context) && bookmark.getAlbumId() != null)) && !bookmark.isPodcast();
+ if(allowPlayAll && "all".equals(Util.getSongPressAction(context))) {
+ new RecursiveLoader(context) {
+ @Override
+ protected Boolean doInBackground() throws Throwable {
+ getSiblingsRecursively(bookmark);
+
+ if(songs.isEmpty() || !songs.contains(bookmark)) {
+ playNowInTask(Arrays.asList(bookmark), bookmark, bookmark.getBookmark().getPosition());
+ } else {
+ playNowInTask(songs, bookmark, bookmark.getBookmark().getPosition());
+ }
+ return null;
+ }
+
+ @Override
+ protected void done(Boolean result) {
+ context.openNowPlaying();
+ }
+ }.execute();
+ } else {
+ onSongPress(Arrays.asList(bookmark), bookmark, bookmark.getBookmark().getPosition(), false);
+ }
}
private void displayBookmarkInfo(final MusicDirectory.Entry entry) {
diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SelectDirectoryFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SelectDirectoryFragment.java
index d2282117..d3a0bfe8 100644
--- a/app/src/main/java/github/daneren2005/dsub/fragments/SelectDirectoryFragment.java
+++ b/app/src/main/java/github/daneren2005/dsub/fragments/SelectDirectoryFragment.java
@@ -39,6 +39,7 @@ import github.daneren2005.dsub.domain.ArtistInfo;
import github.daneren2005.dsub.domain.MusicDirectory;
import github.daneren2005.dsub.domain.ServerInfo;
import github.daneren2005.dsub.domain.Share;
+import github.daneren2005.dsub.service.CachedMusicService;
import github.daneren2005.dsub.service.DownloadService;
import github.daneren2005.dsub.util.DrawableTint;
import github.daneren2005.dsub.util.ImageLoader;
@@ -86,6 +87,11 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section
private ArtistInfo artistInfo;
private String artistInfoDelayed;
+ private SilentBackgroundTask updateCoverArtTask;
+ private ImageView coverArtView;
+ private Entry coverArtRep;
+ private String coverArtId;
+
String id;
String name;
Entry directory;
@@ -184,27 +190,7 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section
recyclerView.setHasFixedSize(true);
fastScroller = (FastScroller) rootView.findViewById(R.id.fragment_fast_scroller);
setupScrollList(recyclerView);
-
- if(largeAlbums) {
- GridLayoutManager gridLayoutManager = new GridLayoutManager(context, getRecyclerColumnCount());
- gridLayoutManager.setSpanSizeLookup(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 getRecyclerColumnCount();
- } else {
- return 1;
- }
- }
- });
- recyclerView.addItemDecoration(new GridSpacingDecoration());
- recyclerView.setLayoutManager(gridLayoutManager);
- } else {
- LinearLayoutManager layoutManager = new LinearLayoutManager(context);
- layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
- recyclerView.setLayoutManager(layoutManager);
- }
+ setupLayoutManager(recyclerView, largeAlbums);
if(entries == null) {
if(primaryFragment || secondaryFragment) {
@@ -248,7 +234,7 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section
if(!ServerInfo.hasTopSongs(context)) {
menu.removeItem(R.id.menu_top_tracks);
}
- if(!ServerInfo.checkServerVersion(context, "1.11") || (id != null && "root".equals(id))) {
+ if(!ServerInfo.checkServerVersion(context, "1.11")) {
menu.removeItem(R.id.menu_radio);
menu.removeItem(R.id.menu_similar_artists);
} else if(!ServerInfo.hasSimilarArtists(context)) {
@@ -305,9 +291,6 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section
case R.id.menu_show_all:
setShowAll();
return true;
- case R.id.menu_unstar:
- unstarSelected();
- return true;
case R.id.menu_top_tracks:
showTopTracks();
return true;
@@ -329,10 +312,6 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section
if(!entry.isVideo() && !Util.isOffline(context) && (playlistId == null || !playlistOwner) && (podcastId == null || Util.isOffline(context) && podcastId != null)) {
menu.removeItem(R.id.song_menu_remove_playlist);
}
- // Remove show artists if parent is not set and if not on a album list
- if((albumListType == null || (entry.getParent() == null && entry.getArtistId() == null)) && !Util.isOffline(context)) {
- menu.removeItem(R.id.album_menu_show_artist);
- }
recreateContextMenu(menu);
}
@@ -380,28 +359,20 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section
return;
}
- playNow(Arrays.asList(entry));
+ onSongPress(Arrays.asList(entry), entry, false);
} else {
- List<Entry> songs = new ArrayList<Entry>();
-
- if(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_PLAY_NOW_AFTER, true)) {
- Iterator it = entries.listIterator(entries.indexOf(entry));
- while(it.hasNext()) {
- songs.add((Entry) it.next());
- }
- } else {
- songs.add(entry);
- }
-
- playNow(songs);
+ onSongPress(entries, entry, albumListType == null || "starred".equals(albumListType));
}
}
@Override
protected void refresh(boolean refresh) {
- if(!"root".equals(id)) {
- load(refresh);
- }
+ load(refresh);
+ }
+
+ @Override
+ protected boolean isShowArtistEnabled() {
+ return albumListType != null;
}
private void load(boolean refresh) {
@@ -488,8 +459,12 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section
}
List<Entry> songs = new ArrayList<Entry>();
getSongsRecursively(root, songs);
- root.replaceChildren(songs);
- return root;
+
+ // CachedMusicService is refreshing this data in the background, so will wipe out the songs list from root
+ MusicDirectory clonedRoot = new MusicDirectory(songs);
+ clonedRoot.setId(root.getId());
+ clonedRoot.setName(root.getName());
+ return clonedRoot;
}
private void getSongsRecursively(MusicDirectory parent, List<Entry> songs) throws Exception {
@@ -577,6 +552,14 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section
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) {
@@ -593,6 +576,8 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section
}
} 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);
}
@@ -657,9 +642,19 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section
}
@Override
- public void updateCache() {
- if(entryGridAdapter != null) {
+ 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();
+ }
+ }
}
}
}
@@ -669,6 +664,21 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section
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) {
@@ -682,7 +692,6 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section
if(albumListType == null || "starred".equals(albumListType)) {
entryGridAdapter = new EntryGridAdapter(context, entries, getImageLoader(), largeAlbums);
entryGridAdapter.setRemoveFromPlaylist(playlistId != null);
- entryGridAdapter.setRemoveStarred(albumListType == null);
} else {
if("alphabeticalByName".equals(albumListType)) {
entryGridAdapter = new AlphabeticalAlbumAdapter(context, entries, getImageLoader(), largeAlbums);
@@ -728,11 +737,14 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section
if(!artist) {
entryGridAdapter.setShowArtist(true);
}
+ if(topTracks || showAll) {
+ entryGridAdapter.setShowAlbum(true);
+ }
// Show header if not album list type and not root and not artist
// For Subsonic 5.1+ display a header for artists with getArtistInfo data if it exists
boolean addedHeader = false;
- if(albumListType == null && !"root".equals(id) && (!artist || artistInfo != null || artistInfoDelayed != null) && (share == null || entries.size() != albums.size())) {
+ if(albumListType == null && (!artist || artistInfo != null || artistInfoDelayed != null) && (share == null || entries.size() != albums.size())) {
View header = createHeader();
if(header != null) {
@@ -792,7 +804,7 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section
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);
+ playAll(args.getBoolean(Constants.INTENT_EXTRA_NAME_SHUFFLE, false), false, false);
}
}
@@ -802,20 +814,19 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section
if(!songs.isEmpty()) {
download(songs, append, false, !append, playNext, shuffle);
entryGridAdapter.clearSelected();
- }
- else {
- playAll(shuffle, append);
+ } else {
+ playAll(shuffle, append, playNext);
}
}
- private void playAll(final boolean shuffle, final boolean append) {
+ private void playAll(final boolean shuffle, final boolean append, final boolean playNext) {
boolean hasSubFolders = albums != null && !albums.isEmpty();
if (hasSubFolders && (id != null || share != null || "starred".equals(albumListType))) {
- downloadRecursively(id, false, append, !append, shuffle, false);
+ downloadRecursively(id, false, append, !append, shuffle, false, playNext);
} else if(hasSubFolders && albumListType != null) {
- downloadRecursively(albums, shuffle, append);
+ downloadRecursively(albums, shuffle, append, playNext);
} else {
- download(entries, append, false, !append, false, shuffle);
+ download(entries, append, false, !append, playNext, shuffle);
}
}
@@ -909,7 +920,7 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section
for(Integer index: indexes) {
entryGridAdapter.removeAt(index);
}
- Util.toast(context, context.getResources().getString(R.string.removed_playlist, indexes.size(), name));
+ Util.toast(context, context.getResources().getString(R.string.removed_playlist, String.valueOf(indexes.size()), name));
}
@Override
@@ -953,70 +964,28 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section
}.execute();
}
- public void unstarSelected() {
- List<Entry> selected = getSelectedEntries();
- if(selected.size() == 0) {
- selected = entries;
- }
- if(selected.size() == 0) {
- return;
- }
- final List<Entry> unstar = new ArrayList<Entry>();
- unstar.addAll(selected);
+ @Override
+ protected void toggleSelectedStarred() {
+ UpdateHelper.OnStarChange onStarChange = null;
+ if(albumListType != null && "starred".equals(albumListType)) {
+ onStarChange = new UpdateHelper.OnStarChange() {
+ @Override
+ public void starChange(boolean starred) {
- new LoadingTask<Void>(context, true) {
- @Override
- protected Void doInBackground() throws Throwable {
- MusicService musicService = MusicServiceFactory.getMusicService(context);
- List<Entry> entries = new ArrayList<Entry>();
- List<Entry> artists = new ArrayList<Entry>();
- List<Entry> albums = new ArrayList<Entry>();
- for(Entry entry: unstar) {
- if(entry.isDirectory() && Util.isTagBrowsing(context)) {
- if(entry.isAlbum()) {
- albums.add(entry);
- } else {
- artists.add(entry);
- }
- } else {
- entries.add(entry);
- }
}
- musicService.setStarred(entries, artists, albums, false, this, context);
- for(Entry entry: unstar) {
- new UpdateHelper.EntryInstanceUpdater(entry) {
- @Override
- public void update(Entry found) {
- found.setStarred(false);
+ @Override
+ public void starCommited(boolean starred) {
+ if(!starred) {
+ for (Entry entry : entries) {
+ entryGridAdapter.removeItem(entry);
}
- }.execute();
- }
-
- return null;
- }
-
- @Override
- protected void done(Void result) {
- Util.toast(context, context.getResources().getString(R.string.starring_content_unstarred, Integer.toString(unstar.size())));
-
- for(Entry entry: unstar) {
- entryGridAdapter.removeItem(entry);
- }
- }
-
- @Override
- protected void error(Throwable error) {
- String msg;
- if (error instanceof OfflineException || error instanceof ServerTooOldException) {
- msg = getErrorMessage(error);
- } else {
- msg = context.getResources().getString(R.string.starring_content_error, Integer.toString(unstar.size())) + " " + getErrorMessage(error);
+ }
}
+ };
+ }
- Util.toast(context, msg, false);
- }
- }.execute();
+ UpdateHelper.toggleStarred(context, getSelectedEntries(), onStarChange);
}
private void checkLicenseAndTrialPeriod(LoadingTask onValid) {
@@ -1105,11 +1074,8 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section
@Override
protected Void doInBackground() throws Throwable {
DownloadService downloadService = getDownloadService();
+ downloadService.clear();
downloadService.setArtistRadio(artistId);
- if(downloadService.size() == 0) {
- Log.e(TAG, "Failed to create artist radio");
- throw new Exception("Failed to create artist radio");
- }
return null;
}
@@ -1159,22 +1125,22 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section
});
imageLoader.loadImage(coverArtView, url, false);
} else if(entries.size() > 0) {
- Entry coverArt = null;
- for (int i = 0; (i < 3) && (coverArt == null || coverArt.getCoverArt() == null); i++) {
- coverArt = entries.get(random.nextInt(entries.size()));
+ coverArtRep = null;
+ this.coverArtView = coverArtView;
+ for (int i = 0; (i < 3) && (coverArtRep == null || coverArtRep.getCoverArt() == null); i++) {
+ coverArtRep = entries.get(random.nextInt(entries.size()));
}
- final Entry albumRep = coverArt;
coverArtView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
- if (albumRep.getCoverArt() == null) {
+ if (coverArtRep == null || coverArtRep.getCoverArt() == null) {
return;
}
AlertDialog.Builder builder = new AlertDialog.Builder(context);
ImageView fullScreenView = new ImageView(context);
- imageLoader.loadImage(fullScreenView, albumRep, true, true);
+ imageLoader.loadImage(fullScreenView, coverArtRep, true, true);
builder.setCancelable(true);
AlertDialog imageDialog = builder.create();
@@ -1183,7 +1149,10 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section
imageDialog.show();
}
});
- imageLoader.loadImage(coverArtView, albumRep, false, true);
+ synchronized (coverArtRep) {
+ coverArtId = coverArtRep.getCoverArt();
+ updateCoverArtTask = imageLoader.loadImage(coverArtView, coverArtRep, false, true);
+ }
}
coverArtView.setOnInvalidated(new RecyclingImageView.OnInvalidated() {
@@ -1349,6 +1318,11 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section
starButton.setImageResource(DrawableTint.getDrawableRes(context, R.attr.star_outline));
}
}
+
+ @Override
+ public void starCommited(boolean starred) {
+
+ }
});
}
});
diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SelectInternetRadioStationFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SelectInternetRadioStationFragment.java
new file mode 100644
index 00000000..74c4b269
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/fragments/SelectInternetRadioStationFragment.java
@@ -0,0 +1,157 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2016 (C) Scott Jackson
+*/
+package github.daneren2005.dsub.fragments;
+
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.adapter.InternetRadioStationAdapter;
+import github.daneren2005.dsub.adapter.SectionAdapter;
+import github.daneren2005.dsub.domain.InternetRadioStation;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.util.ProgressListener;
+import github.daneren2005.dsub.util.TabBackgroundTask;
+import github.daneren2005.dsub.util.Util;
+import github.daneren2005.dsub.view.UpdateView;
+
+public class SelectInternetRadioStationFragment extends SelectRecyclerFragment<InternetRadioStation> {
+ private static final String TAG = SelectInternetRadioStationFragment.class.getSimpleName();
+
+ @Override
+ public int getOptionsMenu() {
+ return R.menu.abstract_top_menu;
+ }
+
+ @Override
+ public SectionAdapter<InternetRadioStation> getAdapter(List<InternetRadioStation> objs) {
+ return new InternetRadioStationAdapter(context, objs, this);
+ }
+
+ @Override
+ public List<InternetRadioStation> getObjects(MusicService musicService, boolean refresh, ProgressListener listener) throws Exception {
+ return musicService.getInternetRadioStations(refresh, context, listener);
+ }
+
+ @Override
+ public int getTitleResource() {
+ return R.string.button_bar_internet_radio;
+ }
+
+ @Override
+ public void onItemClicked(UpdateView<InternetRadioStation> updateView, final InternetRadioStation item) {
+ new TabBackgroundTask<Void>(this) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ DownloadService downloadService = getDownloadService();
+ if(downloadService == null) {
+ return null;
+ }
+
+ getStreamFromPlaylist(item);
+ downloadService.download(item);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ context.openNowPlaying();
+ }
+ }.execute();
+ }
+
+ @Override
+ public void onCreateContextMenu(Menu menu, MenuInflater menuInflater, UpdateView<InternetRadioStation> updateView, InternetRadioStation item) {
+ menuInflater.inflate(R.menu.select_internet_radio_context, menu);
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem menuItem, UpdateView<InternetRadioStation> updateView, InternetRadioStation item) {
+ switch (menuItem.getItemId()) {
+ case R.id.internet_radio_info:
+ displayInternetRadioStationInfo(item);
+ break;
+ }
+
+ return false;
+ }
+
+ private void getStreamFromPlaylist(InternetRadioStation internetRadioStation) {
+ if(internetRadioStation.getStreamUrl() != null && (internetRadioStation.getStreamUrl().indexOf(".m3u") != -1 || internetRadioStation.getStreamUrl().indexOf(".pls") != -1)) {
+ try {
+ URL url = new URL(internetRadioStation.getStreamUrl());
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+
+ try {
+ BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
+ String line;
+ while((line = in.readLine()) != null) {
+ // Not blank line or comment
+ if(line.length() > 0 && line.indexOf('#') != 0) {
+ if(internetRadioStation.getStreamUrl().indexOf(".m3u") != -1) {
+ internetRadioStation.setStreamUrl(line);
+ break;
+ } else {
+ if(line.indexOf("File1=") == 0) {
+ internetRadioStation.setStreamUrl(line.replace("File1=", ""));
+ } else if(line.indexOf("Title1=") == 0) {
+ internetRadioStation.setTitle(line.replace("Title1=", ""));
+ }
+ }
+ }
+ }
+ } finally {
+ connection.disconnect();
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to get stream data from playlist", e);
+ }
+
+ }
+ }
+
+ private void displayInternetRadioStationInfo(final InternetRadioStation station) {
+ List<Integer> headers = new ArrayList<>();
+ List<String> details = new ArrayList<>();
+
+ headers.add(R.string.details_title);
+ details.add(station.getTitle());
+
+ headers.add(R.string.details_home_page);
+ details.add(station.getHomePageUrl());
+
+ headers.add(R.string.details_stream_url);
+ details.add(station.getStreamUrl());
+
+ Util.showDetailsDialog(context, R.string.details_title_internet_radio_station, headers, details);
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SelectPlaylistFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SelectPlaylistFragment.java
index 6e2c9da5..5cb413fe 100644
--- a/app/src/main/java/github/daneren2005/dsub/fragments/SelectPlaylistFragment.java
+++ b/app/src/main/java/github/daneren2005/dsub/fragments/SelectPlaylistFragment.java
@@ -1,5 +1,8 @@
package github.daneren2005.dsub.fragments;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
import android.support.v7.app.AlertDialog;
import android.content.DialogInterface;
import android.content.res.Resources;
@@ -184,6 +187,22 @@ public class SelectPlaylistFragment extends SelectRecyclerFragment<Playlist> {
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
@@ -326,7 +345,24 @@ public class SelectPlaylistFragment extends SelectRecyclerFragment<Playlist> {
private void syncPlaylist(Playlist playlist) {
SyncUtil.addSyncedPlaylist(context, playlist.getId());
- downloadPlaylist(playlist.getId(), playlist.getName(), true, true, false, false, true);
+
+ boolean syncImmediately;
+ if(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_SYNC_WIFI, true)) {
+ ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo networkInfo = manager.getActiveNetworkInfo();
+
+ if(networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
+ syncImmediately = true;
+ } else {
+ syncImmediately = false;
+ }
+ } else {
+ syncImmediately = true;
+ }
+
+ if(syncImmediately) {
+ downloadPlaylist(playlist.getId(), playlist.getName(), true, true, false, false, true);
+ }
}
private void stopSyncPlaylist(final Playlist playlist) {
diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SelectPodcastsFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SelectPodcastsFragment.java
index 3f8f7844..9011b4c5 100644
--- a/app/src/main/java/github/daneren2005/dsub/fragments/SelectPodcastsFragment.java
+++ b/app/src/main/java/github/daneren2005/dsub/fragments/SelectPodcastsFragment.java
@@ -154,25 +154,18 @@ public class SelectPodcastsFragment extends SelectRecyclerFragment<Serializable>
if(newestEpisodes == null || newestEpisodes.getChildrenSize() == 0) {
return new PodcastChannelAdapter(context, channels, hasCoverArt ? getImageLoader() : null, this, largeAlbums);
} else {
- List<String> headers = Arrays.asList(PodcastChannelAdapter.EPISODE_HEADER, PodcastChannelAdapter.CHANNEL_HEADER);
+ Resources res = context.getResources();
+ List<String> headers = Arrays.asList(res.getString(R.string.main_albums_newest), res.getString(R.string.select_podcasts_channels));
List<MusicDirectory.Entry> episodes = newestEpisodes.getChildren(false, true);
List<Serializable> serializableEpisodes = new ArrayList<>();
-
- // Put 3 in current list
- while(serializableEpisodes.size() < 3 && !episodes.isEmpty()) {
- serializableEpisodes.add(episodes.remove(0));
- }
-
- // Put rest in extra set
- List<Serializable> extraEpisodes = new ArrayList<>();
- extraEpisodes.addAll(episodes);
+ serializableEpisodes.addAll(episodes);
List<List<Serializable>> sections = new ArrayList<>();
sections.add(serializableEpisodes);
sections.add(channels);
- return new PodcastChannelAdapter(context, headers, sections, extraEpisodes, ServerInfo.checkServerVersion(context, "1.13") ? getImageLoader() : null, this, largeAlbums);
+ return new PodcastChannelAdapter(context, headers, sections, ServerInfo.checkServerVersion(context, "1.13") ? getImageLoader() : null, this, largeAlbums);
}
}
@@ -182,7 +175,7 @@ public class SelectPodcastsFragment extends SelectRecyclerFragment<Serializable>
if(!Util.isOffline(context) && ServerInfo.hasNewestPodcastEpisodes(context)) {
try {
- newestEpisodes = musicService.getNewestPodcastEpisodes(10, context, listener);
+ newestEpisodes = musicService.getNewestPodcastEpisodes(refresh, context, listener, 10);
for(MusicDirectory.Entry entry: newestEpisodes.getChildren()) {
for(PodcastChannel channel: channels) {
@@ -246,12 +239,12 @@ public class SelectPodcastsFragment extends SelectRecyclerFragment<Serializable>
return;
}
- playNow(Arrays.asList((MusicDirectory.Entry) episode));
+ onSongPress(Arrays.asList((MusicDirectory.Entry) episode), episode, false);
}
}
@Override
- public GridLayoutManager.SpanSizeLookup getSpanSizeLookup() {
+ public GridLayoutManager.SpanSizeLookup getSpanSizeLookup(final GridLayoutManager gridLayoutManager) {
return new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
@@ -259,7 +252,7 @@ public class SelectPodcastsFragment extends SelectRecyclerFragment<Serializable>
if(adapter != null) {
int viewType = getCurrentAdapter().getItemViewType(position);
if (viewType == SectionAdapter.VIEW_TYPE_HEADER || viewType == PodcastChannelAdapter.VIEW_TYPE_PODCAST_EPISODE || viewType == PodcastChannelAdapter.VIEW_TYPE_PODCAST_LEGACY) {
- return getRecyclerColumnCount();
+ return gridLayoutManager.getSpanCount();
} else {
return 1;
}
@@ -270,6 +263,25 @@ public class SelectPodcastsFragment extends SelectRecyclerFragment<Serializable>
};
}
+ @Override
+ public void onFinishRefresh() {
+ Bundle args = getArguments();
+ if(args != null) {
+ String podcastId = args.getString(Constants.INTENT_EXTRA_NAME_ID, null);
+ if (podcastId != null && objects != null) {
+ for (Serializable ser : objects) {
+ if (ser instanceof PodcastChannel) {
+ PodcastChannel podcast = (PodcastChannel) ser;
+ if (podcastId.equals(podcast.getId())) {
+ onItemClicked(null, podcast);
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
public void refreshPodcasts() {
new SilentBackgroundTask<Void>(context) {
@Override
diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SelectRecyclerFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SelectRecyclerFragment.java
index 7ae7fff8..0d4506ac 100644
--- a/app/src/main/java/github/daneren2005/dsub/fragments/SelectRecyclerFragment.java
+++ b/app/src/main/java/github/daneren2005/dsub/fragments/SelectRecyclerFragment.java
@@ -15,10 +15,15 @@
package github.daneren2005.dsub.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;
@@ -102,6 +107,7 @@ public abstract class SelectRecyclerFragment<T> extends SubsonicFragment impleme
}
menuInflater.inflate(getOptionsMenu(), menu);
+ onFinishSetupOptionsMenu(menu);
}
@Override
diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SelectShareFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SelectShareFragment.java
index cb0e48b9..f231fa33 100644
--- a/app/src/main/java/github/daneren2005/dsub/fragments/SelectShareFragment.java
+++ b/app/src/main/java/github/daneren2005/dsub/fragments/SelectShareFragment.java
@@ -27,6 +27,7 @@ import android.widget.CompoundButton;
import android.widget.DatePicker;
import android.widget.EditText;
+import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@@ -104,10 +105,34 @@ public class SelectShareFragment extends SelectRecyclerFragment<Share> {
}
private void displayShareInfo(final Share share) {
- String message = context.getResources().getString(R.string.share_info,
- share.getUsername(), (share.getDescription() != null) ? share.getDescription() : "", share.getUrl(),
- Util.formatDate(share.getCreated()), Util.formatDate(share.getLastVisited()), Util.formatDate(share.getExpires()), share.getVisitCount());
- Util.info(context, share.getName(), message);
+ List<Integer> headers = new ArrayList<>();
+ List<String> details = new ArrayList<>();
+
+ headers.add(R.string.details_title);
+ details.add(share.getName());
+
+ headers.add(R.string.details_owner);
+ details.add(share.getUsername());
+
+ headers.add(R.string.details_description);
+ details.add(share.getDescription());
+
+ headers.add(R.string.details_url);
+ details.add(share.getUrl());
+
+ headers.add(R.string.details_created);
+ details.add(Util.formatDate(share.getCreated()));
+
+ headers.add(R.string.details_last_played);
+ details.add(Util.formatDate(share.getLastVisited()));
+
+ headers.add(R.string.details_expiration);
+ details.add(Util.formatDate(share.getExpires(), false));
+
+ headers.add(R.string.details_played_count);
+ details.add(Long.toString(share.getVisitCount()));
+
+ Util.showDetailsDialog(context, R.string.details_title_playlist, headers, details);
}
private void updateShareInfo(final Share share) {
diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SettingsFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SettingsFragment.java
index 9853e046..fa2ca340 100644
--- a/app/src/main/java/github/daneren2005/dsub/fragments/SettingsFragment.java
+++ b/app/src/main/java/github/daneren2005/dsub/fragments/SettingsFragment.java
@@ -15,12 +15,14 @@
package github.daneren2005.dsub.fragments;
+import android.Manifest;
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.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.preference.CheckBoxPreference;
@@ -29,6 +31,8 @@ import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceCategory;
import android.preference.PreferenceScreen;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.content.ContextCompat;
import android.text.InputType;
import android.util.Log;
import android.view.LayoutInflater;
@@ -47,6 +51,7 @@ import java.util.LinkedHashMap;
import java.util.Map;
import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.activity.SubsonicActivity;
import github.daneren2005.dsub.service.DownloadService;
import github.daneren2005.dsub.service.HeadphoneListenerService;
import github.daneren2005.dsub.service.MusicService;
@@ -54,8 +59,10 @@ import github.daneren2005.dsub.service.MusicServiceFactory;
import github.daneren2005.dsub.util.Constants;
import github.daneren2005.dsub.util.FileUtil;
import github.daneren2005.dsub.util.LoadingTask;
+import github.daneren2005.dsub.util.MediaRouteManager;
import github.daneren2005.dsub.util.SyncUtil;
import github.daneren2005.dsub.util.Util;
+import github.daneren2005.dsub.view.CacheLocationPreference;
import github.daneren2005.dsub.view.ErrorDialog;
public class SettingsFragment extends PreferenceCompatFragment implements SharedPreferences.OnSharedPreferenceChangeListener {
@@ -69,7 +76,7 @@ public class SettingsFragment extends PreferenceCompatFragment implements Shared
private ListPreference maxVideoBitrateWifi;
private ListPreference maxVideoBitrateMobile;
private ListPreference networkTimeout;
- private EditTextPreference cacheLocation;
+ private CacheLocationPreference cacheLocation;
private ListPreference preloadCountWifi;
private ListPreference preloadCountMobile;
private ListPreference keepPlayedCount;
@@ -77,6 +84,7 @@ public class SettingsFragment extends PreferenceCompatFragment implements Shared
private ListPreference pauseDisconnect;
private Preference addServerPreference;
private PreferenceCategory serversCategory;
+ private ListPreference songPressAction;
private ListPreference videoPlayer;
private ListPreference syncInterval;
private CheckBoxPreference syncEnabled;
@@ -137,6 +145,8 @@ public class SettingsFragment extends PreferenceCompatFragment implements Shared
xml = R.xml.settings_playback;
} else if("servers".equals(name)) {
xml = R.xml.settings_servers;
+ } else if ("cast".equals(name)) {
+ xml = R.xml.settings_cast;
}
if(xml != 0) {
@@ -184,6 +194,25 @@ public class SettingsFragment extends PreferenceCompatFragment implements Shared
} else {
context.stopService(serviceIntent);
}
+ } else if(Constants.PREFERENCES_KEY_THEME.equals(key)) {
+ String value = sharedPreferences.getString(key, null);
+ if("day/night".equals(value) || "day/black".equals(value)) {
+ if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
+ ActivityCompat.requestPermissions(context, new String[]{ Manifest.permission.ACCESS_COARSE_LOCATION }, SubsonicActivity.PERMISSIONS_REQUEST_LOCATION);
+ }
+ }
+ } else if(Constants.PREFERENCES_KEY_DLNA_CASTING_ENABLED.equals(key)) {
+ DownloadService downloadService = DownloadService.getInstance();
+ if(downloadService != null) {
+ MediaRouteManager mediaRouter = downloadService.getMediaRouter();
+
+ Boolean enabled = sharedPreferences.getBoolean(key, true);
+ if (enabled) {
+ mediaRouter.addDLNAProvider();
+ } else {
+ mediaRouter.removeDLNAProvider();
+ }
+ }
}
scheduleBackup();
@@ -205,7 +234,7 @@ public class SettingsFragment extends PreferenceCompatFragment implements Shared
maxVideoBitrateWifi = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_MAX_VIDEO_BITRATE_WIFI);
maxVideoBitrateMobile = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_MAX_VIDEO_BITRATE_MOBILE);
networkTimeout = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_NETWORK_TIMEOUT);
- cacheLocation = (EditTextPreference) this.findPreference(Constants.PREFERENCES_KEY_CACHE_LOCATION);
+ 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);
@@ -214,6 +243,7 @@ public class SettingsFragment extends PreferenceCompatFragment implements Shared
serversCategory = (PreferenceCategory) this.findPreference(Constants.PREFERENCES_KEY_SERVER_KEY);
addServerPreference = this.findPreference(Constants.PREFERENCES_KEY_SERVER_ADD);
videoPlayer = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_VIDEO_PLAYER);
+ 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);
@@ -350,6 +380,8 @@ public class SettingsFragment extends PreferenceCompatFragment implements Shared
if(theme != null) {
theme.setSummary(theme.getEntry());
+ }
+ if(openToTab != null) {
openToTab.setSummary(openToTab.getEntry());
}
@@ -379,6 +411,7 @@ public class SettingsFragment extends PreferenceCompatFragment implements Shared
keepPlayedCount.setSummary(keepPlayedCount.getEntry());
tempLoss.setSummary(tempLoss.getEntry());
pauseDisconnect.setSummary(pauseDisconnect.getEntry());
+ songPressAction.setSummary(songPressAction.getEntry());
videoPlayer.setSummary(videoPlayer.getEntry());
if(replayGain.isChecked()) {
@@ -423,6 +456,7 @@ public class SettingsFragment extends PreferenceCompatFragment implements Shared
for (ServerSettings ss : serverSettings.values()) {
if(!ss.update()) {
serversCategory.removePreference(ss.getScreen());
+ serverCount--;
}
}
}
@@ -633,7 +667,7 @@ public class SettingsFragment extends PreferenceCompatFragment implements Shared
} catch(Exception e) {
Log.w(TAG, "Failed to create " + musicNoMedia, e);
}
- } else if (nomediaDir.exists()) {
+ } else if (!hide && nomediaDir.exists()) {
if (!nomediaDir.delete()) {
Log.w(TAG, "Failed to delete " + nomediaDir);
}
@@ -664,8 +698,11 @@ public class SettingsFragment extends PreferenceCompatFragment implements Shared
SharedPreferences.Editor editor = prefs.edit();
editor.putString(Constants.PREFERENCES_KEY_CACHE_LOCATION, defaultPath);
editor.commit();
- cacheLocation.setSummary(defaultPath);
- cacheLocation.setText(defaultPath);
+
+ if(cacheLocation != null) {
+ cacheLocation.setSummary(defaultPath);
+ cacheLocation.setText(defaultPath);
+ }
}
// Clear download queue.
diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SimilarArtistFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SimilarArtistFragment.java
index 93e3a93a..a41b9d6f 100644
--- a/app/src/main/java/github/daneren2005/dsub/fragments/SimilarArtistFragment.java
+++ b/app/src/main/java/github/daneren2005/dsub/fragments/SimilarArtistFragment.java
@@ -15,14 +15,16 @@
package github.daneren2005.dsub.fragments;
+import android.content.Intent;
+import android.net.Uri;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import github.daneren2005.dsub.R;
-import github.daneren2005.dsub.adapter.ArtistAdapter;
import github.daneren2005.dsub.adapter.SectionAdapter;
+import github.daneren2005.dsub.adapter.SimilarArtistAdapter;
import github.daneren2005.dsub.domain.Artist;
import github.daneren2005.dsub.domain.ArtistInfo;
import github.daneren2005.dsub.domain.MusicDirectory;
@@ -35,6 +37,8 @@ import github.daneren2005.dsub.util.Util;
import github.daneren2005.dsub.view.UpdateView;
import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
@@ -52,18 +56,6 @@ public class SimilarArtistFragment extends SelectRecyclerFragment<Artist> {
}
@Override
- public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) {
- super.onCreateOptionsMenu(menu, menuInflater);
- if(!primaryFragment) {
- return;
- }
-
- if(info.getMissingArtists().isEmpty()) {
- menu.removeItem(R.id.menu_show_missing);
- }
- }
-
- @Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_play_now:
@@ -72,9 +64,6 @@ public class SimilarArtistFragment extends SelectRecyclerFragment<Artist> {
case R.id.menu_shuffle:
playAll(true);
return true;
- case R.id.menu_show_missing:
- showMissingArtists();
- break;
}
return super.onOptionsItemSelected(item);
@@ -82,8 +71,10 @@ public class SimilarArtistFragment extends SelectRecyclerFragment<Artist> {
@Override
public void onCreateContextMenu(Menu menu, MenuInflater menuInflater, UpdateView<Artist> updateView, Artist item) {
- onCreateContextMenuSupport(menu, menuInflater, updateView, item);
- recreateContextMenu(menu);
+ if(!Artist.MISSING_ID.equals(item.getId())) {
+ onCreateContextMenuSupport(menu, menuInflater, updateView, item);
+ recreateContextMenu(menu);
+ }
}
@Override
@@ -93,14 +84,21 @@ public class SimilarArtistFragment extends SelectRecyclerFragment<Artist> {
@Override
public void onItemClicked(UpdateView<Artist> updateView, Artist artist) {
- SubsonicFragment fragment = new SelectDirectoryFragment();
- Bundle args = new Bundle();
- args.putString(Constants.INTENT_EXTRA_NAME_ID, artist.getId());
- args.putString(Constants.INTENT_EXTRA_NAME_NAME, artist.getName());
- args.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, true);
- fragment.setArguments(args);
-
- replaceFragment(fragment);
+ if(Artist.MISSING_ID.equals(artist.getId())) {
+ String url = "http://www.last.fm/music/" + URLEncoder.encode(artist.getName());
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(Uri.parse(url));
+ startActivity(intent);
+ } else {
+ SubsonicFragment fragment = new SelectDirectoryFragment();
+ Bundle args = new Bundle();
+ args.putString(Constants.INTENT_EXTRA_NAME_ID, artist.getId());
+ args.putString(Constants.INTENT_EXTRA_NAME_NAME, artist.getName());
+ args.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, true);
+ fragment.setArguments(args);
+
+ replaceFragment(fragment);
+ }
}
@Override
@@ -109,8 +107,22 @@ public class SimilarArtistFragment extends SelectRecyclerFragment<Artist> {
}
@Override
- public SectionAdapter getAdapter(List<Artist> objects) {
- return new ArtistAdapter(context, objects, this);
+ public SectionAdapter getAdapter(List<Artist> artists) {
+ if(info.getMissingArtists().isEmpty()) {
+ return new SimilarArtistAdapter(context, artists, this);
+ } else {
+ List<String> headers = new ArrayList<>();
+ headers.add(null);
+ headers.add(context.getResources().getString(R.string.menu_similar_artists_missing));
+
+ List<Artist> missingArtists = new ArrayList<>();
+ for(String artistName: info.getMissingArtists()) {
+ Artist artist = new Artist(Artist.MISSING_ID, artistName);
+ missingArtists.add(artist);
+ }
+
+ return new SimilarArtistAdapter(context, headers, Arrays.asList(artists, missingArtists), this);
+ }
}
@Override
@@ -124,16 +136,6 @@ public class SimilarArtistFragment extends SelectRecyclerFragment<Artist> {
return R.string.menu_similar_artists;
}
- private void showMissingArtists() {
- StringBuilder b = new StringBuilder();
-
- for(String name: info.getMissingArtists()) {
- b.append("<h3><a href=\"https://www.google.com/#q=" + URLEncoder.encode(name) + "\">" + name + "</a></h3> ");
- }
-
- Util.showHTMLDialog(context, R.string.menu_similar_artists, b.toString());
- }
-
private void playAll(final boolean shuffle) {
new RecursiveLoader(context) {
@Override
diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SubsonicFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SubsonicFragment.java
index c503ec6c..de230309 100644
--- a/app/src/main/java/github/daneren2005/dsub/fragments/SubsonicFragment.java
+++ b/app/src/main/java/github/daneren2005/dsub/fragments/SubsonicFragment.java
@@ -20,6 +20,9 @@ package github.daneren2005.dsub.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;
@@ -37,6 +40,7 @@ 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;
@@ -125,6 +129,8 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR
protected boolean artistOverride = false;
protected SwipeRefreshLayout refreshLayout;
protected boolean firstRun;
+ protected MenuItem searchItem;
+ protected SearchView searchView;
public SubsonicFragment() {
super();
@@ -177,15 +183,36 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR
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_global_search:
- context.onSearchRequested();
- return true;
case R.id.menu_exit:
exit();
return true;
@@ -221,6 +248,9 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR
addToPlaylist(songs);
clearSelected();
return true;
+ case R.id.menu_star:case R.id.menu_unstar:
+ toggleSelectedStarred();
+ return true;
}
return false;
@@ -269,7 +299,6 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR
menu.removeItem(R.id.menu_rate);
}
}
- menu.findItem(entry.isDirectory() ? R.id.album_menu_star : R.id.song_menu_star).setTitle(entry.isStarred() ? R.string.common_unstar : R.string.common_star);
} else if(!entry.isVideo()) {
if(Util.isOffline(context)) {
menuInflater.inflate(R.menu.select_song_context_offline, menu);
@@ -280,8 +309,13 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR
if(entry.getBookmark() == null) {
menu.removeItem(R.id.bookmark_menu_delete);
}
+
+
+ String songPressAction = Util.getSongPressAction(context);
+ if(!"next".equals(songPressAction) && !"last".equals(songPressAction)) {
+ menu.setGroupVisible(R.id.hide_play_now, false);
+ }
}
- menu.findItem(entry.isDirectory() ? R.id.album_menu_star : R.id.song_menu_star).setTitle(entry.isStarred() ? R.string.common_unstar : R.string.common_star);
} else {
if(Util.isOffline(context)) {
menuInflater.inflate(R.menu.select_video_context_offline, menu);
@@ -290,6 +324,15 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR
menuInflater.inflate(R.menu.select_video_context, menu);
}
}
+
+ MenuItem starMenu = menu.findItem(entry.isDirectory() ? R.id.album_menu_star : R.id.song_menu_star);
+ if(starMenu != null) {
+ starMenu.setTitle(entry.isStarred() ? R.string.common_unstar : R.string.common_star);
+ }
+
+ 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)) {
@@ -323,6 +366,9 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR
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<Entry> songs = new ArrayList<Entry>(1);
songs.add(entry);
@@ -390,6 +436,9 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR
case R.id.album_menu_share:
createShare(songs);
break;
+ case R.id.song_menu_play_now:
+ playNow(songs);
+ break;
case R.id.song_menu_play_next:
getDownloadService().download(songs, false, false, true, false);
break;
@@ -611,7 +660,7 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR
}
});
- refreshLayout.setColorScheme(
+ refreshLayout.setColorSchemeResources(
R.color.holo_blue_light,
R.color.holo_orange_light,
R.color.holo_green_light,
@@ -634,7 +683,7 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR
}
});
- refreshLayout.setColorScheme(
+ refreshLayout.setColorSchemeResources(
R.color.holo_blue_light,
R.color.holo_orange_light,
R.color.holo_green_light,
@@ -656,7 +705,7 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR
final int columns = getRecyclerColumnCount();
GridLayoutManager gridLayoutManager = new GridLayoutManager(context, columns);
- GridLayoutManager.SpanSizeLookup spanSizeLookup = getSpanSizeLookup();
+ GridLayoutManager.SpanSizeLookup spanSizeLookup = getSpanSizeLookup(gridLayoutManager);
if(spanSizeLookup != null) {
gridLayoutManager.setSpanSizeLookup(spanSizeLookup);
}
@@ -671,15 +720,15 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR
layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
return layoutManager;
}
- public GridLayoutManager.SpanSizeLookup getSpanSizeLookup() {
+ 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 = getCurrentAdapter().getItemViewType(position);
+ int viewType = adapter.getItemViewType(position);
if (viewType == SectionAdapter.VIEW_TYPE_HEADER) {
- return getRecyclerColumnCount();
+ return gridLayoutManager.getSpanCount();
} else {
return 1;
}
@@ -722,6 +771,7 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR
if(downloadService == null) {
return;
}
+ downloadService.clear();
downloadService.setShufflePlayEnabled(true);
context.openNowPlaying();
return;
@@ -825,6 +875,8 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR
if (downloadService == null) {
return;
}
+
+ downloadService.clear();
downloadService.setShufflePlayEnabled(true);
context.openNowPlaying();
}
@@ -909,7 +961,7 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR
}.execute();
}
- protected void downloadRecursively(final List<Entry> albums, final boolean shuffle, final boolean append) {
+ protected void downloadRecursively(final List<Entry> albums, final boolean shuffle, final boolean append, final boolean playNext) {
new RecursiveLoader(context) {
@Override
protected Boolean doInBackground() throws Throwable {
@@ -937,7 +989,7 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR
downloadService.clear();
}
- downloadService.download(songs, false, true, false, false);
+ downloadService.download(songs, false, true, playNext, false);
if(!append) {
transition = true;
}
@@ -965,6 +1017,14 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR
}
protected void addToPlaylist(final List<Entry> songs) {
+ Iterator<Entry> 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;
@@ -1053,7 +1113,7 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR
@Override
protected void done(Void result) {
- Util.toast(context, context.getResources().getString(R.string.updated_playlist, songs.size(), playlist.getName()));
+ Util.toast(context, context.getResources().getString(R.string.updated_playlist, String.valueOf(songs.size()), playlist.getName()));
}
@Override
@@ -1075,16 +1135,24 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR
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;
+ DownloadService downloadService = getDownloadService();
+ String playlistName = null;
+ String playlistId = null;
+ if(downloadService != null) {
+ playlistName = downloadService.getSuggestedPlaylistName();
+ playlistId = downloadService.getSuggestedPlaylistId();
+ }
if (playlistName != null) {
playlistNameView.setText(playlistName);
- try {
- if(ServerInfo.checkServerVersion(context, "1.8.0") && Integer.parseInt(getDownloadService().getSuggestedPlaylistId()) != -1) {
- overwriteCheckBox.setChecked(true);
- overwriteCheckBox.setVisibility(View.VISIBLE);
+ if(playlistId != null) {
+ try {
+ if (ServerInfo.checkServerVersion(context, "1.8.0") && Integer.parseInt(playlistId) != -1) {
+ overwriteCheckBox.setChecked(true);
+ overwriteCheckBox.setVisibility(View.VISIBLE);
+ }
+ } catch (Exception e) {
+ Log.i(TAG, "Playlist id isn't a integer, probably MusicCabinet", e);
}
- } catch(Exception e) {
- Log.i(TAG, "Playlist id isn't a integer, probably MusicCabinet");
}
} else {
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
@@ -1145,6 +1213,7 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR
@Override
protected void error(Throwable error) {
String msg = context.getResources().getString(R.string.download_playlist_error) + " " + getErrorMessage(error);
+ Log.e(TAG, "Failed to create playlist", error);
Util.toast(context, msg);
}
}.execute();
@@ -1174,6 +1243,7 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR
msg = context.getResources().getString(R.string.download_playlist_error) + " " + getErrorMessage(error);
}
+ Log.e(TAG, "Failed to overwrite playlist", error);
Util.toast(context, msg, false);
}
}.execute();
@@ -1572,7 +1642,7 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR
.setPositiveButton(R.string.bookmark_action_resume, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
- playNow(songs, song, position);
+ playNow(songs, song, position, playlistName, playlistId);
}
})
.setNegativeButton(R.string.bookmark_action_start_over, new DialogInterface.OnClickListener() {
@@ -1605,13 +1675,40 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR
}
}.execute();
- playNow(songs, 0);
+ playNow(songs, 0, playlistName, playlistId);
}
});
AlertDialog dialog = builder.create();
dialog.show();
}
+ protected void onSongPress(List<Entry> entries, Entry entry) {
+ onSongPress(entries, entry, 0, true);
+ }
+ protected void onSongPress(List<Entry> entries, Entry entry, boolean allowPlayAll) {
+ onSongPress(entries, entry, 0, allowPlayAll);
+ }
+ protected void onSongPress(List<Entry> entries, Entry entry, int position, boolean allowPlayAll) {
+ List<Entry> songs = new ArrayList<Entry>();
+
+ String songPressAction = Util.getSongPressAction(context);
+ if("all".equals(songPressAction) && allowPlayAll) {
+ for(Entry song: entries) {
+ if(!song.isDirectory() && !song.isVideo()) {
+ 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<Entry> entries) {
playNow(entries, null, null);
}
@@ -1659,15 +1756,7 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR
new LoadingTask<Void>(context) {
@Override
protected Void doInBackground() throws Throwable {
- DownloadService downloadService = getDownloadService();
- if(downloadService == null) {
- return null;
- }
-
- downloadService.clear();
- downloadService.download(entries, false, true, true, false, entries.indexOf(song), position);
- downloadService.setSuggestedPlaylistName(playlistName, playlistId);
-
+ playNowInTask(entries, song, position, playlistName, playlistId);
return null;
}
@@ -1677,6 +1766,19 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR
}
}.execute();
}
+ protected void playNowInTask(final List<Entry> entries, final Entry song, final int position) {
+ playNowInTask(entries, song, position, null, null);
+ }
+ protected void playNowInTask(final List<Entry> 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);
+ }
protected void deleteBookmark(final MusicDirectory.Entry entry, final SectionAdapter adapter) {
Util.confirmDialog(context, R.string.bookmark_delete_title, entry.getTitle(), new DialogInterface.OnClickListener() {
@@ -1691,7 +1793,7 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR
MusicService musicService = MusicServiceFactory.getMusicService(context);
musicService.deleteBookmark(entry, context, null);
- new UpdateHelper.EntryInstanceUpdater(entry) {
+ new UpdateHelper.EntryInstanceUpdater(entry, DownloadService.METADATA_UPDATED_BOOKMARK) {
@Override
public void update(Entry found) {
found.setBookmark(null);
@@ -1898,6 +2000,18 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR
}
}
+ protected void toggleSelectedStarred() {
+ UpdateHelper.toggleStarred(context, getSelectedEntries());
+ }
+
+ protected boolean isShowArtistEnabled() {
+ return false;
+ }
+
+ protected String getCurrentQuery() {
+ return null;
+ }
+
public abstract class RecursiveLoader extends LoadingTask<Boolean> {
protected MusicService musicService;
protected static final int MAX_SONGS = 500;
@@ -1909,6 +2023,23 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR
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> entry) throws Exception {
getSongsRecursively(entry, false);
}
diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/UserFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/UserFragment.java
index dab104bd..dde76624 100644
--- a/app/src/main/java/github/daneren2005/dsub/fragments/UserFragment.java
+++ b/app/src/main/java/github/daneren2005/dsub/fragments/UserFragment.java
@@ -20,6 +20,8 @@ import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
+import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import github.daneren2005.dsub.R;
@@ -78,7 +80,7 @@ public class UserFragment extends SelectRecyclerFragment<User.Setting>{
@Override
public SectionAdapter<User.Setting> getAdapter(List<User.Setting> objs) {
- return new SettingsAdapter(context, user, getImageLoader(), UserUtil.isCurrentAdmin() && ServerInfo.checkServerVersion(context, "1.10"), this);
+ return SettingsAdapter.getSettingsAdapter(context, user, getImageLoader(), this);
}
@Override
diff --git a/app/src/main/java/github/daneren2005/dsub/provider/DLNARouteProvider.java b/app/src/main/java/github/daneren2005/dsub/provider/DLNARouteProvider.java
index c5632362..0ee16723 100644
--- a/app/src/main/java/github/daneren2005/dsub/provider/DLNARouteProvider.java
+++ b/app/src/main/java/github/daneren2005/dsub/provider/DLNARouteProvider.java
@@ -71,6 +71,7 @@ public class DLNARouteProvider extends MediaRouteProvider {
private List<String> removing = new ArrayList<String>();
private AndroidUpnpService dlnaService;
private ServiceConnection dlnaServiceConnection;
+ private RegistryListener registryListener;
private boolean searchOnConnect = false;
public DLNARouteProvider(Context context) {
@@ -84,7 +85,7 @@ public class DLNARouteProvider extends MediaRouteProvider {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
dlnaService = (AndroidUpnpService) service;
- dlnaService.getRegistry().addListener(new RegistryListener() {
+ dlnaService.getRegistry().addListener(registryListener = new RegistryListener() {
@Override
public void remoteDeviceDiscoveryStarted(Registry registry, RemoteDevice remoteDevice) {
@@ -142,6 +143,7 @@ public class DLNARouteProvider extends MediaRouteProvider {
@Override
public void onServiceDisconnected(ComponentName name) {
dlnaService = null;
+ registryListener = null;
}
};
@@ -166,7 +168,7 @@ public class DLNARouteProvider extends MediaRouteProvider {
DLNADevice device = deviceEntry.getValue();
int volume;
- if(device.volumeMax == 0) {
+ if(device.volumeMax <= 0) {
volume = 5;
} else {
int increments = (int) Math.ceil(device.volumeMax / 10.0);
@@ -290,6 +292,17 @@ public class DLNARouteProvider extends MediaRouteProvider {
}
}
+ public void destroy() {
+ if(dlnaService != null) {
+ dlnaService.getRegistry().removeListener(registryListener);
+ registryListener = null;
+ }
+
+ if(dlnaServiceConnection != null) {
+ getContext().getApplicationContext().unbindService(dlnaServiceConnection);
+ }
+ }
+
private class DLNARouteController extends RouteController {
private DLNADevice device;
diff --git a/app/src/main/java/github/daneren2005/dsub/provider/DSubSearchProvider.java b/app/src/main/java/github/daneren2005/dsub/provider/DSubSearchProvider.java
index f91c364e..ba8c80c1 100644
--- a/app/src/main/java/github/daneren2005/dsub/provider/DSubSearchProvider.java
+++ b/app/src/main/java/github/daneren2005/dsub/provider/DSubSearchProvider.java
@@ -58,6 +58,10 @@ public class DSubSearchProvider extends ContentProvider {
@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);
diff --git a/app/src/main/java/github/daneren2005/dsub/provider/DSubWidgetProvider.java b/app/src/main/java/github/daneren2005/dsub/provider/DSubWidgetProvider.java
index 18660fa2..5c90c250 100644
--- a/app/src/main/java/github/daneren2005/dsub/provider/DSubWidgetProvider.java
+++ b/app/src/main/java/github/daneren2005/dsub/provider/DSubWidgetProvider.java
@@ -278,7 +278,7 @@ public class DSubWidgetProvider extends AppWidgetProvider {
private void linkButtons(Context context, RemoteViews views, boolean playerActive) {
Intent intent = new Intent(context, SubsonicFragmentActivity.class);
intent.putExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD, true);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ 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);
diff --git a/app/src/main/java/github/daneren2005/dsub/service/AutoMediaBrowserService.java b/app/src/main/java/github/daneren2005/dsub/service/AutoMediaBrowserService.java
index d579ef54..35f6d37a 100644
--- a/app/src/main/java/github/daneren2005/dsub/service/AutoMediaBrowserService.java
+++ b/app/src/main/java/github/daneren2005/dsub/service/AutoMediaBrowserService.java
@@ -21,6 +21,7 @@ package github.daneren2005.dsub.service;
import android.annotation.TargetApi;
import android.content.Intent;
import android.media.MediaDescription;
+import android.media.MediaMetadata;
import android.media.browse.MediaBrowser;
import android.os.Build;
import android.os.Bundle;
@@ -33,7 +34,14 @@ import java.util.ArrayList;
import java.util.List;
import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.Artist;
+import github.daneren2005.dsub.domain.Indexes;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.MusicDirectory.Entry;
+import github.daneren2005.dsub.domain.MusicFolder;
import github.daneren2005.dsub.domain.Playlist;
+import github.daneren2005.dsub.domain.PodcastChannel;
+import github.daneren2005.dsub.domain.PodcastEpisode;
import github.daneren2005.dsub.domain.ServerInfo;
import github.daneren2005.dsub.util.Constants;
import github.daneren2005.dsub.util.SilentBackgroundTask;
@@ -48,8 +56,14 @@ public class AutoMediaBrowserService extends MediaBrowserService {
private static final String BROWSER_ALBUM_LISTS = "albumLists";
private static final String BROWSER_LIBRARY = "library";
private static final String BROWSER_PLAYLISTS = "playlists";
+ private static final String BROWSER_PODCASTS = "podcasts";
+ private static final String BROWSER_BOOKMARKS = "bookmarks";
private static final String PLAYLIST_PREFIX = "pl-";
+ private static final String PODCAST_PREFIX = "po-";
private static final String ALBUM_TYPE_PREFIX = "ty-";
+ private static final String MUSIC_DIRECTORY_PREFIX = "md-";
+ private static final String MUSIC_FOLDER_PREFIX = "mf-";
+ private static final String MUSIC_DIRECTORY_CONTENTS_PREFIX = "mdc-";
private DownloadService downloadService;
private Handler handler = new Handler();
@@ -76,12 +90,29 @@ public class AutoMediaBrowserService extends MediaBrowserService {
} else if(parentId.startsWith(ALBUM_TYPE_PREFIX)) {
int id = Integer.valueOf(parentId.substring(ALBUM_TYPE_PREFIX.length()));
getAlbumList(result, id);
+ } else if(parentId.startsWith(MUSIC_DIRECTORY_PREFIX)) {
+ String id = parentId.substring(MUSIC_DIRECTORY_PREFIX.length());
+ getPlayOptions(result, id, Constants.INTENT_EXTRA_NAME_ID);
} else if(BROWSER_LIBRARY.equals(parentId)) {
getLibrary(result);
+ } else if(parentId.startsWith(MUSIC_FOLDER_PREFIX)) {
+ String id = parentId.substring(MUSIC_FOLDER_PREFIX.length());
+ getIndexes(result, id);
+ } else if(parentId.startsWith(MUSIC_DIRECTORY_CONTENTS_PREFIX)) {
+ String id = parentId.substring(MUSIC_DIRECTORY_CONTENTS_PREFIX.length());
+ getMusicDirectory(result, id);
} else if(BROWSER_PLAYLISTS.equals(parentId)) {
getPlaylists(result);
} else if(parentId.startsWith(PLAYLIST_PREFIX)) {
- getPlayOptions(result, parentId.substring(PLAYLIST_PREFIX.length()), Constants.INTENT_EXTRA_NAME_PLAYLIST_ID);
+ String id = parentId.substring(PLAYLIST_PREFIX.length());
+ getPlayOptions(result, id, Constants.INTENT_EXTRA_NAME_PLAYLIST_ID);
+ } else if(BROWSER_PODCASTS.equals(parentId)) {
+ getPodcasts(result);
+ } else if(parentId.startsWith(PODCAST_PREFIX)) {
+ String id = parentId.substring(PODCAST_PREFIX.length());
+ getPodcastEpisodes(result, id);
+ } else if(BROWSER_BOOKMARKS.equals(parentId)) {
+ getBookmarks(result);
} else {
// No idea what it is, send empty result
result.sendResult(new ArrayList<MediaBrowser.MediaItem>());
@@ -91,7 +122,7 @@ public class AutoMediaBrowserService extends MediaBrowserService {
private void getRootFolders(Result<List<MediaBrowser.MediaItem>> result) {
List<MediaBrowser.MediaItem> mediaItems = new ArrayList<>();
- /*MediaDescription.Builder albumLists = new MediaDescription.Builder();
+ MediaDescription.Builder albumLists = new MediaDescription.Builder();
albumLists.setTitle(downloadService.getString(R.string.main_albums_title))
.setMediaId(BROWSER_ALBUM_LISTS);
mediaItems.add(new MediaBrowser.MediaItem(albumLists.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE));
@@ -99,13 +130,27 @@ public class AutoMediaBrowserService extends MediaBrowserService {
MediaDescription.Builder library = new MediaDescription.Builder();
library.setTitle(downloadService.getString(R.string.button_bar_browse))
.setMediaId(BROWSER_LIBRARY);
- mediaItems.add(new MediaBrowser.MediaItem(library.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE));*/
+ mediaItems.add(new MediaBrowser.MediaItem(library.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE));
MediaDescription.Builder playlists = new MediaDescription.Builder();
playlists.setTitle(downloadService.getString(R.string.button_bar_playlists))
.setMediaId(BROWSER_PLAYLISTS);
mediaItems.add(new MediaBrowser.MediaItem(playlists.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE));
+ if(Util.getPreferences(downloadService).getBoolean(Constants.PREFERENCES_KEY_PODCASTS_ENABLED, true)) {
+ MediaDescription.Builder podcasts = new MediaDescription.Builder();
+ podcasts.setTitle(downloadService.getString(R.string.button_bar_podcasts))
+ .setMediaId(BROWSER_PODCASTS);
+ mediaItems.add(new MediaBrowser.MediaItem(podcasts.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE));
+ }
+
+ if(Util.getPreferences(downloadService).getBoolean(Constants.PREFERENCES_KEY_BOOKMARKS_ENABLED, true)) {
+ MediaDescription.Builder podcasts = new MediaDescription.Builder();
+ podcasts.setTitle(downloadService.getString(R.string.button_bar_bookmarks))
+ .setMediaId(BROWSER_BOOKMARKS);
+ mediaItems.add(new MediaBrowser.MediaItem(podcasts.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE));
+ }
+
result.sendResult(mediaItems);
}
@@ -113,15 +158,10 @@ public class AutoMediaBrowserService extends MediaBrowserService {
List<Integer> albums = new ArrayList<>();
albums.add(R.string.main_albums_newest);
albums.add(R.string.main_albums_random);
- if(ServerInfo.checkServerVersion(downloadService, "1.8")) {
- albums.add(R.string.main_albums_alphabetical);
- }
if(!Util.isTagBrowsing(downloadService)) {
albums.add(R.string.main_albums_highest);
}
- // albums.add(R.string.main_albums_starred);
- // albums.add(R.string.main_albums_genres);
- // albums.add(R.string.main_albums_year);
+ albums.add(R.string.main_albums_starred);
albums.add(R.string.main_albums_recent);
albums.add(R.string.main_albums_frequent);
@@ -138,12 +178,158 @@ public class AutoMediaBrowserService extends MediaBrowserService {
result.sendResult(mediaItems);
}
- private void getAlbumList(Result<List<MediaBrowser.MediaItem>> result, int id) {
+ private void getAlbumList(final Result<List<MediaBrowser.MediaItem>> result, final int id) {
+ new SilentServiceTask<MusicDirectory>(downloadService) {
+ @Override
+ protected MusicDirectory doInBackground(MusicService musicService) throws Throwable {
+ String albumListType;
+ switch(id) {
+ case R.string.main_albums_newest:
+ albumListType = "newest";
+ break;
+ case R.string.main_albums_random:
+ albumListType = "random";
+ break;
+ case R.string.main_albums_highest:
+ albumListType = "highest";
+ break;
+ case R.string.main_albums_starred:
+ albumListType = "starred";
+ break;
+ case R.string.main_albums_recent:
+ albumListType = "recent";
+ break;
+ case R.string.main_albums_frequent:
+ albumListType = "frequent";
+ break;
+ default:
+ albumListType = "newest";
+ }
+
+ return musicService.getAlbumList(albumListType, 20, 0, true, downloadService, null);
+ }
+
+ @Override
+ protected void done(MusicDirectory albumSet) {
+ List<MediaBrowser.MediaItem> mediaItems = new ArrayList<>();
+
+ for(Entry album: albumSet.getChildren(true, false)) {
+ MediaDescription description = new MediaDescription.Builder()
+ .setTitle(album.getAlbumDisplay())
+ .setSubtitle(album.getArtist())
+ .setMediaId(MUSIC_DIRECTORY_PREFIX + album.getId())
+ .build();
+
+ mediaItems.add(new MediaBrowser.MediaItem(description, MediaBrowser.MediaItem.FLAG_BROWSABLE));
+ }
+ result.sendResult(mediaItems);
+ }
+ }.execute();
+
+ result.detach();
}
- private void getLibrary(Result<List<MediaBrowser.MediaItem>> result) {
+ private void getLibrary(final Result<List<MediaBrowser.MediaItem>> result) {
+ new SilentServiceTask<List<MusicFolder>>(downloadService) {
+ @Override
+ protected List<MusicFolder> doInBackground(MusicService musicService) throws Throwable {
+ return musicService.getMusicFolders(false, downloadService, null);
+ }
+
+ @Override
+ protected void done(List<MusicFolder> folders) {
+ List<MediaBrowser.MediaItem> mediaItems = new ArrayList<>();
+
+ for(MusicFolder folder: folders) {
+ MediaDescription description = new MediaDescription.Builder()
+ .setTitle(folder.getName())
+ .setMediaId(MUSIC_FOLDER_PREFIX + folder.getId())
+ .build();
+
+ mediaItems.add(new MediaBrowser.MediaItem(description, MediaBrowser.MediaItem.FLAG_BROWSABLE));
+ }
+
+ result.sendResult(mediaItems);
+ }
+ }.execute();
+
+ result.detach();
+ }
+ private void getIndexes(final Result<List<MediaBrowser.MediaItem>> result, final String musicFolderId) {
+ new SilentServiceTask<Indexes>(downloadService) {
+ @Override
+ protected Indexes doInBackground(MusicService musicService) throws Throwable {
+ return musicService.getIndexes(musicFolderId, false, downloadService, null);
+ }
+
+ @Override
+ protected void done(Indexes indexes) {
+ List<MediaBrowser.MediaItem> mediaItems = new ArrayList<>();
+
+ // music directories
+ for(Artist artist : indexes.getArtists()) {
+ MediaDescription description = new MediaDescription.Builder()
+ .setTitle(artist.getName())
+ .setMediaId(MUSIC_DIRECTORY_CONTENTS_PREFIX + artist.getId())
+ .build();
+
+ mediaItems.add(new MediaBrowser.MediaItem(description, MediaBrowser.MediaItem.FLAG_BROWSABLE));
+ }
+
+ // music files
+ for(Entry entry: indexes.getEntries()) {
+ MediaDescription description = new MediaDescription.Builder()
+ .setTitle(entry.getTitle())
+ .setMediaId(MUSIC_DIRECTORY_PREFIX + entry.getId())
+ .build();
+
+ mediaItems.add(new MediaBrowser.MediaItem(description, MediaBrowser.MediaItem.FLAG_BROWSABLE));
+ }
+
+ result.sendResult(mediaItems);
+ }
+ }.execute();
+
+ result.detach();
+ }
+
+ private void getMusicDirectory(final Result<List<MediaBrowser.MediaItem>> result, final String musicDirectoryId) {
+ new SilentServiceTask<MusicDirectory>(downloadService) {
+ @Override
+ protected MusicDirectory doInBackground(MusicService musicService) throws Throwable {
+ return musicService.getMusicDirectory(musicDirectoryId, "", false, downloadService, null);
+ }
+
+ @Override
+ protected void done(MusicDirectory directory) {
+ List<MediaBrowser.MediaItem> mediaItems = new ArrayList<>();
+
+ addPlayOptions(mediaItems, musicDirectoryId, Constants.INTENT_EXTRA_NAME_ID);
+
+ for(Entry entry : directory.getChildren()) {
+ MediaDescription description;
+ if (entry.isDirectory()) {
+ // browse deeper
+ description = new MediaDescription.Builder()
+ .setTitle(entry.getTitle())
+ .setMediaId(MUSIC_DIRECTORY_CONTENTS_PREFIX + entry.getId())
+ .build();
+ } else {
+ // playback options for a single item
+ description = new MediaDescription.Builder()
+ .setTitle(entry.getTitle())
+ .setMediaId(MUSIC_DIRECTORY_PREFIX + entry.getId())
+ .build();
+ }
+ mediaItems.add(new MediaBrowser.MediaItem(description, MediaBrowser.MediaItem.FLAG_BROWSABLE));
+ }
+ result.sendResult(mediaItems);
+ }
+ }.execute();
+
+ result.detach();
}
private void getPlaylists(final Result<List<MediaBrowser.MediaItem>> result) {
@@ -172,9 +358,101 @@ public class AutoMediaBrowserService extends MediaBrowserService {
result.detach();
}
- private void getPlayOptions(Result<List<MediaBrowser.MediaItem>> result, String id, String idConstant) {
- List<MediaBrowser.MediaItem> mediaItems = new ArrayList<>();
+ private void getPodcasts(final Result<List<MediaBrowser.MediaItem>> result) {
+ new SilentServiceTask<List<PodcastChannel>>(downloadService) {
+ @Override
+ protected List<PodcastChannel> doInBackground(MusicService musicService) throws Throwable {
+ return musicService.getPodcastChannels(false, downloadService, null);
+ }
+
+ @Override
+ protected void done(List<PodcastChannel> podcasts) {
+ List<MediaBrowser.MediaItem> mediaItems = new ArrayList<>();
+
+ for(PodcastChannel podcast: podcasts) {
+ MediaDescription description = new MediaDescription.Builder()
+ .setTitle(podcast.getName())
+ .setMediaId(PODCAST_PREFIX + podcast.getId())
+ .build();
+
+ mediaItems.add(new MediaBrowser.MediaItem(description, MediaBrowser.MediaItem.FLAG_BROWSABLE));
+ }
+
+ result.sendResult(mediaItems);
+ }
+ }.execute();
+
+ result.detach();
+ }
+ private void getPodcastEpisodes(final Result<List<MediaBrowser.MediaItem>> result, final String podcastId) {
+ new SilentServiceTask<MusicDirectory>(downloadService) {
+ @Override
+ protected MusicDirectory doInBackground(MusicService musicService) throws Throwable {
+ return musicService.getPodcastEpisodes(false, podcastId, downloadService, null);
+ }
+
+ @Override
+ protected void done(MusicDirectory podcasts) {
+ List<MediaBrowser.MediaItem> mediaItems = new ArrayList<>();
+
+ for(Entry entry: podcasts.getChildren(false, true)) {
+ PodcastEpisode podcast = (PodcastEpisode) entry;
+ Bundle podcastExtras = new Bundle();
+ podcastExtras.putSerializable(Constants.INTENT_EXTRA_ENTRY, podcast);
+ podcastExtras.putString(Constants.INTENT_EXTRA_NAME_PODCAST_ID, podcast.getId());
+
+ MediaDescription description = new MediaDescription.Builder()
+ .setTitle(podcast.getTitle())
+ .setSubtitle(Util.formatDate(downloadService, podcast.getDate(), false))
+ .setMediaId(PODCAST_PREFIX + podcast.getId())
+ .setExtras(podcastExtras)
+ .build();
+
+ mediaItems.add(new MediaBrowser.MediaItem(description, MediaBrowser.MediaItem.FLAG_PLAYABLE));
+ }
+
+ result.sendResult(mediaItems);
+ }
+ }.execute();
+
+ result.detach();
+ }
+
+ private void getBookmarks(final Result<List<MediaBrowser.MediaItem>> result) {
+ new SilentServiceTask<MusicDirectory>(downloadService) {
+ @Override
+ protected MusicDirectory doInBackground(MusicService musicService) throws Throwable {
+ return musicService.getBookmarks(false, downloadService, null);
+ }
+
+ @Override
+ protected void done(MusicDirectory bookmarkList) {
+ List<MediaBrowser.MediaItem> mediaItems = new ArrayList<>();
+
+ for(Entry entry: bookmarkList.getChildren(false, true)) {
+ Bundle extras = new Bundle();
+ extras.putSerializable(Constants.INTENT_EXTRA_ENTRY, entry);
+ extras.putString(Constants.INTENT_EXTRA_NAME_CHILD_ID, entry.getId());
+
+ MediaDescription description = new MediaDescription.Builder()
+ .setTitle(entry.getTitle())
+ .setSubtitle(Util.formatDuration(entry.getBookmark().getPosition() / 1000))
+ .setMediaId(entry.getId())
+ .setExtras(extras)
+ .build();
+
+ mediaItems.add(new MediaBrowser.MediaItem(description, MediaBrowser.MediaItem.FLAG_PLAYABLE));
+ }
+
+ result.sendResult(mediaItems);
+ }
+ }.execute();
+
+ result.detach();
+ }
+
+ private void addPlayOptions(List<MediaBrowser.MediaItem> mediaItems, String id, String idConstant) {
Bundle playAllExtras = new Bundle();
playAllExtras.putString(idConstant, id);
@@ -194,7 +472,7 @@ public class AutoMediaBrowserService extends MediaBrowserService {
.setExtras(shuffleExtras);
mediaItems.add(new MediaBrowser.MediaItem(shuffle.build(), MediaBrowser.MediaItem.FLAG_PLAYABLE));
- /*Bundle playLastExtras = new Bundle();
+ Bundle playLastExtras = new Bundle();
playLastExtras.putString(idConstant, id);
playLastExtras.putBoolean(Constants.INTENT_EXTRA_PLAY_LAST, true);
@@ -202,7 +480,13 @@ public class AutoMediaBrowserService extends MediaBrowserService {
playLast.setTitle(downloadService.getString(R.string.menu_play_last))
.setMediaId("playLast-" + id)
.setExtras(playLastExtras);
- mediaItems.add(new MediaBrowser.MediaItem(playLast.build(), MediaBrowser.MediaItem.FLAG_PLAYABLE));*/
+ mediaItems.add(new MediaBrowser.MediaItem(playLast.build(), MediaBrowser.MediaItem.FLAG_PLAYABLE));
+ }
+
+ private void getPlayOptions(Result<List<MediaBrowser.MediaItem>> result, String id, String idConstant) {
+ List<MediaBrowser.MediaItem> mediaItems = new ArrayList<>();
+
+ addPlayOptions(mediaItems, id, idConstant);
result.sendResult(mediaItems);
}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/CachedMusicService.java b/app/src/main/java/github/daneren2005/dsub/service/CachedMusicService.java
index 53433f5c..9fd26fe5 100644
--- a/app/src/main/java/github/daneren2005/dsub/service/CachedMusicService.java
+++ b/app/src/main/java/github/daneren2005/dsub/service/CachedMusicService.java
@@ -20,6 +20,7 @@ package github.daneren2005.dsub.service;
import java.io.File;
import java.io.IOException;
+import java.net.HttpURLConnection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
@@ -27,8 +28,6 @@ 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;
@@ -39,6 +38,7 @@ import github.daneren2005.dsub.domain.Bookmark;
import github.daneren2005.dsub.domain.ChatMessage;
import github.daneren2005.dsub.domain.Genre;
import github.daneren2005.dsub.domain.Indexes;
+import github.daneren2005.dsub.domain.InternetRadioStation;
import github.daneren2005.dsub.domain.PlayerQueue;
import github.daneren2005.dsub.domain.PodcastEpisode;
import github.daneren2005.dsub.domain.RemoteStatus;
@@ -69,6 +69,9 @@ public class CachedMusicService implements MusicService {
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<Boolean> cachedLicenseValid = new TimeLimitedCache<Boolean>(120, TimeUnit.SECONDS);
@@ -121,11 +124,13 @@ public class CachedMusicService implements MusicService {
if(!refresh) {
result = FileUtil.deserialize(context, getCacheName(context, "musicFolders"), ArrayList.class);
}
-
+
if(result == null) {
result = musicService.getMusicFolders(refresh, context, progressListener);
FileUtil.serialize(context, new ArrayList<MusicFolder>(result), getCacheName(context, "musicFolders"));
}
+
+ MusicFolder.sort(result);
cachedMusicFolders.set(result);
}
return result;
@@ -150,7 +155,7 @@ public class CachedMusicService implements MusicService {
if(!refresh) {
result = FileUtil.deserialize(context, name, Indexes.class);
}
-
+
if(result == null) {
result = musicService.getIndexes(musicFolderId, refresh, context, progressListener);
FileUtil.serialize(context, result, name);
@@ -169,12 +174,13 @@ public class CachedMusicService implements MusicService {
new SilentBackgroundTask<Void>(context) {
MusicDirectory refreshed;
+ private boolean metadataUpdated;
@Override
protected Void doInBackground() throws Throwable {
refreshed = musicService.getMusicDirectory(id, name, true, context, null);
updateAllSongs(context, refreshed);
- cached.updateMetadata(refreshed);
+ metadataUpdated = cached.updateMetadata(refreshed);
deleteRemovedEntries(context, refreshed, cached);
FileUtil.serialize(context, refreshed, getCacheName(context, "directory", id));
return null;
@@ -185,7 +191,10 @@ public class CachedMusicService implements MusicService {
public void done(Void result) {
if(progressListener != null) {
if(cached.updateEntriesList(context, musicService.getInstance(context), refreshed)) {
- progressListener.updateCache();
+ progressListener.updateCache(CACHE_UPDATE_LIST);
+ }
+ if(metadataUpdated) {
+ progressListener.updateCache(CACHE_UPDATE_METADATA);
}
}
}
@@ -201,7 +210,7 @@ public class CachedMusicService implements MusicService {
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);
}
@@ -234,7 +243,7 @@ public class CachedMusicService implements MusicService {
public void done(Void result) {
if(progressListener != null) {
if(cached.updateEntriesList(context, musicService.getInstance(context), refreshed)) {
- progressListener.updateCache();
+ progressListener.updateCache(CACHE_UPDATE_LIST);
}
}
}
@@ -267,12 +276,13 @@ public class CachedMusicService implements MusicService {
new SilentBackgroundTask<Void>(context) {
MusicDirectory refreshed;
+ private boolean metadataUpdated;
@Override
protected Void doInBackground() throws Throwable {
refreshed = musicService.getAlbum(id, name, refresh, context, null);
updateAllSongs(context, refreshed);
- cached.updateMetadata(refreshed);
+ metadataUpdated = cached.updateMetadata(refreshed);
deleteRemovedEntries(context, refreshed, cached);
FileUtil.serialize(context, refreshed, getCacheName(context, "album", id));
return null;
@@ -283,7 +293,10 @@ public class CachedMusicService implements MusicService {
public void done(Void result) {
if(progressListener != null) {
if(cached.updateEntriesList(context, musicService.getInstance(context), refreshed)) {
- progressListener.updateCache();
+ progressListener.updateCache(CACHE_UPDATE_LIST);
+ }
+ if(metadataUpdated) {
+ progressListener.updateCache(CACHE_UPDATE_METADATA);
}
}
}
@@ -656,6 +669,11 @@ public class CachedMusicService implements MusicService {
}
@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);
}
@@ -714,11 +732,16 @@ public class CachedMusicService implements MusicService {
@Override
public Bitmap getCoverArt(Context context, Entry entry, int size, ProgressListener progressListener, SilentBackgroundTask task) throws Exception {
- return musicService.getCoverArt(context, entry, size, progressListener, task);
+ Bitmap bitmap = FileUtil.getAlbumArtBitmap(context, entry, size);
+ if (bitmap != null) {
+ return bitmap;
+ } else {
+ return musicService.getCoverArt(context, entry, size, progressListener, task);
+ }
}
@Override
- public HttpResponse getDownloadInputStream(Context context, Entry song, long offset, int maxBitrate, SilentBackgroundTask task) throws Exception {
+ public HttpURLConnection getDownloadInputStream(Context context, Entry song, long offset, int maxBitrate, SilentBackgroundTask task) throws Exception {
return musicService.getDownloadInputStream(context, song, offset, maxBitrate, task);
}
@@ -905,8 +928,18 @@ public class CachedMusicService implements MusicService {
}
@Override
- public MusicDirectory getNewestPodcastEpisodes(int count, Context context, ProgressListener progressListener) throws Exception {
- return musicService.getNewestPodcastEpisodes(count, context, progressListener);
+ public MusicDirectory getNewestPodcastEpisodes(boolean refresh, Context context, ProgressListener progressListener, int count) throws Exception {
+ MusicDirectory result = null;
+
+ String cacheName = getCacheName(context, "newestPodcastEpisodes");
+ try {
+ result = musicService.getNewestPodcastEpisodes(refresh, context, progressListener, count);
+ FileUtil.serialize(context, result, cacheName);
+ } catch(IOException e) {
+ result = FileUtil.deserialize(context, cacheName, MusicDirectory.class, 24);
+ } finally {
+ return result;
+ }
}
@Override
@@ -1128,7 +1161,12 @@ public class CachedMusicService implements MusicService {
@Override
public Bitmap getAvatar(String username, int size, Context context, ProgressListener progressListener, SilentBackgroundTask task) throws Exception {
- return musicService.getAvatar(username, size, context, progressListener, task);
+ Bitmap bitmap = FileUtil.getAvatarBitmap(context, username, size);
+ if(bitmap != null) {
+ return bitmap;
+ } else {
+ return musicService.getAvatar(username, size, context, progressListener, task);
+ }
}
@Override
@@ -1136,12 +1174,22 @@ public class CachedMusicService implements MusicService {
String cacheName = getCacheName(context, "artistInfo", id);
ArtistInfo info = null;
if(!refresh) {
- info = FileUtil.deserialize(context, cacheName, ArtistInfo.class);
+ info = FileUtil.deserialize(context, cacheName, ArtistInfo.class, CACHED_LAST_FM);
}
if(info == null && allowNetwork) {
- info = musicService.getArtistInfo(id, refresh, allowNetwork, context, progressListener);
- FileUtil.serialize(context, info, cacheName);
+ try {
+ info = musicService.getArtistInfo(id, refresh, allowNetwork, context, progressListener);
+ FileUtil.serialize(context, info, cacheName);
+ } catch(Exception e) {
+ Log.w(TAG, "Failed to refresh Artist Info");
+ info = FileUtil.deserialize(context, cacheName, ArtistInfo.class);
+
+ // Nothing ever cached, throw error further upstream
+ if(info == null) {
+ throw e;
+ }
+ }
}
return info;
@@ -1149,7 +1197,12 @@ public class CachedMusicService implements MusicService {
@Override
public Bitmap getBitmap(String url, int size, Context context, ProgressListener progressListener, SilentBackgroundTask task) throws Exception {
- return musicService.getBitmap(url, size, context, progressListener, task);
+ Bitmap bitmap = FileUtil.getMiscBitmap(context, url, size);
+ if(bitmap != null) {
+ return bitmap;
+ } else {
+ return musicService.getBitmap(url, size, context, progressListener, task);
+ }
}
@Override
@@ -1180,6 +1233,22 @@ public class CachedMusicService implements MusicService {
}
@Override
+ public List<InternetRadioStation> getInternetRadioStations(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ List<InternetRadioStation> result = null;
+
+ if(!refresh) {
+ result = FileUtil.deserialize(context, getCacheName(context, "internetRadioStations"), ArrayList.class);
+ }
+
+ if(result == null) {
+ result = musicService.getInternetRadioStations(refresh, context, progressListener);
+ FileUtil.serialize(context, new ArrayList<>(result), getCacheName(context, "internetRadioStations"));
+ }
+
+ return result;
+ }
+
+ @Override
public int processOfflineSyncs(final Context context, final ProgressListener progressListener) throws Exception{
return musicService.processOfflineSyncs(context, progressListener);
}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/ChromeCastController.java b/app/src/main/java/github/daneren2005/dsub/service/ChromeCastController.java
index 670ea7d2..f9e2bfb1 100644
--- a/app/src/main/java/github/daneren2005/dsub/service/ChromeCastController.java
+++ b/app/src/main/java/github/daneren2005/dsub/service/ChromeCastController.java
@@ -23,6 +23,7 @@ import android.util.Log;
import com.google.android.gms.cast.ApplicationMetadata;
import com.google.android.gms.cast.Cast;
import com.google.android.gms.cast.CastDevice;
+import com.google.android.gms.cast.CastStatusCodes;
import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaMetadata;
import com.google.android.gms.cast.MediaStatus;
@@ -41,6 +42,7 @@ import github.daneren2005.dsub.domain.MusicDirectory;
import github.daneren2005.dsub.domain.PlayerState;
import github.daneren2005.dsub.domain.RemoteControlState;
import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.EnvironmentVariables;
import github.daneren2005.dsub.util.FileUtil;
import github.daneren2005.dsub.util.Util;
import github.daneren2005.dsub.util.compat.CastCompat;
@@ -62,18 +64,15 @@ public class ChromeCastController extends RemoteController {
private boolean error = false;
private boolean ignoreNextPaused = false;
private String sessionId;
+ private boolean isStopping = false;
+ private Runnable afterUpdateComplete = null;
- private ServerProxy proxy;
- private String rootLocation;
private RemoteMediaPlayer mediaPlayer;
private double gain = 0.5;
public ChromeCastController(DownloadService downloadService, CastDevice castDevice) {
- this.downloadService = downloadService;
+ super(downloadService);
this.castDevice = castDevice;
-
- SharedPreferences prefs = Util.getPreferences(downloadService);
- rootLocation = prefs.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, null);
}
@Override
@@ -247,66 +246,44 @@ public class ChromeCastController extends RemoteController {
}
}
- void startSong(DownloadFile currentPlaying, boolean autoStart, int position) {
+ void startSong(final DownloadFile currentPlaying, final boolean autoStart, final int position) {
if(currentPlaying == null) {
try {
- if (mediaPlayer != null && !error) {
- mediaPlayer.stop(apiClient);
+ if (mediaPlayer != null && !error && !isStopping) {
+ isStopping = true;
+ mediaPlayer.stop(apiClient).setResultCallback(new ResultCallback<RemoteMediaPlayer.MediaChannelResult>() {
+ @Override
+ public void onResult(RemoteMediaPlayer.MediaChannelResult mediaChannelResult) {
+ isStopping = false;
+
+ if(afterUpdateComplete != null) {
+ afterUpdateComplete.run();
+ afterUpdateComplete = null;
+ }
+ }
+ });
}
} catch(Exception e) {
// Just means it didn't need to be stopped
}
downloadService.setPlayerState(PlayerState.IDLE);
return;
+ } else if(isStopping) {
+ afterUpdateComplete = new Runnable() {
+ @Override
+ public void run() {
+ startSong(currentPlaying, autoStart, position);
+ }
+ };
+ return;
}
+
downloadService.setPlayerState(PlayerState.PREPARING);
MusicDirectory.Entry song = currentPlaying.getSong();
try {
MusicService musicService = MusicServiceFactory.getMusicService(downloadService);
- String url;
- // Offline, use file proxy
- if(Util.isOffline(downloadService) || song.getId().indexOf(rootLocation) != -1) {
- if(proxy == null) {
- proxy = new FileProxy(downloadService);
- proxy.start();
- }
-
- // Offline song
- if(song.getId().indexOf(rootLocation) != -1) {
- url = proxy.getPublicAddress(song.getId());
- } else {
- // Playing online song in offline mode
- url = proxy.getPublicAddress(currentPlaying.getCompleteFile().getPath());
- }
- } else {
- // Check if we want a proxy going still
- if(Util.isCastProxy(downloadService)) {
- if(proxy instanceof FileProxy) {
- proxy.stop();
- proxy = null;
- }
-
- if(proxy == null) {
- proxy = createWebProxy();
- proxy.start();
- }
- } else if(proxy != null) {
- proxy.stop();
- proxy = null;
- }
-
- if(song.isVideo()) {
- url = musicService.getHlsUrl(song.getId(), currentPlaying.getBitRate(), downloadService);
- } else {
- url = musicService.getMusicUrl(downloadService, song, currentPlaying.getBitRate());
- }
-
- // If proxy is going, it is a WebProxy
- if(proxy != null) {
- url = proxy.getPublicAddress(url);
- }
- }
+ String url = getStreamUrl(musicService, currentPlaying);
// Setup song/video information
MediaMetadata meta = new MediaMetadata(song.isVideo() ? MediaMetadata.MEDIA_TYPE_MOVIE : MediaMetadata.MEDIA_TYPE_MUSIC_TRACK);
@@ -367,6 +344,8 @@ public class ChromeCastController extends RemoteController {
public void onResult(RemoteMediaPlayer.MediaChannelResult result) {
if (result.getStatus().isSuccess()) {
// Handled in other handler
+ } else if(result.getStatus().getStatusCode() == CastStatusCodes.REPLACED) {
+ Log.w(TAG, "Request was replaced: " + currentPlaying.toString());
} else {
Log.e(TAG, "Failed to load: " + result.getStatus().toString());
failedLoad();
@@ -443,14 +422,14 @@ public class ChromeCastController extends RemoteController {
void launchApplication() {
try {
- Cast.CastApi.launchApplication(apiClient, CastCompat.APPLICATION_ID, false).setResultCallback(resultCallback);
+ Cast.CastApi.launchApplication(apiClient, EnvironmentVariables.CAST_APPLICATION_ID, false).setResultCallback(resultCallback);
} catch (Exception e) {
Log.e(TAG, "Failed to launch application", e);
}
}
void reconnectApplication() {
try {
- Cast.CastApi.joinApplication(apiClient, CastCompat.APPLICATION_ID, sessionId).setResultCallback(resultCallback);
+ Cast.CastApi.joinApplication(apiClient, EnvironmentVariables.CAST_APPLICATION_ID, sessionId).setResultCallback(resultCallback);
} catch (Exception e) {
Log.e(TAG, "Failed to reconnect application", e);
}
@@ -483,7 +462,9 @@ public class ChromeCastController extends RemoteController {
break;
case MediaStatus.PLAYER_STATE_IDLE:
if (mediaStatus.getIdleReason() == MediaStatus.IDLE_REASON_FINISHED) {
- downloadService.onSongCompleted();
+ if(downloadService.getPlayerState() != PlayerState.PREPARING) {
+ downloadService.onSongCompleted();
+ }
} else if (mediaStatus.getIdleReason() == MediaStatus.IDLE_REASON_INTERRUPTED) {
if (downloadService.getPlayerState() != PlayerState.PREPARING) {
downloadService.setPlayerState(PlayerState.PREPARING);
@@ -503,7 +484,7 @@ public class ChromeCastController extends RemoteController {
try {
Cast.CastApi.setMessageReceivedCallbacks(apiClient, mediaPlayer.getNamespace(), mediaPlayer);
- } catch (IOException e) {
+ } catch (Exception e) {
Log.e(TAG, "Exception while creating channel", e);
}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/DLNAController.java b/app/src/main/java/github/daneren2005/dsub/service/DLNAController.java
index 0673cdeb..143be330 100644
--- a/app/src/main/java/github/daneren2005/dsub/service/DLNAController.java
+++ b/app/src/main/java/github/daneren2005/dsub/service/DLNAController.java
@@ -83,9 +83,6 @@ public class DLNAController extends RemoteController {
SubscriptionCallback callback;
boolean supportsSeek = false;
boolean supportsSetupNext = false;
-
- private ServerProxy proxy;
- String rootLocation = "";
boolean error = false;
final AtomicLong lastUpdate = new AtomicLong();
@@ -108,12 +105,9 @@ public class DLNAController extends RemoteController {
};
public DLNAController(DownloadService downloadService, ControlPoint controlPoint, DLNADevice device) {
- this.downloadService = downloadService;
+ super(downloadService);
this.controlPoint = controlPoint;
this.device = device;
-
- SharedPreferences prefs = Util.getPreferences(downloadService);
- rootLocation = prefs.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, null);
nextSupported = true;
}
@@ -163,7 +157,10 @@ public class DLNAController extends RemoteController {
protected void eventReceived(GENASubscription genaSubscription) {
Map<String, StateVariableValue> m = genaSubscription.getCurrentValues();
try {
- LastChange lastChange = new LastChange(new AVTransportLastChangeParser(), m.get("LastChange").toString());
+ String lastChangeText = m.get("LastChange").toString();
+ lastChangeText = lastChangeText.replace(",X_DLNA_SeekTime","").replace(",X_DLNA_SeekByte", "");
+ LastChange lastChange = new LastChange(new AVTransportLastChangeParser(), lastChangeText);
+
if (lastChange.getEventedValue(0, AVTransportVariable.TransportState.class) == null) {
return;
}
@@ -470,49 +467,7 @@ public class DLNAController extends RemoteController {
// Get url for entry
MusicService musicService = MusicServiceFactory.getMusicService(downloadService);
- String url;
- // In offline mode or playing offline song
- if(Util.isOffline(downloadService) || song.getId().indexOf(rootLocation) != -1) {
- if(proxy == null) {
- proxy = new FileProxy(downloadService);
- proxy.start();
- }
-
- // Offline song
- if(song.getId().indexOf(rootLocation) != -1) {
- url = proxy.getPublicAddress(song.getId());
- } else {
- // Playing online song in offline mode
- url = proxy.getPublicAddress(downloadFile.getCompleteFile().getPath());
- }
- } else {
- // Check if we want a proxy going still
- if(Util.isCastProxy(downloadService)) {
- if(proxy instanceof FileProxy) {
- proxy.stop();
- proxy = null;
- }
-
- if(proxy == null) {
- proxy = createWebProxy();
- proxy.start();
- }
- } else if(proxy != null) {
- proxy.stop();
- proxy = null;
- }
-
- if(song.isVideo()) {
- url = musicService.getHlsUrl(song.getId(), downloadFile.getBitRate(), downloadService);
- } else {
- url = musicService.getMusicUrl(downloadService, song, downloadFile.getBitRate());
- }
-
- // If proxy is going, it is a WebProxy
- if(proxy != null) {
- url = proxy.getPublicAddress(url);
- }
- }
+ String url = getStreamUrl(musicService, downloadFile);
// Create metadata for entry
Item track;
@@ -582,7 +537,7 @@ public class DLNAController extends RemoteController {
Log.w(TAG, "Metadata generation failed", e);
}
- return new Pair<String, String>(url, metadata);
+ return new Pair<>(url, metadata);
}
private void failedLoad() {
diff --git a/app/src/main/java/github/daneren2005/dsub/service/DownloadFile.java b/app/src/main/java/github/daneren2005/dsub/service/DownloadFile.java
index 3febfaea..30e06982 100644
--- a/app/src/main/java/github/daneren2005/dsub/service/DownloadFile.java
+++ b/app/src/main/java/github/daneren2005/dsub/service/DownloadFile.java
@@ -24,11 +24,14 @@ import java.io.IOException;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.net.HttpURLConnection;
import android.content.Context;
import android.net.wifi.WifiManager;
import android.os.PowerManager;
import android.util.Log;
+
+import github.daneren2005.dsub.domain.InternetRadioStation;
import github.daneren2005.dsub.domain.MusicDirectory;
import github.daneren2005.dsub.util.Constants;
import github.daneren2005.dsub.util.SilentBackgroundTask;
@@ -37,15 +40,6 @@ import github.daneren2005.dsub.util.Util;
import github.daneren2005.dsub.util.CacheCleaner;
import github.daneren2005.serverproxy.BufferFile;
-import org.apache.http.Header;
-
-import org.apache.http.HttpResponse;
-import org.apache.http.HttpStatus;
-
-/**
- * @author Sindre Mehus
- * @version $Id$
- */
public class DownloadFile implements BufferFile {
private static final String TAG = DownloadFile.class.getSimpleName();
private static final int MAX_FAILURES = 5;
@@ -84,6 +78,9 @@ public class DownloadFile implements BufferFile {
public MusicDirectory.Entry getSong() {
return song;
}
+ public boolean isSong() {
+ return song.isSong();
+ }
public Context getContext() {
return context;
@@ -111,7 +108,7 @@ public class DownloadFile implements BufferFile {
}
} else if(song.getSuffix() != null && (song.getTranscodedSuffix() == null || song.getSuffix().equals(song.getTranscodedSuffix()))) {
// If just downsampling, don't try to upsample (ie: 128 kpbs -> 192 kpbs)
- if(song.getBitRate() != null && br > song.getBitRate()) {
+ if(song.getBitRate() != null && (br == 0 || br > song.getBitRate())) {
br = song.getBitRate();
}
}
@@ -374,11 +371,37 @@ public class DownloadFile implements BufferFile {
}
}
+ public boolean isStream() {
+ return song != null && song instanceof InternetRadioStation;
+ }
+ public String getStream() {
+ if(song != null && song instanceof InternetRadioStation) {
+ InternetRadioStation station = (InternetRadioStation) song;
+ return station.getStreamUrl();
+ } else {
+ return null;
+ }
+ }
+
@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<Void> {
private MusicService musicService;
@@ -436,17 +459,15 @@ public class DownloadFile implements BufferFile {
}
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);
- }
+ HttpURLConnection connection = musicService.getDownloadInputStream(context, song, partialFile.length(), bitRate, DownloadTask.this);
+ long contentLength = connection.getContentLength();
+ if(contentLength > 0) {
+ Log.i(TAG, "Content Length: " + contentLength);
+ DownloadFile.this.contentLength = contentLength;
}
- in = response.getEntity().getContent();
- boolean partial = response.getStatusLine().getStatusCode() == HttpStatus.SC_PARTIAL_CONTENT;
+
+ in = connection.getInputStream();
+ boolean partial = connection.getResponseCode() == HttpURLConnection.HTTP_PARTIAL;
if (partial) {
Log.i(TAG, "Executed partial HTTP GET, skipping " + partialFile.length() + " bytes");
}
@@ -515,8 +536,9 @@ public class DownloadFile implements BufferFile {
}
// Only run these if not interrupted, ie: cancelled
- if(!isCancelled()) {
- new CacheCleaner(context, DownloadService.getInstance()).cleanSpace();
+ DownloadService downloadService = DownloadService.getInstance();
+ if(downloadService != null && !isCancelled()) {
+ new CacheCleaner(context, downloadService).cleanSpace();
checkDownloads();
}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/DownloadService.java b/app/src/main/java/github/daneren2005/dsub/service/DownloadService.java
index 6641d040..8213a7d4 100644
--- a/app/src/main/java/github/daneren2005/dsub/service/DownloadService.java
+++ b/app/src/main/java/github/daneren2005/dsub/service/DownloadService.java
@@ -35,6 +35,7 @@ import github.daneren2005.dsub.activity.SubsonicActivity;
import github.daneren2005.dsub.audiofx.AudioEffectsController;
import github.daneren2005.dsub.audiofx.EqualizerController;
import github.daneren2005.dsub.domain.Bookmark;
+import github.daneren2005.dsub.domain.InternetRadioStation;
import github.daneren2005.dsub.domain.MusicDirectory;
import github.daneren2005.dsub.domain.PlayerState;
import github.daneren2005.dsub.domain.PodcastEpisode;
@@ -60,6 +61,7 @@ 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;
@@ -76,6 +78,7 @@ 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;
@@ -87,6 +90,7 @@ import android.support.v7.media.MediaRouteSelector;
import android.support.v7.media.MediaRouter;
import android.util.Log;
import android.support.v4.util.LruCache;
+import android.view.KeyEvent;
/**
* @author Sindre Mehus
@@ -105,6 +109,7 @@ public class DownloadService extends Service {
public static final String START_PLAY = "github.daneren2005.dsub.START_PLAYING";
public static final int FAST_FORWARD = 30000;
public static final int REWIND = 10000;
+ private static final 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;
@@ -116,10 +121,11 @@ public class DownloadService extends Service {
public static final int METADATA_UPDATED_STAR = 1;
public static final int METADATA_UPDATED_RATING = 2;
public static final int METADATA_UPDATED_BOOKMARK = 4;
+ public static final int METADATA_UPDATED_COVER_ART = 8;
private RemoteControlClientBase mRemoteControl;
- private final IBinder binder = new SimpleServiceBinder<DownloadService>(this);
+ private final IBinder binder = new SimpleServiceBinder<>(this);
private Looper mediaPlayerLooper;
private MediaPlayer mediaPlayer;
private MediaPlayer nextMediaPlayer;
@@ -160,6 +166,7 @@ public class DownloadService extends Service {
private int cachedPosition = 0;
private boolean downloadOngoing = false;
private float volume = 1.0f;
+ private long delayUpdateProgress = DEFAULT_DELAY_UPDATE_PROGRESS;
private AudioEffectsController effectsController;
private RemoteControlState remoteState = LOCAL;
@@ -190,13 +197,16 @@ public class DownloadService extends Service {
mediaPlayer = new MediaPlayer();
mediaPlayer.setWakeMode(DownloadService.this, PowerManager.PARTIAL_WAKE_LOCK);
+ // We want to change audio session id's between upgrading Android versions. Upgrading to Android 7.0 is broken (probably updated session id format)
audioSessionId = -1;
- Integer id = prefs.getInt(Constants.CACHE_AUDIO_SESSION_ID, -1);
- if(id != -1) {
+ int id = prefs.getInt(Constants.CACHE_AUDIO_SESSION_ID, -1);
+ int versionCode = prefs.getInt(Constants.CACHE_AUDIO_SESSION_VERSION_CODE, -1);
+ if(versionCode == Build.VERSION.SDK_INT && id != -1) {
try {
audioSessionId = id;
mediaPlayer.setAudioSessionId(audioSessionId);
} catch (Throwable e) {
+ Log.w(TAG, "Failed to use cached audio session", e);
audioSessionId = -1;
}
}
@@ -205,7 +215,11 @@ public class DownloadService extends Service {
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
try {
audioSessionId = mediaPlayer.getAudioSessionId();
- prefs.edit().putInt(Constants.CACHE_AUDIO_SESSION_ID, audioSessionId).commit();
+
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putInt(Constants.CACHE_AUDIO_SESSION_ID, audioSessionId);
+ editor.putInt(Constants.CACHE_AUDIO_SESSION_VERSION_CODE, Build.VERSION.SDK_INT);
+ editor.commit();
} catch (Throwable t) {
// Froyo or lower
}
@@ -219,14 +233,14 @@ public class DownloadService extends Service {
}
});
- try {
+ /*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)) {
@@ -377,6 +391,10 @@ public class DownloadService extends Service {
handler.postDelayed(r, millis);
}
+ public synchronized void download(InternetRadioStation station) {
+ clear();
+ download(Arrays.asList((MusicDirectory.Entry) station), false, true, false, false);
+ }
public synchronized void download(List<MusicDirectory.Entry> songs, boolean save, boolean autoplay, boolean playNext, boolean shuffle) {
download(songs, save, autoplay, playNext, shuffle, 0, 0);
}
@@ -389,7 +407,10 @@ public class DownloadService extends Service {
if (songs.isEmpty()) {
return;
+ } else if(isCurrentPlayingSingle()) {
+ clear();
}
+
if (playNext) {
if (autoplay && getCurrentPlayingIndex() >= 0) {
offset = 0;
@@ -573,7 +594,6 @@ public class DownloadService extends Service {
public synchronized void setShufflePlayEnabled(boolean enabled) {
shufflePlay = enabled;
if (shufflePlay) {
- clear();
checkDownloads();
}
SharedPreferences.Editor editor = Util.getPreferences(this).edit();
@@ -600,6 +620,9 @@ public class DownloadService extends Service {
}
editor.commit();
}
+ public boolean isArtistRadio() {
+ return artistRadio;
+ }
public synchronized void shuffle() {
Collections.shuffle(downloadList);
@@ -739,7 +762,7 @@ public class DownloadService extends Service {
int position = getPlayerPosition();
int duration = getPlayerDuration();
boolean cutoff = isPastCutoff(position, duration, true);
- if(currentPlaying != null && currentPlaying.getSong() instanceof PodcastEpisode) {
+ if(currentPlaying != null && currentPlaying.getSong() instanceof PodcastEpisode && !currentPlaying.isSaved()) {
if(cutoff) {
currentPlaying.delete();
}
@@ -757,7 +780,7 @@ public class DownloadService extends Service {
checkAddBookmark();
}
if(currentPlaying != null) {
- scrobbler.conditionalScrobble(this, currentPlaying, position, duration);
+ scrobbler.conditionalScrobble(this, currentPlaying, position, duration, cutoff);
}
reset();
@@ -781,6 +804,10 @@ public class DownloadService extends Service {
suggestedPlaylistName = null;
suggestedPlaylistId = null;
+
+ setShufflePlayEnabled(false);
+ setArtistRadio(null);
+ checkDownloads();
}
public synchronized void remove(int which) {
@@ -807,6 +834,8 @@ public class DownloadService extends Service {
if(downloadFile == nextPlaying) {
setNextPlaying();
}
+
+ checkDownloads();
}
public synchronized void removeBackground(DownloadFile downloadFile) {
if (downloadFile == currentDownloading && downloadFile != currentPlaying && downloadFile != nextPlaying) {
@@ -843,6 +872,9 @@ public class DownloadService extends Service {
if(this.currentPlaying != null) {
this.currentPlaying.setPlaying(false);
}
+ if(delayUpdateProgress != DEFAULT_DELAY_UPDATE_PROGRESS && !isNextPlayingSameAlbum(currentPlaying, this.currentPlaying)) {
+// resetPlaybackSpeed();
+ }
this.currentPlaying = currentPlaying;
if(currentPlaying == null) {
currentPlayingIndex = -1;
@@ -853,7 +885,10 @@ public class DownloadService extends Service {
if (currentPlaying != null && currentPlaying.getSong() != null) {
Util.broadcastNewTrackInfo(this, currentPlaying.getSong());
- mRemoteControl.updateMetadata(this, currentPlaying.getSong());
+
+ if(mRemoteControl != null) {
+ mRemoteControl.updateMetadata(this, currentPlaying.getSong());
+ }
} else {
Util.broadcastNewTrackInfo(this, null);
Notifications.hidePlayingNotification(this, this, handler);
@@ -977,6 +1012,25 @@ public class DownloadService extends Service {
public List<DownloadFile> getToDelete() { return toDelete; }
+ public boolean isCurrentPlayingSingle() {
+ if(currentPlaying != null && currentPlaying.getSong() instanceof InternetRadioStation) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+ public boolean isCurrentPlayingStream() {
+ if(currentPlaying != null) {
+ return currentPlaying.isStream();
+ } else {
+ return false;
+ }
+ }
+
+ public synchronized boolean shouldFastForward() {
+ return size() == 1 || (currentPlaying != null && !currentPlaying.isSong());
+ }
+
public synchronized List<DownloadFile> getDownloads() {
List<DownloadFile> temp = new ArrayList<DownloadFile>();
temp.addAll(downloadList);
@@ -1033,6 +1087,8 @@ public class DownloadService extends Service {
bufferAndPlay(position, start);
checkDownloads();
setNextPlaying();
+ } else {
+ checkDownloads();
}
}
}
@@ -1070,6 +1126,7 @@ public class DownloadService extends Service {
setCurrentPlaying(nextPlaying, true);
setPlayerState(PlayerState.STARTED);
setupHandlers(currentPlaying, false, start);
+ applyPlaybackParamsMain();
setNextPlaying();
// Proxy should not be being used here since the next player was already setup to play
@@ -1120,6 +1177,27 @@ public class DownloadService extends Service {
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();
@@ -1128,12 +1206,13 @@ public class DownloadService extends Service {
}
// If only one song, just skip within song
- if(size() == 1) {
- seekTo(getPlayerPosition() - REWIND);
+ if(shouldFastForward()) {
+ rewind();
+ return;
+ } else if(playerState == PREPARING || playerState == PREPARED) {
return;
}
-
// Restart song if played more than five seconds.
if (getPlayerPosition() > 5000 || (index == 0 && getRepeatMode() != RepeatMode.ALL)) {
seekTo(0);
@@ -1154,8 +1233,8 @@ public class DownloadService extends Service {
}
public synchronized void next(boolean forceCutoff, boolean forceStart) {
// If only one song, just skip within song
- if(size() == 1) {
- seekTo(getPlayerPosition() + FAST_FORWARD);
+ if(shouldFastForward()) {
+ fastForward();
return;
} else if(playerState == PREPARING || playerState == PREPARED) {
return;
@@ -1170,7 +1249,7 @@ public class DownloadService extends Service {
} else {
cutoff = isPastCutoff(position, duration);
}
- if(currentPlaying != null && currentPlaying.getSong() instanceof PodcastEpisode) {
+ if(currentPlaying != null && currentPlaying.getSong() instanceof PodcastEpisode && !currentPlaying.isSaved()) {
if(cutoff) {
toDelete.add(currentPlaying);
}
@@ -1179,7 +1258,7 @@ public class DownloadService extends Service {
clearCurrentBookmark(true);
}
if(currentPlaying != null) {
- scrobbler.conditionalScrobble(this, currentPlaying, position, duration);
+ scrobbler.conditionalScrobble(this, currentPlaying, position, duration, cutoff);
}
int index = getCurrentPlayingIndex();
@@ -1258,6 +1337,7 @@ public class DownloadService extends Service {
// Only start if done preparing
if(playerState != PREPARING) {
mediaPlayer.start();
+ applyPlaybackParamsMain();
} else {
// Otherwise, we need to set it up to start when done preparing
autoPlayStart = true;
@@ -1372,6 +1452,9 @@ public class DownloadService extends Service {
if (playerState == PAUSED) {
lifecycleSupport.serializeDownloadQueue();
+ if(!isPastCutoff()) {
+ checkAddBookmark(true);
+ }
}
boolean show = playerState == PlayerState.STARTED;
@@ -1398,13 +1481,13 @@ public class DownloadService extends Service {
Notifications.hidePlayingNotification(this, this, handler);
}
if(mRemoteControl != null) {
- mRemoteControl.setPlaybackState(playerState.getRemoteControlClientPlayState());
+ mRemoteControl.setPlaybackState(playerState.getRemoteControlClientPlayState(), getCurrentPlayingIndex(), size());
}
if (playerState == STARTED) {
- scrobbler.scrobble(this, currentPlaying, false);
+ scrobbler.scrobble(this, currentPlaying, false, false);
} else if (playerState == COMPLETED) {
- scrobbler.scrobble(this, currentPlaying, true);
+ scrobbler.scrobble(this, currentPlaying, true, true);
}
if(playerState == STARTED && positionCache == null) {
@@ -1450,7 +1533,7 @@ public class DownloadService extends Service {
positionCache.stop();
positionCache = null;
}
- scrobbler.scrobble(this, currentPlaying, true);
+ scrobbler.scrobble(this, currentPlaying, true, true);
onStateUpdate();
}
@@ -1468,7 +1551,7 @@ public class DownloadService extends Service {
while(isRunning) {
try {
onSongProgress();
- Thread.sleep(1000L);
+ Thread.sleep(delayUpdateProgress);
}
catch(Exception e) {
isRunning = false;
@@ -1510,7 +1593,7 @@ public class DownloadService extends Service {
}
}
onSongProgress(cachedPosition < 2000 ? true: false);
- Thread.sleep(1000L);
+ Thread.sleep(delayUpdateProgress);
}
catch(Exception e) {
Log.w(TAG, "Crashed getting current position", e);
@@ -1587,6 +1670,9 @@ public class DownloadService extends Service {
return controller;
}
+ public MediaRouteManager getMediaRouter() {
+ return mediaRouter;
+ }
public MediaRouteSelector getRemoteSelector() {
return mediaRouter.getSelector();
}
@@ -1699,11 +1785,12 @@ public class DownloadService extends Service {
nextPlayingTask.cancel();
nextPlayingTask = null;
}
- }
- if(remoteState == LOCAL) {
- checkDownloads();
+ if(nextPlayerState != IDLE) {
+ setNextPlayerState(IDLE);
+ }
}
+ checkDownloads();
if(routeId != null) {
final Runnable delayedReconnect = new Runnable() {
@@ -1771,7 +1858,7 @@ public class DownloadService extends Service {
bufferAndPlay(position, true);
}
private synchronized void bufferAndPlay(int position, boolean start) {
- if(!currentPlaying.isCompleteFileAvailable()) {
+ if(!currentPlaying.isCompleteFileAvailable() && !currentPlaying.isStream()) {
if(Util.isAllowedToDownload(this)) {
reset();
@@ -1787,11 +1874,6 @@ public class DownloadService extends Service {
private synchronized void doPlay(final DownloadFile downloadFile, final int position, final boolean start) {
try {
- downloadFile.setPlaying(true);
- final File file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteFile() : downloadFile.getPartialFile();
- boolean isPartial = file.equals(downloadFile.getPartialFile());
- downloadFile.updateModificationDate();
-
subtractPosition = 0;
mediaPlayer.setOnCompletionListener(null);
mediaPlayer.setOnPreparedListener(null);
@@ -1803,19 +1885,33 @@ public class DownloadService extends Service {
} catch(Throwable e) {
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
}
- String dataSource = file.getAbsolutePath();
- if(isPartial && !Util.isOffline(this)) {
- if (proxy == null) {
- proxy = new BufferProxy(this);
- proxy.start();
- }
- proxy.setBufferFile(downloadFile);
- dataSource = proxy.getPrivateAddress(dataSource);
+
+ String dataSource;
+ boolean isPartial = false;
+ if(downloadFile.isStream()) {
+ dataSource = downloadFile.getStream();
Log.i(TAG, "Data Source: " + dataSource);
- } else if(proxy != null) {
- proxy.stop();
- proxy = null;
+ } else {
+ 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);
@@ -1844,6 +1940,7 @@ public class DownloadService extends Service {
if (start || autoPlayStart) {
mediaPlayer.start();
+ applyPlaybackParamsMain();
setPlayerState(STARTED);
// Disable autoPlayStart after done
@@ -2111,13 +2208,16 @@ public class DownloadService extends Service {
checkArtistRadio();
}
- if (!Util.isNetworkConnected(this, true) || Util.isOffline(this)) {
+ if (!Util.isAllowedToDownload(this)) {
return;
}
if (downloadList.isEmpty() && backgroundDownloadList.isEmpty()) {
return;
}
+ if(currentPlaying != null && currentPlaying.isStream()) {
+ return;
+ }
// Need to download current playing and not casting?
if (currentPlaying != null && remoteState == LOCAL && currentPlaying != currentDownloading && !currentPlaying.isWorkDone()) {
@@ -2138,7 +2238,7 @@ public class DownloadService extends Service {
int preloaded = 0;
- if(n != 0 && remoteState == LOCAL) {
+ if(n != 0 && (remoteState == LOCAL || Util.shouldCacheDuringCasting(this))) {
int start = currentPlaying == null ? 0 : getCurrentPlayingIndex();
if(start == -1) {
start = 0;
@@ -2342,7 +2442,7 @@ public class DownloadService extends Service {
}
// Make cutoff a maximum of 10 minutes
- int cutoffPoint = Math.min((int) (duration * DELETE_CUTOFF), 10 * 60 * 1000);
+ int cutoffPoint = Math.max((int) (duration * DELETE_CUTOFF), duration - 10 * 60 * 1000);
boolean isPastCutoff = duration > 0 && position > cutoffPoint;
// Check to make sure song isn't within 10 seconds of where it was created
@@ -2423,8 +2523,11 @@ public class DownloadService extends Service {
}.execute();
}
}
-
+
private void checkAddBookmark() {
+ checkAddBookmark(false);
+ }
+ private void checkAddBookmark(final boolean updateMetadata) {
// Don't do anything if no current playing
if(currentPlaying == null || !ServerInfo.canBookmark(this)) {
return;
@@ -2454,6 +2557,9 @@ public class DownloadService extends Service {
if(found != null) {
found.setBookmark(new Bookmark(position));
}
+ if(updateMetadata) {
+ onMetadataUpdate(METADATA_UPDATED_BOOKMARK);
+ }
return null;
}
@@ -2482,9 +2588,9 @@ public class DownloadService extends Service {
SharedPreferences prefs = Util.getPreferences(this);
try {
- float[] rg = BastpUtil.getReplayGainValues(downloadFile.getFile().getCanonicalPath()); /* track, album */
float adjust = 0f;
if (prefs.getBoolean(Constants.PREFERENCES_KEY_REPLAY_GAIN, false)) {
+ float[] rg = BastpUtil.getReplayGainValues(downloadFile.getFile().getCanonicalPath()); /* track, album */
boolean singleAlbum = false;
String replayGainType = prefs.getString(Constants.PREFERENCES_KEY_REPLAY_GAIN_TYPE, "1");
@@ -2558,6 +2664,63 @@ public class DownloadService extends Service {
}
}
+ public void setPlaybackSpeed(float playbackSpeed) {
+ if(currentPlaying.isSong())
+ Util.getPreferences(this).edit().putFloat(Constants.PREFERENCES_KEY_SONG_PLAYBACK_SPEED, playbackSpeed).commit();
+ else
+ Util.getPreferences(this).edit().putFloat(Constants.PREFERENCES_KEY_PLAYBACK_SPEED, playbackSpeed).commit();
+ if(mediaPlayer != null && (playerState == PREPARED || playerState == STARTED || playerState == PAUSED || playerState == PAUSED_TEMP)) {
+ applyPlaybackParamsMain();
+ }
+
+ delayUpdateProgress = Math.round(DEFAULT_DELAY_UPDATE_PROGRESS / playbackSpeed);
+ }
+ private void resetPlaybackSpeed() {
+ Util.getPreferences(this).edit().remove(Constants.PREFERENCES_KEY_PLAYBACK_SPEED).commit();
+ Util.getPreferences(this).edit().remove(Constants.PREFERENCES_KEY_SONG_PLAYBACK_SPEED).commit();
+ }
+
+ public float getPlaybackSpeed() {
+ if (currentPlaying == null)
+ return 1.0f;
+ else {
+ if (currentPlaying.isSong())
+ return Util.getPreferences(this).getFloat(Constants.PREFERENCES_KEY_SONG_PLAYBACK_SPEED, 1.0f);
+ else
+ return Util.getPreferences(this).getFloat(Constants.PREFERENCES_KEY_PLAYBACK_SPEED, 1.0f);
+ }
+ }
+
+ private synchronized void applyPlaybackParamsMain() {
+ applyPlaybackParams(mediaPlayer);
+ }
+ 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());
+ }
+ }
+
+ private synchronized void applyPlaybackParams(MediaPlayer mediaPlayer) {
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ float playbackSpeed = getPlaybackSpeed();
+
+ try {
+ if (Math.abs(playbackSpeed - 1.0) > 0.01 || mediaPlayer.getPlaybackParams() != null) {
+ PlaybackParams playbackParams = new PlaybackParams();
+ playbackParams.setSpeed(playbackSpeed);
+ mediaPlayer.setPlaybackParams(playbackParams);
+ }
+ } catch(Exception e) {
+ Log.e(TAG, "Error while applying media player params", e);
+ }
+ }
+ }
+
public void toggleStarred() {
final DownloadFile currentPlaying = this.currentPlaying;
if(currentPlaying == null) {
@@ -2571,6 +2734,11 @@ public class DownloadService extends Service {
onMetadataUpdate(METADATA_UPDATED_STAR);
}
}
+
+ @Override
+ public void starCommited(boolean starred) {
+
+ }
});
}
public void toggleRating(int rating) {
@@ -2600,7 +2768,7 @@ public class DownloadService extends Service {
UpdateHelper.setRating(this, entry, rating, new UpdateHelper.OnRatingChange() {
@Override
public void ratingChange(int rating) {
- if(currentPlaying == DownloadService.this.currentPlaying) {
+ if (currentPlaying == DownloadService.this.currentPlaying) {
onMetadataUpdate(METADATA_UPDATED_RATING);
}
}
@@ -2613,13 +2781,19 @@ public class DownloadService extends Service {
wakeLock.acquire(ms);
}
+ public void handleKeyEvent(KeyEvent keyEvent) {
+ lifecycleSupport.handleKeyEvent(keyEvent);
+ }
+
public void addOnSongChangedListener(OnSongChangedListener listener) {
addOnSongChangedListener(listener, false);
}
- public synchronized void addOnSongChangedListener(OnSongChangedListener listener, boolean run) {
- int index = onSongChangedListeners.indexOf(listener);
- if(index == -1) {
- onSongChangedListeners.add(listener);
+ public void addOnSongChangedListener(OnSongChangedListener listener, boolean run) {
+ synchronized(onSongChangedListeners) {
+ int index = onSongChangedListeners.indexOf(listener);
+ if (index == -1) {
+ onSongChangedListeners.add(listener);
+ }
}
if(run) {
@@ -2630,6 +2804,7 @@ public class DownloadService extends Service {
onSongsChanged();
onSongProgress();
onStateUpdate();
+ onMetadataUpdate(METADATA_UPDATED_ALL);
}
});
} else {
@@ -2637,53 +2812,61 @@ public class DownloadService extends Service {
}
}
}
- public synchronized void removeOnSongChangeListener(OnSongChangedListener listener) {
- int index = onSongChangedListeners.indexOf(listener);
- if(index != -1) {
- onSongChangedListeners.remove(index);
+ public void removeOnSongChangeListener(OnSongChangedListener listener) {
+ synchronized(onSongChangedListeners) {
+ int index = onSongChangedListeners.indexOf(listener);
+ if (index != -1) {
+ onSongChangedListeners.remove(index);
+ }
}
}
- private synchronized void onSongChanged() {
+ private void onSongChanged() {
final long atRevision = revision;
- for(final OnSongChangedListener listener: onSongChangedListeners) {
- handler.post(new Runnable() {
- @Override
- public void run() {
- if(revision == atRevision && instance != null) {
- listener.onSongChanged(currentPlaying, currentPlayingIndex);
+ synchronized(onSongChangedListeners) {
+ final boolean shouldFastForward = shouldFastForward();
+ for (final OnSongChangedListener listener : onSongChangedListeners) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (revision == atRevision && instance != null) {
+ listener.onSongChanged(currentPlaying, currentPlayingIndex, shouldFastForward);
- MusicDirectory.Entry entry = currentPlaying != null ? currentPlaying.getSong() : null;
- listener.onMetadataUpdate(entry, METADATA_UPDATED_ALL);
+ 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();
- }
- });
+ if (mediaPlayerHandler != null && !onSongChangedListeners.isEmpty()) {
+ mediaPlayerHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ onSongProgress();
+ }
+ });
+ }
}
}
- private synchronized void onSongsChanged() {
+ private void onSongsChanged() {
final long atRevision = revision;
- for(final OnSongChangedListener listener: onSongChangedListeners) {
- handler.post(new Runnable() {
- @Override
- public void run() {
- if(revision == atRevision && instance != null) {
- listener.onSongsChanged(downloadList, currentPlaying, currentPlayingIndex);
+ synchronized(onSongChangedListeners) {
+ final boolean shouldFastForward = shouldFastForward();
+ for (final OnSongChangedListener listener : onSongChangedListeners) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (revision == atRevision && instance != null) {
+ listener.onSongsChanged(downloadList, currentPlaying, currentPlayingIndex, shouldFastForward);
+ }
}
- }
- });
+ });
+ }
}
}
- private synchronized void onSongProgress() {
+ private void onSongProgress() {
onSongProgress(true);
}
private synchronized void onSongProgress(boolean manual) {
@@ -2691,22 +2874,29 @@ public class DownloadService extends Service {
final Integer duration = getPlayerDuration();
final boolean isSeekable = isSeekable();
final int position = getPlayerPosition();
- for(final OnSongChangedListener listener: onSongChangedListeners) {
- handler.post(new Runnable() {
- @Override
- public void run() {
- if(revision == atRevision && instance != null) {
- listener.onSongProgress(currentPlaying, position, duration, isSeekable);
+ final int index = getCurrentPlayingIndex();
+ final int queueSize = size();
+
+ 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() {
- mRemoteControl.setPlaybackState(playerState.getRemoteControlClientPlayState());
+ if(mRemoteControl != null) {
+ mRemoteControl.setPlaybackState(playerState.getRemoteControlClientPlayState(), index, queueSize);
+ }
}
});
}
@@ -2718,39 +2908,45 @@ public class DownloadService extends Service {
}
}
}
- private synchronized void onStateUpdate() {
+ private void onStateUpdate() {
final long atRevision = revision;
- for(final OnSongChangedListener listener: onSongChangedListeners) {
- handler.post(new Runnable() {
- @Override
- public void run() {
- if(revision == atRevision && instance != null) {
- listener.onStateUpdate(currentPlaying, playerState);
+ synchronized(onSongChangedListeners) {
+ for (final OnSongChangedListener listener : onSongChangedListeners) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (revision == atRevision && instance != null) {
+ listener.onStateUpdate(currentPlaying, playerState);
+ }
}
- }
- });
+ });
+ }
}
}
- private synchronized void onMetadataUpdate() {
+ public void onMetadataUpdate() {
onMetadataUpdate(METADATA_UPDATED_ALL);
}
- private synchronized void onMetadataUpdate(final int updateType) {
- 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);
+ 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() {
- mRemoteControl.metadataChanged(currentPlaying.getSong());
+ if(currentPlaying != null) {
+ mRemoteControl.metadataChanged(currentPlaying.getSong());
+ }
}
});
}
@@ -2862,8 +3058,8 @@ public class DownloadService extends Service {
}
public interface OnSongChangedListener {
- void onSongChanged(DownloadFile currentPlaying, int currentPlayingIndex);
- void onSongsChanged(List<DownloadFile> songs, DownloadFile currentPlaying, int currentPlayingIndex);
+ void onSongChanged(DownloadFile currentPlaying, int currentPlayingIndex, boolean shouldFastForward);
+ void onSongsChanged(List<DownloadFile> songs, DownloadFile currentPlaying, int currentPlayingIndex, boolean shouldFastForward);
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/daneren2005/dsub/service/DownloadServiceLifecycleSupport.java b/app/src/main/java/github/daneren2005/dsub/service/DownloadServiceLifecycleSupport.java
index 4989db40..1c80b622 100644
--- a/app/src/main/java/github/daneren2005/dsub/service/DownloadServiceLifecycleSupport.java
+++ b/app/src/main/java/github/daneren2005/dsub/service/DownloadServiceLifecycleSupport.java
@@ -36,6 +36,8 @@ import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.util.Log;
import android.view.KeyEvent;
+
+import github.daneren2005.dsub.domain.InternetRadioStation;
import github.daneren2005.dsub.domain.MusicDirectory;
import github.daneren2005.dsub.domain.PlayerQueue;
import github.daneren2005.dsub.domain.PlayerState;
@@ -56,6 +58,7 @@ import static github.daneren2005.dsub.domain.PlayerState.PREPARING;
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;
@@ -154,6 +157,8 @@ public class DownloadServiceLifecycleSupport {
// 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);
@@ -225,6 +230,7 @@ public class DownloadServiceLifecycleSupport {
}
editor.commit();
+ downloadService.clear();
downloadService.setShufflePlayEnabled(true);
} else {
downloadService.start();
@@ -306,7 +312,7 @@ public class DownloadServiceLifecycleSupport {
FileUtil.serialize(downloadService, state, FILENAME_DOWNLOADS_SER);
// If we are on Subsonic 5.2+, save play queue
- if(serializeRemote && ServerInfo.canSavePlayQueue(downloadService) && !Util.isOffline(downloadService) && state.songs.size() > 0) {
+ if(serializeRemote && ServerInfo.canSavePlayQueue(downloadService) && !Util.isOffline(downloadService) && state.songs.size() > 0 && !(state.songs.get(0) instanceof InternetRadioStation)) {
// Cancel any currently running tasks
if(currentSavePlayQueueTask != null) {
currentSavePlayQueueTask.cancel();
@@ -384,14 +390,25 @@ public class DownloadServiceLifecycleSupport {
return lastChange;
}
- private void handleKeyEvent(KeyEvent event) {
- if(event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
+ public void handleKeyEvent(KeyEvent event) {
+ if(event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() > 0) {
+ switch (event.getKeyCode()) {
+ case RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS:
+ case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
+ downloadService.fastForward();
+ break;
+ case RemoteControlClient.FLAG_KEY_MEDIA_NEXT:
+ case KeyEvent.KEYCODE_MEDIA_NEXT:
+ downloadService.rewind();
+ break;
+ }
+ } else if(event.getAction() == KeyEvent.ACTION_UP) {
switch (event.getKeyCode()) {
case RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE:
- case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
downloadService.togglePlayPause();
break;
case KeyEvent.KEYCODE_HEADSETHOOK:
+ case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
if(lastPressTime < (System.currentTimeMillis() - 500)) {
lastPressTime = System.currentTimeMillis();
downloadService.togglePlayPause();
@@ -401,11 +418,23 @@ public class DownloadServiceLifecycleSupport {
break;
case RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS:
case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
- downloadService.previous();
+ if(lastPressTime < (System.currentTimeMillis() - DEBOUNCE_TIME)) {
+ lastPressTime = System.currentTimeMillis();
+ downloadService.previous();
+ }
break;
case RemoteControlClient.FLAG_KEY_MEDIA_NEXT:
case KeyEvent.KEYCODE_MEDIA_NEXT:
- downloadService.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 RemoteControlClient.FLAG_KEY_MEDIA_STOP:
case KeyEvent.KEYCODE_MEDIA_STOP:
diff --git a/app/src/main/java/github/daneren2005/dsub/service/JukeboxController.java b/app/src/main/java/github/daneren2005/dsub/service/JukeboxController.java
index e9d7cbc8..b9f40f32 100644
--- a/app/src/main/java/github/daneren2005/dsub/service/JukeboxController.java
+++ b/app/src/main/java/github/daneren2005/dsub/service/JukeboxController.java
@@ -22,6 +22,7 @@ import github.daneren2005.dsub.R;
import github.daneren2005.dsub.domain.RemoteStatus;
import github.daneren2005.dsub.domain.PlayerState;
import github.daneren2005.dsub.domain.RemoteControlState;
+import github.daneren2005.dsub.domain.RepeatMode;
import github.daneren2005.dsub.service.parser.SubsonicRESTException;
import github.daneren2005.dsub.util.Util;
@@ -47,7 +48,7 @@ public class JukeboxController extends RemoteController {
private float gain = 0.5f;
public JukeboxController(DownloadService downloadService, Handler handler) {
- this.downloadService = downloadService;
+ super(downloadService);
this.handler = handler;
}
@@ -200,11 +201,15 @@ public class JukeboxController extends RemoteController {
// Track change?
Integer index = jukeboxStatus.getCurrentPlayingIndex();
+ int currentPlayingIndex = downloadService.getCurrentPlayingIndex();
if (index != null && index != -1 && index != downloadService.getCurrentPlayingIndex()) {
downloadService.setPlayerState(PlayerState.COMPLETED);
downloadService.setCurrentPlaying(index, true);
if(jukeboxStatus.isPlaying()) {
downloadService.setPlayerState(PlayerState.STARTED);
+ } else if(index == 0 && currentPlayingIndex == downloadService.size() - 1 && downloadService.getRepeatMode() == RepeatMode.ALL) {
+ // Jukebox does not support any form of auto repeat
+ start();
}
}
}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/MusicService.java b/app/src/main/java/github/daneren2005/dsub/service/MusicService.java
index 2972bb7c..6a58e340 100644
--- a/app/src/main/java/github/daneren2005/dsub/service/MusicService.java
+++ b/app/src/main/java/github/daneren2005/dsub/service/MusicService.java
@@ -18,18 +18,18 @@
*/
package github.daneren2005.dsub.service;
+import java.net.HttpURLConnection;
import java.util.List;
-import org.apache.http.HttpResponse;
import android.content.Context;
import android.graphics.Bitmap;
import github.daneren2005.dsub.domain.ArtistInfo;
-import github.daneren2005.dsub.domain.Bookmark;
import github.daneren2005.dsub.domain.ChatMessage;
import github.daneren2005.dsub.domain.Genre;
import github.daneren2005.dsub.domain.Indexes;
+import github.daneren2005.dsub.domain.InternetRadioStation;
import github.daneren2005.dsub.domain.PlayerQueue;
import github.daneren2005.dsub.domain.RemoteStatus;
import github.daneren2005.dsub.domain.Lyrics;
@@ -94,6 +94,8 @@ public interface MusicService {
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;
@@ -101,7 +103,7 @@ public interface MusicService {
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;
+ HttpURLConnection 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;
@@ -147,7 +149,7 @@ public interface MusicService {
MusicDirectory getPodcastEpisodes(boolean refresh, String id, Context context, ProgressListener progressListener) throws Exception;
- MusicDirectory getNewestPodcastEpisodes(int count, Context context, ProgressListener progressListener) throws Exception;
+ MusicDirectory getNewestPodcastEpisodes(boolean refresh, Context context, ProgressListener progressListener, int count) throws Exception;
void refreshPodcasts(Context context, ProgressListener progressListener) throws Exception;
@@ -192,6 +194,8 @@ public interface MusicService {
void savePlayQueue(List<MusicDirectory.Entry> songs, MusicDirectory.Entry currentPlaying, int position, Context context, ProgressListener progressListener) throws Exception;
PlayerQueue getPlayQueue(Context context, ProgressListener progressListener) throws Exception;
+
+ List<InternetRadioStation> getInternetRadioStations(boolean refresh, Context context, ProgressListener progressListener) throws Exception;
int processOfflineSyncs(final Context context, final ProgressListener progressListener) throws Exception;
diff --git a/app/src/main/java/github/daneren2005/dsub/service/OfflineMusicService.java b/app/src/main/java/github/daneren2005/dsub/service/OfflineMusicService.java
index b8633624..da6c37f1 100644
--- a/app/src/main/java/github/daneren2005/dsub/service/OfflineMusicService.java
+++ b/app/src/main/java/github/daneren2005/dsub/service/OfflineMusicService.java
@@ -21,6 +21,7 @@ package github.daneren2005.dsub.service;
import java.io.File;
import java.io.Reader;
import java.io.FileReader;
+import java.net.HttpURLConnection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
@@ -34,13 +35,12 @@ import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.util.Log;
-import org.apache.http.HttpResponse;
-
import github.daneren2005.dsub.domain.Artist;
import github.daneren2005.dsub.domain.ArtistInfo;
import github.daneren2005.dsub.domain.ChatMessage;
import github.daneren2005.dsub.domain.Genre;
import github.daneren2005.dsub.domain.Indexes;
+import github.daneren2005.dsub.domain.InternetRadioStation;
import github.daneren2005.dsub.domain.MusicDirectory.Entry;
import github.daneren2005.dsub.domain.PlayerQueue;
import github.daneren2005.dsub.domain.PodcastEpisode;
@@ -95,7 +95,7 @@ public class OfflineMusicService implements MusicService {
artist.setIndex(file.getName().substring(0, 1));
artist.setName(file.getName());
artists.add(artist);
- } else {
+ } else if(!file.getName().equals("albumart.jpg") && !file.getName().equals(".nomedia")) {
entries.add(createEntry(context, file));
}
}
@@ -226,7 +226,7 @@ public class OfflineMusicService implements MusicService {
}
@Override
- public HttpResponse getDownloadInputStream(Context context, Entry song, long offset, int maxBitrate, SilentBackgroundTask task) throws Exception {
+ public HttpURLConnection getDownloadInputStream(Context context, Entry song, long offset, int maxBitrate, SilentBackgroundTask task) throws Exception {
throw new OfflineException(ERRORMSG);
}
@@ -307,7 +307,15 @@ public class OfflineMusicService implements MusicService {
}
}
});
-
+
+ // 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);
}
@@ -359,20 +367,13 @@ public class OfflineMusicService implements MusicService {
}
}
private int matchCriteria(SearchCritera criteria, String name) {
- String query = criteria.getQuery().toLowerCase();
- String[] queryParts = query.split(" ");
- String[] nameParts = name.toLowerCase().split(" ");
-
- int closeness = 0;
- for(String queryPart : queryParts) {
- for(String namePart : nameParts) {
- if(namePart.equals(queryPart)) {
- closeness++;
- }
- }
+ if (criteria.getPattern().matcher(name).matches()) {
+ return Util.getStringDistance(
+ criteria.getQuery().toLowerCase(),
+ name.toLowerCase());
+ } else {
+ return 0;
}
-
- return closeness;
}
@Override
@@ -587,6 +588,11 @@ public class OfflineMusicService implements MusicService {
}
@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);
}
@@ -769,7 +775,7 @@ public class OfflineMusicService implements MusicService {
}
@Override
- public MusicDirectory getNewestPodcastEpisodes(int count, Context context, ProgressListener progressListener) throws Exception {
+ public MusicDirectory getNewestPodcastEpisodes(boolean refresh, Context context, ProgressListener progressListener, int count) throws Exception {
throw new OfflineException(ERRORMSG);
}
@@ -884,6 +890,11 @@ public class OfflineMusicService implements MusicService {
}
@Override
+ public List<InternetRadioStation> getInternetRadioStations(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException(ERRORMSG);
+ }
+
+ @Override
public int processOfflineSyncs(final Context context, final ProgressListener progressListener) throws Exception{
throw new OfflineException(ERRORMSG);
}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/RESTMusicService.java b/app/src/main/java/github/daneren2005/dsub/service/RESTMusicService.java
index 4a6e5108..657ac4a9 100644
--- a/app/src/main/java/github/daneren2005/dsub/service/RESTMusicService.java
+++ b/app/src/main/java/github/daneren2005/dsub/service/RESTMusicService.java
@@ -24,61 +24,37 @@ import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
+import java.net.HttpURLConnection;
+import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
+import java.util.HashMap;
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.Base64;
import android.util.Log;
+import com.google.android.gms.security.ProviderInstaller;
+
import github.daneren2005.dsub.R;
import github.daneren2005.dsub.domain.*;
-import github.daneren2005.dsub.service.parser.AlbumListParser;
+import github.daneren2005.dsub.fragments.MainFragment;
+import github.daneren2005.dsub.service.parser.EntryListParser;
import github.daneren2005.dsub.service.parser.ArtistInfoParser;
import github.daneren2005.dsub.service.parser.BookmarkParser;
import github.daneren2005.dsub.service.parser.ChatMessageParser;
import github.daneren2005.dsub.service.parser.ErrorParser;
import github.daneren2005.dsub.service.parser.GenreParser;
import github.daneren2005.dsub.service.parser.IndexesParser;
+import github.daneren2005.dsub.service.parser.InternetRadioStationParser;
import github.daneren2005.dsub.service.parser.JukeboxStatusParser;
import github.daneren2005.dsub.service.parser.LicenseParser;
import github.daneren2005.dsub.service.parser.LyricsParser;
@@ -95,11 +71,9 @@ import github.daneren2005.dsub.service.parser.SearchResult2Parser;
import github.daneren2005.dsub.service.parser.SearchResultParser;
import github.daneren2005.dsub.service.parser.ShareParser;
import github.daneren2005.dsub.service.parser.StarredListParser;
+import github.daneren2005.dsub.service.parser.TopSongsParser;
import github.daneren2005.dsub.service.parser.UserParser;
import github.daneren2005.dsub.service.parser.VideosParser;
-import github.daneren2005.dsub.service.ssl.SSLSocketFactory;
-import github.daneren2005.dsub.service.ssl.TrustSelfSignedStrategy;
-import github.daneren2005.dsub.util.BackgroundTask;
import github.daneren2005.dsub.util.Pair;
import github.daneren2005.dsub.util.SilentBackgroundTask;
import github.daneren2005.dsub.util.Constants;
@@ -108,19 +82,23 @@ import github.daneren2005.dsub.util.ProgressListener;
import github.daneren2005.dsub.util.SongDBHandler;
import github.daneren2005.dsub.util.Util;
import java.io.*;
+import java.util.Map;
import java.util.zip.GZIPInputStream;
-/**
- * @author Sindre Mehus
- */
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
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.
@@ -129,51 +107,46 @@ public class RESTMusicService implements MusicService {
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 SSLSocketFactory sslSocketFactory;
+ private HostnameVerifier selfSignedHostnameVerifier;
private long redirectionLastChecked;
private int redirectionNetworkType = -1;
private String redirectFrom;
private String redirectTo;
- private final ThreadSafeClientConnManager connManager;
private Integer instance;
+ private boolean hasInstalledGoogleSSL = false;
public RESTMusicService() {
+ TrustManager[] trustAllCerts = new TrustManager[]{
+ new X509TrustManager() {
+ public java.security.cert.X509Certificate[] getAcceptedIssuers() {
+ return null;
+ }
+ public void checkClientTrusted(
+ java.security.cert.X509Certificate[] certs, String authType) {
+ }
+ public void checkServerTrusted(
+ java.security.cert.X509Certificate[] certs, String authType) {
+ }
+ }
+ };
+ try {
+ SSLContext sslContext = SSLContext.getInstance("TLS");
+ sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
+ sslSocketFactory = sslContext.getSocketFactory();
+ } catch (Exception e) {
+ }
- // 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();
- }
+ selfSignedHostnameVerifier = new HostnameVerifier() {
+ public boolean verify(String hostname, SSLSession session) {
+ return true;
+ }
+ };
}
@Override
public void ping(Context context, ProgressListener progressListener) throws Exception {
- Reader reader = getReader(context, progressListener, "ping", null);
+ Reader reader = getReader(context, progressListener, "ping");
try {
new ErrorParser(context, getInstance(context)).parse(reader);
} finally {
@@ -184,7 +157,7 @@ public class RESTMusicService implements MusicService {
@Override
public boolean isLicenseValid(Context context, ProgressListener progressListener) throws Exception {
- Reader reader = getReader(context, progressListener, "getLicense", null);
+ Reader reader = getReader(context, progressListener, "getLicense");
try {
ServerInfo serverInfo = new LicenseParser(context, getInstance(context)).parse(reader);
return serverInfo.isLicenseValid();
@@ -194,7 +167,7 @@ public class RESTMusicService implements MusicService {
}
public List<MusicFolder> getMusicFolders(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
- Reader reader = getReader(context, progressListener, "getMusicFolders", null);
+ Reader reader = getReader(context, progressListener, "getMusicFolders");
try {
return new MusicFoldersParser(context, getInstance(context)).parse(reader, progressListener);
} finally {
@@ -204,7 +177,17 @@ public class RESTMusicService implements MusicService {
@Override
public void startRescan(Context context, ProgressListener listener) throws Exception {
- Reader reader = getReader(context, listener, "startRescan", null);
+ String startMethod = ServerInfo.isMadsonic(context, getInstance(context)) ? "startRescan" : "startScan";
+ String refreshMethod = null;
+ if(ServerInfo.isMadsonic(context, getInstance(context))) {
+ startMethod = "startRescan";
+ refreshMethod = "scanstatus";
+ } else {
+ startMethod = "startScan";
+ refreshMethod = "getScanStatus";
+ }
+
+ Reader reader = getReader(context, listener, startMethod);
try {
new ErrorParser(context, getInstance(context)).parse(reader);
} finally {
@@ -214,7 +197,7 @@ public class RESTMusicService implements MusicService {
// Now check if still running
boolean done = false;
while(!done) {
- reader = getReader(context, null, "scanstatus", null);
+ reader = getReader(context, null, refreshMethod);
try {
boolean running = new ScanStatusParser(context, getInstance(context)).parse(reader, listener);
if(running) {
@@ -241,7 +224,7 @@ public class RESTMusicService implements MusicService {
parameterValues.add(musicFolderId);
}
- Reader reader = getReader(context, progressListener, Util.isTagBrowsing(context, getInstance(context)) ? "getArtists" : "getIndexes", null, parameterNames, parameterValues);
+ Reader reader = getReader(context, progressListener, Util.isTagBrowsing(context, getInstance(context)) ? "getArtists" : "getIndexes", parameterNames, parameterValues);
try {
return new IndexesParser(context, getInstance(context)).parse(reader, progressListener);
} finally {
@@ -287,7 +270,7 @@ public class RESTMusicService implements MusicService {
}
private MusicDirectory getMusicDirectoryImpl(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception {
- Reader reader = getReader(context, progressListener, "getMusicDirectory", null, "id", id);
+ Reader reader = getReader(context, progressListener, "getMusicDirectory", "id", id);
try {
return new MusicDirectoryParser(context, getInstance(context)).parse(name, reader, progressListener);
} finally {
@@ -297,7 +280,7 @@ public class RESTMusicService implements MusicService {
@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);
+ Reader reader = getReader(context, progressListener, "getArtist", "id", id);
try {
return new MusicDirectoryParser(context, getInstance(context)).parse(name, reader, progressListener);
} finally {
@@ -307,7 +290,7 @@ public class RESTMusicService implements MusicService {
@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);
+ Reader reader = getReader(context, progressListener, "getAlbum", "id", id);
try {
return new MusicDirectoryParser(context, getInstance(context)).parse(name, reader, progressListener);
} finally {
@@ -331,7 +314,7 @@ public class RESTMusicService implements MusicService {
private SearchResult searchOld(SearchCritera critera, Context context, ProgressListener progressListener) throws Exception {
List<String> parameterNames = Arrays.asList("any", "songCount");
List<Object> parameterValues = Arrays.<Object>asList(critera.getQuery(), critera.getSongCount());
- Reader reader = getReader(context, progressListener, "search", null, parameterNames, parameterValues);
+ Reader reader = getReader(context, progressListener, "search", parameterNames, parameterValues);
try {
return new SearchResultParser(context, getInstance(context)).parse(reader, progressListener);
} finally {
@@ -363,7 +346,7 @@ public class RESTMusicService implements MusicService {
method = "search2";
}
}
- Reader reader = getReader(context, progressListener, method, null, parameterNames, parameterValues);
+ Reader reader = getReader(context, progressListener, method, parameterNames, parameterValues);
try {
return new SearchResult2Parser(context, getInstance(context)).parse(reader, progressListener);
} finally {
@@ -373,10 +356,7 @@ public class RESTMusicService implements MusicService {
@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);
+ Reader reader = getReader(context, progressListener, "getPlaylist", "id", id, SOCKET_READ_TIMEOUT_GET_PLAYLIST);
try {
return new PlaylistParser(context, getInstance(context)).parse(reader, progressListener);
} finally {
@@ -386,7 +366,7 @@ public class RESTMusicService implements MusicService {
@Override
public List<Playlist> getPlaylists(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
- Reader reader = getReader(context, progressListener, "getPlaylists", null);
+ Reader reader = getReader(context, progressListener, "getPlaylists");
try {
return new PlaylistsParser(context, getInstance(context)).parse(reader, progressListener);
} finally {
@@ -412,7 +392,7 @@ public class RESTMusicService implements MusicService {
parameterValues.add(getOfflineSongId(entry.getId(), context, progressListener));
}
- Reader reader = getReader(context, progressListener, "createPlaylist", null, parameterNames, parameterValues);
+ Reader reader = getReader(context, progressListener, "createPlaylist", parameterNames, parameterValues);
try {
new ErrorParser(context, getInstance(context)).parse(reader);
} finally {
@@ -422,7 +402,7 @@ public class RESTMusicService implements MusicService {
@Override
public void deletePlaylist(String id, Context context, ProgressListener progressListener) throws Exception {
- Reader reader = getReader(context, progressListener, "deletePlaylist", null, "id", id);
+ Reader reader = getReader(context, progressListener, "deletePlaylist", "id", id);
try {
new ErrorParser(context, getInstance(context)).parse(reader);
} finally {
@@ -441,7 +421,7 @@ public class RESTMusicService implements MusicService {
names.add("songIdToAdd");
values.add(getOfflineSongId(song.getId(), context, progressListener));
}
- Reader reader = getReader(context, progressListener, "updatePlaylist", null, names, values);
+ Reader reader = getReader(context, progressListener, "updatePlaylist", names, values);
try {
new ErrorParser(context, getInstance(context)).parse(reader);
} finally {
@@ -460,7 +440,7 @@ public class RESTMusicService implements MusicService {
names.add("songIndexToRemove");
values.add(song);
}
- Reader reader = getReader(context, progressListener, "updatePlaylist", null, names, values);
+ Reader reader = getReader(context, progressListener, "updatePlaylist", names, values);
try {
new ErrorParser(context, getInstance(context)).parse(reader);
} finally {
@@ -485,7 +465,7 @@ public class RESTMusicService implements MusicService {
names.add("songIndexToRemove");
values.add(i);
}
- Reader reader = getReader(context, progressListener, "updatePlaylist", null, names, values);
+ Reader reader = getReader(context, progressListener, "updatePlaylist", names, values);
try {
new ErrorParser(context, getInstance(context)).parse(reader);
} finally {
@@ -496,7 +476,7 @@ public class RESTMusicService implements MusicService {
@Override
public void updatePlaylist(String id, String name, String comment, boolean pub, Context context, ProgressListener progressListener) throws Exception {
checkServerVersion(context, "1.8", "Updating playlists is not supported.");
- Reader reader = getReader(context, progressListener, "updatePlaylist", null, Arrays.asList("playlistId", "name", "comment", "public"), Arrays.<Object>asList(id, name, comment, pub));
+ Reader reader = getReader(context, progressListener, "updatePlaylist", Arrays.asList("playlistId", "name", "comment", "public"), Arrays.<Object>asList(id, name, comment, pub));
try {
new ErrorParser(context, getInstance(context)).parse(reader);
} finally {
@@ -506,7 +486,7 @@ public class RESTMusicService implements MusicService {
@Override
public Lyrics getLyrics(String artist, String title, Context context, ProgressListener progressListener) throws Exception {
- Reader reader = getReader(context, progressListener, "getLyrics", null, Arrays.asList("artist", "title"), Arrays.<Object>asList(artist, title));
+ Reader reader = getReader(context, progressListener, "getLyrics", Arrays.asList("artist", "title"), Arrays.<Object>asList(artist, title));
try {
return new LyricsParser(context, getInstance(context)).parse(reader, progressListener);
} finally {
@@ -525,10 +505,10 @@ public class RESTMusicService implements MusicService {
Reader reader;
if(time > 0){
checkServerVersion(context, "1.8", "Scrobbling with a time not supported.");
- reader = getReader(context, progressListener, "scrobble", null, Arrays.asList("id", "submission", "time"), Arrays.<Object>asList(id, submission, time));
+ reader = getReader(context, progressListener, "scrobble", Arrays.asList("id", "submission", "time"), Arrays.<Object>asList(id, submission, time));
}
else
- reader = getReader(context, progressListener, "scrobble", null, Arrays.asList("id", "submission"), Arrays.<Object>asList(id, submission));
+ reader = getReader(context, progressListener, "scrobble", Arrays.asList("id", "submission"), Arrays.<Object>asList(id, submission));
try {
new ErrorParser(context, getInstance(context)).parse(reader);
} finally {
@@ -569,9 +549,9 @@ public class RESTMusicService implements MusicService {
method = "getAlbumList";
}
- Reader reader = getReader(context, progressListener, method, null, names, values, true);
+ Reader reader = getReader(context, progressListener, method, names, values, true);
try {
- return new AlbumListParser(context, getInstance(context)).parse(reader, progressListener);
+ return new EntryListParser(context, getInstance(context)).parse(reader, progressListener);
} finally {
Util.close(reader);
}
@@ -606,7 +586,7 @@ public class RESTMusicService implements MusicService {
int decade = Integer.parseInt(extra);
// Reverse chronological order only supported in 5.3+
- if(ServerInfo.checkServerVersion(context, "1.13", instance) && ServerInfo.isStockSubsonic(context, instance)) {
+ if(ServerInfo.checkServerVersion(context, "1.13", instance) && !ServerInfo.isMadsonic(context, instance)) {
values.add(decade + 9);
values.add(decade);
} else {
@@ -635,15 +615,51 @@ public class RESTMusicService implements MusicService {
method = "getAlbumList";
}
- Reader reader = getReader(context, progressListener, method, null, names, values, true);
+ Reader reader = getReader(context, progressListener, method, names, values, true);
try {
- return new AlbumListParser(context, instance).parse(reader, progressListener);
+ return new EntryListParser(context, instance).parse(reader, progressListener);
} finally {
Util.close(reader);
}
}
- @Override
+ @Override
+ public MusicDirectory getSongList(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception {
+ List<String> names = new ArrayList<String>();
+ List<Object> values = new ArrayList<Object>();
+
+ names.add("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, 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 {
checkServerVersion(context, "1.11", "Artist radio is not supported");
@@ -658,7 +674,13 @@ public class RESTMusicService implements MusicService {
int instance = getInstance(context);
String method;
- if(ServerInfo.isMadsonic(context, instance)) {
+ if(ServerInfo.isMadsonic6(context, instance)) {
+ if (Util.isTagBrowsing(context, instance)) {
+ method = "getSimilarSongsID3";
+ } else {
+ method = "getSimilarSongs";
+ }
+ } else if(ServerInfo.isMadsonic(context, instance)) {
method = "getPandoraSongs";
} else {
if (Util.isTagBrowsing(context, instance)) {
@@ -668,7 +690,7 @@ public class RESTMusicService implements MusicService {
}
}
- Reader reader = getReader(context, progressListener, method, null, names, values);
+ Reader reader = getReader(context, progressListener, method, names, values);
try {
return new RandomSongsParser(context, instance).parse(reader, progressListener);
} finally {
@@ -702,7 +724,7 @@ public class RESTMusicService implements MusicService {
method = "getStarred";
}
- Reader reader = getReader(context, progressListener, method, null, names, values, true);
+ Reader reader = getReader(context, progressListener, method, names, values, true);
try {
return new StarredListParser(context, instance).parse(reader, progressListener);
} finally {
@@ -712,10 +734,7 @@ public class RESTMusicService implements MusicService {
@Override
public MusicDirectory getRandomSongs(int size, String musicFolderId, String genre, String startYear, String endYear, Context context, ProgressListener progressListener) throws Exception {
- HttpParams params = new BasicHttpParams();
- HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_GET_RANDOM_SONGS);
-
- List<String> names = new ArrayList<String>();
+ List<String> names = new ArrayList<String>();
List<Object> values = new ArrayList<Object>();
names.add("size");
@@ -754,7 +773,7 @@ public class RESTMusicService implements MusicService {
values.add(endYear);
}
- Reader reader = getReader(context, progressListener, "getRandomSongs", params, names, values);
+ Reader reader = getReader(context, progressListener, "getRandomSongs", names, values);
try {
return new RandomSongsParser(context, getInstance(context)).parse(reader, progressListener);
} finally {
@@ -784,87 +803,24 @@ public class RESTMusicService implements MusicService {
@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) {
+ String url = getRestUrl(context, "getCoverArt");
+ List<String> parameterNames = Arrays.asList("id");
+ List<Object> parameterValues = Arrays.<Object>asList(entry.getCoverArt());
- // Use cached file, if existing.
- Bitmap bitmap = FileUtil.getAlbumArtBitmap(context, entry, size);
- if (bitmap != null) {
- return bitmap;
- }
-
- String url = getRestUrl(context, "getCoverArt");
-
- InputStream in = null;
- try {
- List<String> parameterNames = Arrays.asList("id");
- List<Object> parameterValues = Arrays.<Object>asList(entry.getCoverArt());
- HttpEntity entity = getEntityForURL(context, url, null, parameterNames, parameterValues, progressListener, task);
-
- in = entity.getContent();
- Header contentEncoding = entity.getContentEncoding();
- if (contentEncoding != null && contentEncoding.getValue().equalsIgnoreCase("gzip")) {
- in = new GZIPInputStream(in);
- }
-
- // If content type is XML, an error occured. Get it.
- String contentType = Util.getContentType(entity);
- if (contentType != null && (contentType.startsWith("text/xml") || contentType.startsWith("text/html"))) {
- new ErrorParser(context, getInstance(context)).parse(new InputStreamReader(in, Constants.UTF_8));
- return null; // Never reached.
- }
-
- byte[] bytes = Util.toByteArray(in);
-
- // Handle case where partial was downloaded before being cancelled
- if(task != null && task.isCancelled()) {
- return null;
- }
-
- OutputStream out = null;
- try {
- out = new FileOutputStream(FileUtil.getAlbumArtFile(context, entry));
- out.write(bytes);
- } finally {
- Util.close(out);
- }
-
- // Size == 0 -> only want to download
- if(size == 0) {
- return null;
- } else {
- return FileUtil.getSampledBitmap(bytes, size);
- }
- } finally {
- Util.close(in);
- }
+ return getBitmapFromUrl(context, url, parameterNames, parameterValues, size, FileUtil.getAlbumArtFile(context, entry), true, progressListener, task);
}
}
@Override
- public HttpResponse getDownloadInputStream(Context context, MusicDirectory.Entry song, long offset, int maxBitrate, SilentBackgroundTask task) throws Exception {
-
+ public HttpURLConnection getDownloadInputStream(Context context, MusicDirectory.Entry song, long offset, int maxBitrate, SilentBackgroundTask task) throws Exception {
String url = getRestUrl(context, "stream");
-
- // Set socket read timeout. Note: The timeout increases as the offset gets larger. This is
- // to avoid the thrashing effect seen when offset is combined with transcoding/downsampling on the server.
- // In that case, the server uses a long time before sending any data, causing the client to time out.
- HttpParams params = new BasicHttpParams();
- int timeout = (int) (SOCKET_READ_TIMEOUT_DOWNLOAD + offset * TIMEOUT_MILLIS_PER_OFFSET_BYTE);
- HttpConnectionParams.setSoTimeout(params, timeout);
-
- // Add "Range" header if offset is given.
- List<Header> headers = new ArrayList<Header>();
- if (offset > 0) {
- headers.add(new BasicHeader("Range", "bytes=" + offset + "-"));
- }
-
List<String> parameterNames = new ArrayList<String>();
parameterNames.add("id");
parameterNames.add("maxBitRate");
- List<Object> parameterValues = new ArrayList<Object>();
+ List<Object> parameterValues = new ArrayList<>();
parameterValues.add(song.getId());
parameterValues.add(maxBitrate);
@@ -884,24 +840,32 @@ public class RESTMusicService implements MusicService {
parameterValues.add("raw");
}
}
- HttpResponse response = getResponseForURL(context, url, params, parameterNames, parameterValues, headers, null, task, false);
- // If content type is XML, an error occurred. Get it.
- String contentType = Util.getContentType(response.getEntity());
- if (contentType != null && (contentType.startsWith("text/xml") || contentType.startsWith("text/html"))) {
- InputStream in = response.getEntity().getContent();
- Header contentEncoding = response.getEntity().getContentEncoding();
- if (contentEncoding != null && contentEncoding.getValue().equalsIgnoreCase("gzip")) {
- in = new GZIPInputStream(in);
+ // Add "Range" header if offset is given
+ Map<String, String> headers = new HashMap<>();
+ if (offset > 0) {
+ headers.put("Range", "bytes=" + offset + "-");
+ }
+
+ // 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.
+ int timeout = (int) (SOCKET_READ_TIMEOUT_DOWNLOAD + offset * TIMEOUT_MILLIS_PER_OFFSET_BYTE);
+ HttpURLConnection connection = getConnection(context, url, parameterNames, parameterValues, headers, timeout);
+
+ // If content type is XML, an error occurred. Get it.
+ String contentType = connection.getContentType();
+ if (contentType != null && (contentType.startsWith("text/xml") || contentType.startsWith("text/html"))) {
+ InputStream in = getInputStreamFromConnection(connection);
+
+ try {
+ new ErrorParser(context, getInstance(context)).parse(new InputStreamReader(in, Constants.UTF_8));
+ } finally {
+ Util.close(in);
}
- try {
- new ErrorParser(context, getInstance(context)).parse(new InputStreamReader(in, Constants.UTF_8));
- } finally {
- Util.close(in);
- }
- }
+ }
- return response;
+ return connection;
}
@Override
@@ -909,10 +873,9 @@ public class RESTMusicService implements MusicService {
StringBuilder builder = new StringBuilder(getRestUrl(context, "stream"));
builder.append("&id=").append(song.getId());
- // If we are doing mp3 to mp3, just specify raw so that stuff works better
- if("mp3".equals(song.getSuffix()) && (song.getTranscodedSuffix() == null || "mp3".equals(song.getTranscodedSuffix())) && ServerInfo.checkServerVersion(context, "1.9", getInstance(context))) {
+ // Allow user to specify to stream raw formats if available
+ if(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_CAST_STREAM_ORIGINAL, true) && ("mp3".equals(song.getSuffix()) || "flac".equals(song.getSuffix()) || "wav".equals(song.getSuffix()) || "aac".equals(song.getSuffix())) && ServerInfo.checkServerVersion(context, "1.9", getInstance(context))) {
builder.append("&format=raw");
- builder.append("&estimateContentLength=true");
} else {
builder.append("&maxBitRate=").append(maxBitrate);
}
@@ -1013,7 +976,7 @@ public class RESTMusicService implements MusicService {
private RemoteStatus executeJukeboxCommand(Context context, ProgressListener progressListener, List<String> parameterNames, List<Object> parameterValues) throws Exception {
checkServerVersion(context, "1.7", "Jukebox not supported.");
- Reader reader = getReader(context, progressListener, "jukeboxControl", null, parameterNames, parameterValues);
+ Reader reader = getReader(context, progressListener, "jukeboxControl", parameterNames, parameterValues);
try {
return new JukeboxStatusParser(context, getInstance(context)).parse(reader);
} finally {
@@ -1052,7 +1015,7 @@ public class RESTMusicService implements MusicService {
}
}
- Reader reader = getReader(context, progressListener, starred ? "star" : "unstar", null, names, values);
+ Reader reader = getReader(context, progressListener, starred ? "star" : "unstar", names, values);
try {
new ErrorParser(context, getInstance(context)).parse(reader);
} finally {
@@ -1064,7 +1027,7 @@ public class RESTMusicService implements MusicService {
public List<Share> getShares(Context context, ProgressListener progressListener) throws Exception {
checkServerVersion(context, "1.6", "Shares not supported.");
- Reader reader = getReader(context, progressListener, "getShares", null);
+ Reader reader = getReader(context, progressListener, "getShares");
try {
return new ShareParser(context, getInstance(context)).parse(reader, progressListener);
} finally {
@@ -1092,7 +1055,7 @@ public class RESTMusicService implements MusicService {
parameterValues.add(expires);
}
- Reader reader = getReader(context, progressListener, "createShare", null, parameterNames, parameterValues);
+ Reader reader = getReader(context, progressListener, "createShare", parameterNames, parameterValues);
try {
return new ShareParser(context, getInstance(context)).parse(reader, progressListener);
}
@@ -1105,16 +1068,13 @@ public class RESTMusicService implements MusicService {
public void deleteShare(String id, Context context, ProgressListener progressListener) throws Exception {
checkServerVersion(context, "1.6", "Shares not supported.");
- HttpParams params = new BasicHttpParams();
- HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_GET_RANDOM_SONGS);
-
List<String> parameterNames = new ArrayList<String>();
List<Object> parameterValues = new ArrayList<Object>();
parameterNames.add("id");
parameterValues.add(id);
- Reader reader = getReader(context, progressListener, "deleteShare", params, parameterNames, parameterValues);
+ Reader reader = getReader(context, progressListener, "deleteShare", parameterNames, parameterValues);
try {
new ErrorParser(context, getInstance(context)).parse(reader);
@@ -1128,9 +1088,6 @@ public class RESTMusicService implements MusicService {
public void updateShare(String id, String description, Long expires, Context context, ProgressListener progressListener) throws Exception {
checkServerVersion(context, "1.6", "Updating share not supported.");
- HttpParams params = new BasicHttpParams();
- HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_GET_RANDOM_SONGS);
-
List<String> parameterNames = new ArrayList<String>();
List<Object> parameterValues = new ArrayList<Object>();
@@ -1145,7 +1102,7 @@ public class RESTMusicService implements MusicService {
parameterNames.add("expires");
parameterValues.add(expires);
- Reader reader = getReader(context, progressListener, "updateShare", params, parameterNames, parameterValues);
+ Reader reader = getReader(context, progressListener, "updateShare", parameterNames, parameterValues);
try {
new ErrorParser(context, getInstance(context)).parse(reader);
}
@@ -1158,16 +1115,13 @@ public class RESTMusicService implements MusicService {
public List<ChatMessage> getChatMessages(Long since, Context context, ProgressListener progressListener) throws Exception {
checkServerVersion(context, "1.2", "Chat not supported.");
- HttpParams params = new BasicHttpParams();
- HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_GET_RANDOM_SONGS);
-
List<String> parameterNames = new ArrayList<String>();
List<Object> parameterValues = new ArrayList<Object>();
parameterNames.add("since");
parameterValues.add(since);
- Reader reader = getReader(context, progressListener, "getChatMessages", params, parameterNames, parameterValues);
+ Reader reader = getReader(context, progressListener, "getChatMessages", parameterNames, parameterValues);
try {
return new ChatMessageParser(context, getInstance(context)).parse(reader, progressListener);
@@ -1180,16 +1134,13 @@ public class RESTMusicService implements MusicService {
public void addChatMessage(String message, Context context, ProgressListener progressListener) throws Exception {
checkServerVersion(context, "1.2", "Chat not supported.");
- HttpParams params = new BasicHttpParams();
- HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_GET_RANDOM_SONGS);
-
List<String> parameterNames = new ArrayList<String>();
List<Object> parameterValues = new ArrayList<Object>();
parameterNames.add("message");
parameterValues.add(message);
- Reader reader = getReader(context, progressListener, "addChatMessage", params, parameterNames, parameterValues);
+ Reader reader = getReader(context, progressListener, "addChatMessage", parameterNames, parameterValues);
try {
new ErrorParser(context, getInstance(context)).parse(reader);
@@ -1202,7 +1153,7 @@ public class RESTMusicService implements MusicService {
public List<Genre> getGenres(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
checkServerVersion(context, "1.9", "Genres not supported.");
- Reader reader = getReader(context, progressListener, "getGenres", null);
+ Reader reader = getReader(context, progressListener, "getGenres");
try {
return new GenreParser(context, getInstance(context)).parse(reader, progressListener);
} finally {
@@ -1214,9 +1165,6 @@ public class RESTMusicService implements MusicService {
public MusicDirectory getSongsByGenre(String genre, int count, int offset, Context context, ProgressListener progressListener) throws Exception {
checkServerVersion(context, "1.9", "Genres not supported.");
- HttpParams params = new BasicHttpParams();
- HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_GET_RANDOM_SONGS);
-
List<String> parameterNames = new ArrayList<String>();
List<Object> parameterValues = new ArrayList<Object>();
@@ -1237,7 +1185,7 @@ public class RESTMusicService implements MusicService {
}
}
- Reader reader = getReader(context, progressListener, "getSongsByGenre", params, parameterNames, parameterValues, true);
+ Reader reader = getReader(context, progressListener, "getSongsByGenre", parameterNames, parameterValues, true);
try {
return new RandomSongsParser(context, instance).parse(reader, progressListener);
} finally {
@@ -1256,9 +1204,9 @@ public class RESTMusicService implements MusicService {
parameterValues.add(size);
String method = ServerInfo.isMadsonic(context, getInstance(context)) ? "getTopTrackSongs" : "getTopSongs";
- Reader reader = getReader(context, progressListener, method, null, parameterNames, parameterValues);
+ Reader reader = getReader(context, progressListener, method, parameterNames, parameterValues);
try {
- return new RandomSongsParser(context, getInstance(context)).parse(reader, progressListener);
+ return new TopSongsParser(context, getInstance(context)).parse(reader, progressListener);
} finally {
Util.close(reader);
}
@@ -1268,7 +1216,7 @@ public class RESTMusicService implements MusicService {
public List<PodcastChannel> getPodcastChannels(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
checkServerVersion(context, "1.6", "Podcasts not supported.");
- Reader reader = getReader(context, progressListener, "getPodcasts", null, Arrays.asList("includeEpisodes"), Arrays.<Object>asList("false"));
+ Reader reader = getReader(context, progressListener, "getPodcasts", Arrays.asList("includeEpisodes"), Arrays.<Object>asList("false"));
try {
List<PodcastChannel> channels = new PodcastChannelParser(context, getInstance(context)).parse(reader, progressListener);
@@ -1290,7 +1238,7 @@ public class RESTMusicService implements MusicService {
@Override
public MusicDirectory getPodcastEpisodes(boolean refresh, String id, Context context, ProgressListener progressListener) throws Exception {
- Reader reader = getReader(context, progressListener, "getPodcasts", null, Arrays.asList("id"), Arrays.<Object>asList(id));
+ Reader reader = getReader(context, progressListener, "getPodcasts", Arrays.asList("id"), Arrays.<Object>asList(id));
try {
return new PodcastEntryParser(context, getInstance(context)).parse(id, reader, progressListener);
} finally {
@@ -1299,8 +1247,8 @@ public class RESTMusicService implements MusicService {
}
@Override
- public MusicDirectory getNewestPodcastEpisodes(int count, Context context, ProgressListener progressListener) throws Exception {
- Reader reader = getReader(context, progressListener, "getNewestPodcasts", null, Arrays.asList("count"), Arrays.<Object>asList(count));
+ public MusicDirectory getNewestPodcastEpisodes(boolean refresh, Context context, ProgressListener progressListener, int count) throws Exception {
+ Reader reader = getReader(context, progressListener, "getNewestPodcasts", Arrays.asList("count"), Arrays.<Object>asList(count), true);
try {
return new PodcastEntryParser(context, getInstance(context)).parse(null, reader, progressListener);
@@ -1313,7 +1261,7 @@ public class RESTMusicService implements MusicService {
public void refreshPodcasts(Context context, ProgressListener progressListener) throws Exception {
checkServerVersion(context, "1.9", "Refresh podcasts not supported.");
- Reader reader = getReader(context, progressListener, "refreshPodcasts", null);
+ Reader reader = getReader(context, progressListener, "refreshPodcasts");
try {
new ErrorParser(context, getInstance(context)).parse(reader);
} finally {
@@ -1325,7 +1273,7 @@ public class RESTMusicService implements MusicService {
public void createPodcastChannel(String url, Context context, ProgressListener progressListener) throws Exception{
checkServerVersion(context, "1.9", "Creating podcasts not supported.");
- Reader reader = getReader(context, progressListener, "createPodcastChannel", null, "url", url);
+ Reader reader = getReader(context, progressListener, "createPodcastChannel", "url", url);
try {
new ErrorParser(context, getInstance(context)).parse(reader);
} finally {
@@ -1337,7 +1285,7 @@ public class RESTMusicService implements MusicService {
public void deletePodcastChannel(String id, Context context, ProgressListener progressListener) throws Exception {
checkServerVersion(context, "1.9", "Deleting podcasts not supported.");
- Reader reader = getReader(context, progressListener, "deletePodcastChannel", null, "id", id);
+ Reader reader = getReader(context, progressListener, "deletePodcastChannel", "id", id);
try {
new ErrorParser(context, getInstance(context)).parse(reader);
} finally {
@@ -1349,7 +1297,7 @@ public class RESTMusicService implements MusicService {
public void downloadPodcastEpisode(String id, Context context, ProgressListener progressListener) throws Exception{
checkServerVersion(context, "1.9", "Downloading podcasts not supported.");
- Reader reader = getReader(context, progressListener, "downloadPodcastEpisode", null, "id", id);
+ Reader reader = getReader(context, progressListener, "downloadPodcastEpisode", "id", id);
try {
new ErrorParser(context, getInstance(context)).parse(reader);
} finally {
@@ -1361,7 +1309,7 @@ public class RESTMusicService implements MusicService {
public void deletePodcastEpisode(String id, String parent, ProgressListener progressListener, Context context) throws Exception{
checkServerVersion(context, "1.9", "Deleting podcasts not supported.");
- Reader reader = getReader(context, progressListener, "deletePodcastEpisode", null, "id", id);
+ Reader reader = getReader(context, progressListener, "deletePodcastEpisode", "id", id);
try {
new ErrorParser(context, getInstance(context)).parse(reader);
} finally {
@@ -1373,7 +1321,7 @@ public class RESTMusicService implements MusicService {
public void setRating(MusicDirectory.Entry entry, int rating, Context context, ProgressListener progressListener) throws Exception {
checkServerVersion(context, "1.6", "Setting ratings not supported.");
- Reader reader = getReader(context, progressListener, "setRating", null, Arrays.asList("id", "rating"), Arrays.<Object>asList(entry.getId(), rating));
+ Reader reader = getReader(context, progressListener, "setRating", Arrays.asList("id", "rating"), Arrays.<Object>asList(entry.getId(), rating));
try {
new ErrorParser(context, getInstance(context)).parse(reader);
} finally {
@@ -1385,7 +1333,7 @@ public class RESTMusicService implements MusicService {
public MusicDirectory getBookmarks(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
checkServerVersion(context, "1.9", "Bookmarks not supported.");
- Reader reader = getReader(context, progressListener, "getBookmarks", null);
+ Reader reader = getReader(context, progressListener, "getBookmarks");
try {
return new BookmarkParser(context, getInstance(context)).parse(reader, progressListener);
} finally {
@@ -1397,7 +1345,7 @@ public class RESTMusicService implements MusicService {
public void createBookmark(MusicDirectory.Entry entry, int position, String comment, Context context, ProgressListener progressListener) throws Exception {
checkServerVersion(context, "1.9", "Creating bookmarks not supported.");
- Reader reader = getReader(context, progressListener, "createBookmark", null, Arrays.asList("id", "position", "comment"), Arrays.<Object>asList(entry.getId(), position, comment));
+ Reader reader = getReader(context, progressListener, "createBookmark", Arrays.asList("id", "position", "comment"), Arrays.<Object>asList(entry.getId(), position, comment));
try {
new ErrorParser(context, getInstance(context)).parse(reader);
} finally {
@@ -1409,7 +1357,7 @@ public class RESTMusicService implements MusicService {
public void deleteBookmark(MusicDirectory.Entry entry, Context context, ProgressListener progressListener) throws Exception {
checkServerVersion(context, "1.9", "Deleting bookmarks not supported.");
- Reader reader = getReader(context, progressListener, "deleteBookmark", null, Arrays.asList("id"), Arrays.<Object>asList(entry.getId()));
+ Reader reader = getReader(context, progressListener, "deleteBookmark", Arrays.asList("id"), Arrays.<Object>asList(entry.getId()));
try {
new ErrorParser(context, getInstance(context)).parse(reader);
} finally {
@@ -1419,7 +1367,7 @@ public class RESTMusicService implements MusicService {
@Override
public User getUser(boolean refresh, String username, Context context, ProgressListener progressListener) throws Exception {
- Reader reader = getReader(context, progressListener, "getUser", null, Arrays.asList("username"), Arrays.<Object>asList(username));
+ Reader reader = getReader(context, progressListener, "getUser", Arrays.asList("username"), Arrays.<Object>asList(username));
try {
List<User> users = new UserParser(context, getInstance(context)).parse(reader, progressListener);
if(users.size() > 0) {
@@ -1437,7 +1385,7 @@ public class RESTMusicService implements MusicService {
public List<User> getUsers(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
checkServerVersion(context, "1.8", "Getting user list is not supported");
- Reader reader = getReader(context, progressListener, "getUsers", null);
+ Reader reader = getReader(context, progressListener, "getUsers");
try {
return new UserParser(context, getInstance(context)).parse(reader, progressListener);
} finally {
@@ -1462,7 +1410,16 @@ public class RESTMusicService implements MusicService {
values.add(setting.getValue());
}
- Reader reader = getReader(context, progressListener, "createUser", null, names, values);
+ 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", names, values);
try {
new ErrorParser(context, getInstance(context)).parse(reader);
} finally {
@@ -1487,7 +1444,16 @@ public class RESTMusicService implements MusicService {
}
}
- Reader reader = getReader(context, progressListener, "updateUser", null, names, values);
+ 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", names, values);
try {
new ErrorParser(context, getInstance(context)).parse(reader);
} finally {
@@ -1497,7 +1463,7 @@ public class RESTMusicService implements MusicService {
@Override
public void deleteUser(String username, Context context, ProgressListener progressListener) throws Exception {
- Reader reader = getReader(context, progressListener, "deleteUser", null, Arrays.asList("username"), Arrays.<Object>asList(username));
+ Reader reader = getReader(context, progressListener, "deleteUser", Arrays.asList("username"), Arrays.<Object>asList(username));
try {
new ErrorParser(context, getInstance(context)).parse(reader);
} finally {
@@ -1507,7 +1473,7 @@ public class RESTMusicService implements MusicService {
@Override
public void changeEmail(String username, String email, Context context, ProgressListener progressListener) throws Exception {
- Reader reader = getReader(context, progressListener, "updateUser", null, Arrays.asList("username", "email"), Arrays.<Object>asList(username, email));
+ Reader reader = getReader(context, progressListener, "updateUser", Arrays.asList("username", "email"), Arrays.<Object>asList(username, email));
try {
new ErrorParser(context, getInstance(context)).parse(reader);
} finally {
@@ -1517,7 +1483,7 @@ public class RESTMusicService implements MusicService {
@Override
public void changePassword(String username, String password, Context context, ProgressListener progressListener) throws Exception {
- Reader reader = getReader(context, progressListener, "changePassword", null, Arrays.asList("username", "password"), Arrays.<Object>asList(username, password));
+ Reader reader = getReader(context, progressListener, "changePassword", Arrays.asList("username", "password"), Arrays.<Object>asList(username, password));
try {
new ErrorParser(context, getInstance(context)).parse(reader);
} finally {
@@ -1535,55 +1501,11 @@ public class RESTMusicService implements MusicService {
// Synchronize on the username so that we don't download concurrently for
// the same user.
synchronized (username) {
- // Use cached file, if existing.
- Bitmap bitmap = FileUtil.getAvatarBitmap(context, username, size);
- if(bitmap != null) {
- return bitmap;
- }
-
String url = Util.getRestUrl(context, "getAvatar");
- InputStream in = null;
- try
- {
- List<String> parameterNames;
- List<Object> parameterValues;
-
- parameterNames = Collections.singletonList("username");
- parameterValues = Arrays.<Object>asList(username);
-
- HttpEntity entity = getEntityForURL(context, url, null, parameterNames, parameterValues, progressListener, task);
- in = entity.getContent();
- Header contentEncoding = entity.getContentEncoding();
- if (contentEncoding != null && contentEncoding.getValue().equalsIgnoreCase("gzip")) {
- in = new GZIPInputStream(in);
- }
-
- // If content type is XML, an error occurred. Get it.
- String contentType = Util.getContentType(entity);
- if (contentType != null && (contentType.startsWith("text/xml") || contentType.startsWith("text/html"))) {
- new ErrorParser(context, getInstance(context)).parse(new InputStreamReader(in, Constants.UTF_8));
- return null; // Never reached.
- }
-
- byte[] bytes = Util.toByteArray(in);
- if(task != null && task.isCancelled()) {
- // Handle case where partial is downloaded and cancelled
- return null;
- }
-
- OutputStream out = null;
- try {
- out = new FileOutputStream(FileUtil.getAvatarFile(context, username));
- out.write(bytes);
- } finally {
- Util.close(out);
- }
+ List<String> parameterNames = Collections.singletonList("username");
+ List<Object> parameterValues = Arrays.<Object>asList(username);
- return FileUtil.getSampledBitmap(bytes, size, false);
- }
- finally {
- Util.close(in);
- }
+ return getBitmapFromUrl(context, url, parameterNames, parameterValues, size, FileUtil.getAvatarFile(context, username), false, progressListener, task);
}
}
@@ -1603,7 +1525,7 @@ public class RESTMusicService implements MusicService {
method = "getArtistInfo";
}
- Reader reader = getReader(context, progressListener, method, null, Arrays.asList("id", "includeNotPresent"), Arrays.<Object>asList(id, "true"));
+ Reader reader = getReader(context, progressListener, method, Arrays.asList("id", "includeNotPresent"), Arrays.<Object>asList(id, "true"));
try {
return new ArtistInfoParser(context, getInstance(context)).parse(reader, progressListener);
} finally {
@@ -1615,53 +1537,13 @@ public class RESTMusicService implements MusicService {
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);
- }
+ return getBitmapFromUrl(context, url, null, null, size, FileUtil.getMiscFile(context, url), false, progressListener, task);
}
}
@Override
public MusicDirectory getVideos(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
- Reader reader = getReader(context, progressListener, "getVideos", null, true);
+ Reader reader = getReader(context, progressListener, "getVideos");
try {
return new VideosParser(context, getInstance(context)).parse(reader, progressListener);
} finally {
@@ -1685,7 +1567,7 @@ public class RESTMusicService implements MusicService {
parameterNames.add("position");
parameterValues.add(position);
- Reader reader = getReader(context, progressListener, "savePlayQueue", null, parameterNames, parameterValues);
+ Reader reader = getReader(context, progressListener, "savePlayQueue", parameterNames, parameterValues);
try {
new ErrorParser(context, getInstance(context)).parse(reader);
} finally {
@@ -1695,7 +1577,7 @@ public class RESTMusicService implements MusicService {
@Override
public PlayerQueue getPlayQueue(Context context, ProgressListener progressListener) throws Exception {
- Reader reader = getReader(context, progressListener, "getPlayQueue", null);
+ Reader reader = getReader(context, progressListener, "getPlayQueue");
try {
return new PlayQueueParser(context, getInstance(context)).parse(reader, progressListener);
} finally {
@@ -1704,6 +1586,18 @@ public class RESTMusicService implements MusicService {
}
@Override
+ public List<InternetRadioStation> getInternetRadioStations(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ checkServerVersion(context, "1.9", null);
+
+ Reader reader = getReader(context, progressListener, "getInternetRadioStations");
+ try {
+ return new InternetRadioStationParser(context, getInstance(context)).parse(reader, progressListener);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
public int processOfflineSyncs(final Context context, final ProgressListener progressListener) throws Exception{
return processOfflineScrobbles(context, progressListener) + processOfflineStars(context, progressListener);
}
@@ -1714,13 +1608,13 @@ public class RESTMusicService implements MusicService {
int count = offline.getInt(Constants.OFFLINE_SCROBBLE_COUNT, 0);
int retry = 0;
for(int i = 1; i <= count; i++) {
- String id = offline.getString(Constants.OFFLINE_SCROBBLE_ID + i, null);
- long time = offline.getLong(Constants.OFFLINE_SCROBBLE_TIME + i, 0);
- if(id != null) {
- scrobble(id, true, time, context, progressListener);
- } else {
- String search = offline.getString(Constants.OFFLINE_SCROBBLE_SEARCH + i, "");
- try{
+ try {
+ String id = offline.getString(Constants.OFFLINE_SCROBBLE_ID + i, null);
+ long time = offline.getLong(Constants.OFFLINE_SCROBBLE_TIME + i, 0);
+ if(id != null) {
+ scrobble(id, true, time, context, progressListener);
+ } else {
+ String search = offline.getString(Constants.OFFLINE_SCROBBLE_SEARCH + i, "");
SearchCritera critera = new SearchCritera(search, 0, 0, 1);
SearchResult result = searchNew(critera, context, progressListener);
if(result.getSongs().size() == 1){
@@ -1732,10 +1626,10 @@ public class RESTMusicService implements MusicService {
throw new Exception("Song not found on server");
}
}
- catch(Exception e){
- Log.e(TAG, e.toString());
- retry++;
- }
+ }
+ catch(Exception e){
+ Log.e(TAG, e.toString());
+ retry++;
}
}
@@ -1815,210 +1709,244 @@ public class RESTMusicService implements MusicService {
this.instance = instance;
}
- private Reader getReader(Context context, ProgressListener progressListener, String method, HttpParams requestParams) throws Exception {
- return getReader(context, progressListener, method, requestParams, false);
+ protected Bitmap getBitmapFromUrl(Context context, String url, List<String> parameterNames, List<Object> parameterValues, int size, File saveToFile, boolean allowUnscaled, ProgressListener progressListener, SilentBackgroundTask task) throws Exception {
+ InputStream in = null;
+ try {
+ HttpURLConnection connection = getConnection(context, url, parameterNames, parameterValues, progressListener, true);
+ in = getInputStreamFromConnection(connection);
+
+ String contentType = connection.getContentType();
+ if (contentType != null && (contentType.startsWith("text/xml") || contentType.startsWith("text/html"))) {
+ new ErrorParser(context, getInstance(context)).parse(new InputStreamReader(in, Constants.UTF_8));
+ }
+
+ 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(saveToFile);
+ out.write(bytes);
+ } finally {
+ Util.close(out);
+ }
+
+ // Size == 0 -> only want to download
+ if(size == 0) {
+ return null;
+ } else {
+ return FileUtil.getSampledBitmap(bytes, size, allowUnscaled);
+ }
+ } finally {
+ Util.close(in);
+ }
}
- private Reader getReader(Context context, ProgressListener progressListener, String method, HttpParams requestParams, boolean throwsError) throws Exception {
- return getReader(context, progressListener, method, requestParams, Collections.<String>emptyList(), Collections.emptyList(), throwsError);
- }
- private Reader getReader(Context context, ProgressListener progressListener, String method,
- HttpParams requestParams, String parameterName, Object parameterValue) throws Exception {
- return getReader(context, progressListener, method, requestParams, Arrays.asList(parameterName), Arrays.<Object>asList(parameterValue));
- }
+ // Helper classes to get a reader for the request
+ private Reader getReader(Context context, ProgressListener progressListener, String method) throws Exception {
+ return getReader(context, progressListener, method, (List<String>)null, null);
+ }
- private Reader getReader(Context context, ProgressListener progressListener, String method,
- HttpParams requestParams, List<String> parameterNames, List<Object> parameterValues) throws Exception {
- return getReader(context, progressListener, method, requestParams, parameterNames, parameterValues, false);
+ private Reader getReader(Context context, ProgressListener progressListener, String method, String parameterName, Object parameterValue) throws Exception {
+ return getReader(context, progressListener, method, parameterName, parameterValue, 0);
}
- private Reader getReader(Context context, ProgressListener progressListener, String method,
- HttpParams requestParams, List<String> parameterNames, List<Object> parameterValues, boolean throwErrors) throws Exception {
+ private Reader getReader(Context context, ProgressListener progressListener, String method, String parameterName, Object parameterValue, int minNetworkTimeout) throws Exception {
+ return getReader(context, progressListener, method, Arrays.asList(parameterName), Arrays.asList(parameterValue), minNetworkTimeout);
+ }
+ private Reader getReader(Context context, ProgressListener progressListener, String method, List<String> parameterNames, List<Object> parameterValues) throws Exception {
+ return getReader(context, progressListener, method, parameterNames, parameterValues, 0);
+ }
+ private Reader getReader(Context context, ProgressListener progressListener, String method, List<String> parameterNames, List<Object> parameterValues, int minNetworkTimeout) throws Exception {
+ return getReader(context, progressListener, method, parameterNames, parameterValues, minNetworkTimeout, false);
+ }
+ private Reader getReader(Context context, ProgressListener progressListener, String method, List<String> parameterNames, List<Object> parameterValues, boolean throwErrors) throws Exception {
+ return getReader(context, progressListener, method, parameterNames, parameterValues, 0, throwErrors);
+ }
+ private Reader getReader(Context context, ProgressListener progressListener, String method, List<String> parameterNames, List<Object> parameterValues, int minNetworkTimeout, 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);
+ return getReaderForURL(context, url, parameterNames, parameterValues, minNetworkTimeout, progressListener, throwErrors);
}
- private Reader getReaderForURL(Context context, String url, HttpParams requestParams, List<String> parameterNames,
- List<Object> parameterValues, ProgressListener progressListener) throws Exception {
- return getReaderForURL(context, url, requestParams, parameterNames, parameterValues, progressListener, true);
+ private Reader getReaderForURL(Context context, String url, List<String> parameterNames, List<Object> parameterValues, ProgressListener progressListener) throws Exception {
+ return getReaderForURL(context, url, parameterNames, parameterValues, progressListener, true);
}
- private Reader getReaderForURL(Context context, String url, HttpParams requestParams, List<String> parameterNames,
- List<Object> parameterValues, ProgressListener progressListener, boolean throwErrors) throws Exception {
- HttpEntity entity = getEntityForURL(context, url, requestParams, parameterNames, parameterValues, progressListener, throwErrors);
- if (entity == null) {
- throw new RuntimeException("No entity received for URL " + url);
- }
+ private Reader getReaderForURL(Context context, String url, List<String> parameterNames, List<Object> parameterValues, ProgressListener progressListener, boolean throwErrors) throws Exception {
+ return getReaderForURL(context, url, parameterNames, parameterValues, 0, progressListener, throwErrors);
+ }
+ private Reader getReaderForURL(Context context, String url, List<String> parameterNames, List<Object> parameterValues, int minNetworkTimeout, ProgressListener progressListener, boolean throwErrors) throws Exception {
+ InputStream in = getInputStream(context, url, parameterNames, parameterValues, minNetworkTimeout, progressListener, throwErrors);
+ return new InputStreamReader(in, Constants.UTF_8);
+ }
- InputStream in = entity.getContent();
- Header contentEncoding = entity.getContentEncoding();
- if (contentEncoding != null && contentEncoding.getValue().equalsIgnoreCase("gzip")) {
+ // Helper classes to open a connection to a server
+ private InputStream getInputStream(Context context, String url, List<String> parameterNames, List<Object> parameterValues, ProgressListener progressListener, boolean throwsErrors) throws Exception {
+ return getInputStream(context, url, parameterNames, parameterValues, 0, progressListener, throwsErrors);
+ }
+ private InputStream getInputStream(Context context, String url, List<String> parameterNames, List<Object> parameterValues, int minNetworkTimeout, ProgressListener progressListener, boolean throwsErrors) throws Exception {
+ HttpURLConnection connection = getConnection(context, url, parameterNames, parameterValues, minNetworkTimeout, progressListener, throwsErrors);
+ return getInputStreamFromConnection(connection);
+ }
+ private InputStream getInputStreamFromConnection(HttpURLConnection connection) throws Exception {
+ InputStream in = connection.getInputStream();
+ if("gzip".equals(connection.getContentEncoding())) {
in = new GZIPInputStream(in);
}
- return new InputStreamReader(in, Constants.UTF_8);
- }
- private HttpEntity getEntityForURL(Context context, String url, HttpParams requestParams, List<String> parameterNames,
- List<Object> parameterValues, ProgressListener progressListener, boolean throwErrors) throws Exception {
+ return in;
+ }
- return getEntityForURL(context, url, requestParams, parameterNames, parameterValues, progressListener, null, throwErrors);
+ private HttpURLConnection getConnection(Context context, String url, List<String> parameterNames, List<Object> parameterValues, Map<String, String> headers, int minNetworkTimeout) throws Exception {
+ return getConnection(context, url, parameterNames, parameterValues, headers, minNetworkTimeout, null, true);
+ }
+ private HttpURLConnection getConnection(Context context, String url, List<String> parameterNames, List<Object> parameterValues, ProgressListener progressListener, boolean throwErrors) throws Exception {
+ return getConnection(context, url, parameterNames, parameterValues, 0, progressListener, throwErrors);
+ }
+ private HttpURLConnection getConnection(Context context, String url, List<String> parameterNames, List<Object> parameterValues, int minNetworkTimeout, ProgressListener progressListener, boolean throwErrors) throws Exception {
+ return getConnection(context, url, parameterNames, parameterValues, null, minNetworkTimeout, progressListener, throwErrors);
+ }
+ private HttpURLConnection getConnection(Context context, String url, List<String> parameterNames, List<Object> parameterValues, Map<String, String> headers, int minNetworkTimeout, ProgressListener progressListener, boolean throwErrors) throws Exception {
+ if(throwErrors) {
+ SharedPreferences prefs = Util.getPreferences(context);
+ int networkTimeout = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_NETWORK_TIMEOUT, SOCKET_READ_TIMEOUT_DEFAULT + ""));
+ return getConnectionDirect(context, url, parameterNames, parameterValues, headers, Math.max(minNetworkTimeout, networkTimeout));
+ } else {
+ return getConnection(context, url, parameterNames, parameterValues, headers, minNetworkTimeout, progressListener, HTTP_REQUEST_MAX_ATTEMPTS, 0);
+ }
}
- private HttpEntity getEntityForURL(Context context, String url, HttpParams requestParams, List<String> parameterNames,
- List<Object> parameterValues, ProgressListener progressListener, SilentBackgroundTask task) throws Exception {
- return getResponseForURL(context, url, requestParams, parameterNames, parameterValues, null, progressListener, task, false).getEntity();
+ private HttpURLConnection getConnection(Context context, String url, List<String> parameterNames, List<Object> parameterValues, Map<String, String> headers, int minNetworkTimeout, ProgressListener progressListener, int retriesLeft, int attempts) throws Exception {
+ SharedPreferences prefs = Util.getPreferences(context);
+ int networkTimeout = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_NETWORK_TIMEOUT, SOCKET_READ_TIMEOUT_DEFAULT + ""));
+ minNetworkTimeout = Math.max(minNetworkTimeout, networkTimeout);
+ attempts++;
+ retriesLeft--;
+
+ try {
+ return getConnectionDirect(context, url, parameterNames, parameterValues, headers, minNetworkTimeout);
+ } catch (IOException x) {
+ if(retriesLeft > 0) {
+ 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");
+ Thread.sleep(2000L);
+
+ minNetworkTimeout = (int) (minNetworkTimeout * 1.3);
+ return getConnection(context, url, parameterNames, parameterValues, headers, minNetworkTimeout, progressListener, retriesLeft, attempts);
+ } else {
+ throw x;
+ }
+ }
}
- private HttpEntity getEntityForURL(Context context, String url, HttpParams requestParams, List<String> parameterNames,
- List<Object> parameterValues, ProgressListener progressListener, SilentBackgroundTask task, boolean throwsError) throws Exception {
- return getResponseForURL(context, url, requestParams, parameterNames, parameterValues, null, progressListener, task, throwsError).getEntity();
- }
- private HttpResponse getResponseForURL(Context context, String url, HttpParams requestParams,
- List<String> parameterNames, List<Object> parameterValues,
- List<Header> headers, ProgressListener progressListener, SilentBackgroundTask task, boolean throwsErrors) throws Exception {
- // If not too many parameters, extract them to the URL rather than relying on the HTTP POST request being
- // received intact. Remember, HTTP POST requests are converted to GET requests during HTTP redirects, thus
- // loosing its entity.
- if (parameterNames != null && parameterNames.size() < 10) {
- StringBuilder builder = new StringBuilder(url);
- for (int i = 0; i < parameterNames.size(); i++) {
- builder.append("&").append(parameterNames.get(i)).append("=");
+ private HttpURLConnection getConnectionDirect(Context context, String url, List<String> parameterNames, List<Object> parameterValues, Map<String, String> headers, int minNetworkTimeout) throws Exception {
+ // Add params to query
+ if (parameterNames != null) {
+ StringBuilder builder = new StringBuilder(url);
+ for (int i = 0; i < parameterNames.size(); i++) {
+ builder.append("&").append(parameterNames.get(i)).append("=");
String part = URLEncoder.encode(String.valueOf(parameterValues.get(i)), "UTF-8");
- part = part.replaceAll("\\%27", "&#39;");
- builder.append(part);
- }
- url = builder.toString();
- parameterNames = null;
- parameterValues = null;
- }
+ part = part.replaceAll("\\%27", "'");
+ builder.append(part);
+ }
+ url = builder.toString();
+ }
- String rewrittenUrl = rewriteUrlWithRedirect(context, url);
- return executeWithRetry(context, rewrittenUrl, url, requestParams, parameterNames, parameterValues, headers, progressListener, task, throwsErrors);
- }
+ // Rewrite url based on redirects
+ String rewrittenUrl = rewriteUrlWithRedirect(context, url);
+ if(rewrittenUrl.indexOf("scanstatus") == -1) {
+ Log.i(TAG, stripUrlInfo(rewrittenUrl));
+ }
+
+ return getConnectionDirect(context, rewrittenUrl, headers, minNetworkTimeout);
+ }
+
+ private HttpURLConnection getConnectionDirect(Context context, String url, Map<String, String> headers, int minNetworkTimeout) throws Exception {
+ if(!hasInstalledGoogleSSL) {
+ try {
+ ProviderInstaller.installIfNeeded(context);
+ } catch(Exception e) {
+ // Just continue on anyways, doesn't really harm anything if this fails
+ Log.w(TAG, "Failed to update to use Google Play SSL", e);
+ }
+ hasInstalledGoogleSSL = true;
+ }
- private HttpResponse executeWithRetry(final Context context, String url, String originalUrl, HttpParams requestParams,
- List<String> parameterNames, List<Object> parameterValues,
- List<Header> headers, ProgressListener progressListener, SilentBackgroundTask task, boolean throwErrors) throws Exception {
- // Strip out sensitive information from log
- if(url.indexOf("scanstatus") == -1) {
- Log.i(TAG, stripUrlInfo(url));
+ // Connect and add headers
+ URL urlObj = new URL(url);
+ HttpURLConnection connection = (HttpURLConnection) urlObj.openConnection();
+ if(url.indexOf("getCoverArt") == -1 && url.indexOf("stream") == -1 && url.indexOf("getAvatar") == -1) {
+ connection.addRequestProperty("Accept-Encoding", "gzip");
+ }
+ connection.addRequestProperty("User-Agent", Constants.REST_CLIENT_ID);
+
+ // Set timeout
+ connection.setConnectTimeout(minNetworkTimeout);
+ connection.setReadTimeout(minNetworkTimeout);
+
+ // Add headers
+ if(headers != null) {
+ for(Map.Entry<String, String> header: headers.entrySet()) {
+ connection.setRequestProperty(header.getKey(), header.getValue());
+ }
+ }
+
+ if(connection instanceof HttpsURLConnection) {
+ HttpsURLConnection sslConnection = (HttpsURLConnection) connection;
+ sslConnection.setSSLSocketFactory(sslSocketFactory);
+ sslConnection.setHostnameVerifier(selfSignedHostnameVerifier);
}
SharedPreferences prefs = Util.getPreferences(context);
- int networkTimeout = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_NETWORK_TIMEOUT, "15000"));
- HttpParams newParams = httpClient.getParams();
- HttpConnectionParams.setSoTimeout(newParams, networkTimeout);
- httpClient.setParams(newParams);
-
- final AtomicReference<Boolean> isCancelled = new AtomicReference<Boolean>(false);
- int attempts = 0;
- while (true) {
- attempts++;
- HttpContext httpContext = new BasicHttpContext();
- final 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<Void>(context) {
- @Override
- protected Void doInBackground() throws Throwable {
- request.abort();
- return null;
- }
- }.execute();
- } else {
- request.abort();
- }
- } catch(Exception e) {
- Log.e(TAG, "Failed to stop http task", e);
- }
- }
- });
- }
-
- if (parameterNames != null && request instanceof HttpPost) {
- List<NameValuePair> params = new ArrayList<NameValuePair>();
- for (int i = 0; i < parameterNames.size(); i++) {
- params.add(new BasicNameValuePair(parameterNames.get(i), String.valueOf(parameterValues.get(i))));
- }
- ((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 && url.indexOf("getAvatar") == -1) {
- request.addHeader("Accept-Encoding", "gzip");
+ int instance = getInstance(context);
+ String username = prefs.getString(Constants.PREFERENCES_KEY_USERNAME + instance, null);
+ String password = prefs.getString(Constants.PREFERENCES_KEY_PASSWORD + instance, null);
+ String encoded = Base64.encodeToString((username + ":" + password).getBytes("UTF-8"), Base64.NO_WRAP);;
+ connection.setRequestProperty("Authorization", "Basic " + encoded);
+
+ // Force the connection to initiate
+ if(connection.getResponseCode() >= 500) {
+ throw new IOException("Error code: " + connection.getResponseCode());
+ }
+ if(detectRedirect(context, urlObj, connection)) {
+ String rewrittenUrl = rewriteUrlWithRedirect(context, url);
+ if(!rewrittenUrl.equals(url)) {
+ connection.disconnect();
+ return getConnectionDirect(context, rewrittenUrl, headers, minNetworkTimeout);
}
- 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));
- }
- }
- }
+ return connection;
+ }
- 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();
+ // Returns true when we should immediately retry with the redirect
+ private boolean detectRedirect(Context context, URL originalUrl, HttpURLConnection connection) throws Exception {
+ if(connection.getResponseCode() == HttpURLConnection.HTTP_MOVED_TEMP || connection.getResponseCode() == HttpURLConnection.HTTP_MOVED_PERM) {
+ String redirectLocation = connection.getHeaderField("Location");
+ if(redirectLocation != null) {
+ detectRedirect(context, originalUrl.toExternalForm(), redirectLocation);
+ return true;
+ }
}
+ detectRedirect(context, originalUrl, connection.getURL());
+ return false;
+ }
+ private void detectRedirect(Context context, URL originalUrl, URL redirectedUrl) throws Exception {
+ detectRedirect(context, originalUrl.toExternalForm(), redirectedUrl.toExternalForm());
+ }
+ private void detectRedirect(Context context, String originalUrl, String redirectedUrl) throws Exception {
if(redirectedUrl != null && "http://subsonic.org/pages/".equals(redirectedUrl)) {
throw new Exception("Invalid url, redirects to http://subsonic.org/pages/");
}
@@ -2035,7 +1963,7 @@ public class RESTMusicService implements MusicService {
redirectionLastChecked = System.currentTimeMillis();
redirectionNetworkType = getCurrentNetworkType(context);
}
- }
+ }
private String rewriteUrlWithRedirect(Context context, String url) {
@@ -2084,7 +2012,10 @@ public class RESTMusicService implements MusicService {
}
}
- public HttpClient getHttpClient() {
- return httpClient;
+ public SSLSocketFactory getSSLSocketFactory() {
+ return sslSocketFactory;
+ }
+ public HostnameVerifier getHostNameVerifier() {
+ return selfSignedHostnameVerifier;
}
}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/RemoteController.java b/app/src/main/java/github/daneren2005/dsub/service/RemoteController.java
index 99502f5e..617144d7 100644
--- a/app/src/main/java/github/daneren2005/dsub/service/RemoteController.java
+++ b/app/src/main/java/github/daneren2005/dsub/service/RemoteController.java
@@ -19,18 +19,32 @@
package github.daneren2005.dsub.service;
+import android.content.SharedPreferences;
import android.util.Log;
import java.util.Iterator;
import java.util.concurrent.LinkedBlockingQueue;
+import github.daneren2005.dsub.domain.MusicDirectory;
import github.daneren2005.dsub.domain.RemoteStatus;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.Util;
+import github.daneren2005.serverproxy.FileProxy;
+import github.daneren2005.serverproxy.ServerProxy;
import github.daneren2005.serverproxy.WebProxy;
public abstract class RemoteController {
private static final String TAG = RemoteController.class.getSimpleName();
protected DownloadService downloadService;
protected boolean nextSupported = false;
+ protected ServerProxy proxy;
+ protected String rootLocation = "";
+
+ public RemoteController(DownloadService downloadService) {
+ this.downloadService = downloadService;
+ SharedPreferences prefs = Util.getPreferences(downloadService);
+ rootLocation = prefs.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, null);
+ }
public abstract void create(boolean playing, int seconds);
public abstract void start();
@@ -43,7 +57,11 @@ public abstract class RemoteController {
// Really is abstract, just don't want to require RemoteController's support it
public void changeNextTrack(DownloadFile song) {}
public boolean isNextSupported() {
- return this.nextSupported;
+ if(Util.getPreferences(downloadService).getBoolean(Constants.PREFERENCES_KEY_CAST_GAPLESS_PLAYBACK, true)) {
+ return this.nextSupported;
+ } else {
+ return false;
+ }
}
public abstract void setVolume(int volume);
public abstract void updateVolume(boolean up);
@@ -99,9 +117,62 @@ public abstract class RemoteController {
protected WebProxy createWebProxy() {
MusicService musicService = MusicServiceFactory.getMusicService(downloadService);
if(musicService instanceof CachedMusicService) {
- return new WebProxy(downloadService, ((CachedMusicService)musicService).getMusicService().getHttpClient());
+ RESTMusicService restMusicService = ((CachedMusicService)musicService).getMusicService();
+ return new WebProxy(downloadService, restMusicService.getSSLSocketFactory(), restMusicService.getHostNameVerifier());
} else {
return new WebProxy(downloadService);
}
}
+
+ protected String getStreamUrl(MusicService musicService, DownloadFile downloadFile) throws Exception {
+ MusicDirectory.Entry song = downloadFile.getSong();
+
+ String url;
+ // In offline mode or playing offline song
+ if(downloadFile.isStream()) {
+ url = downloadFile.getStream();
+ } else if(Util.isOffline(downloadService) || song.getId().indexOf(rootLocation) != -1) {
+ if(proxy == null) {
+ proxy = new FileProxy(downloadService);
+ proxy.start();
+ }
+
+ // Offline song
+ if(song.getId().indexOf(rootLocation) != -1) {
+ url = proxy.getPublicAddress(song.getId());
+ } else {
+ // Playing online song in offline mode
+ url = proxy.getPublicAddress(downloadFile.getCompleteFile().getPath());
+ }
+ } else {
+ // Check if we want a proxy going still
+ if(Util.isCastProxy(downloadService)) {
+ if(proxy instanceof FileProxy) {
+ proxy.stop();
+ proxy = null;
+ }
+
+ if(proxy == null) {
+ proxy = createWebProxy();
+ proxy.start();
+ }
+ } else if(proxy != null) {
+ proxy.stop();
+ proxy = null;
+ }
+
+ if(song.isVideo()) {
+ url = musicService.getHlsUrl(song.getId(), downloadFile.getBitRate(), downloadService);
+ } else {
+ url = musicService.getMusicUrl(downloadService, song, downloadFile.getBitRate());
+ }
+
+ // If proxy is going, it is a WebProxy
+ if(proxy != null) {
+ url = proxy.getPublicAddress(url);
+ }
+ }
+
+ return url;
+ }
}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/Scrobbler.java b/app/src/main/java/github/daneren2005/dsub/service/Scrobbler.java
index 1d9fecef..c7ad639e 100644
--- a/app/src/main/java/github/daneren2005/dsub/service/Scrobbler.java
+++ b/app/src/main/java/github/daneren2005/dsub/service/Scrobbler.java
@@ -3,6 +3,8 @@ package github.daneren2005.dsub.service;
import android.content.Context;
import android.util.Log;
+import github.daneren2005.dsub.domain.InternetRadioStation;
+import github.daneren2005.dsub.domain.MusicDirectory;
import github.daneren2005.dsub.domain.PodcastEpisode;
import github.daneren2005.dsub.util.SilentBackgroundTask;
import github.daneren2005.dsub.util.SongDBHandler;
@@ -21,18 +23,18 @@ public class Scrobbler {
private String lastSubmission;
private String lastNowPlaying;
- public void conditionalScrobble(Context context, DownloadFile song, int playerPosition, int duration) {
+ public void conditionalScrobble(Context context, DownloadFile song, int playerPosition, int duration, boolean isPastCutoff) {
// More than 4 minutes
if(playerPosition > FOUR_MINUTES) {
- scrobble(context, song, true);
+ scrobble(context, song, true, isPastCutoff);
}
// More than 50% played
else if(duration > 0 && playerPosition > (duration / 2)) {
- scrobble(context, song, true);
+ scrobble(context, song, true, isPastCutoff);
}
}
- public void scrobble(final Context context, final DownloadFile song, final boolean submission) {
+ public void scrobble(final Context context, final DownloadFile song, final boolean submission, final boolean isPastCutoff) {
if(song == null) {
return;
}
@@ -55,7 +57,9 @@ public class Scrobbler {
new SilentBackgroundTask<Void>(context) {
@Override
protected Void doInBackground() {
- SongDBHandler.getHandler(context).setSongPlayed(song, submission);
+ if(isPastCutoff) {
+ SongDBHandler.getHandler(context).setSongPlayed(song, submission);
+ }
// Scrobbling disabled
if (!Util.isScrobblingEnabled(context)) {
@@ -66,7 +70,7 @@ public class Scrobbler {
return null;
}
// Ignore podcasts
- else if(song.getSong() instanceof PodcastEpisode) {
+ else if(song.getSong() instanceof PodcastEpisode || song.getSong() instanceof InternetRadioStation) {
return null;
}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/AbstractParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/AbstractParser.java
index 664adcfb..d4c090c1 100644
--- a/app/src/main/java/github/daneren2005/dsub/service/parser/AbstractParser.java
+++ b/app/src/main/java/github/daneren2005/dsub/service/parser/AbstractParser.java
@@ -18,6 +18,7 @@
*/
package github.daneren2005.dsub.service.parser;
+import java.io.IOException;
import java.io.Reader;
import org.xmlpull.v1.XmlPullParser;
@@ -36,6 +37,12 @@ import github.daneren2005.dsub.util.Util;
*/
public abstract class AbstractParser {
private static final String TAG = AbstractParser.class.getSimpleName();
+ private static final String SUBSONIC_RESPONSE = "subsonic-response";
+ private static final String MADSONIC_RESPONSE = "madsonic-response";
+ private static final String SUBSONIC = "subsonic";
+ private static final String MADSONIC = "madsonic";
+ private static final String AMPACHE = "ampache";
+
protected final Context context;
protected final int instance;
private XmlPullParser parser;
@@ -66,6 +73,11 @@ public abstract class AbstractParser {
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;
@@ -127,21 +139,32 @@ public abstract class AbstractParser {
}
protected int nextParseEvent() throws Exception {
- return parser.next();
+ try {
+ return parser.next();
+ } catch(Exception e) {
+ if(ServerInfo.isMadsonic6(context, instance)) {
+ ServerInfo overrideInfo = new ServerInfo();
+ overrideInfo.saveServerInfo(context, instance);
+ }
+
+ throw e;
+ }
}
protected String getElementName() {
String name = parser.getName();
- if ("subsonic-response".equals(name) || "madsonic-response".equals(name)) {
+ if (SUBSONIC_RESPONSE.equals(name) || MADSONIC_RESPONSE.equals(name)) {
rootElementFound = true;
String version = get("version");
if (version != null) {
ServerInfo server = new ServerInfo();
server.setRestVersion(new Version(version));
- if("madsonic".equals(get("type")) || "madsonic-response".equals(name)) {
+ if(MADSONIC.equals(get("type")) || MADSONIC_RESPONSE.equals(name)) {
server.setRestType(ServerInfo.TYPE_MADSONIC);
- } else if("subsonic".equals(get("type")) && server.checkServerVersion(context, "1.13")) {
+ } if(AMPACHE.equals(get("type"))) {
+ server.setRestType(ServerInfo.TYPE_AMPACHE);
+ } else if(SUBSONIC.equals(get("type")) && server.checkServerVersion(context, "1.13")) {
// Oh am I going to regret this
server.setRestType(ServerInfo.TYPE_MADSONIC);
server.setRestVersion(new Version("2.0.0"));
@@ -154,6 +177,11 @@ public abstract class AbstractParser {
protected void validate() throws Exception {
if (!rootElementFound) {
+ if(ServerInfo.isMadsonic6(context, instance)) {
+ ServerInfo overrideInfo = new ServerInfo();
+ overrideInfo.saveServerInfo(context, instance);
+ }
+
throw new Exception(context.getResources().getString(R.string.background_task_parse_error));
}
}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/AlbumListParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/EntryListParser.java
index 773c241b..f91aaae1 100644
--- a/app/src/main/java/github/daneren2005/dsub/service/parser/AlbumListParser.java
+++ b/app/src/main/java/github/daneren2005/dsub/service/parser/EntryListParser.java
@@ -29,9 +29,9 @@ import java.io.Reader;
/**
* @author Sindre Mehus
*/
-public class AlbumListParser extends MusicDirectoryEntryParser {
+public class EntryListParser extends MusicDirectoryEntryParser {
- public AlbumListParser(Context context, int instance) {
+ public EntryListParser(Context context, int instance) {
super(context, instance);
}
@@ -46,7 +46,12 @@ public class AlbumListParser extends MusicDirectoryEntryParser {
String name = getElementName();
if ("album".equals(name)) {
MusicDirectory.Entry entry = parseEntry("");
- entry.setDirectory(true);
+ 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();
diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/ErrorParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/ErrorParser.java
index afb05928..1b389f80 100644
--- a/app/src/main/java/github/daneren2005/dsub/service/parser/ErrorParser.java
+++ b/app/src/main/java/github/daneren2005/dsub/service/parser/ErrorParser.java
@@ -27,7 +27,6 @@ import java.io.Reader;
* @author Sindre Mehus
*/
public class ErrorParser extends AbstractParser {
-
public ErrorParser(Context context, int instance) {
super(context, instance);
}
@@ -45,5 +44,6 @@ public class ErrorParser extends AbstractParser {
} while (eventType != XmlPullParser.END_DOCUMENT);
validate();
+ reader.close();
}
} \ No newline at end of file
diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/InternetRadioStationParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/InternetRadioStationParser.java
new file mode 100644
index 00000000..77d7bc4a
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/service/parser/InternetRadioStationParser.java
@@ -0,0 +1,63 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2016 (C) Scott Jackson
+*/
+package github.daneren2005.dsub.service.parser;
+
+import android.content.Context;
+import github.daneren2005.dsub.domain.InternetRadioStation;
+import github.daneren2005.dsub.util.ProgressListener;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.List;
+
+public class InternetRadioStationParser extends ErrorParser {
+ public InternetRadioStationParser(Context context, int instance) {
+ super(context, instance);
+ }
+
+ public List<InternetRadioStation> parse(Reader reader, ProgressListener progressListener) throws Exception {
+ init(reader);
+
+ List<InternetRadioStation> result = new ArrayList<>();
+ int eventType;
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = getElementName();
+ if ("internetRadioStation".equals(name)) {
+ InternetRadioStation station = new InternetRadioStation();
+
+ station.setId(get("id"));
+ station.setTitle(get("name"));
+ station.setStreamUrl(get("streamUrl"));
+ station.setHomePageUrl(get("homePageUrl"));
+
+ result.add(station);
+ } else if ("error".equals(name)) {
+ handleError();
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+ return result;
+ }
+
+} \ No newline at end of file
diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/ScanStatusParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/ScanStatusParser.java
index ffb3ba05..acd00661 100644
--- a/app/src/main/java/github/daneren2005/dsub/service/parser/ScanStatusParser.java
+++ b/app/src/main/java/github/daneren2005/dsub/service/parser/ScanStatusParser.java
@@ -21,6 +21,7 @@ import org.xmlpull.v1.XmlPullParser;
import java.io.Reader;
import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.ServerInfo;
import github.daneren2005.dsub.util.ProgressListener;
public class ScanStatusParser extends AbstractParser {
@@ -32,14 +33,23 @@ public class ScanStatusParser extends AbstractParser {
public boolean parse(Reader reader, ProgressListener progressListener) throws Exception {
init(reader);
- Boolean started = null;
+ String scanName, scanningName;
+ if(ServerInfo.isMadsonic(context, instance)) {
+ scanName = "status";
+ scanningName = "started";
+ } else {
+ scanName = "scanStatus";
+ scanningName = "scanning";
+ }
+
+ Boolean scanning = null;
int eventType;
do {
eventType = nextParseEvent();
if (eventType == XmlPullParser.START_TAG) {
String name = getElementName();
- if("status".equals(name)) {
- started = getBoolean("started");
+ if(scanName.equals(name)) {
+ scanning = getBoolean(scanningName);
String msg = context.getResources().getString(R.string.parser_scan_count, getInteger("count"));
progressListener.updateProgress(msg);
@@ -51,6 +61,6 @@ public class ScanStatusParser extends AbstractParser {
validate();
- return started != null && started;
+ return scanning != null && scanning;
}
} \ No newline at end of file
diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/TopSongsParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/TopSongsParser.java
new file mode 100644
index 00000000..2d1f43dc
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/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 <http://www.gnu.org/licenses/>.
+ Copyright 2016 (C) Scott Jackson
+*/
+package github.daneren2005.dsub.service.parser;
+
+import android.content.Context;
+
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.Reader;
+
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.util.ProgressListener;
+
+public class 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 customOrder = 1;
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = getElementName();
+ if ("song".equals(name)) {
+ MusicDirectory.Entry entry = parseEntry("");
+ entry.setCustomOrder(customOrder);
+ dir.addChild(entry);
+
+ customOrder++;
+ } else if ("error".equals(name)) {
+ handleError();
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+
+ return dir;
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/UserParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/UserParser.java
index e20556c0..fc2ddd7e 100644
--- a/app/src/main/java/github/daneren2005/dsub/service/parser/UserParser.java
+++ b/app/src/main/java/github/daneren2005/dsub/service/parser/UserParser.java
@@ -16,6 +16,7 @@
package github.daneren2005.dsub.service.parser;
import android.content.Context;
+import android.util.Log;
import org.xmlpull.v1.XmlPullParser;
@@ -23,10 +24,16 @@ import java.io.Reader;
import java.util.ArrayList;
import java.util.List;
+import github.daneren2005.dsub.domain.MusicFolder;
import github.daneren2005.dsub.domain.User;
+import github.daneren2005.dsub.domain.User.MusicFolderSetting;
+import github.daneren2005.dsub.domain.User.Setting;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
import github.daneren2005.dsub.util.ProgressListener;
public class UserParser extends AbstractParser {
+ private static final String TAG = UserParser.class.getSimpleName();
public UserParser(Context context, int instance) {
super(context, instance);
@@ -35,14 +42,17 @@ public class UserParser extends AbstractParser {
public List<User> parse(Reader reader, ProgressListener progressListener) throws Exception {
init(reader);
List<User> result = new ArrayList<User>();
+ List<MusicFolder> musicFolders = null;
+ User user = null;
int eventType;
+ String tagName = null;
do {
eventType = nextParseEvent();
if (eventType == XmlPullParser.START_TAG) {
- String name = getElementName();
- if ("user".equals(name)) {
- User user = new User();
+ tagName = getElementName();
+ if ("user".equals(tagName)) {
+ user = new User();
user.setUsername(get("username"));
user.setEmail(get("email"));
@@ -53,9 +63,31 @@ public class UserParser extends AbstractParser {
parseSetting(user, User.LASTFM);
result.add(user);
- } else if ("error".equals(name)) {
+ } 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);
@@ -63,6 +95,11 @@ public class UserParser extends AbstractParser {
return result;
}
+
+ private List<MusicFolder> 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);
diff --git a/app/src/main/java/github/daneren2005/dsub/service/ssl/SSLSocketFactory.java b/app/src/main/java/github/daneren2005/dsub/service/ssl/SSLSocketFactory.java
deleted file mode 100644
index 830950c8..00000000
--- a/app/src/main/java/github/daneren2005/dsub/service/ssl/SSLSocketFactory.java
+++ /dev/null
@@ -1,553 +0,0 @@
-/*
- * ====================================================================
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- * ====================================================================
- *
- * This software consists of voluntary contributions made by many
- * individuals on behalf of the Apache Software Foundation. For more
- * information on the Apache Software Foundation, please see
- * <http://www.apache.org/>.
- *
- */
-
-package github.daneren2005.dsub.service.ssl;
-
-import android.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.
- * <p>
- * SSLSocketFactory can be used to validate the identity of the HTTPS server against a list of
- * trusted certificates and to authenticate to the HTTPS server using a private key.
- * <p>
- * SSLSocketFactory will enable server authentication when supplied with
- * a {@link KeyStore trust-store} file containing one or several trusted certificates. The client
- * secure socket will reject the connection during the SSL session handshake if the target HTTPS
- * server attempts to authenticate itself with a non-trusted certificate.
- * <p>
- * Use JDK keytool utility to import a trusted certificate and generate a trust-store file:
- * <pre>
- * keytool -import -alias "my server cert" -file server.crt -keystore my.truststore
- * </pre>
- * <p>
- * In special cases the standard trust verification process can be bypassed by using a custom
- * {@link TrustStrategy}. This interface is primarily intended for allowing self-signed
- * certificates to be accepted as trusted without having to add them to the trust-store file.
- * <p>
- * The following parameters can be used to customize the behavior of this
- * class:
- * <ul>
- * <li>{@link org.apache.http.params.CoreConnectionPNames#CONNECTION_TIMEOUT}</li>
- * <li>{@link org.apache.http.params.CoreConnectionPNames#SO_TIMEOUT}</li>
- * </ul>
- * <p>
- * SSLSocketFactory will enable client authentication when supplied with
- * a {@link KeyStore key-store} file containing a private key/public certificate
- * pair. The client secure socket will use the private key to authenticate
- * itself to the target HTTPS server during the SSL session handshake if
- * requested to do so by the server.
- * The target HTTPS server will in its turn verify the certificate presented
- * by the client in order to establish client's authenticity
- * <p>
- * Use the following sequence of actions to generate a key-store file
- * </p>
- * <ul>
- * <li>
- * <p>
- * Use JDK keytool utility to generate a new key
- * <pre>keytool -genkey -v -alias "my client key" -validity 365 -keystore my.keystore</pre>
- * For simplicity use the same password for the key as that of the key-store
- * </p>
- * </li>
- * <li>
- * <p>
- * Issue a certificate signing request (CSR)
- * <pre>keytool -certreq -alias "my client key" -file mycertreq.csr -keystore my.keystore</pre>
- * </p>
- * </li>
- * <li>
- * <p>
- * Send the certificate request to the trusted Certificate Authority for signature.
- * One may choose to act as her own CA and sign the certificate request using a PKI
- * tool, such as OpenSSL.
- * </p>
- * </li>
- * <li>
- * <p>
- * Import the trusted CA root certificate
- * <pre>keytool -import -alias "my trusted ca" -file caroot.crt -keystore my.keystore</pre>
- * </p>
- * </li>
- * <li>
- * <p>
- * Import the PKCS#7 file containg the complete certificate chain
- * <pre>keytool -import -alias "my client key" -file mycert.p7 -keystore my.keystore</pre>
- * </p>
- * </li>
- * <li>
- * <p>
- * Verify the content the resultant keystore file
- * <pre>keytool -list -v -keystore my.keystore</pre>
- * </p>
- * </li>
- * </ul>
- *
- * @since 4.0
- */
-public class SSLSocketFactory implements LayeredSocketFactory {
- private static final String TAG = SSLSocketFactory.class.getSimpleName();
- public static final String TLS = "TLS";
-
- public static final X509HostnameVerifier ALLOW_ALL_HOSTNAME_VERIFIER
- = new AllowAllHostnameVerifier();
-
- public static final X509HostnameVerifier BROWSER_COMPATIBLE_HOSTNAME_VERIFIER
- = new BrowserCompatHostnameVerifier();
-
- public static final X509HostnameVerifier STRICT_HOSTNAME_VERIFIER
- = new StrictHostnameVerifier();
-
- /**
- * The default factory using the default JVM settings for secure connections.
- */
- private static final SSLSocketFactory DEFAULT_FACTORY = new SSLSocketFactory();
-
- /**
- * Gets the default factory, which uses the default JVM settings for secure
- * connections.
- *
- * @return the default factory
- */
- public static SSLSocketFactory getSocketFactory() {
- return DEFAULT_FACTORY;
- }
-
- private final javax.net.ssl.SSLSocketFactory socketfactory;
- private final HostNameResolver nameResolver;
- // TODO: make final
- private volatile X509HostnameVerifier hostnameVerifier;
-
- private static SSLContext createSSLContext(
- String algorithm,
- final KeyStore keystore,
- final String keystorePassword,
- final KeyStore truststore,
- final SecureRandom random,
- final TrustStrategy trustStrategy)
- throws NoSuchAlgorithmException, KeyStoreException, UnrecoverableKeyException, KeyManagementException {
- if (algorithm == null) {
- algorithm = TLS;
- }
- KeyManagerFactory kmfactory = KeyManagerFactory.getInstance(
- KeyManagerFactory.getDefaultAlgorithm());
- kmfactory.init(keystore, keystorePassword != null ? keystorePassword.toCharArray(): null);
- KeyManager[] keymanagers = kmfactory.getKeyManagers();
- TrustManagerFactory tmfactory = TrustManagerFactory.getInstance(
- TrustManagerFactory.getDefaultAlgorithm());
- tmfactory.init(keystore);
- TrustManager[] trustmanagers = tmfactory.getTrustManagers();
- if (trustmanagers != null && trustStrategy != null) {
- for (int i = 0; i < trustmanagers.length; i++) {
- TrustManager tm = trustmanagers[i];
- if (tm instanceof X509TrustManager) {
- trustmanagers[i] = new TrustManagerDecorator(
- (X509TrustManager) tm, trustStrategy);
- }
- }
- }
-
- SSLContext sslcontext = SSLContext.getInstance(algorithm);
- sslcontext.init(keymanagers, trustmanagers, random);
- return sslcontext;
- }
-
- /**
- * @deprecated Use {@link #SSLSocketFactory(String, KeyStore, String, KeyStore, SecureRandom, X509HostnameVerifier)}
- */
- @Deprecated
- public SSLSocketFactory(
- final String algorithm,
- final KeyStore keystore,
- final String keystorePassword,
- final KeyStore truststore,
- final SecureRandom random,
- final HostNameResolver nameResolver)
- throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
- this(createSSLContext(
- algorithm, keystore, keystorePassword, truststore, random, null),
- nameResolver);
- }
-
- /**
- * @since 4.1
- */
- public SSLSocketFactory(
- String algorithm,
- final KeyStore keystore,
- final String keystorePassword,
- final KeyStore truststore,
- final SecureRandom random,
- final X509HostnameVerifier hostnameVerifier)
- throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
- this(createSSLContext(
- algorithm, keystore, keystorePassword, truststore, random, null),
- hostnameVerifier);
- }
-
- /**
- * @since 4.1
- */
- public SSLSocketFactory(
- String algorithm,
- final KeyStore keystore,
- final String keystorePassword,
- final KeyStore truststore,
- final SecureRandom random,
- final TrustStrategy trustStrategy,
- final X509HostnameVerifier hostnameVerifier)
- throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
- this(createSSLContext(
- algorithm, keystore, keystorePassword, truststore, random, trustStrategy),
- hostnameVerifier);
- }
-
- public SSLSocketFactory(
- final KeyStore keystore,
- final String keystorePassword,
- final KeyStore truststore)
- throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
- this(TLS, keystore, keystorePassword, truststore, null, null, BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
- }
-
- public SSLSocketFactory(
- final KeyStore keystore,
- final String keystorePassword)
- throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException{
- this(TLS, keystore, keystorePassword, null, null, null, BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
- }
-
- public SSLSocketFactory(
- final KeyStore truststore)
- throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
- this(TLS, null, null, truststore, null, null, BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
- }
-
- /**
- * @since 4.1
- */
- public SSLSocketFactory(
- final TrustStrategy trustStrategy,
- final X509HostnameVerifier hostnameVerifier)
- throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
- this(TLS, null, null, null, null, trustStrategy, hostnameVerifier);
- }
-
- /**
- * @since 4.1
- */
- public SSLSocketFactory(
- final TrustStrategy trustStrategy)
- throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
- this(TLS, null, null, null, null, trustStrategy, BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
- }
-
- public SSLSocketFactory(final SSLContext sslContext) {
- this(sslContext, BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
- }
-
- /**
- * @deprecated Use {@link #SSLSocketFactory(SSLContext)}
- */
- @Deprecated
- public SSLSocketFactory(
- final SSLContext sslContext, final HostNameResolver nameResolver) {
- super();
- this.socketfactory = sslContext.getSocketFactory();
- this.hostnameVerifier = BROWSER_COMPATIBLE_HOSTNAME_VERIFIER;
- this.nameResolver = nameResolver;
- }
-
- /**
- * @since 4.1
- */
- public SSLSocketFactory(
- final SSLContext sslContext, final X509HostnameVerifier hostnameVerifier) {
- super();
- this.socketfactory = sslContext.getSocketFactory();
- this.hostnameVerifier = hostnameVerifier;
- this.nameResolver = null;
- }
-
- private SSLSocketFactory() {
- super();
- this.socketfactory = HttpsURLConnection.getDefaultSSLSocketFactory();
- this.hostnameVerifier = null;
- this.nameResolver = null;
- }
-
- /**
- * @param params Optional parameters. Parameters passed to this method will have no effect.
- * This method will create a unconnected instance of {@link Socket} class
- * using {@link javax.net.ssl.SSLSocketFactory#createSocket()} method.
- * @since 4.1
- */
- @SuppressWarnings("cast")
- public Socket createSocket(final HttpParams params) throws IOException {
- // the cast makes sure that the factory is working as expected
- SSLSocket sslSocket = (SSLSocket) this.socketfactory.createSocket();
- sslSocket.setEnabledProtocols(getProtocols(sslSocket));
- sslSocket.setEnabledCipherSuites(getCiphers(sslSocket));
- return sslSocket;
- }
-
- @SuppressWarnings("cast")
- public Socket createSocket() throws IOException {
- // the cast makes sure that the factory is working as expected
- SSLSocket sslSocket = (SSLSocket) this.socketfactory.createSocket();
- sslSocket.setEnabledProtocols(getProtocols(sslSocket));
- sslSocket.setEnabledCipherSuites(getCiphers(sslSocket));
- return sslSocket;
- }
-
- /**
- * @since 4.1
- */
- public Socket connectSocket(
- final Socket sock,
- final InetSocketAddress remoteAddress,
- final InetSocketAddress localAddress,
- final HttpParams params) throws IOException, UnknownHostException, ConnectTimeoutException {
- if (remoteAddress == null) {
- throw new IllegalArgumentException("Remote address may not be null");
- }
- if (params == null) {
- throw new IllegalArgumentException("HTTP parameters may not be null");
- }
- SSLSocket sslsock = (SSLSocket) (sock != null ? sock : createSocket());
- if (localAddress != null) {
-// sslsock.setReuseAddress(HttpConnectionParams.getSoReuseaddr(params));
- sslsock.bind(localAddress);
- }
-
- setHostName(sslsock, remoteAddress.getHostName());
- int connTimeout = HttpConnectionParams.getConnectionTimeout(params);
- int soTimeout = HttpConnectionParams.getSoTimeout(params);
-
- try {
- sslsock.connect(remoteAddress, connTimeout);
- } catch (SocketTimeoutException ex) {
- throw new ConnectTimeoutException("Connect to " + remoteAddress.getHostName() + "/"
- + remoteAddress.getAddress() + " timed out");
- }
- sslsock.setSoTimeout(soTimeout);
- if (this.hostnameVerifier != null) {
- try {
- this.hostnameVerifier.verify(remoteAddress.getHostName(), sslsock);
- // verifyHostName() didn't blowup - good!
- } catch (IOException iox) {
- // close the socket before re-throwing the exception
- try { sslsock.close(); } catch (Exception x) { /*ignore*/ }
- throw iox;
- }
- }
- return sslsock;
- }
-
-
- /**
- * Checks whether a socket connection is secure.
- * This factory creates TLS/SSL socket connections
- * which, by default, are considered secure.
- * <br/>
- * Derived classes may override this method to perform
- * runtime checks, for example based on the cypher suite.
- *
- * @param sock the connected socket
- *
- * @return <code>true</code>
- *
- * @throws IllegalArgumentException if the argument is invalid
- */
- public boolean isSecure(final Socket sock) throws IllegalArgumentException {
- if (sock == null) {
- throw new IllegalArgumentException("Socket may not be null");
- }
- // This instanceof check is in line with createSocket() above.
- if (!(sock instanceof SSLSocket)) {
- throw new IllegalArgumentException("Socket not created by this factory");
- }
- // This check is performed last since it calls the argument object.
- if (sock.isClosed()) {
- throw new IllegalArgumentException("Socket is closed");
- }
- return true;
- }
-
- /**
- * @since 4.1
- */
- public Socket createLayeredSocket(
- final Socket socket,
- final String host,
- final int port,
- final boolean autoClose) throws IOException, UnknownHostException {
- SSLSocket sslSocket = (SSLSocket) this.socketfactory.createSocket(
- socket,
- host,
- port,
- autoClose
- );
- sslSocket.setEnabledProtocols(getProtocols(sslSocket));
- sslSocket.setEnabledCipherSuites(getCiphers(sslSocket));
- if (this.hostnameVerifier != null) {
- this.hostnameVerifier.verify(host, sslSocket);
- }
- // verifyHostName() didn't blowup - good!
- return sslSocket;
- }
-
- @Deprecated
- public void setHostnameVerifier(X509HostnameVerifier hostnameVerifier) {
- if ( hostnameVerifier == null ) {
- throw new IllegalArgumentException("Hostname verifier may not be null");
- }
- this.hostnameVerifier = hostnameVerifier;
- }
-
- public X509HostnameVerifier getHostnameVerifier() {
- return this.hostnameVerifier;
- }
-
- /**
- * @deprecated Use {@link #connectSocket(Socket, InetSocketAddress, InetSocketAddress, HttpParams)}
- */
- @Deprecated
- public Socket connectSocket(
- final Socket socket,
- final String host, int port,
- final InetAddress localAddress, int localPort,
- final HttpParams params) throws IOException, UnknownHostException, ConnectTimeoutException {
- InetSocketAddress local = null;
- if (localAddress != null || localPort > 0) {
- // we need to bind explicitly
- if (localPort < 0) {
- localPort = 0; // indicates "any"
- }
- local = new InetSocketAddress(localAddress, localPort);
- }
- InetAddress remoteAddress;
- if (this.nameResolver != null) {
- remoteAddress = this.nameResolver.resolve(host);
- } else {
- remoteAddress = InetAddress.getByName(host);
- }
- InetSocketAddress remote = new InetSocketAddress(remoteAddress, port);
- return connectSocket(socket, remote, local, params);
- }
-
- /**
- * @deprecated Use {@link #createLayeredSocket(Socket, String, int, boolean)}
- */
- @Deprecated
- public Socket createSocket(
- final Socket socket,
- final String host, int port,
- boolean autoClose) throws IOException, UnknownHostException {
- SSLSocket sslSocket = (SSLSocket) this.socketfactory.createSocket(socket, host, port, autoClose);
- sslSocket.setEnabledProtocols(getProtocols(sslSocket));
- sslSocket.setEnabledCipherSuites(getCiphers(sslSocket));
- setHostName(sslSocket, host);
- return sslSocket;
- }
-
- private void setHostName(SSLSocket sslsock, String hostname){
- try {
- java.lang.reflect.Method setHostnameMethod = sslsock.getClass().getMethod("setHostname", String.class);
- setHostnameMethod.invoke(sslsock, hostname);
- } catch (Exception e) {
- Log.w(TAG, "SNI not useable", e);
- }
- }
-
- private String[] getProtocols(SSLSocket sslSocket) {
- String[] protocols = sslSocket.getEnabledProtocols();
-
- // Remove SSLv3 if it is not the only option
- if(protocols.length > 1) {
- List<String> protocolList = new ArrayList(Arrays.asList(protocols));
- protocolList.remove("SSLv3");
- protocols = protocolList.toArray(new String[protocolList.size()]);
- }
-
- return protocols;
- }
-
- private String[] getCiphers(SSLSocket sslSocket) {
- String[] ciphers = sslSocket.getEnabledCipherSuites();
-
- List<String> enabledCiphers = new ArrayList(Arrays.asList(ciphers));
- // On Android 5.0 release, Jetty doesn't seem to play nice with these ciphers
- // 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/daneren2005/dsub/service/ssl/TrustManagerDecorator.java b/app/src/main/java/github/daneren2005/dsub/service/ssl/TrustManagerDecorator.java
deleted file mode 100644
index f2364368..00000000
--- a/app/src/main/java/github/daneren2005/dsub/service/ssl/TrustManagerDecorator.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * ====================================================================
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- * ====================================================================
- *
- * This software consists of voluntary contributions made by many
- * individuals on behalf of the Apache Software Foundation. For more
- * information on the Apache Software Foundation, please see
- * <http://www.apache.org/>.
- *
- */
-package github.daneren2005.dsub.service.ssl;
-
-import java.security.cert.CertificateException;
-import java.security.cert.X509Certificate;
-
-import javax.net.ssl.X509TrustManager;
-
-
-/**
- * @since 4.1
- */
-class TrustManagerDecorator implements X509TrustManager {
-
- private final X509TrustManager trustManager;
- private final TrustStrategy trustStrategy;
-
- TrustManagerDecorator(final X509TrustManager trustManager, final TrustStrategy trustStrategy) {
- super();
- this.trustManager = trustManager;
- this.trustStrategy = trustStrategy;
- }
-
- public void checkClientTrusted(
- final X509Certificate[] chain, final String authType) throws CertificateException {
- this.trustManager.checkClientTrusted(chain, authType);
- }
-
- public void checkServerTrusted(
- final X509Certificate[] chain, final String authType) throws CertificateException {
- if (!this.trustStrategy.isTrusted(chain, authType)) {
- this.trustManager.checkServerTrusted(chain, authType);
- }
- }
-
- public X509Certificate[] getAcceptedIssuers() {
- return this.trustManager.getAcceptedIssuers();
- }
-
-}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/ssl/TrustSelfSignedStrategy.java b/app/src/main/java/github/daneren2005/dsub/service/ssl/TrustSelfSignedStrategy.java
deleted file mode 100644
index 637a8931..00000000
--- a/app/src/main/java/github/daneren2005/dsub/service/ssl/TrustSelfSignedStrategy.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * ====================================================================
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- * ====================================================================
- *
- * This software consists of voluntary contributions made by many
- * individuals on behalf of the Apache Software Foundation. For more
- * information on the Apache Software Foundation, please see
- * <http://www.apache.org/>.
- *
- */
-package github.daneren2005.dsub.service.ssl;
-
-import java.security.cert.CertificateException;
-import java.security.cert.X509Certificate;
-
-/**
- * A trust strategy that accepts self-signed certificates as trusted. Verification of all other
- * certificates is done by the trust manager configured in the SSL context.
- *
- * @since 4.1
- */
-public class TrustSelfSignedStrategy implements TrustStrategy {
-
- public boolean isTrusted(final X509Certificate[] chain, final String authType) throws CertificateException {
- return true;
- }
-
-}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/ssl/TrustStrategy.java b/app/src/main/java/github/daneren2005/dsub/service/ssl/TrustStrategy.java
deleted file mode 100644
index 334a97c5..00000000
--- a/app/src/main/java/github/daneren2005/dsub/service/ssl/TrustStrategy.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * ====================================================================
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- * ====================================================================
- *
- * This software consists of voluntary contributions made by many
- * individuals on behalf of the Apache Software Foundation. For more
- * information on the Apache Software Foundation, please see
- * <http://www.apache.org/>.
- *
- */
-package github.daneren2005.dsub.service.ssl;
-
-import java.security.cert.CertificateException;
-import java.security.cert.X509Certificate;
-
-/**
- * A strategy to establish trustworthiness of certificates without consulting the trust manager
- * configured in the actual SSL context. This interface can be used to override the standard
- * JSSE certificate verification process.
- *
- * @since 4.1
- */
-public interface TrustStrategy {
-
- /**
- * Determines whether the certificate chain can be trusted without consulting the trust manager
- * configured in the actual SSL context. This method can be used to override the standard JSSE
- * certificate verification process.
- * <p>
- * Please note that, if this method returns <code>false</code>, the trust manager configured
- * in the actual SSL context can still clear the certificate as trusted.
- *
- * @param chain the peer certificate chain
- * @param authType the authentication type based on the client certificate
- * @return <code>true</code> if the certificate can be trusted without verification by
- * the trust manager, <code>false</code> otherwise.
- * @throws CertificateException thrown if the certificate is not trusted or invalid.
- */
- boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException;
-
-}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/sync/MostRecentSyncAdapter.java b/app/src/main/java/github/daneren2005/dsub/service/sync/MostRecentSyncAdapter.java
index 8da83be1..bcb7b92f 100644
--- a/app/src/main/java/github/daneren2005/dsub/service/sync/MostRecentSyncAdapter.java
+++ b/app/src/main/java/github/daneren2005/dsub/service/sync/MostRecentSyncAdapter.java
@@ -49,7 +49,7 @@ public class MostRecentSyncAdapter extends SubsonicSyncAdapter {
}
@Override
- public void onExecuteSync(Context context, int instance) {
+ public void onExecuteSync(Context context, int instance) throws NetworkNotValidException {
try {
ArrayList<String> syncedList = SyncUtil.getSyncedMostRecent(context, instance);
MusicDirectory albumList = musicService.getAlbumList("newest", 20, 0, tagBrowsing, context, null);
diff --git a/app/src/main/java/github/daneren2005/dsub/service/sync/PlaylistSyncAdapter.java b/app/src/main/java/github/daneren2005/dsub/service/sync/PlaylistSyncAdapter.java
index a0996628..cb3c3877 100644
--- a/app/src/main/java/github/daneren2005/dsub/service/sync/PlaylistSyncAdapter.java
+++ b/app/src/main/java/github/daneren2005/dsub/service/sync/PlaylistSyncAdapter.java
@@ -56,7 +56,7 @@ public class PlaylistSyncAdapter extends SubsonicSyncAdapter {
}
@Override
- public void onExecuteSync(Context context, int instance) {
+ public void onExecuteSync(Context context, int instance) throws NetworkNotValidException {
String serverName = Util.getServerName(context, instance);
List<Playlist> remainder = null;
@@ -69,6 +69,7 @@ public class PlaylistSyncAdapter extends SubsonicSyncAdapter {
ArrayList<SyncSet> playlistList = SyncUtil.getSyncedPlaylists(context, instance);
List<String> updated = new ArrayList<String>();
+ String updatedId = null;
boolean removed = false;
for(int i = 0; i < playlistList.size(); i++) {
SyncSet cachedPlaylist = playlistList.get(i);
@@ -94,9 +95,13 @@ public class PlaylistSyncAdapter extends SubsonicSyncAdapter {
DownloadFile file = new DownloadFile(context, entry, true);
String path = file.getCompleteFile().getPath();
while(!file.isSaved() && !file.isFailedMax()) {
+ throwIfNetworkInvalid();
file.downloadNow(musicService);
if(file.isSaved() && !updated.contains(playlist.getName())) {
updated.add(playlist.getName());
+ if(updatedId == null) {
+ updatedId = playlist.getId();
+ }
}
}
@@ -147,7 +152,7 @@ public class PlaylistSyncAdapter extends SubsonicSyncAdapter {
}
if(updated.size() > 0) {
- Notifications.showSyncNotification(context, R.string.sync_new_playlists, SyncUtil.joinNames(updated));
+ Notifications.showSyncNotification(context, R.string.sync_new_playlists, SyncUtil.joinNames(updated), updatedId);
}
}
}
diff --git a/app/src/main/java/github/daneren2005/dsub/service/sync/PodcastSyncAdapter.java b/app/src/main/java/github/daneren2005/dsub/service/sync/PodcastSyncAdapter.java
index 985a7267..7afcad25 100644
--- a/app/src/main/java/github/daneren2005/dsub/service/sync/PodcastSyncAdapter.java
+++ b/app/src/main/java/github/daneren2005/dsub/service/sync/PodcastSyncAdapter.java
@@ -23,7 +23,6 @@ import android.annotation.TargetApi;
import android.content.Context;
import android.util.Log;
-import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
@@ -54,7 +53,7 @@ public class PodcastSyncAdapter extends SubsonicSyncAdapter {
}
@Override
- public void onExecuteSync(Context context, int instance) {
+ public void onExecuteSync(Context context, int instance) throws NetworkNotValidException {
ArrayList<SyncSet> podcastList = SyncUtil.getSyncedPodcasts(context, instance);
try {
@@ -68,6 +67,7 @@ public class PodcastSyncAdapter extends SubsonicSyncAdapter {
}
List<String> updated = new ArrayList<String>();
+ String updatedId = null;
for(int i = 0; i < podcastList.size(); i++) {
SyncSet set = podcastList.get(i);
String id = set.id;
@@ -80,6 +80,7 @@ public class PodcastSyncAdapter extends SubsonicSyncAdapter {
if(entry.getId() != null && "completed".equals(((PodcastEpisode)entry).getStatus()) && !existingEpisodes.contains(entry.getId())) {
DownloadFile file = new DownloadFile(context, entry, false);
while(!file.isCompleteFileAvailable() && !file.isFailedMax()) {
+ throwIfNetworkInvalid();
file.downloadNow(musicService);
}
// Only add if actualy downloaded correctly
@@ -87,6 +88,9 @@ public class PodcastSyncAdapter extends SubsonicSyncAdapter {
existingEpisodes.add(entry.getId());
if(!updated.contains(podcasts.getName())) {
updated.add(podcasts.getName());
+ if(updatedId == null) {
+ updatedId = podcasts.getId();
+ }
}
}
}
@@ -104,7 +108,7 @@ public class PodcastSyncAdapter extends SubsonicSyncAdapter {
// Make sure there are is at least one change before re-syncing
if(updated.size() > 0) {
FileUtil.serialize(context, podcastList, SyncUtil.getPodcastSyncFile(context, instance));
- Notifications.showSyncNotification(context, R.string.sync_new_podcasts, SyncUtil.joinNames(updated));
+ Notifications.showSyncNotification(context, R.string.sync_new_podcasts, SyncUtil.joinNames(updated), updatedId);
}
} catch(Exception e) {
Log.w(TAG, "Failed to get podcasts for " + Util.getServerName(context, instance));
diff --git a/app/src/main/java/github/daneren2005/dsub/service/sync/StarredSyncAdapter.java b/app/src/main/java/github/daneren2005/dsub/service/sync/StarredSyncAdapter.java
index cf985227..0af8886b 100644
--- a/app/src/main/java/github/daneren2005/dsub/service/sync/StarredSyncAdapter.java
+++ b/app/src/main/java/github/daneren2005/dsub/service/sync/StarredSyncAdapter.java
@@ -20,7 +20,6 @@
package github.daneren2005.dsub.service.sync;
import android.annotation.TargetApi;
-import android.app.Notification;
import android.content.Context;
import android.util.Log;
@@ -50,7 +49,7 @@ public class StarredSyncAdapter extends SubsonicSyncAdapter {
}
@Override
- public void onExecuteSync(Context context, int instance) {
+ public void onExecuteSync(Context context, int instance) throws NetworkNotValidException {
try {
ArrayList<String> syncedList = new ArrayList<String>();
MusicDirectory starredList = musicService.getStarredList(context, null);
diff --git a/app/src/main/java/github/daneren2005/dsub/service/sync/SubsonicSyncAdapter.java b/app/src/main/java/github/daneren2005/dsub/service/sync/SubsonicSyncAdapter.java
index 661f126d..4879d032 100644
--- a/app/src/main/java/github/daneren2005/dsub/service/sync/SubsonicSyncAdapter.java
+++ b/app/src/main/java/github/daneren2005/dsub/service/sync/SubsonicSyncAdapter.java
@@ -65,39 +65,54 @@ public class SubsonicSyncAdapter extends AbstractThreadedSyncAdapter {
@Override
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
- ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
- NetworkInfo networkInfo = manager.getActiveNetworkInfo();
-
- // Don't try to sync if no network!
- if(networkInfo == null || !networkInfo.isConnected() || Util.isOffline(context)) {
- Log.w(TAG, "Not running sync, not connected to network");
+ 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) {
+ 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) {
+
+ 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) {
- executeSync(context);
+ return null;
} else {
- Log.w(TAG, "Not running sync, not connected to wifi");
+ return "Not connected to WIFI";
}
} else {
- executeSync(context);
+ return null;
+ }
+ }
+ protected void throwIfNetworkInvalid() throws NetworkNotValidException {
+ String invalidMessage = isNetworkValid();
+ if(invalidMessage != null) {
+ throw new NetworkNotValidException(invalidMessage);
}
}
@@ -106,32 +121,39 @@ public class SubsonicSyncAdapter extends AbstractThreadedSyncAdapter {
Log.i(TAG, "Running sync for " + className);
long start = System.currentTimeMillis();
int servers = Util.getServerCount(context);
- for(int i = 1; i <= servers; i++) {
- try {
- if(isValidServer(context, i) && Util.isSyncEnabled(context, i)) {
- tagBrowsing = Util.isTagBrowsing(context, i);
- musicService.setInstance(i);
- onExecuteSync(context, i);
- } else {
- Log.i(TAG, "Skipped sync for " + i);
+ 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(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) {
+ public void onExecuteSync(Context context, int instance) throws NetworkNotValidException {
}
- protected boolean downloadRecursively(List<String> paths, MusicDirectory parent, Context context, boolean save) throws Exception {
+ protected boolean downloadRecursively(List<String> paths, MusicDirectory parent, Context context, boolean save) throws Exception,NetworkNotValidException {
boolean downloaded = false;
for (MusicDirectory.Entry song: parent.getChildren(false, true)) {
if (!song.isVideo()) {
DownloadFile file = new DownloadFile(context, song, save);
while(!(save && file.isSaved() || !save && file.isCompleteFileAvailable()) && !file.isFailedMax()) {
+ throwIfNetworkInvalid();
file.downloadNow(musicService);
if(!file.isFailed()) {
downloaded = true;
@@ -171,4 +193,10 @@ public class SubsonicSyncAdapter extends AbstractThreadedSyncAdapter {
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/daneren2005/dsub/updates/Updater.java b/app/src/main/java/github/daneren2005/dsub/updates/Updater.java
index a2870941..2dabb624 100644
--- a/app/src/main/java/github/daneren2005/dsub/updates/Updater.java
+++ b/app/src/main/java/github/daneren2005/dsub/updates/Updater.java
@@ -37,13 +37,18 @@ public class Updater {
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<Updater> updaters = new ArrayList<Updater>();
- updaters.add(new Updater403());
+ updaters.add(new UpdaterSongPress());
+ updaters.add(new UpdaterNoDLNA());
SharedPreferences prefs = Util.getPreferences(context);
int lastVersion = prefs.getInt(Constants.LAST_VERSION, 0);
diff --git a/app/src/main/java/github/daneren2005/dsub/updates/Updater403.java b/app/src/main/java/github/daneren2005/dsub/updates/Updater403.java
deleted file mode 100644
index 4f2cbf43..00000000
--- a/app/src/main/java/github/daneren2005/dsub/updates/Updater403.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- This file is part of Subsonic.
-
- Subsonic is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- Subsonic is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
-
- Copyright 2009 (C) Sindre Mehus
- */
-package github.daneren2005.dsub.updates;
-
-import android.content.Context;
-import android.util.Log;
-import github.daneren2005.dsub.updates.Updater;
-import github.daneren2005.dsub.util.Constants;
-import github.daneren2005.dsub.util.FileUtil;
-import java.io.File;
-
-/**
- *
- * @author Scott
- */
-public class Updater403 extends Updater {
- public Updater403() {
- super(403);
- TAG = Updater403.class.getSimpleName();
- }
-
- @Override
- public void update(Context context) {
- // Rename cover.jpeg to cover.jpg
- Log.i(TAG, "Running Updater403: updating cover.jpg to albumart.jpg");
- File dir = FileUtil.getMusicDirectory(context);
- if(dir != null) {
- moveArt(dir);
- }
- }
-
- private void moveArt(File dir) {
- for(File file: dir.listFiles()) {
- if(file.isDirectory()) {
- moveArt(file);
- } else if("cover.jpg".equals(file.getName()) || "cover.jpeg".equals(file.getName())) {
- File renamed = new File(dir, Constants.ALBUM_ART_FILE);
- file.renameTo(renamed);
- }
- }
- }
-}
diff --git a/app/src/main/java/github/daneren2005/dsub/updates/UpdaterNoDLNA.java b/app/src/main/java/github/daneren2005/dsub/updates/UpdaterNoDLNA.java
new file mode 100644
index 00000000..a060c4fd
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/updates/UpdaterNoDLNA.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 <http://www.gnu.org/licenses/>.
+ Copyright 2016 (C) Scott Jackson
+*/
+
+package github.daneren2005.dsub.updates;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Build;
+
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.Util;
+
+public class UpdaterNoDLNA extends Updater {
+ public UpdaterNoDLNA() {
+ super(534);
+ TAG = this.getClass().getSimpleName();
+ }
+
+ @Override
+ public void update(Context context) {
+ SharedPreferences prefs = Util.getPreferences(context);
+
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putBoolean(Constants.PREFERENCES_KEY_DLNA_CASTING_ENABLED, false);
+ editor.commit();
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/updates/UpdaterSongPress.java b/app/src/main/java/github/daneren2005/dsub/updates/UpdaterSongPress.java
new file mode 100644
index 00000000..7efa18e4
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/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 <http://www.gnu.org/licenses/>.
+ Copyright 2016 (C) Scott Jackson
+*/
+
+package github.daneren2005.dsub.updates;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.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/daneren2005/dsub/util/ArtistRadioBuffer.java b/app/src/main/java/github/daneren2005/dsub/util/ArtistRadioBuffer.java
index 2af468f6..bdd961b4 100644
--- a/app/src/main/java/github/daneren2005/dsub/util/ArtistRadioBuffer.java
+++ b/app/src/main/java/github/daneren2005/dsub/util/ArtistRadioBuffer.java
@@ -66,7 +66,6 @@ public class ArtistRadioBuffer {
buffer.clear();
}
- context.clear();
this.artistId = artistId;
awaitingResults = true;
refill();
@@ -108,7 +107,7 @@ public class ArtistRadioBuffer {
}
private void refill() {
- if (buffer != null && (buffer.size() > refillThreshold || (!Util.isNetworkConnected(context) && !Util.isOffline(context)) || lastCount == 0)) {
+ if (buffer != null && executorService != null && (buffer.size() > refillThreshold || (!Util.isNetworkConnected(context) && !Util.isOffline(context)) || lastCount == 0)) {
executorService.shutdown();
return;
}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/BackgroundTask.java b/app/src/main/java/github/daneren2005/dsub/util/BackgroundTask.java
index 18f245d5..2b0c6279 100644
--- a/app/src/main/java/github/daneren2005/dsub/util/BackgroundTask.java
+++ b/app/src/main/java/github/daneren2005/dsub/util/BackgroundTask.java
@@ -27,6 +27,7 @@ import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
import org.xmlpull.v1.XmlPullParserException;
@@ -54,6 +55,7 @@ public abstract class BackgroundTask<T> implements ProgressListener {
private static final Collection<Thread> threads = Collections.synchronizedCollection(new ArrayList<Thread>());
protected static final BlockingQueue<BackgroundTask.Task> queue = new LinkedBlockingQueue<BackgroundTask.Task>(10);
private static Handler handler = null;
+ private static AtomicInteger currentlyRunning = new AtomicInteger(0);
static {
try {
handler = new Handler(Looper.getMainLooper());
@@ -71,6 +73,11 @@ public abstract class BackgroundTask<T> implements ProgressListener {
threads.add(thread);
thread.start();
}
+ } else if(currentlyRunning.get() >= threads.size()) {
+ Log.w(TAG, "Emergency add new thread: " + (threads.size() + 1));
+ Thread thread = new Thread(new TaskRunnable(), String.format("BackgroundTask_%d", threads.size()));
+ threads.add(thread);
+ thread.start();
}
if(handler == null) {
try {
@@ -176,7 +183,7 @@ public abstract class BackgroundTask<T> implements ProgressListener {
}
@Override
- public void updateCache() {
+ public void updateCache(int changeCode) {
}
@@ -208,8 +215,18 @@ public abstract class BackgroundTask<T> implements ProgressListener {
handler.post(new Runnable() {
@Override
public void run() {
- if(!isCancelled()) {
- onDone(result);
+ if (!isCancelled()) {
+ try {
+ onDone(result);
+ } catch (Throwable t) {
+ if(!isCancelled()) {
+ try {
+ onError(t);
+ } catch(Exception e) {
+ // Don't care
+ }
+ }
+ }
}
taskStart.set(false);
@@ -294,22 +311,30 @@ public abstract class BackgroundTask<T> implements ProgressListener {
@Override
public void run() {
Looper.prepare();
+ final Thread currentThread = Thread.currentThread();
while(running) {
try {
Task task = queue.take();
+ currentlyRunning.incrementAndGet();
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);
+ running = false;
}
+
+ currentlyRunning.decrementAndGet();
+ }
+
+ if(threads.contains(currentThread)) {
+ threads.remove(currentThread);
}
}
}
- public static interface OnCancelListener {
+ public interface OnCancelListener {
void onCancel();
}
}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/Constants.java b/app/src/main/java/github/daneren2005/dsub/util/Constants.java
index 89e7de3b..21adce8c 100644
--- a/app/src/main/java/github/daneren2005/dsub/util/Constants.java
+++ b/app/src/main/java/github/daneren2005/dsub/util/Constants.java
@@ -67,6 +67,7 @@ public final class Constants {
public static final String INTENT_EXTRA_TOP_TRACKS = "topTracks";
public static final String INTENT_EXTRA_SHOW_ALL = "showAll";
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";
@@ -133,6 +134,7 @@ public final class Constants {
public static final String PREFERENCES_KEY_HIDE_WIDGET = "hideWidget";
public static final String PREFERENCES_KEY_PODCASTS_ENABLED = "podcastsEnabled";
public static final String PREFERENCES_KEY_BOOKMARKS_ENABLED = "bookmarksEnabled";
+ public static final String PREFERENCES_KEY_INTERNET_RADIO_ENABLED = "internetRadioEnabled";
public static final String PREFERENCES_KEY_CUSTOM_SORT_ENABLED = "customSortEnabled";
public static final String PREFERENCES_KEY_MENU_PLAY_NOW = "showPlayNow";
public static final String PREFERENCES_KEY_MENU_PLAY_SHUFFLED = "showPlayShuffled";
@@ -147,7 +149,8 @@ public final class Constants {
public static final String PREFERENCES_KEY_BROWSE_TAGS = "browseTags";
public static final String PREFERENCES_KEY_OPEN_TO_TAB = "openToTab";
public static final String PREFERENCES_KEY_OVERRIDE_SYSTEM_LANGUAGE = "overrideSystemLanguage";
- public static final String PREFERENCES_KEY_PLAY_NOW_AFTER = "playNowAfter";
+ // public static final String PREFERENCES_KEY_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_ADMIN_ENABLED = "adminEnabled";
public static final String PREFERENCES_KEY_PLAYLIST_NAME = "suggestedPlaylistName";
@@ -168,6 +171,14 @@ public final class Constants {
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_CAST_GAPLESS_PLAYBACK = "castingGaplessPlayback";
+ public static final String PREFERENCES_KEY_CAST_STREAM_ORIGINAL = "castStreamOriginal";
+ public static final String PREFERENCES_KEY_HEADS_UP_NOTIFICATION = "headsUpNotification";
+ public static final String PREFERENCES_KEY_CAST_CACHE = "castCache";
+ public static final String PREFERENCES_KEY_PLAYBACK_SPEED = "playbackSpeed";
+ public static final String PREFERENCES_KEY_SONG_PLAYBACK_SPEED = "songPlaybackSpeed";
+ public static final String PREFERENCES_KEY_DLNA_CASTING_ENABLED = "dlnaCastingEnabled";
public static final String OFFLINE_SCROBBLE_COUNT = "scrobbleCount";
public static final String OFFLINE_SCROBBLE_ID = "scrobbleID";
@@ -180,10 +191,13 @@ public final class Constants {
public static final String CACHE_KEY_IGNORE = "ignoreArticles";
public static final String CACHE_AUDIO_SESSION_ID = "audioSessionId";
+ public static final String CACHE_AUDIO_SESSION_VERSION_CODE = "audioSessionVersionCode";
+ 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";
diff --git a/app/src/main/java/github/daneren2005/dsub/util/DrawableTint.java b/app/src/main/java/github/daneren2005/dsub/util/DrawableTint.java
index 2da72579..f03906a8 100644
--- a/app/src/main/java/github/daneren2005/dsub/util/DrawableTint.java
+++ b/app/src/main/java/github/daneren2005/dsub/util/DrawableTint.java
@@ -21,6 +21,7 @@ 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;
@@ -48,6 +49,17 @@ public class DrawableTint {
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)) {
@@ -83,7 +95,7 @@ public class DrawableTint {
return getTintedDrawable(context, drawableRes, colorAttr);
}
- public static void wipeTintCache() {
+ public static void clearCache() {
attrMap.clear();
tintedDrawables.clear();
}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/DroppySpeedControl.java b/app/src/main/java/github/daneren2005/dsub/util/DroppySpeedControl.java
new file mode 100644
index 00000000..8f58e60e
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/DroppySpeedControl.java
@@ -0,0 +1,82 @@
+package github.daneren2005.dsub.util;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.view.View;
+import android.widget.SeekBar;
+import android.widget.TextView;
+import com.shehabic.droppy.DroppyClickCallbackInterface;
+import com.shehabic.droppy.DroppyMenuCustomItem;
+
+/**
+ * Created by marcus on 2/14/2017.
+ */
+public class DroppySpeedControl extends DroppyMenuCustomItem {
+
+ private Context context;
+ private SeekBar seekBar;
+ private DroppyClickCallbackInterface updateBarCallback;
+ public DroppySpeedControl(int customResourceId) {
+ super(customResourceId);
+
+ }
+
+ @Override
+ public View render(Context context) {
+ return super.render(context);
+
+
+ }
+
+ public DroppySpeedControl setOnClicks(Context context, final DroppyClickCallbackInterface callback, int ... elementsByID){
+ render(context);
+ View.OnClickListener listener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ callback.call(v, v.getId());
+ }
+ };
+ for (Integer element : elementsByID) {
+ renderedView.findViewById(element).setOnClickListener(listener);
+ }
+ return this;
+ }
+
+
+ public void updateSeekBar(float playbackSpeed){
+ TextView tv = (TextView)seekBar.getTag();
+ tv.setText(Float.toString(playbackSpeed));
+ seekBar.setProgress((int)(playbackSpeed*10)-5);
+ }
+
+ public DroppySpeedControl setOnSeekBarChangeListener(Context context, final DroppyClickCallbackInterface callback, int seekBarByID, int textViewByID, float playbackSpeed) {
+ updateBarCallback = callback;
+ render(context);
+ final TextView textBox = (TextView) renderedView.findViewById(textViewByID);
+ textBox.setText(Float.toString(playbackSpeed));
+ SeekBar seekBar = ((SeekBar) renderedView.findViewById(seekBarByID));
+ this.seekBar = seekBar;
+ seekBar.setTag(textBox);
+ seekBar.setProgress((int)(playbackSpeed*10)-5);
+ seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ if (fromUser) {
+ textBox.setText(new Float((progress + 5) / 10.0).toString());
+ seekBar.setProgress(progress);
+ callback.call(seekBar,seekBar.getId());
+ }
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ }
+ });
+ seekBar.setProgress((int)((playbackSpeed/10.0) - 5));
+ return this;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/EnvironmentVariables.java b/app/src/main/java/github/daneren2005/dsub/util/EnvironmentVariables.java
new file mode 100644
index 00000000..710d5232
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/EnvironmentVariables.java
@@ -0,0 +1,21 @@
+/*
+ This file is part of Subsonic.
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+ Copyright 2016 (C) Scott Jackson
+*/
+
+package github.daneren2005.dsub.util;
+
+public final class EnvironmentVariables {
+ public static final String PASTEBIN_DEV_KEY = "";
+ public static final String CAST_APPLICATION_ID = "";
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/ImageLoader.java b/app/src/main/java/github/daneren2005/dsub/util/ImageLoader.java
index 85844360..2321e69e 100644
--- a/app/src/main/java/github/daneren2005/dsub/util/ImageLoader.java
+++ b/app/src/main/java/github/daneren2005/dsub/util/ImageLoader.java
@@ -43,6 +43,7 @@ import java.lang.ref.WeakReference;
import github.daneren2005.dsub.R;
import github.daneren2005.dsub.domain.ArtistInfo;
+import github.daneren2005.dsub.domain.InternetRadioStation;
import github.daneren2005.dsub.domain.MusicDirectory;
import github.daneren2005.dsub.domain.Playlist;
import github.daneren2005.dsub.domain.PodcastChannel;
@@ -221,8 +222,11 @@ public class ImageLoader {
return loadImage(view, entry, large, size, crossfade);
}
public SilentBackgroundTask loadImage(View view, MusicDirectory.Entry entry, boolean large, int size, boolean crossfade) {
- // TODO: If we know this a artist, try to load artist info instead
- if(entry != null && !entry.isAlbum() && ServerInfo.checkServerVersion(context, "1.11") && !Util.isOffline(context)) {
+ if(entry != null && entry instanceof InternetRadioStation) {
+ // Continue on and load a null bitmap
+ }
+ // If we know this a artist, try to load artist info instead
+ else if(entry != null && !entry.isAlbum() && ServerInfo.checkServerVersion(context, "1.11") && !Util.isOffline(context)) {
SilentBackgroundTask task = new ArtistImageTask(view.getContext(), entry, size, imageSizeLarge, large, view, crossfade);
task.execute();
return task;
@@ -524,44 +528,49 @@ public class ImageLoader {
@Override
protected Void doInBackground() throws Throwable {
- MusicService musicService = MusicServiceFactory.getMusicService(mContext);
- ArtistInfo artistInfo = musicService.getArtistInfo(mEntry.getId(), false, true, mContext, null);
- String url = artistInfo.getImageUrl();
-
- // Figure out whether we are going to get a artist image or the standard image
- if(url != null && !"".equals(url.trim())) {
- // If getting the artist image fails for any reason, retry for the standard version
- subTask = new ViewUrlTask(mContext, mView, url, mSize) {
- @Override
- protected void failedToDownload() {
- // Call loadImage so we can take advantage of all of it's logic checks
- loadImage(mView, mEntry, mSize == imageSizeLarge, mCrossfade);
-
- // Delete subTask so it doesn't get called in done
- subTask = null;
+ try {
+ MusicService musicService = MusicServiceFactory.getMusicService(mContext);
+ ArtistInfo artistInfo = musicService.getArtistInfo(mEntry.getId(), false, true, mContext, null);
+ String url = artistInfo.getImageUrl();
+
+ // Figure out whether we are going to get a artist image or the standard image
+ if (url != null && !"".equals(url.trim())) {
+ // If getting the artist image fails for any reason, retry for the standard version
+ subTask = new ViewUrlTask(mContext, mView, url, mSize) {
+ @Override
+ protected void failedToDownload() {
+ // Call loadImage so we can take advantage of all of it's logic checks
+ loadImage(mView, mEntry, mSize == imageSizeLarge, mCrossfade);
+
+ // Delete subTask so it doesn't get called in done
+ subTask = null;
+ }
+ };
+ } else {
+ if (mEntry != null && mEntry.getCoverArt() == null && mEntry.isDirectory() && !Util.isOffline(context)) {
+ // Try to lookup child cover art
+ MusicDirectory.Entry firstChild = FileUtil.lookupChild(context, mEntry, true);
+ if (firstChild != null) {
+ mEntry.setCoverArt(firstChild.getCoverArt());
+ }
}
- };
- } else {
- if (mEntry != null && mEntry.getCoverArt() == null && mEntry.isDirectory() && !Util.isOffline(context)) {
- // Try to lookup child cover art
- MusicDirectory.Entry firstChild = FileUtil.lookupChild(context, mEntry, true);
- if (firstChild != null) {
- mEntry.setCoverArt(firstChild.getCoverArt());
+
+ if (mEntry != null && mEntry.getCoverArt() != null) {
+ subTask = new ViewImageTask(mContext, mEntry, mSize, mSaveSize, mIsNowPlaying, mView, mCrossfade);
+ } else {
+ // If entry is null as well, we need to just set as a blank image
+ Bitmap bitmap = getUnknownImage(mEntry, mSize);
+ mDrawable = Util.createDrawableFromBitmap(mContext, bitmap);
+ return null;
}
}
- 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);
}
-
- // Execute whichever way we decided to go
- subTask.doInBackground();
return null;
}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/MediaRouteManager.java b/app/src/main/java/github/daneren2005/dsub/util/MediaRouteManager.java
index 9aa54c4b..73ec6aec 100644
--- a/app/src/main/java/github/daneren2005/dsub/util/MediaRouteManager.java
+++ b/app/src/main/java/github/daneren2005/dsub/util/MediaRouteManager.java
@@ -48,6 +48,7 @@ public class MediaRouteManager extends MediaRouter.Callback {
private MediaRouteSelector selector;
private List<MediaRouteProvider> providers = new ArrayList<MediaRouteProvider>();
private List<MediaRouteProvider> onlineProviders = new ArrayList<MediaRouteProvider>();
+ private DLNARouteProvider dlnaProvider;
static {
try {
@@ -159,10 +160,8 @@ public class MediaRouteManager extends MediaRouter.Callback {
addOnlineProviders();
}
- if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
- DLNARouteProvider dlnaProvider = new DLNARouteProvider(downloadService);
- router.addProvider(dlnaProvider);
- providers.add(dlnaProvider);
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH && Util.getPreferences(downloadService).getBoolean(Constants.PREFERENCES_KEY_DLNA_CASTING_ENABLED, true)) {
+ addDLNAProvider();
}
}
public void buildSelector() {
@@ -178,4 +177,20 @@ public class MediaRouteManager extends MediaRouter.Callback {
}
selector = builder.build();
}
+
+ public void addDLNAProvider() {
+ if(dlnaProvider == null) {
+ dlnaProvider = new DLNARouteProvider(downloadService);
+ router.addProvider(dlnaProvider);
+ providers.add(dlnaProvider);
+ }
+ }
+ public void removeDLNAProvider() {
+ if(dlnaProvider != null) {
+ router.removeProvider(dlnaProvider);
+ providers.remove(dlnaProvider);
+ dlnaProvider.destroy();
+ dlnaProvider = null;
+ }
+ }
}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/Notifications.java b/app/src/main/java/github/daneren2005/dsub/util/Notifications.java
index 375c9966..750ab40c 100644
--- a/app/src/main/java/github/daneren2005/dsub/util/Notifications.java
+++ b/app/src/main/java/github/daneren2005/dsub/util/Notifications.java
@@ -41,6 +41,7 @@ import github.daneren2005.dsub.domain.PlayerState;
import github.daneren2005.dsub.provider.DSubWidgetProvider;
import github.daneren2005.dsub.service.DownloadFile;
import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.view.UpdateView;
public final class Notifications {
private static final String TAG = Notifications.class.getSimpleName();
@@ -66,23 +67,29 @@ public final class Notifications {
notification.flags |= Notification.FLAG_NO_CLEAR | Notification.FLAG_ONGOING_EVENT;
}
boolean remote = downloadService.isRemoteEnabled();
+ boolean isSingle = downloadService.isCurrentPlayingSingle();
+ boolean shouldFastForward = downloadService.shouldFastForward();
if (Build.VERSION.SDK_INT>= Build.VERSION_CODES.JELLY_BEAN){
RemoteViews expandedContentView = new RemoteViews(context.getPackageName(), R.layout.notification_expanded);
- setupViews(expandedContentView ,context, song, true, playing, remote);
+ setupViews(expandedContentView ,context, song, true, playing, remote, isSingle, shouldFastForward);
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, remote);
+ setupViews(smallContentView, context, song, false, playing, remote, isSingle, shouldFastForward);
notification.contentView = smallContentView;
Intent notificationIntent = new Intent(context, SubsonicFragmentActivity.class);
notificationIntent.putExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD, true);
- notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ notificationIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
notification.contentIntent = PendingIntent.getActivity(context, 0, notificationIntent, 0);
playShowing = true;
@@ -93,21 +100,35 @@ public final class Notifications {
public void run() {
downloadService.stopForeground(true);
showDownloadingNotification(context, downloadService, handler, downloadService.getCurrentDownloading(), downloadService.getBackgroundDownloads().size());
- downloadService.startForeground(NOTIFICATION_ID_PLAYING, notification);
+
+ try {
+ downloadService.startForeground(NOTIFICATION_ID_PLAYING, notification);
+ } catch(Exception e) {
+ Log.e(TAG, "Failed to start notifications after stopping foreground download");
+ }
}
});
} else {
handler.post(new Runnable() {
@Override
public void run() {
- if(playing) {
- downloadService.startForeground(NOTIFICATION_ID_PLAYING, notification);
+ if (playing) {
+ try {
+ downloadService.startForeground(NOTIFICATION_ID_PLAYING, notification);
+ } catch(Exception e) {
+ Log.e(TAG, "Failed to start notifications while playing");
+ }
} else {
playShowing = false;
persistentPlayingShowing = true;
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
downloadService.stopForeground(false);
- notificationManager.notify(NOTIFICATION_ID_PLAYING, notification);
+
+ try {
+ notificationManager.notify(NOTIFICATION_ID_PLAYING, notification);
+ } catch(Exception e) {
+ Log.e(TAG, "Failed to start notifications while paused");
+ }
}
}
});
@@ -117,8 +138,7 @@ public final class Notifications {
DSubWidgetProvider.notifyInstances(context, downloadService, playing);
}
- private static void setupViews(RemoteViews rv, Context context, MusicDirectory.Entry song, boolean expanded, boolean playing, boolean remote){
-
+ private static void setupViews(RemoteViews rv, Context context, MusicDirectory.Entry song, boolean expanded, boolean playing, boolean remote, boolean isSingleFile, boolean shouldFastForward) {
// Use the same text for the ticker and the expanded notification
String title = song.getTitle();
String arist = song.getArtist();
@@ -152,49 +172,112 @@ public final class Notifications {
if(persistent) {
if(expanded) {
rv.setImageViewResource(R.id.control_pause, playing ? R.drawable.notification_pause : R.drawable.notification_start);
+
+ if(shouldFastForward) {
+ rv.setImageViewResource(R.id.control_previous, R.drawable.notification_rewind);
+ rv.setImageViewResource(R.id.control_next, R.drawable.notification_fastforward);
+ } else {
+ 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);
+ if(shouldFastForward) {
+ rv.setImageViewResource(R.id.control_pause, R.drawable.notification_fastforward);
+ } else {
+ rv.setImageViewResource(R.id.control_pause, R.drawable.notification_forward);
+ }
rv.setImageViewResource(R.id.control_next, R.drawable.notification_close);
}
+ } else if(shouldFastForward) {
+ rv.setImageViewResource(R.id.control_previous, R.drawable.notification_rewind);
+ rv.setImageViewResource(R.id.control_next, R.drawable.notification_fastforward);
+ } 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;
- if(persistent && !expanded) {
- pause = R.id.control_previous;
- next = R.id.control_pause;
- close = R.id.control_next;
+ int previous = 0, pause = 0, next = 0, close = 0, rewind = 0, fastForward = 0;
+ if (expanded) {
+ if (shouldFastForward) {
+ rewind = R.id.control_previous;
+ pause = R.id.control_pause;
+ fastForward = R.id.control_next;
+ } else {
+ previous = R.id.control_previous;
+ pause = R.id.control_pause;
+ next = R.id.control_next;
+ }
+
+ if (remote || persistent) {
+ close = R.id.notification_close;
+ rv.setViewVisibility(close, View.VISIBLE);
+ }
} else {
- previous = R.id.control_previous;
- pause = R.id.control_pause;
- next = R.id.control_next;
+ if (persistent) {
+ pause = R.id.control_previous;
+ if(shouldFastForward) {
+ fastForward = R.id.control_pause;
+ } else {
+ next = R.id.control_pause;
+ }
+ close = R.id.control_next;
+ } else {
+ rewind = R.id.control_previous;
+ pause = R.id.control_pause;
+ fastForward = R.id.control_next;
+ }
}
- if((remote || persistent) && close == 0 && expanded) {
- close = R.id.notification_close;
- rv.setViewVisibility(close, View.VISIBLE);
+ if(isSingleFile) {
+ if(previous > 0) {
+ rv.setViewVisibility(previous, View.GONE);
+ previous = 0;
+ }
+ if(rewind > 0) {
+ rv.setViewVisibility(rewind, View.GONE);
+ rewind = 0;
+ }
+
+ if(next > 0) {
+ rv.setViewVisibility(next, View.GONE);
+ next = 0;
+ }
+
+ if(fastForward > 0) {
+ rv.setViewVisibility(fastForward, View.GONE);
+ fastForward = 0;
+ }
}
+ PendingIntent pendingIntent;
if(previous > 0) {
Intent prevIntent = new Intent("KEYCODE_MEDIA_PREVIOUS");
prevIntent.setComponent(new ComponentName(context, DownloadService.class));
- prevIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PREVIOUS));
+ 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_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE));
+ 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_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY));
+ 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);
}
@@ -202,14 +285,21 @@ public final class Notifications {
if(next > 0) {
Intent nextIntent = new Intent("KEYCODE_MEDIA_NEXT");
nextIntent.setComponent(new ComponentName(context, DownloadService.class));
- nextIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT));
+ 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_DOWN, KeyEvent.KEYCODE_MEDIA_STOP));
+ 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);
}
@@ -270,7 +360,7 @@ public final class Notifications {
Intent notificationIntent = new Intent(context, SubsonicFragmentActivity.class);
notificationIntent.putExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD_VIEW, true);
- notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ notificationIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
builder.setContentIntent(PendingIntent.getActivity(context, 2, notificationIntent, 0));
final Notification notification = builder.build();
@@ -306,6 +396,9 @@ public final class Notifications {
}
public static void showSyncNotification(final Context context, int stringId, String extra) {
+ showSyncNotification(context, stringId, extra, null);
+ }
+ public static void showSyncNotification(final Context context, int stringId, String extra, String extraId) {
if(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_SYNC_NOTIFICATION, true)) {
if(extra == null) {
extra = "";
@@ -323,7 +416,7 @@ public final class Notifications {
.setAutoCancel(true);
Intent notificationIntent = new Intent(context, SubsonicFragmentActivity.class);
- notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ notificationIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
String tab = null, type = null;
switch(stringId) {
@@ -346,6 +439,9 @@ public final class Notifications {
if(type != null) {
notificationIntent.putExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, type);
}
+ if(extraId != null) {
+ notificationIntent.putExtra(Constants.INTENT_EXTRA_NAME_ID, extraId);
+ }
builder.setContentIntent(PendingIntent.getActivity(context, stringId, notificationIntent, 0));
diff --git a/app/src/main/java/github/daneren2005/dsub/util/ProgressListener.java b/app/src/main/java/github/daneren2005/dsub/util/ProgressListener.java
index 22f35efc..603b1ccb 100644
--- a/app/src/main/java/github/daneren2005/dsub/util/ProgressListener.java
+++ b/app/src/main/java/github/daneren2005/dsub/util/ProgressListener.java
@@ -24,5 +24,5 @@ package github.daneren2005.dsub.util;
public interface ProgressListener {
void updateProgress(String message);
void updateProgress(int messageId);
- void updateCache();
+ void updateCache(int changeCode);
}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/SettingsBackupAgent.java b/app/src/main/java/github/daneren2005/dsub/util/SettingsBackupAgent.java
index def97cac..e57658c4 100644
--- a/app/src/main/java/github/daneren2005/dsub/util/SettingsBackupAgent.java
+++ b/app/src/main/java/github/daneren2005/dsub/util/SettingsBackupAgent.java
@@ -21,6 +21,7 @@ package github.daneren2005.dsub.util;
import android.app.backup.BackupAgentHelper;
import android.app.backup.BackupDataInput;
import android.app.backup.SharedPreferencesBackupHelper;
+import android.content.SharedPreferences;
import android.os.ParcelFileDescriptor;
import java.io.IOError;
@@ -39,6 +40,10 @@ public class SettingsBackupAgent extends BackupAgentHelper {
@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();
+
+ SharedPreferences.Editor editor = Util.getPreferences(this).edit();
+ editor.remove(Constants.PREFERENCES_KEY_CACHE_LOCATION);
+ editor.remove(Constants.CACHE_AUDIO_SESSION_ID);
+ editor.apply();
}
}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/SongDBHandler.java b/app/src/main/java/github/daneren2005/dsub/util/SongDBHandler.java
index 8d91a251..1309ee69 100644
--- a/app/src/main/java/github/daneren2005/dsub/util/SongDBHandler.java
+++ b/app/src/main/java/github/daneren2005/dsub/util/SongDBHandler.java
@@ -150,11 +150,23 @@ public class SongDBHandler extends SQLiteOpenHelper {
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<Integer, String> pair) {
- return getLastPlayed(pair.getFirst(), pair.getSecond());
+ if(pair == null) {
+ return null;
+ } else {
+ return getLastPlayed(pair.getFirst(), pair.getSecond());
+ }
}
public synchronized Long[] getLastPlayed(int serverKey, String id) {
SQLiteDatabase db = this.getReadableDatabase();
@@ -169,9 +181,12 @@ public class SongDBHandler extends SQLiteOpenHelper {
dates[0] = cursor.getLong(0);
dates[1] = cursor.getLong(1);
return dates;
- } catch(Exception e) {}
-
- return null;
+ } catch(Exception e) {
+ return null;
+ }
+ finally {
+ db.close();
+ }
}
public synchronized Pair<Integer, String> getOnlineSongId(MusicDirectory.Entry entry) {
@@ -210,9 +225,12 @@ public class SongDBHandler extends SQLiteOpenHelper {
try {
cursor.moveToFirst();
return new Pair(cursor.getInt(0), cursor.getString(1));
- } catch(Exception e) {}
-
- return null;
+ } catch(Exception e) {
+ return null;
+ }
+ finally {
+ db.close();
+ }
}
public synchronized Pair<Integer, String> getIdFromPath(int serverKey, String path) {
SQLiteDatabase db = this.getReadableDatabase();
@@ -223,9 +241,12 @@ public class SongDBHandler extends SQLiteOpenHelper {
try {
cursor.moveToFirst();
return new Pair(cursor.getInt(0), cursor.getString(1));
- } catch(Exception e) {}
-
- return null;
+ } catch(Exception e) {
+ return null;
+ }
+ finally {
+ db.close();
+ }
}
public static SongDBHandler getHandler(Context context) {
diff --git a/app/src/main/java/github/daneren2005/dsub/util/ThemeUtil.java b/app/src/main/java/github/daneren2005/dsub/util/ThemeUtil.java
new file mode 100644
index 00000000..7de4f928
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/ThemeUtil.java
@@ -0,0 +1,112 @@
+/*
+ This file is part of Subsonic.
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+ Copyright 2016 (C) Scott Jackson
+*/
+
+package github.daneren2005.dsub.util;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.res.Configuration;
+
+import java.util.Locale;
+
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.activity.SettingsActivity;
+import github.daneren2005.dsub.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_HOLO = "holo";
+ 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_DSub_Dark_No_Actionbar;
+ } else if (THEME_BLACK.equals(theme)) {
+ return R.style.Theme_DSub_Black_No_Actionbar;
+ } else if (THEME_HOLO.equals(theme)) {
+ return R.style.Theme_DSub_Holo_No_Actionbar;
+ } else {
+ return R.style.Theme_DSub_Light_No_Actionbar;
+ }
+ } else {
+ if (THEME_DARK.equals(theme)) {
+ return R.style.Theme_DSub_Dark_No_Color;
+ } else if (THEME_BLACK.equals(theme)) {
+ return R.style.Theme_DSub_Black_No_Color;
+ } else if (THEME_HOLO.equals(theme)) {
+ return R.style.Theme_DSub_Holo_No_Color;
+ } else {
+ return R.style.Theme_DSub_Light_No_Color;
+ }
+ }
+ } else {
+ if (THEME_DARK.equals(theme)) {
+ return R.style.Theme_DSub_Dark;
+ } else if (THEME_BLACK.equals(theme)) {
+ return R.style.Theme_DSub_Black;
+ } else if (THEME_HOLO.equals(theme)) {
+ return R.style.Theme_DSub_Holo;
+ } else {
+ return R.style.Theme_DSub_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));
+
+ SharedPreferences prefs = Util.getPreferences(context);
+ if(prefs.getBoolean(Constants.PREFERENCES_KEY_OVERRIDE_SYSTEM_LANGUAGE, false)) {
+ Configuration config = new Configuration();
+ config.locale = Locale.ENGLISH;
+ context.getResources().updateConfiguration(config, context.getResources().getDisplayMetrics());
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/UpdateHelper.java b/app/src/main/java/github/daneren2005/dsub/util/UpdateHelper.java
index c7e0a04b..4cf25b30 100644
--- a/app/src/main/java/github/daneren2005/dsub/util/UpdateHelper.java
+++ b/app/src/main/java/github/daneren2005/dsub/util/UpdateHelper.java
@@ -27,6 +27,7 @@ import android.util.Log;
import android.view.View;
import android.widget.RatingBar;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -51,9 +52,24 @@ public final class UpdateHelper {
}
public static void toggleStarred(final Context context, final Entry entry, final OnStarChange onStarChange) {
- final boolean starred = !entry.isStarred();
- entry.setStarred(starred);
+ toggleStarred(context, Arrays.asList(entry), onStarChange);
+ }
+
+ public static void toggleStarred(Context context, List<Entry> entries) {
+ toggleStarred(context, entries, null);
+ }
+ public static void toggleStarred(final Context context, final List<Entry> entries, final OnStarChange onStarChange) {
+ if(entries.isEmpty()) {
+ return;
+ }
+
+ final Entry firstEntry = entries.get(0);
+ final boolean starred = !firstEntry.isStarred();
+ for(Entry entry: entries) {
+ entry.setStarred(starred);
+ }
if(onStarChange != null) {
+ onStarChange.entries = entries;
onStarChange.starChange(starred);
}
@@ -61,22 +77,30 @@ public final class UpdateHelper {
@Override
protected Void doInBackground() throws Throwable {
MusicService musicService = MusicServiceFactory.getMusicService(context);
- if(entry.isDirectory() && Util.isTagBrowsing(context) && !Util.isOffline(context)) {
- if(entry.isAlbum()) {
- musicService.setStarred(null, null, Arrays.asList(entry), starred, null, context);
+ List<Entry> songs = new ArrayList<Entry>();
+ List<Entry> artists = new ArrayList<Entry>();
+ List<Entry> albums = new ArrayList<Entry>();
+ for(Entry entry: entries) {
+ if(entry.isDirectory() && Util.isTagBrowsing(context)) {
+ if(entry.isAlbum()) {
+ albums.add(entry);
+ } else {
+ artists.add(entry);
+ }
} else {
- musicService.setStarred(null, Arrays.asList(entry), null, starred, null, context);
+ songs.add(entry);
}
- } else {
- musicService.setStarred(Arrays.asList(entry), null, null, starred, null, context);
}
-
- new EntryInstanceUpdater(entry) {
- @Override
- public void update(Entry found) {
- found.setStarred(starred);
- }
- }.execute();
+ musicService.setStarred(songs, artists, albums, starred, this, context);
+
+ for(Entry entry: entries) {
+ new UpdateHelper.EntryInstanceUpdater(entry) {
+ @Override
+ public void update(Entry found) {
+ found.setStarred(starred);
+ }
+ }.execute();
+ }
return null;
}
@@ -84,13 +108,21 @@ public final class UpdateHelper {
@Override
protected void done(Void result) {
// UpdateView
- Util.toast(context, context.getResources().getString(starred ? R.string.starring_content_starred : R.string.starring_content_unstarred, entry.getTitle()));
+ int starMsgId = starred ? R.string.starring_content_starred : R.string.starring_content_unstarred;
+ String starMsgBody = (entries.size() > 1) ? Integer.toString(entries.size()) : firstEntry.getTitle();
+ Util.toast(context, context.getResources().getString(starMsgId, starMsgBody));
+
+ if(onStarChange != null) {
+ onStarChange.starCommited(starred);
+ }
}
@Override
protected void error(Throwable error) {
Log.w(TAG, "Failed to star", error);
- entry.setStarred(!starred);
+ for(Entry entry: entries) {
+ entry.setStarred(!starred);
+ }
if(onStarChange != null) {
onStarChange.starChange(!starred);
}
@@ -99,7 +131,8 @@ public final class UpdateHelper {
if (error instanceof OfflineException || error instanceof ServerTooOldException) {
msg = getErrorMessage(error);
} else {
- msg = context.getResources().getString(R.string.starring_content_error, entry.getTitle()) + " " + getErrorMessage(error);
+ String errorBody = (entries.size() > 1) ? Integer.toString(entries.size()) : firstEntry.getTitle();
+ msg = context.getResources().getString(R.string.starring_content_error, errorBody) + " " + getErrorMessage(error);
}
Util.toast(context, msg, false);
@@ -215,6 +248,7 @@ public final class UpdateHelper {
msg = context.getResources().getString(rating > 0 ? R.string.rating_set_rating_failed : R.string.rating_remove_rating_failed, entry.getTitle()) + " " + getErrorMessage(error);
}
+ Log.e(TAG, "Failed to setRating", error);
Util.toast(context, msg, false);
}
}.execute();
@@ -222,10 +256,15 @@ public final class UpdateHelper {
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);
@@ -234,11 +273,17 @@ public final class UpdateHelper {
if(downloadService != null && !entry.isDirectory()) {
boolean serializeChanges = false;
List<DownloadFile> downloadFiles = downloadService.getDownloads();
+ DownloadFile currentPlaying = downloadService.getCurrentPlaying();
+
for(DownloadFile file: downloadFiles) {
Entry check = file.getSong();
if(entry.getId().equals(check.getId())) {
- update(entry);
+ update(check);
serializeChanges = true;
+
+ if(currentPlaying != null && currentPlaying.getSong() != null && currentPlaying.getSong().getId().equals(entry.getId())) {
+ downloadService.onMetadataUpdate(metadataUpdate);
+ }
}
}
@@ -255,7 +300,10 @@ public final class UpdateHelper {
}
public static abstract class OnStarChange {
+ protected List<Entry> entries;
+
public abstract void starChange(boolean starred);
+ public abstract void starCommited(boolean starred);
}
public static abstract class OnRatingChange {
public abstract void ratingChange(int rating);
diff --git a/app/src/main/java/github/daneren2005/dsub/util/UserUtil.java b/app/src/main/java/github/daneren2005/dsub/util/UserUtil.java
index 6417dc81..db1c628f 100644
--- a/app/src/main/java/github/daneren2005/dsub/util/UserUtil.java
+++ b/app/src/main/java/github/daneren2005/dsub/util/UserUtil.java
@@ -29,8 +29,6 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.WindowManager;
-import android.widget.ArrayAdapter;
-import android.widget.ListView;
import android.widget.TextView;
import github.daneren2005.dsub.R;
@@ -159,8 +157,11 @@ public final class UserUtil {
return defaultValue;
}
-
- public static void confirmCredentials(final Activity context, final Runnable onSuccess) {
+
+ public static void confirmCredentials(Activity context, Runnable onSuccess) {
+ confirmCredentials(context, onSuccess, null);
+ }
+ public static void confirmCredentials(final Activity context, final Runnable onSuccess, final Runnable onCancel) {
final long currentTime = System.currentTimeMillis();
// If already ran this check within last x time, just go ahead and auth
if((currentTime - lastVerifiedTime) < MIN_VERIFY_DURATION) {
@@ -175,12 +176,7 @@ public final class UserUtil {
.setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
- String password = passwordView.getText().toString();
-
- SharedPreferences prefs = Util.getPreferences(context);
- String correctPassword = prefs.getString(Constants.PREFERENCES_KEY_PASSWORD + Util.getActiveServer(context), null);
-
- if(password != null && password.equals(correctPassword)) {
+ if(isPasswordCorrect(context, passwordView)) {
lastVerifiedTime = currentTime;
onSuccess.run();
} else {
@@ -188,7 +184,14 @@ public final class UserUtil {
}
}
})
- .setNegativeButton(R.string.common_cancel, null)
+ .setNegativeButton(R.string.common_cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if(onCancel != null) {
+ onCancel.run();
+ }
+ }
+ })
.setCancelable(true);
AlertDialog dialog = builder.create();
@@ -199,8 +202,14 @@ public final class UserUtil {
public static void changePassword(final Activity context, final User user) {
View layout = context.getLayoutInflater().inflate(R.layout.change_password, null);
+ View currentPasswordLayout = layout.findViewById(R.id.current_password_layout);
+ final TextView currentPasswordView = (TextView) layout.findViewById(R.id.current_password);
final TextView passwordView = (TextView) layout.findViewById(R.id.new_password);
+ if(isCurrentAdmin()) {
+ currentPasswordLayout.setVisibility(View.GONE);
+ }
+
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle(R.string.admin_change_password)
.setView(layout)
@@ -215,8 +224,12 @@ public final class UserUtil {
@Override
public void onClick(View v) {
final String password = passwordView.getText().toString();
+ if(!isCurrentAdmin() && !isPasswordCorrect(context, currentPasswordView)) {
+ Util.toast(context, R.string.admin_confirm_password_bad);
+ return;
+ }
// Don't allow blank passwords
- if ("".equals(password)) {
+ else if ("".equals(password)) {
Util.toast(context, R.string.admin_change_password_invalid);
return;
}
@@ -252,6 +265,16 @@ public final class UserUtil {
});
}
+ private static boolean isPasswordCorrect(Context context, TextView passwordView) {
+ return isPasswordCorrect(context, passwordView.getText().toString());
+ }
+ private static boolean isPasswordCorrect(Context context, String password) {
+ SharedPreferences prefs = Util.getPreferences(context);
+ String correctPassword = prefs.getString(Constants.PREFERENCES_KEY_PASSWORD + Util.getActiveServer(context), null);
+
+ return password != null && password.equals(correctPassword);
+ }
+
public static void updateSettings(final Context context, final User user) {
new SilentBackgroundTask<Void>(context) {
@Override
@@ -373,7 +396,7 @@ public final class UserUtil {
});
}
- public static void addNewUser(final Activity context, final SubsonicFragment fragment) {
+ public static void addNewUser(final Activity context, final SubsonicFragment fragment, User sampleUser) {
final User user = new User();
for(String role: User.ROLES) {
if(role.equals(User.SETTINGS) || role.equals(User.STREAM)) {
@@ -383,6 +406,13 @@ public final class UserUtil {
}
}
+ if(sampleUser != null && sampleUser.getMusicFolderSettings() != null) {
+ for(User.Setting setting: sampleUser.getMusicFolderSettings()) {
+ User.MusicFolderSetting musicFolderSetting = (User.MusicFolderSetting) setting;
+ user.addMusicFolder(musicFolderSetting, true);
+ }
+ }
+
View layout = context.getLayoutInflater().inflate(R.layout.create_user, null);
final TextView usernameView = (TextView) layout.findViewById(R.id.username);
final TextView emailView = (TextView) layout.findViewById(R.id.email);
@@ -391,7 +421,7 @@ public final class UserUtil {
LinearLayoutManager layoutManager = new LinearLayoutManager(context);
layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
recyclerView.setLayoutManager(layoutManager);
- recyclerView.setAdapter(new SettingsAdapter(context, user, null, true, new SectionAdapter.OnItemClickedListener<User.Setting>() {
+ recyclerView.setAdapter(SettingsAdapter.getSettingsAdapter(context, user, null, true, new SectionAdapter.OnItemClickedListener<User.Setting>() {
@Override
public void onItemClicked(UpdateView<User.Setting> updateView, User.Setting item) {
if(updateView.isCheckable()) {
diff --git a/app/src/main/java/github/daneren2005/dsub/util/Util.java b/app/src/main/java/github/daneren2005/dsub/util/Util.java
index bf4af20d..7f8a168d 100644
--- a/app/src/main/java/github/daneren2005/dsub/util/Util.java
+++ b/app/src/main/java/github/daneren2005/dsub/util/Util.java
@@ -19,15 +19,15 @@ package github.daneren2005.dsub.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;
@@ -46,15 +46,13 @@ 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.daneren2005.dsub.R;
-import github.daneren2005.dsub.activity.SettingsActivity;
-import github.daneren2005.dsub.activity.SubsonicFragmentActivity;
import github.daneren2005.dsub.adapter.DetailsAdapter;
import github.daneren2005.dsub.domain.MusicDirectory;
import github.daneren2005.dsub.domain.PlayerState;
@@ -63,8 +61,6 @@ import github.daneren2005.dsub.domain.ServerInfo;
import github.daneren2005.dsub.receiver.MediaButtonIntentReceiver;
import github.daneren2005.dsub.service.DownloadService;
-import org.apache.http.HttpEntity;
-
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
@@ -76,7 +72,6 @@ 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;
@@ -107,6 +102,7 @@ public final class Util {
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.daneren2005.dsub.EVENT_META_CHANGED";
@@ -269,65 +265,6 @@ public final class Util {
editor.putBoolean(Constants.PREFERENCES_KEY_ALBUMS_PER_FOLDER + instance, perFolder);
editor.commit();
}
-
- public static String getTheme(Context context) {
- SharedPreferences prefs = getPreferences(context);
- return prefs.getString(Constants.PREFERENCES_KEY_THEME, null);
- }
- public static 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 ("dark".equals(theme)) {
- return R.style.Theme_DSub_Dark_No_Actionbar;
- } else if ("black".equals(theme)) {
- return R.style.Theme_DSub_Black_No_Actionbar;
- } else if ("holo".equals(theme)) {
- return R.style.Theme_DSub_Holo_No_Actionbar;
- } else {
- return R.style.Theme_DSub_Light_No_Actionbar;
- }
- } else {
- if ("dark".equals(theme)) {
- return R.style.Theme_DSub_Dark_No_Color;
- } else if ("black".equals(theme)) {
- return R.style.Theme_DSub_Black_No_Color;
- } else if ("holo".equals(theme)) {
- return R.style.Theme_DSub_Holo_No_Color;
- } else {
- return R.style.Theme_DSub_Light_No_Color;
- }
- }
- } else {
- if ("dark".equals(theme)) {
- return R.style.Theme_DSub_Dark;
- } else if ("black".equals(theme)) {
- return R.style.Theme_DSub_Black;
- } else if ("holo".equals(theme)) {
- return R.style.Theme_DSub_Holo;
- } else {
- return R.style.Theme_DSub_Light;
- }
- }
- }
- public static void setTheme(Context context, String theme) {
- SharedPreferences.Editor editor = getPreferences(context).edit();
- editor.putString(Constants.PREFERENCES_KEY_THEME, theme);
- editor.commit();
- }
-
- public static void applyTheme(Context context, String theme) {
- context.setTheme(getThemeRes(context, theme));
-
- SharedPreferences prefs = Util.getPreferences(context);
- if(prefs.getBoolean(Constants.PREFERENCES_KEY_OVERRIDE_SYSTEM_LANGUAGE, false)) {
- Configuration config = new Configuration();
- config.locale = Locale.ENGLISH;
- context.getResources().updateConfiguration(config, context.getResources().getDisplayMetrics());
- }
- }
public static boolean getDisplayTrack(Context context) {
SharedPreferences prefs = getPreferences(context);
@@ -376,6 +313,12 @@ public final class Util {
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);
@@ -401,13 +344,15 @@ public final class Util {
String serverUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_URL + instance, null);
if(allowAltAddress && Util.isWifiConnected(context)) {
String SSID = prefs.getString(Constants.PREFERENCES_KEY_SERVER_LOCAL_NETWORK_SSID + instance, "");
- String currentSSID = Util.getSSID(context);
-
- String[] ssidParts = SSID.split(",");
- if("".equals(SSID) || SSID.equals(currentSSID) || Arrays.asList(ssidParts).contains(currentSSID)) {
- String internalUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_INTERNAL_URL + instance, null);
- if(internalUrl != null && !"".equals(internalUrl) && !"http://".equals(internalUrl)) {
- serverUrl = internalUrl;
+ 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;
+ }
}
}
}
@@ -467,6 +412,18 @@ public final class Util {
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 String replaceInternalUrl(Context context, String url) {
// Only change to internal when using https
if(url.indexOf("https") != -1) {
@@ -637,13 +594,6 @@ public final class Util {
}
}
- public static String getContentType(HttpEntity entity) {
- if (entity == null || entity.getContentType() == null) {
- return null;
- }
- return entity.getContentType().getValue();
- }
-
public static int getRemainingTrialDays(Context context) {
SharedPreferences prefs = getPreferences(context);
long installTime = prefs.getLong(Constants.PREFERENCES_KEY_INSTALL_TIME, 0L);
@@ -683,11 +633,19 @@ public final class Util {
editor.commit();
}
+ public static boolean shouldCacheDuringCasting(Context context) {
+ return Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_CAST_CACHE, false);
+ }
+
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 <code>InputStream</code> as a <code>byte[]</code>.
* <p/>
@@ -901,26 +859,42 @@ public final class Util {
}
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');
boolean isDateNormalized = ServerInfo.checkServerVersion(context, "1.11");
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH);
if (isDateNormalized) {
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
}
- return formatDate(dateFormat.parse(dateString));
+ 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(date.getYear() != CURRENT_YEAR) {
- return DATE_FORMAT_LONG.format(date);
+ if(includeTime) {
+ if (date.getYear() != CURRENT_YEAR) {
+ return DATE_FORMAT_LONG.format(date);
+ } else {
+ return DATE_FORMAT_SHORT.format(date);
+ }
} else {
- return DATE_FORMAT_SHORT.format(date);
+ return DATE_FORMAT_NO_TIME.format(date);
}
}
}
@@ -1135,7 +1109,7 @@ public final class Util {
}
public static boolean isAllowedToDownload(Context context) {
- return !isWifiRequiredForDownload(context) || isWifiConnected(context);
+ return isNetworkConnected(context, true) && !isOffline(context);
}
public static boolean isWifiRequiredForDownload(Context context) {
SharedPreferences prefs = getPreferences(context);
@@ -1161,22 +1135,22 @@ public final class Util {
showDialog(context, android.R.drawable.ic_dialog_info, title, message, linkify);
}
- private static void showDialog(Context context, int icon, int titleId, int messageId) {
+ public static void showDialog(Context context, int icon, int titleId, int messageId) {
showDialog(context, icon, titleId, messageId, true);
}
- private static void showDialog(Context context, int icon, int titleId, String message) {
+ public static void showDialog(Context context, int icon, int titleId, String message) {
showDialog(context, icon, titleId, message, true);
}
- private static void showDialog(Context context, int icon, String title, String message) {
+ public static void showDialog(Context context, int icon, String title, String message) {
showDialog(context, icon, title, message, true);
}
- private static void showDialog(Context context, int icon, int titleId, int messageId, boolean linkify) {
+ 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);
}
- private static void showDialog(Context context, int icon, int titleId, String message, boolean linkify) {
+ public static void showDialog(Context context, int icon, int titleId, String message, boolean linkify) {
showDialog(context, icon, context.getResources().getString(titleId), message, linkify);
}
- private static void showDialog(Context context, int icon, String title, String message, boolean linkify) {
+ 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);
@@ -1222,12 +1196,36 @@ public final class Util {
}
showDetailsDialog(context, context.getResources().getString(title), headerStrings, details);
}
- public static void showDetailsDialog(Context context, String title, List<String> headers, List<String> details) {
+ public static void showDetailsDialog(Context context, String title, List<String> headers, final List<String> 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)
@@ -1367,72 +1365,79 @@ public final class Util {
* <p>Broadcasts the given song info as the new song being played.</p>
*/
public static void broadcastNewTrackInfo(Context context, MusicDirectory.Entry song) {
- DownloadService downloadService = (DownloadService)context;
- Intent intent = new Intent(EVENT_META_CHANGED);
- Intent avrcpIntent = new Intent(AVRCP_METADATA_CHANGED);
-
- if (song != null) {
- intent.putExtra("title", song.getTitle());
- intent.putExtra("artist", song.getArtist());
- intent.putExtra("album", song.getAlbum());
-
- File albumArtFile = FileUtil.getAlbumArtFile(context, song);
- intent.putExtra("coverart", albumArtFile.getAbsolutePath());
- avrcpIntent.putExtra("playing", true);
- } else {
- intent.putExtra("title", "");
- intent.putExtra("artist", "");
- intent.putExtra("album", "");
- intent.putExtra("coverart", "");
- avrcpIntent.putExtra("playing", false);
- }
- addTrackInfo(context, song, avrcpIntent);
+ 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());
- context.sendBroadcast(intent);
- context.sendBroadcast(avrcpIntent);
+ 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);
+ }
}
/**
* <p>Broadcasts the given player state as the one being set.</p>
*/
public static void broadcastPlaybackStatusChange(Context context, MusicDirectory.Entry song, PlayerState state) {
- Intent intent = new Intent(EVENT_PLAYSTATE_CHANGED);
- Intent avrcpIntent = new Intent(AVRCP_PLAYSTATE_CHANGED);
-
- switch (state) {
- case STARTED:
- intent.putExtra("state", "play");
- avrcpIntent.putExtra("playing", true);
- break;
- case STOPPED:
- intent.putExtra("state", "stop");
- avrcpIntent.putExtra("playing", false);
- break;
- case PAUSED:
- intent.putExtra("state", "pause");
- avrcpIntent.putExtra("playing", false);
- break;
- case PREPARED:
- // Only send quick pause event for samsung devices, causes issues for others
- if(Build.MANUFACTURER.toLowerCase().indexOf("samsung") != -1) {
+ 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);
- } 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);
+ 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);
+ if (state != PlayerState.PREPARED) {
+ context.sendBroadcast(intent);
+ }
+ context.sendBroadcast(avrcpIntent);
+ } catch(Exception e) {
+ Log.e(TAG, "Failed to broadcastPlaybackStatusChange", e);
}
- context.sendBroadcast(avrcpIntent);
}
private static void addTrackInfo(Context context, MusicDirectory.Entry song, Intent intent) {
@@ -1448,6 +1453,7 @@ public final class Util {
intent.putExtra("duration", (long) downloadService.getPlayerDuration());
intent.putExtra("position", (long) downloadService.getPlayerPosition());
intent.putExtra("coverart", albumArtFile.getAbsolutePath());
+ intent.putExtra("package","github.daneren2005.dsub");
} else {
intent.putExtra("track", "");
intent.putExtra("artist", "");
@@ -1457,6 +1463,7 @@ public final class Util {
intent.putExtra("duration", (long) 0);
intent.putExtra("position", (long) 0);
intent.putExtra("coverart", "");
+ intent.putExtra("package","github.daneren2005.dsub");
}
}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/compat/CastCompat.java b/app/src/main/java/github/daneren2005/dsub/util/compat/CastCompat.java
index ab64bca9..415106db 100644
--- a/app/src/main/java/github/daneren2005/dsub/util/compat/CastCompat.java
+++ b/app/src/main/java/github/daneren2005/dsub/util/compat/CastCompat.java
@@ -23,13 +23,9 @@ import com.google.android.gms.cast.CastMediaControlIntent;
import github.daneren2005.dsub.service.ChromeCastController;
import github.daneren2005.dsub.service.DownloadService;
import github.daneren2005.dsub.service.RemoteController;
+import github.daneren2005.dsub.util.EnvironmentVariables;
-/**
- * Created by owner on 2/9/14.
- */
public final class CastCompat {
- public static final String APPLICATION_ID = "5F85EBEB";
-
static {
try {
Class.forName("com.google.android.gms.cast.CastDevice");
@@ -52,6 +48,6 @@ public final class CastCompat {
}
public static String getCastControlCategory() {
- return CastMediaControlIntent.categoryForCast(APPLICATION_ID);
+ return CastMediaControlIntent.categoryForCast(EnvironmentVariables.CAST_APPLICATION_ID);
}
}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientBase.java b/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientBase.java
index 1f7035dc..4f9a27f0 100644
--- a/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientBase.java
+++ b/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientBase.java
@@ -29,7 +29,7 @@ public abstract class RemoteControlClientBase {
public abstract void register(final Context context, final ComponentName mediaButtonReceiverComponent);
public abstract void unregister(final Context context);
- public abstract void setPlaybackState(int state);
+ public abstract void setPlaybackState(int state, int index, int queueSize);
public abstract void updateMetadata(Context context, MusicDirectory.Entry currentSong);
public abstract void metadataChanged(MusicDirectory.Entry currentSong);
public abstract void updateAlbumArt(MusicDirectory.Entry currentSong, Bitmap bitmap);
diff --git a/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientICS.java b/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientICS.java
index 2a06e798..74076afb 100644
--- a/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientICS.java
+++ b/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientICS.java
@@ -54,7 +54,7 @@ public class RemoteControlClientICS extends RemoteControlClientBase {
audioManager.unregisterRemoteControlClient(mRemoteControl);
}
- public void setPlaybackState(final int state) {
+ public void setPlaybackState(final int state, int index, int queueSize) {
if(mRemoteControl == null) {
return;
}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientJB.java b/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientJB.java
index e61e9a47..d10c8594 100644
--- a/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientJB.java
+++ b/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientJB.java
@@ -1,17 +1,10 @@
package github.daneren2005.dsub.util.compat;
-import github.daneren2005.dsub.domain.MusicDirectory;
-import github.daneren2005.dsub.util.ImageLoader;
import android.annotation.TargetApi;
-import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
-import android.content.Intent;
-import android.media.AudioManager;
-import android.media.MediaMetadataRetriever;
import android.media.RemoteControlClient;
-import github.daneren2005.dsub.activity.SubsonicActivity;
-import github.daneren2005.dsub.service.DownloadService;
+
import github.daneren2005.dsub.util.SilentBackgroundTask;
@TargetApi(18)
@@ -36,13 +29,13 @@ public class RemoteControlClientJB extends RemoteControlClientICS {
return null;
}
}.execute();
- setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING);
+ setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING, 0, 0);
}
});
}
@Override
- public void setPlaybackState(final int state) {
+ public void setPlaybackState(final int state, int index, int queueSize) {
if(mRemoteControl == null) {
return;
}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientLP.java b/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientLP.java
index 456446f3..00bca833 100644
--- a/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientLP.java
+++ b/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientLP.java
@@ -34,7 +34,10 @@ import android.media.session.PlaybackState;
import android.os.Build;
import android.os.Bundle;
import android.provider.MediaStore;
+import android.support.annotation.NonNull;
import android.support.v7.media.MediaRouter;
+import android.util.Log;
+import android.view.KeyEvent;
import java.util.ArrayList;
import java.util.List;
@@ -42,7 +45,9 @@ import java.util.List;
import github.daneren2005.dsub.R;
import github.daneren2005.dsub.activity.SubsonicActivity;
import github.daneren2005.dsub.activity.SubsonicFragmentActivity;
+import github.daneren2005.dsub.domain.Bookmark;
import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.MusicDirectory.Entry;
import github.daneren2005.dsub.domain.Playlist;
import github.daneren2005.dsub.domain.SearchCritera;
import github.daneren2005.dsub.domain.SearchResult;
@@ -87,7 +92,7 @@ public class RemoteControlClientLP extends RemoteControlClientBase {
Intent activityIntent = new Intent(context, SubsonicFragmentActivity.class);
activityIntent.putExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD, true);
- activityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ activityIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent activityPendingIntent = PendingIntent.getActivity(context, 0, activityIntent, 0);
mediaSession.setSessionActivity(activityPendingIntent);
@@ -116,8 +121,12 @@ public class RemoteControlClientLP extends RemoteControlClientBase {
mediaSession.release();
}
+ private void setPlaybackState(int state) {
+ setPlaybackState(state, downloadService.getCurrentPlayingIndex(), downloadService.size());
+ }
+
@Override
- public void setPlaybackState(int state) {
+ public void setPlaybackState(int state, int index, int queueSize) {
PlaybackState.Builder builder = new PlaybackState.Builder();
int newState = PlaybackState.STATE_NONE;
@@ -141,11 +150,17 @@ public class RemoteControlClientLP extends RemoteControlClientBase {
position = downloadService.getPlayerPosition();
}
builder.setState(newState, position, 1.0f);
- builder.setActions(getPlaybackActions());
-
DownloadFile downloadFile = downloadService.getCurrentPlaying();
+ Entry entry = null;
+ boolean isSong = true;
if(downloadFile != null) {
- MusicDirectory.Entry entry = downloadFile.getSong();
+ entry = downloadFile.getSong();
+ isSong = entry.isSong();
+ }
+
+ builder.setActions(getPlaybackActions(isSong, index, queueSize));
+
+ if(entry != null) {
addCustomActions(entry, builder);
builder.setActiveQueueItemId(entry.getId().hashCode());
}
@@ -156,7 +171,7 @@ public class RemoteControlClientLP extends RemoteControlClientBase {
}
@Override
- public void updateMetadata(Context context, MusicDirectory.Entry currentSong) {
+ public void updateMetadata(Context context, Entry currentSong) {
setMetadata(currentSong, null);
if(currentSong != null && imageLoader != null) {
@@ -165,11 +180,11 @@ public class RemoteControlClientLP extends RemoteControlClientBase {
}
@Override
- public void metadataChanged(MusicDirectory.Entry currentSong) {
+ public void metadataChanged(Entry currentSong) {
setPlaybackState(previousState);
}
- public void setMetadata(MusicDirectory.Entry currentSong, Bitmap bitmap) {
+ public void setMetadata(Entry currentSong, Bitmap bitmap) {
MediaMetadata.Builder builder = new MediaMetadata.Builder();
builder.putString(MediaMetadata.METADATA_KEY_ARTIST, (currentSong == null) ? null : currentSong.getArtist())
.putString(MediaMetadata.METADATA_KEY_ALBUM, (currentSong == null) ? null : currentSong.getAlbum())
@@ -189,7 +204,7 @@ public class RemoteControlClientLP extends RemoteControlClientBase {
}
@Override
- public void updateAlbumArt(MusicDirectory.Entry currentSong, Bitmap bitmap) {
+ public void updateAlbumArt(Entry currentSong, Bitmap bitmap) {
setMetadata(currentSong, bitmap);
}
@@ -208,7 +223,7 @@ public class RemoteControlClientLP extends RemoteControlClientBase {
List<MediaSession.QueueItem> queue = new ArrayList<>();
for(DownloadFile file: playlist) {
- MusicDirectory.Entry entry = file.getSong();
+ Entry entry = file.getSong();
MediaDescription description = new MediaDescription.Builder()
.setMediaId(entry.getId())
@@ -227,24 +242,27 @@ public class RemoteControlClientLP extends RemoteControlClientBase {
return mediaSession;
}
- protected long getPlaybackActions() {
+ protected long getPlaybackActions(boolean isSong, int currentIndex, int size) {
long actions = PlaybackState.ACTION_PLAY |
PlaybackState.ACTION_PAUSE |
PlaybackState.ACTION_SEEK_TO |
PlaybackState.ACTION_SKIP_TO_QUEUE_ITEM;
- int currentIndex = downloadService.getCurrentPlayingIndex();
- int size = downloadService.size();
- if(currentIndex > 0) {
+ if(isSong) {
+ if (currentIndex > 0) {
+ actions |= PlaybackState.ACTION_SKIP_TO_PREVIOUS;
+ }
+ if (currentIndex < size - 1) {
+ actions |= PlaybackState.ACTION_SKIP_TO_NEXT;
+ }
+ } else {
actions |= PlaybackState.ACTION_SKIP_TO_PREVIOUS;
- }
- if(currentIndex < size - 1) {
actions |= PlaybackState.ACTION_SKIP_TO_NEXT;
}
return actions;
}
- protected void addCustomActions(MusicDirectory.Entry currentSong, PlaybackState.Builder builder) {
+ protected void addCustomActions(Entry currentSong, PlaybackState.Builder builder) {
Bundle showOnWearExtras = new Bundle();
showOnWearExtras.putBoolean(SHOW_ON_WEAR, true);
@@ -296,7 +314,7 @@ public class RemoteControlClientLP extends RemoteControlClientBase {
SearchResult results = musicService.search(searchCritera, downloadService, null);
if(results.hasArtists()) {
- playFromParent(new MusicDirectory.Entry(results.getArtists().get(0)));
+ playFromParent(new Entry(results.getArtists().get(0)));
} else if(results.hasAlbums()) {
playFromParent(results.getAlbums().get(0));
} else if(results.hasSongs()) {
@@ -307,13 +325,13 @@ public class RemoteControlClientLP extends RemoteControlClientBase {
return null;
}
-
- private void playFromParent(MusicDirectory.Entry parent) throws Exception {
- List<MusicDirectory.Entry> songs = new ArrayList<>();
+
+ private void playFromParent(Entry parent) throws Exception {
+ List<Entry> songs = new ArrayList<>();
getSongsRecursively(parent, songs);
playSongs(songs);
}
- private void getSongsRecursively(MusicDirectory.Entry parent, List<MusicDirectory.Entry> songs) throws Exception {
+ private void getSongsRecursively(Entry parent, List<Entry> songs) throws Exception {
MusicDirectory musicDirectory;
if(Util.isTagBrowsing(downloadService) && !Util.isOffline(downloadService)) {
musicDirectory = musicService.getAlbum(parent.getId(), parent.getTitle(), false, downloadService, this);
@@ -321,7 +339,7 @@ public class RemoteControlClientLP extends RemoteControlClientBase {
musicDirectory = musicService.getMusicDirectory(parent.getId(), parent.getTitle(), false, downloadService, this);
}
- for (MusicDirectory.Entry dir : musicDirectory.getChildren(true, false)) {
+ for (Entry dir : musicDirectory.getChildren(true, false)) {
if (dir.getRating() == 1) {
continue;
}
@@ -329,7 +347,7 @@ public class RemoteControlClientLP extends RemoteControlClientBase {
getSongsRecursively(dir, songs);
}
- for (MusicDirectory.Entry song : musicDirectory.getChildren(false, true)) {
+ for (Entry song : musicDirectory.getChildren(false, true)) {
if (!song.isVideo() && song.getRating() != 1) {
songs.add(song);
}
@@ -349,24 +367,75 @@ public class RemoteControlClientLP extends RemoteControlClientBase {
}
}.execute();
}
+ private void playMusicDirectory(Entry dir, boolean shuffle, boolean append, boolean playFromBookmark) {
+ playMusicDirectory(dir.getId(), shuffle, append, playFromBookmark);
+ }
+ private void playMusicDirectory(final String dirId, final boolean shuffle, final boolean append, final boolean playFromBookmark) {
+ new SilentServiceTask<Void>(downloadService) {
+ @Override
+ protected Void doInBackground(MusicService musicService) throws Throwable {
+ MusicDirectory musicDirectory;
+ if(Util.isTagBrowsing(downloadService) && !Util.isOffline(downloadService)) {
+ musicDirectory = musicService.getAlbum(dirId, "dir", false, downloadService, null);
+ } else {
+ musicDirectory = musicService.getMusicDirectory(dirId, "dir", false, downloadService, null);
+ }
+
+ List<Entry> playEntries = new ArrayList<>();
+ List<Entry> allEntries = musicDirectory.getChildren(false, true);
+ for(Entry song: allEntries) {
+ if (!song.isVideo() && song.getRating() != 1) {
+ playEntries.add(song);
+ }
+ }
+ playSongs(playEntries, shuffle, append, playFromBookmark);
+
+ return null;
+ }
+ }.execute();
+ }
+
+ private void playSong(Entry entry) {
- private void playSong(MusicDirectory.Entry entry) {
- List<MusicDirectory.Entry> entries = new ArrayList<>();
+ }
+ private void playSong(Entry entry, boolean resumeFromBookmark) {
+ List<Entry> entries = new ArrayList<>();
entries.add(entry);
- playSongs(entries);
+ playSongs(entries, false, false, resumeFromBookmark);
}
- private void playSongs(List<MusicDirectory.Entry> entries) {
+ private void playSongs(List<Entry> entries) {
playSongs(entries, false, false);
}
- private void playSongs(List<MusicDirectory.Entry> entries, boolean shuffle, boolean append) {
+ private void playSongs(List<Entry> entries, boolean shuffle, boolean append) {
+ playSongs(entries, shuffle, append, false);
+ }
+ private void playSongs(List<Entry> entries, boolean shuffle, boolean append, boolean resumeFromBookmark) {
if(!append) {
downloadService.clear();
}
- downloadService.download(entries, false, true, false, shuffle);
+
+ int startIndex = 0;
+ int startPosition = 0;
+ if(resumeFromBookmark) {
+ int bookmarkIndex = 0;
+ for(Entry entry: entries) {
+ if(entry.getBookmark() != null) {
+ Bookmark bookmark = entry.getBookmark();
+ startIndex = bookmarkIndex;
+ startPosition = bookmark.getPosition();
+ break;
+ }
+ bookmarkIndex++;
+ }
+ }
+
+ downloadService.download(entries, false, !append, false, shuffle, startIndex, startPosition);
}
private void noResults() {
-
+ // Keep getting emails from Google that not playing something with no results is bad
+ downloadService.clear();
+ downloadService.setShufflePlayEnabled(true);
}
private class EventCallback extends MediaSession.Callback {
@@ -415,6 +484,7 @@ public class RemoteControlClientLP extends RemoteControlClientBase {
public void onPlayFromSearch (String query, Bundle extras) {
// User just asked to playing something
if("".equals(query)) {
+ downloadService.clear();
downloadService.setShufflePlayEnabled(true);
} else {
String mediaFocus = extras.getString(MediaStore.EXTRA_MEDIA_FOCUS);
@@ -435,6 +505,7 @@ public class RemoteControlClientLP extends RemoteControlClientBase {
editor.putString(Constants.PREFERENCES_KEY_SHUFFLE_GENRE, genre);
editor.commit();
+ downloadService.clear();
downloadService.setShufflePlayEnabled(true);
}
else {
@@ -467,6 +538,7 @@ public class RemoteControlClientLP extends RemoteControlClientBase {
}
}
+ @Override
public void onPlayFromMediaId (String mediaId, Bundle extras) {
if(extras == null) {
return;
@@ -474,11 +546,33 @@ public class RemoteControlClientLP extends RemoteControlClientBase {
boolean shuffle = extras.getBoolean(Constants.INTENT_EXTRA_NAME_SHUFFLE, false);
boolean playLast = extras.getBoolean(Constants.INTENT_EXTRA_PLAY_LAST, false);
+ Entry entry = (Entry) extras.getSerializable(Constants.INTENT_EXTRA_ENTRY);
+
String playlistId = extras.getString(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, null);
if(playlistId != null) {
Playlist playlist = new Playlist(playlistId, null);
playPlaylist(playlist, shuffle, playLast);
}
+ String musicDirectoryId = extras.getString(Constants.INTENT_EXTRA_NAME_ID);
+ if(musicDirectoryId != null) {
+ Entry dir = new Entry(musicDirectoryId);
+ playMusicDirectory(dir, shuffle, playLast, true);
+ }
+
+ String podcastId = extras.getString(Constants.INTENT_EXTRA_NAME_PODCAST_ID, null);
+ if(podcastId != null) {
+ playSong(entry, true);
+ }
+
+ // Currently only happens when playing bookmarks so we should be looking up parent
+ String childId = extras.getString(Constants.INTENT_EXTRA_NAME_CHILD_ID, null);
+ if(childId != null) {
+ if(Util.isTagBrowsing(downloadService) && !Util.isOffline(downloadService)) {
+ playMusicDirectory(entry.getAlbumId(), shuffle, playLast, true);
+ } else {
+ playMusicDirectory(entry.getParent(), shuffle, playLast, true);
+ }
+ }
}
@Override
@@ -491,5 +585,18 @@ public class RemoteControlClientLP extends RemoteControlClientBase {
downloadService.toggleStarred();
}
}
+
+ @Override
+ public boolean onMediaButtonEvent(@NonNull Intent mediaButtonIntent) {
+ if (getMediaSession() != null && Intent.ACTION_MEDIA_BUTTON.equals(mediaButtonIntent.getAction())) {
+ KeyEvent keyEvent = mediaButtonIntent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
+ if (keyEvent != null) {
+ downloadService.handleKeyEvent(keyEvent);
+ return true;
+ }
+ }
+
+ return super.onMediaButtonEvent(mediaButtonIntent);
+ }
}
}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/tags/ID3v2File.java b/app/src/main/java/github/daneren2005/dsub/util/tags/ID3v2File.java
index ea61f36c..69668475 100644
--- a/app/src/main/java/github/daneren2005/dsub/util/tags/ID3v2File.java
+++ b/app/src/main/java/github/daneren2005/dsub/util/tags/ID3v2File.java
@@ -134,7 +134,11 @@ public class ID3v2File extends Common {
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);
diff --git a/app/src/main/java/github/daneren2005/dsub/view/AlbumView.java b/app/src/main/java/github/daneren2005/dsub/view/AlbumView.java
index 048d5a75..3084e962 100644
--- a/app/src/main/java/github/daneren2005/dsub/view/AlbumView.java
+++ b/app/src/main/java/github/daneren2005/dsub/view/AlbumView.java
@@ -30,6 +30,7 @@ import github.daneren2005.dsub.R;
import github.daneren2005.dsub.domain.MusicDirectory;
import github.daneren2005.dsub.util.FileUtil;
import github.daneren2005.dsub.util.ImageLoader;
+import github.daneren2005.dsub.util.Util;
import java.io.File;
@@ -40,6 +41,7 @@ public class AlbumView extends UpdateView2<MusicDirectory.Entry, ImageLoader> {
private TextView titleView;
private TextView artistView;
private boolean showArtist = true;
+ private String coverArtId;
public AlbumView(Context context, boolean cell) {
super(context);
@@ -82,12 +84,13 @@ public class AlbumView extends UpdateView2<MusicDirectory.Entry, ImageLoader> {
artist += album.getYear();
}
artistView.setText(album.getArtist() == null ? "" : artist);
- imageTask = imageLoader.loadImage(coverArtView, album, false, true);
+ onUpdateImageView();
file = null;
}
public void onUpdateImageView() {
imageTask = item2.loadImage(coverArtView, item, false, true);
+ coverArtId = item.getCoverArt();
}
@Override
@@ -101,6 +104,15 @@ public class AlbumView extends UpdateView2<MusicDirectory.Entry, ImageLoader> {
isRated = item.getRating();
}
+ @Override
+ public void update() {
+ super.update();
+
+ if(!Util.equals(item.getCoverArt(), coverArtId)) {
+ onUpdateImageView();
+ }
+ }
+
public MusicDirectory.Entry getEntry() {
return item;
}
diff --git a/app/src/main/java/github/daneren2005/dsub/view/CacheLocationPreference.java b/app/src/main/java/github/daneren2005/dsub/view/CacheLocationPreference.java
new file mode 100644
index 00000000..35ce71bc
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/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 <http://www.gnu.org/licenses/>.
+ Copyright 2015 (C) Scott Jackson
+*/
+package github.daneren2005.dsub.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.daneren2005.dsub.R;
+import github.daneren2005.dsub.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/daneren2005/dsub/view/CardView.java b/app/src/main/java/github/daneren2005/dsub/view/CardView.java
new file mode 100644
index 00000000..d6bca330
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/view/CardView.java
@@ -0,0 +1,67 @@
+package github.daneren2005.dsub.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.daneren2005.dsub.R;
+import github.daneren2005.dsub.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/daneren2005/dsub/view/FastScroller.java b/app/src/main/java/github/daneren2005/dsub/view/FastScroller.java
index 7cb29835..d3eacefc 100644
--- a/app/src/main/java/github/daneren2005/dsub/view/FastScroller.java
+++ b/app/src/main/java/github/daneren2005/dsub/view/FastScroller.java
@@ -95,7 +95,7 @@ public class FastScroller extends LinearLayout {
switch(action)
{
case MotionEvent.ACTION_DOWN:
- if(event.getX() < (handle.getX() - 20)) {
+ if(event.getX() < (handle.getX() - 30)) {
return false;
}
diff --git a/app/src/main/java/github/daneren2005/dsub/view/GridSpacingDecoration.java b/app/src/main/java/github/daneren2005/dsub/view/GridSpacingDecoration.java
index b59e7157..45b34b9f 100644
--- a/app/src/main/java/github/daneren2005/dsub/view/GridSpacingDecoration.java
+++ b/app/src/main/java/github/daneren2005/dsub/view/GridSpacingDecoration.java
@@ -18,10 +18,17 @@ package github.daneren2005.dsub.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
@@ -39,30 +46,52 @@ public class GridSpacingDecoration extends RecyclerView.ItemDecoration {
}
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;
- outRect.top = halfSpacing;
- outRect.bottom = halfSpacing;
- outRect.left = halfSpacing;
- outRect.right = halfSpacing;
+ 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, spanCount)) {
- outRect.top = spacing;
+ if (isTopEdge(childIndex, spanIndex, spanCount)) {
+ outRect.top = spacing - doubleMargins;
}
if (isLeftEdge(spanIndex, spanCount)) {
- outRect.left = spacing;
+ outRect.left = spacing - doubleMargins;
}
if (isRightEdge(spanIndex, spanCount)) {
- outRect.right = spacing;
+ outRect.right = spacing - doubleMargins;
}
if (isBottomEdge(childIndex, childCount, spanCount)) {
- outRect.bottom = spacing;
+ outRect.bottom = spacing - doubleMargins;
}
}
@@ -94,8 +123,8 @@ public class GridSpacingDecoration extends RecyclerView.ItemDecoration {
return spanIndex == spanCount - 1;
}
- protected boolean isTopEdge(int childIndex, int spanCount) {
- return childIndex < spanCount;
+ protected boolean isTopEdge(int childIndex, int spanIndex, int spanCount) {
+ return childIndex < spanCount && childIndex == spanIndex;
}
protected boolean isBottomEdge(int childIndex, int childCount, int spanCount) {
diff --git a/app/src/main/java/github/daneren2005/dsub/view/InternetRadioStationView.java b/app/src/main/java/github/daneren2005/dsub/view/InternetRadioStationView.java
new file mode 100644
index 00000000..36aaa8af
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/view/InternetRadioStationView.java
@@ -0,0 +1,39 @@
+/*
+ This file is part of Subsonic.
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+ Copyright 2016 (C) Scott Jackson
+*/
+package github.daneren2005.dsub.view;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.InternetRadioStation;
+
+public class InternetRadioStationView extends UpdateView<InternetRadioStation> {
+ private TextView titleView;
+
+ public InternetRadioStationView(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);
+ }
+
+ protected void setObjectImpl(InternetRadioStation station) {
+ titleView.setText(station.getTitle());
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/view/MyViewFlipper.java b/app/src/main/java/github/daneren2005/dsub/view/MyViewFlipper.java
deleted file mode 100644
index 26a3de08..00000000
--- a/app/src/main/java/github/daneren2005/dsub/view/MyViewFlipper.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- This file is part of Subsonic.
-
- Subsonic is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- Subsonic is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
-
- Copyright 2009 (C) Sindre Mehus
- */
-package github.daneren2005.dsub.view;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.widget.ViewFlipper;
-
-/**
- * Work-around for Android Issue 6191 (http://code.google.com/p/android/issues/detail?id=6191)
- *
- * @author Sindre Mehus
- * @version $Id$
- */
-public class MyViewFlipper extends ViewFlipper {
-
- public MyViewFlipper(Context context) {
- super(context);
- }
-
- public MyViewFlipper(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
-
- @Override
- protected void onDetachedFromWindow() {
- try {
- super.onDetachedFromWindow();
- }
- catch (IllegalArgumentException e) {
- // Call stopFlipping() in order to kick off updateRunning()
- stopFlipping();
- }
- }
-}
-
diff --git a/app/src/main/java/github/daneren2005/dsub/view/SettingView.java b/app/src/main/java/github/daneren2005/dsub/view/SettingView.java
index d46dc5d2..6dc116f8 100644
--- a/app/src/main/java/github/daneren2005/dsub/view/SettingView.java
+++ b/app/src/main/java/github/daneren2005/dsub/view/SettingView.java
@@ -23,6 +23,7 @@ import android.widget.TextView;
import github.daneren2005.dsub.R;
import github.daneren2005.dsub.domain.User;
+import github.daneren2005.dsub.domain.User.MusicFolderSetting;
import static github.daneren2005.dsub.domain.User.Setting;
@@ -51,12 +52,14 @@ public class SettingView extends UpdateView2<Setting, Boolean> {
protected void setObjectImpl(Setting setting, Boolean isEditable) {
// Can't edit non-role parts
String name = setting.getName();
- if(name.indexOf("Role") == -1) {
+ if(name.indexOf("Role") == -1 && !(setting instanceof MusicFolderSetting)) {
item2 = false;
}
int res = -1;
- if(User.SCROBBLING.equals(name)) {
+ if(setting instanceof MusicFolderSetting) {
+ titleView.setText(((MusicFolderSetting) setting).getLabel());
+ } else if(User.SCROBBLING.equals(name)) {
res = R.string.admin_scrobblingEnabled;
} else if(User.ADMIN.equals(name)) {
res = R.string.admin_role_admin;
@@ -78,6 +81,8 @@ public class SettingView extends UpdateView2<Setting, Boolean> {
res = R.string.admin_role_jukebox;
} else if(User.SHARE.equals(name)) {
res = R.string.admin_role_share;
+ } else if(User.VIDEO_CONVERSION.equals(name)) {
+ res = R.string.admin_role_video_conversion;
} else if(User.LASTFM.equals(name)) {
res = R.string.admin_role_lastfm;
} else {
diff --git a/app/src/main/java/github/daneren2005/dsub/view/SongView.java b/app/src/main/java/github/daneren2005/dsub/view/SongView.java
index 625303b7..320c5933 100644
--- a/app/src/main/java/github/daneren2005/dsub/view/SongView.java
+++ b/app/src/main/java/github/daneren2005/dsub/view/SongView.java
@@ -29,6 +29,8 @@ import github.daneren2005.dsub.domain.PodcastEpisode;
import github.daneren2005.dsub.service.DownloadService;
import github.daneren2005.dsub.service.DownloadFile;
import github.daneren2005.dsub.util.DrawableTint;
+import github.daneren2005.dsub.util.SongDBHandler;
+import github.daneren2005.dsub.util.ThemeUtil;
import github.daneren2005.dsub.util.Util;
import java.io.File;
@@ -41,12 +43,15 @@ import java.io.File;
public class SongView extends UpdateView2<MusicDirectory.Entry, Boolean> {
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 bookmarkButton;
+ private ImageView playedButton;
private View bottomRowView;
private DownloadService downloadService;
@@ -63,13 +68,17 @@ public class SongView extends UpdateView2<MusicDirectory.Entry, Boolean> {
private boolean partialFileExists = false;
private boolean loaded = false;
private boolean isBookmarked = false;
- private boolean bookmarked = false;
+ private boolean isBookmarkedShown = false;
private boolean showPodcast = 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);
@@ -80,6 +89,7 @@ public class SongView extends UpdateView2<MusicDirectory.Entry, Boolean> {
starButton.setFocusable(false);
bookmarkButton = (ImageButton) findViewById(R.id.song_bookmark);
bookmarkButton.setFocusable(false);
+ playedButton = (ImageButton) findViewById(R.id.song_played);
moreButton = (ImageView) findViewById(R.id.item_more);
bottomRowView = findViewById(R.id.song_bottom);
}
@@ -102,12 +112,15 @@ public class SongView extends UpdateView2<MusicDirectory.Entry, Boolean> {
if(artist.length() != 0) {
artist.append(" - ");
}
- int index = date.indexOf(" ");
- artist.append(date.substring(0, index != -1 ? index : date.length()));
+ artist.append(Util.formatDate(context, date, false));
}
}
else if(song.getArtist() != null) {
- artist.append(song.getArtist());
+ if(showAlbum) {
+ artist.append(song.getAlbum());
+ } else {
+ artist.append(song.getArtist());
+ }
}
if(isPodcast) {
@@ -138,8 +151,26 @@ public class SongView extends UpdateView2<MusicDirectory.Entry, Boolean> {
String title = song.getTitle();
Integer track = song.getTrack();
+ if(song.getCustomOrder() != null) {
+ track = song.getCustomOrder();
+ }
+ TextView newPlayingTextView;
if(track != null && Util.getDisplayTrack(context)) {
- title = String.format("%02d", track) + " " + title;
+ 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);
@@ -191,6 +222,10 @@ public class SongView extends UpdateView2<MusicDirectory.Entry, Boolean> {
item.loadMetadata(downloadFile.getCompleteFile());
loaded = true;
}
+
+ if(item instanceof PodcastEpisode || item.isAudioBook() || item.isPodcast()) {
+ isPlayed = SongDBHandler.getHandler(context).hasBeenCompleted(item);
+ }
}
@Override
@@ -242,32 +277,48 @@ public class SongView extends UpdateView2<MusicDirectory.Entry, Boolean> {
rightImage = false;
}
- boolean playing = downloadService.getCurrentPlaying() == downloadFile;
+ boolean playing = Util.equals(downloadService.getCurrentPlaying(), downloadFile);
if (playing) {
if(!this.playing) {
this.playing = playing;
- titleTextView.setCompoundDrawablesWithIntrinsicBounds(DrawableTint.getDrawableRes(context, R.attr.playing), 0, 0, 0);
+ playingTextView.setCompoundDrawablesWithIntrinsicBounds(DrawableTint.getDrawableRes(context, R.attr.playing), 0, 0, 0);
}
} else {
if(this.playing) {
this.playing = playing;
- titleTextView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
+ playingTextView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
}
}
if(isBookmarked) {
- if(!bookmarked) {
+ if(!isBookmarkedShown) {
if(bookmarkButton.getDrawable() == null) {
bookmarkButton.setImageDrawable(DrawableTint.getTintedDrawable(context, R.drawable.ic_menu_bookmark_selected));
}
bookmarkButton.setVisibility(View.VISIBLE);
- bookmarked = true;
+ isBookmarkedShown = true;
}
} else {
- if(bookmarked) {
+ if(isBookmarkedShown) {
bookmarkButton.setVisibility(View.GONE);
- bookmarked = false;
+ isBookmarkedShown = false;
+ }
+ }
+
+ 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;
}
}
@@ -288,7 +339,15 @@ public class SongView extends UpdateView2<MusicDirectory.Entry, Boolean> {
// Still highlight red if a 1-star
if(isRated == 1) {
this.setBackgroundColor(Color.RED);
- this.getBackground().setAlpha(20);
+
+ String theme = ThemeUtil.getTheme(context);
+ if("black".equals(theme)) {
+ this.getBackground().setAlpha(80);
+ } else if("dark".equals(theme) || "holo".equals(theme)) {
+ this.getBackground().setAlpha(60);
+ } else {
+ this.getBackground().setAlpha(20);
+ }
} else if(rating == 1) {
this.setBackgroundColor(0x00000000);
}
@@ -304,4 +363,8 @@ public class SongView extends UpdateView2<MusicDirectory.Entry, Boolean> {
public void setShowPodcast(boolean showPodcast) {
this.showPodcast = showPodcast;
}
+
+ public void setShowAlbum(boolean showAlbum) {
+ this.showAlbum = showAlbum;
+ }
}
diff --git a/app/src/main/java/github/daneren2005/dsub/view/UpdateView.java b/app/src/main/java/github/daneren2005/dsub/view/UpdateView.java
index 8f3b5271..0041eac5 100644
--- a/app/src/main/java/github/daneren2005/dsub/view/UpdateView.java
+++ b/app/src/main/java/github/daneren2005/dsub/view/UpdateView.java
@@ -201,6 +201,10 @@ public abstract class UpdateView<T> extends LinearLayout {
});
}
+ public static boolean hasActiveActivity() {
+ return activeActivities > 0;
+ }
+
public static void addActiveActivity() {
activeActivities++;
diff --git a/app/src/main/java/github/daneren2005/dsub/view/compat/CustomMediaRouteChooserDialogFragment.java b/app/src/main/java/github/daneren2005/dsub/view/compat/CustomMediaRouteChooserDialogFragment.java
new file mode 100644
index 00000000..a2c898b9
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/view/compat/CustomMediaRouteChooserDialogFragment.java
@@ -0,0 +1,16 @@
+package github.daneren2005.dsub.view.compat;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.support.v7.app.MediaRouteChooserDialog;
+import android.support.v7.app.MediaRouteChooserDialogFragment;
+
+import github.daneren2005.dsub.util.ThemeUtil;
+import github.daneren2005.dsub.util.Util;
+
+public class CustomMediaRouteChooserDialogFragment extends MediaRouteChooserDialogFragment {
+ @Override
+ public MediaRouteChooserDialog onCreateChooserDialog(Context context, Bundle savedInstanceState) {
+ return new MediaRouteChooserDialog(context, ThemeUtil.getThemeRes(context));
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/view/compat/CustomMediaRouteControllerDialogFragment.java b/app/src/main/java/github/daneren2005/dsub/view/compat/CustomMediaRouteControllerDialogFragment.java
new file mode 100644
index 00000000..ea890b9f
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/view/compat/CustomMediaRouteControllerDialogFragment.java
@@ -0,0 +1,16 @@
+package github.daneren2005.dsub.view.compat;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.support.v7.app.MediaRouteControllerDialog;
+import android.support.v7.app.MediaRouteControllerDialogFragment;
+
+import github.daneren2005.dsub.util.ThemeUtil;
+import github.daneren2005.dsub.util.Util;
+
+public class CustomMediaRouteControllerDialogFragment extends MediaRouteControllerDialogFragment {
+ @Override
+ public MediaRouteControllerDialog onCreateControllerDialog(Context context, Bundle savedInstanceState) {
+ return new MediaRouteControllerDialog(context, ThemeUtil.getThemeRes(context));
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/view/compat/CustomMediaRouteDialogFactory.java b/app/src/main/java/github/daneren2005/dsub/view/compat/CustomMediaRouteDialogFactory.java
new file mode 100644
index 00000000..8bc890cb
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/view/compat/CustomMediaRouteDialogFactory.java
@@ -0,0 +1,17 @@
+package github.daneren2005.dsub.view.compat;
+
+import android.support.v7.app.MediaRouteChooserDialogFragment;
+import android.support.v7.app.MediaRouteControllerDialogFragment;
+import android.support.v7.app.MediaRouteDialogFactory;
+
+public class CustomMediaRouteDialogFactory extends MediaRouteDialogFactory {
+ @Override
+ public MediaRouteChooserDialogFragment onCreateChooserDialogFragment() {
+ return new CustomMediaRouteChooserDialogFragment();
+ }
+
+ @Override
+ public MediaRouteControllerDialogFragment onCreateControllerDialogFragment() {
+ return new CustomMediaRouteControllerDialogFragment();
+ }
+}
diff --git a/app/src/main/res/drawable-hdpi/actionbar_button_normal.9.png b/app/src/main/res/drawable-hdpi/actionbar_button_normal.9.png
deleted file mode 100644
index 385f751c..00000000
--- a/app/src/main/res/drawable-hdpi/actionbar_button_normal.9.png
+++ /dev/null
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_action_playback_speed_dark.png b/app/src/main/res/drawable-hdpi/ic_action_playback_speed_dark.png
new file mode 100644
index 00000000..5f4d890a
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_action_playback_speed_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_action_playback_speed_light.png b/app/src/main/res/drawable-hdpi/ic_action_playback_speed_light.png
new file mode 100644
index 00000000..4b481822
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_action_playback_speed_light.png
Binary files differ
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 00000000..944ff8be
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_toggle_played.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/media_fastforward_dark.png b/app/src/main/res/drawable-hdpi/media_fastforward_dark.png
new file mode 100644
index 00000000..eab0cdfd
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/media_fastforward_dark.png
Binary files differ
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 00000000..905faa9c
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/media_fastforward_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/media_rewind_dark.png b/app/src/main/res/drawable-hdpi/media_rewind_dark.png
new file mode 100644
index 00000000..5d2d62a7
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/media_rewind_dark.png
Binary files differ
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 00000000..15bc91a8
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/media_rewind_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/stat_notify_download.png b/app/src/main/res/drawable-hdpi/stat_notify_download.png
deleted file mode 100644
index 48ca6924..00000000
--- a/app/src/main/res/drawable-hdpi/stat_notify_download.png
+++ /dev/null
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/toast_frame.9.png b/app/src/main/res/drawable-hdpi/toast_frame.9.png
deleted file mode 100644
index 8f5d8119..00000000
--- a/app/src/main/res/drawable-hdpi/toast_frame.9.png
+++ /dev/null
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_action_playback_speed_dark.png b/app/src/main/res/drawable-mdpi/ic_action_playback_speed_dark.png
new file mode 100644
index 00000000..b05ecefd
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_action_playback_speed_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_action_playback_speed_light.png b/app/src/main/res/drawable-mdpi/ic_action_playback_speed_light.png
new file mode 100644
index 00000000..392d2b4d
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_action_playback_speed_light.png
Binary files differ
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 00000000..02524f4c
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_toggle_played.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/media_fastforward_dark.png b/app/src/main/res/drawable-mdpi/media_fastforward_dark.png
new file mode 100644
index 00000000..f999e0b8
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/media_fastforward_dark.png
Binary files differ
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 00000000..23107742
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/media_fastforward_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/media_rewind_dark.png b/app/src/main/res/drawable-mdpi/media_rewind_dark.png
new file mode 100644
index 00000000..2ecda48a
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/media_rewind_dark.png
Binary files differ
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 00000000..f7c9b303
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/media_rewind_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/stat_notify_download.png b/app/src/main/res/drawable-mdpi/stat_notify_download.png
deleted file mode 100644
index 4164e0fa..00000000
--- a/app/src/main/res/drawable-mdpi/stat_notify_download.png
+++ /dev/null
Binary files differ
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 00000000..d0ab76a2
--- /dev/null
+++ b/app/src/main/res/drawable-v21/notification_fastforward.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<bitmap
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:src="@drawable/media_fastforward_light"/> \ 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 00000000..25a16a02
--- /dev/null
+++ b/app/src/main/res/drawable-v21/notification_rewind.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<bitmap
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:src="@drawable/media_rewind_light"/> \ No newline at end of file
diff --git a/app/src/main/res/drawable-xhdpi/ic_action_playback_speed_dark.png b/app/src/main/res/drawable-xhdpi/ic_action_playback_speed_dark.png
new file mode 100644
index 00000000..2aeadf7e
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_action_playback_speed_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_action_playback_speed_light.png b/app/src/main/res/drawable-xhdpi/ic_action_playback_speed_light.png
new file mode 100644
index 00000000..ce3f561c
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_action_playback_speed_light.png
Binary files differ
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 00000000..c681150c
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_toggle_played.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/media_fastforward_dark.png b/app/src/main/res/drawable-xhdpi/media_fastforward_dark.png
new file mode 100644
index 00000000..3c653286
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/media_fastforward_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/media_fastforward_light.png b/app/src/main/res/drawable-xhdpi/media_fastforward_light.png
new file mode 100644
index 00000000..f105b458
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/media_fastforward_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/media_rewind_dark.png b/app/src/main/res/drawable-xhdpi/media_rewind_dark.png
new file mode 100644
index 00000000..08fdff06
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/media_rewind_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/media_rewind_light.png b/app/src/main/res/drawable-xhdpi/media_rewind_light.png
new file mode 100644
index 00000000..3998f715
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/media_rewind_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/stat_notify_download.png b/app/src/main/res/drawable-xhdpi/stat_notify_download.png
deleted file mode 100644
index 96ceb383..00000000
--- a/app/src/main/res/drawable-xhdpi/stat_notify_download.png
+++ /dev/null
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_playback_speed_dark.png b/app/src/main/res/drawable-xxhdpi/ic_action_playback_speed_dark.png
new file mode 100644
index 00000000..a9acc5e6
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_action_playback_speed_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_playback_speed_light.png b/app/src/main/res/drawable-xxhdpi/ic_action_playback_speed_light.png
new file mode 100644
index 00000000..dcd6fea8
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_action_playback_speed_light.png
Binary files differ
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 00000000..33f9d819
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_toggle_played.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/media_fastforward_dark.png b/app/src/main/res/drawable-xxhdpi/media_fastforward_dark.png
new file mode 100644
index 00000000..90f045ea
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/media_fastforward_dark.png
Binary files differ
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 00000000..73f0fba4
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/media_fastforward_light.png
Binary files differ
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 00000000..ff5bda9a
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/media_rewind_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/media_rewind_light.png b/app/src/main/res/drawable-xxhdpi/media_rewind_light.png
new file mode 100644
index 00000000..c50c0825
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/media_rewind_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/stat_notify_download.png b/app/src/main/res/drawable-xxhdpi/stat_notify_download.png
deleted file mode 100644
index b2dc5651..00000000
--- a/app/src/main/res/drawable-xxhdpi/stat_notify_download.png
+++ /dev/null
Binary files differ
diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_playback_speed_dark.png b/app/src/main/res/drawable-xxxhdpi/ic_action_playback_speed_dark.png
new file mode 100644
index 00000000..c1e92342
--- /dev/null
+++ b/app/src/main/res/drawable-xxxhdpi/ic_action_playback_speed_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_playback_speed_light.png b/app/src/main/res/drawable-xxxhdpi/ic_action_playback_speed_light.png
new file mode 100644
index 00000000..3dc5f32a
--- /dev/null
+++ b/app/src/main/res/drawable-xxxhdpi/ic_action_playback_speed_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxxhdpi/ic_toggle_played.png b/app/src/main/res/drawable-xxxhdpi/ic_toggle_played.png
new file mode 100644
index 00000000..0907fd2c
--- /dev/null
+++ b/app/src/main/res/drawable-xxxhdpi/ic_toggle_played.png
Binary files differ
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 00000000..63ac58e0
--- /dev/null
+++ b/app/src/main/res/drawable-xxxhdpi/media_fastforward_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxxhdpi/media_fastforward_light.png b/app/src/main/res/drawable-xxxhdpi/media_fastforward_light.png
new file mode 100644
index 00000000..d2f7506b
--- /dev/null
+++ b/app/src/main/res/drawable-xxxhdpi/media_fastforward_light.png
Binary files differ
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 00000000..e911b342
--- /dev/null
+++ b/app/src/main/res/drawable-xxxhdpi/media_rewind_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxxhdpi/media_rewind_light.png b/app/src/main/res/drawable-xxxhdpi/media_rewind_light.png
new file mode 100644
index 00000000..461118f1
--- /dev/null
+++ b/app/src/main/res/drawable-xxxhdpi/media_rewind_light.png
Binary files differ
diff --git a/app/src/main/res/drawable/card_rounded_corners_black.xml b/app/src/main/res/drawable/card_rounded_corners_black.xml
new file mode 100644
index 00000000..7592de64
--- /dev/null
+++ b/app/src/main/res/drawable/card_rounded_corners_black.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="@color/background_material_dark"/>
+ <corners android:radius="@dimen/Card.Radius"/>
+ <padding android:left="0dip" android:top="0dip" android:right="0dip" android:bottom="0dip" />
+</shape> \ 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 00000000..4db7d4b0
--- /dev/null
+++ b/app/src/main/res/drawable/card_rounded_corners_dark.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="@android:color/black"/>
+ <corners android:radius="@dimen/Card.Radius"/>
+ <padding android:left="0dip" android:top="0dip" android:right="0dip" android:bottom="0dip" />
+</shape> \ 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 00000000..5475c3d6
--- /dev/null
+++ b/app/src/main/res/drawable/card_rounded_corners_light.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="@android:color/white"/>
+ <corners android:radius="@dimen/Card.Radius"/>
+ <padding android:left="0dip" android:top="0dip" android:right="0dip" android:bottom="0dip" />
+</shape> \ No newline at end of file
diff --git a/app/src/main/res/drawable/drawer_header.jpg b/app/src/main/res/drawable/drawer_header.jpg
deleted file mode 100644
index f54a30e2..00000000
--- a/app/src/main/res/drawable/drawer_header.jpg
+++ /dev/null
Binary files differ
diff --git a/app/src/main/res/drawable/drawer_header_dark.png b/app/src/main/res/drawable/drawer_header_dark.png
new file mode 100644
index 00000000..a1c8d61f
--- /dev/null
+++ b/app/src/main/res/drawable/drawer_header_dark.png
Binary files differ
diff --git a/app/src/main/res/drawable/drawer_header_holo.png b/app/src/main/res/drawable/drawer_header_holo.png
new file mode 100644
index 00000000..d84d096d
--- /dev/null
+++ b/app/src/main/res/drawable/drawer_header_holo.png
Binary files differ
diff --git a/app/src/main/res/drawable/drawer_header_light.png b/app/src/main/res/drawable/drawer_header_light.png
new file mode 100644
index 00000000..1bcf4ec3
--- /dev/null
+++ b/app/src/main/res/drawable/drawer_header_light.png
Binary files differ
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 00000000..355c6a5b
--- /dev/null
+++ b/app/src/main/res/drawable/notification_fastforward.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<bitmap
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:src="@drawable/media_fastforward_dark"/> \ 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 00000000..ab7827a9
--- /dev/null
+++ b/app/src/main/res/drawable/notification_rewind.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<bitmap
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:src="@drawable/media_rewind_dark"/> \ 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
index f3e39a5f..855bf2a9 100644
--- a/app/src/main/res/layout-land/download.xml
+++ b/app/src/main/res/layout-land/download.xml
@@ -1,129 +1,122 @@
<?xml version="1.0" encoding="utf-8"?>
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/download_layout_container"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
-
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/download_layout"
- android:orientation="vertical"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent">
-
- <LinearLayout android:orientation="horizontal"
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/download_layout"
+ android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <ViewFlipper
+ android:id="@+id/download_playlist_flipper"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_weight="1">
+
+ <github.daneren2005.dsub.view.RecyclingImageView
+ android:id="@+id/download_album_art_image"
+ android:src="@drawable/unknown_album_large"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:scaleType="fitCenter"/>
+
+ <include layout="@layout/download_playlist"/>
+
+ </ViewFlipper>
+
+ <RelativeLayout
+ android:id="@+id/download_control_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:background="@android:color/transparent">
+
+ <LinearLayout
+ android:id="@+id/download_other_controls_wrapper"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerHorizontal="true"
+ android:layout_above="@+id/download_song_title">
+
+ <LinearLayout
+ android:id="@+id/download_other_controls_layout"
+ android:orientation="horizontal"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal">
+
+ <ImageButton
+ android:id="@+id/download_rating_bad"
+ style="@style/DownloadActionImageButton"
+ android:src="?attr/rating_bad"/>
+
+ <ImageButton
+ android:id="@+id/download_star"
+ style="@style/DownloadActionImageButton"
+ android:src="@android:drawable/star_big_off"/>
+
+ <ImageButton
+ android:id="@+id/download_playback_speed"
+ style="@style/DownloadActionImageButton"
+ android:src="?attr/playback_speed"/>
+
+ <ImageButton
+ android:id="@+id/download_bookmark"
+ style="@style/DownloadActionImageButton"
+ android:src="?attr/bookmark"/>
+
+ <ImageButton
+ android:id="@+id/download_rating_good"
+ style="@style/DownloadActionImageButton"
+ android:src="?attr/rating_good"/>
+ </LinearLayout>
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/download_song_title"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_centerHorizontal="true"
+ android:layout_marginLeft="12dip"
+ android:layout_marginRight="12dip"
+ android:singleLine="true"
+ android:ellipsize="end"
+ android:gravity="center_horizontal"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textColor="?android:textColorPrimary"
+ android:layout_above="@+id/download_status"/>
+
+ <TextView
+ android:id="@+id/download_status"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:layout_marginBottom="8dip"
+ android:layout_marginLeft="12dip"
+ android:layout_marginRight="12dip"
+ android:singleLine="true"
+ android:ellipsize="end"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="?android:textColorSecondary"
+ android:layout_above="@+id/download_media_buttons_wrapper"/>
+
+ <LinearLayout
+ android:id="@+id/download_media_buttons_wrapper"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_above="@+id/download_slider_wrapper">
+
+ <include layout="@layout/download_media_buttons"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/download_slider_wrapper"
android:layout_width="fill_parent"
- android:layout_height="0dip"
- android:layout_weight="1">
-
- <github.daneren2005.dsub.view.MyViewFlipper
- android:id="@+id/download_playlist_flipper"
- android:layout_width="0dp"
- android:layout_height="fill_parent"
- android:layout_weight="1">
-
- <github.daneren2005.dsub.view.RecyclingImageView
- android:id="@+id/download_album_art_image"
- android:src="@drawable/unknown_album_large"
- android:layout_width="wrap_content"
- android:layout_height="fill_parent"
- android:layout_weight="1"
- android:scaleType="fitStart"/>
-
- <include layout="@layout/download_playlist"/>
-
- </github.daneren2005.dsub.view.MyViewFlipper>
-
- <RelativeLayout android:orientation="vertical"
- android:id="@+id/download_control_layout"
- android:layout_width="0dp"
- android:layout_height="fill_parent"
- android:layout_weight="1"
- android:background="@android:color/transparent">
-
- <LinearLayout
- android:id="@+id/download_other_controls_wrapper"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_centerHorizontal="true"
- android:layout_above="@+id/download_song_title">
-
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/download_other_controls_layout"
- android:orientation="horizontal"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center_horizontal">
-
- <ImageButton
- android:id="@+id/download_rating_bad"
- style="@style/DownloadActionImageButton"
- android:src="?attr/rating_bad"/>
-
- <ImageButton
- android:id="@+id/download_star"
- style="@style/DownloadActionImageButton"
- android:src="@android:drawable/star_big_off"/>
-
- <ImageButton
- android:id="@+id/download_bookmark"
- style="@style/DownloadActionImageButton"
- android:src="?attr/bookmark"/>
-
- <ImageButton
- android:id="@+id/download_rating_good"
- style="@style/DownloadActionImageButton"
- android:src="?attr/rating_good"/>
- </LinearLayout>
- </LinearLayout>
-
- <TextView
- android:id="@+id/download_song_title"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:layout_centerHorizontal="true"
- android:layout_marginLeft="12dip"
- android:layout_marginRight="12dip"
- android:singleLine="true"
- android:ellipsize="end"
- android:gravity="center_horizontal"
- android:textAppearance="?android:attr/textAppearanceMedium"
- android:textColor="?android:textColorPrimary"
- android:layout_above="@+id/download_status"/>
-
- <TextView
- android:id="@+id/download_status"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:gravity="center_horizontal"
- android:layout_marginBottom="8dip"
- android:layout_marginLeft="12dip"
- android:layout_marginRight="12dip"
- android:singleLine="true"
- android:ellipsize="end"
- android:textAppearance="?android:attr/textAppearanceSmall"
- android:textColor="?android:textColorSecondary"
- android:layout_above="@+id/download_media_buttons_wrapper"/>
-
- <LinearLayout
- android:id="@+id/download_media_buttons_wrapper"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:layout_above="@+id/download_slider_wrapper">
-
- <include layout="@layout/download_media_buttons"/>
- </LinearLayout>
-
- <LinearLayout
- android:id="@+id/download_slider_wrapper"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:layout_alignParentBottom="true">
-
- <include layout="@layout/download_slider"/>
- </LinearLayout>
-
- </RelativeLayout>
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true">
+ <include layout="@layout/download_slider"/>
</LinearLayout>
- </LinearLayout>
-</FrameLayout>
+
+ </RelativeLayout>
+
+</LinearLayout>
diff --git a/app/src/main/res/layout-large-land/download.xml b/app/src/main/res/layout-large-land/download.xml
index 8b252190..cf5ef571 100644
--- a/app/src/main/res/layout-large-land/download.xml
+++ b/app/src/main/res/layout-large-land/download.xml
@@ -1,130 +1,121 @@
<?xml version="1.0" encoding="utf-8"?>
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/download_layout_container"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
-
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/download_layout"
- android:orientation="vertical"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent">
-
- <LinearLayout android:orientation="horizontal"
- android:layout_width="fill_parent"
- android:layout_height="0dip"
- android:layout_weight="1">
-
- <github.daneren2005.dsub.view.RecyclingImageView
- android:id="@+id/download_album_art_image"
- android:src="@drawable/unknown_album_large"
- android:layout_width="0dp"
- android:layout_height="fill_parent"
- android:layout_weight="1"
- android:scaleType="fitStart"/>
-
- <RelativeLayout android:orientation="vertical"
- android:id="@+id/download_control_layout"
- android:layout_width="0dp"
- android:layout_height="fill_parent"
- android:layout_weight="1"
- android:background="@android:color/transparent">
-
- <github.daneren2005.dsub.view.MyViewFlipper
- android:id="@+id/download_playlist_flipper"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- android:layout_above="@+id/download_song_title">
-
- <RelativeLayout
- android:id="@+id/download_other_controls_wrapper"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:layout_gravity="center_horizontal"
- android:orientation="vertical">
-
- <LinearLayout
- android:id="@+id/download_other_controls_layout"
- android:orientation="horizontal"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_centerHorizontal="true"
- android:layout_alignParentBottom="true">
-
- <ImageButton
- android:id="@+id/download_rating_bad"
- style="@style/DownloadActionImageButton"
- android:src="?attr/rating_bad"/>
-
- <ImageButton
- android:id="@+id/download_star"
- style="@style/DownloadActionImageButton"
- android:src="@android:drawable/star_big_off"/>
-
- <ImageButton
- android:id="@+id/download_bookmark"
- style="@style/DownloadActionImageButton"
- android:src="?attr/bookmark"/>
-
- <ImageButton
- android:id="@+id/download_rating_good"
- style="@style/DownloadActionImageButton"
- android:src="?attr/rating_good"/>
- </LinearLayout>
- </RelativeLayout>
-
- <include layout="@layout/download_playlist"/>
-
- </github.daneren2005.dsub.view.MyViewFlipper>
-
- <TextView
- android:id="@+id/download_song_title"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:layout_centerHorizontal="true"
- android:layout_marginLeft="12dip"
- android:layout_marginRight="12dip"
- android:singleLine="true"
- android:ellipsize="end"
- android:gravity="center_horizontal"
- android:textAppearance="?android:attr/textAppearanceMedium"
- android:textColor="?android:textColorPrimary"
- android:layout_above="@+id/download_status"/>
-
- <TextView
- android:id="@+id/download_status"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:gravity="center_horizontal"
- android:layout_marginBottom="8dip"
- android:layout_marginLeft="12dip"
- android:layout_marginRight="12dip"
- android:singleLine="true"
- android:ellipsize="end"
- android:textAppearance="?android:attr/textAppearanceSmall"
- android:textColor="?android:textColorSecondary"
- android:layout_above="@+id/download_media_buttons_wrapper"/>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/download_layout"
+ android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <github.daneren2005.dsub.view.RecyclingImageView
+ android:id="@+id/download_album_art_image"
+ android:src="@drawable/unknown_album_large"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:scaleType="fitCenter"/>
+
+ <RelativeLayout
+ android:id="@+id/download_control_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:background="@android:color/transparent">
+
+ <ViewFlipper
+ android:id="@+id/download_playlist_flipper"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:layout_above="@+id/download_song_title">
+
+ <RelativeLayout
+ android:id="@+id/download_other_controls_wrapper"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:orientation="vertical">
<LinearLayout
- android:id="@+id/download_media_buttons_wrapper"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:layout_above="@+id/download_slider_wrapper">
-
- <include layout="@layout/download_media_buttons"/>
- </LinearLayout>
-
- <LinearLayout
- android:id="@+id/download_slider_wrapper"
- android:layout_width="fill_parent"
+ android:id="@+id/download_other_controls_layout"
+ android:orientation="horizontal"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:layout_centerHorizontal="true"
android:layout_alignParentBottom="true">
- <include layout="@layout/download_slider"/>
+ <ImageButton
+ android:id="@+id/download_rating_bad"
+ style="@style/DownloadActionImageButton"
+ android:src="?attr/rating_bad"/>
+
+ <ImageButton
+ android:id="@+id/download_star"
+ style="@style/DownloadActionImageButton"
+ android:src="@android:drawable/star_big_off"/>
+
+ <ImageButton
+ android:id="@+id/download_playback_speed"
+ style="@style/DownloadActionImageButton"
+ android:src="?attr/playback_speed"/>
+
+ <ImageButton
+ android:id="@+id/download_bookmark"
+ style="@style/DownloadActionImageButton"
+ android:src="?attr/bookmark"/>
+
+ <ImageButton
+ android:id="@+id/download_rating_good"
+ style="@style/DownloadActionImageButton"
+ android:src="?attr/rating_good"/>
</LinearLayout>
-
</RelativeLayout>
+ <include layout="@layout/download_playlist"/>
+
+ </ViewFlipper>
+
+ <TextView
+ android:id="@+id/download_song_title"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_centerHorizontal="true"
+ android:layout_marginLeft="12dip"
+ android:layout_marginRight="12dip"
+ android:singleLine="true"
+ android:ellipsize="end"
+ android:gravity="center_horizontal"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textColor="?android:textColorPrimary"
+ android:layout_above="@+id/download_status"/>
+
+ <TextView
+ android:id="@+id/download_status"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:layout_marginBottom="8dip"
+ android:layout_marginLeft="12dip"
+ android:layout_marginRight="12dip"
+ android:singleLine="true"
+ android:ellipsize="end"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="?android:textColorSecondary"
+ android:layout_above="@+id/download_media_buttons_wrapper"/>
+
+ <LinearLayout
+ android:id="@+id/download_media_buttons_wrapper"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_above="@+id/download_slider_wrapper">
+
+ <include layout="@layout/download_media_buttons"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/download_slider_wrapper"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true">
+
+ <include layout="@layout/download_slider"/>
</LinearLayout>
- </LinearLayout>
-</FrameLayout>
+ </RelativeLayout>
+</LinearLayout>
diff --git a/app/src/main/res/layout-port/download.xml b/app/src/main/res/layout-port/download.xml
index 899b46b7..39b5e5e9 100644
--- a/app/src/main/res/layout-port/download.xml
+++ b/app/src/main/res/layout-port/download.xml
@@ -4,57 +4,50 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/download_layout"
- android:orientation="vertical"
+ <LinearLayout
+ android:id="@+id/download_layout"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+
+ <ViewFlipper
+ android:id="@+id/download_playlist_flipper"
android:layout_width="fill_parent"
- android:layout_height="fill_parent">
-
- <github.daneren2005.dsub.view.MyViewFlipper
- android:id="@+id/download_playlist_flipper"
- android:layout_width="fill_parent"
- android:layout_height="0dip"
- android:layout_weight="1">
-
- <RelativeLayout
- android:id="@+id/download_album_art_layout"
- android:orientation="vertical"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- android:layout_weight="1"
- android:background="@android:color/transparent">
-
- <FrameLayout android:orientation="vertical"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_alignParentTop="true">
-
- <github.daneren2005.dsub.view.RecyclingImageView
- android:id="@+id/download_album_art_image"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:scaleType="fitCenter"
- android:layout_gravity="center_horizontal|top"/>
-
- <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/download_overlay_buttons"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:background="@color/overlayColor"
- android:layout_gravity="center_horizontal|bottom"
- android:visibility="invisible">
-
- <LinearLayout
+ android:layout_height="0dip"
+ android:layout_weight="1">
+
+ <LinearLayout
+ android:id="@+id/download_album_art_layout"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:background="@android:color/transparent">
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1">
+
+ <FrameLayout android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <github.daneren2005.dsub.view.RecyclingImageView
+ android:id="@+id/download_album_art_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_centerHorizontal="true">
+ android:scaleType="fitCenter"
+ android:layout_gravity="center_horizontal|top"/>
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/download_other_controls_layout"
- android:orientation="horizontal"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center_horizontal">
+ <LinearLayout
+ android:id="@+id/download_overlay_buttons"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal|bottom"
+ android:background="@color/overlayColor"
+ android:visibility="invisible"
+ android:gravity="center"
+ android:orientation="horizontal">
<ImageButton
android:id="@+id/download_rating_bad"
@@ -67,6 +60,11 @@
android:src="@drawable/ic_toggle_star_outline_dark"/>
<ImageButton
+ android:id="@+id/download_playback_speed"
+ style="@style/DownloadActionImageButton"
+ android:src="@drawable/ic_action_playback_speed_dark"/>
+
+ <ImageButton
android:id="@+id/download_bookmark"
style="@style/DownloadActionImageButton"
android:src="@drawable/ic_menu_bookmark_dark"/>
@@ -75,44 +73,39 @@
android:id="@+id/download_rating_good"
style="@style/DownloadActionImageButton"
android:src="@drawable/ic_action_rating_good_dark"/>
- </LinearLayout>
</LinearLayout>
- </RelativeLayout>
+ </FrameLayout>
</FrameLayout>
<TextView
- android:id="@+id/download_status"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_alignParentBottom="true"
- android:layout_centerHorizontal="true"
- android:layout_marginLeft="16dip"
- android:layout_marginRight="16dip"
- android:singleLine="true"
- android:ellipsize="end"
- android:textAppearance="?android:attr/textAppearanceSmall"
- android:textColor="?android:textColorSecondary"/>
+ android:id="@+id/download_status"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:layout_marginLeft="16dip"
+ android:layout_marginRight="16dip"
+ android:layout_marginTop="6dp"
+ android:singleLine="true"
+ android:ellipsize="end"
+ style="?attr/actionbarSubtitleStyle"
+ android:textColor="?android:textColorSecondary"/>
<TextView
- android:id="@+id/download_song_title"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center_horizontal"
- android:layout_above="@+id/download_status"
- android:layout_centerHorizontal="true"
- android:layout_marginLeft="16dip"
- android:layout_marginRight="16dip"
- android:singleLine="true"
- android:textColor="?android:textColorPrimary"
- android:textStyle="bold"
- android:textSize="18sp"
- android:ellipsize="end"/>
-
- </RelativeLayout>
+ android:id="@+id/download_song_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:layout_marginLeft="16dip"
+ android:layout_marginRight="16dip"
+ android:singleLine="true"
+ android:textStyle="bold"
+ style="?attr/actionbarTitleStyle"
+ android:textColor="?android:textColorPrimary"/>
+ </LinearLayout>
<include layout="@layout/download_playlist"/>
- </github.daneren2005.dsub.view.MyViewFlipper>
+ </ViewFlipper>
<include layout="@layout/download_media_buttons"/>
diff --git a/app/src/main/res/layout/abstract_fragment_activity.xml b/app/src/main/res/layout/abstract_fragment_activity.xml
index d41b0115..ae6647c4 100644
--- a/app/src/main/res/layout/abstract_fragment_activity.xml
+++ b/app/src/main/res/layout/abstract_fragment_activity.xml
@@ -44,7 +44,7 @@
android:layout_width="match_parent"
android:elevation="4dp"
android:visibility="gone"
- app:theme="?attr/actionbarThemeStyle"
+ android:theme="?attr/actionbarThemeStyle"
app:popupTheme="?attr/actionbarPopupStyle"/>
<LinearLayout
@@ -60,7 +60,7 @@
android:layout_width="?attr/actionBarSize"
android:layout_height="?attr/actionBarSize"
android:layout_gravity="left|center"
- android:scaleType="fitStart"/>
+ android:scaleType="fitCenter"/>
<LinearLayout
android:layout_width="0dp"
@@ -105,6 +105,13 @@
<ImageButton
style="@style/PlaybackControl.BottomBar"
+ android:id="@+id/download_rewind"
+ android:src="?attr/actionbar_rewind"
+ android:padding="2dp"
+ android:visibility="gone"/>
+
+ <ImageButton
+ style="@style/PlaybackControl.BottomBar"
android:id="@+id/download_previous"
android:src="?attr/actionbar_backward"
android:padding="2dp"/>
@@ -119,11 +126,18 @@
android:id="@+id/download_next"
android:src="?attr/actionbar_forward"
android:padding="2dp"/>
+
+ <ImageButton
+ style="@style/PlaybackControl.BottomBar"
+ android:id="@+id/download_fastforward"
+ android:src="?attr/actionbar_fastforward"
+ android:padding="2dp"
+ android:visibility="gone"/>
</LinearLayout>
</LinearLayout>
</FrameLayout>
- <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ <FrameLayout
android:id="@+id/now_playing_fragment_container"
android:layout_width="match_parent"
android:layout_height="0dp"
diff --git a/app/src/main/res/layout/abstract_recycler_fragment.xml b/app/src/main/res/layout/abstract_recycler_fragment.xml
index 0e0c87f4..0a443ed6 100644
--- a/app/src/main/res/layout/abstract_recycler_fragment.xml
+++ b/app/src/main/res/layout/abstract_recycler_fragment.xml
@@ -19,7 +19,10 @@
android:id="@+id/fragment_recycler"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
- android:scrollbars="vertical"/>
+ android:scrollbars="vertical"
+ android:paddingRight="@dimen/FastScroller.LeftAlignedMargin"
+ android:layout_marginRight="@dimen/FastScroller.NormalBarMargin"
+ android:scrollbarStyle="outsideOverlay"/>
<github.daneren2005.dsub.view.FastScroller
android:id="@+id/fragment_fast_scroller"
diff --git a/app/src/main/res/layout/actionbar_spinner.xml b/app/src/main/res/layout/actionbar_spinner.xml
index 22fa7f43..f719a67c 100644
--- a/app/src/main/res/layout/actionbar_spinner.xml
+++ b/app/src/main/res/layout/actionbar_spinner.xml
@@ -1,14 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:orientation="vertical"
- android:layout_width="fill_parent"
+<Spinner xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/spinner"
+ android:layout_width="wrap_content"
android:layout_height="fill_parent"
- android:gravity="fill_horizontal" >
-
- <Spinner
- android:id="@+id/spinner"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- android:prompt="@string/common.appname"
- style="?attr/android:spinnerItemStyle"/>
-</RelativeLayout>
+ android:prompt="@string/common.appname"
+ style="?attr/android:spinnerItemStyle"
+ android:overlapAnchor="false"/> \ 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
index f6693a7f..4ad32409 100644
--- a/app/src/main/res/layout/album_cell_item.xml
+++ b/app/src/main/res/layout/album_cell_item.xml
@@ -1,90 +1,98 @@
<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:orientation="vertical"
+<github.daneren2005.dsub.view.CardView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:background="?attr/selectableItemBackground">
+ android:layout_margin="2dp">
- <RelativeLayout
+ <LinearLayout
+ android:orientation="vertical"
android:layout_width="match_parent"
- android:layout_height="0dp"
- android:layout_weight="1">
+ android:layout_height="match_parent"
+ android:background="?attr/selectableItemBackground">
- <github.daneren2005.dsub.view.SquareImageView
- android:id="@+id/album_coverart"
+ <RelativeLayout
android:layout_width="match_parent"
- android:layout_height="match_parent"/>
+ android:layout_height="0dp"
+ android:layout_weight="1">
- <RatingBar
- android:id="@+id/album_rating"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:isIndicator="true"
- android:layout_centerHorizontal="true"
- android:numStars="5"
- style="@android:style/Widget.Holo.RatingBar.Small"
- android:layout_alignParentBottom="true"
- android:visibility="gone"/>
- </RelativeLayout>
+ <github.daneren2005.dsub.view.SquareImageView
+ android:id="@+id/album_coverart"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
- <LinearLayout
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:orientation="horizontal"
- android:paddingTop="4dp"
- android:paddingLeft="2dp">
+ <RatingBar
+ android:id="@+id/album_rating"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:isIndicator="true"
+ android:layout_centerHorizontal="true"
+ android:numStars="5"
+ style="@android:style/Widget.Holo.RatingBar.Small"
+ android:layout_alignParentBottom="true"
+ android:visibility="gone"/>
+ </RelativeLayout>
<LinearLayout
- android:layout_width="0dp"
+ android:layout_width="fill_parent"
android:layout_height="wrap_content"
- android:layout_weight="1"
- android:gravity="center_vertical"
- android:orientation="vertical">
-
- <TextView
- android:id="@+id/album_title"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:textAppearance="?android:attr/textAppearanceSmall"
- android:singleLine="true"
- android:ellipsize="marquee"
- android:text="@string/search.albums"
- android:textColor="?android:textColorPrimary"/>
+ android:orientation="horizontal"
+ android:paddingTop="4dp"
+ android:paddingBottom="4dp"
+ android:paddingLeft="2dp">
<LinearLayout
- android:layout_width="fill_parent"
- android:layout_height="wrap_content">
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:gravity="center_vertical"
+ android:orientation="vertical">
<TextView
- android:id="@+id/album_artist"
- android:layout_width="0dp"
- android:layout_weight="1"
+ android:id="@+id/album_title"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:textSize="12sp"
- android:textColor="?android:textColorSecondary"
+ android:textAppearance="?android:attr/textAppearanceSmall"
android:singleLine="true"
- android:text="@string/search.artists"/>
+ android:ellipsize="marquee"
+ android:text="@string/search.albums"
+ android:textColor="?android:textColorPrimary"
+ android:paddingLeft="@dimen/Card.TextLeftPadding"/>
+
+ <LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
- <ImageButton
- android:id="@+id/album_star"
- android:layout_width="@dimen/Star.Small"
- android:layout_height="@dimen/Star.Small"
- android:scaleType="fitCenter"
- android:layout_gravity="right|center_vertical"
- android:background="@android:color/transparent"
- android:focusable="false"
- android:visibility="gone"/>
+ <TextView
+ android:id="@+id/album_artist"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"
+ android:textSize="12sp"
+ android:textColor="?android:textColorSecondary"
+ android:singleLine="true"
+ android:text="@string/search.artists"
+ android:paddingLeft="@dimen/Card.TextLeftPadding"/>
+
+ <ImageButton
+ android:id="@+id/album_star"
+ android:layout_width="@dimen/Star.Small"
+ android:layout_height="@dimen/Star.Small"
+ android:scaleType="fitCenter"
+ android:layout_gravity="right|center_vertical"
+ android:background="@android:color/transparent"
+ android:focusable="false"
+ android:visibility="gone"/>
+ </LinearLayout>
</LinearLayout>
+
+ <ImageView
+ android:id="@+id/item_more"
+ android:src="?attr/download_none"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="right|center_vertical"
+ style="@style/MoreButton"/>
</LinearLayout>
- <ImageView
- android:id="@+id/item_more"
- android:src="?attr/download_none"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="right|center_vertical"
- android:paddingRight="2dp"
- style="@style/BasicButton"/>
</LinearLayout>
-
-</LinearLayout> \ No newline at end of file
+</github.daneren2005.dsub.view.CardView> \ No newline at end of file
diff --git a/app/src/main/res/layout/basic_cell_item.xml b/app/src/main/res/layout/basic_cell_item.xml
index f522b196..a10fc4be 100644
--- a/app/src/main/res/layout/basic_cell_item.xml
+++ b/app/src/main/res/layout/basic_cell_item.xml
@@ -1,39 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:orientation="vertical"
+<github.daneren2005.dsub.view.CardView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:background="?attr/selectableItemBackground">
-
- <github.daneren2005.dsub.view.SquareImageView
- android:id="@+id/item_art"
- android:layout_width="match_parent"
- android:layout_height="match_parent"/>
+ android:layout_margin="2dp">
<LinearLayout
+ android:orientation="vertical"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="horizontal"
- android:paddingTop="4dp"
- android:paddingLeft="2dp">
+ android:layout_height="match_parent"
+ android:background="?attr/selectableItemBackground">
- <TextView
- android:id="@+id/item_name"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:textAppearance="?android:attr/textAppearanceMedium"
- android:singleLine="true"
- android:ellipsize="marquee"
- android:textColor="?android:textColorPrimary"/>
+ <github.daneren2005.dsub.view.SquareImageView
+ android:id="@+id/item_art"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
- <ImageView
- android:id="@+id/item_more"
- android:src="?attr/download_none"
- android:layout_width="wrap_content"
+ <LinearLayout
+ android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_gravity="right|center_vertical"
- android:paddingRight="2dp"
- style="@style/BasicButton"/>
+ android:orientation="horizontal"
+ android:paddingTop="4dp"
+ android:paddingLeft="2dp">
+
+ <TextView
+ android:id="@+id/item_name"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:singleLine="true"
+ android:ellipsize="marquee"
+ android:textColor="?android:textColorPrimary"
+ android:paddingLeft="@dimen/Card.TextLeftPadding"/>
+
+ <ImageView
+ android:id="@+id/item_more"
+ android:src="?attr/download_none"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="right|center_vertical"
+ style="@style/MoreButton"/>
+ </LinearLayout>
</LinearLayout>
-</LinearLayout> \ No newline at end of file
+</github.daneren2005.dsub.view.CardView> \ No newline at end of file
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 00000000..31e12642
--- /dev/null
+++ b/app/src/main/res/layout/cache_location_buttons.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center">
+
+ <Button
+ android:id="@+id/location_internal"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/settings.cache_location_internal"/>
+
+ <Button
+ android:id="@+id/location_external"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/settings.cache_location_external"/>
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/change_email.xml b/app/src/main/res/layout/change_email.xml
index 87d297be..d78edd13 100644
--- a/app/src/main/res/layout/change_email.xml
+++ b/app/src/main/res/layout/change_email.xml
@@ -4,7 +4,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ <LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
diff --git a/app/src/main/res/layout/change_password.xml b/app/src/main/res/layout/change_password.xml
index d8043c05..68861b0b 100644
--- a/app/src/main/res/layout/change_password.xml
+++ b/app/src/main/res/layout/change_password.xml
@@ -4,7 +4,30 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ <LinearLayout
+ android:id="@+id/current_password_layout"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/current_password_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="4dp"
+ android:textSize="20dp"
+ android:text="@string/admin.change_password_current_label"
+ android:textColor="?android:textColorPrimary"/>
+ <EditText
+ android:id="@+id/current_password"
+ android:inputType="textPassword"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginLeft="4dp" />
+ </LinearLayout>
+
+ <LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
diff --git a/app/src/main/res/layout/confirm_password.xml b/app/src/main/res/layout/confirm_password.xml
index 9ec61c0a..d74eecfd 100644
--- a/app/src/main/res/layout/confirm_password.xml
+++ b/app/src/main/res/layout/confirm_password.xml
@@ -4,7 +4,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ <LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
diff --git a/app/src/main/res/layout/create_bookmark.xml b/app/src/main/res/layout/create_bookmark.xml
index d6f077c3..22d96227 100644
--- a/app/src/main/res/layout/create_bookmark.xml
+++ b/app/src/main/res/layout/create_bookmark.xml
@@ -3,7 +3,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content">
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ <LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
diff --git a/app/src/main/res/layout/create_podcast.xml b/app/src/main/res/layout/create_podcast.xml
index 04e74ec3..a5e66792 100644
--- a/app/src/main/res/layout/create_podcast.xml
+++ b/app/src/main/res/layout/create_podcast.xml
@@ -3,7 +3,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content">
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ <LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
diff --git a/app/src/main/res/layout/create_user.xml b/app/src/main/res/layout/create_user.xml
index b2d8f6e0..7d77ade9 100644
--- a/app/src/main/res/layout/create_user.xml
+++ b/app/src/main/res/layout/create_user.xml
@@ -4,7 +4,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ <LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
@@ -26,7 +26,7 @@
android:textColor="?android:textColorPrimary"/>
</LinearLayout>
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ <LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
@@ -48,7 +48,7 @@
android:textColor="?android:textColorPrimary"/>
</LinearLayout>
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ <LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
diff --git a/app/src/main/res/layout/download_media_buttons.xml b/app/src/main/res/layout/download_media_buttons.xml
index 58fda5c0..0610c5f9 100644
--- a/app/src/main/res/layout/download_media_buttons.xml
+++ b/app/src/main/res/layout/download_media_buttons.xml
@@ -21,6 +21,14 @@
android:layout_centerVertical="true"
/>
+ <github.daneren2005.dsub.view.AutoRepeatButton
+ style="@style/PlaybackControl.Large"
+ android:id="@+id/download_rewind"
+ android:src="?attr/media_button_rewind"
+ android:layout_toLeftOf="@+id/download_pause"
+ android:layout_centerVertical="true"
+ android:visibility="invisible"/>
+
<ImageButton
style="@style/PlaybackControl.Large"
android:id="@+id/download_pause"
@@ -44,6 +52,14 @@
android:layout_centerInParent="true"
/>
+ <github.daneren2005.dsub.view.AutoRepeatButton
+ style="@style/PlaybackControl.Large"
+ android:id="@+id/download_fastforward"
+ android:src="?attr/media_button_fastforward"
+ android:layout_toRightOf="@+id/download_start"
+ android:layout_centerVertical="true"
+ android:visibility="invisible"/>
+
<github.daneren2005.dsub.view.AutoRepeatButton
style="@style/PlaybackControl.Small"
android:id="@+id/download_next"
diff --git a/app/src/main/res/layout/download_playlist.xml b/app/src/main/res/layout/download_playlist.xml
index db74f8ca..161056db 100644
--- a/app/src/main/res/layout/download_playlist.xml
+++ b/app/src/main/res/layout/download_playlist.xml
@@ -28,7 +28,9 @@
android:id="@+id/download_list"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
- android:scrollbars="vertical"/>
+ android:scrollbars="vertical"
+ android:paddingRight="@dimen/FastScroller.LeftAlignedMargin"
+ android:layout_marginRight="@dimen/FastScroller.NormalBarMargin"/>
<github.daneren2005.dsub.view.FastScroller
android:id="@+id/download_fast_scroller"
diff --git a/app/src/main/res/layout/drawer_header.xml b/app/src/main/res/layout/drawer_header.xml
index 86222f43..3a9a2d49 100644
--- a/app/src/main/res/layout/drawer_header.xml
+++ b/app/src/main/res/layout/drawer_header.xml
@@ -4,7 +4,7 @@
android:layout_height="178dp"
android:orientation="vertical"
android:weightSum="1"
- android:background="@drawable/drawer_header">
+ android:background="?attr/drawerHeaderBackground">
<LinearLayout
android:layout_width="match_parent"
diff --git a/app/src/main/res/layout/newest_episode_header.xml b/app/src/main/res/layout/expandable_header.xml
index bd78275e..bd78275e 100644
--- a/app/src/main/res/layout/newest_episode_header.xml
+++ b/app/src/main/res/layout/expandable_header.xml
diff --git a/app/src/main/res/layout/fast_scroller.xml b/app/src/main/res/layout/fast_scroller.xml
index b2e244e3..4d37ca63 100644
--- a/app/src/main/res/layout/fast_scroller.xml
+++ b/app/src/main/res/layout/fast_scroller.xml
@@ -18,8 +18,8 @@
<ImageView
android:id="@+id/fastscroller_handle"
android:layout_width="wrap_content"
- android:layout_marginRight="8dp"
- android:layout_marginLeft="8dp"
+ android:layout_marginRight="@dimen/FastScroller.RightMargin"
+ android:layout_marginLeft="12dp"
android:layout_height="wrap_content"
android:src="@drawable/fast_scroller_handle"/>
</merge> \ No newline at end of file
diff --git a/app/src/main/res/layout/home.xml b/app/src/main/res/layout/home.xml
deleted file mode 100644
index 043886a2..00000000
--- a/app/src/main/res/layout/home.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/home_layout"
- android:orientation="vertical"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent">
-
- <View
- android:layout_width="fill_parent"
- android:layout_height="1px"
- android:background="?attr/colorPrimary"/>
-
- <ListView
- android:id="@+id/main_list"
- android:layout_width="fill_parent"
- android:layout_height="0px"
- android:layout_weight="1"/>
-
- <View android:id="@+id/main_dummy"
- android:layout_width="0px"
- android:layout_height="0px"/>
-</LinearLayout>
-
diff --git a/app/src/main/res/layout/notification.xml b/app/src/main/res/layout/notification.xml
index 4a89db49..0ab5a884 100644
--- a/app/src/main/res/layout/notification.xml
+++ b/app/src/main/res/layout/notification.xml
@@ -13,7 +13,6 @@
android:gravity="center" />
<LinearLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1"
diff --git a/app/src/main/res/layout/notification_expanded.xml b/app/src/main/res/layout/notification_expanded.xml
index 7b378e12..a1586214 100644
--- a/app/src/main/res/layout/notification_expanded.xml
+++ b/app/src/main/res/layout/notification_expanded.xml
@@ -12,7 +12,6 @@
android:gravity="center" />
<LinearLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="0.0"
diff --git a/app/src/main/res/layout/progress.xml b/app/src/main/res/layout/progress.xml
deleted file mode 100644
index 8a299d63..00000000
--- a/app/src/main/res/layout/progress.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:orientation="horizontal"
- android:layout_weight="1"
- android:layout_width="0dip"
- android:layout_height="fill_parent"
- android:padding="10dp">
-
- <ProgressBar
- android:layout_width="wrap_content"
- android:layout_height="fill_parent"
- android:layout_marginRight="10dp"/>
-
- <TextView
- android:id="@+id/progress_message"
- android:text="@string/progress.wait"
- android:layout_width="wrap_content"
- android:layout_height="fill_parent"
- android:textColor="?android:textColorPrimary"/>
-</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/seekbar_preference.xml b/app/src/main/res/layout/seekbar_preference.xml
index 74dad4cd..e0c9bb31 100644
--- a/app/src/main/res/layout/seekbar_preference.xml
+++ b/app/src/main/res/layout/seekbar_preference.xml
@@ -9,7 +9,7 @@
android:padding="5dip"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
- android:gravity="right"
+ android:gravity="center"
android:textColor="?android:textColorPrimary"/>
<SeekBar
android:id="@+id/seek_bar"
diff --git a/app/src/main/res/layout/select_album_header.xml b/app/src/main/res/layout/select_album_header.xml
index 5b2294f0..891db891 100644
--- a/app/src/main/res/layout/select_album_header.xml
+++ b/app/src/main/res/layout/select_album_header.xml
@@ -121,7 +121,7 @@
android:contentDescription="@null"/>
</RelativeLayout>
- <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ <FrameLayout
android:id="@+id/header_progress"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
diff --git a/app/src/main/res/layout/set_playback_speed.xml b/app/src/main/res/layout/set_playback_speed.xml
new file mode 100644
index 00000000..c85719bb
--- /dev/null
+++ b/app/src/main/res/layout/set_playback_speed.xml
@@ -0,0 +1,66 @@
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:background="?android:colorBackground"
+ >
+
+
+ <LinearLayout
+ android:id="@+id/playback_speed_buttons"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:orientation="horizontal"
+ >
+
+ <Button
+ android:id="@+id/playback_speed_normal"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="1.0x"
+ android:tag="1.0"/>
+ <Button
+ android:id="@+id/playback_speed_one_half"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:tag="1.5"
+ android:text="1.5x"/>
+
+ <Button
+ android:id="@+id/playback_speed_double"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:tag="2.0"
+ android:text="2.0x"/>
+
+
+ <Button
+ android:id="@+id/playback_speed_triple"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="3.0x"
+ android:tag="3.0"/>
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/playback_speed_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="8dp"
+ android:textSize="20dp"
+ android:paddingRight="10px"
+ android:textColor="?android:textColorPrimary"
+ android:layout_below="@+id/playback_speed_buttons"/>
+
+ <SeekBar
+ android:id="@+id/playback_speed_bar"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:max="25"
+ android:layout_toRightOf="@+id/playback_speed_label"
+ android:layout_below="@+id/playback_speed_buttons"
+ />
+
+</RelativeLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/settings_activity.xml b/app/src/main/res/layout/settings_activity.xml
index 3baa30d4..bdcc4a23 100644
--- a/app/src/main/res/layout/settings_activity.xml
+++ b/app/src/main/res/layout/settings_activity.xml
@@ -10,7 +10,7 @@
android:layout_width="match_parent"
android:background="?attr/actionbarBackgroundColor"
android:elevation="4dp"
- app:theme="?attr/actionbarThemeStyle"
+ android:theme="?attr/actionbarThemeStyle"
app:popupTheme="?attr/actionbarPopupStyle"/>
<FrameLayout
diff --git a/app/src/main/res/layout/shuffle_dialog.xml b/app/src/main/res/layout/shuffle_dialog.xml
index 63778ed7..012c220f 100644
--- a/app/src/main/res/layout/shuffle_dialog.xml
+++ b/app/src/main/res/layout/shuffle_dialog.xml
@@ -3,7 +3,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content">
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ <LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
@@ -26,7 +26,7 @@
android:hint="@string/shuffle.startYear" />
</LinearLayout>
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ <LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
@@ -49,7 +49,7 @@
android:hint="@string/shuffle.endYear" />
</LinearLayout>
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ <LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
diff --git a/app/src/main/res/layout/song_list_item.xml b/app/src/main/res/layout/song_list_item.xml
index 6bf025b8..d7c8d312 100644
--- a/app/src/main/res/layout/song_list_item.xml
+++ b/app/src/main/res/layout/song_list_item.xml
@@ -17,25 +17,36 @@
android:layout_height="wrap_content"
android:layout_gravity="center_vertical">
+ <TextView
+ android:id="@+id/song_track"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="left|top"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textColor="?android:textColorPrimary"
+ android:paddingLeft="6dip"
+ android:paddingRight="6dip"
+ android:drawablePadding="6dip"/>
+
<TextView
android:id="@+id/song_title"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1"
- android:layout_gravity="left|center_vertical"
+ android:layout_gravity="left|top"
android:textAppearance="?android:attr/textAppearanceMedium"
android:singleLine="true"
android:ellipsize="marquee"
- android:drawablePadding="6dip"
android:paddingLeft="6dip"
android:paddingRight="6dip"
+ android:drawablePadding="6dip"
android:textColor="?android:textColorPrimary"/>
<ImageButton
android:id="@+id/song_bookmark"
- android:layout_width="24dp"
- android:layout_height="24dp"
- android:layout_gravity="right|center_vertical"
+ android:layout_width="@dimen/SongStatusIcon"
+ android:layout_height="@dimen/SongStatusIcon"
+ android:layout_gravity="right|top"
android:background="@null"
android:focusable="false"
android:scaleType="fitCenter"
@@ -46,7 +57,17 @@
android:layout_width="@dimen/Star.Small"
android:layout_height="@dimen/Star.Small"
android:scaleType="fitCenter"
- android:layout_gravity="right|center_vertical"
+ android:layout_gravity="right|top"
+ android:background="@null"
+ android:focusable="false"
+ android:visibility="gone"/>
+
+ <ImageButton
+ android:id="@+id/song_played"
+ android:layout_width="@dimen/SongStatusIcon"
+ android:layout_height="@dimen/SongStatusIcon"
+ android:scaleType="fitCenter"
+ android:layout_gravity="right|top"
android:background="@null"
android:focusable="false"
android:visibility="gone"/>
@@ -55,15 +76,15 @@
android:id="@+id/song_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_gravity="right|center_vertical"
+ android:layout_gravity="right|top"
android:drawablePadding="1dip"
android:paddingRight="2dip"/>
<ImageView
android:id="@+id/song_status_icon"
- android:layout_width="24dip"
- android:layout_height="24dip"
- android:layout_gravity="center_vertical"
+ android:layout_width="@dimen/SongStatusIcon"
+ android:layout_height="@dimen/SongStatusIcon"
+ android:layout_gravity="top"
android:src="?attr/downloading"
android:visibility="gone"/>
</LinearLayout>
diff --git a/app/src/main/res/layout/start_timer.xml b/app/src/main/res/layout/start_timer.xml
index 59bd60e3..61a72233 100644
--- a/app/src/main/res/layout/start_timer.xml
+++ b/app/src/main/res/layout/start_timer.xml
@@ -18,5 +18,5 @@
android:id="@+id/timer_length_bar"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
- android:max="97"/>
+ android:max="92"/>
</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/update_playlist.xml b/app/src/main/res/layout/update_playlist.xml
index cc7e5ee6..f9cc6a90 100644
--- a/app/src/main/res/layout/update_playlist.xml
+++ b/app/src/main/res/layout/update_playlist.xml
@@ -3,7 +3,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content">
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ <LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
@@ -26,7 +26,7 @@
android:textColor="?android:textColorPrimary"/>
</LinearLayout>
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ <LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
@@ -49,7 +49,7 @@
android:hint="@string/common.comment" />
</LinearLayout>
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ <LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
diff --git a/app/src/main/res/layout/update_share.xml b/app/src/main/res/layout/update_share.xml
index ef44e304..0d06e00d 100644
--- a/app/src/main/res/layout/update_share.xml
+++ b/app/src/main/res/layout/update_share.xml
@@ -3,7 +3,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content">
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ <LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
@@ -26,7 +26,7 @@
android:hint="@string/common.name" />
</LinearLayout>
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ <LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
@@ -48,7 +48,7 @@
android:calendarViewShown="false"/>
</LinearLayout>
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ <LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
diff --git a/app/src/main/res/menu/abstract_top_menu.xml b/app/src/main/res/menu/abstract_top_menu.xml
index 7c8d414d..b768879d 100644
--- a/app/src/main/res/menu/abstract_top_menu.xml
+++ b/app/src/main/res/menu/abstract_top_menu.xml
@@ -5,7 +5,8 @@
android:id="@+id/menu_global_search"
android:icon="?attr/search"
android:title="@string/menu.search"
- compat:showAsAction="always|withText"/>
+ compat:actionViewClass="android.support.v7.widget.SearchView"
+ compat:showAsAction="always|collapseActionView"/>
<group android:id="@+id/not_touchscreen">
<item
diff --git a/app/src/main/res/menu/drawer_menu.xml b/app/src/main/res/menu/drawer_menu.xml
deleted file mode 100644
index b3e70cfa..00000000
--- a/app/src/main/res/menu/drawer_menu.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<menu xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:compat="http://schemas.android.com/apk/res-auto">
-
- <item
- android:id="@+id/menu_search"
- android:icon="?attr/search"
- android:title="@string/menu.search"
- compat:showAsAction="always|withText"/>
-
- <item
- android:id="@+id/menu_exit"
- android:title="@string/menu.exit"/>
-</menu>
diff --git a/app/src/main/res/menu/drawer_navigation.xml b/app/src/main/res/menu/drawer_navigation.xml
index bd309455..32de5cd5 100644
--- a/app/src/main/res/menu/drawer_navigation.xml
+++ b/app/src/main/res/menu/drawer_navigation.xml
@@ -22,6 +22,10 @@
android:icon="?attr/drawerBookmarks"
android:title="@string/button_bar.bookmarks"/>
<item
+ android:id="@+id/drawer_internet_radio_stations"
+ android:icon="?attr/drawerInternetRadioStations"
+ android:title="@string/button_bar.internet_radio"/>
+ <item
android:id="@+id/drawer_shares"
android:icon="?attr/drawerShares"
android:title="@string/button_bar.shares"/>
diff --git a/app/src/main/res/menu/main.xml b/app/src/main/res/menu/main.xml
index 0970c8ce..b264d4fa 100644
--- a/app/src/main/res/menu/main.xml
+++ b/app/src/main/res/menu/main.xml
@@ -5,15 +5,16 @@
android:id="@+id/menu_global_search"
android:icon="?attr/search"
android:title="@string/menu.search"
- compat:showAsAction="always|withText"/>
+ compat:actionViewClass="android.support.v7.widget.SearchView"
+ compat:showAsAction="ifRoom|collapseActionView"/>
<item
android:id="@+id/menu_global_shuffle"
android:icon="?attr/shuffle"
android:title="@string/menu.shuffle"
- compat:showAsAction="always|withText"/>
+ compat:showAsAction="ifRoom|withText"/>
- <group android:id="@+id/madsonic">
+ <group android:id="@+id/rescan_server">
<item
android:id="@+id/menu_rescan"
android:title="@string/menu.rescan"/>
diff --git a/app/src/main/res/menu/multiselect_media.xml b/app/src/main/res/menu/multiselect_media.xml
index 6adb4567..85bb5217 100644
--- a/app/src/main/res/menu/multiselect_media.xml
+++ b/app/src/main/res/menu/multiselect_media.xml
@@ -34,8 +34,8 @@
android:title="@string/menu.remove_playlist"/>
<item
- android:id="@+id/menu_unstar"
- android:title="@string/common.unstar"/>
+ android:id="@+id/menu_star"
+ android:title="@string/common.star"/>
<group android:id="@+id/hide_play_next">
<item
diff --git a/app/src/main/res/menu/multiselect_nowplaying.xml b/app/src/main/res/menu/multiselect_nowplaying.xml
new file mode 100644
index 00000000..9d361bf0
--- /dev/null
+++ b/app/src/main/res/menu/multiselect_nowplaying.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:compat="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/menu_download"
+ android:title="@string/common.download"
+ android:icon="?attr/download"
+ compat:showAsAction="ifRoom|withText"/>
+
+ <item
+ android:id="@+id/menu_delete"
+ android:title="@string/menu.delete_cache"
+ android:icon="?attr/remove"
+ compat:showAsAction="ifRoom|withText"/>
+
+ <item
+ android:id="@+id/menu_cache"
+ android:title="@string/common.pin"/>
+
+ <item
+ android:id="@+id/menu_add_playlist"
+ android:title="@string/menu.add_playlist"/>
+
+ <item
+ android:id="@+id/menu_star"
+ android:title="@string/common.star"/>
+</menu>
diff --git a/app/src/main/res/menu/multiselect_nowplaying_offline.xml b/app/src/main/res/menu/multiselect_nowplaying_offline.xml
new file mode 100644
index 00000000..044836c6
--- /dev/null
+++ b/app/src/main/res/menu/multiselect_nowplaying_offline.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:compat="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/menu_delete"
+ android:title="@string/menu.delete_cache"
+ android:icon="?attr/remove"
+ compat:showAsAction="ifRoom|withText"/>
+</menu>
diff --git a/app/src/main/res/menu/nowplaying.xml b/app/src/main/res/menu/nowplaying.xml
index 60255692..9c198e9a 100644
--- a/app/src/main/res/menu/nowplaying.xml
+++ b/app/src/main/res/menu/nowplaying.xml
@@ -33,6 +33,11 @@
android:title="@string/equalizer.label"
android:checkable="true"/>
+ <item
+ android:id="@+id/menu_batch_mode"
+ android:title="@string/download.batch_mode"
+ android:checkable="true"/>
+
<item
android:id="@+id/menu_screen_on_off"
android:title="@string/download.menu_screen_on"
diff --git a/app/src/main/res/menu/nowplaying_context.xml b/app/src/main/res/menu/nowplaying_context.xml
index 60d6288e..06f4afcb 100644
--- a/app/src/main/res/menu/nowplaying_context.xml
+++ b/app/src/main/res/menu/nowplaying_context.xml
@@ -26,7 +26,7 @@
<group android:id="@+id/hide_star">
<item
- android:id="@+id/menu_star"
+ android:id="@+id/song_menu_star"
android:title="@string/common.star"/>
</group>
@@ -44,7 +44,7 @@
<group android:id="@+id/server_1.8">
<item
- android:id="@+id/menu_add_playlist"
+ android:id="@+id/song_menu_add_playlist"
android:title="@string/menu.add_playlist"/>
</group>
</menu>
diff --git a/app/src/main/res/menu/nowplaying_context_offline.xml b/app/src/main/res/menu/nowplaying_context_offline.xml
index 5f8009ff..14c95ab6 100644
--- a/app/src/main/res/menu/nowplaying_context_offline.xml
+++ b/app/src/main/res/menu/nowplaying_context_offline.xml
@@ -22,7 +22,7 @@
<group android:id="@+id/hide_star">
<item
- android:id="@+id/menu_star"
+ android:id="@+id/song_menu_star"
android:title="@string/common.star"/>
</group>
</menu>
diff --git a/app/src/main/res/menu/nowplaying_offline.xml b/app/src/main/res/menu/nowplaying_offline.xml
index bba5ba00..d1f6f706 100644
--- a/app/src/main/res/menu/nowplaying_offline.xml
+++ b/app/src/main/res/menu/nowplaying_offline.xml
@@ -25,6 +25,11 @@
android:id="@+id/menu_equalizer"
android:title="@string/equalizer.label"
android:checkable="true"/>
+
+ <item
+ android:id="@+id/menu_batch_mode"
+ android:title="@string/download.batch_mode"
+ android:checkable="true"/>
<item
android:id="@+id/menu_screen_on_off"
diff --git a/app/src/main/res/menu/search.xml b/app/src/main/res/menu/search.xml
index e9377d68..b957a1e4 100644
--- a/app/src/main/res/menu/search.xml
+++ b/app/src/main/res/menu/search.xml
@@ -5,7 +5,8 @@
android:id="@+id/menu_global_search"
android:icon="?attr/search"
android:title="@string/menu.search"
- compat:showAsAction="ifRoom|withText"/>
+ compat:actionViewClass="android.support.v7.widget.SearchView"
+ compat:showAsAction="always|collapseActionView"/>
<item
android:id="@+id/menu_exit"
diff --git a/app/src/main/res/menu/select_album_context.xml b/app/src/main/res/menu/select_album_context.xml
index e4a901ac..0415da3c 100644
--- a/app/src/main/res/menu/select_album_context.xml
+++ b/app/src/main/res/menu/select_album_context.xml
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
-
<item
android:id="@+id/album_menu_info"
android:title="@string/common.info"/>
@@ -47,9 +46,11 @@
android:title="@string/menu.delete_cache"/>
</group>
- <item
- android:id="@+id/album_menu_show_artist"
- android:title="@string/menu.show_artist"/>
+ <group android:id="@+id/hide_show_artist">
+ <item
+ android:id="@+id/album_menu_show_artist"
+ android:title="@string/menu.show_artist"/>
+ </group>
<group android:id="@+id/hide_star">
<item
diff --git a/app/src/main/res/menu/select_album_context_offline.xml b/app/src/main/res/menu/select_album_context_offline.xml
index c10f2c62..37e2ae66 100644
--- a/app/src/main/res/menu/select_album_context_offline.xml
+++ b/app/src/main/res/menu/select_album_context_offline.xml
@@ -35,6 +35,12 @@
android:title="@string/menu.delete_cache"/>
</group>
+ <group android:id="@+id/hide_show_artist">
+ <item
+ android:id="@+id/album_menu_show_artist"
+ android:title="@string/menu.show_artist"/>
+ </group>
+
<group android:id="@+id/hide_star">
<item
android:id="@+id/album_menu_star"
diff --git a/app/src/main/res/menu/select_artist.xml b/app/src/main/res/menu/select_artist.xml
index 66ba37ba..e974c28e 100644
--- a/app/src/main/res/menu/select_artist.xml
+++ b/app/src/main/res/menu/select_artist.xml
@@ -1,16 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:compat="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/menu_global_search"
+ android:icon="?attr/search"
+ android:title="@string/menu.search"
+ compat:actionViewClass="android.support.v7.widget.SearchView"
+ compat:showAsAction="ifRoom|collapseActionView"/>
+
<item
android:id="@+id/menu_global_shuffle"
android:icon="?attr/shuffle"
android:title="@string/menu.shuffle"
- compat:showAsAction="always|withText"/>
-
- <item
- android:id="@+id/menu_global_search"
- android:icon="?attr/search"
- android:title="@string/menu.search"
compat:showAsAction="ifRoom|withText"/>
<group android:id="@+id/not_touchscreen">
diff --git a/app/src/main/res/menu/select_internet_radio_context.xml b/app/src/main/res/menu/select_internet_radio_context.xml
new file mode 100644
index 00000000..e739aec5
--- /dev/null
+++ b/app/src/main/res/menu/select_internet_radio_context.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/internet_radio_info"
+ android:title="@string/common.info"/>
+
+</menu>
diff --git a/app/src/main/res/menu/select_podcasts.xml b/app/src/main/res/menu/select_podcasts.xml
index 41ad62fa..25bb6188 100644
--- a/app/src/main/res/menu/select_podcasts.xml
+++ b/app/src/main/res/menu/select_podcasts.xml
@@ -5,13 +5,14 @@
android:id="@+id/menu_global_search"
android:icon="?attr/search"
android:title="@string/menu.search"
- compat:showAsAction="always|withText"/>
+ compat:actionViewClass="android.support.v7.widget.SearchView"
+ compat:showAsAction="ifRoom|collapseActionView"/>
<item
android:id="@+id/menu_add_podcast"
android:icon="?attr/add"
android:title="@string/menu.add_podcast"
- compat:showAsAction="always|withText"/>
+ compat:showAsAction="ifRoom|withText"/>
<group android:id="@+id/not_touchscreen">
<item
diff --git a/app/src/main/res/menu/select_song_context.xml b/app/src/main/res/menu/select_song_context.xml
index 34ea27a6..46eaaa38 100644
--- a/app/src/main/res/menu/select_song_context.xml
+++ b/app/src/main/res/menu/select_song_context.xml
@@ -1,11 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
-<menu xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:compat="http://schemas.android.com/apk/res-auto">
-
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/song_menu_info"
- android:title="@string/common.info"
- />
+ android:title="@string/common.info"/>
+
+
+ <group android:id="@+id/hide_show_artist">
+ <item
+ android:id="@+id/song_menu_show_album"
+ android:title="@string/download.menu_show_album"/>
+
+ <item
+ android:id="@+id/song_menu_show_artist"
+ android:title="@string/menu.show_artist"/>
+ </group>
+
+ <group android:id="@+id/hide_play_now">
+ <item
+ android:id="@+id/song_menu_play_now"
+ android:title="@string/common.play_now"/>
+ </group>
<group android:id="@+id/hide_play_next">
<item
diff --git a/app/src/main/res/menu/select_song_context_offline.xml b/app/src/main/res/menu/select_song_context_offline.xml
index 1c52e792..cc914563 100644
--- a/app/src/main/res/menu/select_song_context_offline.xml
+++ b/app/src/main/res/menu/select_song_context_offline.xml
@@ -1,11 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
-<menu xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:compat="http://schemas.android.com/apk/res-auto">
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/song_menu_info"
android:title="@string/common.info"/>
+ <group android:id="@+id/hide_show_artist">
+ <item
+ android:id="@+id/song_menu_show_album"
+ android:title="@string/download.menu_show_album"/>
+
+ <item
+ android:id="@+id/song_menu_show_artist"
+ android:title="@string/menu.show_artist"/>
+ </group>
+
+ <group android:id="@+id/hide_play_now">
+ <item
+ android:id="@+id/song_menu_play_now"
+ android:title="@string/common.play_now"/>
+ </group>
+
<group android:id="@+id/hide_play_next">
<item
android:id="@+id/song_menu_play_next"
diff --git a/app/src/main/res/menu/select_video_context.xml b/app/src/main/res/menu/select_video_context.xml
index 95576efc..51301722 100644
--- a/app/src/main/res/menu/select_video_context.xml
+++ b/app/src/main/res/menu/select_video_context.xml
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<menu xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:compat="http://schemas.android.com/apk/res-auto">
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/song_menu_info"
android:title="@string/common.info"/>
@@ -30,4 +29,10 @@
android:id="@+id/song_menu_delete"
android:title="@string/menu.delete_cache"/>
</group>
+
+ <group android:id="@+id/hide_star">
+ <item
+ android:id="@+id/song_menu_star"
+ android:title="@string/common.star"/>
+ </group>
</menu>
diff --git a/app/src/main/res/menu/similar_artists.xml b/app/src/main/res/menu/similar_artists.xml
index f6c30fb2..2557381e 100644
--- a/app/src/main/res/menu/similar_artists.xml
+++ b/app/src/main/res/menu/similar_artists.xml
@@ -13,8 +13,4 @@
android:icon="?attr/shuffle"
android:title="@string/menu.shuffle"
compat:showAsAction="ifRoom|withText"/>
-
- <item
- android:id="@+id/menu_show_missing"
- android:title="@string/menu.show_missing"/>
</menu> \ No newline at end of file
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 534a0f75..c43f3aea 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -28,8 +28,7 @@
<string name="button_bar.home">Übersicht</string>
<string name="button_bar.browse">Bibliothek</string>
- <string name="button_bar.search">Suchen</string>
- <string name="button_bar.playlists">Wiedergabeliste</string>
+ <string name="button_bar.playlists">Wiedergabeliste</string>
<string name="button_bar.now_playing">Aktuelle Wiedergabe</string>
<string name="button_bar.podcasts">Podcasts</string>
<string name="button_bar.bookmarks">Lesezeichen</string>
@@ -53,8 +52,7 @@
<br/>Im Optionsmenü deselektiere "Erste Ebene sind Künstler". Dann wird die erste Verzeichnisebene wie Künstlergruppen anstelle von Künstler behandelt.
]]>
</string>
- <string name="main.select_server">Wähle Server</string>
- <string name="main.shuffle">Zufallswiedergabe</string>
+ <string name="main.shuffle">Zufallswiedergabe</string>
<string name="main.offline">Gehe Offline</string>
<string name="main.online">Gehe Online</string>
<string name="main.settings">Einstellungen</string>
@@ -106,7 +104,6 @@
<string name="menu.rate">Setze Bewertung</string>
<string name="menu.top_tracks">Last.FM Top Medien</string>
<string name="menu.similar_artists">Ähnliche Künstler</string>
- <string name="menu.show_missing">Zeige fehlende</string>
<string name="menu.start_radio">Starte Radio</string>
<string name="menu.first_level_artist">Erste Ebene sind Künstler</string>
@@ -126,20 +123,13 @@
<string name="search.artists">Künstler</string>
<string name="search.albums">Alben</string>
<string name="search.songs">Lieder</string>
- <string name="search.more">Zeige mehr</string>
- <string name="progress.wait">Bitte warten...</string>
+ <string name="progress.wait">Bitte warten...</string>
<string name="progress.artist_info">Lade Informationen zum Künstler</string>
- <string name="music_library.label">Medienbibliothek</string>
- <string name="music_library.label_offline">Offline Medien</string>
-
- <string name="select_album.select">Alle auswählen</string>
- <string name="select_album.n_selected">%d Lieder ausgewählt.</string>
- <string name="select_album.more">Mehr</string>
- <string name="select_album.offline">Offline</string>
- <string name="select_album.searching">Suche...</string>
- <string name="select_album.no_sdcard">Fehler: Keine SD-Karte verfügbar.</string>
+ <string name="select_album.n_selected">%d Lieder ausgewählt.</string>
+ <string name="select_album.offline">Offline</string>
+ <string name="select_album.no_sdcard">Fehler: Keine SD-Karte verfügbar.</string>
<string name="select_album.no_network">Warnung: Kein Netzwerk verfügbar.</string>
<string name="select_album.no_room">Warnung: Es sind nur %s verfügbar</string>
<string name="select_album.not_licensed">Server ist nicht lizensiert. Testzeitraum läuft ab in %d Tagen.</string>
@@ -198,13 +188,9 @@
<string name="download.repeat_off">Keine Wiederholung</string>
<string name="download.repeat_all">Wiederhole alle</string>
<string name="download.repeat_single">Aktuelles Lied wiederholen</string>
- <string name="download.jukebox_on">Fernbedienung aktiviert. Musik wird auf dem Computer abgespielt.</string>
- <string name="download.jukebox_off">Fernbedienung deaktiviert. Musik wird auf dem Telefon abgespielt.</string>
- <string name="download.jukebox_volume">Lautstärke</string>
- <string name="download.jukebox_server_too_old">Fernbedienung wird nicht unterstützt. Aktualisierung des Subsonic-Servers notwendig.</string>
+ <string name="download.jukebox_server_too_old">Fernbedienung wird nicht unterstützt. Aktualisierung des Subsonic-Servers notwendig.</string>
<string name="download.jukebox_offline">Fernbedienung im Offline-Modus nicht verfügbar.</string>
<string name="download.jukebox_not_authorized">Fernbedienung ist nicht erlaubt. Bitte aktivieren Sie den Jukebox-Modus unter <b>Nutzer &gt; Einstellungen</b> auf Ihrem Subsonic-Server.</string>
- <string name="download.timer_length">Timer:</string>
<string name="download.start_timer">Starte Timer</string>
<string name="download.need_download">Video muss zuerst heruntergeladen werden</string>
<string name="download.no_streaming_player">Stream kann nicht wiedergegeben werden.</string>
@@ -411,8 +397,6 @@
<string name="settings.override_system_language">In Englisch anzeigen</string>
<string name="settings.override_system_language_summary">Verwende Englisch anstatt Deutsch für DSub. Benötigt einen Neustart der App.</string>
<string name="settings.drawer_items_title">Seitenmenü</string>
- <string name="settings.play_now_after">Jetzt wiedergeben bis zum Listenende</string>
- <string name="settings.play_now_after_summary">\"Jetzt wiedergeben\" im Kontextmenü spielt das ausgewählte Lied und alle in der Liste nachfolgenden Lieder ab (wie in der Web-Schnittstelle des Subsonic-Server)</string>
<string name="settings.large_album_art">Große Cover anzeigen</string>
<string name="settings.large_album_art_summary">Verwende große Cover zur Anzeige der Alben anstatt einer Liste</string>
<string name="settings.admin_enabled">Administration aktiviert</string>
@@ -439,15 +423,6 @@
<string name="shuffle.genre">Genre:</string>
<string name="shuffle.pick_genre">Wähle ein Genre</string>
- <string name="share.info">Eigentümer: %1$s
- \nBeschreibung: %2$s
- \nURL: %3$s
- \nErzeugt: %4$s
- \nZuletzt besucht: %5$s
- \nAblauf: %6$s
- \nBesuchszähler: %7$s
-
- </string>
<string name="share.expires">Ablauf: %s</string>
<string name="share.expires_never">nie</string>
<string name="share.deleted">Lösche Freigabe %s</string>
@@ -499,9 +474,7 @@
<string name="music_service.retry">Ein Netzwerkfehler ist aufgetreten. Versuch %1$d von %2$d.</string>
- <string name="background_task.wait">Bitte warten...</string>
- <string name="background_task.loading">Lade.</string>
- <string name="background_task.no_network">Diese Programm benötigt Netzwerkzugriff. Bitte schalten Sie Wi-Fi oder Mobiles Netzwerk ein.</string>
+ <string name="background_task.no_network">Diese Programm benötigt Netzwerkzugriff. Bitte schalten Sie Wi-Fi oder Mobiles Netzwerk ein.</string>
<string name="background_task.network_error">Ein Netzwerkfehler ist aufgetreten. Bitte prüfen Sie die Serveradresse oder versuchen Sie es später nochmal.</string>
<string name="background_task.not_found">Quelle wurde nicht gefunden. Bitte prüfen Sie die Serveradresse.</string>
<string name="background_task.parse_error">Ein Fehler ist bei der Kommunikation mit dem Server aufgetreten. Bitte prüfen Sie die Serveradresse und stellen Sie sicher, das Sie der Server mit einem Webbrowser erreichen.</string>
@@ -516,8 +489,7 @@
<string name="parser.server_error">Serverfehler: %s</string>
<string name="parser.scan_count">%d Einträge gefunden</string>
- <string name="select_artist.refresh">Aktualisieren</string>
- <string name="select_artist.folder">Wähle Ordner</string>
+ <string name="select_artist.folder">Wähle Ordner</string>
<string name="select_artist.all_folders">Alle Ordner</string>
<string name="equalizer.label">Equalizer</string>
@@ -537,9 +509,7 @@
<string name="changelog_ok_button">OK</string>
<string name="changelog_show_full">Mehr…</string>
- <string name="chat.send_a_message">Nachricht senden</string>
-
- <string name="changelog_version_format" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">Version <xliff:g id="version_name">%s</xliff:g></string>
+ <string name="chat.send_a_message">Nachricht senden</string>
<string name="tasker.start_playing">Starte Wiedergabe</string>
<string name="tasker.start_playing_shuffled">Starte Zufallswiedergabe</string>
@@ -642,6 +612,7 @@
<string name="settings.keep_played_count_two">2 abgespielte Lieder behalten</string>
<string name="settings.keep_played_count_title">Abgespielte Lieder behalten</string>
<string name="details.updated">Aktualisiert</string>
+ <string name="details.last_played">Zuletzt gespielt</string>
<string name="details.position">Position</string>
<string name="details.song">Lied</string>
<string name="button_bar.offline">Offline</string>
@@ -656,5 +627,25 @@
<string name="settings.shuffle_by_album.false">Alle Titel mischen</string>
<string name="settings.shuffle_by_album.true">Albenreihenfolge mischen</string>
<string name="settings.shuffle_by_album">Alben mischen</string>
+ <string name="common.never">Nie</string>
+ <string name="details.starred">Favorit</string>
+ <string name="download.thumbs_up">Gefällt mir</string>
+ <string name="download.thumbs_down">Gefällt mir nicht</string>
+ <string name="select_podcasts.channels">Podcastkanäle</string>
+ <string name="settings.cache_location_external">Externer Speicher</string>
+ <string name="settings.cache_location_internal">Interner Speicher</string>
+ <string name="admin.change_password_current_label">Aktuelles Passwort:</string>
+ <string name="admin.musicFolders">Musikordner</string>
+ <string name="admin.permissions">Berechtigungen</string>
+ <string name="details.played_count">Abspielzähler</string>
+ <string name="details.expiration">Ablauf</string>
+ <string name="download.playerstate_playing_artist_radio">Künstlerradio</string>
+ <string name="main.songs_top_played">Am meisten gespielt</string>
+ <string name="main.songs_frequent">@string/main.albums_frequent</string>
+ <string name="main.songs_newest">@string/main.albums_newest</string>
+ <string name="main.songs_recent">@string/main.albums_recent</string>
+ <string name="menu.similar_artists.missing">Fehlende Künstler</string>
+ <string name="settings.casting_stream_original">Original streamen</string>
+ <string name="settings.casting_stream_original_summary">Zum streamen möglichst das Originalformat verwenden.</string>
</resources>
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index f56e939e..a5273a50 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -28,8 +28,7 @@
<string name="button_bar.home">Inicio</string>
<string name="button_bar.browse">Biblioteca</string>
- <string name="button_bar.search">Buscar</string>
- <string name="button_bar.playlists">Listas de reproducción</string>
+ <string name="button_bar.playlists">Listas de reproducción</string>
<string name="button_bar.now_playing">Ahora suena</string>
<string name="button_bar.podcasts">Podcasts</string>
<string name="button_bar.bookmarks">Marcadores</string>
@@ -41,8 +40,7 @@
<string name="main.welcome_title">Bienvenido!</string>
<string name="main.welcome_text">Bienvenido a DSub! Ahora la aplicación está configurada para usar el servidor de demostración de Subsonic. Cuando configures tu servidor personal (disponible en <b>subsonic.org</b>), accede a <b>Preferencias</b> y cambia la configuración para conectarte.</string>
<string name="main.about_title">Acerca de DSub</string>
- <string name="main.select_server">Seleccionar servidor</string>
- <string name="main.shuffle">Reproducción aleatoria</string>
+ <string name="main.shuffle">Reproducción aleatoria</string>
<string name="main.offline">Modo Offline</string>
<string name="main.online">Modo Online</string>
<string name="main.settings">Preferencias</string>
@@ -96,7 +94,6 @@
<string name="menu.rate">Establecer valoración</string>
<string name="menu.top_tracks">Tp Tracks de Last.FM</string>
<string name="menu.similar_artists">Artistas similares</string>
- <string name="menu.show_missing">Mostrar los que faltan</string>
<string name="menu.start_radio">Iniciar radio</string>
<string name="playlist.label">Listas de reproducción</string>
@@ -115,19 +112,12 @@
<string name="search.artists">Artista</string>
<string name="search.albums">Disco</string>
<string name="search.songs">Canción</string>
- <string name="search.more">Mostrar más</string>
- <string name="progress.wait">Espere por favor...</string>
+ <string name="progress.wait">Espere por favor...</string>
- <string name="music_library.label">Biblioteca de medios</string>
- <string name="music_library.label_offline">Archivos Offline</string>
-
- <string name="select_album.select">Seleccionar todo</string>
- <string name="select_album.n_selected">Seleccionadas %d canciones</string>
- <string name="select_album.more">Más</string>
- <string name="select_album.offline">Offline</string>
- <string name="select_album.searching">Buscando...</string>
- <string name="select_album.no_sdcard">Error: No hay tarjeta SD disponible</string>
+ <string name="select_album.n_selected">Seleccionadas %d canciones</string>
+ <string name="select_album.offline">Offline</string>
+ <string name="select_album.no_sdcard">Error: No hay tarjeta SD disponible</string>
<string name="select_album.no_network">Aviso: No hay red disponible</string>
<string name="select_album.not_licensed">Servidor sin licencia. Quedan %d días de prueba</string>
<string name="select_album.donate_dialog_message">Consigue descargas ilimitadas haciendo una donación a Subsonic</string>
@@ -183,13 +173,9 @@
<string name="download.repeat_off">Repetir off</string>
<string name="download.repeat_all">Repetir todo</string>
<string name="download.repeat_single">Repetir canción</string>
- <string name="download.jukebox_on">Control remoto encendido. La música se está reproduciendo en el ordenador.</string>
- <string name="download.jukebox_off">Control remoto apagado. La música se está reproduciendo en el dispositivo móvil.</string>
- <string name="download.jukebox_volume">Volumen remoto</string>
- <string name="download.jukebox_server_too_old">Control remoto no soportado. Por favor, actualice su servidor Subsonic.</string>
+ <string name="download.jukebox_server_too_old">Control remoto no soportado. Por favor, actualice su servidor Subsonic.</string>
<string name="download.jukebox_offline">Control remoto no disponible en modo offline.</string>
<string name="download.jukebox_not_authorized">Control remoto no permitido. Por favor, active el modo jukebox en <b>Users &gt; Settings</b> en su servidor Subsonic.</string>
- <string name="download.timer_length">Temporizador</string>
<string name="download.start_timer">Iniciar temporizador</string>
<string name="download.need_download">El vídeo ha de ser descargado antes</string>
<string name="download.no_streaming_player">Ningún reproductor puede reproducir este stream</string>
@@ -400,8 +386,6 @@
<string name="settings.override_system_language">Sobreescribir idioma del sistema</string>
<string name="settings.override_system_language_summary">Mostrar la aplicación en inglés aún teniendo disponible DSub en el idioma del sistema. Probablemente necesite borrar la aplicación de la memoria para efectuar los cambios.</string>
<string name="settings.drawer_items_title">Pestañas</string>
- <string name="settings.play_now_after">Reproducir ahora - Después</string>
- <string name="settings.play_now_after_summary">Pulsar "Reproducir ahora" desde el menú contextual actúa como la interfaz web de Subsonic, reproduciendo todos los items a partir del seleccionado</string>
<string name="settings.large_album_art">Carátulas grandes</string>
<string name="settings.large_album_art_summary">Mostrar los discos con carátulas grandes en vez de en lista</string>
<string name="settings.admin_enabled">Admin Habilitado</string>
@@ -420,14 +404,6 @@
<string name="settings.open_to_tab">Abrir en pestaña</string>
<string name="settings.open_to_tab_summary">Abrir directamente a esta pestaña</string>
- <string name="share.info">Dueño: %1$s
- \nDescripción: %2$s
- \nURL: %3$s
- \nCreado: %4$s
- \nÚltima visita: %5$s
- \nExpira: %6$s
- \nNúmero de visitas: %7$s
- </string>
<string name="share.expires">Expira: %s</string>
<string name="share.expires_never">Nunca expira</string>
<string name="share.deleted">Compartición eliminada %s</string>
@@ -490,9 +466,7 @@
<string name="music_service.retry">Error de red. Reintentando %1$d de %2$d.</string>
- <string name="background_task.wait">Por favor, espere...</string>
- <string name="background_task.loading">Cargando.</string>
- <string name="background_task.no_network">Este programa requiere de acceso a la red. Encienda el Wi-Fi o la conexión de datos móviles.</string>
+ <string name="background_task.no_network">Este programa requiere de acceso a la red. Encienda el Wi-Fi o la conexión de datos móviles.</string>
<string name="background_task.network_error">Error de red. Por favor, compruebe la dirección del servidor o inténtelo más tarde.</string>
<string name="background_task.not_found">Recurso no encontrado. Por favor, compruebe la dirección del servidor.</string>
<string name="background_task.parse_error">Respuesta desconocida. Por favor, compruebe la dirección del servidor.</string>
@@ -506,8 +480,7 @@
<string name="parser.artist_count">Recibidos %d artistas.</string>
<string name="parser.scan_count">Escaneados %d entradas</string>
- <string name="select_artist.refresh">Actualizar</string>
- <string name="select_artist.folder">Seleccionar carpeta</string>
+ <string name="select_artist.folder">Seleccionar carpeta</string>
<string name="select_artist.all_folders">Todas las carpetas</string>
<string name="equalizer.bass_booster">Potenciar bajos</string>
@@ -537,9 +510,7 @@
<string name="changelog_ok_button">OK</string>
<string name="changelog_show_full">Más…</string>
- <string name="chat.send_a_message">Enviar un mensaje</string>
-
- <string name="changelog_version_format" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">Version <xliff:g id="version_name">%s</xliff:g></string>
+ <string name="chat.send_a_message">Enviar un mensaje</string>
<string name="tasker.start_playing">Comenzar reproduciendo</string>
<string name="tasker.start_playing_title">Tasker -> Encender DSub</string>
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 4674ca32..f384060a 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -7,10 +7,10 @@
<string name="common.cancel">Annuler</string>
<string name="common.play_now">Jouer</string>
<string name="common.play_shuffled">Jouer au hasard</string>
- <string name="common.play_next">Suivant</string>
- <string name="common.play_last">Précédent</string>
+ <string name="common.play_next">Jouer juste après</string>
+ <string name="common.play_last">Jouer en fin de liste</string>
<string name="common.download">Mettre en cache</string>
- <string name="common.pin">Mettre en cache Permanent</string>
+ <string name="common.pin">Mettre en cache permanent</string>
<string name="common.delete">Supprimer</string>
<string name="common.star">Favori</string>
<string name="common.unstar">Supp. favori</string>
@@ -19,20 +19,19 @@
<string name="common.comment">Commentaire</string>
<string name="common.public">Publique</string>
<string name="common.play_external">Jouer Video</string>
- <string name="common.stream_external">Stream Video</string>
+ <string name="common.stream_external">Stream Vidéo</string>
<string name="common.confirm">Confirmer</string>
<string name="common.confirm_message">Voulez-vous %1$s %2$s ?</string>
<string name="common.confirm_message_cache">cache</string>
- <string name="common.empty">Aucune donnée</string>
+ <string name="common.empty">Aucune données</string>
<string name="common.warning">Avertissement</string>
<string name="button_bar.home">Accueil</string>
<string name="button_bar.browse">Bibliothèque</string>
- <string name="button_bar.search">Recherche</string>
- <string name="button_bar.playlists">Playlists</string>
+ <string name="button_bar.playlists">Playlists</string>
<string name="button_bar.now_playing">Lecture en cours</string>
<string name="button_bar.podcasts">Podcasts</string>
- <string name="button_bar.bookmarks">Favoris</string>
+ <string name="button_bar.bookmarks">Signets</string>
<string name="button_bar.shares">Partages</string>
<string name="button_bar.chat">Chat</string>
<string name="button_bar.admin">Admin</string>
@@ -40,7 +39,7 @@
<string name="main.welcome_title">Bienvenue !</string>
<string name="main.welcome_text">Bienvenue dans DSub ! L\'application est actuellement configurée pour se connecter au serveur de démo Subsonic (<b>demo.subsonic.org</b>). Vous pouvez configurer votre propre serveur dans les paramètres. Choisir <b>Paramètres</b> et mettre à jour la configuration pour vous y connecter.</string>
- <string name="main.about_title">A propos de DSub</string>
+ <string name="main.about_title">À propos de DSub</string>
<string name="main.faq_title">FAQ</string>
<string name="main.faq_text">
<![CDATA[
@@ -50,14 +49,13 @@
<br/>Assurez-vous de ne pas utiliser un certificat auto-signé, Chromecast les rejette systématiquement.
]]>
</string>
- <string name="main.select_server">Choisir un serveur</string>
- <string name="main.shuffle">Jouer au hasard</string>
+ <string name="main.shuffle">Jouer au hasard</string>
<string name="main.offline">Déconnecter</string>
<string name="main.online">Connecter</string>
<string name="main.settings">Paramètres</string>
<string name="main.albums_title">Albums</string>
<string name="main.albums_newest">Ajoutés récemments</string>
- <string name="main.albums_recent">Joués récemment</string>
+ <string name="main.albums_recent">Joués récemments</string>
<string name="main.albums_frequent">Les plus joués</string>
<string name="main.albums_highest">Les mieux notés</string>
<string name="main.albums_starred">Favoris</string>
@@ -66,7 +64,7 @@
<string name="main.albums_year">Par décennies</string>
<string name="main.songs_genres">@string/main.albums_genres</string>
<string name="main.back_confirm">Presser retour à nouveau pour quitter</string>
- <string name="main.scan_complete">Completed scan of Server</string>
+ <string name="main.scan_complete">Analyse du server terminée</string>
<string name="menu.search">Recherche</string>
<string name="menu.shuffle">Hasard</string>
@@ -77,31 +75,31 @@
<string name="menu.settings">Paramètres</string>
<string name="menu.help">Aide</string>
- <string name="menu.about">A propos</string>
+ <string name="menu.about">À propos</string>
<string name="menu.add_playlist">Ajouter à la playlist</string>
<string name="menu.remove_playlist">Supprimer de la playlist</string>
<string name="menu.deleted_playlist">Supprimer la playlist %s</string>
- <string name="menu.deleted_playlist_error">Echec de la suppression de la playlist %s</string>
+ <string name="menu.deleted_playlist_error">Échec de la suppression de la playlist %s</string>
<string name="menu.log">Envoyer le journal</string>
<string name="menu.set_timer">Ajuster le minuteur</string>
<string name="menu.check_podcasts">Vérifier les nouveaux podcasts</string>
<string name="menu.add_podcast">Ajouter une chaîne</string>
<string name="menu.keep_synced">Synchronisation automatique</string>
<string name="menu.stop_sync">Arrêter la synchro.</string>
- <string name="menu.show_all">Afficher tous les media</string>
+ <string name="menu.show_all">Afficher tous les médias</string>
<string name="menu.show_artist">Afficher l\'artiste</string>
<string name="menu.share">Partager</string>
<string name="menu.delete_cache">Supprimer du cache</string>
<string name="menu.cast">Diffuser vers appareil</string>
<string name="menu.faq">FAQ</string>
- <string name="menu.add_user">Ajouter utilisateur</string>
- <string name="menu.rescan">Relire le server</string>
+ <string name="menu.add_user">Ajouter un utilisateur</string>
+ <string name="menu.rescan">Relire le serveur</string>
<string name="menu.rate">Noter</string>
<string name="playlist.label">Playlists</string>
- <string name="playlist.update_info">Mise à jour informations</string>
+ <string name="playlist.update_info">Mise à jour des informations</string>
<string name="playlist.updated_info">Informations de la playlist %s mises à jour</string>
- <string name="playlist.updated_info_error">Echec de la mise à jour des informations de la playlist %s</string>
+ <string name="playlist.updated_info_error">Échec de la mise à jour des informations de la playlist %s</string>
<string name="playlist.overwrite">Remplacer la playlist existante</string>
<string name="playlist.add_to">Ajouter à la playlist</string>
<string name="playlist.create_new">Créer une nouvelle</string>
@@ -114,21 +112,14 @@
<string name="search.artists">Artistes</string>
<string name="search.albums">Albums</string>
<string name="search.songs">Chansons</string>
- <string name="search.more">Afficher plus</string>
- <string name="progress.wait">Patientez…</string>
+ <string name="progress.wait">Patientez…</string>
- <string name="music_library.label">Bibliothèque</string>
- <string name="music_library.label_offline">Média mode déconnecté</string>
-
- <string name="select_album.select">Tout sélectionner</string>
- <string name="select_album.n_selected">%d pistes sélectionnées.</string>
- <string name="select_album.more">Plus</string>
- <string name="select_album.offline">Déconnecté</string>
- <string name="select_album.searching">Recherche en cours...</string>
- <string name="select_album.no_sdcard">Erreur : Aucune carte SD card disponible.</string>
+ <string name="select_album.n_selected">%d pistes sélectionnées.</string>
+ <string name="select_album.offline">Déconnecté</string>
+ <string name="select_album.no_sdcard">Erreur : Aucune carte SD card disponible.</string>
<string name="select_album.no_network">Problème : Aucun réseau disponible.</string>
- <string name="select_album.not_licensed">Serveur sans licence valide. %d jours restant.</string>
+ <string name="select_album.not_licensed">Serveur sans licence valide. %d jours restants.</string>
<string name="select_album.donate_dialog_message">Téléchargement illimité en supportant Subsonic.</string>
<string name="select_album.donate_dialog_now">Maintenant</string>
<string name="select_album.donate_dialog_later">Plus tard</string>
@@ -147,19 +138,19 @@
<string name="select_genre.songs">%d chansons</string>
<string name="select_genre.albums">%d albums</string>
- <string name="select_podcasts.error">Une erreur est survenue avec ce podcast pendant le chargement. Le serveur doit d\'abord le télécharger.</string>
- <string name="select_podcasts.skipped">Ce podcast n\'a pas été chargé sur le serveur. Le serveur doit d\'abord le télécharger.</string>
- <string name="select_podcasts.initializing">Le chargement du podcast a commencer sur le serveur. Recharger SVP dans quelques instants.</string>
+ <string name="select_podcasts.error">Une erreur est survenue avec ce podcast pendant le chargement. Le serveur doit d\'abord le télécharger.</string>
+ <string name="select_podcasts.skipped">Ce podcast n\'a pas été chargé sur le serveur. Le serveur doit d\'abord le télécharger.</string>
+ <string name="select_podcasts.initializing">Le chargement du podcast a commencé sur le serveur. Recharger SVP dans quelques instants.</string>
<string name="select_podcasts.server_download">Télécharger sur le serveur</string>
<string name="select_podcasts.server_delete">Supprimer du serveur</string>
<string name="select_podcasts.downloading">Téléchargement %s sur le serveur</string>
- <string name="select_podcasts.refreshing">Le serveur recherche les mises à jour de podcasts</string>
+ <string name="select_podcasts.refreshing">Le serveur recherche les mises à jour des podcasts</string>
<string name="select_podcasts.deleted">Podcast supprimé %s</string>
<string name="select_podcasts.deleted_error">Erreur lors de la suppression du podcast %s</string>
<string name="select_podcasts.add_url">URL :</string>
<string name="select_podcasts.created_error">Erreur lors de l\'ajout du podcast</string>
<string name="select_podcasts.invalid_podcast_channel">Podcast invalide : %s</string>
- <string name="select_podcasts.delete">Supprimer podcast</string>
+ <string name="select_podcasts.delete">Supprimer le podcast</string>
<string name="download.empty">La playlist est vide</string>
<string name="download.shuffle_loading">Chargement en cours liste au hasard...</string>
@@ -169,7 +160,7 @@
<string name="download.menu_show_album">Afficher l\'album</string>
<string name="download.menu_lyrics">Paroles</string>
<string name="download.menu_remove_all">Enlever tout</string>
- <string name="download.menu_screen_on">Ecran actif</string>
+ <string name="download.menu_screen_on">Écran actif</string>
<string name="download.menu_shuffle">Hasard</string>
<string name="download.menu_toggle">Basculer</string>
<string name="download.menu_save">Enregistrer la playlist</string>
@@ -183,13 +174,9 @@
<string name="download.repeat_off">Répéter inactif</string>
<string name="download.repeat_all">Répéter tout</string>
<string name="download.repeat_single">Répéter titre</string>
- <string name="download.jukebox_on">Télécommande activée. La musique est diffusée sur l\'ordinateur.</string>
- <string name="download.jukebox_off">Télécommande désactivée. La musique est diffusée sur le mobile.</string>
- <string name="download.jukebox_volume">Volume distant</string>
- <string name="download.jukebox_server_too_old">Télécommande non supportée. Mettre à jour le serveur Subsonic.</string>
+ <string name="download.jukebox_server_too_old">Télécommande non supportée. Mettre à jour le serveur Subsonic.</string>
<string name="download.jukebox_offline">La télécommande n\'est pas disponible en mode déconnecté.</string>
<string name="download.jukebox_not_authorized">Mode télécommande non autorisée. Activer le mode jukebox.<b>Users &gt; Settings</b> on your Subsonic server.</string>
- <string name="download.timer_length">Minuteur :</string>
<string name="download.start_timer">Démarrer le minuteur</string>
<string name="download.need_download">La vidéo doit d\'abord être téléchargée</string>
<string name="download.no_streaming_player">Aucun lecteur ne peut afficher ce flux</string>
@@ -200,8 +187,8 @@
<string name="download.downloading_summary">En cours : %1$s</string>
<string name="download.downloading_summary_expanded">En cours : %1$s
\nTaille estimée : %2$s</string>
- <string name="download.failed_to_load">Echec du chargement</string>
- <string name="download.save_bookmark_failed">Echec de la création du favori</string>
+ <string name="download.failed_to_load">Échec du chargement</string>
+ <string name="download.save_bookmark_failed">Échec de la création du favori</string>
<string name="sync.new_podcasts">Nouveaux podcasts disponibles</string>
<string name="sync.new_playlists">Nouveaux titres dans les playlists</string>
@@ -210,18 +197,18 @@
<string name="starring_content_starred">Noté \&quot;%s\&quot;</string>
<string name="starring_content_unstarred">Dévalués \&quot;%s\&quot;</string>
- <string name="starring_content_error">Echec de la mise à jour \&quot;%s\&quot;, Réessayer plus tard.</string>
+ <string name="starring_content_error">Échec de la mise à jour \&quot;%s\&quot;, réessayer plus tard.</string>
- <string name="playlist_error">Echec de la récupération des playlists</string>
+ <string name="playlist_error">Échec de la récupération des playlists</string>
<string name="updated_playlist">Titre %1$s ajouté à \&quot;%2$s\&quot;</string>
- <string name="updated_playlist_error">Echec de la mise à jour \&quot;%s\&quot;, réessayer plus tard.</string>
+ <string name="updated_playlist_error">Échec de la mise à jour \&quot;%s\&quot;, réessayer plus tard.</string>
<string name="removed_playlist">Titre %1$s retiré de \&quot;%2$s\&quot;</string>
- <string name="bookmark.delete">Supprimer le favori</string>
- <string name="bookmark.delete_title">Dupprimer le favori pour</string>
- <string name="bookmark.deleted">Favori pour \&quot;%s\&quot; supprimé</string>
- <string name="bookmark.deleted_error">Echec de la suppression du favori pour \&quot;%s\&quot;</string>
- <string name="bookmark.details_title">Détails du favori</string>
+ <string name="bookmark.delete">Supprimer le signet</string>
+ <string name="bookmark.delete_title">Supprimer le signet pour</string>
+ <string name="bookmark.deleted">Signet pour \&quot;%s\&quot; supprimé</string>
+ <string name="bookmark.deleted_error">Échec de la suppression du signet pour \&quot;%s\&quot;</string>
+ <string name="bookmark.details_title">Détails du signet</string>
<string name="bookmark.details">Titre : %1$s
\nPosition : %2$s
\nCréé le : %3$s
@@ -230,13 +217,13 @@
<string name="bookmark.resume_title">Reprendre la lecture ?</string>
<string name="bookmark.resume">Reprendre la lecture de \'%1$s\' depuis %2$s</string>
<string name="bookmark.action_resume">Reprendre</string>
- <string name="bookmark.action_start_over">Start Over</string>
+ <string name="bookmark.action_start_over">Relire depuis le début</string>
<string name="rating.title">Noter \"%s\"</string>
<string name="rating.set_rating">Note attribuée à \"%s\"</string>
<string name="rating.set_rating_failed">Echec de l\'attribution de la note à \"%s\"</string>
<string name="rating.remove_rating">Note supprimée pour \"%s\"</string>
- <string name="rating.remove_rating_failed">Echec de la suppression de la note pour \"%s\"</string>
+ <string name="rating.remove_rating_failed">Échec de la suppression de la note pour \"%s\"</string>
<string name="song_details.error">Erreur</string>
<string name="song_details.skipped">Ignoré</string>
@@ -277,9 +264,9 @@
<string name="settings.invalid_username">Saisir un nom d\'utilisateur valide (espaces interdits).</string>
<string name="settings.appearance_title">Apparence</string>
<string name="settings.theme_title">Thème</string>
- <string name="settings.theme_light">Light</string>
- <string name="settings.theme_dark">Dark</string>
- <string name="settings.theme_black">Black</string>
+ <string name="settings.theme_light">Clair</string>
+ <string name="settings.theme_dark">Sombre</string>
+ <string name="settings.theme_black">Noir</string>
<string name="settings.theme_holo">Holo</string>
<string name="settings.theme_fullscreen">Plein écran</string>
<string name="settings.theme_fullscreen_summary">Cacher autant d\'élément graphique que possible</string>
@@ -313,8 +300,8 @@
<string name="settings.max_video_bitrate_3000">3000 Kbps</string>
<string name="settings.max_video_bitrate_5000">5000 Kbps</string>
<string name="settings.max_bitrate_unlimited">Illimité</string>
- <string name="settings.wifi_required_title">Streaming en Wifi uniquement</string>
- <string name="settings.wifi_required_summary">Ne lire les média qu\'avec une connexion Wifi</string>
+ <string name="settings.wifi_required_title">Streaming en wifi uniquement</string>
+ <string name="settings.wifi_required_summary">Ne lire les média qu\'avec une connexion wifi</string>
<string name="settings.network_timeout_title">Délai d\'attente réseau (timeout)</string>
<string name="settings.network_timeout_10000">10 secondes</string>
<string name="settings.network_timeout_15000">15 secondes</string>
@@ -344,7 +331,7 @@
<string name="settings.playlist_random_size_title">Taille de la liste de lecture aléatoire</string>
<string name="settings.sleep_timer_title">Temporisateur</string>
<string name="settings.sleep_timer_duration_title">Durée temporisation</string>
- <string name="settings.sleep_timer_off">Eteindre</string>
+ <string name="settings.sleep_timer_off">Éteindre</string>
<string name="settings.sleep_timer_on">Allumer</string>
<string name="settings.sleep_timer_always_on">Toujours en fonctionnement</string>
<string name="settings.temp_loss_title">Perte temporaire de focus</string>
@@ -374,8 +361,8 @@
<string name="settings.hide_widget_summary">Cacher le widget après avoir quitté l\'application</string>
<string name="settings.podcasts_enabled">Podcasts autorisés</string>
<string name="settings.podcasts_enabled_summary">Afficher ou non l\'accès aux podcasts</string>
- <string name="settings.bookmarks_enabled">Favoris autorisés</string>
- <string name="settings.bookmarks_enabled_summary">Afficher ou non l\'accès aux favoris</string>
+ <string name="settings.bookmarks_enabled">Signets autorisés</string>
+ <string name="settings.bookmarks_enabled_summary">Afficher ou non l\'accès aux signets</string>
<string name="settings.shares_enabled">Partages autorisés</string>
<string name="settings.shares_enabled_summary">Afficher ou non l\'accès aux partages</string>
<string name="settings.sync_title">Sync</string>
@@ -411,36 +398,25 @@
<string name="settings.override_system_language">Ne pas utiliser la langue du système</string>
<string name="settings.override_system_language_summary">Afficher DSub en Anglais même si une traduction existe pour la langue système. Peut nécessiter un vidage du cache de l\'application.</string>
<string name="settings.drawer_items_title">Entrées de menu</string>
- <string name="settings.play_now_after">Jouer maintenant - Plus tard</string>
- <string name="settings.play_now_after_summary">Play Now context menu for a song plays everything after selected item (like the Subsonic web GUI)</string>
<string name="settings.large_album_art">Pochettes larges</string>
<string name="settings.large_album_art_summary">Afficher les pochettes en grand plutôt qu\'en liste.</string>
<string name="settings.admin_enabled">Administration</string>
<string name="settings.admin_enabled_summary">Afficher ou non l\'accès aux outils d\'administration</string>
- <string name="shuffle.title">Shuffle By</string>
+ <string name="shuffle.title">Mélanger par</string>
<string name="shuffle.startYear">Année début :</string>
<string name="shuffle.endYear">Année fin :</string>
<string name="shuffle.genre">Genre :</string>
<string name="shuffle.pick_genre">Choisir un genre</string>
- <string name="share.info">Propriétaire : %1$s
- \nDescription: %2$s
- \nURL: %3$s
- \nCréation : %4$s
- \nDernière visite : %5$s
- \nExpiration : %6$s
- \nNombre de visites : %7$s
-
- </string>
<string name="share.expires">Expiration : %s</string>
<string name="share.expires_never">N\'expire jamais</string>
<string name="share.deleted">Supprimer le partage %s</string>
- <string name="share.deleted_error">Echec de la suppression du partage %s</string>
+ <string name="share.deleted_error">Échec de la suppression du partage %s</string>
<string name="share.no_expiration">Pas d\'expiration</string>
<string name="share.expiration">Expiration :</string>
<string name="share.updated_info">Informations de partage mises à jour pour %s</string>
- <string name="share.updated_info_error">Echec de la mise à jour des informations de partage pour %s</string>
+ <string name="share.updated_info_error">Échec de la mise à jour des informations de partage pour %s</string>
<string name="share.via">Partager via</string>
<string name="share.delete">Supprimer le partage</string>
@@ -452,20 +428,20 @@
<string name="admin.change_username_invalid">Saisir un nom d\'utilisateur valide</string>
<string name="admin.update_permissions">Mettre à jour les autorisations</string>
<string name="admin.update_permissions_success">Autorisation mises à jour pour %1$s</string>
- <string name="admin.update_permissions_error">Echec lors de lamise à jour des autorisations de %1$s</string>
- <string name="admin.change_email">Modifier Email</string>
+ <string name="admin.update_permissions_error">Échec lors de la mise à jour des autorisations de %1$s</string>
+ <string name="admin.change_email">Modifier l\'email</string>
<string name="admin.change_email_success">Email remplacé pour %1$s</string>
- <string name="admin.change_email_error">Echec lors du remplacement de l\'Email de %1$s</string>
- <string name="admin.change_email_label">Nouvel Email :</string>
- <string name="admin.change_email_invalid">Saisir un Email valide</string>
+ <string name="admin.change_email_error">Échec lors du remplacement de l\'email de %1$s</string>
+ <string name="admin.change_email_label">Nouvel email :</string>
+ <string name="admin.change_email_invalid">Saisir un email valide</string>
<string name="admin.change_password">Modifier le mot de passe</string>
<string name="admin.change_password_success">Mot de passe modifié pour %1$s</string>
- <string name="admin.change_password_error">Echec du remplacement du mot de passe pour %1$s</string>
+ <string name="admin.change_password_error">Échec du remplacement du mot de passe pour %1$s</string>
<string name="admin.change_password_label">Nouveau mot de passe :</string>
<string name="admin.change_password_invalid">Saisir un mot de passe valide</string>
<string name="admin.delete_user">Supprimer l\'utilisateur</string>
<string name="admin.delete_user_success">Suppression effectuée %1$s</string>
- <string name="admin.delete_user_error">Echec de la suppression %1$s</string>
+ <string name="admin.delete_user_error">Échec de la suppression %1$s</string>
<string name="admin.confirm_password">Confirmer le mot de passe</string>
<string name="admin.confirm_password_bad">Mot de passe saisi erroné</string>
@@ -477,37 +453,34 @@
<string name="admin.role.coverArt">Modifier les pochettes</string>
<string name="admin.role.comment">Ajouter des commentaires</string>
<string name="admin.role.podcast">Gérer les podcasts</string>
- <string name="admin.role.stream">Ecouter de la musique</string>
+ <string name="admin.role.stream">Écouter de la musique</string>
<string name="admin.role.jukebox">Télécommander la lecture (jukebox)</string>
<string name="admin.role.share">Gérer les partages</string>
<string name="admin.role.lastfm">Utiliser Last.FM</string>
<string name="music_service.retry">Erreur réseau. Nouvelle tentative %1$d de %2$d.</string>
- <string name="background_task.wait">Patienter...</string>
- <string name="background_task.loading">Chargement.</string>
- <string name="background_task.no_network">Cette application nécessite un accès réseau. Activer les connexion Wifi ou mobile.</string>
+ <string name="background_task.no_network">Cette application nécessite un accès réseau. Activer les connexion Wifi ou mobile.</string>
<string name="background_task.network_error">Une erreur réseau est survenue. Merci de vérifier l\'adresse du serveur ou réessayer plus tard.</string>
<string name="background_task.not_found">Ressource non trouvée. Vérifier l\'adresse du serveur.</string>
<string name="background_task.parse_error">Erreur de communication avec le serveur.Vérifier l\'adresse du serveur et que la connexion via un navigateur fonctionne.</string>
<string name="service.connecting">Interrogation du serveur, veuillez patienter.</string>
- <string name="parser.upgrade_client"> Versions incompatible. Mettre à jour DSub.</string>
- <string name="parser.upgrade_server">Versions incompatibles. Mettre à jour le serveur Subsonic.</string>
+ <string name="parser.upgrade_client">Version incompatible. Mettre à jour DSub.</string>
+ <string name="parser.upgrade_server">Version incompatible. Mettre à jour le serveur Subsonic.</string>
<string name="parser.not_authenticated">Mauvais nom d\'utilisateur ou mot de passe.</string>
<string name="parser.not_authorized">Non autorisé. Vérifier les droit de l\'utilisateur sur le serveur Subsonic.</string>
<string name="parser.artist_count">%d artistes récupérés.</string>
<string name="parser.server_error">Erreur serveur : %s</string>
<string name="parser.scan_count">%d entrées trouvées</string>
- <string name="select_artist.refresh">Recharger</string>
- <string name="select_artist.folder">Sélectionner un dossier</string>
- <string name="select_artist.all_folders">Tous les dossier</string>
+ <string name="select_artist.folder">Sélectionner un dossier</string>
+ <string name="select_artist.all_folders">Tous les dossiers</string>
- <string name="equalizer.label">Equaliseur</string>
+ <string name="equalizer.label">Équaliseur</string>
<string name="equalizer.enabled">Activé</string>
- <string name="equalizer.preset">Selectioner un préréglage</string>
+ <string name="equalizer.preset">Sélectionner un préréglage</string>
<string name="equalizer.bass_booster">Bass Booster</string>
<string name="equalizer.voice_booster">Voice Booster</string>
<string name="equalizer.db_size">%d dB</string>
@@ -526,14 +499,12 @@
<string name="util.bytes_format.kilobyte">0 KB</string>
<string name="util.bytes_format.byte">0 B</string>
- <string name="changelog_full_title">Change Log</string>
- <string name="changelog_title">What\'s New</string>
+ <string name="changelog_full_title">Liste des changements</string>
+ <string name="changelog_title">Nouveautés</string>
<string name="changelog_ok_button">OK</string>
<string name="changelog_show_full">Plus…</string>
- <string name="chat.send_a_message">Envoyer un message</string>
-
- <string name="changelog_version_format" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">Version <xliff:g id="version_name">%s</xliff:g></string>
+ <string name="chat.send_a_message">Envoyer un message</string>
<string name="tasker.start_playing">Commencer la lecture</string>
<string name="tasker.start_playing_title">Tasker -> Démarrer DSub</string>
diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml
index 60b127be..12102043 100644
--- a/app/src/main/res/values-hu/strings.xml
+++ b/app/src/main/res/values-hu/strings.xml
@@ -32,11 +32,11 @@
<string name="button_bar.home">Főoldal</string>
<string name="button_bar.browse">Médiatár</string>
- <string name="button_bar.search">Keresés</string>
- <string name="button_bar.playlists">Lejátszási listák</string>
+ <string name="button_bar.playlists">Lejátszási listák</string>
<string name="button_bar.now_playing">Várólista</string>
<string name="button_bar.podcasts">Podcastok</string>
<string name="button_bar.bookmarks">Könyvjelzők</string>
+ <string name="button_bar.internet_radio">Internet rádió</string>
<string name="button_bar.shares">Megosztások</string>
<string name="button_bar.chat">Csevegés (Chat)</string>
<string name="button_bar.admin">Admin</string>
@@ -54,12 +54,11 @@
<br/>Míg a normál módon gyorsítótárazott dalok törlődhetnek amikor újak kerülnek letöltésre, addig a \"Letöltés tárolásra (megőrzés)\" menüpont segítségével letöltött dalok soha nem törlődnek automatikusan.
<p/><font color="red">Ha a ChromeCast sikertelen</font>:
<br/>Próbálja meg bejelölni: Beállítások -> Lejátszás -> Eszköz használata proxyként. Ez egy kerülő megoldás arra, ha a ChromeCast elutasítja a saját aláírású tanúsítványt.
- <p/><font color="red">A Médiatár első szintje tulajdonképpen az előadók csoportja</font>:
- <br/>A Beállítások menüben törölje az "Előadók első szintje" jelölést. Ez teszi lehetővé, hogy a mappák teljes első szintjének megjelenítése előadói csoportonként és ne előadónként legyen kezelve.
+ <p/><font color="red">A Médiatár első szintje ugyanolyan, mint az előadók listája</font>:
+ <br/>Ha a Beállítások menüben kikapcsolja az "Részletes megjelenítés" opciót, akkor a mappák teljes első szintje ugyanúgy lesz megjelenítve, mint az előadók listája.
]]>
</string>
- <string name="main.select_server">Kiszolgáló kiválasztása</string>
- <string name="main.shuffle">Lejátszás kevert sorrendben</string>
+ <string name="main.shuffle">Lejátszás kevert sorrendben</string>
<string name="main.offline">Offline mód</string>
<string name="main.online">Online mód</string>
<string name="main.settings">Beállítások</string>
@@ -76,6 +75,10 @@
<string name="main.albums_alphabetical">Betűrendben</string>
<string name="main.videos">Videók</string>
<string name="main.songs_genres">@string/main.albums_genres</string>
+ <string name="main.songs_newest">@string/main.albums_newest</string>
+ <string name="main.songs_top_played">Top Played</string>
+ <string name="main.songs_recent">@string/main.albums_recent</string>
+ <string name="main.songs_frequent">@string/main.albums_frequent</string>
<string name="main.back_confirm">Nyomja meg még egyszer a kilépéshez!</string>
<string name="main.scan_complete">A médiatár frissítése befejeződött a kiszolgálón!</string>
<string name="main.artist">Előadó</string>
@@ -113,7 +116,7 @@
<string name="menu.similar_artists">Hasonló előadók</string>
<string name="menu.show_missing">Hiányzó megjelenítése</string>
<string name="menu.start_radio">Rádió indítása</string>
- <string name="menu.first_level_artist">Előadók első szintje</string>
+ <string name="menu.first_level_artist">Részletes megjelenítés</string>
<string name="playlist.label">Lejátszási listák</string>
<string name="playlist.update_info">Szerkesztés</string>
@@ -131,20 +134,13 @@
<string name="search.artists">Előadók</string>
<string name="search.albums">Albumok</string>
<string name="search.songs">Dalok</string>
- <string name="search.more">Továbbiak</string>
- <string name="progress.wait">Kérem várjon...</string>
+ <string name="progress.wait">Kérem várjon...</string>
<string name="progress.artist_info">Az előadó életrajzának betöltése...</string>
- <string name="music_library.label">Médiatár</string>
- <string name="music_library.label_offline">Kapcsolat nélküli médiák</string>
-
- <string name="select_album.select">Összes jelölése be/ki</string>
- <string name="select_album.n_selected">%d kijelölve.</string>
- <string name="select_album.more">Továbbiak</string>
- <string name="select_album.offline">Offline</string>
- <string name="select_album.searching">Keresés...</string>
- <string name="select_album.no_sdcard">Hiba: SD kártya nem áll rendelkezésre!</string>
+ <string name="select_album.n_selected">%d kijelölve.</string>
+ <string name="select_album.offline">Offline</string>
+ <string name="select_album.no_sdcard">Hiba: SD kártya nem áll rendelkezésre!</string>
<string name="select_album.no_network">Figyelem: Hálózat nem áll rendelkezésre!</string>
<string name="select_album.no_room">Figyelem: Már csak %s hely áll rendelkezésre!</string>
<string name="select_album.not_licensed">A kiszolgálónak nincs licence! %d próbanap van hátra!</string>
@@ -187,6 +183,7 @@
<string name="download.playerstate_mobile_disabled">Letöltés, várakozás a Wi-Fi hálózatra...</string>
<string name="download.playerstate_buffering">Pufferelés</string>
<string name="download.playerstate_playing_shuffle">Dalsorrend keverése</string>
+ <string name="download.playerstate_playing_artist_radio">Előadó rádió</string>
<string name="download.menu_show_album">Ugrás az albumhoz</string>
<string name="download.menu_lyrics">Dalszöveg</string>
<string name="download.menu_remove_all">Összes eltávolítása</string>
@@ -204,13 +201,9 @@
<string name="download.repeat_off">Ismétlés ki</string>
<string name="download.repeat_all">Összes ismétlése</string>
<string name="download.repeat_single">Dal ismétlése</string>
- <string name="download.jukebox_on">Távvezérlés bekapcsolása. A zenelejátszás a számítógépen történik.</string>
- <string name="download.jukebox_off">Távvezérlés kikapcsolása. A zenelejátszás az eszközön történik.</string>
- <string name="download.jukebox_volume">Hangerő távvezérlése</string>
- <string name="download.jukebox_server_too_old">A távvezérlés nem támogatott. Kérjük, frissítse a Subsonic kiszolgálót!</string>
+ <string name="download.jukebox_server_too_old">A távvezérlés nem támogatott. Kérjük, frissítse a Subsonic kiszolgálót!</string>
<string name="download.jukebox_offline">A távvezérlés nem lehetséges offline módban!</string>
<string name="download.jukebox_not_authorized">A távvezérlés nem lehetséges! Engedélyezze a Jukebox módot a <b>Users &gt; Settings</b> menüben a Subsonic kiszolgálón!</string>
- <string name="download.timer_length">Időhossz:</string>
<string name="download.start_timer">Időzítő indítása</string>
<string name="download.stop_timer">Időzítő megállítása</string>
<string name="download.need_download">A videót először le kell tölteni!</string>
@@ -227,6 +220,13 @@
<string name="download.restore_play_queue">Folytatás onnan, ahol egy másik eszközön abbahagyta.</string>
<string name="download.thumbs_up">Jó</string>
<string name="download.thumbs_down">Nem jó</string>
+ <string name="download.batch_mode">Kötegelt mód</string>
+ <string name="download.playback_speed_half">0.5x</string>
+ <string name="download.playback_speed_normal">1x</string>
+ <string name="download.playback_speed_one_half">1.5x</string>
+ <string name="download.playback_speed_double">2x</string>
+ <string name="download.playback_speed_tripple">3x</string>
+ <string name="download.playback_speed_custom">Egyéni</string>
<string name="sync.new_podcasts">Új podcastok: \"%s\"</string>
<string name="sync.new_playlists">Új lejátszási listák: \"%s\"</string>
@@ -291,6 +291,8 @@
<string name="settings.cache_location">Gyorsítótár helye</string>
<string name="settings.cache_location_error">Hibás gyorsítótár hely! Az alapértelmezett használata.</string>
<string name="settings.cache_location_reset">A beállított gyorsítótár-hely már nem írható! Ha a közelmúltban frissítette telefonja Android rendszerét 4.4.x KitKat verzióra, abban az SD kártya kezelése megváltozott, és az alkalmazások csak egy speciális helyre tudnak írni. A Dsub már automatikusan átállt a megfelelő helyre. Ahhoz, hogy a régi adatokat törölni tudja, csatlakoztassa az SD kártyát a számítógépéhez, és törölje a régi mappát!</string>
+ <string name="settings.cache_location_internal">Belső</string>
+ <string name="settings.cache_location_external">Külső</string>
<string name="settings.cache_clear">Gyorsítótár törlése</string>
<string name="settings.cache_clear_complete">Gyorsítótár törlése kész.</string>
<string name="settings.testing_connection">Kapcsolat tesztelése...</string>
@@ -305,6 +307,8 @@
<string name="settings.theme_dark">Sötét</string>
<string name="settings.theme_black">Fekete</string>
<string name="settings.theme_holo">Holo</string>
+ <string name="settings.theme_day_night">Nappal/Éjszaka</string>
+ <string name="settings.theme_day_black_night">Nappal/Fekete éjszaka</string>
<string name="settings.theme_fullscreen">Teljes képernyős</string>
<string name="settings.theme_fullscreen_summary">Teljes képernyős üzemmód (értesítési sáv elrejtése).</string>
<string name="settings.track_title">Dalsorszám megjelenítése</string>
@@ -407,6 +411,8 @@
<string name="settings.podcasts_enabled_summary">Podcastok menüpont megjelenítése az elhúzható oldalsávon.</string>
<string name="settings.bookmarks_enabled">Könyvjelzők engedélyezése</string>
<string name="settings.bookmarks_enabled_summary">Könyvjelzők menüpont megjelenítése az elhúzható oldalsávon.</string>
+ <string name="settings.internet_radio_enabled">Internet rádió engedélyezése</string>
+ <string name="settings.internet_radio_enabled_summary">Internet rádió menüpont megjelenítése az elhúzható oldalsávon.</string>
<string name="settings.shares_enabled">Megosztások engedélyezése</string>
<string name="settings.shares_enabled_summary">Megosztások menüpont megjelenítése az elhúzható oldalsávon.</string>
<string name="settings.sync_title">Szinkronizálás</string>
@@ -447,8 +453,11 @@
<string name="settings.override_system_language">A rendszer nyelvének felülbírálása</string>
<string name="settings.override_system_language_summary">A Dsub megjelenítése angol nyelven abban az esetben is, ha rendelkezik fordítással. Az alkalmazást törölni kell a memóriából, mert a beállítás csak újraindítás után lép érvénybe!</string>
<string name="settings.drawer_items_title">Oldalsáv elemei</string>
- <string name="settings.play_now_after">Lejátszás utána</string>
- <string name="settings.play_now_after_summary">Egy helyi menü, amivel lehetővé válik minden dal lejátszása a kijelölt elem után (mint a Subsonic webes felületén)</string>
+ <string name="settings.song_press_action">Viselkedés dal megérintésekor</string>
+ <string name="settings.song_press_play_single">Dal lejátszása</string>
+ <string name="settings.song_press_play_all">A teljes album hozzáadása a várólistához</string>
+ <string name="settings.song_press_play_next">Dal sorba állítása következőnek</string>
+ <string name="settings.song_press_play_last">Dal sorba állítása utolsónak</string>
<string name="settings.large_album_art">Nagyméretű albumborítók</string>
<string name="settings.large_album_art_summary">Albumok megjelenítése rácsnézetben és nagyméretű albumborítóval a listanézet helyett.</string>
<string name="settings.admin_enabled">Admin engedélyezése</string>
@@ -461,7 +470,7 @@
<string name="settings.replay_gain_type.track">Dal értékeiből</string>
<string name="settings.replay_gain_bump">Hangerő-kiegyenlítés előerősítése</string>
<string name="settings.replay_gain_untagged">Dalok hangerő-kiegyenlítés nélkül</string>
- <string name="settings.casting">Casting (Tartalmak átküldése)</string>
+ <string name="settings.casting">Casting (tartalmak átküldése)</string>
<string name="settings.casting_proxy">Eszköz használata proxyként</string>
<string name="settings.casting_proxy_summary">Streamelés az eszközön (mint egy proxyn) keresztül. Ez megoldást jelenthet néhány esetben, pl. saját aláírású tanúsítvány használatakor.</string>
<string name="settings.rename_duplicates">Duplikált dalok átnevezése</string>
@@ -473,6 +482,12 @@
<string name="settings.shuffle_by_album">Keverés albumok szerint</string>
<string name="settings.shuffle_by_album.true">Dalsorrend keverése albumonként szétválasztva.</string>
<string name="settings.shuffle_by_album.false">Dalsorrend keverése az összes dalt együtt kezelve.</string>
+ <string name="settings.casting_stream_original">Eredeti stream</string>
+ <string name="settings.casting_stream_original_summary">Az eredeti fájl streamelése, ha a Cast-kompatibilis eszköz támogatja a fájltípust.</string>
+ <string name="settings.heads_up_notification">Felugró értesítések (5.0+)</string>
+ <string name="settings.heads_up_notification_summary">Lejátszási értesítések megjelenítése felugró értesítésekként (Android Lollipop+).</string>
+ <string name="settings.casting_cache">Casting közbeni gyorsítótárazás</string>
+ <string name="settings.casting_cache_summary">Az éppen lejátszott dal gyorsítótárazása a tartalomátküldés (Casting) alatt.</string>
<string name="shuffle.title">Dalsorrend keverése</string>
<string name="shuffle.startYear">Kezdő év:</string>
@@ -480,15 +495,6 @@
<string name="shuffle.genre">Műfaj:</string>
<string name="shuffle.pick_genre">Műfaj kiválasztása</string>
- <string name="share.info">Tulajdonos: %1$s
- \nLeírás: %2$s
- \nURL: %3$s
- \nLétrehozva: %4$s
- \nUtolsó látogatás: %5$s
- \nLejárati idő: %6$s
- \nLátogatások száma: %7$s
-
- </string>
<string name="share.expires">Lejárati idő: %s</string>
<string name="share.expires_never">Nincs lejárati idő</string>
<string name="share.deleted">\"%s\" megosztás törölve</string>
@@ -517,6 +523,7 @@
<string name="admin.change_password">Jelszó csere</string>
<string name="admin.change_password_success">\"%1$s\" jelszavának módosítása kész.</string>
<string name="admin.change_password_error">\"%1$s\" jelszavának módosítása sikertelen!</string>
+ <string name="admin.change_password_current_label">Jelenlegi jelszó:</string>
<string name="admin.change_password_label">Új jelszó:</string>
<string name="admin.change_password_invalid">Adjon meg egy érvényes jelszót!</string>
<string name="admin.delete_user">Felhasználó törlése</string>
@@ -524,6 +531,8 @@
<string name="admin.delete_user_error">\"%1$s\" felhasználó törlése sikertelen!</string>
<string name="admin.confirm_password">Jelszó megerősítése</string>
<string name="admin.confirm_password_bad">A beírt jelszó nem egyezik!</string>
+ <string name="admin.permissions">Engedélyek</string>
+ <string name="admin.musicFolders">Zenekönyvtárak</string>
<string name="admin.scrobblingEnabled">Scrobbling használata</string>
<string name="admin.role.admin">Adminisztrátor</string>
@@ -536,13 +545,12 @@
<string name="admin.role.stream">Zene streamelése</string>
<string name="admin.role.jukebox">Jukebox vezérlése</string>
<string name="admin.role.share">Megosztások kezelése</string>
- <string name="admin.role.lastfm">Last.fm funkció használata</string>
+ <string name="admin.role.video_conversion">Videók konvertálása</string>
+ <string name="admin.role.lastfm">Last.fm szolgáltatás használata</string>
<string name="music_service.retry">Hálózati hiba történt! Újrapróbálkozás %1$d/%2$d.</string>
- <string name="background_task.wait">Kérem várjon...</string>
- <string name="background_task.loading">Betöltés...</string>
- <string name="background_task.no_network">Az alkalmazás hálózati hozzáférést igényel. Kérjük, kapcsolja be a Wi-Fi-t vagy a mobilhálózatot!</string>
+ <string name="background_task.no_network">Az alkalmazás hálózati hozzáférést igényel. Kérjük, kapcsolja be a Wi-Fi-t vagy a mobilhálózatot!</string>
<string name="background_task.network_error">Hálózati hiba történt! Kérjük, ellenőrizze a kiszolgáló címét, vagy próbálja később!</string>
<string name="background_task.not_found">Az erőforrás nem található! Kérjük, ellenőrizze a kiszolgáló címét!</string>
<string name="background_task.parse_error">Hiba történt a kiszolgálóval történő kommunikációban. Kérjük, ellenőrizze a kiszolgáló címét, és próbáljon meg web böngészővel kapcsolódni a kiszolgálóhoz!</string>
@@ -557,15 +565,14 @@
<string name="parser.server_error">Kiszolgáló hiba: %s</string>
<string name="parser.scan_count">%d tétel átvizsgálva.</string>
- <string name="select_artist.refresh">Frissítés</string>
- <string name="select_artist.folder">Mappa kiválasztása</string>
+ <string name="select_artist.folder">Mappa kiválasztása</string>
<string name="select_artist.all_folders">Összes mappa</string>
<string name="equalizer.label">Equalizer</string>
<string name="equalizer.enabled">Engedélyezve</string>
<string name="equalizer.preset">Profil kiválasztása</string>
- <string name="equalizer.bass_booster">Basszus fokozás</string>
- <string name="equalizer.voice_booster">Beszédhang fokozás</string>
+ <string name="equalizer.bass_booster">Basszus fokozása</string>
+ <string name="equalizer.voice_booster">Beszédhang fokozása</string>
<string name="equalizer.db_size">%d dB</string>
<string name="equalizer.bass_size">%d ezer</string>
@@ -589,7 +596,7 @@
<string name="chat.send_a_message">Üzenet küldése</string>
- <string name="changelog_version_format" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">Version <xliff:g id="version_name">%s</xliff:g></string>
+ <string name="changelog_version_format">Verzió %s</string>
<string name="tasker.start_playing">Lejátszás indítása</string>
<string name="tasker.start_playing_shuffled">Lejátszás indítása kevert sorrendben</string>
@@ -635,13 +642,16 @@
<string name="details.version">Verzió</string>
<string name="details.files_cached">Gyorsítótárazott fájlok</string>
<string name="details.files_permanent">Megőrzött fájlok</string>
- <string name="details.used_space">Felhasznált tároló</string>
+ <string name="details.used_space">Felhasznált tárhely</string>
<string name="details.available_space">Rendelkezésre álló hely</string>
<string name="details.of">%1$s/%2$s</string>
<string name="details.song">Dal</string>
<string name="details.position">Pozíció</string>
<string name="details.updated">Frissítve</string>
<string name="details.starred">Csillagozott</string>
+ <string name="details.last_played">Utoljára lejátszott</string>
+ <string name="details.expiration">Lejárati idő</string>
+ <string name="details.played_count">Lejátszások száma</string>
<plurals name="select_album_n_songs">
<item quantity="zero">Nincsenek dalok</item>
diff --git a/app/src/main/res/values-large/dimens.xml b/app/src/main/res/values-large/dimens.xml
index dfe7ff84..faf88b3e 100644
--- a/app/src/main/res/values-large/dimens.xml
+++ b/app/src/main/res/values-large/dimens.xml
@@ -5,4 +5,7 @@
<dimen name="Button.Small">54dip</dimen>
<dimen name="AlbumArt.Small">96dip</dimen>
<dimen name="AlbumArt.Header">210dip</dimen>
+ <dimen name="FastScroller.LeftAlignedMargin">10dp</dimen>
+ <dimen name="FastScroller.NormalBarMargin">8dp</dimen>
+ <dimen name="FastScroller.RightMargin">8dp</dimen>
</resources> \ No newline at end of file
diff --git a/app/src/main/res/values-large/integers.xml b/app/src/main/res/values-large/integers.xml
index 914ec84a..243d3fc1 100644
--- a/app/src/main/res/values-large/integers.xml
+++ b/app/src/main/res/values-large/integers.xml
@@ -2,4 +2,5 @@
<resources>
<integer name="Grid.Columns">3</integer>
<integer name="TextDescriptionLength">10</integer>
+ <integer name="Card.Elevation">4</integer>
</resources> \ No newline at end of file
diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
new file mode 100644
index 00000000..93ff1f1d
--- /dev/null
+++ b/app/src/main/res/values-nl/strings.xml
@@ -0,0 +1,636 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="common.appname">DSub</string>
+ <string name="common.ok">OK</string>
+ <string name="common.save">Opslaan</string>
+ <string name="common.cancel">Annuleer</string>
+ <string name="common.play_now">Afspelen</string>
+ <string name="common.play_shuffled">Willekeurig afspelen</string>
+ <string name="common.play_next">Volgend afspelen</string>
+ <string name="common.play_last">Laatst afspelen</string>
+ <string name="common.download">Cache</string>
+ <string name="common.pin">Permanente Cache</string>
+ <string name="common.delete">Verwijder</string>
+ <string name="common.star">Ster</string>
+ <string name="common.unstar">Verwijder ster</string>
+ <string name="common.info">Details</string>
+ <string name="common.name">Naam</string>
+ <string name="common.comment">Opmerking</string>
+ <string name="common.public">Publiek</string>
+ <string name="common.play_external">Speel video</string>
+ <string name="common.stream_external">Stream video</string>
+ <string name="common.confirm">Bevestig</string>
+ <string name="common.confirm_message">Wil je %1$s %2$s?</string>
+ <string name="common.confirm_message_cache">cache</string>
+ <string name="common.empty">Geen data</string>
+ <string name="common.warning">Waarschuwing</string>
+ <string name="common.close">Sluiten</string>
+ <string name="common.false">Nee</string>
+ <string name="common.true">Ja</string>
+ <string name="common.never">Nooit</string>
+
+ <string name="button_bar.home">Home</string>
+ <string name="button_bar.browse">Bibliotheek</string>
+ <string name="button_bar.playlists">Playlists</string>
+ <string name="button_bar.now_playing">Speelt nu</string>
+ <string name="button_bar.podcasts">Podcasts</string>
+ <string name="button_bar.bookmarks">Bladwijzers</string>
+ <string name="button_bar.shares">Shares</string>
+ <string name="button_bar.chat">Chatten</string>
+ <string name="button_bar.admin">Admin</string>
+ <string name="button_bar.downloading">Downloaden</string>
+ <string name="button_bar.offline">Offline</string>
+
+ <string name="main.welcome_title">Welkom!</string>
+ <string name="main.welcome_text">Welkom bij DSub! De app is momenteel alleen geconfigureerd om de demo server te gebruiken. Nadat je eigen server geïnstalleerd hebt (beschikbaar bij <b>subsonic.org</b>), ga dan naar <b>Instellingen</b> en verander de configuratie zodat je ermee kunt verbinden.</string>
+ <string name="main.about_title">Over DSub</string>
+ <string name="main.faq_title">FAQ</string>
+ <string name="main.faq_text">
+ <![CDATA[
+ <font color="red">Cache versus Permanente Cache</font>:
+ <br/>Wanneer nummers zijn gedownload door DSub (cache), kunnen ze verwijderd worden om ruimte te maken voor nieuwe downloads. Permanente cache, daarentegen, wordt nooit automatisch verwijderd.
+ <p/><font color="red">ChromeCast faalt</font>:
+ <br/>Probeer de optie Instellingen -> Afspelen -> Gebruik apparaat proxy. Hiermee wordt het probleem van niet geaccepteerde \'self signed\' certificaten omzeild.
+ <p/><font color="red">Het eerste niveau in de bibliotheek zijn eigenlijk groepen artiesten.</font>:
+ <br/>In het optiemenu deselecteer "Eerste niveau artiesten". Dit zorgt ervoor dat het gehele eerste niveau directories wordt behandeld als een groep artisten ipv individuele artiesten.
+ ]]>
+ </string>
+ <string name="main.shuffle">Willekeurig afspelen</string>
+ <string name="main.offline">Ga offline</string>
+ <string name="main.online">Ga online</string>
+ <string name="main.settings">Instellingen</string>
+ <string name="main.albums_title">Album lijsten</string>
+ <string name="main.albums_per_folder">Per folder</string>
+ <string name="main.albums_newest">Recent toegevoegd</string>
+ <string name="main.albums_recent">Recent afgespeeld</string>
+ <string name="main.albums_frequent">Meest afgespeeld</string>
+ <string name="main.albums_highest">Hoogst beoordeeld</string>
+ <string name="main.albums_starred">Met ster</string>
+ <string name="main.albums_random">Willekeurig</string>
+ <string name="main.albums_genres">Genres</string>
+ <string name="main.albums_year">Decennia</string>
+ <string name="main.albums_alphabetical">Alfabetisch</string>
+ <string name="main.videos">Video\'s</string>
+ <string name="main.songs_genres">@string/main.albums_genres</string>
+ <string name="main.back_confirm">Druk nogmaals terug om te stoppen</string>
+ <string name="main.scan_complete">Server scan afgerond</string>
+ <string name="main.artist">Artiest</string>
+ <string name="main.title">Titel</string>
+
+ <string name="menu.search">Zoek</string>
+ <string name="menu.shuffle">Willekeurig</string>
+ <string name="menu.refresh">Ververs</string>
+ <string name="menu.play">Speel af</string>
+ <string name="menu.play_last">Speel laatst</string>
+ <string name="menu.exit">Beëindigen</string>
+ <string name="menu.settings">Instellingen</string>
+ <string name="menu.help">Help</string>
+ <string name="menu.about">Over</string>
+ <string name="menu.add_playlist">Voeg toe aan playlist</string>
+ <string name="menu.remove_playlist">Verwijder van playlist</string>
+ <string name="menu.deleted_playlist">Verwijderde playlist %s</string>
+ <string name="menu.deleted_playlist_error">Playlist %s kon niet verwijderd worden</string>
+ <string name="menu.log">Stuur log</string>
+ <string name="menu.set_timer">Stel timer in</string>
+ <string name="menu.check_podcasts">Controleer op nieuwe afleveringen</string>
+ <string name="menu.add_podcast">Voeg kanaal toe</string>
+ <string name="menu.keep_synced">Gesynchroniseerd houden</string>
+ <string name="menu.stop_sync">Stop synchroniseren</string>
+ <string name="menu.show_all">Toon alle media</string>
+ <string name="menu.show_artist">Toon artist</string>
+ <string name="menu.share">Deel</string>
+ <string name="menu.delete_cache">Verwijder cache</string>
+ <string name="menu.cast">Cast naar apparaat</string>
+ <string name="menu.faq">FAQ</string>
+ <string name="menu.add_user">Gebruiker toevoegen</string>
+ <string name="menu.rescan">Server scannen</string>
+ <string name="menu.rate">Bepaal rating</string>
+ <string name="menu.top_tracks">Laatste .FM Top Nummers</string>
+ <string name="menu.similar_artists">Gelijke artiesten</string>
+ <string name="menu.start_radio">Start radio</string>
+ <string name="menu.first_level_artist">Eerste niveau artiesten</string>
+
+ <string name="playlist.label">Speellijsten</string>
+ <string name="playlist.update_info">Update informatie</string>
+ <string name="playlist.updated_info">Update speellijst informatie voor %s</string>
+ <string name="playlist.updated_info_error">Niet gelust speellijst information for %s te updaten</string>
+ <string name="playlist.overwrite">Overschrijf bestaande playlist</string>
+ <string name="playlist.add_to">Voeg toe aan playlist</string>
+ <string name="playlist.create_new">Creëer nieuw</string>
+ <string name="playlist.delete">Verwijder playlist</string>
+
+ <string name="search.label">Zoek</string>
+ <string name="search.title">Zoek</string>
+ <string name="search.search">Klik om te zoeken</string>
+ <string name="search.no_match">Geen overeenkomst, zoek nogmaals</string>
+ <string name="search.artists">Artiesten</string>
+ <string name="search.albums">Albums</string>
+ <string name="search.songs">Nummers</string>
+
+ <string name="progress.wait">Moment aub...</string>
+ <string name="progress.artist_info">Laadt artiesten bio</string>
+
+ <string name="select_album.n_selected">%d geselecteerd.</string>
+ <string name="select_album.offline">Offline</string>
+ <string name="select_album.no_sdcard">Fout: Geen SD kaart beschikbaar.</string>
+ <string name="select_album.no_network">Waarschuwing: Netwerk onbeschikbaar.</string>
+ <string name="select_album.no_room">Waarschuwing: je hebt nog maar %s over</string>
+ <string name="select_album.not_licensed">Server niet gelicenseerd. %d proefdagen over.</string>
+ <string name="select_album.donate_dialog_message">Verkrijg ongelimeteerde downloads door te doneren aan Subsonic.</string>
+ <string name="select_album.donate_dialog_now">Nu</string>
+ <string name="select_album.donate_dialog_later">Later</string>
+ <string name="select_album.donate_dialog_0_trial_days_left">Proeftijd is voorbij</string>
+
+ <string name="offline.sync_dialog_title">Offline nummers om gesynchroniseerd te worden</string>
+ <string name="offline.sync_dialog_message">Verwerk %1$d offline scrobbles?
+ \nVerwerk %2$d offline sterren?
+ </string>
+ <string name="offline.sync_dialog_default">Gebruik actie als standaard</string>
+ <string name="offline.sync_success">Succesvol %1$d nummers gesynchroniseerd</string>
+ <string name="offline.sync_partial">Succesvol %1$d van %2$d nummers gesynchroniseerd</string>
+ <string name="offline.sync_error">Mislukt om nummers te synchroniseren</string>
+
+ <string name="select_genre.blank">Leeg</string>
+ <string name="select_genre.songs">%d nummers</string>
+ <string name="select_genre.albums">%d albums</string>
+
+ <string name="select_podcasts.error">Deze podcast had een foutmelding tijdens het downloaden naar de server. De server moet het eerst downloaden.</string>
+ <string name="select_podcasts.skipped">Deze podcast is niet op de server gedownload. De server moet het eerst downloaden.</string>
+ <string name="select_podcasts.initializing">Dit podcast kanaal wordt ge\ïnitieerd door de server. Over een enkel moment herladen.</string>
+ <string name="select_podcasts.server_download">Download op de server</string>
+ <string name="select_podcasts.server_delete">Verwijder van server</string>
+ <string name="select_podcasts.downloading">Wordt nu gedownload %s op de server</string>
+ <string name="select_podcasts.refreshing">De server controleert nu op nieuwe podcasts</string>
+ <string name="select_podcasts.deleted">Verwijderde podcast %s</string>
+ <string name="select_podcasts.deleted_error">Kon podcast %s niet verwijderen</string>
+ <string name="select_podcasts.add_url">URL:</string>
+ <string name="select_podcasts.created_error">Niet gelukt podcast toe te voegen</string>
+ <string name="select_podcasts.invalid_podcast_channel">Ongeldig podcast kanaal: %s</string>
+ <string name="select_podcasts.delete">Verwijder podcast</string>
+ <string name="select_podcasts.channels">Podcast kanalen</string>
+
+ <string name="download.empty">Playlist is leeg</string>
+ <string name="download.shuffle_loading">Shuffle lijst wordt geladen...</string>
+ <string name="download.playerstate_downloading">Downloaden - %s</string>
+ <string name="download.playerstate_mobile_disabled">Wachten op WiFi network om te kunnen downloaden</string>
+ <string name="download.playerstate_buffering">Bufferen</string>
+ <string name="download.playerstate_playing_shuffle">Afspelen shuffle</string>
+ <string name="download.menu_show_album">Toon album</string>
+ <string name="download.menu_lyrics">Songteksten</string>
+ <string name="download.menu_remove_all">Verwijder alles</string>
+ <string name="download.menu_screen_on">Scherm aan</string>
+ <string name="download.menu_shuffle">Shuffle</string>
+ <string name="download.menu_toggle">Aan\/Uit</string>
+ <string name="download.menu_save">Bewaar speellijst</string>
+ <string name="download.menu_shuffle_notification">Speellijst was geshufffeld</string>
+ <string name="download.menu_remove_played_songs">Verwijder afgespeelde nummers</string>
+ <string name="download.playlist_title">Bewaar playlist</string>
+ <string name="download.playlist_name">Voer de naam in van de playlist:</string>
+ <string name="download.playlist_saving">Playlist \"%s\ wordt opgeslagen"...</string>
+ <string name="download.playlist_done">Playlist was succesvol opgeslagen.</string>
+ <string name="download.playlist_error">Niet gelukt playlist op te slaan, probeer het later nog eens.</string>
+ <string name="download.repeat_off">Herhalen uit</string>
+ <string name="download.repeat_all">Herhaal alles</string>
+ <string name="download.repeat_single">Herhaal nummer</string>
+ <string name="download.jukebox_server_too_old">Afstandbediending wordt niet ondersteund. Upgrade de Subsonic server.</string>
+ <string name="download.jukebox_offline">Afstandbediending is in offline mode niet beschikbaar.</string>
+ <string name="download.jukebox_not_authorized">Afstandbediening is niet toegestaan. Zet de jukebox mode aan in <b>Users &gt; Instellingen</b> op je Subsonic server.</string>
+ <string name="download.start_timer">Start Timer</string>
+ <string name="download.stop_time_remaining">Stop in %1$s</string>
+ <string name="download.need_download">Video moet eerst gedownload worden</string>
+ <string name="download.no_streaming_player">Deze stream kan niet afgespeeld worden</string>
+ <string name="download.playing_out_of">Speelt: %1$d/%2$d</string>
+ <string name="download.save_bookmark_title">Cre\ëer bladwijzer</string>
+ <string name="download.save_bookmark">Bladwijzer aangemaakt</string>
+ <string name="download.save_bookmark_failed">Niet gelukt om bladwijzer te maken</string>
+ <string name="download.downloading_title">Downloaden %1$d nummers</string>
+ <string name="download.downloading_summary">Huidig: %1$s</string>
+ <string name="download.downloading_summary_expanded">Huidig: %1$s
+ \nGeschatte grootte: %2$s</string>
+ <string name="download.failed_to_load">Niet gelukt te laden</string>
+ <string name="download.restore_play_queue">Ga verder waar je op een ander apparaat gestopt bent bij</string>
+ <string name="download.thumbs_up">Duim omhoog</string>
+ <string name="download.thumbs_down">Duim omlaag</string>
+
+ <string name="sync.new_podcasts">Nieuwe podcasts beschikbaar</string>
+ <string name="sync.new_playlists">Nieuwe nummers in playlists</string>
+ <string name="sync.new_albums">Nieuwe albums beschikbaar</string>
+ <string name="sync.new_starred">Nieuwe nummers met sterren beschikbaar</string>
+
+ <string name="starring_content_starred">Met sterren \"%s\"</string>
+ <string name="starring_content_unstarred">Zoner sterren \"%s\"</string>
+ <string name="starring_content_error">Kon \"%s\" niet updaten, probeer het later nog eens.</string>
+
+ <string name="playlist.mine">Mijn playlists</string>
+ <string name="playlist.shared">Gedeelde playlists</string>
+ <string name="playlist_error">Niet gelukt de lijst playlists op te halen</string>
+ <string name="updated_playlist">Toegevoegd %1$s nummer aan \"%2$s\"</string>
+ <string name="updated_playlist_error">Niet gelukt te updaten \"%s\", probeer later nog eens.</string>
+ <string name="removed_playlist">Verwijderd %1$s nummers van \"%2$s\"</string>
+
+ <string name="bookmark.delete">Verwijder bladwijzer</string>
+ <string name="bookmark.delete_title">Verwijder de bladwijzer voor</string>
+ <string name="bookmark.deleted">Bladwijzer verwijderd voor \"%s\"</string>
+ <string name="bookmark.deleted_error">Niet gelukt om bladwijzer te verwijderen voor \"%s\"</string>
+ <string name="bookmark.details_title">Bladwijzer details</string>
+ <string name="bookmark.resume_title">Hervat afspelen?</string>
+ <string name="bookmark.resume">Hervat afspelen \'%1$s\' van %2$s</string>
+ <string name="bookmark.action_resume">Hervat</string>
+ <string name="bookmark.action_start_over">Start opnieuw</string>
+
+ <string name="rating.title">Beoordeling \"%s\"</string>
+ <string name="rating.set_rating">Beoordeling ingesteld voor \"%s\"</string>
+ <string name="rating.set_rating_failed">Niet gelukt beoordeling in te stellen voor \"%s\"</string>
+ <string name="rating.remove_rating">Beoordeling verwijderd voor \"%s\"</string>
+ <string name="rating.remove_rating_failed">Niet gelukt beoordeling te verwijderen voor \"%s\"</string>
+
+ <string name="song_details.error">Fout</string>
+ <string name="song_details.skipped">Overgeslagen</string>
+ <string name="song_details.downloading">Downloaden</string>
+
+ <string name="lyrics.nomatch">Geen songteksten gevonden</string>
+
+ <string name="error.label">Fout</string>
+
+ <string name="settings.title">Instellingen</string>
+ <string name="settings.test_connection_title">Test verbinding</string>
+ <string name="settings.servers_add">Voeg server toe</string>
+ <string name="settings.servers_remove">Verwijder server</string>
+ <string name="settings.servers_title">Servers</string>
+ <string name="settings.server_unused">Ongebruikt</string>
+ <string name="settings.server_name">Naam</string>
+ <string name="settings.server_address">Server addres</string>
+ <string name="settings.server_local_network_ssid" >Lokaal netwerk SSID</string>
+ <string name="settings.server_local_network_ssid_hint">Huidig SSID: %s</string>
+ <string name="settings.server_internal_address">Lokaal netwerk adres</string>
+ <string name="settings.server_username">Gebruikersnaam</string>
+ <string name="settings.server_password">Wachtwoord</string>
+ <string name="settings.server_open_browser">Open in browser</string>
+ <string name="settings.server_sync_summary">Of synchronisatie ingesteld is voor deze server</string>
+ <string name="settings.server_sync">Synchronisatie ingesteld</string>
+ <string name="settings.cache_title">Muziek cache</string>
+ <string name="settings.preload_wifi">Nummers vooraf laden (Wifi)</string>
+ <string name="settings.preload_mobile">Nummers vooraf laden (Mobiel)</string>
+ <string name="settings.cache_size">Cache grootte</string>
+ <string name="settings.cache_location">Cache locatie</string>
+ <string name="settings.cache_location_error">Onjuiste cache locatie. Standaard wordt gebruikt.</string>
+ <string name="settings.cache_location_reset">De locatie van de cache die u hebt ingesteld is niet meer beschrijfbaar. Als onlangs uw Android telefoon geüpgraded is naar KitKat 4.4 en hoger, dan is de manier waarop apps naar de SD-kaart schrijven veranderd zodat er alleen naar een specifieke locatie geschreven kan worden. De locatie die DSub gebruikt is al automatisch veranderd naar de juiste locatie. Om de oude app-data te verwijderen, moet de SD-kaart gekoppeld worden aan een computer om de oude folder handmatig te verwijderen.</string>
+ <string name="settings.cache_clear">Cache legen</string>
+ <string name="settings.cache_clear_complete">Klaar met cache legen</string>
+ <string name="settings.testing_connection">Verbinding testen...</string>
+ <string name="settings.testing_ok">De verbinding is OK</string>
+ <string name="settings.testing_unlicensed">Verbinding is OK. Server niet gelicenseerd.</string>
+ <string name="settings.connection_failure">Verbinding mislukt.</string>
+ <string name="settings.invalid_url">Geef een valide URL op.</string>
+ <string name="settings.invalid_username">Geef een valide gebruikersnaam op (geen volgspaties).</string>
+ <string name="settings.appearance_title">Uiterlijk</string>
+ <string name="settings.theme_title">Thema</string>
+ <string name="settings.theme_light">Licht</string>
+ <string name="settings.theme_dark">Donker</string>
+ <string name="settings.theme_black">Zwart</string>
+ <string name="settings.theme_holo">Holo</string>
+ <string name="settings.theme_fullscreen">Schermvullend</string>
+ <string name="settings.theme_fullscreen_summary">Verberg zoveel mogelijk schermelementen als Android toestaat</string>
+ <string name="settings.track_title">Toon track #</string>
+ <string name="settings.track_summary">Toon track # voor een nummer indien er een bestaat</string>
+ <string name="settings.custom_sort">Sorteer op jaartal</string>
+ <string name="settings.custom_sort_summary">Sorteer albums op jaartal, of alfabetisch</string>
+ <string name="settings.open_to_tab">Open naar tabblad</string>
+ <string name="settings.open_to_tab_summary">Open direct naar dit tabblad</string>
+ <string name="settings.network_title">Netwerk</string>
+ <string name="settings.max_bitrate_wifi">Max audio bitrate - wifi</string>
+ <string name="settings.max_bitrate_mobile">Max audio bitrate - mobiel</string>
+ <string name="settings.max_bitrate_32">32 Kbps</string>
+ <string name="settings.max_bitrate_64">64 Kbps</string>
+ <string name="settings.max_bitrate_80">80 Kbps</string>
+ <string name="settings.max_bitrate_96">96 Kbps</string>
+ <string name="settings.max_bitrate_112">112 Kbps</string>
+ <string name="settings.max_bitrate_128">128 Kbps</string>
+ <string name="settings.max_bitrate_160">160 Kbps</string>
+ <string name="settings.max_bitrate_192">192 Kbps</string>
+ <string name="settings.max_bitrate_256">256 Kbps</string>
+ <string name="settings.max_bitrate_320">320 Kbps</string>
+ <string name="settings.max_video_bitrate_wifi">Max video bitrate - wifi</string>
+ <string name="settings.max_video_bitrate_mobile">Max video bitrate - mobiel</string>
+ <string name="settings.max_video_bitrate_200">200 Kbps</string>
+ <string name="settings.max_video_bitrate_300">300 Kbps</string>
+ <string name="settings.max_video_bitrate_400">400 Kbps</string>
+ <string name="settings.max_video_bitrate_500">500 Kbps</string>
+ <string name="settings.max_video_bitrate_700">700 Kbps</string>
+ <string name="settings.max_video_bitrate_1000">1000 Kbps</string>
+ <string name="settings.max_video_bitrate_1500">1500 Kbps</string>
+ <string name="settings.max_video_bitrate_2000">2000 Kbps</string>
+ <string name="settings.max_video_bitrate_3000">3000 Kbps</string>
+ <string name="settings.max_video_bitrate_5000">5000 Kbps</string>
+ <string name="settings.max_bitrate_unlimited">Unlimited</string>
+ <string name="settings.wifi_required_title">Alleen wifi streaming</string>
+ <string name="settings.wifi_required_summary">Alleen media streamen wanneer verbonden met wifi.</string>
+ <string name="settings.network_timeout_title">Netwerk timeout</string>
+ <string name="settings.network_timeout_10000">10 seconds</string>
+ <string name="settings.network_timeout_15000">15 seconds</string>
+ <string name="settings.network_timeout_30000">30 seconds</string>
+ <string name="settings.network_timeout_45000">45 seconds</string>
+ <string name="settings.network_timeout_60000">60 seconds</string>
+ <string name="settings.preload_0">0 nummer</string>
+ <string name="settings.preload_1">1 nummer</string>
+ <string name="settings.preload_2">2 nummers</string>
+ <string name="settings.preload_3">3 nummers</string>
+ <string name="settings.preload_5">5 nummers</string>
+ <string name="settings.preload_10">10 nummers</string>
+ <string name="settings.preload_unlimited">Onbeperkt</string>
+ <string name="settings.clear_search_history">Verwijder zoekgeschiedenis</string>
+ <string name="settings.search_history_cleared">Zoekgeschiedenis verwijderd</string>
+ <string name="settings.other_title">Andere instellingen</string>
+ <string name="settings.scrobble_title">Scrobble naar Last.fm</string>
+ <string name="settings.scrobble_summary">Vergeet niet om je last.fm account in te stellen op de Subsonic server.</string>
+ <string name="settings.hide_media_title">Verberg voor andere</string>
+ <string name="settings.hide_media_summary">Verberg muziek voor andere apps.</string>
+ <string name="settings.hide_media_toast">Is actief wanneer Android de volgende keer je telefoon scant voor nieuwe muziek.</string>
+ <string name="settings.media_button_title">Mediaknoppen</string>
+ <string name="settings.media_button_summary">Reageer op telefoon, koptelefoon en bluetooth mediaknoppen</string>
+ <string name="settings.screen_lit_title">Houd scherm aan</string>
+ <string name="settings.screen_lit_summary">Houdt scherm aan tijdens het downloaden verbetert de download snelheid.</string>
+ <string name="settings.playlist_title">Afspelen</string>
+ <string name="settings.playlist_random_size_title">Shuffle playlist grootte</string>
+ <string name="settings.sleep_timer_title">Sleeptimer</string>
+ <string name="settings.sleep_timer_duration_title">Sleeptimer duur</string>
+ <string name="settings.sleep_timer_off">Uit</string>
+ <string name="settings.sleep_timer_on">Aan</string>
+ <string name="settings.sleep_timer_always_on">Altijd aan</string>
+ <string name="settings.temp_loss_title">Tijdelijk focus verlies</string>
+ <string name="settings.temp_loss_pause">Altijd pauzeren</string>
+ <string name="settings.temp_loss_pause_lower">Pauze, lagere volume desgevraagd</string>
+ <string name="settings.temp_loss_lower">Altijd lagere volume</string>
+ <string name="settings.temp_loss_nothing">Doe niets</string>
+ <string name="settings.keep_played_count_title">Behoud afgespeelde nummers</string>
+ <string name="settings.keep_played_count_none">Verwijder alle afspeelde nummers</string>
+ <string name="settings.keep_played_count_one">Behoud laatste afgespeelde nummers</string>
+ <string name="settings.keep_played_count_two">Behoud laatste 2 afgespeelde nummers</string>
+ <string name="settings.keep_played_count_three">Behoud laatste 3 afgespeelde nummers</string>
+ <string name="settings.disconnect_pause_title">Pauzeren bij verbroken verbinding</string>
+ <string name="settings.disconnect_pause_both">Pauzeren</string>
+ <string name="settings.disconnect_pause_neither">Doe niets</string>
+ <string name="settings.persistent_title">Permanente vermelding</string>
+ <string name="settings.persistent_summary">Toon de vermelding zelfs na pauzeren. Druk op de stopknop om de vermelding te verwijderen.</string>
+ <string name="settings.gapless_playback">Ononderbroken afspelen</string>
+ <string name="settings.gapless_playback_summary">Indien je vreemde storingen ondervindt tijdens het afspelen, kan het uitzetten van deze functie misschien helpen.</string>
+ <string name="settings.chat_refresh">Chat vernieuwingsfrequentie (sec.)</string>
+ <string name="settings.chat_enabled">Chat aanzetten</string>
+ <string name="settings.chat_enabled_summary">Al dan niet weergeven van de chatlijst in de lade</string>
+ <string name="settings.video_title">Video</string>
+ <string name="settings.video_player">Videospeler</string>
+ <string name="settings.video_raw">Raw (Vereist Subsonic 4.8+)</string>
+ <string name="settings.video_hls">HTTP Live Stream (HLS) (Vereist Subsonic 4.8+)</string>
+ <string name="settings.video_transcode">Direct Transcode (Vereist video -> mp4 or similar setup on Server)</string>
+ <string name="settings.video_flash">Flash (Vereist plugin)</string>
+ <string name="settings.cache_screen_title">Cache/Netwerk</string>
+ <string name="settings.playback_title">Playback</string>
+ <string name="settings.hide_widget_title">Widget verbergen</string>
+ <string name="settings.hide_widget_summary">Verberg widget na verlaten app</string>
+ <string name="settings.podcasts_enabled">Podcasts ingeschakeld</string>
+ <string name="settings.podcasts_enabled_summary">Al dan niet weergeven van de podcastlijst in de lade</string>
+ <string name="settings.bookmarks_enabled">Bladwijzers ingeschakelPlayback</string>
+ <string name="settings.bookmarks_enabled_summary">Al dan niet weergeven van de bladwijzerlijst in de lade</string>
+ <string name="settings.shares_enabled">Shares ingeschakeld</string>
+ <string name="settings.shares_enabled_summary">Al dan niet weergeven van de shareslijst in de lade</string>
+ <string name="settings.sync_title">Sync</string>
+ <string name="settings.sync_enabled">Sync ingeschakeld</string>
+ <string name="settings.sync_enabled_summary">Al dan of niet playlists of padcasts periodiek gecontroleerd worden op wijzigingen</string>
+ <string name="settings.sync_interval">Sync interval</string>
+ <string name="settings.sync_interval_15">15 minuten</string>
+ <string name="settings.sync_interval_30">30 minute</string>
+ <string name="settings.sync_interval_60">1 uur</string>
+ <string name="settings.sync_interval_120">2 uren</string>
+ <string name="settings.sync_interval_240">4 uren</string>
+ <string name="settings.sync_interval_360">6 uren</string>
+ <string name="settings.sync_interval_720">12 uren</string>
+ <string name="settings.sync_interval_1440">Dagelijks</string>
+ <string name="settings.sync_wifi">Sync alleen met wifi</string>
+ <string name="settings.sync_wifi_summary">Alleen synchroniseren met een wifi-verbinding</string>
+ <string name="settings.sync_most_recent">Sync recentelijk toegevoegd</string>
+ <string name="settings.sync_most_recent_summary">Automatisch cache nieuw toegevoegde albums</string>
+ <string name="settings.sync_starred">Sync met sterren</string>
+ <string name="settings.sync_starred_summary">Cache automatisch nummers, album en artiesten met sterren</string>
+ <string name="settings.sync_notification">Toon sync notificatie</string>
+ <string name="settings.sync_notification_summary">Toon een notificatie nadat er nieuwe media is gesynct</string>
+ <string name="settings.menu_options.title">Optionele menu opties</string>
+ <string name="settings.menu_options.play_now_summary">Toon nu in menus</string>
+ <string name="settings.menu_options.play_shuffled_summary">Toon Shuffled in menu\'s</string>
+ <string name="settings.menu_options.play_next_summary">Toon Speel Volgend in menu\'s</string>
+ <string name="settings.menu_options.play_last_summary">Toon Speel Laatst in menu\'s</string>
+ <string name="settings.menu_options.download_summary">Toon Download in menu\'s</string>
+ <string name="settings.menu_options.pin_summary">Toon Permanente Cache in menu\'s</string>
+ <string name="settings.menu_options.delete_summary">Toon Verwijder in menu\'s</string>
+ <string name="settings.menu_options.star_summary">Toon Ster in menu\'s</string>
+ <string name="settings.menu_options.shared_summary">Toon Share in menu\'s</string>
+ <string name="settings.menu_options.rate_summary">Toon Waarderingen in menu\'s</string>
+ <string name="settings.browse_by_tags">Bladeren door tags</string>
+ <string name="settings.browse_by_tags_summary">Bladeren door tags in plaats van folder structuur. Vereist Subsonic 4.7+</string>
+ <string name="settings.disable_exit_prompt">Exit Prompt uitschakelen</string>
+ <string name="settings.disable_exit_prompt_summary">Verlaat meteen de app bij het op back drukken vanuit het Home scherm</string>
+ <string name="settings.override_system_language">Systeemtaal overschrijven</string>
+ <string name="settings.override_system_language_summary">Toon de app in Engels, ook als de systeemtaal voor Dsub beschikbaar is. Het is misschien nodig om de app uit cache geheugen te wissen voordat het effectief is.</string>
+ <string name="settings.drawer_items_title">Server tabs</string>
+ <string name="settings.large_album_art">Grote albumhoezen</string>
+ <string name="settings.large_album_art_summary">Toon albums met grote hoes in plaats van een lijst</string>
+ <string name="settings.admin_enabled">Beheer ingeschakeld</string>
+ <string name="settings.admin_enabled_summary">Al dan niet weergeven van de adminlijst in de lade</string>
+ <string name="settings.replay_gain">Normaliseren</string>
+ <string name="settings.replay_gain_summary">Aldan of niet de afspeelvolume per nummer of album normaliseren adhv (replay gain) tags</string>
+ <string name="settings.replay_gain_type">Lees van tags</string>
+ <string name="settings.replay_gain_type.smart">Slimme detectie</string>
+ <string name="settings.replay_gain_type.album">Album tags</string>
+ <string name="settings.replay_gain_type.track">Nummer tags</string>
+ <string name="settings.replay_gain_bump">Normaliseren Pre-amp</string>
+ <string name="settings.replay_gain_untagged">Nummers zonder normalisatie</string>
+ <string name="settings.casting">Casten</string>
+ <string name="settings.casting_proxy">Gebruik apparaat proxy</string>
+ <string name="settings.casting_proxy_summary">Stream alles via het apparaat als een proxy. Dit omzeilt problemen zoals zelf toegekende certificaten.</string>
+ <string name="settings.rename_duplicates">Hernoem dubbele nummers</string>
+ <string name="settings.rename_duplicates_summary">Hernoem dubbele nummers naar de originele bestandsnaam om ze te kunnen onderscheiden.</string>
+ <string name="settings.start_on_headphones">Start met koptelefoon</string>
+ <string name="settings.start_on_headphones_summary">Start als een koptelefoon gekoppeld wordt. Dit vereist het gebruik van een service bij het opstarten van de telefoon om de de koptelefoonplugin te monitoren, zelfs als Dsub niet gebruikt wordt.</string>
+ <string name="settings.color_action_bar">Kleur actiebalk</string>
+ <string name="settings.color_action_bar.summary">Kleur de actiebalk en statusbalk, of laat ze ongemoeid.</string>
+ <string name="settings.shuffle_by_album">Shuffle op basis van albums</string>
+ <string name="settings.shuffle_by_album.true">Shuffle de volgorde van albums</string>
+ <string name="settings.shuffle_by_album.false">Shuffle alle nummers</string>
+
+ <string name="shuffle.title">Shuffle op basis van</string>
+ <string name="shuffle.startYear">Start jaar:</string>
+ <string name="shuffle.endYear">Einde jaar:</string>
+ <string name="shuffle.genre">Genre:</string>
+ <string name="shuffle.pick_genre">Kies een genre</string>
+
+ <string name="share.expires">Vervaldatum: %s</string>
+ <string name="share.expires_never">Verloopt nooit</string>
+ <string name="share.deleted">Verwijderde share %s</string>
+ <string name="share.deleted_error">Mislukt om de share %s te verwijderen</string>
+ <string name="share.no_expiration">Verloopt nooit</string>
+ <string name="share.expiration">Verloopt:</string>
+ <string name="share.updated_info">Share informatie geüpdatet voor %s</string>
+ <string name="share.updated_info_error">Mislukt om share informatie te updaten voor %s</string>
+ <string name="share.via">Share via</string>
+ <string name="share.delete">Verwijder share</string>
+
+ <string name="admin.add_user_username">Gebruikersnaam:</string>
+ <string name="admin.add_user_email">E-mail:</string>
+ <string name="admin.add_user_password">Wachtwoord:</string>
+ <string name="admin.create_user_success">Succesvol niet gebruiker aangemaakt</string>
+ <string name="admin.create_user_error">Mislukt nieuwe gebruiker aan te maken</string>
+ <string name="admin.change_username_invalid">Voer valide gebruikersnaam in</string>
+ <string name="admin.update_permissions">Update machtigingen</string>
+ <string name="admin.update_permissions_success">Succesvol machtigingen geüpdatet voor %1$s</string>
+ <string name="admin.update_permissions_error">Mislukt om machtigingen te updaten voor %1$s</string>
+ <string name="admin.change_email">Wijzig e-mail</string>
+ <string name="admin.change_email_success">Succesvol e-mail gewijzigd voor %1$s</string>
+ <string name="admin.change_email_error">Mislukt om e-mail te wijzigen voor %1$s</string>
+ <string name="admin.change_email_label">Nieuwe e-mail:</string>
+ <string name="admin.change_email_invalid">Voor een valide e-mail in</string>
+ <string name="admin.change_password">Wijzig wachtwoord</string>
+ <string name="admin.change_password_success">Succesvol wachtwoord gewijzigd voor %1$s</string>
+ <string name="admin.change_password_error">Mislukt om wachtwoord te wijzigen voor %1$s</string>
+ <string name="admin.change_password_label">Nieuw wachtwoord:</string>
+ <string name="admin.change_password_invalid">Voerin een valide wachtwoord</string>
+ <string name="admin.delete_user">Verwijder gebruiker</string>
+ <string name="admin.delete_user_success">Succesvol %1$s verwijderd</string>
+ <string name="admin.delete_user_error">Mislukt om %1$s te verwijderen</string>
+ <string name="admin.confirm_password">Bevestig wachtwoord</string>
+ <string name="admin.confirm_password_bad">Ingevoerde wachtwoord is onjuist</string>
+
+ <string name="admin.scrobblingEnabled">Scrobbelen toegestaan</string>
+ <string name="admin.role.admin">Beheerder</string>
+ <string name="admin.role.settings">Verander instellingen</string>
+ <string name="admin.role.download">Download originele bestanden</string>
+ <string name="admin.role.upload">Upload naar server</string>
+ <string name="admin.role.coverArt">Wijzig hoes</string>
+ <string name="admin.role.comment">Voeg commentaar toe</string>
+ <string name="admin.role.podcast">Beheer podcasts</string>
+ <string name="admin.role.stream">Muziek streamen</string>
+ <string name="admin.role.jukebox">Beheer jukebox</string>
+ <string name="admin.role.share">Beheer shares</string>
+ <string name="admin.role.lastfm">Gebruik Last.FM functie</string>
+
+ <string name="music_service.retry">Er is een netwerkfout opgetreden. Probeer opnieuw %1$d van %2$d.</string>
+
+ <string name="background_task.no_network">Dit programma het netwerktoegang nodig. Zet wifi of mobiel netwerk aan.</string>
+ <string name="background_task.network_error">Er is een netwerkfout opgetreden. Controleer het serveradres of probeer het later nog eens.</string>
+ <string name="background_task.not_found">De bron is niet gevonden. Controleer het serveradres.</string>
+ <string name="background_task.parse_error">Er is een communicatieprobleem ontstaan met de server. Controleer het serveradres en verifieer dat je met een webbrowser vanaf je apparaat een verbinding kan maken met het serveradres.</string>
+
+ <string name="service.connecting">Serververbinding wordt opgezet, moment geduld a.u.b.</string>
+
+ <string name="parser.upgrade_client">Onverenigbare versies. DSub upgraden a.u.b.</string>
+ <string name="parser.upgrade_server">Onverenigbare versies. Subsonic server upgraden a.u.b.</string>
+ <string name="parser.not_authenticated">Verkeerde gebruikersnaam of wachtwoord.</string>
+ <string name="parser.not_authorized">Niet geautoriseerd. Controleer de gebruikerspermissies op Subsonic server.</string>
+ <string name="parser.artist_count">Heb %d artiesten.</string>
+ <string name="parser.server_error">Server fout: %s</string>
+ <string name="parser.scan_count">%d Items gescand</string>
+
+ <string name="select_artist.folder">Selecteer map</string>
+ <string name="select_artist.all_folders">Alle mappen</string>
+
+ <string name="equalizer.label">Equalizer</string>
+ <string name="equalizer.enabled">Ingeschakeld</string>
+ <string name="equalizer.preset">Selecteer voorinstelling</string>
+ <string name="equalizer.bass_booster">Bass booster</string>
+ <string name="equalizer.voice_booster">Stem booster</string>
+ <string name="equalizer.db_size">%d dB</string>
+ <string name="equalizer.bass_size">%d mille</string>
+
+ <string name="widget.4x1">DSub (4x1)</string>
+ <string name="widget.4x2">DSub (4x2)</string>
+ <string name="widget.4x3">DSub (4x3)</string>
+ <string name="widget.4x4">DSub (4x4)</string>
+ <string name="widget.initial_text">Aanraken om muziek te selecteren</string>
+ <string name="widget.sdcard_busy">SD-kaart onbeschikbaar</string>
+ <string name="widget.sdcard_missing">Geen SD-kaart</string>
+
+ <string name="util.bytes_format.gigabyte">0.00 GB</string>
+ <string name="util.bytes_format.megabyte">0.00 MB</string>
+ <string name="util.bytes_format.kilobyte">0 KB</string>
+ <string name="util.bytes_format.byte">0 B</string>
+
+ <string name="changelog_full_title">Verander Log</string>
+ <string name="changelog_title">Wat is er nieuw</string>
+ <string name="changelog_ok_button">OK</string>
+ <string name="changelog_show_full">Meer…</string>
+
+ <string name="chat.send_a_message">Stuur een bericht</string>
+
+ <string name="changelog_version_format">Versie %s</string>
+
+ <string name="tasker.start_playing">Begin met spelen</string>
+ <string name="tasker.start_playing_shuffled">Begin met spelen in shuffle-modus</string>
+ <string name="tasker.start_playing_title">Tasker -> Start DSub</string>
+ <string name="tasker.edit_shuffle_mode">Start in shuffle-modus: </string>
+ <string name="tasker.edit_shuffle_start_year">Shuffle start jaar:</string>
+ <string name="tasker.edit_shuffle_end_year">Shuffle einde jaar:</string>
+ <string name="tasker.edit_shuffle_genre">Shuffle genre:</string>
+ <string name="tasker.edit_server_offline">Schakel offline: </string>
+ <string name="tasker.edit_do_nothing">Doe niets</string>
+
+ <string name="details.title.song">Nummer details</string>
+ <string name="details.title.album">Album details</string>
+ <string name="details.title.podcast">Podcast details</string>
+ <string name="details.title.playlist">Playlist details</string>
+ <string name="details.title.artist">Artiest details</string>
+ <string name="details.podcast">Podcast</string>
+ <string name="details.status">Status</string>
+ <string name="details.artist">Artiest</string>
+ <string name="details.album">Album</string>
+ <string name="details.track">Nummer</string>
+ <string name="details.genre">Genre</string>
+ <string name="details.year">Jaar</string>
+ <string name="details.server_format">Server format</string>
+ <string name="details.server_bitrate">Server bitrate</string>
+ <string name="details.cached_format">Cached format</string>
+ <string name="details.cached_bitrate">Gecacht bitrate</string>
+ <string name="details.size">Grootte</string>
+ <string name="details.length">Lengte</string>
+ <string name="details.bookmark_position">Bookmark positie</string>
+ <string name="details.rating">Beoordeling</string>
+ <string name="details.description">Beschrijving</string>
+ <string name="details.owner">Eigenaar</string>
+ <string name="details.comments">Opmerkingen</string>
+ <string name="details.song_count">Nummer telling</string>
+ <string name="details.public">Publiek</string>
+ <string name="details.created">Gecreëerd</string>
+ <string name="details.title">Titel</string>
+ <string name="details.url">URL</string>
+ <string name="details.error">Foutmelding</string>
+ <string name="details.author">Auteur</string>
+ <string name="details.email">E-mail</string>
+ <string name="details.version">Versie</string>
+ <string name="details.files_cached">Bestanden gecached</string>
+ <string name="details.files_permanent">Permanent gecached</string>
+ <string name="details.used_space">Gebruikte ruimte</string>
+ <string name="details.available_space">Beschikbare ruimte</string>
+ <string name="details.of">%1$s van %2$s</string>
+ <string name="details.song">Nummer</string>
+ <string name="details.position">Positie</string>
+ <string name="details.updated">Geüpdatet</string>
+ <string name="details.starred">Met ster</string>
+ <string name="details.last_played">Laatst afgespeeld</string>
+
+ <plurals name="select_album_n_songs">
+ <item quantity="zero">Geen nummer</item>
+ <item quantity="one">Een nummer</item>
+ <item quantity="other">%d nummers</item>
+ </plurals>
+ <plurals name="select_album_n_songs_downloading">
+ <item quantity="one">Een nummer geplanned om te downloaden</item>
+ <item quantity="other">%d nummers geplanned om te downloaden.</item>
+ </plurals>
+ <plurals name="select_album_n_songs_added">
+ <item quantity="one">Een nummer toegevoegd aan de speellijst</item>
+ <item quantity="other">%d nummers toegevoegd aan de speellijst.</item>
+ </plurals>
+ <plurals name="select_album_donate_dialog_n_trial_days_left">
+ <item quantity="one">Eén dag over van proefperiode</item>
+ <item quantity="other">%d dagen over van proefperiode</item>
+ </plurals>
+
+</resources>
+
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index ffbb0485..97ba1880 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -24,7 +24,6 @@
<string name="button_bar.home">Домой</string>
<string name="button_bar.browse">Медиатека</string>
- <string name="button_bar.search">Поиск</string>
<string name="button_bar.playlists">Списки</string>
<string name="button_bar.now_playing">Плеер</string>
@@ -32,7 +31,6 @@
<string name="main.welcome_text">Добро пожаловать в DSub! Это приложение настроено на работу с демо сервером Subsonic. После настройки Вашего персонального сервера (доступен на <b>subsonic.org</b>), пожалуйста, перейдите в <b>Настройки</b> и измените параметры для подключения.</string>
<string name="main.about_title">О программе DSub</string>
- <string name="main.select_server">Выбрать сервер</string>
<string name="main.shuffle">Случайное воспроизведение</string>
<string name="main.offline">Отключиться</string>
<string name="main.online">Подключиться</string>
@@ -74,18 +72,11 @@
<string name="search.artists">Исполнители</string>
<string name="search.albums">Альбомы</string>
<string name="search.songs">Композиции</string>
- <string name="search.more">Показать еще</string>
<string name="progress.wait">Пожалуйста, подождите...</string>
- <string name="music_library.label">Медиатека</string>
- <string name="music_library.label_offline">Оффлайн медиа</string>
-
- <string name="select_album.select">Выбрать все</string>
<string name="select_album.n_selected">%d композиций выбрано.</string>
- <string name="select_album.more">Еще</string>
<string name="select_album.offline">Оффлайн</string>
- <string name="select_album.searching">Выполняется поиск...</string>
<string name="select_album.no_sdcard">Ошибка: SD карта недоступна</string>
<string name="select_album.no_network">Внимание: сеть недоступна.</string>
<string name="select_album.not_licensed">Сервер не лицензирован. %d дней до окончания пробного периода.</string>
@@ -115,14 +106,10 @@
<string name="download.repeat_off">Повторение отключено</string>
<string name="download.repeat_all">Повторять все</string>
<string name="download.repeat_single">Повторять композицию</string>
- <string name="download.jukebox_on">Удаленное управление включено. Музыка воспроизводится на компьютере.</string>
- <string name="download.jukebox_off">Удаленное управление отключено. Музыка воспроизводится на устройстве.</string>
- <string name="download.jukebox_volume">Удаленное управление громкостью</string>
<string name="download.jukebox_server_too_old">Удаленное управление не поддерживается. Пожалуйста, обновите Ваш сервер Subsonic.</string>
<string name="download.jukebox_offline">Удаленное управление не поддерживается в оффлайн режиме.</string>
<string name="download.jukebox_not_authorized">Удаленное управление запрещено. Пожалуйста, активируйте режим jukebox в разделе <b>Настройки &gt; Проигрыватели</b> на вашем сервере Subsonic.</string>
- <string name="download.timer_length">Длительность</string>
- <string name="download.start_timer">Запустить таймер</string>
+ <string name="download.start_timer">Запустить таймер</string>
<string name="download.need_download">Необходимо сначала скачать видео</string>
<string name="download.no_streaming_player">Нет плеера для воспроизведения потока</string>
@@ -233,8 +220,6 @@
<string name="music_service.retry">Ошибка подключения. Попытка %1$d из %2$d.</string>
- <string name="background_task.wait">Пожалуйста, подождите...</string>
- <string name="background_task.loading">Загрузка</string>
<string name="background_task.no_network">Эта программа требует доступ к сети. Пожалуйста, включите Wi-Fi или мобильный интернет</string>
<string name="background_task.network_error">Ошибка сети. Пожалуйста, проверьте адрес сервера и попробуйте снова</string>
<string name="background_task.not_found">Ресурс не найден. Пожалуйста, проверьте адрес сервера</string>
@@ -248,7 +233,6 @@
<string name="parser.not_authorized">Не авторизирован. Проверьте права пользователя на сервере Subsonic.</string>
<string name="parser.artist_count">Получено %d исполнителей.</string>
- <string name="select_artist.refresh">Обновить</string>
<string name="select_artist.folder">Выбрать папку</string>
<string name="select_artist.all_folders">Все папки</string>
diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml
index 38add733..ac219530 100644
--- a/app/src/main/res/values-sv/strings.xml
+++ b/app/src/main/res/values-sv/strings.xml
@@ -32,8 +32,7 @@
<string name="button_bar.home">Hem</string>
<string name="button_bar.browse">Bibliotek</string>
- <string name="button_bar.search">Sök</string>
- <string name="button_bar.playlists">Spellista</string>
+ <string name="button_bar.playlists">Spellista</string>
<string name="button_bar.now_playing">Spelar nu</string>
<string name="button_bar.podcasts">Podcasts</string>
<string name="button_bar.bookmarks">Bokmärken</string>
@@ -58,8 +57,7 @@
<br/>Avmarkera "Första steget artister" i inställningsmenyn. Det kommer göra så att hela första steget kommer att hanteras som grupper istället för som enstaka artister.
]]>
</string>
- <string name="main.select_server">Välj server</string>
- <string name="main.shuffle">Slumpad uppspelning</string>
+ <string name="main.shuffle">Slumpad uppspelning</string>
<string name="main.offline">Gå Offline</string>
<string name="main.online">Gå Online</string>
<string name="main.settings">Inställningar</string>
@@ -111,7 +109,6 @@
<string name="menu.rate">Sätt betyg </string>
<string name="menu.top_tracks">Last.FM topp spår</string>
<string name="menu.similar_artists">Liknande artister</string>
- <string name="menu.show_missing">Visa saknade</string>
<string name="menu.start_radio">Starta radio</string>
<string name="menu.first_level_artist">Första steget artister</string>
@@ -131,20 +128,13 @@
<string name="search.artists">Artister</string>
<string name="search.albums">Album</string>
<string name="search.songs">Spår</string>
- <string name="search.more">Visa mer</string>
- <string name="progress.wait">Vänta...</string>
+ <string name="progress.wait">Vänta...</string>
<string name="progress.artist_info">Laddar information om artist</string>
- <string name="music_library.label">Media bibliotek</string>
- <string name="music_library.label_offline">Offline media</string>
-
- <string name="select_album.select">Markera allt</string>
- <string name="select_album.n_selected">%d vald.</string>
- <string name="select_album.more">Mer</string>
- <string name="select_album.offline">Offline</string>
- <string name="select_album.searching">Söker...</string>
- <string name="select_album.no_sdcard">Error: Inget SD-kort tillgängligt.</string>
+ <string name="select_album.n_selected">%d vald.</string>
+ <string name="select_album.offline">Offline</string>
+ <string name="select_album.no_sdcard">Error: Inget SD-kort tillgängligt.</string>
<string name="select_album.no_network">Varning: Inget nätverk tillgängligt.</string>
<string name="select_album.no_room">Varning: Du har bara %s kvar</string>
<string name="select_album.not_licensed">Servern är inte licenserad. %d testdagar kvar.</string>
@@ -203,13 +193,9 @@
<string name="download.repeat_off">Upprepa inte</string>
<string name="download.repeat_all">Upprepa alla</string>
<string name="download.repeat_single">Upprepa spåret</string>
- <string name="download.jukebox_on">Startade fjärrkontroll. Musiken spelas upp på datorn.</string>
- <string name="download.jukebox_off">Stängde av fjärrkontroll. Musiken spelas upp på telefonen.</string>
- <string name="download.jukebox_volume">Fjärrkontroll volym</string>
- <string name="download.jukebox_server_too_old">Fjärrkontroll stöds inte. Uppdatera din Subsonic server.</string>
+ <string name="download.jukebox_server_too_old">Fjärrkontroll stöds inte. Uppdatera din Subsonic server.</string>
<string name="download.jukebox_offline">Fjärrkontroll fungerar inte i offline läge.</string>
<string name="download.jukebox_not_authorized">Fjärrkontroll acceptaras inte. Acceptera Jukebox läge i <b>Användare &gt; Inställningar</b> på din Subsonic server.</string>
- <string name="download.timer_length">Timer:</string>
<string name="download.start_timer">Starta timer</string>
<string name="download.stop_time_remaining">Stoppa om %1$s</string>
<string name="download.need_download">Videon måste laddas ner först</string>
@@ -444,8 +430,6 @@
<string name="settings.override_system_language">Ignorera systemspråket</string>
<string name="settings.override_system_language_summary">Visa appen på engelska även om systemet är på ett språk Subsonic har stöd för. Du kan eventuellt behöva rensa appen från minnet för att det ska ta effekt.</string>
<string name="settings.drawer_items_title">Sidoflikar</string>
- <string name="settings.play_now_after">Spela nu - Efter</string>
- <string name="settings.play_now_after_summary">Spela nu i context menyn spelar allt efter spåret (som i Subsonic webb GUI)</string>
<string name="settings.large_album_art">Stora album bilder</string>
<string name="settings.large_album_art_summary">Visa album med stora bilder istället för en lista</string>
<string name="settings.admin_enabled">Admin aktiverad</string>
@@ -477,15 +461,6 @@
<string name="shuffle.genre">Genre:</string>
<string name="shuffle.pick_genre">Välj en genre</string>
- <string name="share.info">Ägare: %1$s
- \nBeskrivning: %2$s
- \nURL: %3$s
- \nSkapad: %4$s
- \nSenast besökt: %5$s
- \nGår ut: %6$s
- \nAntal besök: %7$s
-
- </string>
<string name="share.expires">Går ut: %s</string>
<string name="share.expires_never">Går aldrig ut</string>
<string name="share.deleted">Tog bort delning %s</string>
@@ -537,9 +512,7 @@
<string name="music_service.retry">Ett nätverksproblem har uppstått. Försök %1$d av %2$d.</string>
- <string name="background_task.wait">Vänta...</string>
- <string name="background_task.loading">Laddar.</string>
- <string name="background_task.no_network">Appen kräver nätverk. Slå på Wi-Fi eller mkobil data för att kunna använda den.</string>
+ <string name="background_task.no_network">Appen kräver nätverk. Slå på Wi-Fi eller mkobil data för att kunna använda den.</string>
<string name="background_task.network_error">Ett nätverksproblem uppstod. Kontrollera server adressen och försök igen.</string>
<string name="background_task.not_found">Resurs kunde inte hittas. Kontrollera server adressen och försök igen.</string>
<string name="background_task.parse_error">Problem med kommunikationen med servern. Kontrollera server adressen och att du kan ansluta till servern via webbläsaren.</string>
@@ -554,8 +527,7 @@
<string name="parser.server_error">Server fel: %s</string>
<string name="parser.scan_count">Scannade %d rader</string>
- <string name="select_artist.refresh">Ladda om</string>
- <string name="select_artist.folder">Välj mapp</string>
+ <string name="select_artist.folder">Välj mapp</string>
<string name="select_artist.all_folders">Alla mappar</string>
<string name="equalizer.label">Equalizer</string>
@@ -586,8 +558,6 @@
<string name="chat.send_a_message">Skicka ett meddelande</string>
- <string name="changelog_version_format" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">Version <xliff:g id="version_name">%s</xliff:g></string>
-
<string name="tasker.start_playing">Börja spela</string>
<string name="tasker.start_playing_shuffled">Börja spela i slumpat läge</string>
<string name="tasker.start_playing_title">Tasker -> Starta DSub</string>
diff --git a/app/src/main/res/values-v21/themes.xml b/app/src/main/res/values-v21/themes.xml
index 5c54b75a..6461130a 100644
--- a/app/src/main/res/values-v21/themes.xml
+++ b/app/src/main/res/values-v21/themes.xml
@@ -7,6 +7,12 @@
<style name="Theme.DSub.Dark" parent="Theme.DSub.Dark.Base">
<item name="android:windowTranslucentStatus">true</item>
</style>
+ <style name="Theme.DSub.Black" parent="Theme.DSub.Black.Base">
+ <item name="android:windowTranslucentStatus">true</item>
+ </style>
+ <style name="Theme.DSub.Holo" parent="Theme.DSub.Holo.Base">
+ <item name="android:windowTranslucentStatus">true</item>
+ </style>
<style name="Theme.DSub.Light.No_Color" parent="Theme.DSub.Light.No_Color.Base">
<item name="android:windowTranslucentStatus">false</item>
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
index a7dd932d..118ccc80 100644
--- a/app/src/main/res/values/arrays.xml
+++ b/app/src/main/res/values/arrays.xml
@@ -1,24 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
- <string-array name="drawerItemsDescriptions">
- <item>Home</item>
- <item>Artist</item>
- <item>Playlist</item>
- <item>Podcast</item>
- <item>Bookmark</item>
- <item>Share</item>
- <item>Chat</item>
- <item>Admin</item>
- <item>Download</item>
- <item>Settings</item>
- </string-array>
-
<string-array name="defaultDrawerItems">
<item>@string/button_bar.home</item>
<item>@string/button_bar.browse</item>
<item>@string/button_bar.playlists</item>
<item>@string/button_bar.podcasts</item>
<item>@string/button_bar.bookmarks</item>
+ <item>@string/button_bar.internet_radio</item>
<item>@string/button_bar.shares</item>
<item>@string/button_bar.chat</item>
</string-array>
@@ -29,6 +17,7 @@
<item>Playlist</item>
<item>Podcast</item>
<item>Bookmark</item>
+ <item>Internet Radio</item>
<item>Share</item>
<item>Chat</item>
</string-array>
@@ -38,6 +27,8 @@
<item>dark</item>
<item>black</item>
<item>holo</item>
+ <item>day/night</item>
+ <item>day/black</item>
</string-array>
<string-array name="themeNames">
@@ -45,6 +36,8 @@
<item>@string/settings.theme_dark</item>
<item>@string/settings.theme_black</item>
<item>@string/settings.theme_holo</item>
+ <item>@string/settings.theme_day_night</item>
+ <item>@string/settings.theme_day_black_night</item>
</string-array>
<string-array name="sleepTimerValues">
@@ -81,6 +74,7 @@
<string-array name="maxBitrateValues">
<item>32</item>
+ <item>48</item>
<item>64</item>
<item>80</item>
<item>96</item>
@@ -95,6 +89,7 @@
<string-array name="maxBitrateNames">
<item>@string/settings.max_bitrate_32</item>
+ <item>@string/settings.max_bitrate_48</item>
<item>@string/settings.max_bitrate_64</item>
<item>@string/settings.max_bitrate_80</item>
<item>@string/settings.max_bitrate_96</item>
@@ -235,4 +230,17 @@
<item>@string/main.online</item>
<item>@string/main.offline</item>
</string-array>
+
+ <string-array name="songPressActionValues">
+ <item>single</item>
+ <item>all</item>
+ <item>next</item>
+ <item>last</item>
+ </string-array>
+ <string-array name="songPressActionNames">
+ <item>@string/settings.song_press_play_single</item>
+ <item>@string/settings.song_press_play_all</item>
+ <item>@string/settings.song_press_play_next</item>
+ <item>@string/settings.song_press_play_last</item>
+ </string-array>
</resources>
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
index c4f80478..055726b8 100644
--- a/app/src/main/res/values/attrs.xml
+++ b/app/src/main/res/values/attrs.xml
@@ -3,6 +3,8 @@
<attr name="offline_icon" format="reference"/>
<attr name="media_button_backward" format="reference"/>
<attr name="media_button_forward" format="reference"/>
+ <attr name="media_button_rewind" format="reference"/>
+ <attr name="media_button_fastforward" format="reference"/>
<attr name="media_button_pause" format="reference"/>
<attr name="media_button_repeat_off" format="reference"/>
<attr name="media_button_repeat_single" format="reference"/>
@@ -14,6 +16,8 @@
<attr name="actionbar_pause" format="reference"/>
<attr name="actionbar_start" format="reference"/>
<attr name="actionbar_stop" format="reference"/>
+ <attr name="actionbar_rewind" format="reference"/>
+ <attr name="actionbar_fastforward" format="reference"/>
<attr name="chat_send" format="reference"/>
<attr name="add" format="reference"/>
<attr name="download_none" format="reference"/>
@@ -37,12 +41,15 @@
<attr name="rating_good" format="reference"/>
<attr name="radio" format="reference"/>
<attr name="star_outline" format="reference"/>
+ <attr name="download" format="reference"/>
+ <attr name="playback_speed" format="reference"/>
<attr name="drawerItemsIcons" format="reference"/>
<attr name="drawerHome" format="reference"/>
<attr name="drawerLibrary" format="reference"/>
<attr name="drawerPlaylists" format="reference"/>
<attr name="drawerPodcasts" format="reference"/>
<attr name="drawerBookmarks" format="reference"/>
+ <attr name="drawerInternetRadioStations" format="reference"/>
<attr name="drawerShares" format="reference"/>
<attr name="drawerChat" format="reference"/>
<attr name="drawerAdmin" format="reference"/>
@@ -55,6 +62,8 @@
<attr name="actionbarBackgroundColor" format="reference"/>
<attr name="drawerTitleStyle" format="reference"/>
<attr name="drawerSubtitleStyle" format="reference"/>
+ <attr name="cardBackgroundDrawable" format="reference"/>
+ <attr name="drawerHeaderBackground" format="reference"/>
<declare-styleable name="SeekBarPreference">
<attr name="min" format="integer"/>
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index 23a3f4a3..9c53f472 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -6,4 +6,11 @@
<dimen name="AlbumArt.Small">78dip</dimen>
<dimen name="AlbumArt.Header">120dip</dimen>
<dimen name="Star.Small">20dp</dimen>
+ <dimen name="SongStatusIcon">24dp</dimen>
+ <dimen name="Card.Radius">4dp</dimen>
+ <dimen name="Card.TextLeftPadding">8dp</dimen>
+ <dimen name="Card.MarginsForShadow">2dp</dimen>
+ <dimen name="FastScroller.LeftAlignedMargin">6dp</dimen>
+ <dimen name="FastScroller.NormalBarMargin">4dp</dimen>
+ <dimen name="FastScroller.RightMargin">4dp</dimen>
</resources> \ No newline at end of file
diff --git a/app/src/main/res/values/integers.xml b/app/src/main/res/values/integers.xml
index 05bcdb28..db45c591 100644
--- a/app/src/main/res/values/integers.xml
+++ b/app/src/main/res/values/integers.xml
@@ -3,4 +3,5 @@
<integer name="Grid.Columns">2</integer>
<integer name="Grid.FullScreen.Columns">@integer/Grid.Columns</integer>
<integer name="TextDescriptionLength">5</integer>
+ <integer name="Card.Elevation">10</integer>
</resources> \ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 3acbff3e..b54e4ff2 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -32,11 +32,11 @@
<string name="button_bar.home">Home</string>
<string name="button_bar.browse">Library</string>
- <string name="button_bar.search">Search</string>
- <string name="button_bar.playlists">Playlists</string>
+ <string name="button_bar.playlists">Playlists</string>
<string name="button_bar.now_playing">Now Playing</string>
<string name="button_bar.podcasts">Podcasts</string>
<string name="button_bar.bookmarks">Bookmarks</string>
+ <string name="button_bar.internet_radio">Internet Radio</string>
<string name="button_bar.shares">Shares</string>
<string name="button_bar.chat">Chat</string>
<string name="button_bar.admin">Admin</string>
@@ -58,8 +58,7 @@
<br/>In the option menu, deselect "First level artists". This will make it so that the entire first level of directories shown will be treated like groups of artists instead of the artists themselves.
]]>
</string>
- <string name="main.select_server">Select server</string>
- <string name="main.shuffle">Shuffle play</string>
+ <string name="main.shuffle">Shuffle play</string>
<string name="main.offline">Go Offline</string>
<string name="main.online">Go Online</string>
<string name="main.settings">Settings</string>
@@ -76,6 +75,10 @@
<string name="main.albums_alphabetical">Alphabetically</string>
<string name="main.videos">Videos</string>
<string name="main.songs_genres">@string/main.albums_genres</string>
+ <string name="main.songs_newest">@string/main.albums_newest</string>
+ <string name="main.songs_top_played">Top Played</string>
+ <string name="main.songs_recent">@string/main.albums_recent</string>
+ <string name="main.songs_frequent">@string/main.albums_frequent</string>
<string name="main.back_confirm">Press back again to exit</string>
<string name="main.scan_complete">Completed scan of Server</string>
<string name="main.artist">Artist</string>
@@ -100,7 +103,7 @@
<string name="menu.add_podcast">Add Channel</string>
<string name="menu.keep_synced">Keep Synced</string>
<string name="menu.stop_sync">Stop syncing</string>
- <string name="menu.show_all">Show all media</string>
+ <string name="menu.show_all">Show all songs</string>
<string name="menu.show_artist">Show Artist</string>
<string name="menu.share">Share</string>
<string name="menu.delete_cache">Delete Cache</string>
@@ -111,7 +114,7 @@
<string name="menu.rate">Set Rating</string>
<string name="menu.top_tracks">Last.FM Top Tracks</string>
<string name="menu.similar_artists">Similar Artists</string>
- <string name="menu.show_missing">Show missing</string>
+ <string name="menu.similar_artists.missing">Missing Artists</string>
<string name="menu.start_radio">Start Radio</string>
<string name="menu.first_level_artist">First level artists</string>
@@ -131,20 +134,13 @@
<string name="search.artists">Artists</string>
<string name="search.albums">Albums</string>
<string name="search.songs">Songs</string>
- <string name="search.more">Show more</string>
- <string name="progress.wait">Please wait...</string>
+ <string name="progress.wait">Please wait...</string>
<string name="progress.artist_info">Loading Artist Bio</string>
- <string name="music_library.label">Media library</string>
- <string name="music_library.label_offline">Offline media</string>
-
- <string name="select_album.select">Select all</string>
- <string name="select_album.n_selected">%d selected.</string>
- <string name="select_album.more">More</string>
- <string name="select_album.offline">Offline</string>
- <string name="select_album.searching">Searching...</string>
- <string name="select_album.no_sdcard">Error: No SD card available.</string>
+ <string name="select_album.n_selected">%d selected.</string>
+ <string name="select_album.offline">Offline</string>
+ <string name="select_album.no_sdcard">Error: No SD card available.</string>
<string name="select_album.no_network">Warning: No network available.</string>
<string name="select_album.no_room">Warning: you only have %s left</string>
<string name="select_album.not_licensed">Server not licensed. %d trial days left.</string>
@@ -186,7 +182,8 @@
<string name="download.playerstate_downloading">Downloading - %s</string>
<string name="download.playerstate_mobile_disabled">Waiting for WiFi network to download</string>
<string name="download.playerstate_buffering">Buffering</string>
- <string name="download.playerstate_playing_shuffle">Playing shuffle</string>
+ <string name="download.playerstate_playing_shuffle">Shuffle mode</string>
+ <string name="download.playerstate_playing_artist_radio">Artist radio</string>
<string name="download.menu_show_album">Show Album</string>
<string name="download.menu_lyrics">Lyrics</string>
<string name="download.menu_remove_all">Remove all</string>
@@ -204,13 +201,9 @@
<string name="download.repeat_off">Repeat off</string>
<string name="download.repeat_all">Repeat all</string>
<string name="download.repeat_single">Repeat song</string>
- <string name="download.jukebox_on">Turned on remote control. Music is played on the computer.</string>
- <string name="download.jukebox_off">Turned off remote control. Music is played on the phone.</string>
- <string name="download.jukebox_volume">Remote volume</string>
- <string name="download.jukebox_server_too_old">Remote control is not supported. Please upgrade your Subsonic server.</string>
+ <string name="download.jukebox_server_too_old">Remote control is not supported. Please upgrade your Subsonic server.</string>
<string name="download.jukebox_offline">Remote control is not available in offline mode.</string>
<string name="download.jukebox_not_authorized">Remote control is not allowed. Please enable jukebox mode in <b>Users &gt; Settings</b> on your Subsonic server.</string>
- <string name="download.timer_length">Timer:</string>
<string name="download.start_timer">Start Timer</string>
<string name="download.stop_time_remaining">Stop in %1$s</string>
<string name="download.need_download">Video needs to be downloaded first</string>
@@ -227,6 +220,13 @@
<string name="download.restore_play_queue">continue from where you left off on another device at</string>
<string name="download.thumbs_up">Thumbs Up</string>
<string name="download.thumbs_down">Thumbs Down</string>
+ <string name="download.batch_mode">Batch Mode</string>
+ <string name="download.playback_speed_half">0.5x</string>
+ <string name="download.playback_speed_normal">1x</string>
+ <string name="download.playback_speed_one_half">1.5x</string>
+ <string name="download.playback_speed_double">2x</string>
+ <string name="download.playback_speed_tripple">3x</string>
+ <string name="download.playback_speed_custom">Custom</string>
<string name="sync.new_podcasts">New podcasts available</string>
<string name="sync.new_playlists">New songs in playlists</string>
@@ -291,6 +291,8 @@
<string name="settings.cache_location">Cache location</string>
<string name="settings.cache_location_error">Invalid cache location. Using default.</string>
<string name="settings.cache_location_reset">The cache location you have set is no longer writable. If you recently upgraded your phone OS to KitKat 4.4, then the way apps write to the SD Card has changed so that they can only write to a specific location. The location that DSub uses has already been automatically changed to the correct location. In order to delete all of the old app data, you will need to mount the SD Card on your computer and delete the old folder manually</string>
+ <string name="settings.cache_location_internal">Internal</string>
+ <string name="settings.cache_location_external">External</string>
<string name="settings.cache_clear">Clear Cache</string>
<string name="settings.cache_clear_complete">Finished clearing cache</string>
<string name="settings.testing_connection">Testing connection...</string>
@@ -305,6 +307,8 @@
<string name="settings.theme_dark">Dark</string>
<string name="settings.theme_black">Black</string>
<string name="settings.theme_holo">Holo</string>
+ <string name="settings.theme_day_night">Day/Night</string>
+ <string name="settings.theme_day_black_night">Day/Black Night</string>
<string name="settings.theme_fullscreen">Fullscreen</string>
<string name="settings.theme_fullscreen_summary">Hide as many UI elements as Android will allow</string>
<string name="settings.track_title">Display Track #</string>
@@ -317,6 +321,7 @@
<string name="settings.max_bitrate_wifi">Max Audio bitrate - Wi-Fi</string>
<string name="settings.max_bitrate_mobile">Max Audio bitrate - Mobile</string>
<string name="settings.max_bitrate_32">32 Kbps</string>
+ <string name="settings.max_bitrate_48">48 Kbps</string>
<string name="settings.max_bitrate_64">64 Kbps</string>
<string name="settings.max_bitrate_80">80 Kbps</string>
<string name="settings.max_bitrate_96">96 Kbps</string>
@@ -380,7 +385,7 @@
<string name="settings.temp_loss_nothing">Do Nothing</string>
<string name="settings.keep_played_count_title">Keep played songs</string>
<string name="settings.keep_played_count_none">Remove all played songs</string>
- <string name="settings.keep_played_count_one">Keep last played songs</string>
+ <string name="settings.keep_played_count_one">Keep last played song</string>
<string name="settings.keep_played_count_two">Keep 2 played songs</string>
<string name="settings.keep_played_count_three">Keep 3 played songs</string>
<string name="settings.disconnect_pause_title">Pause on Disconnect</string>
@@ -407,6 +412,8 @@
<string name="settings.podcasts_enabled_summary">Whether or not to display the podcast listing in the pull out drawer</string>
<string name="settings.bookmarks_enabled">Bookmarks Enabled</string>
<string name="settings.bookmarks_enabled_summary">Whether or not to display the bookmarks listing in the pull out drawer</string>
+ <string name="settings.internet_radio_enabled">Internet Radio Enabled</string>
+ <string name="settings.internet_radio_enabled_summary">Whether or not to display the internet radio listing in the pull out drawer</string>
<string name="settings.shares_enabled">Shares Enabled</string>
<string name="settings.shares_enabled_summary">Whether or not to display the shares listing in the pull out drawer</string>
<string name="settings.sync_title">Sync</string>
@@ -447,8 +454,11 @@
<string name="settings.override_system_language">Override System Language</string>
<string name="settings.override_system_language_summary">Display app in english even if the system language is something DSub has a translation for. May need to clear the app from memory for changes to take affect.</string>
<string name="settings.drawer_items_title">Drawer Tabs</string>
- <string name="settings.play_now_after">Play Now - After</string>
- <string name="settings.play_now_after_summary">Play Now context menu for a song plays everything after selected item (like the Subsonic web GUI)</string>
+ <string name="settings.song_press_action">Song Press Action</string>
+ <string name="settings.song_press_play_single">Play only that song</string>
+ <string name="settings.song_press_play_all">Adds everything in the album to the Now Playing Queue</string>
+ <string name="settings.song_press_play_next">Adds song as next song</string>
+ <string name="settings.song_press_play_last">Adds song to end of Now Playing Queue</string>
<string name="settings.large_album_art">Large Album Art</string>
<string name="settings.large_album_art_summary">Display albums with large album art instead of in a list</string>
<string name="settings.admin_enabled">Admin Enabled</string>
@@ -473,6 +483,14 @@
<string name="settings.shuffle_by_album">Shuffle By Album</string>
<string name="settings.shuffle_by_album.true">Shuffle order of albums</string>
<string name="settings.shuffle_by_album.false">Shuffle all songs together</string>
+ <string name="settings.casting_stream_original">Stream original</string>
+ <string name="settings.casting_stream_original_summary">Stream original files where supported by cast device</string>
+ <string name="settings.heads_up_notification">Heads Up Notifications (5.0+)</string>
+ <string name="settings.heads_up_notification_summary">Show playing notifications as Heads Up notifications (Android Lollipop+ only)</string>
+ <string name="settings.casting_cache">Cache While Casting</string>
+ <string name="settings.casting_cache_summary">Cache currently playing songs while casting</string>
+ <string name="settings.casting.dlna_casting_enabled">DLNA Enabled</string>
+ <string name="settings.casting.dlna_casting_enabled.summary">If you are having battery drain problems on Android 7.0 try turning this off</string>
<string name="shuffle.title">Shuffle By</string>
<string name="shuffle.startYear">Start Year:</string>
@@ -480,15 +498,6 @@
<string name="shuffle.genre">Genre:</string>
<string name="shuffle.pick_genre">Pick a genre</string>
- <string name="share.info">Owner: %1$s
- \nDescription: %2$s
- \nURL: %3$s
- \nCreation: %4$s
- \nLast Visited: %5$s
- \nExpiration: %6$s
- \nVisit Count: %7$s
-
- </string>
<string name="share.expires">Expires: %s</string>
<string name="share.expires_never">Never Expires</string>
<string name="share.deleted">Deleted share %s</string>
@@ -517,6 +526,7 @@
<string name="admin.change_password">Change Password</string>
<string name="admin.change_password_success">Successfully changed password for %1$s</string>
<string name="admin.change_password_error">Failed to change password for %1$s</string>
+ <string name="admin.change_password_current_label">Current Password:</string>
<string name="admin.change_password_label">New Password:</string>
<string name="admin.change_password_invalid">Enter a valid password</string>
<string name="admin.delete_user">Delete User</string>
@@ -524,6 +534,8 @@
<string name="admin.delete_user_error">Failed to delete %1$s</string>
<string name="admin.confirm_password">Confirm Password</string>
<string name="admin.confirm_password_bad">Entered password is wrong</string>
+ <string name="admin.permissions">Permissions</string>
+ <string name="admin.musicFolders">Music Folders</string>
<string name="admin.scrobblingEnabled">Scrobbling allowed</string>
<string name="admin.role.admin">Administrator</string>
@@ -536,13 +548,12 @@
<string name="admin.role.stream">Stream music</string>
<string name="admin.role.jukebox">Control jukebox</string>
<string name="admin.role.share">Manage shares</string>
+ <string name="admin.role.video_conversion">Convert videos</string>
<string name="admin.role.lastfm">Use Last.FM feature</string>
<string name="music_service.retry">A network error occurred. Retrying %1$d of %2$d.</string>
- <string name="background_task.wait">Please wait...</string>
- <string name="background_task.loading">Loading.</string>
- <string name="background_task.no_network">This program requires network access. Please turn on Wi-Fi or mobile network.</string>
+ <string name="background_task.no_network">This program requires network access. Please turn on Wi-Fi or mobile network.</string>
<string name="background_task.network_error">A network error occurred. Please check the server address or try again later.</string>
<string name="background_task.not_found">Resource not found. Please check the server address.</string>
<string name="background_task.parse_error">A problem occurred communicating with the server. Please check the server address and verify that you can connect using a regular browser on your device.</string>
@@ -557,8 +568,7 @@
<string name="parser.server_error">Server error: %s</string>
<string name="parser.scan_count">Scanned %d entries</string>
- <string name="select_artist.refresh">Refresh</string>
- <string name="select_artist.folder">Select folder</string>
+ <string name="select_artist.folder">Select folder</string>
<string name="select_artist.all_folders">All folders</string>
<string name="equalizer.label">Equalizer</string>
@@ -589,7 +599,7 @@
<string name="chat.send_a_message">Send a message</string>
- <string name="changelog_version_format" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">Version <xliff:g id="version_name">%s</xliff:g></string>
+ <string name="changelog_version_format">Version %s</string>
<string name="tasker.start_playing">Start playing</string>
<string name="tasker.start_playing_shuffled">Start playing in Shuffle Mode</string>
@@ -606,6 +616,7 @@
<string name="details.title.podcast">Podcast Details</string>
<string name="details.title.playlist">Playlist Details</string>
<string name="details.title.artist">Artist Details</string>
+ <string name="details.title.internet_radio_station">Internet Radio Details</string>
<string name="details.podcast">Podcast</string>
<string name="details.status">Status</string>
<string name="details.artist">Artist</string>
@@ -643,6 +654,12 @@
<string name="details.updated">Updated</string>
<string name="details.starred">Starred</string>
<string name="details.last_played">Last Played</string>
+ <string name="details.expiration">Expiration</string>
+ <string name="details.played_count">Played Count</string>
+ <string name="details.stream_url">Stream URL</string>
+ <string name="details.home_page">Home Page</string>
+
+ <string name="permission.external_storage.failed">DSub cannot function without the ability to write to storage</string>
<plurals name="select_album_n_songs">
<item quantity="zero">No songs</item>
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 0b45b69e..04d92fa5 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -37,8 +37,8 @@
<item name="android:background">?android:dividerHorizontal</item>
</style>
- <style name="MoreButton" parent="BasicButton">
- <item name="android:paddingRight">14dip</item>
+ <style name="MoreButton" parent="@style/BasicButton">
+ <item name="android:paddingRight">2dp</item>
</style>
<style name="PlaybackControl" parent="@style/BasicButton">
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 6321852f..8cccab93 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -4,6 +4,8 @@
<item name="offline_icon">@drawable/main_offline_light</item>
<item name="media_button_backward">@drawable/media_backward_light</item>
<item name="media_button_forward">@drawable/media_forward_light</item>
+ <item name="media_button_rewind">@drawable/media_rewind_light</item>
+ <item name="media_button_fastforward">@drawable/media_fastforward_light</item>
<item name="media_button_pause">@drawable/media_pause_light</item>
<item name="media_button_repeat_off">@drawable/media_repeat_off_light</item>
<item name="media_button_repeat_single">@drawable/media_repeat_single_light</item>
@@ -15,6 +17,8 @@
<item name="actionbar_pause">@drawable/media_pause_dark</item>
<item name="actionbar_start">@drawable/media_start_dark</item>
<item name="actionbar_stop">@drawable/media_stop_dark</item>
+ <item name="actionbar_rewind">@drawable/media_rewind_dark</item>
+ <item name="actionbar_fastforward">@drawable/media_fastforward_dark</item>
<item name="chat_send">@drawable/ic_menu_chat_send_light</item>
<item name="add">@drawable/ic_action_add_dark</item>
<item name="download_none">@drawable/download_none_light</item>
@@ -38,11 +42,14 @@
<item name="rating_good">@drawable/ic_action_rating_good_light</item>
<item name="radio">@drawable/ic_menu_radio_dark</item>
<item name="star_outline">@drawable/ic_toggle_star_outline_light</item>
+ <item name="download">@drawable/ic_menu_download_dark</item>
+ <item name="playback_speed">@drawable/ic_action_playback_speed_light</item>
<item name="drawerHome">@drawable/main_offline_light</item>
<item name="drawerLibrary">@drawable/ic_menu_library_light</item>
<item name="drawerPlaylists">@drawable/ic_menu_playlist_light</item>
<item name="drawerPodcasts">@drawable/ic_menu_podcast_light</item>
<item name="drawerBookmarks">@drawable/ic_menu_bookmark_light</item>
+ <item name="drawerInternetRadioStations">@drawable/ic_menu_radio_light</item>
<item name="drawerShares">@drawable/ic_menu_share_light</item>
<item name="drawerChat">@drawable/ic_menu_chat_light</item>
<item name="drawerAdmin">@drawable/ic_menu_admin_light</item>
@@ -62,11 +69,15 @@
<item name="drawerSubtitleStyle">@style/TextAppearance.AppCompat.Widget.ActionBar.Subtitle.Inverse</item>
<item name="windowActionModeOverlay">true</item>
<item name="actionModeBackground">?attr/colorPrimary</item>
+ <item name="cardBackgroundDrawable">@drawable/card_rounded_corners_light</item>
+ <item name="drawerHeaderBackground">@drawable/drawer_header_light</item>
</style>
<style name="Theme.DSub.Dark.Base" parent="@style/Theme.AppCompat">
<item name="offline_icon">@drawable/main_offline_dark</item>
<item name="media_button_backward">@drawable/media_backward_dark</item>
<item name="media_button_forward">@drawable/media_forward_dark</item>
+ <item name="media_button_rewind">@drawable/media_rewind_dark</item>
+ <item name="media_button_fastforward">@drawable/media_fastforward_dark</item>
<item name="media_button_pause">@drawable/media_pause_dark</item>
<item name="media_button_repeat_off">@drawable/media_repeat_off_dark</item>
<item name="media_button_repeat_single">@drawable/media_repeat_single_dark</item>
@@ -78,6 +89,8 @@
<item name="actionbar_pause">@drawable/media_pause_dark</item>
<item name="actionbar_start">@drawable/media_start_dark</item>
<item name="actionbar_stop">@drawable/media_stop_dark</item>
+ <item name="actionbar_rewind">@drawable/media_rewind_dark</item>
+ <item name="actionbar_fastforward">@drawable/media_fastforward_dark</item>
<item name="chat_send">@drawable/ic_menu_chat_send_dark</item>
<item name="add">@drawable/ic_action_add_dark</item>
<item name="download_none">@drawable/download_none_dark</item>
@@ -101,11 +114,14 @@
<item name="rating_good">@drawable/ic_action_rating_good_dark</item>
<item name="radio">@drawable/ic_menu_radio_dark</item>
<item name="star_outline">@drawable/ic_toggle_star_outline_dark</item>
+ <item name="download">@drawable/ic_menu_download_dark</item>
+ <item name="playback_speed">@drawable/ic_action_playback_speed_dark</item>
<item name="drawerHome">@drawable/main_offline_dark</item>
<item name="drawerLibrary">@drawable/ic_menu_library_dark</item>
<item name="drawerPlaylists">@drawable/ic_menu_playlist_dark</item>
<item name="drawerPodcasts">@drawable/ic_menu_podcast_dark</item>
<item name="drawerBookmarks">@drawable/ic_menu_bookmark_dark</item>
+ <item name="drawerInternetRadioStations">@drawable/ic_menu_radio_dark</item>
<item name="drawerShares">@drawable/ic_menu_share_dark</item>
<item name="drawerChat">@drawable/ic_menu_chat_dark</item>
<item name="drawerAdmin">@drawable/ic_menu_admin_dark</item>
@@ -124,20 +140,29 @@
<item name="drawerSubtitleStyle">@style/TextAppearance.AppCompat.Widget.ActionBar.Subtitle</item>
<item name="windowActionModeOverlay">true</item>
<item name="actionModeBackground">?attr/colorPrimary</item>
+ <item name="cardBackgroundDrawable">@drawable/card_rounded_corners_dark</item>
+ <item name="drawerHeaderBackground">@drawable/drawer_header_dark</item>
</style>
<style name="Theme.DSub.Light" parent="Theme.DSub.Light.Base">
</style>
<style name="Theme.DSub.Dark" parent="Theme.DSub.Dark.Base">
</style>
- <style name="Theme.DSub.Black" parent="Theme.DSub.Dark">
+ <style name="Theme.DSub.Black.Base" parent="Theme.DSub.Dark.Base">
+ <item name="cardBackgroundDrawable">@drawable/card_rounded_corners_black</item>
<item name="android:windowBackground">@android:color/black</item>
</style>
- <style name="Theme.DSub.Holo" parent="Theme.DSub.Dark">
+ <style name="Theme.DSub.Black" parent="Theme.DSub.Black.Base">
+ </style>
+ <style name="Theme.DSub.Holo.Base" parent="Theme.DSub.Dark.Base">
<item name="android:windowBackground">@drawable/background</item>
<item name="colorPrimary">@color/holoPrimary</item>
<item name="colorPrimaryDark">@color/holoPrimaryDark</item>
<item name="colorAccent">@color/holoAccent</item>
<item name="actionbarBackgroundColor">@color/holoPrimary</item>
+ <item name="drawerHeaderBackground">@drawable/drawer_header_holo</item>
+ <item name="cardBackgroundDrawable">@drawable/card_rounded_corners_black</item>
+ </style>
+ <style name="Theme.DSub.Holo" parent="Theme.DSub.Holo.Base">
</style>
<style name="Theme.DSub.Light.No_Actionbar" parent="Theme.DSub.Light">
@@ -170,6 +195,8 @@
<item name="actionbar_pause">@drawable/media_pause_light</item>
<item name="actionbar_start">@drawable/media_start_light</item>
<item name="actionbar_stop">@drawable/media_stop_light</item>
+ <item name="actionbar_rewind">@drawable/media_rewind_light</item>
+ <item name="actionbar_fastforward">@drawable/media_fastforward_light</item>
<item name="add">@drawable/ic_action_add_light</item>
<item name="shuffle">@drawable/ic_menu_shuffle_light</item>
<item name="refresh">@drawable/ic_menu_refresh_light</item>
@@ -179,6 +206,7 @@
<item name="add_person">@drawable/ic_menu_add_person_light</item>
<item name="password">@drawable/ic_menu_password_light</item>
<item name="radio">@drawable/ic_menu_radio_light</item>
+ <item name="download">@drawable/ic_menu_download_light</item>
<item name="actionModeBackground">@color/background_material_light</item>
<item name="actionModeStyle">@style/LightActionMode</item>
<item name="actionModeCloseButtonStyle">@style/DarkCloseButton</item>
diff --git a/app/src/main/res/xml/changelog.xml b/app/src/main/res/xml/changelog.xml
index 39fc7496..d31f6df6 100644
--- a/app/src/main/res/xml/changelog.xml
+++ b/app/src/main/res/xml/changelog.xml
@@ -1,5 +1,153 @@
<?xml version="1.0" encoding="utf-8"?>
<changelog>
+ <release version="5.3.5" versioncode="195" releasedate="04/18/2016">
+ <change>Add option to scan server for Subsonic 6.1+</change>
+ <change>Enhanced custom playback speed UI (thanks SilentViking)</change>
+ <change>Keep custom playback speed between songs (thanks SilentViking)</change>
+ <change>Auto: Browse full library while parked (thanks hufman)</change>
+ <change>Add bitrate option for 48kpbs</change>
+ <change>Only download playlist when setting to sync if on Wifi</change>
+ </release>
+ <release version="5.3.4" versioncode="193" releasedate="11/28/2016">
+ <change>Added toggle for DLNA casting to fix battery issues some users are having on Android 7.0+</change>
+ <change>Fix rotating screen in offline mode</change>
+ </release>
+ <release version="5.3.3" versioncode="191" releasedate="11/9/2016">
+ <change>Add support for Android 7's split screen UI</change>
+ <change>Fix for Android 7 not playing</change>
+ <change>Fix basic authentication for servers</change>
+ <change>Fix top track songs using different save file</change>
+ </release>
+ <release version="5.3.2" versioncode="190" releasedate="10/22/2016">
+ <change>Add support for casting Internet Radio to ChromeCast/DLNA</change>
+ <change>Add support for Play Title by Artist from Google Search</change>
+ <change>Move to more modern connection framework</change>
+ <change>Use Google Play SSL</change>
+ <change>Show album instead of artist for Show all media</change>
+ <change>Ask for location permissions for Day/Night themes</change>
+ <change>Fix a change to the ChromeCast API</change>
+ <change>Fix Show all media sometimes failing</change>
+ </release>
+ <release version="5.3.1" versioncode="187" releasedate="10/4/2016">
+ <change>Fix Internet Radio streams which point to playlists</change>
+ <change>Don't show playback speed button below Android 6.0</change>
+ </release>
+ <release version="5.3" versioncode="186" releasedate="9/23/2016">
+ <change>Listen to Radio Internet Stations</change>
+ <change>Automatic Day/Night theme</change>
+ <change>Android 6.0 runtime permissions</change>
+ <change>Custom variable playback speed</change>
+ <change>Add Play Now for songs when click action is add to queue</change>
+ <change>More secure connections with tokens (Subsonic 6+)</change>
+ <change>Auto skip uncached songs when no network</change>
+ <change>Shrink install size</change>
+ <change>Fix DLNA casting on Android 7.0+</change>
+ </release>
+ <release version="5.2.2" versioncode="184" releasedate="8/30/2016">
+ <change>Fix lagging in landscape view on the Now Playing screen</change>
+ </release>
+ <release version="5.2.1" versioncode="183" releasedate="8/24/2016">
+ <change>Add option for different actions when pressing a song</change>
+ <change>Fix some N release issues</change>
+ <change>Fix grid alignment for some albums</change>
+ </release>
+ <release version="5.2" versioncode="182" releasedate="7/27/2016">
+ <change>Variable Playback Speed (Android 6.0+)</change>
+ <change>Show album instead of artist on Top Tracks</change>
+ <change>Fix search/comments with ' not working</change>
+ <change>Fix double press to skip from some headsets</change>
+ <change>Fix newer versions of Madsonic's Artist Radio</change>
+ <change>Fix no SSID using local network address on all WiFi connections</change>
+ <change>Fix rotation/sleep while EQ was visible</change>
+ <change>Fix being able to add albums to playlists</change>
+ <change>Fix cast dialog in landscape</change>
+ </release>
+ <release version="5.1.9" versioncode="181" releasedate="6/13/2016">
+ <change>Fix First level artists option</change>
+ <change>Fix some artist menu items missing</change>
+ </release>
+ <release version="5.1.8" versioncode="179" releasedate="6/9/2016">
+ <change>Improved Search Bar</change>
+ <change>Display songs in root folder</change>
+ <change>Heads Up Setting: Only when out of app</change>
+ <change>Speed up resuming app from notifications/widgets</change>
+ <change>Bluetooth: Double click play/pause button to skip to next some</change>
+ <change>Auto: Previous/Next rewind/fast forward on Podcasts/Audio Books</change>
+ <change>Fix playlist not overwriting when resumed from bookmark</change>
+ <change>Fix some bluetooth devices sending next/previous multiple times</change>
+ <change>Fix reverting to stock Subsonic after using Madsonic 6+ servers</change>
+ <change>Fix Podcast list cache not working with no internet</change>
+ </release>
+ <release version="5.1.7" versioncode="177" releasedate="4/22/2016">
+ <change>Audio Books/Podcasts: Replace back/forward with rewind/fast forward buttons</change>
+ <change>Expandable Search Results</change>
+ <change>Add option to cache while casting</change>
+ <change>Add optional Heads Up Notifications (Lollipop+)</change>
+ <change>Clicking bookmarked song plays entire album from bookmarked position</change>
+ <change>Auto: Remove podcast/album limit now that Google removed limit</change>
+ <change>Fix tall album art running into controls</change>
+ <change>Fix Show Artist not being an option when browsing By Tags</change>
+ <change>Fix 1-star song background on darker themes</change>
+ <change>Fix Bluetooth multiple skips</change>
+ </release>
+ <release version="5.1.6" versioncode="175" releasedate="3/16/2016">
+ <change>Podcasts/Audio Books: Show listened indicator</change>
+ <change>Podcasts/Audio Books: Improve what is considered fully played</change>
+ <change>Improved Artist sorting to closer match Web GUI</change>
+ <change>Show Artist/Album from Search and Album Lists</change>
+ <change>Videos: ability to star</change>
+ <change>Admin: Show/update Video Conversion role (Subsonic 6.0+)</change>
+ <change>Stop background sync if lose Wifi</change>
+ <change>Fix upsampling mp3s when cache rate is set to unlimited</change>
+ <change>Fix "Failed to create artist radio" error</change>
+ <change>Fix rare issue causing arrow to get stuck</change>
+ <change>Fix rare issue with background download getting stuck</change>
+ </release>
+ <release version="5.1.5" versioncode="173" releasedate="2/15/2016">
+ <change>Toggle for Batch Mode on Now Playing</change>
+ <change>Admin tab: view/update music folders (Subsonic 5.2+)</change>
+ <change>Tag Browsing: can specify Music Folder (Subsonic 6.0+)</change>
+ <change>Show shuffle/radio mode in title bar</change>
+ <change>Madsonic only: Songs lists on Home tab</change>
+ <change>Clicking songs adds songs before it as well</change>
+ <change>Similar Artists: show missing artists below main list</change>
+ <change>Improve offline search (thanks fxthomas)</change>
+ <change>Improve podcast date formatting</change>
+ <change>Increase max sleep timer to 1 hour</change>
+ <change>Cast: added setting to disable DLNA gapless playback</change>
+ <change>Cast: added setting to stream original source files</change>
+ <change>Long press details dialogs to copy information</change>
+ <change>Android Auto improvements</change>
+ <change>Top Tracks: display #</change>
+ <change>Require current password when changing password</change>
+ <change>Improved Share Details dialog</change>
+ <change>Sort Music Folders alphabetically</change>
+ <change>Fix repeating same song if multiples copies in play queue</change>
+ <change>Fix freeze on Play x from Google Now</change>
+ <change>Fix Show Artist/Album back behavior</change>
+ <change>Minor Chromecast fixes</change>
+ </release>
+ <release version="5.1.4" versioncode="172" releasedate="1/17/2016">
+ <change>Minor theme improvements</change>
+ </release>
+ <release version="5.1.3" versioncode="169" releasedate="1/11/2016">
+ <change>Display albums in cards</change>
+ <change>Improve ActionBar dropdown</change>
+ <change>Add Portuguese and Dutch translations</change>
+ <change>Add batch Star/Unstar</change>
+ <change>Add quick Internal/External buttons when changing Cache Location</change>
+ <change>Auto add bookmarks on pause for Podcasts/Audio Books</change>
+ <change>Android Auto: Add Podcasts, Album Lists, and Bookmarks tabs</change>
+ <change>Add more spacing between Track # and Title</change>
+ <change>Go directly to synced Playlist/Podcast on Sync notification</change>
+ <change>Themed drawer header images</change>
+ <change>Update Cover Art in background refresh</change>
+ <change>Fix Repeat All in Jukebox mode</change>
+ <change>Fix rotating re-opening Now Playing from widget</change>
+ <change>Fix some DLNA issues</change>
+ <change>Fix Black/Holo themes not having drawer behind notification panel</change>
+ <change>Fix songs in Album Lists for Madsonic</change>
+ </release>
<release version="5.1.2" versioncode="167" releasedate="12/17/2015">
<change>Keep track of played songs locally</change>
<change>Improved DLNA/Chromecast casting</change>
diff --git a/app/src/main/res/xml/settings.xml b/app/src/main/res/xml/settings.xml
index ac247c8f..a067130a 100644
--- a/app/src/main/res/xml/settings.xml
+++ b/app/src/main/res/xml/settings.xml
@@ -31,4 +31,9 @@
android:title="@string/settings.playback_title"
android:key="playback">
</PreferenceScreen>
+
+ <PreferenceScreen
+ android:title="@string/settings.casting"
+ android:key="cast">
+ </PreferenceScreen>
</PreferenceScreen>
diff --git a/app/src/main/res/xml/settings_appearance.xml b/app/src/main/res/xml/settings_appearance.xml
index 5e355526..530d92a1 100644
--- a/app/src/main/res/xml/settings_appearance.xml
+++ b/app/src/main/res/xml/settings_appearance.xml
@@ -53,14 +53,6 @@
android:key="renameDuplicates"
android:defaultValue="true"/>
- <ListPreference
- android:title="@string/settings.open_to_tab"
- android:summary="@string/settings.open_to_tab_summary"
- android:key="openToTab"
- android:entryValues="@array/defaultDrawerItemsDescriptions"
- android:entries="@array/defaultDrawerItems"
- android:defaultValue="Home"/>
-
<CheckBoxPreference
android:title="@string/settings.disable_exit_prompt"
android:summary="@string/settings.disable_exit_prompt_summary"
diff --git a/app/src/main/res/xml/settings_cache.xml b/app/src/main/res/xml/settings_cache.xml
index bb5710b2..248572ca 100644
--- a/app/src/main/res/xml/settings_cache.xml
+++ b/app/src/main/res/xml/settings_cache.xml
@@ -57,7 +57,7 @@
android:defaultValue="2000"
android:digits="0123456789"/>
- <EditTextPreference
+ <github.daneren2005.dsub.view.CacheLocationPreference
android:title="@string/settings.cache_location"
android:key="cacheLocation"/>
diff --git a/app/src/main/res/xml/settings_cast.xml b/app/src/main/res/xml/settings_cast.xml
new file mode 100644
index 00000000..78bafdd4
--- /dev/null
+++ b/app/src/main/res/xml/settings_cast.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+ android:title="@string/settings.casting">
+ <PreferenceCategory
+ android:title="@string/settings.casting">
+
+ <CheckBoxPreference
+ android:title="@string/settings.casting_proxy"
+ android:summary="@string/settings.casting_proxy_summary"
+ android:key="castProxy"
+ android:defaultValue="false"/>
+
+ <CheckBoxPreference
+ android:title="@string/settings.gapless_playback"
+ android:summary="@string/settings.gapless_playback_summary"
+ android:key="castingGaplessPlayback"
+ android:defaultValue="true"/>
+
+ <CheckBoxPreference
+ android:title="@string/settings.casting_stream_original"
+ android:summary="@string/settings.casting_stream_original_summary"
+ android:key="castStreamOriginal"
+ android:defaultValue="true"/>
+
+ <CheckBoxPreference
+ android:title="@string/settings.casting_cache"
+ android:summary="@string/settings.casting_cache_summary"
+ android:key="castCache"
+ android:defaultValue="false"/>
+ </PreferenceCategory>
+
+ <PreferenceCategory
+ android:title="@string/settings.other_title">
+
+ <CheckBoxPreference
+ android:title="@string/settings.casting.dlna_casting_enabled"
+ android:summary="@string/settings.casting.dlna_casting_enabled.summary"
+ android:key="dlnaCastingEnabled"
+ android:defaultValue="true"/>
+ </PreferenceCategory>
+</PreferenceScreen> \ No newline at end of file
diff --git a/app/src/main/res/xml/settings_drawer.xml b/app/src/main/res/xml/settings_drawer.xml
index a874881a..f89fb990 100644
--- a/app/src/main/res/xml/settings_drawer.xml
+++ b/app/src/main/res/xml/settings_drawer.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:myns="http://schemas.android.com/apk/res/github.daneren2005.dsub"
+ xmlns:myns="http://schemas.android.com/apk/res-auto"
android:title="@string/settings.drawer_items_title">
<PreferenceCategory
@@ -19,6 +19,12 @@
android:defaultValue="true"/>
<CheckBoxPreference
+ android:title="@string/settings.internet_radio_enabled"
+ android:summary="@string/settings.internet_radio_enabled_summary"
+ android:key="internetRadioEnabled"
+ android:defaultValue="true"/>
+
+ <CheckBoxPreference
android:title="@string/settings.shares_enabled"
android:summary="@string/settings.shares_enabled_summary"
android:key="sharedEnabled"
@@ -38,7 +44,15 @@
</PreferenceCategory>
<PreferenceCategory
- android:title="@string/button_bar.chat">
+ android:title="@string/settings.other_title">
+
+ <ListPreference
+ android:title="@string/settings.open_to_tab"
+ android:summary="@string/settings.open_to_tab_summary"
+ android:key="openToTab"
+ android:entryValues="@array/defaultDrawerItemsDescriptions"
+ android:entries="@array/defaultDrawerItems"
+ android:defaultValue="Home"/>
<github.daneren2005.dsub.view.SeekBarPreference
android:title="@string/settings.chat_refresh"
diff --git a/app/src/main/res/xml/settings_playback.xml b/app/src/main/res/xml/settings_playback.xml
index 3c505b6e..da31d071 100644
--- a/app/src/main/res/xml/settings_playback.xml
+++ b/app/src/main/res/xml/settings_playback.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:myns="http://schemas.android.com/apk/res/github.daneren2005.dsub"
+ xmlns:myns="http://schemas.android.com/apk/res-auto"
android:title="@string/settings.playback_title">
<PreferenceCategory
@@ -41,10 +41,17 @@
android:defaultValue="false"/>
<CheckBoxPreference
- android:title="@string/settings.play_now_after"
- android:summary="@string/settings.play_now_after_summary"
- android:key="playNowAfter"
- android:defaultValue="true"/>
+ android:title="@string/settings.heads_up_notification"
+ android:summary="@string/settings.heads_up_notification_summary"
+ android:key="headsUpNotification"
+ android:defaultValue="false"/>
+
+ <ListPreference
+ android:title="@string/settings.song_press_action"
+ android:key="songPressAction"
+ android:defaultValue="all"
+ android:entryValues="@array/songPressActionValues"
+ android:entries="@array/songPressActionNames"/>
</PreferenceCategory>
<PreferenceCategory
@@ -59,16 +66,6 @@
</PreferenceCategory>
<PreferenceCategory
- android:title="@string/settings.casting">
-
- <CheckBoxPreference
- android:title="@string/settings.casting_proxy"
- android:summary="@string/settings.casting_proxy_summary"
- android:key="castProxy"
- android:defaultValue="false"/>
- </PreferenceCategory>
-
- <PreferenceCategory
android:title="@string/settings.replay_gain">
<CheckBoxPreference