aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorScott Jackson <daneren2005@gmail.com>2013-07-27 14:36:03 -0700
committerScott Jackson <daneren2005@gmail.com>2013-07-27 14:36:03 -0700
commitfc19957642783f8d6f65e9eb24d89efcf9c900eb (patch)
tree342a8f3af72b050a2f7dbd6f089bca8c11cfb0c1
parent4738428c2c205f42200386ae09b44b9ec07b9144 (diff)
downloaddsub-fc19957642783f8d6f65e9eb24d89efcf9c900eb.tar.gz
dsub-fc19957642783f8d6f65e9eb24d89efcf9c900eb.tar.bz2
dsub-fc19957642783f8d6f65e9eb24d89efcf9c900eb.zip
Missed in commit
-rw-r--r--AndroidManifest.xml148
-rw-r--r--Subsonic.iml21
-rw-r--r--ant.properties20
-rw-r--r--assets/fonts/Storopia.ttfbin0 -> 89888 bytes
-rw-r--r--assets/html/en/index.html98
-rw-r--r--assets/html/fr/index.html100
-rw-r--r--assets/html/img/paypal.gifbin0 -> 2127 bytes
-rw-r--r--assets/html/img/subsonic.pngbin0 -> 2084 bytes
-rw-r--r--assets/html/ru/index.html98
-rw-r--r--assets/html/style.css11
-rw-r--r--build.properties0
-rw-r--r--build.xml92
-rw-r--r--debug.keystorebin0 -> 1268 bytes
-rw-r--r--default.properties0
-rw-r--r--libs/CWAC-AdapterWrapper.jarbin0 -> 4841 bytes
-rw-r--r--libs/CWAC-EndlessAdapter.jarbin0 -> 5317 bytes
-rw-r--r--libs/android-support-v4.jarbin0 -> 385685 bytes
-rw-r--r--proguard-project.txt20
-rw-r--r--proguard.cfg40
-rw-r--r--project.properties13
-rw-r--r--releases/DSub 4.0.1.apkbin0 -> 1268342 bytes
-rw-r--r--releases/DSub 4.0.2.apkbin0 -> 1269700 bytes
-rw-r--r--releases/DSub 4.0.3.apkbin0 -> 1274029 bytes
-rw-r--r--releases/DSub 4.0.4.apkbin0 -> 1289208 bytes
-rw-r--r--releases/DSub 4.0.5.apkbin0 -> 1288993 bytes
-rw-r--r--releases/DSub 4.0.6.apkbin0 -> 1297293 bytes
-rw-r--r--releases/DSub 4.0.7.apkbin0 -> 1310165 bytes
-rw-r--r--releases/DSub 4.1.0.apkbin0 -> 1315422 bytes
-rw-r--r--releases/DSub 4.1.1.apkbin0 -> 1315520 bytes
-rw-r--r--releases/DSub 4.1.2.apkbin0 -> 1266662 bytes
-rw-r--r--res/anim/push_down_in.xml22
-rw-r--r--res/anim/push_down_out.xml22
-rw-r--r--res/anim/push_up_in.xml22
-rw-r--r--res/anim/push_up_out.xml22
-rw-r--r--res/drawable-hdpi-v4/action_browse.pngbin0 -> 1673 bytes
-rw-r--r--res/drawable-hdpi-v4/action_compass.pngbin0 -> 2069 bytes
-rw-r--r--res/drawable-hdpi-v4/action_exit.pngbin0 -> 923 bytes
-rw-r--r--res/drawable-hdpi-v4/action_help.pngbin0 -> 1197 bytes
-rw-r--r--res/drawable-hdpi-v4/action_moreoverflow.pngbin0 -> 126 bytes
-rw-r--r--res/drawable-hdpi-v4/action_offline.pngbin0 -> 1558 bytes
-rw-r--r--res/drawable-hdpi-v4/action_play_all.pngbin0 -> 1359 bytes
-rw-r--r--res/drawable-hdpi-v4/action_refresh.pngbin0 -> 1820 bytes
-rw-r--r--res/drawable-hdpi-v4/action_remove_all.pngbin0 -> 1201 bytes
-rw-r--r--res/drawable-hdpi-v4/action_save.pngbin0 -> 1044 bytes
-rw-r--r--res/drawable-hdpi-v4/action_screen_on_off.pngbin0 -> 1525 bytes
-rw-r--r--res/drawable-hdpi-v4/action_search.pngbin0 -> 1592 bytes
-rw-r--r--res/drawable-hdpi-v4/action_select.pngbin0 -> 1158 bytes
-rw-r--r--res/drawable-hdpi-v4/action_settings.pngbin0 -> 1649 bytes
-rw-r--r--res/drawable-hdpi-v4/action_share.pngbin0 -> 1823 bytes
-rw-r--r--res/drawable-hdpi-v4/action_shuffle.pngbin0 -> 1386 bytes
-rw-r--r--res/drawable-hdpi-v4/action_toggle_list.pngbin0 -> 552 bytes
-rw-r--r--res/drawable-hdpi-v4/actionbar_background.9.pngbin0 -> 234 bytes
-rw-r--r--res/drawable-hdpi-v4/actionbar_button_normal.9.pngbin0 -> 208 bytes
-rw-r--r--res/drawable-hdpi-v4/album_art_background.pngbin0 -> 9287 bytes
-rw-r--r--res/drawable-hdpi-v4/appwidget_art_default.pngbin0 -> 3711 bytes
-rw-r--r--res/drawable-hdpi-v4/appwidget_art_unknown.pngbin0 -> 3711 bytes
-rw-r--r--res/drawable-hdpi-v4/appwidget_bg.9.pngbin0 -> 489 bytes
-rw-r--r--res/drawable-hdpi-v4/background.pngbin0 -> 1701 bytes
-rw-r--r--res/drawable-hdpi-v4/btn_check_buttonless_off.pngbin0 -> 762 bytes
-rw-r--r--res/drawable-hdpi-v4/btn_check_buttonless_on.pngbin0 -> 2996 bytes
-rw-r--r--res/drawable-hdpi-v4/downloaded.pngbin0 -> 799 bytes
-rw-r--r--res/drawable-hdpi-v4/downloading.pngbin0 -> 457 bytes
-rw-r--r--res/drawable-hdpi-v4/ic_appwidget_music_next.pngbin0 -> 489 bytes
-rw-r--r--res/drawable-hdpi-v4/ic_appwidget_music_pause.pngbin0 -> 232 bytes
-rw-r--r--res/drawable-hdpi-v4/ic_appwidget_music_play.pngbin0 -> 344 bytes
-rw-r--r--res/drawable-hdpi-v4/ic_appwidget_music_previous.pngbin0 -> 666 bytes
-rw-r--r--res/drawable-hdpi-v4/ic_menu_chat_dark.pngbin0 -> 1401 bytes
-rw-r--r--res/drawable-hdpi-v4/ic_menu_chat_light.pngbin0 -> 1374 bytes
-rw-r--r--res/drawable-hdpi-v4/ic_menu_chat_send_dark.pngbin0 -> 1668 bytes
-rw-r--r--res/drawable-hdpi-v4/ic_menu_chat_send_light.pngbin0 -> 1691 bytes
-rw-r--r--res/drawable-hdpi-v4/ic_menu_exit.pngbin0 -> 5689 bytes
-rw-r--r--res/drawable-hdpi-v4/ic_menu_help.pngbin0 -> 5250 bytes
-rw-r--r--res/drawable-hdpi-v4/ic_menu_settings.pngbin0 -> 5455 bytes
-rw-r--r--res/drawable-hdpi-v4/ic_menu_shuffle.pngbin0 -> 1386 bytes
-rw-r--r--res/drawable-hdpi-v4/ic_stat_star.pngbin0 -> 3611 bytes
-rw-r--r--res/drawable-hdpi-v4/launch.pngbin0 -> 5436 bytes
-rw-r--r--res/drawable-hdpi-v4/launch2.pngbin0 -> 5932 bytes
-rw-r--r--res/drawable-hdpi-v4/list_item_more.9.pngbin0 -> 5838 bytes
-rw-r--r--res/drawable-hdpi-v4/list_item_more_saved.9.pngbin0 -> 5919 bytes
-rw-r--r--res/drawable-hdpi-v4/list_item_more_shaded.9.pngbin0 -> 5994 bytes
-rw-r--r--res/drawable-hdpi-v4/main_header_icon.pngbin0 -> 5356 bytes
-rw-r--r--res/drawable-hdpi-v4/main_header_icon2.pngbin0 -> 5839 bytes
-rw-r--r--res/drawable-hdpi-v4/main_offline.pngbin0 -> 1405 bytes
-rw-r--r--res/drawable-hdpi-v4/main_offline_light.pngbin0 -> 564 bytes
-rw-r--r--res/drawable-hdpi-v4/main_select_server.pngbin0 -> 1920 bytes
-rw-r--r--res/drawable-hdpi-v4/media_backward.pngbin0 -> 939 bytes
-rw-r--r--res/drawable-hdpi-v4/media_backward_light.pngbin0 -> 1106 bytes
-rw-r--r--res/drawable-hdpi-v4/media_forward.pngbin0 -> 913 bytes
-rw-r--r--res/drawable-hdpi-v4/media_forward_light.pngbin0 -> 1128 bytes
-rw-r--r--res/drawable-hdpi-v4/media_pause.pngbin0 -> 168 bytes
-rw-r--r--res/drawable-hdpi-v4/media_pause_light.pngbin0 -> 443 bytes
-rw-r--r--res/drawable-hdpi-v4/media_repeat_all.pngbin0 -> 5090 bytes
-rw-r--r--res/drawable-hdpi-v4/media_repeat_off.pngbin0 -> 1079 bytes
-rw-r--r--res/drawable-hdpi-v4/media_repeat_off_light.pngbin0 -> 1512 bytes
-rw-r--r--res/drawable-hdpi-v4/media_repeat_single.pngbin0 -> 5564 bytes
-rw-r--r--res/drawable-hdpi-v4/media_start.pngbin0 -> 742 bytes
-rw-r--r--res/drawable-hdpi-v4/media_start_light.pngbin0 -> 1121 bytes
-rw-r--r--res/drawable-hdpi-v4/media_stop.pngbin0 -> 162 bytes
-rw-r--r--res/drawable-hdpi-v4/media_stop_light.pngbin0 -> 301 bytes
-rw-r--r--res/drawable-hdpi-v4/menu_browse.pngbin0 -> 1673 bytes
-rw-r--r--res/drawable-hdpi-v4/menu_home.pngbin0 -> 1814 bytes
-rw-r--r--res/drawable-hdpi-v4/menu_now_playing.pngbin0 -> 1359 bytes
-rw-r--r--res/drawable-hdpi-v4/menu_playlists.pngbin0 -> 1158 bytes
-rw-r--r--res/drawable-hdpi-v4/menubar_button_selected.9.pngbin0 -> 302 bytes
-rw-r--r--res/drawable-hdpi-v4/notification_next.pngbin0 -> 941 bytes
-rw-r--r--res/drawable-hdpi-v4/notification_pause.pngbin0 -> 233 bytes
-rw-r--r--res/drawable-hdpi-v4/notification_play.pngbin0 -> 807 bytes
-rw-r--r--res/drawable-hdpi-v4/notification_prev.pngbin0 -> 957 bytes
-rw-r--r--res/drawable-hdpi-v4/notification_stop.pngbin0 -> 338 bytes
-rw-r--r--res/drawable-hdpi-v4/refresh.pngbin0 -> 3585 bytes
-rw-r--r--res/drawable-hdpi-v4/saved.pngbin0 -> 838 bytes
-rw-r--r--res/drawable-hdpi-v4/search.pngbin0 -> 2940 bytes
-rw-r--r--res/drawable-hdpi-v4/select_album_play_all_normal.pngbin0 -> 5059 bytes
-rw-r--r--res/drawable-hdpi-v4/select_album_play_all_pressed.pngbin0 -> 4962 bytes
-rw-r--r--res/drawable-hdpi-v4/slider_knob.pngbin0 -> 1125 bytes
-rw-r--r--res/drawable-hdpi-v4/stat_notify_playing.pngbin0 -> 716 bytes
-rw-r--r--res/drawable-hdpi-v4/toast_frame.9.pngbin0 -> 2461 bytes
-rw-r--r--res/drawable-hdpi-v4/unknown_album.pngbin0 -> 4650 bytes
-rw-r--r--res/drawable-hdpi-v4/unknown_album_large.pngbin0 -> 48194 bytes
-rw-r--r--res/drawable/actionbar_button.xml18
-rw-r--r--res/drawable/btn_bg.xml17
-rw-r--r--res/drawable/btn_check.xml28
-rw-r--r--res/drawable/media_button.xml9
-rw-r--r--res/drawable/menubar_button.xml14
-rw-r--r--res/drawable/menubar_button_normal.xml4
-rw-r--r--res/drawable/menubar_button_pressed.xml4
-rw-r--r--res/drawable/select_album_play_all.xml5
-rw-r--r--res/layout-land/download.xml132
-rw-r--r--res/layout-port/download.xml151
-rw-r--r--res/layout/actionbar_spinner.xml13
-rw-r--r--res/layout/album_list_item.xml59
-rw-r--r--res/layout/appwidget4x1.xml106
-rw-r--r--res/layout/appwidget4x2.xml129
-rw-r--r--res/layout/appwidget4x3.xml112
-rw-r--r--res/layout/appwidget4x4.xml114
-rw-r--r--res/layout/artist_list_item.xml37
-rw-r--r--res/layout/button_bar.xml27
-rw-r--r--res/layout/chat.xml46
-rw-r--r--res/layout/chat_item.xml47
-rw-r--r--res/layout/chat_item_reverse.xml50
-rw-r--r--res/layout/create_podcast.xml27
-rw-r--r--res/layout/download_activity.xml4
-rw-r--r--res/layout/download_media_buttons.xml61
-rw-r--r--res/layout/download_playlist.xml31
-rw-r--r--res/layout/download_slider.xml42
-rw-r--r--res/layout/equalizer.xml47
-rw-r--r--res/layout/equalizer_bar.xml39
-rw-r--r--res/layout/help.xml42
-rw-r--r--res/layout/home.xml23
-rw-r--r--res/layout/jukebox_volume.xml29
-rw-r--r--res/layout/lyrics.xml57
-rw-r--r--res/layout/main.xml81
-rw-r--r--res/layout/main_buttons.xml157
-rw-r--r--res/layout/notification.xml103
-rw-r--r--res/layout/notification_expanded.xml100
-rw-r--r--res/layout/play_video.xml11
-rw-r--r--res/layout/playlist_list_item.xml26
-rw-r--r--res/layout/progress.xml20
-rw-r--r--res/layout/save_playlist.xml26
-rw-r--r--res/layout/search.xml22
-rw-r--r--res/layout/search_buttons.xml85
-rw-r--r--res/layout/select_album.xml31
-rw-r--r--res/layout/select_album_footer.xml21
-rw-r--r--res/layout/select_album_header.xml72
-rw-r--r--res/layout/select_artist.xml22
-rw-r--r--res/layout/select_artist_header.xml43
-rw-r--r--res/layout/select_genres.xml30
-rw-r--r--res/layout/select_playlist.xml30
-rw-r--r--res/layout/select_podcasts.xml29
-rw-r--r--res/layout/shuffle_dialog.xml80
-rw-r--r--res/layout/song_list_item.xml96
-rw-r--r--res/layout/start_timer.xml27
-rw-r--r--res/layout/sync_dialog.xml12
-rw-r--r--res/layout/tab_progress.xml24
-rw-r--r--res/layout/update_playlist.xml70
-rw-r--r--res/menu/chat.xml18
-rw-r--r--res/menu/empty.xml8
-rw-r--r--res/menu/main.xml43
-rw-r--r--res/menu/nowplaying.xml39
-rw-r--r--res/menu/nowplaying_context.xml31
-rw-r--r--res/menu/nowplaying_context_offline.xml23
-rw-r--r--res/menu/nowplaying_downloading.xml27
-rw-r--r--res/menu/nowplaying_offline.xml30
-rw-r--r--res/menu/search.xml24
-rw-r--r--res/menu/select_album.xml20
-rw-r--r--res/menu/select_album_context.xml37
-rw-r--r--res/menu/select_album_context_offline.xml25
-rw-r--r--res/menu/select_album_list.xml8
-rw-r--r--res/menu/select_artist.xml30
-rw-r--r--res/menu/select_artist_context.xml32
-rw-r--r--res/menu/select_artist_context_offline.xml22
-rw-r--r--res/menu/select_genres.xml18
-rw-r--r--res/menu/select_playlist.xml24
-rw-r--r--res/menu/select_playlist_context.xml39
-rw-r--r--res/menu/select_playlist_context_offline.xml12
-rw-r--r--res/menu/select_podcast_episode.xml13
-rw-r--r--res/menu/select_podcast_episode_context.xml35
-rw-r--r--res/menu/select_podcast_episode_context_offline.xml22
-rw-r--r--res/menu/select_podcast_episode_offline.xml13
-rw-r--r--res/menu/select_podcasts.xml28
-rw-r--r--res/menu/select_podcasts_context.xml9
-rw-r--r--res/menu/select_song.xml58
-rw-r--r--res/menu/select_song_context.xml50
-rw-r--r--res/menu/select_song_context_offline.xml31
-rw-r--r--res/menu/select_song_offline.xml38
-rw-r--r--res/menu/select_video_context.xml23
-rw-r--r--res/menu/select_video_context_offline.xml14
-rw-r--r--res/raw/changelog.xml101
-rw-r--r--res/values-fr/strings.xml208
-rw-r--r--res/values-ru/strings.xml343
-rw-r--r--res/values-v11/colors.xml5
-rw-r--r--res/values/arrays.xml153
-rw-r--r--res/values/attrs.xml12
-rw-r--r--res/values/colors.xml12
-rw-r--r--res/values/ids.xml4
-rw-r--r--res/values/strings.xml401
-rw-r--r--res/values/styles.xml45
-rw-r--r--res/values/themes.xml82
-rw-r--r--res/xml/appwidget4x1.xml7
-rw-r--r--res/xml/appwidget4x2.xml7
-rw-r--r--res/xml/appwidget4x3.xml7
-rw-r--r--res/xml/appwidget4x4.xml7
-rw-r--r--res/xml/changelog.xml2
-rw-r--r--res/xml/searchable.xml9
-rw-r--r--res/xml/settings.xml209
-rw-r--r--src/github/daneren2005/dsub/activity/DownloadActivity.java96
-rw-r--r--src/github/daneren2005/dsub/activity/EqualizerActivity.java278
-rw-r--r--src/github/daneren2005/dsub/activity/HelpActivity.java117
-rw-r--r--src/github/daneren2005/dsub/activity/MainActivity.java320
-rw-r--r--src/github/daneren2005/dsub/activity/QueryReceiverActivity.java56
-rw-r--r--src/github/daneren2005/dsub/activity/SearchActivity.java91
-rw-r--r--src/github/daneren2005/dsub/activity/SettingsActivity.java539
-rw-r--r--src/github/daneren2005/dsub/activity/SubsonicActivity.java641
-rw-r--r--src/github/daneren2005/dsub/activity/VoiceQueryReceiverActivity.java59
-rw-r--r--src/github/daneren2005/dsub/audiofx/EqualizerController.java151
-rw-r--r--src/github/daneren2005/dsub/audiofx/VisualizerController.java104
-rw-r--r--src/github/daneren2005/dsub/domain/Artist.java78
-rw-r--r--src/github/daneren2005/dsub/domain/ChatMessage.java51
-rw-r--r--src/github/daneren2005/dsub/domain/Genre.java29
-rw-r--r--src/github/daneren2005/dsub/domain/Indexes.java50
-rw-r--r--src/github/daneren2005/dsub/domain/JukeboxStatus.java63
-rw-r--r--src/github/daneren2005/dsub/domain/Lyrics.java55
-rw-r--r--src/github/daneren2005/dsub/domain/MusicDirectory.java374
-rw-r--r--src/github/daneren2005/dsub/domain/MusicFolder.java46
-rw-r--r--src/github/daneren2005/dsub/domain/PlayerState.java46
-rw-r--r--src/github/daneren2005/dsub/domain/Playlist.java109
-rw-r--r--src/github/daneren2005/dsub/domain/PodcastChannel.java80
-rw-r--r--src/github/daneren2005/dsub/domain/PodcastEpisode.java54
-rw-r--r--src/github/daneren2005/dsub/domain/RepeatMode.java28
-rw-r--r--src/github/daneren2005/dsub/domain/SearchCritera.java55
-rw-r--r--src/github/daneren2005/dsub/domain/SearchResult.java51
-rw-r--r--src/github/daneren2005/dsub/domain/ServerInfo.java46
-rw-r--r--src/github/daneren2005/dsub/domain/Share.java140
-rw-r--r--src/github/daneren2005/dsub/domain/Version.java171
-rw-r--r--src/github/daneren2005/dsub/fragments/ChatFragment.java223
-rw-r--r--src/github/daneren2005/dsub/fragments/DownloadFragment.java1169
-rw-r--r--src/github/daneren2005/dsub/fragments/LyricsFragment.java81
-rw-r--r--src/github/daneren2005/dsub/fragments/MainFragment.java373
-rw-r--r--src/github/daneren2005/dsub/fragments/SearchFragment.java333
-rw-r--r--src/github/daneren2005/dsub/fragments/SelectArtistFragment.java208
-rw-r--r--src/github/daneren2005/dsub/fragments/SelectDirectoryFragment.java818
-rw-r--r--src/github/daneren2005/dsub/fragments/SelectGenreFragment.java142
-rw-r--r--src/github/daneren2005/dsub/fragments/SelectPlaylistFragment.java286
-rw-r--r--src/github/daneren2005/dsub/fragments/SelectPodcastsFragment.java310
-rw-r--r--src/github/daneren2005/dsub/fragments/SubsonicFragment.java968
-rw-r--r--src/github/daneren2005/dsub/provider/DSubSearchProvider.java36
-rw-r--r--src/github/daneren2005/dsub/provider/DSubWidget4x1.java29
-rw-r--r--src/github/daneren2005/dsub/provider/DSubWidget4x2.java29
-rw-r--r--src/github/daneren2005/dsub/provider/DSubWidget4x3.java29
-rw-r--r--src/github/daneren2005/dsub/provider/DSubWidget4x4.java29
-rw-r--r--src/github/daneren2005/dsub/provider/DSubWidgetProvider.java277
-rw-r--r--src/github/daneren2005/dsub/receiver/A2dpIntentReceiver.java48
-rw-r--r--src/github/daneren2005/dsub/receiver/BluetoothIntentReceiver.java78
-rw-r--r--src/github/daneren2005/dsub/receiver/MediaButtonIntentReceiver.java52
-rw-r--r--src/github/daneren2005/dsub/service/CachedMusicService.java373
-rw-r--r--src/github/daneren2005/dsub/service/DownloadFile.java398
-rw-r--r--src/github/daneren2005/dsub/service/DownloadService.java141
-rw-r--r--src/github/daneren2005/dsub/service/DownloadServiceImpl.java1539
-rw-r--r--src/github/daneren2005/dsub/service/DownloadServiceLifecycleSupport.java352
-rw-r--r--src/github/daneren2005/dsub/service/JukeboxService.java358
-rw-r--r--src/github/daneren2005/dsub/service/MediaStoreService.java109
-rw-r--r--src/github/daneren2005/dsub/service/MusicService.java140
-rw-r--r--src/github/daneren2005/dsub/service/MusicServiceFactory.java36
-rw-r--r--src/github/daneren2005/dsub/service/OfflineException.java32
-rw-r--r--src/github/daneren2005/dsub/service/OfflineMusicService.java676
-rw-r--r--src/github/daneren2005/dsub/service/RESTMusicService.java1296
-rw-r--r--src/github/daneren2005/dsub/service/Scrobbler.java52
-rw-r--r--src/github/daneren2005/dsub/service/ServerTooOldException.java60
-rw-r--r--src/github/daneren2005/dsub/service/StreamProxy.java248
-rw-r--r--src/github/daneren2005/dsub/service/parser/AbstractParser.java138
-rw-r--r--src/github/daneren2005/dsub/service/parser/AlbumListParser.java62
-rw-r--r--src/github/daneren2005/dsub/service/parser/ChatMessageParser.java67
-rw-r--r--src/github/daneren2005/dsub/service/parser/ErrorParser.java49
-rw-r--r--src/github/daneren2005/dsub/service/parser/GenreParser.java122
-rw-r--r--src/github/daneren2005/dsub/service/parser/IndexesParser.java120
-rw-r--r--src/github/daneren2005/dsub/service/parser/JukeboxStatusParser.java62
-rw-r--r--src/github/daneren2005/dsub/service/parser/LicenseParser.java62
-rw-r--r--src/github/daneren2005/dsub/service/parser/LyricsParser.java65
-rw-r--r--src/github/daneren2005/dsub/service/parser/MusicDirectoryEntryParser.java74
-rw-r--r--src/github/daneren2005/dsub/service/parser/MusicDirectoryParser.java83
-rw-r--r--src/github/daneren2005/dsub/service/parser/MusicFoldersParser.java69
-rw-r--r--src/github/daneren2005/dsub/service/parser/PlaylistParser.java62
-rw-r--r--src/github/daneren2005/dsub/service/parser/PlaylistsParser.java73
-rw-r--r--src/github/daneren2005/dsub/service/parser/PodcastChannelParser.java67
-rw-r--r--src/github/daneren2005/dsub/service/parser/PodcastEntryParser.java105
-rw-r--r--src/github/daneren2005/dsub/service/parser/RandomSongsParser.java62
-rw-r--r--src/github/daneren2005/dsub/service/parser/SearchResult2Parser.java75
-rw-r--r--src/github/daneren2005/dsub/service/parser/SearchResultParser.java67
-rw-r--r--src/github/daneren2005/dsub/service/parser/ShareParser.java77
-rw-r--r--src/github/daneren2005/dsub/service/parser/StarredListParser.java64
-rw-r--r--src/github/daneren2005/dsub/service/parser/SubsonicRESTException.java19
-rw-r--r--src/github/daneren2005/dsub/service/parser/VersionParser.java47
-rw-r--r--src/github/daneren2005/dsub/service/ssl/SSLSocketFactory.java497
-rw-r--r--src/github/daneren2005/dsub/service/ssl/TrustManagerDecorator.java65
-rw-r--r--src/github/daneren2005/dsub/service/ssl/TrustSelfSignedStrategy.java44
-rw-r--r--src/github/daneren2005/dsub/service/ssl/TrustStrategy.java57
-rw-r--r--src/github/daneren2005/dsub/updates/Updater.java91
-rw-r--r--src/github/daneren2005/dsub/updates/Updater403.java58
-rw-r--r--src/github/daneren2005/dsub/util/BackgroundTask.java97
-rw-r--r--src/github/daneren2005/dsub/util/CacheCleaner.java239
-rw-r--r--src/github/daneren2005/dsub/util/CancellableTask.java87
-rw-r--r--src/github/daneren2005/dsub/util/Constants.java149
-rw-r--r--src/github/daneren2005/dsub/util/FileUtil.java396
-rw-r--r--src/github/daneren2005/dsub/util/ImageLoader.java332
-rw-r--r--src/github/daneren2005/dsub/util/LoadingTask.java97
-rw-r--r--src/github/daneren2005/dsub/util/Pair.java54
-rw-r--r--src/github/daneren2005/dsub/util/ProgressListener.java27
-rw-r--r--src/github/daneren2005/dsub/util/SettingsBackupAgent.java31
-rw-r--r--src/github/daneren2005/dsub/util/ShufflePlayBuffer.java127
-rw-r--r--src/github/daneren2005/dsub/util/SilentBackgroundTask.java67
-rw-r--r--src/github/daneren2005/dsub/util/SimpleServiceBinder.java37
-rw-r--r--src/github/daneren2005/dsub/util/TabBackgroundTask.java67
-rw-r--r--src/github/daneren2005/dsub/util/TimeLimitedCache.java55
-rw-r--r--src/github/daneren2005/dsub/util/Util.java1173
-rw-r--r--src/github/daneren2005/dsub/util/compat/RemoteControlClientBase.java32
-rw-r--r--src/github/daneren2005/dsub/util/compat/RemoteControlClientHelper.java27
-rw-r--r--src/github/daneren2005/dsub/util/compat/RemoteControlClientICS.java79
-rw-r--r--src/github/daneren2005/dsub/view/AlbumListAdapter.java83
-rw-r--r--src/github/daneren2005/dsub/view/AlbumView.java93
-rw-r--r--src/github/daneren2005/dsub/view/ArtistAdapter.java95
-rw-r--r--src/github/daneren2005/dsub/view/ArtistEntryView.java83
-rw-r--r--src/github/daneren2005/dsub/view/ArtistView.java84
-rw-r--r--src/github/daneren2005/dsub/view/AutoRepeatButton.java86
-rw-r--r--src/github/daneren2005/dsub/view/ChangeLog.java552
-rw-r--r--src/github/daneren2005/dsub/view/ChatAdapter.java100
-rw-r--r--src/github/daneren2005/dsub/view/EntryAdapter.java79
-rw-r--r--src/github/daneren2005/dsub/view/ErrorDialog.java70
-rw-r--r--src/github/daneren2005/dsub/view/FadeOutAnimation.java77
-rw-r--r--src/github/daneren2005/dsub/view/GenreAdapter.java59
-rw-r--r--src/github/daneren2005/dsub/view/GenreView.java53
-rw-r--r--src/github/daneren2005/dsub/view/MergeAdapter.java292
-rw-r--r--src/github/daneren2005/dsub/view/MyViewFlipper.java53
-rw-r--r--src/github/daneren2005/dsub/view/PlaylistAdapter.java68
-rw-r--r--src/github/daneren2005/dsub/view/PlaylistView.java76
-rw-r--r--src/github/daneren2005/dsub/view/PodcastChannelAdapter.java59
-rw-r--r--src/github/daneren2005/dsub/view/PodcastChannelView.java76
-rw-r--r--src/github/daneren2005/dsub/view/SackOfViewsAdapter.java181
-rw-r--r--src/github/daneren2005/dsub/view/SongView.java241
-rw-r--r--src/github/daneren2005/dsub/view/UpdateView.java133
-rw-r--r--src/github/daneren2005/dsub/view/VisualizerView.java137
-rw-r--r--subsonic.keystorebin0 -> 1194 bytes
-rw-r--r--subsonic.pngbin0 -> 44676 bytes
-rw-r--r--subsonic2.pngbin0 -> 53838 bytes
363 files changed, 29761 insertions, 0 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
new file mode 100644
index 00000000..5eabe457
--- /dev/null
+++ b/AndroidManifest.xml
@@ -0,0 +1,148 @@
+<?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="59"
+ android:versionName="4.1.2">
+
+ <uses-permission android:name="android.permission.INTERNET"/>
+ <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+ <uses-permission android:name="android.permission.WAKE_LOCK"/>
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+ <uses-permission android:name="android.permission.RECORD_AUDIO"/>
+ <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
+ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+ <uses-permission android:name="android.permission.BLUETOOTH"/>
+ <uses-permission android:name="android.permission.READ_LOGS"/>
+
+ <uses-feature android:name="android.hardware.bluetooth" android:required="false" />
+ <uses-feature android:name="android.hardware.microphone" android:required="false" />
+
+ <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="17"/>
+
+ <supports-screens android:anyDensity="true" android:xlargeScreens="true" android:largeScreens="true" android:normalScreens="true" android:smallScreens="true"/>
+
+ <application android:label="@string/common.appname"
+ android:backupAgent="github.daneren2005.dsub.util.SettingsBackupAgent"
+ android:icon="@drawable/launch2"
+ android:theme="@style/Theme.DSub.Holo">
+
+ <activity android:name="github.daneren2005.dsub.activity.MainActivity"
+ android:label="DSub"
+ android:configChanges="orientation|keyboardHidden"
+ android:launchMode="standard">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ </activity>
+
+ <activity android:name="github.daneren2005.dsub.activity.SearchActivity"
+ android:label="@string/search.label"
+ android:configChanges="orientation|keyboardHidden"
+ android:launchMode="singleTask"/>
+
+ <activity android:name="github.daneren2005.dsub.activity.DownloadActivity"
+ android:configChanges="keyboardHidden"
+ android:launchMode="singleTask"/>
+
+ <activity android:name="github.daneren2005.dsub.activity.SettingsActivity"
+ android:theme="@style/Theme.DSub.Dark"
+ android:configChanges="orientation|keyboardHidden"
+ android:launchMode="singleTask"/>
+
+ <activity android:name="github.daneren2005.dsub.activity.HelpActivity"
+ android:label="@string/help.label"
+ android:launchMode="singleTask"/>
+
+ <activity android:name="github.daneren2005.dsub.activity.EqualizerActivity"
+ android:label="@string/equalizer.label"
+ android:configChanges="orientation|keyboardHidden"
+ android:launchMode="singleTask"/>
+
+ <activity android:name="github.daneren2005.dsub.activity.VoiceQueryReceiverActivity"
+ android:launchMode="singleTask">
+ <intent-filter>
+ <action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+
+ <activity android:name="github.daneren2005.dsub.activity.QueryReceiverActivity"
+ android:launchMode="singleTask">
+ <intent-filter>
+ <action android:name="android.intent.action.SEARCH"/>
+ </intent-filter>
+ <meta-data android:name="android.app.searchable" android:resource="@xml/searchable"/>
+ </activity>
+
+ <service android:name="github.daneren2005.dsub.service.DownloadServiceImpl"
+ android:label="Subsonic Download Service"/>
+
+ <receiver android:name="github.daneren2005.dsub.receiver.MediaButtonIntentReceiver">
+ <intent-filter android:priority="999">
+ <action android:name="android.intent.action.MEDIA_BUTTON" />
+ </intent-filter>
+ </receiver>
+
+ <receiver android:name="github.daneren2005.dsub.receiver.BluetoothIntentReceiver">
+ <intent-filter>
+ <action android:name="android.bluetooth.a2dp.action.SINK_STATE_CHANGED"/>
+ <action android:name="android.bluetooth.headset.profile.action.CONNECTION_STATE_CHANGED"/> <!-- API Level 11 -->
+ <action android:name="android.bluetooth.device.action.ACL_CONNECTED"/>
+ <action android:name="android.bluetooth.device.action.ACL_DISCONNECTED"/>
+ </intent-filter>
+ </receiver>
+
+ <receiver android:name="github.daneren2005.dsub.receiver.A2dpIntentReceiver">
+ <intent-filter>
+ <action android:name="android.music.playstatusrequest"/>
+ </intent-filter>
+ </receiver>
+
+ <receiver
+ android:name="github.daneren2005.dsub.provider.DSubWidget4x1"
+ android:label="@string/widget.4x1">
+ <intent-filter>
+ <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
+ </intent-filter>
+ <meta-data android:name="android.appwidget.provider" android:resource="@xml/appwidget4x1"/>
+ </receiver>
+ <receiver
+ android:name="github.daneren2005.dsub.provider.DSubWidget4x2"
+ android:label="@string/widget.4x2">
+ <intent-filter>
+ <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
+ </intent-filter>
+ <meta-data android:name="android.appwidget.provider" android:resource="@xml/appwidget4x2"/>
+ </receiver>
+ <receiver
+ android:name="github.daneren2005.dsub.provider.DSubWidget4x3"
+ android:label="@string/widget.4x3">
+ <intent-filter>
+ <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
+ </intent-filter>
+ <meta-data android:name="android.appwidget.provider" android:resource="@xml/appwidget4x3"/>
+ </receiver>
+ <receiver
+ android:name="github.daneren2005.dsub.provider.DSubWidget4x4"
+ android:label="@string/widget.4x4">
+ <intent-filter>
+ <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
+ </intent-filter>
+ <meta-data android:name="android.appwidget.provider" android:resource="@xml/appwidget4x4"/>
+ </receiver>
+
+ <provider android:name="github.daneren2005.dsub.provider.DSubSearchProvider"
+ android:authorities="github.daneren2005.dsub.provider.DSubSearchProvider"/>
+
+ <meta-data android:name="android.app.default_searchable"
+ android:value="github.daneren2005.dsub.activity.QueryReceiverActivity"/>
+
+ <meta-data android:name="com.google.android.backup.api_key"
+ android:value="AEdPqrEAAAAIUhOMtwa_eG-f0oYUHnetl_Cz7cO9zae8ZXOK5w"/>
+
+ </application>
+
+</manifest>
diff --git a/Subsonic.iml b/Subsonic.iml
new file mode 100644
index 00000000..f770f3fc
--- /dev/null
+++ b/Subsonic.iml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="JAVA_MODULE" version="4">
+ <component name="FacetManager">
+ <facet type="android" name="Android">
+ <configuration />
+ </facet>
+ </component>
+ <component name="NewModuleRootManager" inherit-compiler-output="true">
+ <exclude-output />
+ <content url="file://$MODULE_DIR$">
+ <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/gen" isTestSource="false" />
+ </content>
+ <orderEntry type="inheritedJdk" />
+ <orderEntry type="sourceFolder" forTests="false" />
+ <orderEntry type="library" name="libs1" level="project" />
+ <orderEntry type="module" module-name="ActionBarSherlock" />
+ <orderEntry type="module" module-name="DragSortListView" />
+ </component>
+</module>
+
diff --git a/ant.properties b/ant.properties
new file mode 100644
index 00000000..de5f19ef
--- /dev/null
+++ b/ant.properties
@@ -0,0 +1,20 @@
+# This file is used to override default values used by the Ant build system.
+#
+# This file must be checked in Version Control Systems, as it is
+# integral to the build system of your project.
+
+# This file is only used by the Ant script.
+
+# You can use this to override default values such as
+# 'source.dir' for the location of your java source folder and
+# 'out.dir' for the location of your output folder.
+
+# You can also use it define how the release builds are signed by declaring
+# the following properties:
+# 'key.store' for the location of your keystore and
+# 'key.alias' for the name of the key to use.
+# The password will be asked during the build when you use the 'release' target.
+
+key.store=C:/Users/Scott/Documents/Subsonic/subsonic-android/subsonic.keystore
+key.alias=subsonic
+
diff --git a/assets/fonts/Storopia.ttf b/assets/fonts/Storopia.ttf
new file mode 100644
index 00000000..cbdc4c1f
--- /dev/null
+++ b/assets/fonts/Storopia.ttf
Binary files differ
diff --git a/assets/html/en/index.html b/assets/html/en/index.html
new file mode 100644
index 00000000..9ad7542c
--- /dev/null
+++ b/assets/html/en/index.html
@@ -0,0 +1,98 @@
+<html>
+<head>
+ <title>DSub Help</title>
+ <link rel="stylesheet" href="../style.css" type="text/css">
+
+</head>
+
+<body>
+
+<h3><img src="../img/subsonic.png" alt=""> Welcome to DSub!</h3>
+
+<p>
+ With <b>DSub</b> you can easily stream or download music from your home computer to your Android phone
+ (and do lots of other cool stuff too).
+</p>
+
+<p>
+ To install the Subsonic server software on your computer, please visit <a href="http://subsonic.org">subsonic.org</a>.
+ It's available for Windows, Mac, Linux and Unix.
+</p>
+
+<p>
+ By default, this program is configured to use the <b>Subsonic demo server</b>. Once you've set up your own
+ server, please go to <b>Settings</b> and change the configuration so that it connects to your own computer.
+</p>
+
+<p>
+ You can use this program freely for 30 days. After that you will have to make a donation to the Subsonic project.
+ As a donor you get the following benefits:
+</p>
+<ul>
+ <li>Unlimited streaming and download to any number of iPhone and Android phones.</li>
+ <li>Video streaming.</li>
+ <li>A personal web address for your Subsonic server (<em>yourname</em>.subsonic.org).</li>
+ <li>No ads in the Subsonic web interface.</li>
+ <li>Free access to new premium features.</li>
+</ul>
+
+<p>
+ The suggested donation amount is <b>&euro;20</b>, but you can give any amount you like.
+</p>
+
+<p>
+ Click one of the buttons to go to PayPal where you can pay by credit card or by using your PayPal account.
+ Once the donation is processed, you will receive a license key by email.
+</p>
+
+<table>
+ <tr>
+ <td style="border:none;">
+ <table>
+ <tr>
+ <td style="border:none;padding:0;padding:0"><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=3RTGWJRNAW2PU"><img src="../img/paypal.gif" alt=""/></a> </td>
+ </tr>
+ <tr>
+ <td style="text-align:center;border:none;padding:0">&euro;10</td>
+ </tr>
+ </table>
+ </td>
+ <td style="border:none;">
+ <table>
+ <tr>
+ <td style="border:none;padding:0"><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=UCUUB2TYE4PGN"><img src="../img/paypal.gif" alt=""/></a> </td>
+ </tr>
+ <tr>
+ <td style="text-align:center;border:none;padding:0">&euro;20</td>
+ </tr>
+ </table>
+ </td>
+ <td style="border:none;">
+ <table>
+ <tr>
+ <td style="border:none;padding:0"><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=3M6TFHWEPSU44"><img src="../img/paypal.gif" alt=""/></a> </td>
+ </tr>
+ <tr>
+ <td style="text-align:center;border:none;padding:0">&euro;25</td>
+ </tr>
+ </table>
+ </td>
+ <td style="border:none;">
+ <table>
+ <tr>
+ <td style="border:none;padding:0"><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=5KP7LPQU77UAS"><img src="../img/paypal.gif" alt=""/></a> </td>
+ </tr>
+ <tr>
+ <td style="text-align:center;border:none;padding:0">&euro;30</td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+</table>
+
+<p>
+ For more information, please visit <a href="http://subsonic.org/">subsonic.org</a>
+</p>
+
+</body>
+</html>
diff --git a/assets/html/fr/index.html b/assets/html/fr/index.html
new file mode 100644
index 00000000..4ac8c9c3
--- /dev/null
+++ b/assets/html/fr/index.html
@@ -0,0 +1,100 @@
+<html>
+<head>
+ <title>Aide de Subsonic</title>
+ <link rel="stylesheet" href="../style.css" type="text/css">
+
+</head>
+
+<body>
+
+<h3><img src="../img/subsonic.png" alt=""> Bienvenue dans Subsonic</h3>
+
+<p>
+ Avec <b>Subsonic</b>, vous pouvez facilement &eacute;couter ou t&eacute;l&eacute;charger de la musique &agrave; partir de votre ordinateur personnel sur votre appareil Android
+ (et faire plein d'autres trucs cools aussi).
+</p>
+
+<p>
+ Pour installer le serveur Subsonic sur votre ordinateur, veuillez visiter <a href="http://subsonic.org">subsonic.org</a>.
+ Celui-ci est disponible pour Windows, Mac, Linux et Unix.
+</p>
+
+<p>
+ Par d&eacute;faut, cette application est configur&eacute; pour utiliser le <b>serveur d&eacute;mo Subsonic</b>.
+ Apr&egrave;s avoir configur&eacute; votre serveur personnel, veuillez acc&eacute;der aux <b>Param&egrave;tres</b> et modifier la configuration
+ afin de vous connecter &agrave; votre propre ordinateur.
+</p>
+
+<p>
+ Vous pouvez utiliser cette application gratuitement pendant 30 jours.
+ Ensuite, vous devrez effectuer un don au projet Subsonic.
+ En tant que donateur, vous obtiendrez les b&eacute;n&eacute;fices suivants:
+</p>
+<ul>
+ <li>&Eacute;coute et t&eacute;l&eacute;chargement illimit&eacute;s vers autant de iPhones et d'appareils Android que d&eacute;sir&eacute;.</li>
+ <li>&Eacute;coute de vid&eacute;os.</li>
+ <li>Une adresse web personnalis&eacute;e pour votre serveur Subsonic (<em>votrenom</em>.subsonic.org).</li>
+ <li>Aucune publicit&eacute; dans l'interface web de Subsonic.</li>
+ <li>Acc&egrave;s gratuit aux nouvelles fonctionnalit&eacute;s avanc&eacute;es.</li>
+</ul>
+
+<p>
+ Le montant sugg&eacute;r&eacute; pour le don est de <b>20&euro;</b>, mais n'importe quel montant fera l'affaire.
+</p>
+
+<p>
+ Cliquez l'un des boutons suivants pour acc&eacute;der &agrave; PayPal, d'o&ugrave; vous pourrez payer soit par carte de cr&eacute;dit ou en utilisant votre compte PayPal.
+ Une fois le don re&ccedil;u et trait&eacute;, vous recevrez votre cl&eacute; d'activation par courriel.
+</p>
+
+<table>
+ <tr>
+ <td style="border:none;">
+ <table>
+ <tr>
+ <td style="border:none;padding:0;padding:0"><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=3RTGWJRNAW2PU"><img src="../img/paypal.gif" alt=""/></a> </td>
+ </tr>
+ <tr>
+ <td style="text-align:center;border:none;padding:0">10&euro;</td>
+ </tr>
+ </table>
+ </td>
+ <td style="border:none;">
+ <table>
+ <tr>
+ <td style="border:none;padding:0"><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=UCUUB2TYE4PGN"><img src="../img/paypal.gif" alt=""/></a> </td>
+ </tr>
+ <tr>
+ <td style="text-align:center;border:none;padding:0">20&euro;</td>
+ </tr>
+ </table>
+ </td>
+ <td style="border:none;">
+ <table>
+ <tr>
+ <td style="border:none;padding:0"><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=3M6TFHWEPSU44"><img src="../img/paypal.gif" alt=""/></a> </td>
+ </tr>
+ <tr>
+ <td style="text-align:center;border:none;padding:0">25&euro;</td>
+ </tr>
+ </table>
+ </td>
+ <td style="border:none;">
+ <table>
+ <tr>
+ <td style="border:none;padding:0"><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=5KP7LPQU77UAS"><img src="../img/paypal.gif" alt=""/></a> </td>
+ </tr>
+ <tr>
+ <td style="text-align:center;border:none;padding:0">30&euro;</td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+</table>
+
+<p>
+ Pour plus d'information, veuillez visiter <a href="http://subsonic.org/">subsonic.org</a>
+</p>
+
+</body>
+</html>
diff --git a/assets/html/img/paypal.gif b/assets/html/img/paypal.gif
new file mode 100644
index 00000000..d017250a
--- /dev/null
+++ b/assets/html/img/paypal.gif
Binary files differ
diff --git a/assets/html/img/subsonic.png b/assets/html/img/subsonic.png
new file mode 100644
index 00000000..38c521c5
--- /dev/null
+++ b/assets/html/img/subsonic.png
Binary files differ
diff --git a/assets/html/ru/index.html b/assets/html/ru/index.html
new file mode 100644
index 00000000..57979152
--- /dev/null
+++ b/assets/html/ru/index.html
@@ -0,0 +1,98 @@
+<html>
+<head>
+ <title>Помощь DSub</title>
+ <link rel="stylesheet" href="../style.css" type="text/css">
+
+</head>
+
+<body>
+
+<h3><img src="../img/subsonic.png" alt=""> Добро пожаловать в DSub!</h3>
+
+<p>
+ С программой <b>DSub</b> Вы можете легко включить поточное воспроизведение или скачивать музыку с Вашего домашнего компьютера на Android устройство
+ (и использовать множество других полезных функции).
+</p>
+
+<p>
+ Для установки серверного приложения Subsonic на Ваш компьютер, пожалуйста, посетите <a href="http://subsonic.org">subsonic.org</a>.
+ Приложение доступно для Windows, Mac, а также Linux и Unix.
+</p>
+
+<p>
+ По умолчанию данная программа настроена на работу с <b>демо сервером Subsonic</b>. После установки серверного
+ приложения, пожалуйста, перейдите в раздел <b>Настройки</b> и измените параметры для подключения.
+</p>
+
+<p>
+ Вы можете бесплатно использовать программу до 30 дней. После этого Вам необходимо сделать пожертвование проекту Subsonic.
+ После этого Вы получите следующие возможности:
+</p>
+<ul>
+ <li>Неограниченное поточное воспроизведение или скачивание с любого количества iPhone и Android устройств.</li>
+ <li>Потоковое воспроизведение видео.</li>
+ <li>Персональный адрес страницы на сервере Subsonic (<em>вашеимя</em>.subsonic.org).</li>
+ <li>Отсутствие рекламы в веб-интерфейсе Subsonic.</li>
+ <li>Бесплатный доступ к новым премиум-функциям.</li>
+</ul>
+
+<p>
+ Рекомендуемая сумма пожертвования - <b>&euro;20</b>, но Вы можете пожертвовать любую сумму.
+</p>
+
+<p>
+ Нажмите одну из кнопок для перехода на страницу PayPal, откуда Вы сможете сделать перевод с Вашей кредитной карты или используя аккаунт PayPal.
+ После отправки пожертвования Вы получите лицензионный ключ на Ваш email.
+</p>
+
+<table>
+ <tr>
+ <td style="border:none;">
+ <table>
+ <tr>
+ <td style="border:none;padding:0;padding:0"><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=3RTGWJRNAW2PU"><img src="../img/paypal.gif" alt=""/></a> </td>
+ </tr>
+ <tr>
+ <td style="text-align:center;border:none;padding:0">&euro;10</td>
+ </tr>
+ </table>
+ </td>
+ <td style="border:none;">
+ <table>
+ <tr>
+ <td style="border:none;padding:0"><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=UCUUB2TYE4PGN"><img src="../img/paypal.gif" alt=""/></a> </td>
+ </tr>
+ <tr>
+ <td style="text-align:center;border:none;padding:0">&euro;20</td>
+ </tr>
+ </table>
+ </td>
+ <td style="border:none;">
+ <table>
+ <tr>
+ <td style="border:none;padding:0"><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=3M6TFHWEPSU44"><img src="../img/paypal.gif" alt=""/></a> </td>
+ </tr>
+ <tr>
+ <td style="text-align:center;border:none;padding:0">&euro;25</td>
+ </tr>
+ </table>
+ </td>
+ <td style="border:none;">
+ <table>
+ <tr>
+ <td style="border:none;padding:0"><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=5KP7LPQU77UAS"><img src="../img/paypal.gif" alt=""/></a> </td>
+ </tr>
+ <tr>
+ <td style="text-align:center;border:none;padding:0">&euro;30</td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+</table>
+
+<p>
+ За дополнительной информацией посетите <a href="http://subsonic.org/">subsonic.org</a>
+</p>
+
+</body>
+</html> \ No newline at end of file
diff --git a/assets/html/style.css b/assets/html/style.css
new file mode 100644
index 00000000..9c1d55f2
--- /dev/null
+++ b/assets/html/style.css
@@ -0,0 +1,11 @@
+/*
+* Taken from http://yui.yahooapis.com/2.8.0r4/build/fonts/fonts.css
+*/
+body {
+ font: 13px / 1.231 arial, helvetica, clean, sans-serif;
+}
+
+table {
+ font-size:inherit;
+ font:100%;
+} \ No newline at end of file
diff --git a/build.properties b/build.properties
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/build.properties
diff --git a/build.xml b/build.xml
new file mode 100644
index 00000000..6d0d026f
--- /dev/null
+++ b/build.xml
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project name="subsonic-android" default="help">
+
+ <!-- The local.properties file is created and updated by the 'android' tool.
+ It contains the path to the SDK. It should *NOT* be checked into
+ Version Control Systems. -->
+ <property file="local.properties" />
+
+ <!-- The ant.properties file can be created by you. It is only edited by the
+ 'android' tool to add properties to it.
+ This is the place to change some Ant specific build properties.
+ Here are some properties you may want to change/update:
+
+ source.dir
+ The name of the source directory. Default is 'src'.
+ out.dir
+ The name of the output directory. Default is 'bin'.
+
+ For other overridable properties, look at the beginning of the rules
+ files in the SDK, at tools/ant/build.xml
+
+ Properties related to the SDK location or the project target should
+ be updated using the 'android' tool with the 'update' action.
+
+ This file is an integral part of the build system for your
+ application and should be checked into Version Control Systems.
+
+ -->
+ <property file="ant.properties" />
+
+ <!-- if sdk.dir was not set from one of the property file, then
+ get it from the ANDROID_HOME env var.
+ This must be done before we load project.properties since
+ the proguard config can use sdk.dir -->
+ <property environment="env" />
+ <condition property="sdk.dir" value="${env.ANDROID_HOME}">
+ <isset property="env.ANDROID_HOME" />
+ </condition>
+
+ <!-- The project.properties file is created and updated by the 'android'
+ tool, as well as ADT.
+
+ This contains project specific properties such as project target, and library
+ dependencies. Lower level build properties are stored in ant.properties
+ (or in .classpath for Eclipse projects).
+
+ This file is an integral part of the build system for your
+ application and should be checked into Version Control Systems. -->
+ <loadproperties srcFile="project.properties" />
+
+ <!-- quick check on sdk.dir -->
+ <fail
+ message="sdk.dir is missing. Make sure to generate local.properties using 'android update project' or to inject it through the ANDROID_HOME environment variable."
+ unless="sdk.dir"
+ />
+
+ <!--
+ Import per project custom build rules if present at the root of the project.
+ This is the place to put custom intermediary targets such as:
+ -pre-build
+ -pre-compile
+ -post-compile (This is typically used for code obfuscation.
+ Compiled code location: ${out.classes.absolute.dir}
+ If this is not done in place, override ${out.dex.input.absolute.dir})
+ -post-package
+ -post-build
+ -pre-clean
+ -->
+ <import file="custom_rules.xml" optional="true" />
+
+ <!-- Import the actual build file.
+
+ To customize existing targets, there are two options:
+ - Customize only one target:
+ - copy/paste the target into this file, *before* the
+ <import> task.
+ - customize it to your needs.
+ - Customize the whole content of build.xml
+ - copy/paste the content of the rules files (minus the top node)
+ into this file, replacing the <import> task.
+ - customize to your needs.
+
+ ***********************
+ ****** IMPORTANT ******
+ ***********************
+ In all cases you must update the value of version-tag below to read 'custom' instead of an integer,
+ in order to avoid having your file be overridden by tools such as "android update project"
+ -->
+ <!-- version-tag: 1 -->
+ <import file="${sdk.dir}/tools/ant/build.xml" />
+
+</project>
diff --git a/debug.keystore b/debug.keystore
new file mode 100644
index 00000000..4e662d41
--- /dev/null
+++ b/debug.keystore
Binary files differ
diff --git a/default.properties b/default.properties
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/default.properties
diff --git a/libs/CWAC-AdapterWrapper.jar b/libs/CWAC-AdapterWrapper.jar
new file mode 100644
index 00000000..692fe4d3
--- /dev/null
+++ b/libs/CWAC-AdapterWrapper.jar
Binary files differ
diff --git a/libs/CWAC-EndlessAdapter.jar b/libs/CWAC-EndlessAdapter.jar
new file mode 100644
index 00000000..ec20d936
--- /dev/null
+++ b/libs/CWAC-EndlessAdapter.jar
Binary files differ
diff --git a/libs/android-support-v4.jar b/libs/android-support-v4.jar
new file mode 100644
index 00000000..6080877d
--- /dev/null
+++ b/libs/android-support-v4.jar
Binary files differ
diff --git a/proguard-project.txt b/proguard-project.txt
new file mode 100644
index 00000000..b60ae7ea
--- /dev/null
+++ b/proguard-project.txt
@@ -0,0 +1,20 @@
+# To enable ProGuard in your project, edit project.properties
+# to define the proguard.config property as described in that file.
+#
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in ${sdk.dir}/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/proguard.cfg b/proguard.cfg
new file mode 100644
index 00000000..f0b04dc3
--- /dev/null
+++ b/proguard.cfg
@@ -0,0 +1,40 @@
+-optimizationpasses 5
+-dontusemixedcaseclassnames
+-dontskipnonpubliclibraryclasses
+-dontpreverify
+-verbose
+-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*
+
+-keep public class * extends android.app.Activity
+-keep public class * extends android.app.Application
+-keep public class * extends android.app.Service
+-keep public class * extends android.content.BroadcastReceiver
+-keep public class * extends android.content.ContentProvider
+-keep public class * extends android.app.backup.BackupAgentHelper
+-keep public class * extends android.preference.Preference
+-keep public class com.android.vending.licensing.ILicensingService
+
+-keepclasseswithmembernames class * {
+ native <methods>;
+}
+
+-keepclasseswithmembers class * {
+ public <init>(android.content.Context, android.util.AttributeSet);
+}
+
+-keepclasseswithmembers class * {
+ public <init>(android.content.Context, android.util.AttributeSet, int);
+}
+
+-keepclassmembers class * extends android.app.Activity {
+ public void *(android.view.View);
+}
+
+-keepclassmembers enum * {
+ public static **[] values();
+ public static ** valueOf(java.lang.String);
+}
+
+-keep class * implements android.os.Parcelable {
+ public static final android.os.Parcelable$Creator *;
+}
diff --git a/project.properties b/project.properties
new file mode 100644
index 00000000..56014cda
--- /dev/null
+++ b/project.properties
@@ -0,0 +1,13 @@
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must be checked in Version Control Systems.
+#
+# To customize properties used by the Ant build system use,
+# "ant.properties", and override values to adapt the script to your
+# project structure.
+
+# Project target.
+target=android-17
+android.library.reference.1=ActionBarSherlock/actionbarsherlock
+android.library.reference.2=DragSortListView/library \ No newline at end of file
diff --git a/releases/DSub 4.0.1.apk b/releases/DSub 4.0.1.apk
new file mode 100644
index 00000000..850a4bfa
--- /dev/null
+++ b/releases/DSub 4.0.1.apk
Binary files differ
diff --git a/releases/DSub 4.0.2.apk b/releases/DSub 4.0.2.apk
new file mode 100644
index 00000000..0c168ba8
--- /dev/null
+++ b/releases/DSub 4.0.2.apk
Binary files differ
diff --git a/releases/DSub 4.0.3.apk b/releases/DSub 4.0.3.apk
new file mode 100644
index 00000000..a1749e3c
--- /dev/null
+++ b/releases/DSub 4.0.3.apk
Binary files differ
diff --git a/releases/DSub 4.0.4.apk b/releases/DSub 4.0.4.apk
new file mode 100644
index 00000000..1332af32
--- /dev/null
+++ b/releases/DSub 4.0.4.apk
Binary files differ
diff --git a/releases/DSub 4.0.5.apk b/releases/DSub 4.0.5.apk
new file mode 100644
index 00000000..1ece93f6
--- /dev/null
+++ b/releases/DSub 4.0.5.apk
Binary files differ
diff --git a/releases/DSub 4.0.6.apk b/releases/DSub 4.0.6.apk
new file mode 100644
index 00000000..278256d7
--- /dev/null
+++ b/releases/DSub 4.0.6.apk
Binary files differ
diff --git a/releases/DSub 4.0.7.apk b/releases/DSub 4.0.7.apk
new file mode 100644
index 00000000..a97c215c
--- /dev/null
+++ b/releases/DSub 4.0.7.apk
Binary files differ
diff --git a/releases/DSub 4.1.0.apk b/releases/DSub 4.1.0.apk
new file mode 100644
index 00000000..1bb3fe41
--- /dev/null
+++ b/releases/DSub 4.1.0.apk
Binary files differ
diff --git a/releases/DSub 4.1.1.apk b/releases/DSub 4.1.1.apk
new file mode 100644
index 00000000..9d582aec
--- /dev/null
+++ b/releases/DSub 4.1.1.apk
Binary files differ
diff --git a/releases/DSub 4.1.2.apk b/releases/DSub 4.1.2.apk
new file mode 100644
index 00000000..0a4ee2bc
--- /dev/null
+++ b/releases/DSub 4.1.2.apk
Binary files differ
diff --git a/res/anim/push_down_in.xml b/res/anim/push_down_in.xml
new file mode 100644
index 00000000..6ab9a047
--- /dev/null
+++ b/res/anim/push_down_in.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <translate android:fromYDelta="-100%p" android:toYDelta="0"
+ android:duration="@android:integer/config_longAnimTime"/>
+ <alpha android:fromAlpha="0.0" android:toAlpha="1.0"
+ android:duration="@android:integer/config_longAnimTime" />
+</set>
diff --git a/res/anim/push_down_out.xml b/res/anim/push_down_out.xml
new file mode 100644
index 00000000..ce36458a
--- /dev/null
+++ b/res/anim/push_down_out.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <translate android:fromYDelta="0" android:toYDelta="100%p"
+ android:duration="@android:integer/config_longAnimTime"/>
+ <alpha android:fromAlpha="1.0" android:toAlpha="0.0"
+ android:duration="@android:integer/config_longAnimTime" />
+</set>
diff --git a/res/anim/push_up_in.xml b/res/anim/push_up_in.xml
new file mode 100644
index 00000000..6ef582c4
--- /dev/null
+++ b/res/anim/push_up_in.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <translate android:fromYDelta="100%p" android:toYDelta="0"
+ android:duration="@android:integer/config_longAnimTime"/>
+ <alpha android:fromAlpha="0.0" android:toAlpha="1.0"
+ android:duration="@android:integer/config_longAnimTime" />
+</set>
diff --git a/res/anim/push_up_out.xml b/res/anim/push_up_out.xml
new file mode 100644
index 00000000..2b267d59
--- /dev/null
+++ b/res/anim/push_up_out.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <translate android:fromYDelta="0" android:toYDelta="-100%p"
+ android:duration="@android:integer/config_longAnimTime"/>
+ <alpha android:fromAlpha="1.0" android:toAlpha="0.0"
+ android:duration="@android:integer/config_longAnimTime" />
+</set>
diff --git a/res/drawable-hdpi-v4/action_browse.png b/res/drawable-hdpi-v4/action_browse.png
new file mode 100644
index 00000000..54296909
--- /dev/null
+++ b/res/drawable-hdpi-v4/action_browse.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/action_compass.png b/res/drawable-hdpi-v4/action_compass.png
new file mode 100644
index 00000000..39760f89
--- /dev/null
+++ b/res/drawable-hdpi-v4/action_compass.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/action_exit.png b/res/drawable-hdpi-v4/action_exit.png
new file mode 100644
index 00000000..09e18dee
--- /dev/null
+++ b/res/drawable-hdpi-v4/action_exit.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/action_help.png b/res/drawable-hdpi-v4/action_help.png
new file mode 100644
index 00000000..aaf8304c
--- /dev/null
+++ b/res/drawable-hdpi-v4/action_help.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/action_moreoverflow.png b/res/drawable-hdpi-v4/action_moreoverflow.png
new file mode 100644
index 00000000..cb6ebdaf
--- /dev/null
+++ b/res/drawable-hdpi-v4/action_moreoverflow.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/action_offline.png b/res/drawable-hdpi-v4/action_offline.png
new file mode 100644
index 00000000..a85f0931
--- /dev/null
+++ b/res/drawable-hdpi-v4/action_offline.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/action_play_all.png b/res/drawable-hdpi-v4/action_play_all.png
new file mode 100644
index 00000000..6ce5629a
--- /dev/null
+++ b/res/drawable-hdpi-v4/action_play_all.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/action_refresh.png b/res/drawable-hdpi-v4/action_refresh.png
new file mode 100644
index 00000000..9f30dc95
--- /dev/null
+++ b/res/drawable-hdpi-v4/action_refresh.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/action_remove_all.png b/res/drawable-hdpi-v4/action_remove_all.png
new file mode 100644
index 00000000..97b88837
--- /dev/null
+++ b/res/drawable-hdpi-v4/action_remove_all.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/action_save.png b/res/drawable-hdpi-v4/action_save.png
new file mode 100644
index 00000000..7bda97d6
--- /dev/null
+++ b/res/drawable-hdpi-v4/action_save.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/action_screen_on_off.png b/res/drawable-hdpi-v4/action_screen_on_off.png
new file mode 100644
index 00000000..c7168563
--- /dev/null
+++ b/res/drawable-hdpi-v4/action_screen_on_off.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/action_search.png b/res/drawable-hdpi-v4/action_search.png
new file mode 100644
index 00000000..6bc3d426
--- /dev/null
+++ b/res/drawable-hdpi-v4/action_search.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/action_select.png b/res/drawable-hdpi-v4/action_select.png
new file mode 100644
index 00000000..e9e83e3d
--- /dev/null
+++ b/res/drawable-hdpi-v4/action_select.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/action_settings.png b/res/drawable-hdpi-v4/action_settings.png
new file mode 100644
index 00000000..1ab7722b
--- /dev/null
+++ b/res/drawable-hdpi-v4/action_settings.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/action_share.png b/res/drawable-hdpi-v4/action_share.png
new file mode 100644
index 00000000..28376157
--- /dev/null
+++ b/res/drawable-hdpi-v4/action_share.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/action_shuffle.png b/res/drawable-hdpi-v4/action_shuffle.png
new file mode 100644
index 00000000..0613965c
--- /dev/null
+++ b/res/drawable-hdpi-v4/action_shuffle.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/action_toggle_list.png b/res/drawable-hdpi-v4/action_toggle_list.png
new file mode 100644
index 00000000..87f9280f
--- /dev/null
+++ b/res/drawable-hdpi-v4/action_toggle_list.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/actionbar_background.9.png b/res/drawable-hdpi-v4/actionbar_background.9.png
new file mode 100644
index 00000000..9ce38a61
--- /dev/null
+++ b/res/drawable-hdpi-v4/actionbar_background.9.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/actionbar_button_normal.9.png b/res/drawable-hdpi-v4/actionbar_button_normal.9.png
new file mode 100644
index 00000000..385f751c
--- /dev/null
+++ b/res/drawable-hdpi-v4/actionbar_button_normal.9.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/album_art_background.png b/res/drawable-hdpi-v4/album_art_background.png
new file mode 100644
index 00000000..f0757695
--- /dev/null
+++ b/res/drawable-hdpi-v4/album_art_background.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/appwidget_art_default.png b/res/drawable-hdpi-v4/appwidget_art_default.png
new file mode 100644
index 00000000..5bd39cc2
--- /dev/null
+++ b/res/drawable-hdpi-v4/appwidget_art_default.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/appwidget_art_unknown.png b/res/drawable-hdpi-v4/appwidget_art_unknown.png
new file mode 100644
index 00000000..5bd39cc2
--- /dev/null
+++ b/res/drawable-hdpi-v4/appwidget_art_unknown.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/appwidget_bg.9.png b/res/drawable-hdpi-v4/appwidget_bg.9.png
new file mode 100644
index 00000000..6bacc7fe
--- /dev/null
+++ b/res/drawable-hdpi-v4/appwidget_bg.9.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/background.png b/res/drawable-hdpi-v4/background.png
new file mode 100644
index 00000000..07d6a9cc
--- /dev/null
+++ b/res/drawable-hdpi-v4/background.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/btn_check_buttonless_off.png b/res/drawable-hdpi-v4/btn_check_buttonless_off.png
new file mode 100644
index 00000000..d705b420
--- /dev/null
+++ b/res/drawable-hdpi-v4/btn_check_buttonless_off.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/btn_check_buttonless_on.png b/res/drawable-hdpi-v4/btn_check_buttonless_on.png
new file mode 100644
index 00000000..a2612d7d
--- /dev/null
+++ b/res/drawable-hdpi-v4/btn_check_buttonless_on.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/downloaded.png b/res/drawable-hdpi-v4/downloaded.png
new file mode 100644
index 00000000..f854aaf4
--- /dev/null
+++ b/res/drawable-hdpi-v4/downloaded.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/downloading.png b/res/drawable-hdpi-v4/downloading.png
new file mode 100644
index 00000000..afff39a9
--- /dev/null
+++ b/res/drawable-hdpi-v4/downloading.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/ic_appwidget_music_next.png b/res/drawable-hdpi-v4/ic_appwidget_music_next.png
new file mode 100644
index 00000000..99d28766
--- /dev/null
+++ b/res/drawable-hdpi-v4/ic_appwidget_music_next.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/ic_appwidget_music_pause.png b/res/drawable-hdpi-v4/ic_appwidget_music_pause.png
new file mode 100644
index 00000000..a05a8c50
--- /dev/null
+++ b/res/drawable-hdpi-v4/ic_appwidget_music_pause.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/ic_appwidget_music_play.png b/res/drawable-hdpi-v4/ic_appwidget_music_play.png
new file mode 100644
index 00000000..a754b920
--- /dev/null
+++ b/res/drawable-hdpi-v4/ic_appwidget_music_play.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/ic_appwidget_music_previous.png b/res/drawable-hdpi-v4/ic_appwidget_music_previous.png
new file mode 100644
index 00000000..7fb3921b
--- /dev/null
+++ b/res/drawable-hdpi-v4/ic_appwidget_music_previous.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/ic_menu_chat_dark.png b/res/drawable-hdpi-v4/ic_menu_chat_dark.png
new file mode 100644
index 00000000..be04b06e
--- /dev/null
+++ b/res/drawable-hdpi-v4/ic_menu_chat_dark.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/ic_menu_chat_light.png b/res/drawable-hdpi-v4/ic_menu_chat_light.png
new file mode 100644
index 00000000..3f58695c
--- /dev/null
+++ b/res/drawable-hdpi-v4/ic_menu_chat_light.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/ic_menu_chat_send_dark.png b/res/drawable-hdpi-v4/ic_menu_chat_send_dark.png
new file mode 100644
index 00000000..bd37dc59
--- /dev/null
+++ b/res/drawable-hdpi-v4/ic_menu_chat_send_dark.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/ic_menu_chat_send_light.png b/res/drawable-hdpi-v4/ic_menu_chat_send_light.png
new file mode 100644
index 00000000..0c870d2c
--- /dev/null
+++ b/res/drawable-hdpi-v4/ic_menu_chat_send_light.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/ic_menu_exit.png b/res/drawable-hdpi-v4/ic_menu_exit.png
new file mode 100644
index 00000000..847a1ed3
--- /dev/null
+++ b/res/drawable-hdpi-v4/ic_menu_exit.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/ic_menu_help.png b/res/drawable-hdpi-v4/ic_menu_help.png
new file mode 100644
index 00000000..9f11f434
--- /dev/null
+++ b/res/drawable-hdpi-v4/ic_menu_help.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/ic_menu_settings.png b/res/drawable-hdpi-v4/ic_menu_settings.png
new file mode 100644
index 00000000..48775c1e
--- /dev/null
+++ b/res/drawable-hdpi-v4/ic_menu_settings.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/ic_menu_shuffle.png b/res/drawable-hdpi-v4/ic_menu_shuffle.png
new file mode 100644
index 00000000..0613965c
--- /dev/null
+++ b/res/drawable-hdpi-v4/ic_menu_shuffle.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/ic_stat_star.png b/res/drawable-hdpi-v4/ic_stat_star.png
new file mode 100644
index 00000000..b16e803c
--- /dev/null
+++ b/res/drawable-hdpi-v4/ic_stat_star.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/launch.png b/res/drawable-hdpi-v4/launch.png
new file mode 100644
index 00000000..10693360
--- /dev/null
+++ b/res/drawable-hdpi-v4/launch.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/launch2.png b/res/drawable-hdpi-v4/launch2.png
new file mode 100644
index 00000000..a23d09d9
--- /dev/null
+++ b/res/drawable-hdpi-v4/launch2.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/list_item_more.9.png b/res/drawable-hdpi-v4/list_item_more.9.png
new file mode 100644
index 00000000..79ca860d
--- /dev/null
+++ b/res/drawable-hdpi-v4/list_item_more.9.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/list_item_more_saved.9.png b/res/drawable-hdpi-v4/list_item_more_saved.9.png
new file mode 100644
index 00000000..f3805bfb
--- /dev/null
+++ b/res/drawable-hdpi-v4/list_item_more_saved.9.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/list_item_more_shaded.9.png b/res/drawable-hdpi-v4/list_item_more_shaded.9.png
new file mode 100644
index 00000000..99c2f5b8
--- /dev/null
+++ b/res/drawable-hdpi-v4/list_item_more_shaded.9.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/main_header_icon.png b/res/drawable-hdpi-v4/main_header_icon.png
new file mode 100644
index 00000000..4252ba5b
--- /dev/null
+++ b/res/drawable-hdpi-v4/main_header_icon.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/main_header_icon2.png b/res/drawable-hdpi-v4/main_header_icon2.png
new file mode 100644
index 00000000..0889aee6
--- /dev/null
+++ b/res/drawable-hdpi-v4/main_header_icon2.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/main_offline.png b/res/drawable-hdpi-v4/main_offline.png
new file mode 100644
index 00000000..a1d27cec
--- /dev/null
+++ b/res/drawable-hdpi-v4/main_offline.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/main_offline_light.png b/res/drawable-hdpi-v4/main_offline_light.png
new file mode 100644
index 00000000..69bee782
--- /dev/null
+++ b/res/drawable-hdpi-v4/main_offline_light.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/main_select_server.png b/res/drawable-hdpi-v4/main_select_server.png
new file mode 100644
index 00000000..c2cefead
--- /dev/null
+++ b/res/drawable-hdpi-v4/main_select_server.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/media_backward.png b/res/drawable-hdpi-v4/media_backward.png
new file mode 100644
index 00000000..3bb85e68
--- /dev/null
+++ b/res/drawable-hdpi-v4/media_backward.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/media_backward_light.png b/res/drawable-hdpi-v4/media_backward_light.png
new file mode 100644
index 00000000..14188c86
--- /dev/null
+++ b/res/drawable-hdpi-v4/media_backward_light.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/media_forward.png b/res/drawable-hdpi-v4/media_forward.png
new file mode 100644
index 00000000..cf39f1f0
--- /dev/null
+++ b/res/drawable-hdpi-v4/media_forward.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/media_forward_light.png b/res/drawable-hdpi-v4/media_forward_light.png
new file mode 100644
index 00000000..9e172d8f
--- /dev/null
+++ b/res/drawable-hdpi-v4/media_forward_light.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/media_pause.png b/res/drawable-hdpi-v4/media_pause.png
new file mode 100644
index 00000000..d4cab525
--- /dev/null
+++ b/res/drawable-hdpi-v4/media_pause.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/media_pause_light.png b/res/drawable-hdpi-v4/media_pause_light.png
new file mode 100644
index 00000000..8ebf9b45
--- /dev/null
+++ b/res/drawable-hdpi-v4/media_pause_light.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/media_repeat_all.png b/res/drawable-hdpi-v4/media_repeat_all.png
new file mode 100644
index 00000000..c2255058
--- /dev/null
+++ b/res/drawable-hdpi-v4/media_repeat_all.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/media_repeat_off.png b/res/drawable-hdpi-v4/media_repeat_off.png
new file mode 100644
index 00000000..10315ab3
--- /dev/null
+++ b/res/drawable-hdpi-v4/media_repeat_off.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/media_repeat_off_light.png b/res/drawable-hdpi-v4/media_repeat_off_light.png
new file mode 100644
index 00000000..39408bec
--- /dev/null
+++ b/res/drawable-hdpi-v4/media_repeat_off_light.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/media_repeat_single.png b/res/drawable-hdpi-v4/media_repeat_single.png
new file mode 100644
index 00000000..6d280e7a
--- /dev/null
+++ b/res/drawable-hdpi-v4/media_repeat_single.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/media_start.png b/res/drawable-hdpi-v4/media_start.png
new file mode 100644
index 00000000..2af5996f
--- /dev/null
+++ b/res/drawable-hdpi-v4/media_start.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/media_start_light.png b/res/drawable-hdpi-v4/media_start_light.png
new file mode 100644
index 00000000..45cad73c
--- /dev/null
+++ b/res/drawable-hdpi-v4/media_start_light.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/media_stop.png b/res/drawable-hdpi-v4/media_stop.png
new file mode 100644
index 00000000..329eb906
--- /dev/null
+++ b/res/drawable-hdpi-v4/media_stop.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/media_stop_light.png b/res/drawable-hdpi-v4/media_stop_light.png
new file mode 100644
index 00000000..110d538e
--- /dev/null
+++ b/res/drawable-hdpi-v4/media_stop_light.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/menu_browse.png b/res/drawable-hdpi-v4/menu_browse.png
new file mode 100644
index 00000000..54296909
--- /dev/null
+++ b/res/drawable-hdpi-v4/menu_browse.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/menu_home.png b/res/drawable-hdpi-v4/menu_home.png
new file mode 100644
index 00000000..3cec6246
--- /dev/null
+++ b/res/drawable-hdpi-v4/menu_home.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/menu_now_playing.png b/res/drawable-hdpi-v4/menu_now_playing.png
new file mode 100644
index 00000000..6ce5629a
--- /dev/null
+++ b/res/drawable-hdpi-v4/menu_now_playing.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/menu_playlists.png b/res/drawable-hdpi-v4/menu_playlists.png
new file mode 100644
index 00000000..e9e83e3d
--- /dev/null
+++ b/res/drawable-hdpi-v4/menu_playlists.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/menubar_button_selected.9.png b/res/drawable-hdpi-v4/menubar_button_selected.9.png
new file mode 100644
index 00000000..d47bec40
--- /dev/null
+++ b/res/drawable-hdpi-v4/menubar_button_selected.9.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/notification_next.png b/res/drawable-hdpi-v4/notification_next.png
new file mode 100644
index 00000000..5835f654
--- /dev/null
+++ b/res/drawable-hdpi-v4/notification_next.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/notification_pause.png b/res/drawable-hdpi-v4/notification_pause.png
new file mode 100644
index 00000000..3324f88f
--- /dev/null
+++ b/res/drawable-hdpi-v4/notification_pause.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/notification_play.png b/res/drawable-hdpi-v4/notification_play.png
new file mode 100644
index 00000000..8c95b6a5
--- /dev/null
+++ b/res/drawable-hdpi-v4/notification_play.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/notification_prev.png b/res/drawable-hdpi-v4/notification_prev.png
new file mode 100644
index 00000000..73fb16f2
--- /dev/null
+++ b/res/drawable-hdpi-v4/notification_prev.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/notification_stop.png b/res/drawable-hdpi-v4/notification_stop.png
new file mode 100644
index 00000000..ab98e188
--- /dev/null
+++ b/res/drawable-hdpi-v4/notification_stop.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/refresh.png b/res/drawable-hdpi-v4/refresh.png
new file mode 100644
index 00000000..2f887c26
--- /dev/null
+++ b/res/drawable-hdpi-v4/refresh.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/saved.png b/res/drawable-hdpi-v4/saved.png
new file mode 100644
index 00000000..6c7c276f
--- /dev/null
+++ b/res/drawable-hdpi-v4/saved.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/search.png b/res/drawable-hdpi-v4/search.png
new file mode 100644
index 00000000..43d8c87e
--- /dev/null
+++ b/res/drawable-hdpi-v4/search.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/select_album_play_all_normal.png b/res/drawable-hdpi-v4/select_album_play_all_normal.png
new file mode 100644
index 00000000..bcf0efe6
--- /dev/null
+++ b/res/drawable-hdpi-v4/select_album_play_all_normal.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/select_album_play_all_pressed.png b/res/drawable-hdpi-v4/select_album_play_all_pressed.png
new file mode 100644
index 00000000..31bbfff0
--- /dev/null
+++ b/res/drawable-hdpi-v4/select_album_play_all_pressed.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/slider_knob.png b/res/drawable-hdpi-v4/slider_knob.png
new file mode 100644
index 00000000..ae21a4f9
--- /dev/null
+++ b/res/drawable-hdpi-v4/slider_knob.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/stat_notify_playing.png b/res/drawable-hdpi-v4/stat_notify_playing.png
new file mode 100644
index 00000000..bfd3e6a5
--- /dev/null
+++ b/res/drawable-hdpi-v4/stat_notify_playing.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/toast_frame.9.png b/res/drawable-hdpi-v4/toast_frame.9.png
new file mode 100644
index 00000000..8f5d8119
--- /dev/null
+++ b/res/drawable-hdpi-v4/toast_frame.9.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/unknown_album.png b/res/drawable-hdpi-v4/unknown_album.png
new file mode 100644
index 00000000..18b664e4
--- /dev/null
+++ b/res/drawable-hdpi-v4/unknown_album.png
Binary files differ
diff --git a/res/drawable-hdpi-v4/unknown_album_large.png b/res/drawable-hdpi-v4/unknown_album_large.png
new file mode 100644
index 00000000..bd9c6cc9
--- /dev/null
+++ b/res/drawable-hdpi-v4/unknown_album_large.png
Binary files differ
diff --git a/res/drawable/actionbar_button.xml b/res/drawable/actionbar_button.xml
new file mode 100644
index 00000000..5445cdcb
--- /dev/null
+++ b/res/drawable/actionbar_button.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item
+ android:state_pressed="true"
+ android:drawable="@drawable/menubar_button_pressed"
+ />
+
+ <item
+ android:state_focused="true"
+ android:drawable="@drawable/menubar_button_pressed"
+ />
+
+ <item
+ android:drawable="@drawable/actionbar_button_normal"
+ />
+
+</selector> \ No newline at end of file
diff --git a/res/drawable/btn_bg.xml b/res/drawable/btn_bg.xml
new file mode 100644
index 00000000..79d40784
--- /dev/null
+++ b/res/drawable/btn_bg.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2010 The Android Open Source Project Licensed under the
+ Apache License, Version 2.0 (the "License"); you may not use this file except
+ in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software distributed
+ under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
+ OR CONDITIONS OF ANY KIND, either express or implied. See the License for
+ the specific language governing permissions and limitations under the License.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android" android:exitFadeDuration="@android:integer/config_mediumAnimTime">
+
+ <item android:drawable="@color/ics_opaque" android:state_pressed="true"/>
+ <item android:drawable="@color/ics_opaque" android:state_enabled="true" android:state_focused="true"/>
+
+</selector> \ No newline at end of file
diff --git a/res/drawable/btn_check.xml b/res/drawable/btn_check.xml
new file mode 100644
index 00000000..f363a2d2
--- /dev/null
+++ b/res/drawable/btn_check.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:state_checked="true"
+ android:drawable="@drawable/btn_check_buttonless_on" />
+
+ <item android:state_checked="false"
+ android:drawable="@drawable/btn_check_buttonless_off" />
+
+ <item
+ android:drawable="@drawable/btn_check_buttonless_off" />
+
+</selector>
diff --git a/res/drawable/media_button.xml b/res/drawable/media_button.xml
new file mode 100644
index 00000000..f144393d
--- /dev/null
+++ b/res/drawable/media_button.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:state_pressed="true"
+ android:drawable="@drawable/menubar_button_pressed"/>
+
+ <item android:drawable="@drawable/menubar_button_normal"/>
+
+</selector> \ No newline at end of file
diff --git a/res/drawable/menubar_button.xml b/res/drawable/menubar_button.xml
new file mode 100644
index 00000000..1dc79176
--- /dev/null
+++ b/res/drawable/menubar_button.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ 
+ <item android:state_pressed="true"
+ android:drawable="@drawable/menubar_button_pressed"/>
+
+ <item android:state_enabled="true"
+ android:drawable="@drawable/menubar_button_normal"/>
+
+ <item android:drawable="@drawable/menubar_button_selected"/>
+
+</selector>
+
+ \ No newline at end of file
diff --git a/res/drawable/menubar_button_normal.xml b/res/drawable/menubar_button_normal.xml
new file mode 100644
index 00000000..76589c0c
--- /dev/null
+++ b/res/drawable/menubar_button_normal.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="@android:color/transparent" />
+</shape>
diff --git a/res/drawable/menubar_button_pressed.xml b/res/drawable/menubar_button_pressed.xml
new file mode 100644
index 00000000..b7b42ee5
--- /dev/null
+++ b/res/drawable/menubar_button_pressed.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="@color/dividerColor" />
+</shape>
diff --git a/res/drawable/select_album_play_all.xml b/res/drawable/select_album_play_all.xml
new file mode 100644
index 00000000..7e6a81ac
--- /dev/null
+++ b/res/drawable/select_album_play_all.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_pressed="true" android:drawable="@drawable/select_album_play_all_pressed" />
+ <item android:drawable="@drawable/select_album_play_all_normal" />
+</selector> \ No newline at end of file
diff --git a/res/layout-land/download.xml b/res/layout-land/download.xml
new file mode 100644
index 00000000..5b4db35e
--- /dev/null
+++ b/res/layout-land/download.xml
@@ -0,0 +1,132 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/download_layout_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/download_layout"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+
+ <LinearLayout android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1">
+
+ <github.daneren2005.dsub.view.MyViewFlipper
+ android:id="@+id/download_playlist_flipper"
+ android:layout_width="0dp"
+ android:layout_height="fill_parent"
+ android:layout_weight="1">
+
+ <ImageView
+ 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>
+
+ <LinearLayout 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_layout"
+ android:orientation="horizontal"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal">
+
+ <Button
+ android:id="@+id/download_jukebox"
+ android:text="RC"
+ android:textStyle="bold"
+ android:textSize="22sp"
+ android:background="@drawable/menubar_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="3dip"
+ android:padding="9dip"/>
+ <Button
+ android:id="@+id/download_equalizer"
+ android:text="EQ"
+ android:textStyle="bold"
+ android:textSize="22sp"
+ android:background="@drawable/menubar_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="3dip"
+ android:padding="9dip"/>
+ <Button
+ android:id="@+id/download_visualizer"
+ android:text="VIS"
+ android:textStyle="bold"
+ android:textSize="22sp"
+ android:background="@drawable/menubar_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="3dip"
+ android:padding="9dip"/>
+ <ImageButton
+ android:id="@+id/download_star"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:background="@drawable/menubar_button"
+ android:src="@android:drawable/star_big_off"
+ android:padding="10dip"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/download_visualizer_view_layout"
+ android:layout_width="fill_parent"
+ android:layout_height="60dip"
+ android:layout_marginLeft="12dip"
+ android:layout_marginRight="12dip"
+ android:layout_gravity="center_horizontal"/>
+
+ <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"/>
+
+ <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" />
+
+ <include layout="@layout/download_media_buttons"/>
+
+ <include layout="@layout/download_slider"/>
+
+ </LinearLayout>
+
+ </LinearLayout>
+ </LinearLayout>
+</FrameLayout>
diff --git a/res/layout-port/download.xml b/res/layout-port/download.xml
new file mode 100644
index 00000000..4b39286a
--- /dev/null
+++ b/res/layout-port/download.xml
@@ -0,0 +1,151 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/download_layout_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/download_layout"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+
+ <github.daneren2005.dsub.view.MyViewFlipper
+ android:id="@+id/download_playlist_flipper"
+ android:layout_width="fill_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1">
+
+ <RelativeLayout
+ android:id="@+id/download_album_art_layout"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:layout_weight="1"
+ android:background="@android:color/transparent">
+
+ <RelativeLayout android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:layout_weight="1"
+ android:layout_above="@+id/download_song_title">
+
+ <ImageView
+ android:id="@+id/download_album_art_image"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerHorizontal="true"
+ android:layout_alignParentTop="true"
+ android:scaleType="centerCrop"/>
+
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/download_overlay_buttons"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/overlayColor"
+ android:layout_alignParentBottom="true">
+
+ <Button
+ android:id="@+id/download_jukebox"
+ android:text="RC"
+ android:textStyle="bold"
+ android:textSize="22sp"
+ android:background="@drawable/menubar_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom"
+ android:paddingTop="4dip"
+ android:paddingLeft="14dip"
+ android:paddingBottom="4dip"/>
+
+ <Button
+ android:id="@+id/download_equalizer"
+ android:text="EQ"
+ android:textStyle="bold"
+ android:textSize="22sp"
+ android:background="@drawable/menubar_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom"
+ android:paddingTop="4dip"
+ android:paddingLeft="7dip"
+ android:paddingRight="7dip"
+ android:paddingBottom="4dip"/>
+
+ <Button
+ android:id="@+id/download_visualizer"
+ android:text="VIS"
+ android:textStyle="bold"
+ android:textSize="22sp"
+ android:background="@drawable/menubar_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom"
+ android:paddingTop="4dip"
+ android:paddingLeft="7dip"
+ android:paddingRight="7dip"
+ android:paddingBottom="4dip"/>
+
+ <ImageButton
+ android:id="@+id/download_star"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/download_jukebox"
+ android:background="@drawable/menubar_button"
+ android:src="@android:drawable/star_big_off"
+ android:paddingTop="8dip"
+ android:paddingLeft="10dip"
+ android:paddingRight="10dip"
+ android:paddingBottom="8dip"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/download_visualizer_view_layout"
+ android:layout_width="fill_parent"
+ android:layout_height="60dip"
+ android:layout_marginLeft="16dip"
+ android:layout_marginRight="16dip"
+ android:layout_gravity="center_horizontal"
+ android:layout_alignParentBottom="true"/>
+ </RelativeLayout>
+
+ <TextView
+ android:id="@+id/download_status"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_centerHorizontal="true"
+ android:layout_marginLeft="16dip"
+ android:layout_marginRight="16dip"
+ android:singleLine="true"
+ android:ellipsize="end"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="?android:textColorSecondary"/>
+
+ <TextView
+ android:id="@+id/download_song_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:layout_above="@+id/download_status"
+ android:layout_centerHorizontal="true"
+ android:layout_marginLeft="16dip"
+ android:layout_marginRight="16dip"
+ android:singleLine="true"
+ android:textColor="?android:textColorPrimary"
+ android:textStyle="bold"
+ android:textSize="18sp"
+ android:ellipsize="end"/>
+
+ </RelativeLayout>
+
+ <include layout="@layout/download_playlist"/>
+
+ </github.daneren2005.dsub.view.MyViewFlipper>
+
+ <include layout="@layout/download_media_buttons"/>
+
+ <include layout="@layout/download_slider"/>
+ </LinearLayout>
+</FrameLayout>
diff --git a/res/layout/actionbar_spinner.xml b/res/layout/actionbar_spinner.xml
new file mode 100644
index 00000000..22055901
--- /dev/null
+++ b/res/layout/actionbar_spinner.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:gravity="fill_horizontal" >
+ <Spinner
+ android:id="@+id/spinner"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:prompt="@string/common.appname"
+ />
+</RelativeLayout>
diff --git a/res/layout/album_list_item.xml b/res/layout/album_list_item.xml
new file mode 100644
index 00000000..0b84b4f3
--- /dev/null
+++ b/res/layout/album_list_item.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@id/drag_handle"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <ImageView
+ android:id="@+id/album_coverart"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="left|center_vertical"
+ android:paddingTop="1dip"
+ android:paddingBottom="1dip"/>
+
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_gravity="left|center_vertical"
+ android:paddingLeft="10dip"
+ android:paddingRight="3dip">
+
+ <TextView
+ android:id="@+id/album_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:singleLine="true"
+ android:ellipsize="marquee"/>
+
+ <TextView
+ android:id="@+id/album_artist"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:singleLine="true"/>
+
+ </LinearLayout>
+
+ <ImageButton
+ android:id="@+id/album_star"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="right|center_vertical"
+ android:src="@drawable/ic_stat_star"
+ android:background="@android:color/transparent"
+ android:focusable="false"/>
+
+ <ImageView
+ android:id="@+id/album_more"
+ android:src="@drawable/list_item_more"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:layout_gravity="right|center_vertical"
+ android:paddingRight="6dip"
+ android:background="@drawable/menubar_button"/>
+</LinearLayout>
diff --git a/res/layout/appwidget4x1.xml b/res/layout/appwidget4x1.xml
new file mode 100644
index 00000000..5e55aa37
--- /dev/null
+++ b/res/layout/appwidget4x1.xml
@@ -0,0 +1,106 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:minWidth="250dp"
+ android:minHeight="40dp"
+ android:background="@drawable/appwidget_bg"
+ android:orientation="horizontal" >
+
+ <ImageView
+ android:id="@+id/appwidget_coverart"
+ android:layout_width="80dp"
+ android:layout_height="80dp"
+ android:layout_gravity="center_vertical"
+ android:clickable="true"
+ android:focusable="true"
+ android:src="@drawable/appwidget_art_default" />
+
+ <LinearLayout
+ android:id="@+id/linearLayout1"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:id="@+id/appwidget_top"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:clickable="true"
+ android:focusable="true"
+ android:orientation="vertical"
+ android:background="@drawable/media_button">
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="none"
+ android:fadingEdge="horizontal"
+ android:fadingEdgeLength="20dip"
+ android:minHeight="16sp"
+ android:paddingLeft="4dip"
+ android:paddingRight="4dip"
+ android:paddingTop="4dip"
+ android:singleLine="true"
+ android:gravity="center_horizontal"
+ android:text="Title"
+ android:textColor="@color/appwidget_text"
+ android:textSize="16sp"
+ android:textStyle="bold" />
+
+ <TextView
+ android:id="@+id/artist"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="none"
+ android:fadingEdge="horizontal"
+ android:fadingEdgeLength="10dip"
+ android:minHeight="12sp"
+ android:paddingBottom="4dip"
+ android:paddingLeft="4dip"
+ android:paddingRight="4dip"
+ android:singleLine="true"
+ android:gravity="center_horizontal"
+ android:text="Artist"
+ android:textColor="@color/appwidget_text"
+ android:textSize="12sp" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:orientation="horizontal"
+ android:paddingBottom="4dip"
+ android:paddingTop="4dip" >
+
+ <ImageButton
+ android:id="@+id/control_previous"
+ android:layout_width="0dip"
+ android:layout_height="fill_parent"
+ android:layout_weight="1"
+ android:scaleType="center"
+ android:background="@drawable/media_button"
+ android:src="@drawable/ic_appwidget_music_previous" />
+
+ <ImageButton
+ android:id="@+id/control_play"
+ android:layout_width="0dip"
+ android:layout_height="fill_parent"
+ android:layout_weight="1"
+ android:scaleType="center"
+ android:src="@drawable/ic_appwidget_music_play"
+ android:background="@drawable/media_button" />
+
+ <ImageButton
+ android:id="@+id/control_next"
+ android:layout_width="0dip"
+ android:layout_height="fill_parent"
+ android:layout_weight="1"
+ android:scaleType="center"
+ android:src="@drawable/ic_appwidget_music_next"
+ android:background="@drawable/media_button" />
+ </LinearLayout>
+ </LinearLayout>
+
+</LinearLayout> \ No newline at end of file
diff --git a/res/layout/appwidget4x2.xml b/res/layout/appwidget4x2.xml
new file mode 100644
index 00000000..575ae1c2
--- /dev/null
+++ b/res/layout/appwidget4x2.xml
@@ -0,0 +1,129 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:minWidth="250dp"
+ android:minHeight="110dp"
+ android:background="@drawable/appwidget_bg"
+ android:orientation="horizontal" >
+
+ <ImageView
+ android:id="@+id/appwidget_coverart"
+ android:layout_width="120dp"
+ android:layout_height="120dp"
+ android:layout_gravity="center_vertical"
+ android:clickable="true"
+ android:focusable="true"
+ android:src="@drawable/appwidget_art_default" />
+
+ <LinearLayout
+ android:id="@+id/linearLayout1"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:id="@+id/appwidget_top"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:clickable="true"
+ android:focusable="true"
+ android:orientation="vertical"
+ android:paddingTop="4dip"
+ android:paddingBottom="4dip"
+ android:background="@drawable/media_button">
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="none"
+ android:fadingEdge="horizontal"
+ android:fadingEdgeLength="20dip"
+ android:minHeight="16sp"
+ android:paddingLeft="4dip"
+ android:paddingRight="4dip"
+ android:paddingTop="4dip"
+ android:paddingBottom="2dip"
+ android:singleLine="true"
+ android:gravity="center_horizontal"
+ android:text="Title"
+ android:textColor="@color/appwidget_text"
+ android:textSize="16sp"
+ android:textStyle="bold" />
+
+ <TextView
+ android:id="@+id/artist"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="none"
+ android:fadingEdge="horizontal"
+ android:fadingEdgeLength="10dip"
+ android:minHeight="12sp"
+ android:paddingLeft="4dip"
+ android:paddingRight="4dip"
+ android:paddingBottom="2dip"
+ android:singleLine="true"
+ android:gravity="center_horizontal"
+ android:text="Artist"
+ android:textColor="@color/appwidget_text"
+ android:textSize="12sp" />
+
+ <TextView
+ android:id="@+id/album"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="none"
+ android:fadingEdge="horizontal"
+ android:fadingEdgeLength="10dip"
+ android:minHeight="12sp"
+ android:paddingBottom="2dip"
+ android:paddingLeft="4dip"
+ android:paddingRight="4dip"
+ android:singleLine="true"
+ android:gravity="center_horizontal"
+ android:text="Album"
+ android:textColor="@color/appwidget_text"
+ android:textSize="12sp" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:layout_gravity="bottom"
+ android:gravity="bottom"
+ android:paddingBottom="4dip"
+ android:paddingTop="4dip" >"
+
+ <ImageButton
+ android:id="@+id/control_previous"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:scaleType="center"
+ android:background="@drawable/media_button"
+ android:src="@drawable/ic_appwidget_music_previous" />
+
+ <ImageButton
+ android:id="@+id/control_play"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:scaleType="center"
+ android:src="@drawable/ic_appwidget_music_play"
+ android:background="@drawable/media_button" />
+
+ <ImageButton
+ android:id="@+id/control_next"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:scaleType="center"
+ android:src="@drawable/ic_appwidget_music_next"
+ android:background="@drawable/media_button" />
+ </LinearLayout>
+ </LinearLayout>
+
+</LinearLayout> \ No newline at end of file
diff --git a/res/layout/appwidget4x3.xml b/res/layout/appwidget4x3.xml
new file mode 100644
index 00000000..b4f685bc
--- /dev/null
+++ b/res/layout/appwidget4x3.xml
@@ -0,0 +1,112 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:background="@drawable/appwidget_bg"
+ android:orientation="vertical" >
+
+ <ImageView
+ android:id="@+id/appwidget_coverart"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:scaleType="fitCenter"
+ android:layout_weight="1"
+ android:layout_gravity="center_horizontal"
+ android:clickable="true"
+ android:focusable="true"
+ android:paddingTop="6dip"
+ android:paddingBottom="6dip"
+ android:src="@drawable/appwidget_art_default" />
+
+ <LinearLayout
+ android:id="@+id/linearLayout1"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:id="@+id/appwidget_top"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:clickable="true"
+ android:focusable="true"
+ android:orientation="vertical"
+ android:paddingBottom="4dip"
+ android:paddingTop="4dip"
+ android:background="@drawable/media_button">
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="none"
+ android:fadingEdge="horizontal"
+ android:fadingEdgeLength="20dip"
+ android:minHeight="16sp"
+ android:paddingLeft="5dip"
+ android:paddingRight="5dip"
+ android:singleLine="true"
+ android:textColor="@color/appwidget_text"
+ android:textSize="16sp"
+ android:text="Title"
+ android:layout_gravity="center_horizontal"
+ android:gravity="center"
+ android:textStyle="bold" />
+
+ <TextView
+ android:id="@+id/artist"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="none"
+ android:fadingEdge="horizontal"
+ android:fadingEdgeLength="10dip"
+ android:minHeight="12sp"
+ android:paddingBottom="2dip"
+ android:paddingLeft="5dip"
+ android:singleLine="true"
+ android:text="Artist"
+ android:layout_gravity="center_horizontal"
+ android:gravity="center"
+ android:textColor="@color/appwidget_text"
+ android:textSize="12sp" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom"
+ android:gravity="bottom"
+ android:orientation="horizontal"
+ android:paddingBottom="4dip">
+
+ <ImageButton
+ android:id="@+id/control_previous"
+ android:layout_width="0dip"
+ android:layout_height="56dip"
+ android:layout_weight="1"
+ android:scaleType="center"
+ android:background="@drawable/media_button"
+ android:src="@drawable/ic_appwidget_music_previous" />
+
+ <ImageButton
+ android:id="@+id/control_play"
+ android:layout_width="0dip"
+ android:layout_height="56dip"
+ android:layout_weight="1"
+ android:scaleType="center"
+ android:src="@drawable/ic_appwidget_music_play"
+ android:background="@drawable/media_button" />
+
+ <ImageButton
+ android:id="@+id/control_next"
+ android:layout_width="0dip"
+ android:layout_height="56dip"
+ android:layout_weight="1"
+ android:scaleType="center"
+ android:src="@drawable/ic_appwidget_music_next"
+ android:background="@drawable/media_button" />
+ </LinearLayout>
+
+ </LinearLayout>
+
+</LinearLayout> \ No newline at end of file
diff --git a/res/layout/appwidget4x4.xml b/res/layout/appwidget4x4.xml
new file mode 100644
index 00000000..6e6c12ab
--- /dev/null
+++ b/res/layout/appwidget4x4.xml
@@ -0,0 +1,114 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:orientation="vertical"
+ android:background="@drawable/appwidget_bg" >
+
+ <ImageView
+ android:id="@+id/appwidget_coverart"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:scaleType="fitCenter"
+ android:layout_weight="1"
+ android:layout_gravity="center_horizontal"
+ android:clickable="true"
+ android:focusable="true"
+ android:layout_margin="6dip"
+ android:paddingTop="6dip"
+ android:paddingBottom="6dip"
+ android:src="@drawable/appwidget_art_default" />
+
+ <LinearLayout
+ android:id="@+id/linearLayout1"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:id="@+id/appwidget_top"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:clickable="true"
+ android:focusable="true"
+ android:orientation="vertical"
+ android:paddingTop="4dip"
+ android:paddingBottom="4dip"
+ android:background="@drawable/media_button">
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="none"
+ android:fadingEdge="horizontal"
+ android:fadingEdgeLength="20dip"
+ android:minHeight="16sp"
+ android:paddingLeft="5dip"
+ android:paddingRight="5dip"
+ android:singleLine="true"
+ android:textColor="@color/appwidget_text"
+ android:textSize="16sp"
+ android:text="Title"
+ android:layout_gravity="center_horizontal"
+ android:gravity="center"
+ android:textStyle="bold" />
+
+ <TextView
+ android:id="@+id/artist"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="none"
+ android:fadingEdge="horizontal"
+ android:fadingEdgeLength="10dip"
+ android:minHeight="12sp"
+ android:paddingBottom="2dip"
+ android:paddingLeft="5dip"
+ android:singleLine="true"
+ android:text="Artist"
+ android:layout_gravity="center_horizontal"
+ android:gravity="center"
+ android:textColor="@color/appwidget_text"
+ android:textSize="12sp" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom"
+ android:gravity="bottom"
+ android:orientation="horizontal"
+ android:paddingBottom="4dip"
+ android:paddingTop="4dip" >
+
+ <ImageButton
+ android:id="@+id/control_previous"
+ android:layout_width="0dip"
+ android:layout_height="56dip"
+ android:layout_weight="1"
+ android:scaleType="center"
+ android:background="@drawable/media_button"
+ android:src="@drawable/ic_appwidget_music_previous" />
+
+ <ImageButton
+ android:id="@+id/control_play"
+ android:layout_width="0dip"
+ android:layout_height="56dip"
+ android:layout_weight="1"
+ android:scaleType="center"
+ android:src="@drawable/ic_appwidget_music_play"
+ android:background="@drawable/media_button" />
+
+ <ImageButton
+ android:id="@+id/control_next"
+ android:layout_width="0dip"
+ android:layout_height="56dip"
+ android:layout_weight="1"
+ android:scaleType="center"
+ android:src="@drawable/ic_appwidget_music_next"
+ android:background="@drawable/media_button" />
+ </LinearLayout>
+
+ </LinearLayout>
+
+</LinearLayout> \ No newline at end of file
diff --git a/res/layout/artist_list_item.xml b/res/layout/artist_list_item.xml
new file mode 100644
index 00000000..3684e176
--- /dev/null
+++ b/res/layout/artist_list_item.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:background="@android:color/transparent">
+
+ <TextView
+ android:id="@+id/artist_name"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:gravity="left|center_vertical"
+ android:paddingLeft="6dip"
+ android:paddingRight="6dip"
+ android:minHeight="50dip"
+ android:background="@android:color/transparent"/>
+
+ <ImageButton
+ android:id="@+id/artist_star"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="right|center_vertical"
+ android:src="@drawable/ic_stat_star"
+ android:background="@android:color/transparent"
+ android:focusable="false"/>
+
+ <ImageView
+ android:id="@+id/artist_more"
+ android:src="@drawable/list_item_more"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:layout_gravity="right|center_vertical"
+ android:paddingRight="6dip"
+ android:background="@drawable/menubar_button"/>
+</LinearLayout> \ No newline at end of file
diff --git a/res/layout/button_bar.xml b/res/layout/button_bar.xml
new file mode 100644
index 00000000..8f49c99a
--- /dev/null
+++ b/res/layout/button_bar.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/button_bar"
+ android:layout_gravity="bottom"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:background="@android:color/transparent">
+
+ <Button style="@style/MenuBarButton"
+ android:id="@+id/button_bar_home"
+ android:text="@string/button_bar.home"/>
+
+ <Button style="@style/MenuBarButton"
+ android:id="@+id/button_bar_music"
+ android:text="@string/button_bar.browse"/>
+
+ <Button style="@style/MenuBarButton"
+ android:id="@+id/button_bar_playlists"
+ android:text="@string/button_bar.playlists"/>
+
+ <Button style="@style/MenuBarButton"
+ android:id="@+id/button_bar_now_playing"
+ android:text="@string/button_bar.now_playing"/>
+</LinearLayout>
+
diff --git a/res/layout/chat.xml b/res/layout/chat.xml
new file mode 100644
index 00000000..fdeb5b36
--- /dev/null
+++ b/res/layout/chat.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:orientation="vertical" >
+
+ <include layout="@layout/tab_progress" />
+
+ <ListView
+ android:id="@+id/chat_entries"
+ android:layout_width="fill_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1.0"
+ android:textFilterEnabled="true" />
+
+ <LinearLayout
+ android:layout_height="4dip"
+ android:layout_width="fill_parent"
+ android:layout_marginTop="4dip"/>
+
+ <LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="bottom" >
+
+ <EditText
+ android:id="@+id/chat_edittext"
+ android:layout_width="0dip"
+ android:layout_height="40dip"
+ android:layout_weight="1"
+ android:autoLink="all"
+ android:hint="@string/chat.send_a_message"
+ android:inputType="textEmailAddress|textMultiLine"
+ android:linksClickable="true"
+ android:paddingBottom="10dip"
+ android:paddingTop="10dip" />
+
+ <ImageButton
+ android:id="@+id/chat_send"
+ android:layout_width="60dip"
+ android:layout_height="40dip"
+ android:src="?attr/chat_send" />
+
+ </LinearLayout>
+</LinearLayout> \ No newline at end of file
diff --git a/res/layout/chat_item.xml b/res/layout/chat_item.xml
new file mode 100644
index 00000000..b44631d1
--- /dev/null
+++ b/res/layout/chat_item.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical" >
+
+ <TextView
+ android:id="@+id/chat_username"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="6dip"
+ android:layout_marginRight="6dip"
+ android:ellipsize="marquee"
+ android:singleLine="true"
+ android:text="User"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textColor="?android:textColorSecondary"/>
+
+ <LinearLayout
+ android:id="@+id/chat_message_layout"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="2dip"
+ android:orientation="horizontal" >
+
+ <TextView
+ android:id="@+id/chat_time"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="6dip"
+ android:singleLine="true"
+ android:text="00:00"
+ android:textAppearance="?android:attr/textAppearanceMedium" />
+
+ <TextView
+ android:id="@+id/chat_message"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="6dip"
+ android:layout_marginRight="6dip"
+ android:autoLink="all"
+ android:linksClickable="true"
+ android:singleLine="false"
+ android:text="Message Text Goes Here"
+ android:textAppearance="?android:attr/textAppearanceMedium" />
+ </LinearLayout>
+</LinearLayout> \ No newline at end of file
diff --git a/res/layout/chat_item_reverse.xml b/res/layout/chat_item_reverse.xml
new file mode 100644
index 00000000..62695521
--- /dev/null
+++ b/res/layout/chat_item_reverse.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical" >
+
+ <TextView
+ android:id="@+id/chat_username"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="6dip"
+ android:gravity="right"
+ android:layout_gravity="right"
+ android:ellipsize="marquee"
+ android:singleLine="true"
+ android:text="User"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textColor="?android:textColorSecondary"/>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="2dip"
+ android:orientation="horizontal"
+ android:layout_gravity="right" >
+
+ <TextView
+ android:id="@+id/chat_time"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="6dip"
+ android:singleLine="true"
+ android:gravity="right"
+ android:text="00:00"
+ android:textAppearance="?android:attr/textAppearanceMedium" />
+
+ <TextView
+ android:id="@+id/chat_message"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="6dip"
+ android:layout_marginRight="6dip"
+ android:autoLink="all"
+ android:linksClickable="true"
+ android:singleLine="false"
+ android:gravity="right"
+ android:text="Chat message"
+ android:textAppearance="?android:attr/textAppearanceMedium" />
+ </LinearLayout>
+</LinearLayout> \ No newline at end of file
diff --git a/res/layout/create_podcast.xml b/res/layout/create_podcast.xml
new file mode 100644
index 00000000..5a2ec970
--- /dev/null
+++ b/res/layout/create_podcast.xml
@@ -0,0 +1,27 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/create_podcast_url_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="4dp"
+ android:textSize="20dp"
+ android:text="@string/select_podcasts.add_url"/>
+ <EditText
+ android:id="@+id/create_podcast_url"
+ android:inputType="textUri"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginLeft="4dp"
+ android:text="http://"/>
+ </LinearLayout>
+</LinearLayout> \ No newline at end of file
diff --git a/res/layout/download_activity.xml b/res/layout/download_activity.xml
new file mode 100644
index 00000000..3a1aa5e4
--- /dev/null
+++ b/res/layout/download_activity.xml
@@ -0,0 +1,4 @@
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/download_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" /> \ No newline at end of file
diff --git a/res/layout/download_media_buttons.xml b/res/layout/download_media_buttons.xml
new file mode 100644
index 00000000..1835a373
--- /dev/null
+++ b/res/layout/download_media_buttons.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:background="@android:color/transparent">
+
+ <ImageButton
+ style="@style/PlaybackControl.Small"
+ android:id="@+id/download_repeat"
+ android:src="?attr/media_button_repeat_off"
+ android:layout_alignParentLeft="true"
+ android:layout_centerVertical="true"
+ />
+
+ <github.daneren2005.dsub.view.AutoRepeatButton
+ style="@style/PlaybackControl"
+ android:id="@+id/download_previous"
+ android:src="?attr/media_button_backward"
+ android:layout_toLeftOf="@+id/download_pause"
+ android:layout_centerVertical="true"
+ />
+
+ <ImageButton
+ style="@style/PlaybackControl"
+ android:id="@+id/download_pause"
+ android:src="?attr/media_button_pause"
+ android:layout_centerInParent="true"
+ />
+
+ <ImageButton
+ style="@style/PlaybackControl"
+ android:id="@+id/download_stop"
+ android:src="?attr/media_button_stop"
+ android:layout_centerInParent="true"
+ />
+
+ <ImageButton
+ style="@style/PlaybackControl"
+ android:id="@+id/download_start"
+ android:src="?attr/media_button_start"
+ android:layout_centerInParent="true"
+ />
+
+ <github.daneren2005.dsub.view.AutoRepeatButton
+ style="@style/PlaybackControl"
+ android:id="@+id/download_next"
+ android:src="?attr/media_button_forward"
+ android:layout_toRightOf="@+id/download_start"
+ android:layout_centerVertical="true"
+ />
+
+ <ImageButton
+ style="@style/PlaybackControl.Small"
+ android:id="@+id/download_toggle_list"
+ android:src="@drawable/action_toggle_list"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ />
+</RelativeLayout> \ No newline at end of file
diff --git a/res/layout/download_playlist.xml b/res/layout/download_playlist.xml
new file mode 100644
index 00000000..e37981e2
--- /dev/null
+++ b/res/layout/download_playlist.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:layout_weight="1">
+
+ <View
+ android:layout_width="fill_parent"
+ android:layout_height="1px"
+ android:background="@color/dividerColor"/>
+
+ <TextView
+ android:id="@+id/download_empty"
+ android:text="@string/download.empty"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:padding="10dip"/>
+
+ <com.mobeta.android.dslv.DragSortListView
+ style="@style/DragDropListView"
+ android:id="@+id/download_list"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:layout_weight="1"
+ android:cacheColorHint="#00000000"
+ android:fastScrollEnabled="true"/>
+
+</LinearLayout> \ No newline at end of file
diff --git a/res/layout/download_slider.xml b/res/layout/download_slider.xml
new file mode 100644
index 00000000..d4998eea
--- /dev/null
+++ b/res/layout/download_slider.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_height="wrap_content"
+ android:layout_width="fill_parent"
+ android:background="@android:color/transparent"
+ android:paddingBottom="10dip">
+
+ <TextView
+ android:id="@+id/download_position"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_centerVertical="true"
+ android:paddingLeft="8dip"
+ android:text="0:00"
+ android:textSize="12sp"
+ android:textColor="?android:textColorPrimary"
+ android:paddingBottom="4dip"/>
+
+ <SeekBar
+ android:id="@+id/download_progress_bar"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:indeterminate="false"
+ android:paddingLeft="55dip"
+ android:paddingRight="55dip"
+ android:paddingTop="3dip"
+ android:paddingBottom="7dip" />
+
+ <TextView
+ android:id="@+id/download_duration"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:paddingRight="8dip"
+ android:text="-:--"
+ android:textSize="12sp"
+ android:textColor="?android:textColorPrimary"
+ android:paddingBottom="4dip"/>
+</RelativeLayout> \ No newline at end of file
diff --git a/res/layout/equalizer.xml b/res/layout/equalizer.xml
new file mode 100644
index 00000000..ee1a9560
--- /dev/null
+++ b/res/layout/equalizer.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:background="@drawable/album_art_background"
+ android:padding="16dip">
+
+ <CheckBox
+ android:id="@+id/equalizer_enabled"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/equalizer.enabled"
+ android:textColor="#c0c0c0"
+ android:textAppearance="?android:attr/textAppearanceMedium"/>
+
+ <ScrollView
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <LinearLayout
+ android:id="@+id/equalizer_layout"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"/>
+
+ <Button
+ android:id="@+id/equalizer_preset"
+ android:text="@string/equalizer.preset"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:layout_marginTop="20dip"
+ android:paddingLeft="40dip"
+ android:paddingRight="40dip"/>
+
+ </LinearLayout>
+ </ScrollView>
+
+</LinearLayout>
+
diff --git a/res/layout/equalizer_bar.xml b/res/layout/equalizer_bar.xml
new file mode 100644
index 00000000..c34d1108
--- /dev/null
+++ b/res/layout/equalizer_bar.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/equalizer.frequency"
+ android:textSize="12sp"
+ android:textColor="#c0c0c0"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:layout_alignParentLeft="true"
+ />
+
+ <TextView
+ android:id="@+id/equalizer.level"
+ android:text="0 dB"
+ android:textSize="12sp"
+ android:textColor="#c0c0c0"
+ android:gravity="right"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:layout_alignParentRight="true"
+ android:layout_toRightOf="@+id/equalizer.frequency"
+ />
+
+ <SeekBar
+ android:id="@+id/equalizer.bar"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/equalizer.frequency"
+ />
+
+
+</RelativeLayout>
+
diff --git a/res/layout/help.xml b/res/layout/help.xml
new file mode 100644
index 00000000..f22dee37
--- /dev/null
+++ b/res/layout/help.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+
+ <LinearLayout android:id="@+id/help_buttons"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="0"
+ android:layout_alignParentBottom="true"
+ android:padding="4dip"
+ android:gravity="center_horizontal"
+ android:background="#ffcccccc">
+
+ <Button android:id="@+id/help_back"
+ android:text="@string/help.back"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:layout_marginRight="5dip"
+ android:paddingLeft="25dip"
+ android:paddingRight="25dip"/>
+
+ <Button android:id="@+id/help_close"
+ android:text="@string/help.close"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:layout_marginLeft="5dip"
+ android:paddingLeft="25dip"
+ android:paddingRight="25dip"/>
+ </LinearLayout>
+
+
+ <WebView
+ android:id="@+id/help_contents"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_above="@id/help_buttons"
+ android:layout_weight="1"
+ android:fadingEdge="vertical"
+ android:fadingEdgeLength="12dip"/>
+
+ </RelativeLayout>
diff --git a/res/layout/home.xml b/res/layout/home.xml
new file mode 100644
index 00000000..018061fa
--- /dev/null
+++ b/res/layout/home.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/home_layout"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+
+ <View
+ android:layout_width="fill_parent"
+ android:layout_height="1px"
+ android:background="@color/dividerColor"/>
+
+ <ListView
+ android:id="@+id/main_list"
+ android:layout_width="fill_parent"
+ android:layout_height="0px"
+ android:layout_weight="1"/>
+
+ <View android:id="@+id/main_dummy"
+ android:layout_width="0px"
+ android:layout_height="0px"/>
+</LinearLayout>
+
diff --git a/res/layout/jukebox_volume.xml b/res/layout/jukebox_volume.xml
new file mode 100644
index 00000000..e124734b
--- /dev/null
+++ b/res/layout/jukebox_volume.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/toast_layout_root"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:background="@drawable/toast_frame">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/download.jukebox_volume"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textColor="#ffffffff"
+ android:shadowColor="#bb000000"
+ android:shadowRadius="2.75"
+ android:paddingLeft="32dp"
+ android:paddingRight="32dp"
+ android:paddingBottom="12dp"
+ />
+
+ <ProgressBar android:id="@+id/jukebox_volume_progress_bar"
+ style="@android:style/Widget.ProgressBar.Horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:paddingBottom="3dp"
+ />
+
+</LinearLayout> \ No newline at end of file
diff --git a/res/layout/lyrics.xml b/res/layout/lyrics.xml
new file mode 100644
index 00000000..4307d8dd
--- /dev/null
+++ b/res/layout/lyrics.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+
+ <include layout="@layout/tab_progress"/>
+
+ <ScrollView
+ android:layout_width="fill_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1.0">
+
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+ <TextView
+ android:id="@+id/lyrics_artist"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:gravity="center_horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:paddingLeft="10dip"
+ android:paddingRight="10dip"
+ android:paddingTop="10dip"
+ android:paddingBottom="4dip"
+ />
+
+ <TextView
+ android:id="@+id/lyrics_title"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:gravity="center_horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:paddingLeft="10dip"
+ android:paddingRight="10dip"
+ />
+
+ <TextView
+ android:id="@+id/lyrics_text"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:gravity="center_horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:paddingLeft="10dip"
+ android:paddingRight="10dip"
+ />
+
+ </LinearLayout>
+
+ </ScrollView>
+
+ <include layout="@layout/button_bar"/>
+
+</LinearLayout>
+
diff --git a/res/layout/main.xml b/res/layout/main.xml
new file mode 100644
index 00000000..f1509db6
--- /dev/null
+++ b/res/layout/main.xml
@@ -0,0 +1,81 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center_horizontal"
+ android:orientation="vertical" >
+
+ <android.support.v4.view.ViewPager
+ android:id="@+id/pager"
+ android:layout_width="fill_parent"
+ android:layout_height="0px"
+ android:layout_weight="1" >
+ </android.support.v4.view.ViewPager>
+
+ <View
+ android:layout_width="fill_parent"
+ android:layout_height="1px"
+ android:background="@color/dividerColor"/>
+
+ <LinearLayout
+ android:id="@+id/bottom_bar"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:background="@drawable/media_button"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@+id/album_art"
+ android:layout_width="50dip"
+ android:layout_height="50dip"
+ android:layout_gravity="left|center"
+ android:scaleType="fitStart"
+ android:src="@drawable/unknown_album"/>
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:paddingLeft="8dip">
+
+ <TextView
+ android:id="@+id/track_name"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:textColor="?android:textColorPrimary"
+ android:singleLine="true"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textSize="13sp"
+ android:text="@string/search.artists"/>
+
+ <TextView
+ android:id="@+id/artist_name"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:textColor="?android:textColorSecondary"
+ android:singleLine="true"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textSize="12sp"
+ android:text="@string/search.albums"/>
+ </LinearLayout>
+
+ <ImageButton
+ style="@style/PlaybackControl.Small"
+ android:id="@+id/download_previous"
+ android:src="?attr/media_button_backward"
+ android:layout_centerVertical="true"/>
+
+ <ImageButton
+ style="@style/PlaybackControl.Small"
+ android:id="@+id/download_start"
+ android:src="?attr/media_button_start"
+ android:layout_centerVertical="true"/>
+
+ <ImageButton
+ style="@style/PlaybackControl.Small"
+ android:id="@+id/download_next"
+ android:src="?attr/media_button_forward"
+ android:layout_centerVertical="true"/>
+ </LinearLayout>
+</LinearLayout> \ No newline at end of file
diff --git a/res/layout/main_buttons.xml b/res/layout/main_buttons.xml
new file mode 100644
index 00000000..1e60838d
--- /dev/null
+++ b/res/layout/main_buttons.xml
@@ -0,0 +1,157 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <LinearLayout
+ android:id="@+id/main_select_server"
+ android:orientation="horizontal"
+ android:paddingTop="2dip"
+ android:paddingBottom="2dip"
+ android:paddingLeft="6dp"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeight">
+
+ <ImageView
+ android:src="@drawable/main_select_server"
+ android:layout_gravity="center_vertical"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+
+ <TextView android:id="@+id/main.select_server_1"
+ android:text="@string/main.select_server"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="10dip"
+ android:layout_marginTop="6dip"
+ android:textAppearance="?android:attr/textAppearanceLarge"/>
+
+ <TextView android:id="@+id/main.select_server_2"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="10dip"
+ android:textAppearance="?android:attr/textAppearanceSmall"/>
+
+ </LinearLayout>
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/main_offline"
+ android:text="@string/main.offline"
+ android:drawablePadding="12dip"
+ android:drawableLeft="?attr/offline_icon"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:gravity="center_vertical"
+ android:paddingLeft="6dp"
+ android:paddingBottom="4dp"
+ android:minHeight="50dip"/>
+
+ <TextView
+ android:id="@+id/main_albums"
+ android:text="@string/main.albums_title"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="@color/cyan"
+ android:gravity="center_vertical"
+ android:paddingLeft="6dp"
+ android:textAllCaps="true"
+ android:textStyle="bold"
+ android:textSize="16sp"/>
+
+ <TextView
+ android:id="@+id/main_albums_newest"
+ android:text="@string/main.albums_newest"
+ android:drawableRight="@drawable/list_item_more"
+ android:drawablePadding="6dip"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:gravity="center_vertical"
+ android:paddingLeft="6dip"
+ android:paddingRight="6dip"
+ android:minHeight="50dip"/>
+ <TextView
+ android:id="@+id/main_albums_recent"
+ android:text="@string/main.albums_recent"
+ android:drawableRight="@drawable/list_item_more"
+ android:drawablePadding="6dip"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:gravity="center_vertical"
+ android:paddingLeft="6dip"
+ android:paddingRight="6dip"
+ android:minHeight="50dip"/>
+ <TextView
+ android:id="@+id/main_albums_frequent"
+ android:text="@string/main.albums_frequent"
+ android:drawableRight="@drawable/list_item_more"
+ android:drawablePadding="6dip"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:gravity="center_vertical"
+ android:paddingLeft="6dip"
+ android:paddingRight="6dip"
+ android:minHeight="50dip"/>
+ <TextView
+ android:id="@+id/main_albums_highest"
+ android:text="@string/main.albums_highest"
+ android:drawableRight="@drawable/list_item_more"
+ android:drawablePadding="6dip"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:gravity="center_vertical"
+ android:paddingLeft="6dip"
+ android:paddingRight="6dip"
+ android:minHeight="50dip"/>
+ <TextView
+ android:id="@+id/main_albums_starred"
+ android:text="@string/main.albums_starred"
+ android:drawableRight="@drawable/list_item_more"
+ android:drawablePadding="6dip"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:gravity="center_vertical"
+ android:paddingLeft="6dip"
+ android:paddingRight="6dip"
+ android:minHeight="50dip"/>
+ <TextView
+ android:id="@+id/main_albums_genres"
+ android:text="@string/main.albums_genres"
+ android:drawableRight="@drawable/list_item_more"
+ android:drawablePadding="6dip"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:gravity="center_vertical"
+ android:paddingLeft="6dip"
+ android:paddingRight="6dip"
+ android:minHeight="50dip"/>
+ <TextView
+ android:id="@+id/main_albums_random"
+ android:text="@string/main.albums_random"
+ android:drawableRight="@drawable/list_item_more"
+ android:drawablePadding="6dip"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:gravity="center_vertical"
+ android:paddingLeft="6dip"
+ android:paddingRight="6dip"
+ android:minHeight="50dip"/>
+
+</LinearLayout>
+
diff --git a/res/layout/notification.xml b/res/layout/notification.xml
new file mode 100644
index 00000000..22e2cb63
--- /dev/null
+++ b/res/layout/notification.xml
@@ -0,0 +1,103 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/statusbar"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:orientation="horizontal" >
+
+ <ImageView
+ android:id="@+id/notification_image"
+ android:layout_width="64.0dip"
+ android:layout_height="64.0dip"
+ android:layout_weight="0.0"
+ android:gravity="center" />
+
+ <LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:orientation="vertical"
+ android:paddingLeft="11.0dip">
+
+ <TextView
+ android:id="@+id/notification_title"
+ style="@android:style/TextAppearance.StatusBar.EventContent.Title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="left"
+ android:ellipsize="marquee"
+ android:focusable="true"
+ android:singleLine="true" />
+
+ <LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:orientation="horizontal" >
+
+ <LinearLayout
+ android:layout_width="0.0dp"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:layout_weight="1.0"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/notification_artist"
+ style="@android:style/TextAppearance.StatusBar.EventContent"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="left"
+ android:ellipsize="end"
+ android:scrollHorizontally="true"
+ android:singleLine="true" />
+
+ <TextView
+ android:id="@+id/notification_album"
+ style="@android:style/TextAppearance.StatusBar.EventContent"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="left"
+ android:ellipsize="end"
+ android:scrollHorizontally="true"
+ android:singleLine="true" />
+ </LinearLayout>
+
+ <ImageButton
+ android:id="@+id/control_previous"
+ android:src="@drawable/notification_prev"
+ android:background="@drawable/btn_bg"
+ android:layout_width="34dip"
+ android:layout_height="34dip"
+ android:layout_gravity="center|right"
+ android:layout_marginRight="10dip"
+ android:layout_marginTop="2dip"
+ android:layout_weight="0.0"
+ android:scaleType="fitXY"/>
+
+ <ImageButton
+ android:id="@+id/control_pause"
+ android:src="@drawable/notification_pause"
+ android:background="@drawable/btn_bg"
+ android:layout_width="34dip"
+ android:layout_height="34dip"
+ android:layout_gravity="center|right"
+ android:layout_marginRight="10dip"
+ android:layout_marginTop="2dip"
+ android:layout_weight="0.0"
+ android:scaleType="fitXY"/>
+
+ <ImageButton
+ android:id="@+id/control_next"
+ android:src="@drawable/notification_next"
+ android:background="@drawable/btn_bg"
+ android:layout_width="34dip"
+ android:layout_height="34dip"
+ android:layout_gravity="center|right"
+ android:layout_marginRight="10dip"
+ android:layout_marginTop="2dip"
+ android:layout_weight="0.0"
+ android:scaleType="fitXY"/>
+ </LinearLayout>
+ </LinearLayout>
+</LinearLayout>
diff --git a/res/layout/notification_expanded.xml b/res/layout/notification_expanded.xml
new file mode 100644
index 00000000..70e7269c
--- /dev/null
+++ b/res/layout/notification_expanded.xml
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/statusbar"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:orientation="horizontal" >
+
+ <ImageView
+ android:id="@+id/notification_image"
+ android:layout_width="128dp"
+ android:layout_height="128dp"
+ android:gravity="center" />
+
+ <LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="0.0"
+ android:orientation="vertical"
+ android:paddingLeft="11.0dip" >
+
+ <TextView
+ android:id="@+id/notification_title"
+ style="@android:style/TextAppearance.StatusBar.EventContent.Title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="left"
+ android:ellipsize="marquee"
+ android:focusable="true"
+ android:singleLine="true" />
+
+ <TextView
+ android:id="@+id/notification_artist"
+ style="@android:style/TextAppearance.StatusBar.EventContent"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="left"
+ android:ellipsize="end"
+ android:scrollHorizontally="true"
+ android:singleLine="true" />
+
+ <TextView
+ android:id="@+id/notification_album"
+ style="@android:style/TextAppearance.StatusBar.EventContent"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="left"
+ android:ellipsize="end"
+ android:scrollHorizontally="true"
+ android:singleLine="true" />
+
+ <TextView
+ android:id="@+id/textView1"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent" />
+
+ <LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center|fill"
+ android:gravity="center_horizontal"
+ android:orientation="horizontal" >
+
+ <ImageButton
+ android:id="@+id/control_previous"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:layout_weight="0.0"
+ android:background="@drawable/btn_bg"
+ android:scaleType="fitXY"
+ android:src="@drawable/notification_prev" />
+
+ <ImageButton
+ android:id="@+id/control_pause"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:layout_marginLeft="10dp"
+ android:layout_marginRight="10dp"
+ android:layout_weight="0.0"
+ android:background="@drawable/btn_bg"
+ android:scaleType="fitXY"
+ android:src="@drawable/notification_pause" />
+
+ <ImageButton
+ android:id="@+id/control_next"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:layout_weight="0.0"
+ android:background="@drawable/btn_bg"
+ android:scaleType="fitXY"
+ android:src="@drawable/notification_next" />
+ </LinearLayout>
+
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/res/layout/play_video.xml b/res/layout/play_video.xml
new file mode 100644
index 00000000..6a9f3f74
--- /dev/null
+++ b/res/layout/play_video.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+
+ <WebView
+ android:id="@+id/play_video_contents"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"/>
+
+</FrameLayout>
diff --git a/res/layout/playlist_list_item.xml b/res/layout/playlist_list_item.xml
new file mode 100644
index 00000000..1ec5753f
--- /dev/null
+++ b/res/layout/playlist_list_item.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/playlist_name"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:gravity="left|center_vertical"
+ android:paddingLeft="6dip"
+ android:paddingRight="6dip"
+ android:minHeight="50dip"/>
+
+ <ImageView
+ android:id="@+id/playlist_more"
+ android:src="@drawable/list_item_more"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:layout_gravity="right|center_vertical"
+ android:paddingRight="6dip"
+ android:background="@drawable/menubar_button"/>
+</LinearLayout> \ No newline at end of file
diff --git a/res/layout/progress.xml b/res/layout/progress.xml
new file mode 100644
index 00000000..4a693cb3
--- /dev/null
+++ b/res/layout/progress.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_weight="1"
+ android:layout_width="0dip"
+ android:layout_height="fill_parent"
+ android:padding="10dp">
+
+ <ProgressBar
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:layout_marginRight="10dp"/>
+
+ <TextView
+ android:id="@+id/progress_message"
+ android:text="@string/progress.wait"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"/>
+
+</LinearLayout> \ No newline at end of file
diff --git a/res/layout/save_playlist.xml b/res/layout/save_playlist.xml
new file mode 100644
index 00000000..43f1827a
--- /dev/null
+++ b/res/layout/save_playlist.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/save_playlist_root"
+ android:padding="10dip"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+
+ <EditText
+ android:id="@+id/save_playlist_name"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:inputType="text"
+ android:singleLine="true"/>
+
+ <CheckBox
+ android:id="@+id/save_playlist_overwrite"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/playlist.overwrite"
+ android:layout_marginLeft="4dp"
+ android:checked="false"
+ android:visibility="gone"/>
+
+</LinearLayout>
+
diff --git a/res/layout/search.xml b/res/layout/search.xml
new file mode 100644
index 00000000..d1c5c84c
--- /dev/null
+++ b/res/layout/search.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/search_layout"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+
+ <View
+ android:layout_width="fill_parent"
+ android:layout_height="1px"
+ android:background="@color/dividerColor"/>
+
+ <include layout="@layout/tab_progress"/>
+
+ <ListView
+ android:id="@+id/search_list"
+ android:layout_width="fill_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1.0"
+ android:fastScrollEnabled="true"
+ />
+</LinearLayout> \ No newline at end of file
diff --git a/res/layout/search_buttons.xml b/res/layout/search_buttons.xml
new file mode 100644
index 00000000..10b72166
--- /dev/null
+++ b/res/layout/search_buttons.xml
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/search_search"
+ android:text="@string/search.search"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:drawablePadding="0dp"
+ android:drawableLeft="@drawable/search"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:gravity="center"
+ android:padding="12dp"/>
+
+ <TextView
+ android:id="@+id/search_artists"
+ android:text="@string/search.artists"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="#EFEFEF"
+ android:textStyle="bold"
+ android:background="#A5A5A5"
+ android:gravity="center_vertical"
+ android:paddingLeft="4dp"/>
+
+ <TextView
+ android:id="@+id/search_albums"
+ android:text="@string/search.albums"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="#EFEFEF"
+ android:textStyle="bold"
+ android:background="#A5A5A5"
+ android:gravity="center_vertical"
+ android:paddingLeft="4dp"/>
+
+ <TextView
+ android:id="@+id/search_songs"
+ android:text="@string/search.songs"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="#EFEFEF"
+ android:textStyle="bold"
+ android:background="#A5A5A5"
+ android:gravity="center_vertical"
+ android:paddingLeft="4dp"/>
+
+ <TextView
+ android:id="@+id/search_more_artists"
+ android:text="@string/search.more"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:gravity="center"
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp"/>
+
+ <TextView
+ android:id="@+id/search_more_albums"
+ android:text="@string/search.more"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:gravity="center"
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp"/>
+
+ <TextView
+ android:id="@+id/search_more_songs"
+ android:text="@string/search.more"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:gravity="center"
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp"/>
+
+</LinearLayout>
+
diff --git a/res/layout/select_album.xml b/res/layout/select_album.xml
new file mode 100644
index 00000000..01df495a
--- /dev/null
+++ b/res/layout/select_album.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/select_album_layout"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+
+ <View
+ android:layout_width="fill_parent"
+ android:layout_height="1px"
+ android:background="@color/dividerColor"/>
+
+ <include layout="@layout/tab_progress"/>
+
+ <TextView
+ android:id="@+id/select_album_empty"
+ android:text="@string/select_album.empty"
+ android:visibility="gone"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:padding="10dip"/>
+
+ <com.mobeta.android.dslv.DragSortListView
+ style="@style/DragDropListView"
+ android:id="@+id/select_album_entries"
+ android:textFilterEnabled="true"
+ android:layout_width="fill_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1.0"
+ android:fastScrollEnabled="true"/>
+</LinearLayout> \ No newline at end of file
diff --git a/res/layout/select_album_footer.xml b/res/layout/select_album_footer.xml
new file mode 100644
index 00000000..c1a30a1a
--- /dev/null
+++ b/res/layout/select_album_footer.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:background="@android:color/transparent"
+ android:paddingTop="6dp"
+ android:paddingBottom="0dp"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <Button android:id="@+id/select_album_more"
+ android:text="@string/select_album.more"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:visibility="gone"
+ android:layout_marginLeft="6dp"
+ android:layout_marginRight="6dp"
+ android:layout_weight="1"
+ android:layout_width="0dp"
+ android:layout_height="fill_parent"/>
+
+</LinearLayout>
+
diff --git a/res/layout/select_album_header.xml b/res/layout/select_album_header.xml
new file mode 100644
index 00000000..2bf74110
--- /dev/null
+++ b/res/layout/select_album_header.xml
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/select_album_header"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <ImageView
+ android:id="@+id/select_album_art"
+ android:src="@drawable/unknown_album_large"
+ android:layout_width="120dip"
+ android:layout_height="120dip"
+ android:layout_alignParentTop="true"
+ android:layout_alignParentLeft="true"
+ android:layout_marginRight="10dip"
+ android:scaleType="fitCenter"
+ android:contentDescription="@null"/>
+
+ <TextView
+ android:text="This is the album title"
+ android:id="@+id/select_album_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_toRightOf="@+id/select_album_art"
+ android:paddingTop="20dip"
+ android:paddingBottom="8dip"
+ android:paddingRight="4dip"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textStyle="bold"
+ android:singleLine="true"
+ android:ellipsize="end"/>
+
+ <TextView
+ android:text="This is the artist name"
+ android:id="@+id/select_album_artist"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/select_album_title"
+ android:layout_toRightOf="@+id/select_album_art"
+ android:paddingBottom="2dip"
+ android:paddingRight="4dip"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:singleLine="true"
+ android:ellipsize="end"/>
+
+ <TextView
+ android:text="XX SONGS"
+ android:id="@+id/select_album_song_count"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/select_album_artist"
+ android:layout_toRightOf="@+id/select_album_art"
+ android:paddingRight="4dip"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textSize="10sp"
+ android:singleLine="true"
+ android:ellipsize="none"/>
+
+ <TextView
+ android:text="0:00"
+ android:id="@+id/select_album_song_length"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/select_album_song_count"
+ android:layout_toRightOf="@+id/select_album_art"
+ android:paddingRight="4dip"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textSize="10sp"
+ android:singleLine="true"
+ android:ellipsize="none"/>
+</RelativeLayout>
+
+
diff --git a/res/layout/select_artist.xml b/res/layout/select_artist.xml
new file mode 100644
index 00000000..fef51d3c
--- /dev/null
+++ b/res/layout/select_artist.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/select_artist_layout"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+
+ <View
+ android:layout_width="fill_parent"
+ android:layout_height="1px"
+ android:background="@color/dividerColor"/>
+
+ <include layout="@layout/tab_progress"/>
+
+ <ListView android:id="@+id/select_artist_list"
+ android:textFilterEnabled="true"
+ android:fastScrollEnabled="true"
+ android:layout_width="fill_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1.0"/>
+</LinearLayout>
+
diff --git a/res/layout/select_artist_header.xml b/res/layout/select_artist_header.xml
new file mode 100644
index 00000000..0b3d151b
--- /dev/null
+++ b/res/layout/select_artist_header.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+ <LinearLayout
+ android:id="@+id/select_artist_folder"
+ android:orientation="horizontal"
+ android:paddingTop="2dip"
+ android:paddingBottom="2dip"
+ android:paddingLeft="6dp"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeight">
+
+ <ImageView
+ android:src="@drawable/main_select_server"
+ android:layout_gravity="center_vertical"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+
+ <TextView android:id="@+id/select_artist_folder_1"
+ android:text="@string/select_artist.folder"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="10dip"
+ android:layout_marginTop="6dip"
+ android:textAppearance="?android:attr/textAppearanceLarge"/>
+
+ <TextView android:id="@+id/select_artist_folder_2"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="10dip"
+ android:textAppearance="?android:attr/textAppearanceSmall"/>
+
+ </LinearLayout>
+ </LinearLayout>
+</LinearLayout> \ No newline at end of file
diff --git a/res/layout/select_genres.xml b/res/layout/select_genres.xml
new file mode 100644
index 00000000..95f9d415
--- /dev/null
+++ b/res/layout/select_genres.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/select_genre_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:orientation="vertical" >
+
+ <include layout="@layout/tab_progress" />
+
+ <TextView
+ android:id="@+id/select_genre_empty"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:padding="10dip"
+ android:text="@string/select_genre.empty"
+ android:visibility="gone" />
+
+ <ListView
+ android:id="@+id/select_genre_list"
+ android:layout_width="fill_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1.0"
+ android:textFilterEnabled="true"
+ android:fastScrollEnabled="true"/>
+ </LinearLayout>
+</FrameLayout> \ No newline at end of file
diff --git a/res/layout/select_playlist.xml b/res/layout/select_playlist.xml
new file mode 100644
index 00000000..e18283bd
--- /dev/null
+++ b/res/layout/select_playlist.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/select_playlist_layout"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+
+ <View
+ android:layout_width="fill_parent"
+ android:layout_height="1px"
+ android:background="@color/dividerColor"/>
+
+ <include layout="@layout/tab_progress"/>
+
+ <TextView
+ android:id="@+id/select_playlist_empty"
+ android:text="@string/select_playlist.empty"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:padding="10dip"
+ android:visibility="gone"/>
+
+ <ListView android:id="@+id/select_playlist_list"
+ android:layout_width="fill_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1.0"
+ android:fastScrollEnabled="true"/>
+
+</LinearLayout>
+
diff --git a/res/layout/select_podcasts.xml b/res/layout/select_podcasts.xml
new file mode 100644
index 00000000..ea4fb07c
--- /dev/null
+++ b/res/layout/select_podcasts.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/select_podcasts_layout"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:orientation="vertical" >
+
+ <View
+ android:layout_width="fill_parent"
+ android:layout_height="1px"
+ android:background="@color/dividerColor"/>
+
+ <include layout="@layout/tab_progress" />
+
+ <TextView
+ android:id="@+id/select_podcasts_empty"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:padding="10dip"
+ android:text="@string/select_podcasts.empty"
+ android:visibility="gone" />
+
+ <ListView
+ android:id="@+id/select_podcasts_list"
+ android:layout_width="fill_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1.0"
+ android:fastScrollEnabled="true"/>
+</LinearLayout>
diff --git a/res/layout/shuffle_dialog.xml b/res/layout/shuffle_dialog.xml
new file mode 100644
index 00000000..295f57cb
--- /dev/null
+++ b/res/layout/shuffle_dialog.xml
@@ -0,0 +1,80 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/start_year_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="4dp"
+ android:textSize="20dp"
+ android:text="@string/shuffle.startYear" />
+ <EditText
+ android:id="@+id/start_year"
+ android:inputType="number"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginLeft="4dp"
+ android:hint="@string/shuffle.startYear" />
+ </LinearLayout>
+
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/end_year_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="4dp"
+ android:textSize="20dp"
+ android:text="@string/shuffle.endYear" />
+ <EditText
+ android:id="@+id/end_year"
+ android:inputType="number"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginLeft="4dp"
+ android:hint="@string/shuffle.endYear" />
+ </LinearLayout>
+
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/genre_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="4dp"
+ android:textSize="20dp"
+ android:text="@string/shuffle.genre" />
+ <EditText
+ android:id="@+id/genre"
+ android:inputType="text"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginLeft="4dp"
+ android:hint="@string/shuffle.genre"/>
+
+ <Button
+ android:id="@+id/genre_combo"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginLeft="4dp"
+ android:text="@string/shuffle.genre"
+ style="?android:attr/spinnerStyle"/>
+ </LinearLayout>
+</LinearLayout>
diff --git a/res/layout/song_list_item.xml b/res/layout/song_list_item.xml
new file mode 100644
index 00000000..90060894
--- /dev/null
+++ b/res/layout/song_list_item.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@id/drag_handle"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="?android:attr/listPreferredItemHeight">
+
+ <CheckedTextView
+ android:id="@+id/song_check"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:gravity="center_vertical"
+ android:checkMark="@drawable/btn_check"
+ android:paddingLeft="3dip"/>
+
+ <LinearLayout android:orientation="vertical"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_gravity="center_vertical">
+
+ <LinearLayout android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical">
+
+ <TextView
+ android:id="@+id/song_title"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_gravity="left|center_vertical"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:singleLine="true"
+ android:ellipsize="marquee"
+ android:drawablePadding="6dip"
+ android:paddingLeft="6dip"
+ android:paddingRight="6dip"/>
+
+ <ImageButton
+ android:id="@+id/song_star"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="right|center_vertical"
+ android:src="@drawable/ic_stat_star"
+ android:background="@null"
+ android:focusable="false"
+ android:visibility="gone"/>
+
+ <TextView
+ android:id="@+id/song_status"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="right|center_vertical"
+ android:drawablePadding="1dip"
+ android:paddingRight="6dip"/>
+ </LinearLayout>
+
+ <LinearLayout android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical">
+
+ <TextView
+ android:id="@+id/song_artist"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_gravity="left|center_vertical"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:singleLine="true"
+ android:ellipsize="middle"
+ android:paddingLeft="6dip"/>
+
+ <TextView
+ android:id="@+id/song_duration"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="right|center_vertical"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:singleLine="true"
+ android:paddingLeft="3dip"
+ android:paddingRight="4dip"/>
+
+ </LinearLayout>
+ </LinearLayout>
+
+ <ImageView
+ android:id="@+id/artist_more"
+ android:src="@drawable/list_item_more"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:layout_gravity="right|center_vertical"
+ android:paddingRight="6dip"
+ android:background="@drawable/menubar_button"/>
+</LinearLayout>
diff --git a/res/layout/start_timer.xml b/res/layout/start_timer.xml
new file mode 100644
index 00000000..3b607a44
--- /dev/null
+++ b/res/layout/start_timer.xml
@@ -0,0 +1,27 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/timer_length_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="4dp"
+ android:textSize="20dp"
+ android:text="@string/download.timer_length" />
+ <EditText
+ android:id="@+id/timer_length"
+ android:inputType="number"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginLeft="4dp"
+ android:hint="@string/download.timer_length" />
+ </LinearLayout>
+</LinearLayout> \ No newline at end of file
diff --git a/res/layout/sync_dialog.xml b/res/layout/sync_dialog.xml
new file mode 100644
index 00000000..5133b753
--- /dev/null
+++ b/res/layout/sync_dialog.xml
@@ -0,0 +1,12 @@
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content" >
+ <CheckBox
+ android:id="@+id/sync_default"
+ style="?android:attr/textAppearanceMedium"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="5dp"
+ android:checked="false"
+ android:text="@string/offline.sync_dialog_default"/>
+</FrameLayout> \ No newline at end of file
diff --git a/res/layout/tab_progress.xml b/res/layout/tab_progress.xml
new file mode 100644
index 00000000..6a88600c
--- /dev/null
+++ b/res/layout/tab_progress.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/tab_progress"
+ android:orientation="horizontal"
+ android:visibility="gone"
+ android:padding="10dp"
+ android:layout_gravity="top"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <ProgressBar
+ android:layout_width="16dp"
+ android:layout_height="16dp"
+ android:layout_gravity="center_vertical"
+ android:layout_marginRight="6dp"/>
+
+ <TextView
+ android:id="@+id/tab_progress_message"
+ android:text="@string/progress.wait"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"/>
+
+</LinearLayout> \ No newline at end of file
diff --git a/res/layout/update_playlist.xml b/res/layout/update_playlist.xml
new file mode 100644
index 00000000..7354ef5c
--- /dev/null
+++ b/res/layout/update_playlist.xml
@@ -0,0 +1,70 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/get_playlist_name_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="4dp"
+ android:textSize="20dp"
+ android:text="@string/common.name" />
+ <EditText
+ android:id="@+id/get_playlist_name"
+ android:inputType="text"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginLeft="4dp"
+ android:hint="@string/common.name" />
+ </LinearLayout>
+
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/get_playlist_comment_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="4dp"
+ android:textSize="20dp"
+ android:text="@string/common.comment" />
+ <EditText
+ android:id="@+id/get_playlist_comment"
+ android:inputType="text"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginLeft="4dp"
+ android:hint="@string/common.comment" />
+ </LinearLayout>
+
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/get_playlist_public_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="4dp"
+ android:textSize="20dp"
+ android:text="@string/common.public" />
+ <CheckBox
+ android:id="@+id/get_playlist_public"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginLeft="4dp"
+ android:checked="false"/>
+ </LinearLayout>
+</LinearLayout> \ No newline at end of file
diff --git a/res/menu/chat.xml b/res/menu/chat.xml
new file mode 100644
index 00000000..e0f9a718
--- /dev/null
+++ b/res/menu/chat.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/menu_refresh"
+ android:icon="@drawable/action_refresh"
+ android:title="@string/menu.refresh"
+ android:showAsAction="always|withText"/>
+
+ <item
+ android:id="@+id/menu_settings"
+ android:icon="@drawable/action_settings"
+ android:title="@string/menu.settings"/>
+
+ <item
+ android:id="@+id/menu_exit"
+ android:icon="@drawable/action_exit"
+ android:title="@string/menu.exit"/>
+</menu> \ No newline at end of file
diff --git a/res/menu/empty.xml b/res/menu/empty.xml
new file mode 100644
index 00000000..b6db96aa
--- /dev/null
+++ b/res/menu/empty.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/menu_refresh"
+ android:icon="@drawable/action_refresh"
+ android:title="@string/menu.refresh"
+ android:showAsAction="always|withText"/>
+</menu>
diff --git a/res/menu/main.xml b/res/menu/main.xml
new file mode 100644
index 00000000..c9420236
--- /dev/null
+++ b/res/menu/main.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/menu_search"
+ android:icon="@drawable/action_search"
+ android:title="@string/menu.search"
+ android:showAsAction="always|withText"/>
+
+ <item
+ android:id="@+id/menu_shuffle"
+ android:icon="@drawable/action_shuffle"
+ android:title="@string/menu.shuffle"
+ android:showAsAction="always|withText"/>
+
+ <item
+ android:id="@+id/menu_help"
+ android:icon="@drawable/action_help"
+ android:title="@string/menu.help"/>
+
+ <item
+ android:id="@+id/menu_about"
+ android:icon="@drawable/action_help"
+ android:title="@string/menu.about"/>
+
+ <item
+ android:id="@+id/menu_log"
+ android:title="@string/menu.log"/>
+
+ <item
+ android:id="@+id/menu_changelog"
+ android:title="@string/changelog_full_title"/>
+
+ <item
+ android:id="@+id/menu_settings"
+ android:icon="@drawable/action_settings"
+ android:title="@string/menu.settings"/>
+
+ <item
+ android:id="@+id/menu_exit"
+ android:icon="@drawable/action_exit"
+ android:title="@string/menu.exit"/>
+
+</menu>
diff --git a/res/menu/nowplaying.xml b/res/menu/nowplaying.xml
new file mode 100644
index 00000000..572c5bac
--- /dev/null
+++ b/res/menu/nowplaying.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/menu_shuffle"
+ android:icon="@drawable/action_shuffle"
+ android:title="@string/download.menu_shuffle"
+ android:showAsAction="ifRoom|withText"/>
+
+ <item
+ android:id="@+id/menu_remove_all"
+ android:icon="@drawable/action_remove_all"
+ android:title="@string/download.menu_remove_all"
+ android:showAsAction="ifRoom|withText"/>
+
+ <item
+ android:id="@+id/menu_save_playlist"
+ android:icon="@drawable/action_save"
+ android:title="@string/download.menu_save"
+ android:showAsAction="ifRoom|withText"/>
+
+ <item
+ android:id="@+id/menu_screen_on_off"
+ android:icon="@drawable/action_screen_on_off"
+ android:title="@string/download.menu_screen_on"
+ android:showAsAction="ifRoom|withText"/>
+
+ <item
+ android:id="@+id/menu_toggle_timer"
+ android:title="@string/download.start_timer"/>
+
+ <item
+ android:id="@+id/menu_toggle_now_playing"
+ android:title="@string/download.show_downloading"/>
+
+ <item
+ android:id="@+id/menu_exit"
+ android:icon="@drawable/action_exit"
+ android:title="@string/menu.exit"/>
+</menu>
diff --git a/res/menu/nowplaying_context.xml b/res/menu/nowplaying_context.xml
new file mode 100644
index 00000000..f42c3644
--- /dev/null
+++ b/res/menu/nowplaying_context.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/menu_info"
+ android:title="@string/common.info"
+ />
+
+ <item
+ android:id="@+id/menu_show_album"
+ android:title="@string/download.menu_show_album"/>
+
+ <item
+ android:id="@+id/menu_lyrics"
+ android:title="@string/download.menu_lyrics"/>
+
+ <item
+ android:id="@+id/menu_remove"
+ android:title="@string/download.menu_remove"/>
+
+ <item
+ android:id="@+id/menu_delete"
+ android:title="@string/download.menu_delete"/>
+
+ <item
+ android:id="@+id/menu_star"
+ android:title="@string/common.star"/>
+
+ <item
+ android:id="@+id/menu_add_playlist"
+ android:title="@string/menu.add_playlist"/>
+</menu>
diff --git a/res/menu/nowplaying_context_offline.xml b/res/menu/nowplaying_context_offline.xml
new file mode 100644
index 00000000..1446353f
--- /dev/null
+++ b/res/menu/nowplaying_context_offline.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/menu_info"
+ android:title="@string/common.info"
+ />
+
+ <item
+ android:id="@+id/menu_show_album"
+ android:title="@string/download.menu_show_album"/>
+
+ <item
+ android:id="@+id/menu_remove"
+ android:title="@string/download.menu_remove"/>
+
+ <item
+ android:id="@+id/menu_delete"
+ android:title="@string/download.menu_delete"/>
+
+ <item
+ android:id="@+id/menu_star"
+ android:title="@string/common.star"/>
+</menu>
diff --git a/res/menu/nowplaying_downloading.xml b/res/menu/nowplaying_downloading.xml
new file mode 100644
index 00000000..9376731a
--- /dev/null
+++ b/res/menu/nowplaying_downloading.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/menu_remove_all"
+ android:icon="@drawable/action_remove_all"
+ android:title="@string/download.menu_remove_all"
+ android:showAsAction="always|withText"/>
+
+ <item
+ android:id="@+id/menu_screen_on_off"
+ android:icon="@drawable/action_screen_on_off"
+ android:title="@string/download.menu_screen_on"
+ android:showAsAction="ifRoom|withText"/>
+
+ <item
+ android:id="@+id/menu_toggle_timer"
+ android:title="@string/download.start_timer"/>
+
+ <item
+ android:id="@+id/menu_toggle_now_playing"
+ android:title="@string/download.show_now_playing"/>
+
+ <item
+ android:id="@+id/menu_exit"
+ android:icon="@drawable/action_exit"
+ android:title="@string/menu.exit"/>
+</menu>
diff --git a/res/menu/nowplaying_offline.xml b/res/menu/nowplaying_offline.xml
new file mode 100644
index 00000000..e3e85040
--- /dev/null
+++ b/res/menu/nowplaying_offline.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item
+ android:id="@+id/menu_shuffle"
+ android:icon="@drawable/action_shuffle"
+ android:title="@string/download.menu_shuffle"
+ android:showAsAction="always|withText"/>
+
+ <item
+ android:id="@+id/menu_remove_all"
+ android:icon="@drawable/action_remove_all"
+ android:title="@string/download.menu_remove_all"
+ android:showAsAction="always|withText"/>
+
+ <item
+ android:id="@+id/menu_screen_on_off"
+ android:icon="@drawable/action_screen_on_off"
+ android:title="@string/download.menu_screen_on"
+ android:showAsAction="ifRoom|withText"/>
+
+ <item
+ android:id="@+id/menu_toggle_timer"
+ android:title="@string/download.start_timer"/>
+
+ <item
+ android:id="@+id/menu_exit"
+ android:icon="@drawable/action_exit"
+ android:title="@string/menu.exit"/>
+</menu>
diff --git a/res/menu/search.xml b/res/menu/search.xml
new file mode 100644
index 00000000..b9cdecac
--- /dev/null
+++ b/res/menu/search.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/menu_search"
+ android:icon="@drawable/action_search"
+ android:title="@string/menu.search"
+ android:showAsAction="ifRoom|withText"/>
+
+ <item
+ android:id="@+id/menu_help"
+ android:icon="@drawable/ic_menu_help"
+ android:title="@string/menu.help"/>
+
+ <item
+ android:id="@+id/menu_settings"
+ android:icon="@drawable/ic_menu_settings"
+ android:title="@string/menu.settings"/>
+
+ <item
+ android:id="@+id/menu_exit"
+ android:icon="@drawable/ic_menu_exit"
+ android:title="@string/menu.exit"/>
+
+</menu>
diff --git a/res/menu/select_album.xml b/res/menu/select_album.xml
new file mode 100644
index 00000000..5ca9c537
--- /dev/null
+++ b/res/menu/select_album.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/menu_play_now"
+ android:icon="@drawable/action_play_all"
+ android:title="@string/menu.play"
+ android:showAsAction="always|withText"/>
+
+ <item
+ android:id="@+id/menu_refresh"
+ android:icon="@drawable/action_refresh"
+ android:title="@string/menu.refresh"
+ android:showAsAction="always|withText"/>
+
+ <item
+ android:id="@+id/menu_shuffle"
+ android:icon="@drawable/action_shuffle"
+ android:title="@string/menu.shuffle"
+ android:showAsAction="ifRoom|withText"/>
+</menu>
diff --git a/res/menu/select_album_context.xml b/res/menu/select_album_context.xml
new file mode 100644
index 00000000..00fe7993
--- /dev/null
+++ b/res/menu/select_album_context.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item
+ android:id="@+id/album_menu_play_now"
+ android:title="@string/common.play_now"
+ />
+
+ <item
+ android:id="@+id/album_menu_play_shuffled"
+ android:title="@string/common.play_shuffled"
+ />
+
+ <item
+ android:id="@+id/album_menu_play_last"
+ android:title="@string/common.play_last"
+ />
+
+ <item
+ android:id="@+id/album_menu_download"
+ android:title="@string/common.download"
+ />
+
+ <item
+ android:id="@+id/album_menu_pin"
+ android:title="@string/common.pin"
+ />
+
+ <item
+ android:id="@+id/album_menu_delete"
+ android:title="@string/common.delete"/>
+
+ <item
+ android:id="@+id/album_menu_star"
+ android:title="@string/common.star"/>
+
+</menu>
diff --git a/res/menu/select_album_context_offline.xml b/res/menu/select_album_context_offline.xml
new file mode 100644
index 00000000..70cf9da9
--- /dev/null
+++ b/res/menu/select_album_context_offline.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/album_menu_play_now"
+ android:title="@string/common.play_now"
+ />
+
+ <item
+ android:id="@+id/album_menu_play_shuffled"
+ android:title="@string/common.play_shuffled"
+ />
+
+ <item
+ android:id="@+id/album_menu_play_last"
+ android:title="@string/common.play_last"
+ />
+
+ <item
+ android:id="@+id/album_menu_delete"
+ android:title="@string/common.delete"/>
+
+ <item
+ android:id="@+id/album_menu_star"
+ android:title="@string/common.star"/>
+</menu> \ No newline at end of file
diff --git a/res/menu/select_album_list.xml b/res/menu/select_album_list.xml
new file mode 100644
index 00000000..b6db96aa
--- /dev/null
+++ b/res/menu/select_album_list.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/menu_refresh"
+ android:icon="@drawable/action_refresh"
+ android:title="@string/menu.refresh"
+ android:showAsAction="always|withText"/>
+</menu>
diff --git a/res/menu/select_artist.xml b/res/menu/select_artist.xml
new file mode 100644
index 00000000..a7b988a5
--- /dev/null
+++ b/res/menu/select_artist.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/menu_refresh"
+ android:icon="@drawable/action_refresh"
+ android:title="@string/menu.refresh"
+ android:showAsAction="always|withText"/>
+
+ <item
+ android:id="@+id/menu_shuffle"
+ android:icon="@drawable/action_shuffle"
+ android:title="@string/menu.shuffle"
+ android:showAsAction="always|withText"/>
+
+ <item
+ android:id="@+id/menu_search"
+ android:icon="@drawable/action_search"
+ android:title="@string/menu.search"
+ android:showAsAction="ifRoom|withText"/>
+
+ <item
+ android:id="@+id/menu_settings"
+ android:icon="@drawable/action_settings"
+ android:title="@string/menu.settings"/>
+
+ <item
+ android:id="@+id/menu_exit"
+ android:icon="@drawable/action_exit"
+ android:title="@string/menu.exit"/>
+</menu>
diff --git a/res/menu/select_artist_context.xml b/res/menu/select_artist_context.xml
new file mode 100644
index 00000000..23d64c4e
--- /dev/null
+++ b/res/menu/select_artist_context.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item
+ android:id="@+id/artist_menu_play_now"
+ android:title="@string/common.play_now"
+ />
+
+ <item
+ android:id="@+id/artist_menu_play_shuffled"
+ android:title="@string/common.play_shuffled"
+ />
+
+ <item
+ android:id="@+id/artist_menu_play_last"
+ android:title="@string/common.play_last"
+ />
+
+ <item
+ android:id="@+id/artist_menu_download"
+ android:title="@string/common.download"
+ />
+
+ <item
+ android:id="@+id/artist_menu_pin"
+ android:title="@string/common.pin"
+ />
+
+ <item
+ android:id="@+id/artist_menu_delete"
+ android:title="@string/common.delete"/>
+</menu>
diff --git a/res/menu/select_artist_context_offline.xml b/res/menu/select_artist_context_offline.xml
new file mode 100644
index 00000000..c80db020
--- /dev/null
+++ b/res/menu/select_artist_context_offline.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item
+ android:id="@+id/artist_menu_play_now"
+ android:title="@string/common.play_now"
+ />
+
+ <item
+ android:id="@+id/artist_menu_play_shuffled"
+ android:title="@string/common.play_shuffled"
+ />
+
+ <item
+ android:id="@+id/artist_menu_play_last"
+ android:title="@string/common.play_last"
+ />
+
+ <item
+ android:id="@+id/artist_menu_delete"
+ android:title="@string/common.delete"/>
+</menu>
diff --git a/res/menu/select_genres.xml b/res/menu/select_genres.xml
new file mode 100644
index 00000000..e0f9a718
--- /dev/null
+++ b/res/menu/select_genres.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/menu_refresh"
+ android:icon="@drawable/action_refresh"
+ android:title="@string/menu.refresh"
+ android:showAsAction="always|withText"/>
+
+ <item
+ android:id="@+id/menu_settings"
+ android:icon="@drawable/action_settings"
+ android:title="@string/menu.settings"/>
+
+ <item
+ android:id="@+id/menu_exit"
+ android:icon="@drawable/action_exit"
+ android:title="@string/menu.exit"/>
+</menu> \ No newline at end of file
diff --git a/res/menu/select_playlist.xml b/res/menu/select_playlist.xml
new file mode 100644
index 00000000..a68e6da0
--- /dev/null
+++ b/res/menu/select_playlist.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/menu_refresh"
+ android:icon="@drawable/action_refresh"
+ android:title="@string/menu.refresh"
+ android:showAsAction="always|withText"/>
+
+ <item
+ android:id="@+id/menu_search"
+ android:icon="@drawable/action_search"
+ android:title="@string/menu.search"
+ android:showAsAction="always|withText"/>
+
+ <item
+ android:id="@+id/menu_settings"
+ android:icon="@drawable/action_settings"
+ android:title="@string/menu.settings"/>
+
+ <item
+ android:id="@+id/menu_exit"
+ android:icon="@drawable/action_exit"
+ android:title="@string/menu.exit"/>
+</menu>
diff --git a/res/menu/select_playlist_context.xml b/res/menu/select_playlist_context.xml
new file mode 100644
index 00000000..6d844a16
--- /dev/null
+++ b/res/menu/select_playlist_context.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item
+ android:id="@+id/playlist_info"
+ android:title="@string/common.info"
+ />
+
+ <item
+ android:id="@+id/playlist_menu_play_now"
+ android:title="@string/common.play_now"
+ />
+
+ <item
+ android:id="@+id/playlist_menu_play_shuffled"
+ android:title="@string/common.play_shuffled"
+ />
+
+ <item
+ android:id="@+id/playlist_menu_download"
+ android:title="@string/common.download"
+ />
+
+ <item
+ android:id="@+id/playlist_menu_pin"
+ android:title="@string/common.pin"
+ />
+
+ <item
+ android:id="@+id/playlist_update_info"
+ android:title="@string/playlist.update_info"
+ />
+
+ <item
+ android:id="@+id/playlist_menu_delete"
+ android:title="@string/common.delete"
+ />
+
+</menu> \ No newline at end of file
diff --git a/res/menu/select_playlist_context_offline.xml b/res/menu/select_playlist_context_offline.xml
new file mode 100644
index 00000000..644df2d3
--- /dev/null
+++ b/res/menu/select_playlist_context_offline.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/playlist_menu_play_now"
+ android:title="@string/common.play_now"
+ />
+
+ <item
+ android:id="@+id/playlist_menu_play_shuffled"
+ android:title="@string/common.play_shuffled"
+ />
+</menu> \ No newline at end of file
diff --git a/res/menu/select_podcast_episode.xml b/res/menu/select_podcast_episode.xml
new file mode 100644
index 00000000..ff5898e2
--- /dev/null
+++ b/res/menu/select_podcast_episode.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/menu_refresh"
+ android:icon="@drawable/action_refresh"
+ android:title="@string/menu.refresh"
+ android:showAsAction="always|withText"/>
+
+ <item
+ android:id="@+id/menu_delete"
+ android:icon="@drawable/action_remove_all"
+ android:title="@string/common.delete"/>
+</menu>
diff --git a/res/menu/select_podcast_episode_context.xml b/res/menu/select_podcast_episode_context.xml
new file mode 100644
index 00000000..25c83989
--- /dev/null
+++ b/res/menu/select_podcast_episode_context.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item
+ android:id="@+id/song_menu_info"
+ android:title="@string/common.info"
+ />
+
+ <item
+ android:id="@+id/song_menu_play_now"
+ android:title="@string/common.play_now"
+ />
+
+ <item
+ android:id="@+id/song_menu_play_last"
+ android:title="@string/common.play_last"
+ />
+
+ <item
+ android:id="@+id/song_menu_download"
+ android:title="@string/common.download"
+ />
+
+ <item
+ android:id="@+id/song_menu_delete"
+ android:title="@string/common.delete"/>
+
+ <item
+ android:id="@+id/song_menu_server_download"
+ android:title="@string/select_podcasts.server_download"/>
+
+ <item
+ android:id="@+id/song_menu_server_delete"
+ android:title="@string/select_podcasts.server_delete"/>
+</menu>
diff --git a/res/menu/select_podcast_episode_context_offline.xml b/res/menu/select_podcast_episode_context_offline.xml
new file mode 100644
index 00000000..38c4569b
--- /dev/null
+++ b/res/menu/select_podcast_episode_context_offline.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item
+ android:id="@+id/song_menu_info"
+ android:title="@string/common.info"
+ />
+
+ <item
+ android:id="@+id/song_menu_play_now"
+ android:title="@string/common.play_now"
+ />
+
+ <item
+ android:id="@+id/song_menu_play_last"
+ android:title="@string/common.play_last"
+ />
+
+ <item
+ android:id="@+id/song_menu_delete"
+ android:title="@string/common.delete"/>
+</menu>
diff --git a/res/menu/select_podcast_episode_offline.xml b/res/menu/select_podcast_episode_offline.xml
new file mode 100644
index 00000000..9bbc2d92
--- /dev/null
+++ b/res/menu/select_podcast_episode_offline.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/menu_refresh"
+ android:icon="@drawable/action_refresh"
+ android:title="@string/menu.refresh"
+ android:showAsAction="always|withText"/>
+
+ <item
+ android:id="@+id/menu_delete"
+ android:icon="@drawable/action_remove_all"
+ android:title="@string/common.delete"/>
+</menu>
diff --git a/res/menu/select_podcasts.xml b/res/menu/select_podcasts.xml
new file mode 100644
index 00000000..e77b43db
--- /dev/null
+++ b/res/menu/select_podcasts.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/menu_refresh"
+ android:icon="@drawable/action_refresh"
+ android:title="@string/menu.refresh"
+ android:showAsAction="always|withText"/>
+
+ <item
+ android:id="@+id/menu_add_podcast"
+ android:icon="@drawable/action_exit"
+ android:title="@string/menu.add_podcast"/>
+
+ <item
+ android:id="@+id/menu_check"
+ android:icon="@drawable/action_refresh"
+ android:title="@string/menu.check_podcasts"/>
+
+ <item
+ android:id="@+id/menu_settings"
+ android:icon="@drawable/action_settings"
+ android:title="@string/menu.settings"/>
+
+ <item
+ android:id="@+id/menu_exit"
+ android:icon="@drawable/action_exit"
+ android:title="@string/menu.exit"/>
+</menu> \ No newline at end of file
diff --git a/res/menu/select_podcasts_context.xml b/res/menu/select_podcasts_context.xml
new file mode 100644
index 00000000..af4edb55
--- /dev/null
+++ b/res/menu/select_podcasts_context.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/podcast_channel_info"
+ android:title="@string/common.info"/>
+ <item
+ android:id="@+id/podcast_channel_delete"
+ android:title="@string/common.delete"/>
+</menu> \ No newline at end of file
diff --git a/res/menu/select_song.xml b/res/menu/select_song.xml
new file mode 100644
index 00000000..3a55fee0
--- /dev/null
+++ b/res/menu/select_song.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/menu_play_now"
+ android:icon="@drawable/action_play_all"
+ android:title="@string/menu.play"
+ android:showAsAction="always|withText"/>
+
+ <item
+ android:id="@+id/menu_refresh"
+ android:icon="@drawable/action_refresh"
+ android:title="@string/menu.refresh"
+ android:showAsAction="always|withText"/>
+
+ <item
+ android:id="@+id/menu_shuffle"
+ android:icon="@drawable/action_shuffle"
+ android:title="@string/menu.shuffle"
+ android:showAsAction="ifRoom|withText"/>
+
+ <item
+ android:id="@+id/menu_select"
+ android:icon="@drawable/action_select"
+ android:title="@string/menu.select"
+ android:showAsAction="ifRoom|withText"/>
+
+ <item
+ android:id="@+id/menu_download"
+ android:icon="@drawable/action_save"
+ android:title="@string/common.download"
+ android:showAsAction="ifRoom|withText"/>
+
+ <item
+ android:id="@+id/menu_cache"
+ android:icon="@drawable/action_save"
+ android:title="@string/common.pin"
+ android:showAsAction="ifRoom|withText"/>
+
+ <item
+ android:id="@+id/menu_delete"
+ android:icon="@drawable/action_remove_all"
+ android:title="@string/common.delete"
+ android:showAsAction="ifRoom|withText"/>
+
+ <item
+ android:id="@+id/menu_add_playlist"
+ android:title="@string/menu.add_playlist"/>
+
+ <item
+ android:id="@+id/menu_remove_playlist"
+ android:title="@string/menu.remove_playlist"/>
+
+ <item
+ android:id="@+id/menu_play_last"
+ android:icon="@drawable/action_play_all"
+ android:title="@string/menu.play_last"
+ android:showAsAction="ifRoom|withText"/>
+</menu>
diff --git a/res/menu/select_song_context.xml b/res/menu/select_song_context.xml
new file mode 100644
index 00000000..4db229f2
--- /dev/null
+++ b/res/menu/select_song_context.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item
+ android:id="@+id/song_menu_info"
+ android:title="@string/common.info"
+ />
+
+ <item
+ android:id="@+id/song_menu_play_now"
+ android:title="@string/common.play_now"
+ />
+
+ <item
+ android:id="@+id/song_menu_play_next"
+ android:title="@string/common.play_next"
+ />
+
+ <item
+ android:id="@+id/song_menu_play_last"
+ android:title="@string/common.play_last"
+ />
+
+ <item
+ android:id="@+id/song_menu_download"
+ android:title="@string/common.download"
+ />
+
+ <item
+ android:id="@+id/song_menu_pin"
+ android:title="@string/common.pin"
+ />
+
+ <item
+ android:id="@+id/song_menu_delete"
+ android:title="@string/common.delete"/>
+
+ <item
+ android:id="@+id/song_menu_add_playlist"
+ android:title="@string/menu.add_playlist"/>
+
+ <item
+ android:id="@+id/song_menu_remove_playlist"
+ android:title="@string/menu.remove_playlist"/>
+
+ <item
+ android:id="@+id/song_menu_star"
+ android:title="@string/common.star"/>
+
+</menu>
diff --git a/res/menu/select_song_context_offline.xml b/res/menu/select_song_context_offline.xml
new file mode 100644
index 00000000..d19eaa70
--- /dev/null
+++ b/res/menu/select_song_context_offline.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item
+ android:id="@+id/song_menu_info"
+ android:title="@string/common.info"
+ />
+
+ <item
+ android:id="@+id/song_menu_play_now"
+ android:title="@string/common.play_now"
+ />
+
+ <item
+ android:id="@+id/song_menu_play_next"
+ android:title="@string/common.play_next"
+ />
+
+ <item
+ android:id="@+id/song_menu_play_last"
+ android:title="@string/common.play_last"
+ />
+
+ <item
+ android:id="@+id/song_menu_delete"
+ android:title="@string/common.delete"/>
+
+ <item
+ android:id="@+id/song_menu_star"
+ android:title="@string/common.star"/>
+</menu>
diff --git a/res/menu/select_song_offline.xml b/res/menu/select_song_offline.xml
new file mode 100644
index 00000000..6ed43b71
--- /dev/null
+++ b/res/menu/select_song_offline.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/menu_play_now"
+ android:icon="@drawable/action_play_all"
+ android:title="@string/menu.play"
+ android:showAsAction="always|withText"/>
+
+ <item
+ android:id="@+id/menu_refresh"
+ android:icon="@drawable/action_refresh"
+ android:title="@string/menu.refresh"
+ android:showAsAction="always|withText"/>
+
+ <item
+ android:id="@+id/menu_shuffle"
+ android:icon="@drawable/action_shuffle"
+ android:title="@string/menu.shuffle"
+ android:showAsAction="ifRoom|withText"/>
+
+ <item
+ android:id="@+id/menu_select"
+ android:icon="@drawable/action_select"
+ android:title="@string/menu.select"
+ android:showAsAction="ifRoom|withText"/>
+
+ <item
+ android:id="@+id/menu_delete"
+ android:icon="@drawable/action_remove_all"
+ android:title="@string/common.delete"
+ android:showAsAction="ifRoom|withText"/>
+
+ <item
+ android:id="@+id/menu_play_last"
+ android:icon="@drawable/action_play_all"
+ android:title="@string/menu.play_last"
+ android:showAsAction="ifRoom|withText"/>
+</menu>
diff --git a/res/menu/select_video_context.xml b/res/menu/select_video_context.xml
new file mode 100644
index 00000000..5926f8a5
--- /dev/null
+++ b/res/menu/select_video_context.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/song_menu_info"
+ android:title="@string/common.info"/>
+
+ <item
+ android:id="@+id/song_menu_stream_external"
+ android:title="@string/common.stream_external"/>
+
+ <item
+ android:id="@+id/song_menu_play_external"
+ android:title="@string/common.play_external"/>
+
+ <item
+ android:id="@+id/song_menu_download"
+ android:title="@string/common.download"
+ />
+
+ <item
+ android:id="@+id/song_menu_delete"
+ android:title="@string/common.delete"/>
+</menu>
diff --git a/res/menu/select_video_context_offline.xml b/res/menu/select_video_context_offline.xml
new file mode 100644
index 00000000..fc354119
--- /dev/null
+++ b/res/menu/select_video_context_offline.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/song_menu_info"
+ android:title="@string/common.info"/>
+
+ <item
+ android:id="@+id/song_menu_play_external"
+ android:title="@string/common.play_external"/>
+
+ <item
+ android:id="@+id/song_menu_delete"
+ android:title="@string/common.delete"/>
+</menu>
diff --git a/res/raw/changelog.xml b/res/raw/changelog.xml
new file mode 100644
index 00000000..54f41c0f
--- /dev/null
+++ b/res/raw/changelog.xml
@@ -0,0 +1,101 @@
+<?xml version="1.0" encoding="utf-8"?>
+<changelog>
+ <release version="4.1.2" versioncode="59" releasedate="7/24/2013">
+ <change>Added option to clear cache from settings</change>
+ <change>Added cloud settings backup so when you reinstall on the same device your settings are still there</change>
+ <change>Fixed Android 4.3 crash</change>
+ <change>Performance enhancements</change>
+ </release>
+ <release version="4.1.1" versioncode="58" releasedate="7/18/2013">
+ <change>Fix some podcasts causing errors</change>
+ </release>
+ <release version="4.1.0" versioncode="57" releasedate="7/17/2013">
+ <change>Added Podcast Tab (4.5+)</change>
+ <change>Add/Delete Podcast Channels. Manage server status of Podcast Episodes (4.8+)</change>
+ <change>Double press pause on headset to skip to next song</change>
+ <change>Added HLS as a option under external video players (4.8+). Skipping doesn't seem to work for me.</change>
+ <change>Fix pressing play from widget from sometimes starting song over</change>
+ <change>Various minor UI tweaks to make things look nicer</change>
+ <change>Gapless Playback setting: if off now acts more like base Subsonic app to hopefully fix some issues</change>
+ </release>
+ <release version="4.0.7" versioncode="56" releasedate="7/2/2013">
+ <change>Added offline starring to library view instead of just now playing</change>
+ <change>Remove * to show downloading, go off of whether arrow is blue or green for cached/perma cached</change>
+ <change>Go back to always showing bottom bar so downloading list is accessible </change>
+ <change>Fix offline mode matching first letters against ignore list (ie: the), instead of first word</change>
+ <change>Add prompt for removing a server</change>
+ <change>Fix some cases where list would incorrectly show up blank</change>
+ </release>
+ <release version="4.0.6" versioncode="55" releasedate="6/25/2013">
+ <change>Scrobble and star songs and sync changes back when going online (has trouble when tags don't match folders)</change>
+ <change>Fix cases where operations didn't work in online mode when originally added in offline mode and vice versa</change>
+ <change>Added blank option to genre picker in the shuffle dialog</change>
+ <change>Added option to show track # in front of song (off by default)</change>
+ <change>Separate cached playlists from different servers in separate folders so they don't interfere with each other</change>
+ <change>Fix for some music files which throw errors at the end not proceeding to the next song</change>
+ <change>Fix flash preference not being obeyed for the Play External option</change>
+ <change>As songs are downloaded in background list, automatically remove them</change>
+ <change>Fix low quality album artwork in large widgets</change>
+ <change>Fix a rare case that can cause a song to be played twice</change>
+ <change>Fix for some who listen to untranscoded flac songs</change>
+ <change>Remove bottom bar if nothing is in the queue</change>
+ <change>Use .nomedia file instead of folder for more compatibility</change>
+ <change>Clean some sensitive info from the logs</change>
+ </release>
+ <release version="4.0.5" versioncode="54" releasedate="6/7/2013">
+ <change>Fix album art on old Subsonic/MusicCabinet servers</change>
+ </release>
+ <release version="4.0.4" versioncode="53" releasedate="6/6/2013">
+ <change>Added Genre parsing (thanks archrival)</change>
+ <change>Changed Genre to combo selection on 4.8+ servers</change>
+ <change>Added video choice similar to Subsonic (Raw is the same as MX but you can choose which player to use)</change>
+ <change>Added 4x2, 4x3, 4x4 widgets (thanks archrival)</change>
+ <change>Add option to create new playlist when adding song to playlists</change>
+ <change>Added option to overwrite existing playlist on 4.7+ servers</change>
+ <change>Fix when removing the current server</change>
+ <change>Fix edge case in new sort</change>
+ </release>
+ <release version="4.0.3" versioncode="52" releasedate="5/31/2013">
+ <change>Sort by disc number if specified in tags</change>
+ <change>Show starred artists in starred list</change>
+ <change>Change folder.jpg to albumart.jpg which galleries shouldn't display</change>
+ <change>Fix Show Album</change>
+ <change>Added support for server Ignored Articles (future server version) + defaults to server's defaults</change>
+ <change>On network error return to front of the app instead of exiting all the way</change>
+ <change>Fix occasional crash when going back into app after running for a while</change>
+ <change>Various minor bugfixes</change>
+ </release>
+ <release version="4.0.2" versioncode="51" releasedate="5/24/2013">
+ <change>Fix if you set chat refresh rate to 0, will just not refresh</change>
+ <change>Revert dark theme modification</change>
+ <change>New Theme called black which is the pure black background</change>
+ <change>Option to disable chat menu, need to exit app and reenter for now</change>
+ </release>
+
+ <release version="4.0.1" versioncode="50" releasedate="5/23/2013">
+ <change>New: Chat Tab (Set chat auto refresh rate from settings)</change>
+ <change>New: Dynamic servers, add as many, or remove all but the ones you are using</change>
+ <change>New: Added separate setting for songs to preload for Wifi/Mobile</change>
+ <change>Improvement: The infinite playlist while shuffling is now persistent between startups</change>
+ <change>Theme: White is now more white, got rid of blue text for white theme only</change>
+ <change>Theme: Black is now a flat black due to popular request</change>
+ <change>Theme: Apply the current theme to settings screen</change>
+ <change>Fix: Don't stretch album art on bottom of main tabs</change>
+ <change>Fix: Possible fix for some who were having crash on starting EQ</change>
+ </release>
+
+ <release version="4.0.0" versioncode="48" releasedate="5/16/2013">
+ <change>Converted everything to fragments!</change>
+ <change>Swipe to switch tabs</change>
+ <change>Breadcrumb trail when going down several levels</change>
+ <change>Require double tapping back to exit app</change>
+ <change>Change log dialog for new versions</change>
+ <change>Endless loading on album lists (ie: Random, Recently Added, etc...) instead of pressing more</change>
+ <change>Look at what is now playing from main tabs</change>
+ <change>Added Playing: Track/Total to Now Playing action bar</change>
+ <change>When clicking on a album in search, the parent is also added to the back stack</change>
+ <change>Added total time to playlist/album headers</change>
+ <change>Fixed a lot of the menu items not working when using search</change>
+ <change>Update to Light/Dark themes</change>
+ </release>
+</changelog> \ No newline at end of file
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
new file mode 100644
index 00000000..a2e1f660
--- /dev/null
+++ b/res/values-fr/strings.xml
@@ -0,0 +1,208 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="common.appname">Subsonic</string>
+ <string name="common.ok">OK</string>
+ <string name="common.save">Enregistrer</string>
+ <string name="common.cancel">Annuler</string>
+
+ <string name="main.welcome_title">Bienvenue!</string>
+ <string name="main.welcome_text">Bienvenue dans Subsonic! L\'application est configurée pour utiliser le serveur démo de Subsonic.
+ Après avoir configuré votre serveur personnel (disponible à partir de <b>subsonic.org</b>), veuillez accéder aux <b>Paramètres</b> et modifier la configuration pour vous y connecter.</string>
+ <string name="main.select_server">Sélectionner un serveur</string>
+ <string name="main.shuffle">Lecture aléatoire</string>
+ <string name="main.offline">Hors-ligne</string>
+ <string name="main.settings">Paramètres</string>
+ <string name="main.albums_title">Albums</string>
+ <string name="main.albums_newest">Plus récents</string>
+ <string name="main.albums_random">Aléatoire</string>
+ <string name="main.albums_highest">Mieux cotés</string>
+ <string name="main.albums_recent">Récemment joués</string>
+ <string name="main.albums_frequent">Fréquemment joués</string>
+
+ <!--<string name="menu.exit">TODO: Exit</string>-->
+ <!--<string name="menu.settings">TODO: Settings</string>-->
+ <!--<string name="menu.help">TODO: Help</string>-->
+
+ <string name="playlist.label">Playlists</string>
+
+ <string name="help.label">Aide</string>
+ <string name="help.title">Bienvenue dans Subsonic!</string>
+ <string name="help.back">Retour</string>
+ <string name="help.close">Fermer</string>
+ <string name="help.url">file:///android_asset/html/fr/index.html</string>
+ <string name="help.loading">Chargement...</string>
+
+ <string name="play_video.loading">Chargement de la vidéo...</string>
+ <string name="play_video.noplugin">Veuillez installer Adobe Flash Player à partir du marché Android.</string>
+
+ <string name="search.label">Recherche</string>
+ <string name="search.title">Recherche</string>
+ <string name="search.search">Cliquer pour rechercher</string>
+ <string name="search.no_match">Aucun résultat, veuillez essayer à nouveau</string>
+ <string name="search.artists">Artistes</string>
+ <string name="search.albums">Albums</string>
+ <string name="search.songs">Pièces</string>
+ <string name="search.more">Afficher plus</string>
+
+ <string name="progress.wait">Veuillez patienter...</string>
+
+ <string name="music_library.label">Bibliothèque musicale</string>
+ <string name="music_library.label_offline">Musique hors-ligne</string>
+
+ <string name="select_album.empty">Aucune musique trouvée</string>
+ <string name="select_album.select">Tout sélectionner</string>
+ <!--<string name="select_album.n_selected">TODO: %d tracks selected.</string>-->
+ <!--<string name="select_album.n_unselected">TODO: %d tracks unselected.</string>-->
+ <string name="select_album.more">Plus</string>
+ <string name="select_album.offline">Hors-ligne</string>
+ <string name="select_album.searching">Recherche en cours...</string>
+ <string name="select_album.no_sdcard">Erreur: Aucune carte SD disponible.</string>
+ <string name="select_album.no_network">Avis: Aucun réseau disponible.</string>
+ <string name="select_album.not_licensed">Serveur sans licence. %d jours d\'essai restant.</string>
+ <string name="select_album.donate_dialog_message">Obtenez des téléchargements illimités en donnant à Subsonic.</string>
+ <string name="select_album.donate_dialog_now">Maintenant</string>
+ <string name="select_album.donate_dialog_later">Plus tard</string>
+ <string name="select_album.donate_dialog_0_trial_days_left">La période d\'essai est terminée</string>
+
+ <string name="select_playlist.empty">Aucune playlist sur le serveur</string>
+
+ <string name="download.empty">Playlist vide</string>
+ <string name="download.playerstate_downloading">Téléchargement - %s</string>
+ <string name="download.playerstate_buffering">Mise en tampon</string>
+ <string name="download.playerstate_playing_shuffle">En jeu aléatoire</string>
+ <string name="download.menu_show_album">Afficher l\'album</string>
+ <string name="download.menu_lyrics">Paroles</string>
+ <string name="download.menu_remove">Retirer la pièce</string>
+ <string name="download.menu_remove_all">Retirer tout</string>
+ <string name="download.menu_shuffle">Mélanger</string>
+ <string name="download.menu_save">Enregistrer la playlist</string>
+ <string name="download.menu_shuffle_notification">Playlist mélangée</string>
+ <string name="download.playlist_title">Enregistrer la playlist</string>
+ <string name="download.playlist_name">Saisissez le nom de la playlist:</string>
+ <string name="download.playlist_saving">Enregistrement de la playlist \"%s\"...</string>
+ <string name="download.playlist_done">Playlist enregistrée avec succès.</string>
+ <string name="download.playlist_error">Échec de l\'enregistrement de la playlist, veuillez réessayer plus tard.</string>
+
+ <string name="song_details.all">%2$s, %1$s</string>
+ <string name="song_details.kbps">%d Kb/s</string>
+
+ <string name="lyrics.nomatch">Aucune parole trouvée</string>
+
+ <string name="error.label">Erreur</string>
+
+ <string name="settings.title">Paramètres de Subsonic</string>
+ <string name="settings.test_connection_title">Tester la connexion</string>
+ <string name="settings.servers_title">Serveurs</string>
+ <string name="settings.server_unused1">Inutilisé 1</string>
+ <string name="settings.server_unused2">Inutilisé 2</string>
+ <string name="settings.server_name">Nom</string>
+ <string name="settings.server_address">Adresse du serveur</string>
+ <string name="settings.server_username">Nom d\'usager</string>
+ <string name="settings.server_password">Mot de passe</string>
+ <string name="settings.cache_title">Cache musicale</string>
+ <string name="settings.preload">Pièces à pré-charger</string>
+ <string name="settings.cache_size">Taille de la cache</string>
+ <string name="settings.testing_connection">Connexion en cours de test...</string>
+ <string name="settings.testing_ok">Connexion correcte</string>
+ <string name="settings.testing_unlicensed">Connection correcte. Serveur sans licence.</string>
+ <string name="settings.connection_failure">Connection échouée.</string>
+ <string name="settings.invalid_url">Veuillez spécifier un URL valide.</string>
+ <string name="settings.invalid_username">Veuillez spécifier un nom d\'usager valide (sans espace à la fin).</string>
+ <string name="settings.appearance_title">Apparence</string>
+ <string name="settings.theme_title">Thème</string>
+ <string name="settings.theme_wheat">Blé</string>
+ <string name="settings.theme_light">Clair</string>
+ <string name="settings.theme_dark">Sombre</string>
+ <string name="settings.network_title">Réseau</string>
+ <string name="settings.max_bitrate_wifi">Débit maximal - Wi-Fi</string>
+ <string name="settings.max_bitrate_mobile">Débit maximal - Mobile</string>
+ <string name="settings.max_bitrate_32">32 Kb/s</string>
+ <string name="settings.max_bitrate_64">64 Kb/s</string>
+ <string name="settings.max_bitrate_80">80 Kb/s</string>
+ <string name="settings.max_bitrate_96">96 Kb/s</string>
+ <string name="settings.max_bitrate_112">112 Kb/s</string>
+ <string name="settings.max_bitrate_128">128 Kb/s</string>
+ <string name="settings.max_bitrate_160">160 Kb/s</string>
+ <string name="settings.max_bitrate_192">192 Kb/s</string>
+ <string name="settings.max_bitrate_256">256 Kb/s</string>
+ <string name="settings.max_bitrate_320">320 Kb/s</string>
+ <string name="settings.max_bitrate_unlimited">Illimité</string>
+ <string name="settings.preload_1">1 pièce</string>
+ <string name="settings.preload_2">2 pièces</string>
+ <string name="settings.preload_3">3 pièces</string>
+ <string name="settings.preload_5">5 pièces</string>
+ <string name="settings.preload_10">10 pièces</string>
+ <string name="settings.preload_unlimited">Illimité</string>
+ <string name="settings.cache_size_100">100 Mo</string>
+ <string name="settings.cache_size_200">200 Mo</string>
+ <string name="settings.cache_size_500">500 Mo</string>
+ <string name="settings.cache_size_1000">1 Go</string>
+ <string name="settings.cache_size_2000">2 Go</string>
+ <string name="settings.cache_size_5000">5 Go</string>
+ <string name="settings.cache_size_10000">10 Go</string>
+ <string name="settings.cache_size_20000">20 Go</string>
+ <string name="settings.cache_size_unlimited">Illimité</string>
+ <string name="settings.clear_search_history">Effacer l\'historique des recherches</string>
+ <string name="settings.search_history_cleared">Historique des recherches effacé</string>
+ <string name="settings.other_title">Autres paramètres</string>
+ <!--<string name="settings.scrobble_title">TODO: Scrobble to Last.fm</string>-->
+ <!--<string name="settings.scrobble_summary">TODO: Remember to set up your Last.fm user and password on the Subsonic server</string>-->
+ <string name="settings.hide_media_title">Masquer aux autres</string>
+ <string name="settings.hide_media_summary">Masquer les fichiers musicaux et les couvertures d\'album aux autres applis (Gallerie, Musique, etc.)</string>
+ <string name="settings.hide_media_toast">Prendra effet la prochaine fois qu\'Android recensera les médias disponibles sur l\'appareil.</string>
+ <string name="settings.media_button_title">Boutons média</string>
+ <string name="settings.media_button_summary">Répondre au boutons média de l\'appareil, du casque et du Bluetooth</string>
+ <!--<string name="settings.screen_lit_title">TODO: Keep screen on</string>-->
+ <!--<string name="settings.screen_lit_summary">TODO: Keeping the screen on when downloading may improve download speed</string>-->
+
+ <string name="music_service.retry">Une erreur de réseau s\'est produite. Essai %1$d de %2$d.</string>
+
+ <string name="background_task.wait">Veuillez patienter...</string>
+ <string name="background_task.loading">Chargement.</string>
+ <string name="background_task.no_network">Cette application requiert un accès au réseau. Veuillez activer le Wi-Fi ou le réseau mobile.</string>
+ <string name="background_task.network_error">Une erreur réseau est survenue. Veuillez vérifier l\'adresse du serveur ou réessayer plus tard.</string>
+ <string name="background_task.not_found">Ressource non trouvée. Veuillez vérifier l\'adresse du serveur.</string>
+ <string name="background_task.parse_error">Réplique incomprise. Veuillez vérifier l\'adresse du serveur.</string>
+
+ <string name="service.connecting">Contact du serveur, veuillez patienter.</string>
+
+ <string name="parser.reading">Lecture du serveur.</string>
+ <string name="parser.reading_done">Lecture du serveur. Terminé!</string>
+ <string name="parser.upgrade_client">Versions incompatible. Veuillez mette à jour l\'application Android Subsonic.</string>
+ <string name="parser.upgrade_server">Versions incompatible. Veuillez mette à jour le serveur Subsonic.</string>
+ <string name="parser.not_authenticated">Mauvais nom d\'usager ou mot de passe.</string>
+ <string name="parser.artist_count">%d artistes récupérés.</string>
+
+ <string name="select_artist.refresh">Rafraîchir</string>
+ <string name="select_artist.folder">Sélectionner le dossier</string>
+ <string name="select_artist.all_folders">Tous les dossiers</string>
+
+ <string name="widget.initial_text">Touchez pour sélectionner une pièce</string>
+ <string name="widget.sdcard_busy">Carte SD non disponible</string>
+ <string name="widget.sdcard_missing">Aucune carte SD</string>
+
+ <string name="util.bytes_format.gigabyte">0.00 Go</string>
+ <string name="util.bytes_format.megabyte">0.00 Mo</string>
+ <string name="util.bytes_format.kilobyte">0 Ko</string>
+ <string name="util.bytes_format.byte">0 o</string>
+
+ <plurals name="select_album_n_songs">
+ <item quantity="zero">Aucune pièce</item>
+ <item quantity="one">Une pièce</item>
+ <item quantity="other">%d pièces</item>
+ </plurals>
+ <plurals name="select_album_n_songs_downloading">
+ <item quantity="one">Une pièce prévue pour téléchargement.</item>
+ <item quantity="other">%d pièces prévues pour téléchargement.</item>
+ </plurals>
+ <plurals name="select_album_n_songs_added">
+ <item quantity="one">Une pièce ajoutée à la file de lecture.</item>
+ <item quantity="other">%d pièces ajoutées à la file de lecture.</item>
+ </plurals>
+ <plurals name="select_album_donate_dialog_n_trial_days_left">
+ <item quantity="one">Un jour restant à la période d\'essai</item>
+ <item quantity="other">%d jours restant à la période d\'essai</item>
+ </plurals>
+
+</resources>
diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml
new file mode 100644
index 00000000..1748ce30
--- /dev/null
+++ b/res/values-ru/strings.xml
@@ -0,0 +1,343 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="common.appname">DSub</string>
+ <string name="common.ok">OK</string>
+ <string name="common.save">Сохранить</string>
+ <string name="common.cancel">Отмена</string>
+ <string name="common.play_now">Воспроизвести сейчас</string>
+ <string name="common.play_shuffled">Случайное воспроизведение</string>
+ <string name="common.play_next">Воспроизвести следующим</string>
+ <string name="common.play_last">Воспроизвести последним</string>
+ <string name="common.download">Скачать</string>
+ <string name="common.pin">Кешировать</string>
+ <string name="common.delete">Удалить</string>
+ <string name="common.star">Добавить в закладки</string>
+ <string name="common.unstar">Удалить из закладок</string>
+ <string name="common.info">Информация</string>
+ <string name="common.name">Название</string>
+ <string name="common.comment">Комментарий</string>
+ <string name="common.public">Общедоступный</string>
+ <string name="common.webview">Воспроизвести в браузере (флэш)</string>
+ <string name="common.play_external">Воспроизвести во внешнем плеере</string>
+ <string name="common.stream_external">Воспроизвести поток во внешнем плеере</string>
+ <string name="common.confirm">Подтверждение</string>
+
+ <string name="button_bar.home">Домой</string>
+ <string name="button_bar.browse">Медиатека</string>
+ <string name="button_bar.search">Поиск</string>
+ <string name="button_bar.playlists">Списки</string>
+ <string name="button_bar.now_playing">Плеер</string>
+
+ <string name="main.welcome_title">Здравствуйте!</string>
+ <string name="main.welcome_text">Добро пожаловать в DSub! Это приложение настроено на работу с демо сервером Subsonic. После настройки Вашего персонального сервера (доступен на <b>subsonic.org</b>), пожалуйста, перейдите в <b>Настройки</b> и измените параметры для подключения.</string>
+
+ <string name="main.about_title">О программе DSub</string>
+ <string name="main.about_text">Автор: Scott Jackson
+ \nEmail: daneren2005@gmail.com
+ \nВерсия: %1$s
+ \nИспользовано места: %2$s из %3$s
+ \nДоступно места: %4$s из %5$s</string>
+ <string name="main.select_server">Выбрать сервер</string>
+ <string name="main.shuffle">Случайное воспроизведение</string>
+ <string name="main.offline">Отключиться</string>
+ <string name="main.online">Подключиться</string>
+ <string name="main.settings">Настройки</string>
+ <string name="main.albums_title">Альбомы</string>
+ <string name="main.albums_newest">Недавно добавленные</string>
+ <string name="main.albums_recent">Недавно прослушанные</string>
+ <string name="main.albums_frequent">Часто прослушиваемые</string>
+ <string name="main.albums_highest">Максимальный рейтинг</string>
+ <string name="main.albums_starred">Закладки</string>
+ <string name="main.albums_random">Случайные</string>
+
+ <string name="menu.search">Поиск</string>
+ <string name="menu.shuffle">Перемешать</string>
+ <string name="menu.refresh">Обновить</string>
+ <string name="menu.select">Выбрать все</string>
+ <string name="menu.play">Воспроизвести</string>
+ <string name="menu.play_last">Воспроизвести последним</string>
+ <string name="menu.exit">Выход</string>
+ <string name="menu.settings">Настройки</string>
+ <string name="menu.help">Помощь</string>
+ <string name="menu.about">О программе</string>
+ <string name="menu.add_playlist">Добавить в список</string>
+ <string name="menu.remove_playlist">Удалить из списка</string>
+ <string name="menu.deleted_playlist">Список воспроизведения %s удален</string>
+ <string name="menu.deleted_playlist_error">Не удалось удалить список %s</string>
+ <string name="menu.log">Отправить журнал событий</string>
+ <string name="menu.set_timer">Установить таймер</string>
+
+ <string name="playlist.label">Списки</string>
+ <string name="playlist.update_info">Изменить информацию</string>
+ <string name="playlist.updated_info">Информация для списка воспроизведения %s обновлена</string>
+ <string name="playlist.updated_info_error">Не удалось обновить информацию для списка воспроизведения %s</string>
+
+ <string name="help.label">Помощь</string>
+ <string name="help.title">Добро пожаловать в DSub!</string>
+ <string name="help.back">Назад</string>
+ <string name="help.close">Закрыть</string>
+ <string name="help.url">file:///android_asset/html/ru/index.html</string>
+ <string name="help.loading">Загрузка...</string>
+
+ <string name="play_video.loading">Загрузка видео...</string>
+ <string name="play_video.noplugin">Пожалуйста, установить Adobe Flash Player из Google Play.</string>
+
+ <string name="search.label">Поиск</string>
+ <string name="search.title">Поиск</string>
+ <string name="search.search">Нажмите для поиска</string>
+ <string name="search.no_match">Ничего не найдено, пожалуйста, попробуйте снова</string>
+ <string name="search.artists">Исполнители</string>
+ <string name="search.albums">Альбомы</string>
+ <string name="search.songs">Композиции</string>
+ <string name="search.more">Показать еще</string>
+
+ <string name="progress.wait">Пожалуйста, подождите...</string>
+
+ <string name="music_library.label">Медиатека</string>
+ <string name="music_library.label_offline">Оффлайн медиа</string>
+
+ <string name="select_album.empty">Медиафайлы не найдены</string>
+ <string name="select_album.select">Выбрать все</string>
+ <string name="select_album.n_selected">%d композиций выбрано.</string>
+ <string name="select_album.n_unselected">Выбор снят с %d композиций.</string>
+ <string name="select_album.more">Еще</string>
+ <string name="select_album.offline">Оффлайн</string>
+ <string name="select_album.searching">Выполняется поиск...</string>
+ <string name="select_album.no_sdcard">Ошибка: SD карта недоступна</string>
+ <string name="select_album.no_network">Внимание: сеть недоступна.</string>
+ <string name="select_album.not_licensed">Сервер не лицензирован. %d дней до окончания пробного периода.</string>
+ <string name="select_album.donate_dialog_message">Осуществите пожертвование для Subsonic и получите возможность неограниченного скачивания.</string>
+ <string name="select_album.donate_dialog_now">Сейчас</string>
+ <string name="select_album.donate_dialog_later">Позже</string>
+ <string name="select_album.donate_dialog_0_trial_days_left">Пробный период закончился</string>
+
+ <string name="select_playlist.empty">Нет сохраненных списков воспроизведения на сервере</string>
+
+ <string name="download.empty">Список воспроизведения пуст</string>
+ <string name="download.shuffle_loading">Загружается случайный список...</string>
+ <string name="download.playerstate_downloading">Загрузка - %s</string>
+ <string name="download.playerstate_buffering">Буферизация</string>
+ <string name="download.playerstate_playing_shuffle">Воспроизводится случайно</string>
+ <string name="download.menu_show_album">Показать альбом</string>
+ <string name="download.menu_lyrics">Текст</string>
+ <string name="download.menu_remove">Убрать из очереди</string>
+ <string name="download.menu_delete">Удалить кэш</string>
+ <string name="download.menu_remove_all">Очистить</string>
+ <string name="download.menu_screen_on">Включить подсветку</string>
+ <string name="download.menu_screen_off">Отключать подсветку</string>
+ <string name="download.menu_shuffle">Перемешать</string>
+ <string name="download.menu_toggle">Переключатель</string>
+ <string name="download.menu_save">Сохранить список</string>
+ <string name="download.menu_shuffle_notification">Список воспроизведения был перемешан</string>
+ <string name="download.playlist_title">Сохранение списка воспроизведения</string>
+ <string name="download.playlist_name">Введите название:</string>
+ <string name="download.playlist_saving">Сохранение списка воспроизведения \"%s\"...</string>
+ <string name="download.playlist_done">Список воспроизведения сохранен</string>
+ <string name="download.playlist_error">Не удалось сохранить список воспроизведения, пожалуйста, попробуйте позже.</string>
+ <string name="download.repeat_off">Повторение отключено</string>
+ <string name="download.repeat_all">Повторять все</string>
+ <string name="download.repeat_single">Повторять композицию</string>
+ <string name="download.visualizer_on">Визуализация включена</string>
+ <string name="download.visualizer_off">Визуализация отключена</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.show_downloading">Показать закачки</string>
+ <string name="download.show_now_playing">Показать воспроизведение</string>
+ <string name="download.timer_length">Длительность</string>
+ <string name="download.start_timer">Запустить таймер</string>
+ <string name="download.stop_timer">Остановить таймер</string>
+ <string name="download.need_download">Необходимо сначала скачать видео</string>
+ <string name="download.no_streaming_player">Нет плеера для воспроизведения потока</string>
+
+ <string name="starring_content_starred">\"%s\" добавлено в закладки</string>
+ <string name="starring_content_unstarred">\"%s\" удалено из закладок</string>
+ <string name="starring_content_error">Не удалось обновить \"%s\", пожалуйста, попробуйте позже.</string>
+
+ <string name="playlist_error">Не удалось прочитать списки воспроизведения</string>
+ <string name="updated_playlist">Добавлено %1$s композиций в \"%2$s\"</string>
+ <string name="updated_playlist_error">Не удалось обновить \"%s\", пожалуйста, попробуйте позже.</string>
+ <string name="removed_playlist">Удалено %1$s из \"%2$s\" композиций</string>
+ <string name="delete_playlist">Удалить %1$s?</string>
+
+ <string name="song_details.all">%1$s %2$s</string>
+ <string name="song_details.kbps">%d kbps</string>
+
+ <string name="lyrics.nomatch">Текст не найден</string>
+
+ <string name="error.label">Ошибка</string>
+
+ <string name="settings.title">Настройки DSub</string>
+ <string name="settings.test_connection_title">Проверить соединение</string>
+ <string name="settings.servers_title">Серверы</string>
+ <string name="settings.server_unused1">Неиспользованный 1</string>
+ <string name="settings.server_unused2">Неиспользованный 2</string>
+ <string name="settings.server_name">Название</string>
+ <string name="settings.server_address">Адрес сервера</string>
+ <string name="settings.server_username">Имя пользователя</string>
+ <string name="settings.server_password">Пароль</string>
+ <string name="settings.cache_title">Кэш музыки</string>
+ <string name="settings.preload">Композиций для предзагрузки</string>
+ <string name="settings.cache_size">Размер кэша (Мб)</string>
+ <string name="settings.cache_location">Путь кэша</string>
+ <string name="settings.cache_location_error">Некорректный путь. Используем путь по умолчанию.</string>
+ <string name="settings.testing_connection">Проверка соединения...</string>
+ <string name="settings.testing_ok">Подключение прошло успешно!</string>
+ <string name="settings.testing_unlicensed">Подключение прошло успешно. Сервер нелицензирован.</string>
+ <string name="settings.connection_failure">Не удалось подключиться.</string>
+ <string name="settings.invalid_url">Пожалуйста, укажите правильный адрес</string>
+ <string name="settings.invalid_username">Пожалуйста, укажите правильное имя пользователя (не должно быть пробелов в конце)</string>
+ <string name="settings.appearance_title">Внешний вид</string>
+ <string name="settings.theme_title">Тема</string>
+ <string name="settings.theme_light">Светлая</string>
+ <string name="settings.theme_dark">Темная</string>
+ <string name="settings.theme_holo">Holo</string>
+ <string name="settings.theme_light_fullscreen">Светлая во весь экран</string>
+ <string name="settings.theme_dark_fullscreen">Темная во весь экран</string>
+ <string name="settings.theme_holo_fullscreen">Holo во весь экран</string>
+ <string name="settings.network_title">Сеть</string>
+ <string name="settings.max_bitrate_wifi">Макс. битрейт аудио по Wi-Fi</string>
+ <string name="settings.max_bitrate_mobile">Макс. битрейт видео по сети</string>
+ <string name="settings.max_bitrate_32">32 Kbps</string>
+ <string name="settings.max_bitrate_64">64 Kbps</string>
+ <string name="settings.max_bitrate_80">80 Kbps</string>
+ <string name="settings.max_bitrate_96">96 Kbps</string>
+ <string name="settings.max_bitrate_112">112 Kbps</string>
+ <string name="settings.max_bitrate_128">128 Kbps</string>
+ <string name="settings.max_bitrate_160">160 Kbps</string>
+ <string name="settings.max_bitrate_192">192 Kbps</string>
+ <string name="settings.max_bitrate_256">256 Kbps</string>
+ <string name="settings.max_bitrate_320">320 Kbps</string>
+ <string name="settings.max_video_bitrate_wifi">Макс. битрейт видео по Wi-Fi</string>
+ <string name="settings.max_video_bitrate_mobile">Макс. битрейт видео по сети</string>
+ <string name="settings.max_video_bitrate_200">200 Kbps</string>
+ <string name="settings.max_video_bitrate_300">300 Kbps</string>
+ <string name="settings.max_video_bitrate_400">400 Kbps</string>
+ <string name="settings.max_video_bitrate_500">500 Kbps</string>
+ <string name="settings.max_video_bitrate_700">700 Kbps</string>
+ <string name="settings.max_video_bitrate_1000">1000 Kbps</string>
+ <string name="settings.max_video_bitrate_1500">1500 Kbps</string>
+ <string name="settings.max_video_bitrate_2000">2000 Kbps</string>
+ <string name="settings.max_video_bitrate_3000">3000 Kbps</string>
+ <string name="settings.max_video_bitrate_5000">5000 Kbps</string>
+ <string name="settings.max_bitrate_unlimited">Неограничен</string>
+ <string name="settings.wifi_required_title">Поток по Wi-Fi</string>
+ <string name="settings.wifi_required_summary">Потокое воспроизведение будет работать только при подключении через Wi-Fi</string>
+ <string name="settings.network_timeout_title">Таймаут сети</string>
+ <string name="settings.network_timeout_10000">10 секунд</string>
+ <string name="settings.network_timeout_15000">15 секунд</string>
+ <string name="settings.network_timeout_30000">30 секунд</string>
+ <string name="settings.network_timeout_45000">45 секунд</string>
+ <string name="settings.network_timeout_60000">60 секунд</string>
+ <string name="settings.preload_1">1 композиция</string>
+ <string name="settings.preload_2">2 композиции</string>
+ <string name="settings.preload_3">3 композиции</string>
+ <string name="settings.preload_5">5 композиций</string>
+ <string name="settings.preload_10">10 композиций</string>
+ <string name="settings.preload_unlimited">Неограничено</string>
+ <string name="settings.clear_search_history">Очистить историю поиска</string>
+ <string name="settings.search_history_cleared">История поиска очищена</string>
+ <string name="settings.other_title">Другие настройки</string>
+ <string name="settings.scrobble_title">Скробблинг на Last.fm</string>
+ <string name="settings.scrobble_summary">Не забудьте установить логин и пароль от Last.fm на сервере DSub</string>
+ <string name="settings.hide_media_title">Прятать от других</string>
+ <string name="settings.hide_media_summary">Прятать музыкальные файлы от других приложений</string>
+ <string name="settings.hide_media_toast">Изменения вступят в силу при следующем поиске музыки на Вашем устройстве.</string>
+ <string name="settings.media_button_title">Кнопки управления</string>
+ <string name="settings.media_button_summary">Разрешить управление кнопками мультимедиа на устройстве и гарнитуре</string>
+ <string name="settings.screen_lit_title">Держать экран включенным</string>
+ <string name="settings.screen_lit_summary">Оставить экран включенным для повышения скорости при скачивании.</string>
+ <string name="settings.playlist_title">Списки воспроизведения</string>
+ <string name="settings.playlist_random_size_title">Размер случайного списка</string>
+ <string name="settings.buffer_length">Размер буфера (0 = кешировать полностью)</string>
+ <string name="settings.sleep_timer_title">Таймер сна</string>
+ <string name="settings.sleep_timer_duration_title">Продолжительность таймера сна</string>
+ <string name="settings.sleep_timer_off">Выключен</string>
+ <string name="settings.sleep_timer_on">Включен</string>
+ <string name="settings.sleep_timer_always_on">Всегда включен</string>
+ <string name="settings.temp_loss_title">Временная потеря связи</string>
+ <string name="settings.temp_loss_pause">Всегда останавливать</string>
+ <string name="settings.temp_loss_pause_lower">Останавливать, понижать громкость, если требуется</string>
+ <string name="settings.temp_loss_lower">Всегда понижать громкость</string>
+ <string name="settings.temp_loss_nothing">Ничего не делать</string>
+
+ <string name="shuffle.startYear">Год начала:</string>
+ <string name="shuffle.endYear">Год окончания:</string>
+ <string name="shuffle.genre">Жанр:</string>
+
+ <string name="music_service.retry">Ошибка подключения. Попытка %1$d из %2$d.</string>
+
+ <string name="background_task.wait">Пожалуйста, подождите...</string>
+ <string name="background_task.loading">Загрузка</string>
+ <string name="background_task.no_network">Эта программа требует доступ к сети. Пожалуйста, включите Wi-Fi или мобильный интернет</string>
+ <string name="background_task.network_error">Ошибка сети. Пожалуйста, проверьте адрес сервера и попробуйте снова</string>
+ <string name="background_task.not_found">Ресурс не найден. Пожалуйста, проверьте адрес сервера</string>
+ <string name="background_task.parse_error">Неизвестный ответ. Пожалуйста, проверьте адрес сервера</string>
+
+ <string name="service.connecting">Подключение к серверу. Пожалуйста, подождите.</string>
+
+ <string name="parser.reading">Чтение с сервера.</string>
+ <string name="parser.reading_done">Чтение с сервера выполнено!</string>
+ <string name="parser.upgrade_client">Несовместимые версии. Пожалуйста, обновите приложение DSub для Android.</string>
+ <string name="parser.upgrade_server">Несовместимые версии. Пожалуйста, обновите сервер Subsonic.</string>
+ <string name="parser.not_authenticated">Неправильное имя пользователя или пароль.</string>
+ <string name="parser.not_authorized">Не авторизирован. Проверьте права пользователя на сервере Subsonic.</string>
+ <string name="parser.artist_count">Получено %d исполнителей.</string>
+
+ <string name="select_artist.refresh">Обновить</string>
+ <string name="select_artist.folder">Выбрать папку</string>
+ <string name="select_artist.all_folders">Все папки</string>
+
+ <string name="equalizer.label">Эквалайзер</string>
+ <string name="equalizer.enabled">Включен</string>
+ <string name="equalizer.preset">Готовые настройки</string>
+
+ <string name="widget.initial_text">Коснитесь для выбора музыки</string>
+ <string name="widget.sdcard_busy">SD карта недоступна</string>
+ <string name="widget.sdcard_missing">Нет SD карты</string>
+
+ <string name="util.bytes_format.gigabyte">0.00 ГБ</string>
+ <string name="util.bytes_format.megabyte">0.00 МБ</string>
+ <string name="util.bytes_format.kilobyte">0 КБ</string>
+ <string name="util.bytes_format.byte">0 Б</string>
+
+ <string name="button_bar.chat">Чат</string>
+ <string name="main.back_confirm">Нажмите "назад" еще раз для выхода</string>
+ <string name="download.playing_out_of">Воспроизведение: %1$d/%2$d</string>
+ <string name="settings.persistent_title">Постоянное уведомление</string>
+ <string name="settings.persistent_summary">Показывать уведомление даже во время паузы. Остановка воспроизведения уберет это уведомление.</string>
+ <string name="settings.gapless_playback">Непрерывное воспроизведение</string>
+ <string name="settings.gapless_playback_summary">Galaxy S3 может зависать или испытывать прочие трудности с момента начала непрерывного воспроизведения. Выключите эту функцию для исправления данной проблемы.</string>
+ <string name="settings.chat_refresh">Частота обновления чата (сек)</string>
+ <string name="settings.chat_enabled">Чат активен</string>
+ <string name="settings.chat_enabled_summary">Показывать или нет вкладку чата</string>
+ <string name="changelog_full_title">Журнал изменений</string>
+ <string name="changelog_title">Что нового</string>
+ <string name="changelog_ok_button">OK</string>
+ <string name="changelog_show_full">Еще…</string>
+ <string name="chat.send_a_message">Отправить сообщение</string>
+
+
+ <plurals name="select_album_n_songs">
+ <item quantity="zero">Нет композиций</item>
+ <item quantity="one">1 композиция</item>
+ <item quantity="other">%d композиций</item>
+ </plurals>
+ <plurals name="select_album_n_songs_downloading">
+ <item quantity="one">1 композиция запланирована для скачивания</item>
+ <item quantity="other">%d композиций запланировано для скачивания</item>
+ </plurals>
+ <plurals name="select_album_n_songs_added">
+ <item quantity="one">1 композиция добавлена в очередь воспроизведения</item>
+ <item quantity="other">%d композиций добавлено в очередь воспроизведения</item>
+ </plurals>
+ <plurals name="select_album_donate_dialog_n_trial_days_left">
+ <item quantity="one">1 день до конца пробного периода</item>
+ <item quantity="other">%d дней до конца пробного периода</item>
+ </plurals>
+
+</resources>
diff --git a/res/values-v11/colors.xml b/res/values-v11/colors.xml
new file mode 100644
index 00000000..f5a422bb
--- /dev/null
+++ b/res/values-v11/colors.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="notificationArtist">#bababa</color>
+ <color name="notificationTitle">#dddddd</color>
+</resources>
diff --git a/res/values/arrays.xml b/res/values/arrays.xml
new file mode 100644
index 00000000..d6541f9f
--- /dev/null
+++ b/res/values/arrays.xml
@@ -0,0 +1,153 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string-array name="themeValues">
+ <item>light</item>
+ <item>dark</item>
+ <item>black</item>
+ <item>holo</item>
+ <item>light_fullscreen</item>
+ <item>dark_fullscreen</item>
+ <item>black_fullscreen</item>
+ <item>holo_fullscreen</item>
+ </string-array>
+
+ <string-array name="themeNames">
+ <item>@string/settings.theme_light</item>
+ <item>@string/settings.theme_dark</item>
+ <item>@string/settings.theme_black</item>
+ <item>@string/settings.theme_holo</item>
+ <item>@string/settings.theme_light_fullscreen</item>
+ <item>@string/settings.theme_dark_fullscreen</item>
+ <item>@string/settings.theme_black_fullscreen</item>
+ <item>@string/settings.theme_holo_fullscreen</item>
+ </string-array>
+
+ <string-array name="sleepTimerValues">
+ <item>0</item>
+ <item>1</item>
+ <item>2</item>
+ </string-array>
+
+ <string-array name="sleepTimerNames">
+ <item>@string/settings.sleep_timer_off</item>
+ <item>@string/settings.sleep_timer_on</item>
+ <item>@string/settings.sleep_timer_always_on</item>
+ </string-array>
+
+ <string-array name="preloadCountValues">
+ <item>1</item>
+ <item>2</item>
+ <item>3</item>
+ <item>5</item>
+ <item>10</item>
+ <item>-1</item>
+ </string-array>
+
+ <string-array name="preloadCountNames">
+ <item>@string/settings.preload_1</item>
+ <item>@string/settings.preload_2</item>
+ <item>@string/settings.preload_3</item>
+ <item>@string/settings.preload_5</item>
+ <item>@string/settings.preload_10</item>
+ <item>@string/settings.preload_unlimited</item>
+ </string-array>
+
+ <string-array name="maxBitrateValues">
+ <item>32</item>
+ <item>64</item>
+ <item>80</item>
+ <item>96</item>
+ <item>112</item>
+ <item>128</item>
+ <item>160</item>
+ <item>192</item>
+ <item>256</item>
+ <item>320</item>
+ <item>0</item>
+ </string-array>
+
+ <string-array name="maxBitrateNames">
+ <item>@string/settings.max_bitrate_32</item>
+ <item>@string/settings.max_bitrate_64</item>
+ <item>@string/settings.max_bitrate_80</item>
+ <item>@string/settings.max_bitrate_96</item>
+ <item>@string/settings.max_bitrate_112</item>
+ <item>@string/settings.max_bitrate_128</item>
+ <item>@string/settings.max_bitrate_160</item>
+ <item>@string/settings.max_bitrate_192</item>
+ <item>@string/settings.max_bitrate_256</item>
+ <item>@string/settings.max_bitrate_320</item>
+ <item>@string/settings.max_bitrate_unlimited</item>
+ </string-array>
+
+ <string-array name="maxVideoBitrateValues">
+ <item>200</item>
+ <item>300</item>
+ <item>400</item>
+ <item>500</item>
+ <item>700</item>
+ <item>1000</item>
+ <item>1500</item>
+ <item>2000</item>
+ <item>3000</item>
+ <item>5000</item>
+ <item>0</item>
+ </string-array>
+
+ <string-array name="maxVideoBitrateNames">
+ <item>@string/settings.max_video_bitrate_200</item>
+ <item>@string/settings.max_video_bitrate_300</item>
+ <item>@string/settings.max_video_bitrate_400</item>
+ <item>@string/settings.max_video_bitrate_500</item>
+ <item>@string/settings.max_video_bitrate_700</item>
+ <item>@string/settings.max_video_bitrate_1000</item>
+ <item>@string/settings.max_video_bitrate_1500</item>
+ <item>@string/settings.max_video_bitrate_2000</item>
+ <item>@string/settings.max_video_bitrate_3000</item>
+ <item>@string/settings.max_video_bitrate_5000</item>
+ <item>@string/settings.max_bitrate_unlimited</item>
+ </string-array>
+
+ <string-array name="networkTimeoutValues">
+ <item>10000</item>
+ <item>15000</item>
+ <item>30000</item>
+ <item>45000</item>
+ <item>60000</item>
+ </string-array>
+ <string-array name="networkTimeoutNames">
+ <item>@string/settings.network_timeout_10000</item>
+ <item>@string/settings.network_timeout_15000</item>
+ <item>@string/settings.network_timeout_30000</item>
+ <item>@string/settings.network_timeout_45000</item>
+ <item>@string/settings.network_timeout_60000</item>
+ </string-array>
+
+ <string-array name="tempLossValues">
+ <item>0</item>
+ <item>1</item>
+ <item>2</item>
+ <item>3</item>
+ </string-array>
+ <string-array name="tempLossNames">
+ <item>@string/settings.temp_loss_pause</item>
+ <item>@string/settings.temp_loss_pause_lower</item>
+ <item>@string/settings.temp_loss_lower</item>
+ <item>@string/settings.temp_loss_nothing</item>
+ </string-array>
+
+ <string-array name="videoPlayerValues">
+ <item>raw</item>
+ <item>hls</item>
+ <item>transcode</item>
+ <item>flash</item>
+ </string-array>
+ <string-array name="videoPlayerNames">
+ <item>@string/settings.video_raw</item>
+ <item>@string/settings.video_hls</item>
+ <item>@string/settings.video_transcode</item>
+ <item>@string/settings.video_flash</item>
+ </string-array>
+
+</resources> \ No newline at end of file
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
new file mode 100644
index 00000000..8f669cd2
--- /dev/null
+++ b/res/values/attrs.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <attr name="offline_icon" format="reference"/>
+ <attr name="media_button_backward" format="reference"/>
+ <attr name="media_button_forward" format="reference"/>
+ <attr name="media_button_pause" format="reference"/>
+ <attr name="media_button_repeat_off" format="reference"/>
+ <attr name="media_button_start" format="reference"/>
+ <attr name="media_button_stop" format="reference"/>
+ <attr name="chat" format="reference"/>
+ <attr name="chat_send" format="reference" />
+</resources>
diff --git a/res/values/colors.xml b/res/values/colors.xml
new file mode 100644
index 00000000..0ce98fc9
--- /dev/null
+++ b/res/values/colors.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="lightBackground">#F1F0E6</color>
+ <color name="dividerColor">#FF33B5E5</color>
+ <color name="appwidget_text">#FFFFFF</color>
+ <color name="notificationArtist">#434343</color>
+ <color name="notificationTitle">#000000</color>
+ <color name="background_holo_light">#ff33b5e5</color>
+ <color name="overlayColor">#80000000</color>
+ <color name="ics_opaque">#8033b5e5</color>
+ <color name="cyan">#ff0099cc</color>
+</resources> \ No newline at end of file
diff --git a/res/values/ids.xml b/res/values/ids.xml
new file mode 100644
index 00000000..edb3bbec
--- /dev/null
+++ b/res/values/ids.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources>
+ <item name="drag_handle" type="id"/>
+</resources> \ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
new file mode 100644
index 00000000..0b2815ec
--- /dev/null
+++ b/res/values/strings.xml
@@ -0,0 +1,401 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="common.appname">DSub</string>
+ <string name="common.ok">OK</string>
+ <string name="common.save">Save</string>
+ <string name="common.cancel">Cancel</string>
+ <string name="common.play_now">Play now</string>
+ <string name="common.play_shuffled">Play shuffled</string>
+ <string name="common.play_next">Play next</string>
+ <string name="common.play_last">Play last</string>
+ <string name="common.download">Cache</string>
+ <string name="common.pin">Permanent Cache</string>
+ <string name="common.delete">Delete</string>
+ <string name="common.star">Star</string>
+ <string name="common.unstar">Unstar</string>
+ <string name="common.info">Details</string>
+ <string name="common.name">Name</string>
+ <string name="common.comment">Comment</string>
+ <string name="common.public">Public</string>
+ <string name="common.play_external">Play Video</string>
+ <string name="common.stream_external">Stream Video</string>
+ <string name="common.confirm">Confirm</string>
+ <string name="common.confirm_message">Do you want to %1$s %2$s?</string>
+
+ <string name="button_bar.home">Home</string>
+ <string name="button_bar.browse">Library</string>
+ <string name="button_bar.search">Search</string>
+ <string name="button_bar.playlists">Playlists</string>
+ <string name="button_bar.now_playing">Playing</string>
+ <string name="button_bar.podcasts">Podcasts</string>
+ <string name="button_bar.chat">Chat</string>
+
+ <string name="main.welcome_title">Welcome!</string>
+ <string name="main.welcome_text">Welcome to DSub! The app is currently configured to use the Subsonic demo server. After you\'ve
+ set up your personal server (available from <b>subsonic.org</b>), please go to <b>Settings</b> and change the configuration to connect to it.</string>
+ <string name="main.about_title">About DSub</string>
+ <string name="main.about_text">Author: Scott Jackson
+ \nEmail: daneren2005@gmail.com
+ \nVersion: %1$s
+ \nUsed Space: %2$s of %3$s
+ \nAvailable Space: %4$s of %5$s</string>
+ <string name="main.select_server">Select server</string>
+ <string name="main.shuffle">Shuffle play</string>
+ <string name="main.offline">Go Offline</string>
+ <string name="main.online">Go Online</string>
+ <string name="main.settings">Settings</string>
+ <string name="main.albums_title">Albums</string>
+ <string name="main.albums_newest">Recently added</string>
+ <string name="main.albums_recent">Recently played</string>
+ <string name="main.albums_frequent">Most played</string>
+ <string name="main.albums_highest">Top rated</string>
+ <string name="main.albums_starred">Starred</string>
+ <string name="main.albums_random">Random</string>
+ <string name="main.albums_genres">Genres</string>
+ <string name="main.back_confirm">Press back again to exit</string>
+
+ <string name="menu.search">Search</string>
+ <string name="menu.shuffle">Shuffle</string>
+ <string name="menu.refresh">Refresh</string>
+ <string name="menu.select">Select All</string>
+ <string name="menu.play">Play</string>
+ <string name="menu.play_last">Play Last</string>
+ <string name="menu.exit">Exit</string>
+ <string name="menu.settings">Settings</string>
+ <string name="menu.help">Help</string>
+ <string name="menu.about">About</string>
+ <string name="menu.add_playlist">Add To Playlist</string>
+ <string name="menu.remove_playlist">Remove From Playlist</string>
+ <string name="menu.deleted_playlist">Deleted playlist %s</string>
+ <string name="menu.deleted_playlist_error">Failed to delete playlist %s</string>
+ <string name="menu.log">Send Log</string>
+ <string name="menu.set_timer">Set Timer</string>
+ <string name="menu.check_podcasts">Check For New Episodes</string>
+ <string name="menu.add_podcast">Add Channel</string>
+
+ <string name="playlist.label">Playlists</string>
+ <string name="playlist.update_info">Update Information</string>
+ <string name="playlist.updated_info">Updated playlist information for %s</string>
+ <string name="playlist.updated_info_error">Failed to update playlist information for %s</string>
+ <string name="playlist.overwrite">Overwrite existing playlist</string>
+ <string name="playlist.add_to">Add to Playlist</string>
+ <string name="playlist.create_new">Create New</string>
+
+ <string name="help.label">Help</string>
+ <string name="help.title">Welcome to DSub!</string>
+ <string name="help.back">Back</string>
+ <string name="help.close">Close</string>
+ <string name="help.url">file:///android_asset/html/en/index.html</string>
+ <string name="help.loading">Loading...</string>
+
+ <string name="play_video.loading">Loading video...</string>
+ <string name="play_video.noplugin">Please install Adobe Flash Player from Android Market.</string>
+
+ <string name="search.label">Search</string>
+ <string name="search.title">Search</string>
+ <string name="search.search">Click to search</string>
+ <string name="search.no_match">No matches, please try again</string>
+ <string name="search.artists">Artists</string>
+ <string name="search.albums">Albums</string>
+ <string name="search.songs">Songs</string>
+ <string name="search.more">Show more</string>
+
+ <string name="progress.wait">Please wait...</string>
+
+ <string name="music_library.label">Media library</string>
+ <string name="music_library.label_offline">Offline media</string>
+
+ <string name="select_album.empty">No media found</string>
+ <string name="select_album.select">Select all</string>
+ <string name="select_album.n_selected">%d tracks selected.</string>
+ <string name="select_album.n_unselected">%d tracks unselected.</string>
+ <string name="select_album.more">More</string>
+ <string name="select_album.offline">Offline</string>
+ <string name="select_album.searching">Searching...</string>
+ <string name="select_album.no_sdcard">Error: No SD card available.</string>
+ <string name="select_album.no_network">Warning: No network available.</string>
+ <string name="select_album.not_licensed">Server not licensed. %d trial days left.</string>
+ <string name="select_album.donate_dialog_message">Get unlimited downloads by donating to Subsonic.</string>
+ <string name="select_album.donate_dialog_now">Now</string>
+ <string name="select_album.donate_dialog_later">Later</string>
+ <string name="select_album.donate_dialog_0_trial_days_left">Trial period is over</string>
+
+ <string name="offline.sync_dialog_title">Offline songs waiting to be synced</string>
+ <string name="offline.sync_dialog_message">Process %1$d offline scrobbles?
+ \nProcess %2$d offline stars?
+ </string>
+ <string name="offline.sync_dialog_default">Use action as default</string>
+ <string name="offline.sync_success">Successfully synced %1$d songs</string>
+ <string name="offline.sync_partial">Successfully synced %1$d of %2$d songs</string>
+ <string name="offline.sync_error">Failed to sync songs</string>
+
+ <string name="select_genre.empty">No genres found</string>
+ <string name="select_genre.blank">Blank</string>
+
+ <string name="select_podcasts.empty">No podcasts found</string>
+ <string name="select_podcasts.error">This podcast had an error while downloading on the server. The server must download it first.</string>
+ <string name="select_podcasts.skipped">This podcast has not been downloaded on the server. The server must download it first.</string>
+ <string name="select_podcasts.initializing">This podcast channel is being initialized on the server. Please reload after a moment.</string>
+ <string name="select_podcasts.server_download">Download on server</string>
+ <string name="select_podcasts.server_delete">Delete from server</string>
+ <string name="select_podcasts.downloading">Now downloading %s on the server</string>
+ <string name="select_podcasts.refreshing">The server is checking for new podcasts now</string>
+ <string name="select_podcasts.deleted">Deleted podcast %s</string>
+ <string name="select_podcasts.deleted_error">Failed to delete podcast %s</string>
+ <string name="select_podcasts.add_url">URL:</string>
+ <string name="select_podcasts.created_error">Failed to add podcast</string>
+ <string name="select_podcasts.invalid_podcast_channel">Invalid podcast channel: %s</string>
+
+ <string name="select_playlist.empty">No saved playlists on server</string>
+
+ <string name="download.empty">Playlist is empty</string>
+ <string name="download.shuffle_loading">Shuffle list is loading...</string>
+ <string name="download.playerstate_downloading">Downloading - %s</string>
+ <string name="download.playerstate_buffering">Buffering</string>
+ <string name="download.playerstate_playing_shuffle">Playing shuffle</string>
+ <string name="download.menu_show_album">Show album</string>
+ <string name="download.menu_lyrics">Lyrics</string>
+ <string name="download.menu_remove">Remove from queue</string>
+ <string name="download.menu_delete">Delete cache</string>
+ <string name="download.menu_remove_all">Remove all</string>
+ <string name="download.menu_screen_on">Screen on</string>
+ <string name="download.menu_screen_off">Screen off</string>
+ <string name="download.menu_shuffle">Shuffle</string>
+ <string name="download.menu_toggle">Toggle</string>
+ <string name="download.menu_save">Save playlist</string>
+ <string name="download.menu_shuffle_notification">Playlist was shuffled</string>
+ <string name="download.playlist_title">Save playlist</string>
+ <string name="download.playlist_name">Enter the playlist name:</string>
+ <string name="download.playlist_saving">Saving playlist \"%s\"...</string>
+ <string name="download.playlist_done">Playlist was successfully saved.</string>
+ <string name="download.playlist_error">Failed to save playlist, please try later.</string>
+ <string name="download.repeat_off">Repeat off</string>
+ <string name="download.repeat_all">Repeat all</string>
+ <string name="download.repeat_single">Repeat song</string>
+ <string name="download.visualizer_on">Turned on visualizer.</string>
+ <string name="download.visualizer_off">Turned off visualizer.</string>
+ <string name="download.jukebox_on">Turned on remote control. Music is played on the computer.</string>
+ <string name="download.jukebox_off">Turned off remote control. Music is played on the phone.</string>
+ <string name="download.jukebox_volume">Remote volume</string>
+ <string name="download.jukebox_server_too_old">Remote control is not supported. Please upgrade your Subsonic server.</string>
+ <string name="download.jukebox_offline">Remote control is not available in offline mode.</string>
+ <string name="download.jukebox_not_authorized">Remote control is not allowed. Please enable jukebox mode in <b>Users &gt; Settings</b> on your Subsonic server.</string>
+ <string name="download.show_downloading">Show Downloading</string>
+ <string name="download.show_now_playing">Show Now Playing</string>
+ <string name="download.timer_length">Timer Length</string>
+ <string name="download.start_timer">Start Timer</string>
+ <string name="download.stop_timer">Stop Timer</string>
+ <string name="download.need_download">Video needs to be downloaded first</string>
+ <string name="download.no_streaming_player">No player can play this stream</string>
+ <string name="download.playing_out_of">Playing: %1$d/%2$d</string>
+
+ <string name="starring_content_starred">Starred \"%s\"</string>
+ <string name="starring_content_unstarred">Unstarred \"%s\"</string>
+ <string name="starring_content_error">Failed to update \"%s\", please try later.</string>
+
+ <string name="playlist_error">Failed to grab list of playlists</string>
+ <string name="updated_playlist">Added %1$s songs to \"%2$s\"</string>
+ <string name="updated_playlist_error">Failed to update \"%s\", please try later.</string>
+ <string name="removed_playlist">Removed %1$s songs from \"%2$s\"</string>
+
+ <string name="song_details.all">%1$s %2$s</string>
+ <string name="song_details.kbps">%d kbps</string>
+ <string name="song_details.error">Error</string>
+ <string name="song_details.skipped">Skipped</string>
+ <string name="song_details.downloading">Downloading</string>
+
+ <string name="lyrics.nomatch">No lyrics found</string>
+
+ <string name="error.label">Error</string>
+
+ <string name="settings.title">DSub settings</string>
+ <string name="settings.test_connection_title">Test connection</string>
+ <string name="settings.servers_add">Add Server</string>
+ <string name="settings.servers_remove">Remove Server</string>
+ <string name="settings.servers_title">Servers</string>
+ <string name="settings.server_unused">Unused</string>
+ <string name="settings.server_name">Name</string>
+ <string name="settings.server_address">Server address</string>
+ <string name="settings.server_username">Username</string>
+ <string name="settings.server_password">Password</string>
+ <string name="settings.server_open_browser">Open in browser</string>
+ <string name="settings.cache_title">Music cache</string>
+ <string name="settings.preload_wifi">Songs to preload (Wifi)</string>
+ <string name="settings.preload_mobile">Songs to preload (Mobile)</string>
+ <string name="settings.cache_size">Cache size (MB)</string>
+ <string name="settings.cache_location">Cache location</string>
+ <string name="settings.cache_location_error">Invalid cache location. Using default.</string>
+ <string name="settings.cache_clear">Clear Cache</string>
+ <string name="settings.cache_clear_complete">Finished clearing cache</string>
+ <string name="settings.testing_connection">Testing connection...</string>
+ <string name="settings.testing_ok">Connection is OK</string>
+ <string name="settings.testing_unlicensed">Connection is OK. Server unlicensed.</string>
+ <string name="settings.connection_failure">Connection failure.</string>
+ <string name="settings.invalid_url">Please specify a valid URL.</string>
+ <string name="settings.invalid_username">Please specify a valid username (no trailing spaces).</string>
+ <string name="settings.appearance_title">Appearance</string>
+ <string name="settings.theme_title">Theme</string>
+ <string name="settings.theme_light">Light</string>
+ <string name="settings.theme_dark">Dark</string>
+ <string name="settings.theme_black">Black</string>
+ <string name="settings.theme_holo">Holo</string>
+ <string name="settings.theme_light_fullscreen">Light Fullscreen</string>
+ <string name="settings.theme_dark_fullscreen">Dark Fullscreen</string>
+ <string name="settings.theme_black_fullscreen">Black Fullscreen</string>
+ <string name="settings.theme_holo_fullscreen">Holo Fullscreen</string>
+ <string name="settings.track_title">Display Track #</string>
+ <string name="settings.track_summary">Display Track # in front of songs if one exists</string>
+ <string name="settings.network_title">Network</string>
+ <string name="settings.max_bitrate_wifi">Max Audio bitrate - Wi-Fi</string>
+ <string name="settings.max_bitrate_mobile">Max Audio bitrate - Mobile</string>
+ <string name="settings.max_bitrate_32">32 Kbps</string>
+ <string name="settings.max_bitrate_64">64 Kbps</string>
+ <string name="settings.max_bitrate_80">80 Kbps</string>
+ <string name="settings.max_bitrate_96">96 Kbps</string>
+ <string name="settings.max_bitrate_112">112 Kbps</string>
+ <string name="settings.max_bitrate_128">128 Kbps</string>
+ <string name="settings.max_bitrate_160">160 Kbps</string>
+ <string name="settings.max_bitrate_192">192 Kbps</string>
+ <string name="settings.max_bitrate_256">256 Kbps</string>
+ <string name="settings.max_bitrate_320">320 Kbps</string>
+ <string name="settings.max_video_bitrate_wifi">Max Video bitrate - Wi-Fi</string>
+ <string name="settings.max_video_bitrate_mobile">Max Video bitrate - Mobile</string>
+ <string name="settings.max_video_bitrate_200">200 Kbps</string>
+ <string name="settings.max_video_bitrate_300">300 Kbps</string>
+ <string name="settings.max_video_bitrate_400">400 Kbps</string>
+ <string name="settings.max_video_bitrate_500">500 Kbps</string>
+ <string name="settings.max_video_bitrate_700">700 Kbps</string>
+ <string name="settings.max_video_bitrate_1000">1000 Kbps</string>
+ <string name="settings.max_video_bitrate_1500">1500 Kbps</string>
+ <string name="settings.max_video_bitrate_2000">2000 Kbps</string>
+ <string name="settings.max_video_bitrate_3000">3000 Kbps</string>
+ <string name="settings.max_video_bitrate_5000">5000 Kbps</string>
+ <string name="settings.max_bitrate_unlimited">Unlimited</string>
+ <string name="settings.wifi_required_title">Wi-Fi streaming only</string>
+ <string name="settings.wifi_required_summary">Only stream media if connected to Wi-Fi</string>
+ <string name="settings.network_timeout_title">Network Timeout</string>
+ <string name="settings.network_timeout_10000">10 seconds</string>
+ <string name="settings.network_timeout_15000">15 seconds</string>
+ <string name="settings.network_timeout_30000">30 seconds</string>
+ <string name="settings.network_timeout_45000">45 seconds</string>
+ <string name="settings.network_timeout_60000">60 seconds</string>
+ <string name="settings.preload_1">1 song</string>
+ <string name="settings.preload_2">2 songs</string>
+ <string name="settings.preload_3">3 songs</string>
+ <string name="settings.preload_5">5 songs</string>
+ <string name="settings.preload_10">10 songs</string>
+ <string name="settings.preload_unlimited">Unlimited</string>
+ <string name="settings.clear_search_history">Clear search history</string>
+ <string name="settings.search_history_cleared">Search history cleared</string>
+ <string name="settings.other_title">Other settings</string>
+ <string name="settings.scrobble_title">Scrobble to Last.fm</string>
+ <string name="settings.scrobble_summary">Remember to set up your Last.fm user and password on the DSub server</string>
+ <string name="settings.hide_media_title">Hide from other</string>
+ <string name="settings.hide_media_summary">Hide music files from other apps.</string>
+ <string name="settings.hide_media_toast">Takes effect next time Android scans your phone for music.</string>
+ <string name="settings.media_button_title">Media buttons</string>
+ <string name="settings.media_button_summary">Respond to phone, headset and Bluetooth media buttons</string>
+ <string name="settings.screen_lit_title">Keep screen on</string>
+ <string name="settings.screen_lit_summary">Keeping the screen on while downloading improves download speed.</string>
+ <string name="settings.playlist_title">Playlists</string>
+ <string name="settings.playlist_random_size_title">Random Size</string>
+ <string name="settings.buffer_length">Buffer Length (0 = when fully cached)</string>
+ <string name="settings.sleep_timer_title">Sleep Timer</string>
+ <string name="settings.sleep_timer_duration_title">Sleep Timer Duration</string>
+ <string name="settings.sleep_timer_off">Off</string>
+ <string name="settings.sleep_timer_on">On</string>
+ <string name="settings.sleep_timer_always_on">Always On</string>
+ <string name="settings.temp_loss_title">Temporary Loss of Focus</string>
+ <string name="settings.temp_loss_pause">Always Pause</string>
+ <string name="settings.temp_loss_pause_lower">Pause, lower volume when requested</string>
+ <string name="settings.temp_loss_lower">Always lower volume</string>
+ <string name="settings.temp_loss_nothing">Do Nothing</string>
+ <string name="settings.persistent_title">Persistent Notification</string>
+ <string name="settings.persistent_summary">Show the notification even after pausing. Press the stop button to clear it away.</string>
+ <string name="settings.gapless_playback">Gapless Playback</string>
+ <string name="settings.gapless_playback_summary">The Galaxy S3 seems to be experiencing freezes/other weird issue since the introduction of gapless playback. Turn this off to fix the issue.</string>
+ <string name="settings.chat_refresh">Chat Refresh Rate (Secs)</string>
+ <string name="settings.chat_enabled">Chat Enabled</string>
+ <string name="settings.chat_enabled_summary">Whether or not to display the chat tab. Restart app after changing.</string>
+ <string name="settings.video_title">Video</string>
+ <string name="settings.video_player">Video Player</string>
+ <string name="settings.video_raw">Raw (Requires Subsonic 4.8+)</string>
+ <string name="settings.video_hls">HTTP Live Stream (HLS) (Requires Subsonic 4.8+)</string>
+ <string name="settings.video_transcode">Direct Transcode (Requires video -> mp4 or similar setup on Server)</string>
+ <string name="settings.video_flash">Flash (Requires Plugin)</string>
+
+ <string name="shuffle.title">Shuffle By</string>
+ <string name="shuffle.startYear">Start Year:</string>
+ <string name="shuffle.endYear">End Year:</string>
+ <string name="shuffle.genre">Genre:</string>
+ <string name="shuffle.pick_genre">Pick a genre</string>
+
+ <string name="music_service.retry">A network error occurred. Retrying %1$d of %2$d.</string>
+
+ <string name="background_task.wait">Please wait...</string>
+ <string name="background_task.loading">Loading.</string>
+ <string name="background_task.no_network">This program requires network access. Please turn on Wi-Fi or mobile network.</string>
+ <string name="background_task.network_error">A network error occurred. Please check the server address or try again later.</string>
+ <string name="background_task.not_found">Resource not found. Please check the server address.</string>
+ <string name="background_task.parse_error">Didn\'t understand the reply. Please check the server address.</string>
+
+ <string name="service.connecting">Contacting server, please wait.</string>
+
+ <string name="parser.reading">Reading from server.</string>
+ <string name="parser.reading_done">Reading from server. Done!</string>
+ <string name="parser.upgrade_client">Incompatible versions. Please upgrade DSub Android app.</string>
+ <string name="parser.upgrade_server">Incompatible versions. Please upgrade Subsonic server.</string>
+ <string name="parser.not_authenticated">Wrong username or password.</string>
+ <string name="parser.not_authorized">Not authorized. Check user permissions in Subsonic server.</string>
+ <string name="parser.artist_count">Got %d artists.</string>
+
+ <string name="select_artist.refresh">Refresh</string>
+ <string name="select_artist.folder">Select folder</string>
+ <string name="select_artist.all_folders">All folders</string>
+
+ <string name="equalizer.label">Equalizer</string>
+ <string name="equalizer.enabled">Enabled</string>
+ <string name="equalizer.preset">Select preset</string>
+
+ <string name="widget.4x1">DSub (4x1)</string>
+ <string name="widget.4x2">DSub (4x2)</string>
+ <string name="widget.4x3">DSub (4x3)</string>
+ <string name="widget.4x4">DSub (4x4)</string>
+ <string name="widget.initial_text">Touch to select music</string>
+ <string name="widget.sdcard_busy">SD card unavailable</string>
+ <string name="widget.sdcard_missing">No SD card</string>
+
+ <string name="util.bytes_format.gigabyte">0.00 GB</string>
+ <string name="util.bytes_format.megabyte">0.00 MB</string>
+ <string name="util.bytes_format.kilobyte">0 KB</string>
+ <string name="util.bytes_format.byte">0 B</string>
+
+ <string name="changelog_full_title">Change Log</string>
+ <string name="changelog_title">What\'s New</string>
+ <string name="changelog_ok_button">OK</string>
+ <string name="changelog_show_full">More…</string>
+
+ <string name="chat.send_a_message">Send a message</string>
+
+ <string name="changelog_version_format" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">Version <xliff:g id="version_name">%s</xliff:g></string>
+
+ <plurals name="select_album_n_songs">
+ <item quantity="zero">No songs</item>
+ <item quantity="one">One song</item>
+ <item quantity="other">%d songs</item>
+ </plurals>
+ <plurals name="select_album_n_songs_downloading">
+ <item quantity="one">One song scheduled for download.</item>
+ <item quantity="other">%d songs scheduled for download.</item>
+ </plurals>
+ <plurals name="select_album_n_songs_added">
+ <item quantity="one">One song added to play queue.</item>
+ <item quantity="other">%d songs added to play queue.</item>
+ </plurals>
+ <plurals name="select_album_donate_dialog_n_trial_days_left">
+ <item quantity="one">One day left of trial period</item>
+ <item quantity="other">%d days left of trial period</item>
+ </plurals>
+
+</resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
new file mode 100644
index 00000000..4dcce34f
--- /dev/null
+++ b/res/values/styles.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <style name="PlaybackControl">
+ <item name="android:background">@drawable/media_button</item>
+ <item name="android:scaleType">fitCenter</item>
+ <item name="android:padding">6dip</item>
+ <item name="android:layout_marginLeft">4dip</item>
+ <item name="android:layout_marginRight">4dip</item>
+ <item name="android:layout_width">54dip</item>
+ <item name="android:layout_height">54dip</item>
+ <item name="android:contentDescription">@null</item>
+ </style>
+
+ <style name="PlaybackControl.Small" parent="@style/PlaybackControl">
+ <item name="android:padding">4dip</item>
+ <item name="android:layout_width">46dip</item>
+ <item name="android:layout_height">46dip</item>
+ </style>
+
+ <style name="MenuBarButton">
+ <item name="android:layout_width">0dip</item>
+ <item name="android:layout_height">45dip</item>
+ <item name="android:layout_weight">1</item>
+ <item name="android:textSize">14sp</item>
+ <item name="android:textStyle">bold</item>
+ <item name="android:background">@drawable/menubar_button</item>
+ <item name="android:textColor">?android:textColorPrimary</item>
+ </style>
+
+ <style name="DragDropListView">
+ <item name="drag_enabled">true</item>
+ <item name="collapsed_height">1dp</item>
+ <item name="drag_scroll_start">1.0</item>
+ <item name="max_drag_scroll_speed">2.0</item>
+ <item name="float_alpha">0.6</item>
+ <item name="slide_shuffle_speed">0.3</item>
+ <item name="track_drag_sort">false</item>
+ <item name="use_default_controller">true</item>
+ <item name="drag_handle_id">@id/drag_handle</item>
+ <item name="sort_enabled">true</item>
+ <item name="remove_enabled">false</item>
+ <item name="remove_mode">flingRemove</item>
+ <item name="drag_start_mode">onLongPress</item>
+ </style>
+</resources> \ No newline at end of file
diff --git a/res/values/themes.xml b/res/values/themes.xml
new file mode 100644
index 00000000..33dd2de7
--- /dev/null
+++ b/res/values/themes.xml
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <style name="Theme.DSub.Light" parent="Theme.Sherlock.Light">
+ <item name="actionBarStyle">@style/Widget.DSub.ActionBarStyle.Light</item>
+ <item name="android:actionBarStyle">@style/Widget.DSub.ActionBarStyle.Light</item>
+ <item name="offline_icon">@drawable/main_offline_light</item>
+ <item name="media_button_backward">@drawable/media_backward_light</item>
+ <item name="media_button_forward">@drawable/media_forward_light</item>
+ <item name="media_button_pause">@drawable/media_pause_light</item>
+ <item name="media_button_repeat_off">@drawable/media_repeat_off_light</item>
+ <item name="media_button_start">@drawable/media_start_light</item>
+ <item name="media_button_stop">@drawable/media_stop_light</item>
+ <item name="chat">@drawable/ic_menu_chat_light</item>
+ <item name="chat_send">@drawable/ic_menu_chat_send_light</item>
+ </style>
+ <style name="Theme.DSub.Dark" parent="Theme.Sherlock">
+ <item name="actionBarStyle">@style/Widget.DSub.ActionBarStyle.Dark</item>
+ <item name="android:actionBarStyle">@style/Widget.DSub.ActionBarStyle.Dark</item>
+ <item name="android:textColorSecondary">@color/cyan</item>
+ <item name="offline_icon">@drawable/main_offline</item>
+ <item name="media_button_backward">@drawable/media_backward</item>
+ <item name="media_button_forward">@drawable/media_forward</item>
+ <item name="media_button_pause">@drawable/media_pause</item>
+ <item name="media_button_repeat_off">@drawable/media_repeat_off</item>
+ <item name="media_button_start">@drawable/media_start</item>
+ <item name="media_button_stop">@drawable/media_stop</item>
+ <item name="chat">@drawable/ic_menu_chat_dark</item>
+ <item name="chat_send">@drawable/ic_menu_chat_send_dark</item>
+ </style>
+ <style name="Theme.DSub.Black" parent="Theme.DSub.Dark">
+ <item name="android:windowBackground">@android:color/black</item>
+ </style>
+ <style name="Theme.DSub.Holo" parent="Theme.Sherlock">
+ <item name="actionBarStyle">@style/Widget.DSub.ActionBarStyle.Holo</item>
+ <item name="android:actionBarStyle">@style/Widget.DSub.ActionBarStyle.Holo</item>
+ <item name="android:textColorSecondary">@color/cyan</item>
+ <item name="android:windowBackground">@drawable/background</item>
+ <item name="offline_icon">@drawable/main_offline</item>
+ <item name="media_button_backward">@drawable/media_backward</item>
+ <item name="media_button_forward">@drawable/media_forward</item>
+ <item name="media_button_pause">@drawable/media_pause</item>
+ <item name="media_button_repeat_off">@drawable/media_repeat_off</item>
+ <item name="media_button_start">@drawable/media_start</item>
+ <item name="media_button_stop">@drawable/media_stop</item>
+ <item name="chat">@drawable/ic_menu_chat_dark</item>
+ <item name="chat_send">@drawable/ic_menu_chat_send_dark</item>
+ </style>
+
+ <style name="Theme.DSub.Light.Fullscreen" parent="Theme.DSub.Light">
+ <item name="android:windowFullscreen">true</item>
+ </style>
+ <style name="Theme.DSub.Dark.Fullscreen" parent="Theme.DSub.Dark">
+ <item name="android:windowFullscreen">true</item>
+ </style>
+ <style name="Theme.DSub.Black.Fullscreen" parent="Theme.DSub.Black">
+ <item name="android:windowFullscreen">true</item>
+ </style>
+ <style name="Theme.DSub.Holo.Fullscreen" parent="Theme.DSub.Holo">
+ <item name="android:windowFullscreen">true</item>
+ </style>
+
+ <style name="Widget.DSub.ActionBarStyle.Light" parent="Widget.Sherlock.Light.ActionBar.Solid">
+ <item name="background">@android:color/transparent</item>
+ <item name="android:background">@android:color/transparent</item>
+ <item name="backgroundStacked">@android:color/transparent</item>
+ <item name="android:backgroundStacked">@android:color/transparent</item>
+ </style>
+
+ <style name="Widget.DSub.ActionBarStyle.Dark" parent="Widget.Sherlock.ActionBar.Solid">
+ <item name="background">@android:color/transparent</item>
+ <item name="android:background">@android:color/transparent</item>
+ <item name="backgroundStacked">@android:color/transparent</item>
+ <item name="android:backgroundStacked">@android:color/transparent</item>
+ </style>
+
+ <style name="Widget.DSub.ActionBarStyle.Holo" parent="Widget.Sherlock.ActionBar.Solid">
+ <item name="background">@android:color/transparent</item>
+ <item name="android:background">@android:color/transparent</item>
+ <item name="backgroundStacked">@android:color/transparent</item>
+ <item name="android:backgroundStacked">@android:color/transparent</item>
+ </style>
+</resources>
diff --git a/res/xml/appwidget4x1.xml b/res/xml/appwidget4x1.xml
new file mode 100644
index 00000000..65f47dba
--- /dev/null
+++ b/res/xml/appwidget4x1.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
+ android:minWidth="272dip"
+ android:minHeight="56dip"
+ android:updatePeriodMillis="0"
+ android:resizeMode="horizontal|vertical"
+ android:initialLayout="@layout/appwidget4x1"/> \ No newline at end of file
diff --git a/res/xml/appwidget4x2.xml b/res/xml/appwidget4x2.xml
new file mode 100644
index 00000000..f40204a7
--- /dev/null
+++ b/res/xml/appwidget4x2.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
+ android:minWidth="272dip"
+ android:minHeight="110dip"
+ android:updatePeriodMillis="0"
+ android:resizeMode="horizontal|vertical"
+ android:initialLayout="@layout/appwidget4x2"/> \ No newline at end of file
diff --git a/res/xml/appwidget4x3.xml b/res/xml/appwidget4x3.xml
new file mode 100644
index 00000000..51ae97ed
--- /dev/null
+++ b/res/xml/appwidget4x3.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
+ android:minWidth="272dip"
+ android:minHeight="180dp"
+ android:updatePeriodMillis="0"
+ android:resizeMode="horizontal|vertical"
+ android:initialLayout="@layout/appwidget4x3"/> \ No newline at end of file
diff --git a/res/xml/appwidget4x4.xml b/res/xml/appwidget4x4.xml
new file mode 100644
index 00000000..40956dcf
--- /dev/null
+++ b/res/xml/appwidget4x4.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
+ android:minWidth="272dip"
+ android:minHeight="250dp"
+ android:updatePeriodMillis="0"
+ android:resizeMode="horizontal|vertical"
+ android:initialLayout="@layout/appwidget4x4"/> \ No newline at end of file
diff --git a/res/xml/changelog.xml b/res/xml/changelog.xml
new file mode 100644
index 00000000..7bc0bddc
--- /dev/null
+++ b/res/xml/changelog.xml
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="utf-8"?>
+<changelog></changelog>
diff --git a/res/xml/searchable.xml b/res/xml/searchable.xml
new file mode 100644
index 00000000..a3713aa3
--- /dev/null
+++ b/res/xml/searchable.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<searchable xmlns:android="http://schemas.android.com/apk/res/android"
+ android:label="@string/common.appname"
+ android:hint="@string/search.title"
+ android:voiceSearchMode="showVoiceSearchButton|launchRecognizer"
+ android:voiceLanguageModel="web_search"
+ android:searchSuggestAuthority="github.daneren2005.dsub.provider.DSubSearchProvider"
+ android:searchSuggestSelection=" ?" >
+</searchable> \ No newline at end of file
diff --git a/res/xml/settings.xml b/res/xml/settings.xml
new file mode 100644
index 00000000..eb139b83
--- /dev/null
+++ b/res/xml/settings.xml
@@ -0,0 +1,209 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+ android:title="@string/settings.title">
+
+ <PreferenceCategory
+ android:key="server"
+ android:title="@string/settings.servers_title">
+
+ <Preference
+ android:key="serverAdd"
+ android:title="@string/settings.servers_add"/>
+
+ </PreferenceCategory>
+
+ <PreferenceCategory
+ android:title="@string/settings.appearance_title">
+
+ <ListPreference
+ android:title="@string/settings.theme_title"
+ android:key="theme"
+ android:defaultValue="holo"
+ android:entryValues="@array/themeValues"
+ android:entries="@array/themeNames"/>
+
+ <CheckBoxPreference
+ android:title="@string/settings.track_title"
+ android:summary="@string/settings.track_summary"
+ android:key="displayTrack"
+ android:defaultValue="false"/>
+
+ </PreferenceCategory>
+
+ <PreferenceCategory
+ android:title="@string/settings.video_title">
+
+ <ListPreference
+ android:title="@string/settings.video_player"
+ android:key="videoPlayer"
+ android:defaultValue="raw"
+ android:entryValues="@array/videoPlayerValues"
+ android:entries="@array/videoPlayerNames"/>
+ </PreferenceCategory>
+
+ <PreferenceCategory
+ android:title="@string/settings.network_title">
+
+ <ListPreference
+ android:title="@string/settings.max_bitrate_wifi"
+ android:key="maxBitrateWifi"
+ android:defaultValue="0"
+ android:entryValues="@array/maxBitrateValues"
+ android:entries="@array/maxBitrateNames"/>
+
+ <ListPreference
+ android:title="@string/settings.max_bitrate_mobile"
+ android:key="maxBitrateMobile"
+ android:defaultValue="0"
+ android:entryValues="@array/maxBitrateValues"
+ android:entries="@array/maxBitrateNames"/>
+
+ <ListPreference
+ android:title="@string/settings.max_video_bitrate_wifi"
+ android:key="maxVideoBitrateWifi"
+ android:defaultValue="0"
+ android:entryValues="@array/maxVideoBitrateValues"
+ android:entries="@array/maxVideoBitrateNames"/>
+
+ <ListPreference
+ android:title="@string/settings.max_video_bitrate_mobile"
+ android:key="maxVideoBitrateMobile"
+ android:defaultValue="0"
+ android:entryValues="@array/maxVideoBitrateValues"
+ android:entries="@array/maxVideoBitrateNames"/>
+
+ <CheckBoxPreference
+ android:title="@string/settings.wifi_required_title"
+ android:summary="@string/settings.wifi_required_summary"
+ android:key="wifiRequiredForDownload"
+ android:defaultValue="false"/>
+
+ <ListPreference
+ android:title="@string/settings.network_timeout_title"
+ android:key="networkTimeout"
+ android:defaultValue="15000"
+ android:entryValues="@array/networkTimeoutValues"
+ android:entries="@array/networkTimeoutNames"/>
+
+ </PreferenceCategory>
+
+ <PreferenceCategory
+ android:title="@string/settings.playlist_title">
+
+ <EditTextPreference
+ android:title="@string/settings.buffer_length"
+ android:key="bufferLength"
+ android:defaultValue="5"
+ android:digits="0123456789"/>
+
+ <EditTextPreference
+ android:title="@string/settings.playlist_random_size_title"
+ android:key="randomSize"
+ android:defaultValue="20"
+ android:digits="0123456789"/>
+
+ <ListPreference
+ android:title="@string/settings.temp_loss_title"
+ android:key="tempLoss"
+ android:defaultValue="1"
+ android:entryValues="@array/tempLossValues"
+ android:entries="@array/tempLossNames"/>
+
+ <CheckBoxPreference
+ android:title="@string/settings.persistent_title"
+ android:summary="@string/settings.persistent_summary"
+ android:key="persistentNotification"
+ android:defaultValue="false"/>
+ </PreferenceCategory>
+
+ <PreferenceCategory
+ android:title="@string/settings.cache_title">
+
+ <EditTextPreference
+ android:title="@string/settings.cache_size"
+ android:key="cacheSize"
+ android:defaultValue="2000"
+ android:digits="0123456789"/>
+
+ <EditTextPreference
+ android:title="@string/settings.cache_location"
+ android:key="cacheLocation"/>
+
+ <ListPreference
+ android:title="@string/settings.preload_wifi"
+ android:key="preloadCountWifi"
+ android:defaultValue="3"
+ android:entryValues="@array/preloadCountValues"
+ android:entries="@array/preloadCountNames"/>
+
+ <ListPreference
+ android:title="@string/settings.preload_mobile"
+ android:key="preloadCountMobile"
+ android:defaultValue="3"
+ android:entryValues="@array/preloadCountValues"
+ android:entries="@array/preloadCountNames"/>
+
+ <Preference
+ android:key="clearCache"
+ android:title="@string/settings.cache_clear"
+ android:persistent="false"/>
+ </PreferenceCategory>
+
+ <PreferenceCategory
+ android:title="@string/button_bar.chat">
+
+ <CheckBoxPreference
+ android:title="@string/settings.chat_enabled"
+ android:summary="@string/settings.chat_enabled_summary"
+ android:key="chatEnabled"
+ android:defaultValue="true"/>
+
+ <EditTextPreference
+ android:title="@string/settings.chat_refresh"
+ android:key="chatRefreshRate"
+ android:defaultValue="30"
+ android:digits="0123456789"/>
+ </PreferenceCategory>
+
+ <PreferenceCategory
+ android:title="@string/settings.other_title">
+
+ <CheckBoxPreference
+ android:title="@string/settings.scrobble_title"
+ android:summary="@string/settings.scrobble_summary"
+ android:key="scrobble"
+ android:defaultValue="false"/>
+
+ <CheckBoxPreference
+ android:title="@string/settings.hide_media_title"
+ android:summary="@string/settings.hide_media_summary"
+ android:key="hideMedia"
+ android:defaultValue="false"/>
+
+ <CheckBoxPreference
+ android:title="@string/settings.media_button_title"
+ android:summary="@string/settings.media_button_summary"
+ android:key="mediaButtons"
+ android:defaultValue="true"/>
+
+ <CheckBoxPreference
+ android:title="@string/settings.screen_lit_title"
+ android:summary="@string/settings.screen_lit_summary"
+ android:key="screenLitOnDownload"
+ android:defaultValue="true"/>
+
+ <CheckBoxPreference
+ android:title="@string/settings.gapless_playback"
+ android:summary="@string/settings.gapless_playback_summary"
+ android:key="gaplessPlayback"
+ android:defaultValue="true"/>
+
+ <Preference
+ android:key="clearSearchHistory"
+ android:title="@string/settings.clear_search_history"
+ android:persistent="false"/>
+
+ </PreferenceCategory>
+
+</PreferenceScreen>
diff --git a/src/github/daneren2005/dsub/activity/DownloadActivity.java b/src/github/daneren2005/dsub/activity/DownloadActivity.java
new file mode 100644
index 00000000..bfdc9eb9
--- /dev/null
+++ b/src/github/daneren2005/dsub/activity/DownloadActivity.java
@@ -0,0 +1,96 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.activity;
+
+import github.daneren2005.dsub.R;
+import android.os.Bundle;
+import android.view.MotionEvent;
+import github.daneren2005.dsub.fragments.DownloadFragment;
+import android.app.Dialog;
+import android.view.LayoutInflater;
+import android.widget.EditText;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.util.Log;
+import android.view.View;
+import com.actionbarsherlock.view.MenuItem;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.service.DownloadFile;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import github.daneren2005.dsub.util.SilentBackgroundTask;
+import github.daneren2005.dsub.util.Util;
+import java.util.LinkedList;
+import java.util.List;
+
+public class DownloadActivity extends SubsonicActivity {
+ private static final String TAG = DownloadActivity.class.getSimpleName();
+ private EditText playlistNameView;
+
+ /**
+ * Called when the activity is first created.
+ */
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.download_activity);
+
+ if (findViewById(R.id.download_container) != null && savedInstanceState == null) {
+ currentFragment = new DownloadFragment();
+ currentFragment.setPrimaryFragment(true);
+ getSupportFragmentManager().beginTransaction().add(R.id.download_container, currentFragment, currentFragment.getSupportTag() + "").commit();
+ }
+
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ getSupportActionBar().setHomeButtonEnabled(true);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if(item.getItemId() == android.R.id.home) {
+ Intent i = new Intent();
+ i.setClass(this, MainActivity.class);
+ i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(i);
+ return true;
+ } else {
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent me) {
+ if(currentFragment != null) {
+ return ((DownloadFragment)currentFragment).getGestureDetector().onTouchEvent(me);
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public void onBackPressed() {
+ if(onBackPressedSupport()) {
+ super.onBackPressed();
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/activity/EqualizerActivity.java b/src/github/daneren2005/dsub/activity/EqualizerActivity.java
new file mode 100644
index 00000000..d9605fc5
--- /dev/null
+++ b/src/github/daneren2005/dsub/activity/EqualizerActivity.java
@@ -0,0 +1,278 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2011 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.activity;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import android.app.Activity;
+import android.content.SharedPreferences;
+import android.media.audiofx.Equalizer;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.LinearLayout;
+import android.widget.SeekBar;
+import android.widget.TextView;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.audiofx.EqualizerController;
+import github.daneren2005.dsub.service.DownloadServiceImpl;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.Util;
+
+/**
+ * Equalizer controls.
+ *
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class EqualizerActivity extends Activity {
+ private static final String TAG = EqualizerActivity.class.getSimpleName();
+
+ private static final int MENU_GROUP_PRESET = 100;
+
+ private final Map<Short, SeekBar> bars = new HashMap<Short, SeekBar>();
+ private EqualizerController equalizerController;
+ private Equalizer equalizer;
+ private short masterLevel = 0;
+
+ @Override
+ public void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ setContentView(R.layout.equalizer);
+ equalizerController = DownloadServiceImpl.getInstance().getEqualizerController();
+ equalizer = equalizerController.getEqualizer();
+
+ initEqualizer();
+
+ final View presetButton = findViewById(R.id.equalizer_preset);
+ registerForContextMenu(presetButton);
+ presetButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ presetButton.showContextMenu();
+ }
+ });
+
+ CheckBox enabledCheckBox = (CheckBox) findViewById(R.id.equalizer_enabled);
+ enabledCheckBox.setChecked(equalizer.getEnabled());
+ enabledCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
+ setEqualizerEnabled(b);
+ }
+ });
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ equalizerController.saveSettings();
+
+ if(!equalizer.getEnabled()) {
+ equalizerController.release();
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ equalizerController = DownloadServiceImpl.getInstance().getEqualizerController();
+ equalizer = equalizerController.getEqualizer();
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, view, menuInfo);
+
+ short currentPreset;
+ try {
+ currentPreset = equalizer.getCurrentPreset();
+ } catch (Exception x) {
+ currentPreset = -1;
+ }
+
+ for (short preset = 0; preset < equalizer.getNumberOfPresets(); preset++) {
+ MenuItem menuItem = menu.add(MENU_GROUP_PRESET, preset, preset, equalizer.getPresetName(preset));
+ if (preset == currentPreset) {
+ menuItem.setChecked(true);
+ }
+ }
+ menu.setGroupCheckable(MENU_GROUP_PRESET, true, true);
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem menuItem) {
+ short preset = (short) menuItem.getItemId();
+ equalizer.usePreset(preset);
+ updateBars(false);
+ return true;
+ }
+
+ private void setEqualizerEnabled(boolean enabled) {
+ SharedPreferences prefs = Util.getPreferences(EqualizerActivity.this);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putBoolean(Constants.PREFERENCES_EQUALIZER_ON, enabled);
+ editor.commit();
+ equalizer.setEnabled(enabled);
+ updateBars(true);
+ }
+
+ private void updateBars(boolean changedEnabled) {
+ boolean isEnabled = equalizer.getEnabled();
+ short minEQLevel = equalizer.getBandLevelRange()[0];
+ short maxEQLevel = equalizer.getBandLevelRange()[1];
+ for (Map.Entry<Short, SeekBar> entry : bars.entrySet()) {
+ short band = entry.getKey();
+ SeekBar bar = entry.getValue();
+ bar.setEnabled(isEnabled);
+ if(band >= (short)0) {
+ short setLevel;
+ if(changedEnabled) {
+ setLevel = (short)(equalizer.getBandLevel(band) - masterLevel);
+ bar.setProgress(equalizer.getBandLevel(band) - minEQLevel);
+ } else {
+ bar.setProgress(equalizer.getBandLevel(band) - minEQLevel);
+ setLevel = (short)(equalizer.getBandLevel(band) + masterLevel);
+ }
+ if(setLevel < minEQLevel) {
+ setLevel = minEQLevel;
+ } else if(setLevel > maxEQLevel) {
+ setLevel = maxEQLevel;
+ }
+ equalizer.setBandLevel(band, setLevel);
+ } else if(!isEnabled) {
+ bar.setProgress(-minEQLevel);
+ }
+ }
+
+ if(!isEnabled) {
+ masterLevel = 0;
+ SharedPreferences prefs = Util.getPreferences(EqualizerActivity.this);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putInt(Constants.PREFERENCES_EQUALIZER_SETTINGS, masterLevel);
+ editor.commit();
+ }
+ }
+
+ private void initEqualizer() {
+ LinearLayout layout = (LinearLayout) findViewById(R.id.equalizer_layout);
+
+ final short minEQLevel = equalizer.getBandLevelRange()[0];
+ final short maxEQLevel = equalizer.getBandLevelRange()[1];
+
+ // Setup Pregain
+ SharedPreferences prefs = Util.getPreferences(this);
+ masterLevel = (short)prefs.getInt(Constants.PREFERENCES_EQUALIZER_SETTINGS, 0);
+ initPregain(layout, minEQLevel, maxEQLevel);
+
+ for (short i = 0; i < equalizer.getNumberOfBands(); i++) {
+ final short band = i;
+
+ View bandBar = LayoutInflater.from(this).inflate(R.layout.equalizer_bar, null);
+ TextView freqTextView = (TextView) bandBar.findViewById(R.id.equalizer_frequency);
+ final TextView levelTextView = (TextView) bandBar.findViewById(R.id.equalizer_level);
+ SeekBar bar = (SeekBar) bandBar.findViewById(R.id.equalizer_bar);
+
+ freqTextView.setText((equalizer.getCenterFreq(band) / 1000) + " Hz");
+
+ bars.put(band, bar);
+ bar.setMax(maxEQLevel - minEQLevel);
+ short level = equalizer.getBandLevel(band);
+ if(equalizer.getEnabled()) {
+ level = (short) (level - masterLevel);
+ }
+ bar.setProgress(level - minEQLevel);
+ bar.setEnabled(equalizer.getEnabled());
+ updateLevelText(levelTextView, level);
+
+ bar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ short level = (short) (progress + minEQLevel);
+ if (fromUser) {
+ equalizer.setBandLevel(band, (short)(level + masterLevel));
+ }
+ updateLevelText(levelTextView, level);
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ }
+ });
+ layout.addView(bandBar);
+ }
+ }
+
+ private void initPregain(LinearLayout layout, final short minEQLevel, final short maxEQLevel) {
+ View bandBar = LayoutInflater.from(this).inflate(R.layout.equalizer_bar, null);
+ TextView freqTextView = (TextView) bandBar.findViewById(R.id.equalizer_frequency);
+ final TextView levelTextView = (TextView) bandBar.findViewById(R.id.equalizer_level);
+ SeekBar bar = (SeekBar) bandBar.findViewById(R.id.equalizer_bar);
+
+ freqTextView.setText("Master");
+
+ bars.put((short)-1, bar);
+ bar.setMax(maxEQLevel - minEQLevel);
+ bar.setProgress(masterLevel - minEQLevel);
+ bar.setEnabled(equalizer.getEnabled());
+ updateLevelText(levelTextView, masterLevel);
+
+ bar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ masterLevel = (short) (progress + minEQLevel);
+ if (fromUser) {
+ SharedPreferences prefs = Util.getPreferences(EqualizerActivity.this);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putInt(Constants.PREFERENCES_EQUALIZER_SETTINGS, masterLevel);
+ editor.commit();
+ for (short i = 0; i < equalizer.getNumberOfBands(); i++) {
+ short level = (short) ((bars.get(i).getProgress() + minEQLevel) + masterLevel);
+ equalizer.setBandLevel(i, level);
+ }
+ }
+ updateLevelText(levelTextView, masterLevel);
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ }
+ });
+ layout.addView(bandBar);
+ }
+
+ private void updateLevelText(TextView levelTextView, short level) {
+ levelTextView.setText((level > 0 ? "+" : "") + level / 100 + " dB");
+ }
+
+}
diff --git a/src/github/daneren2005/dsub/activity/HelpActivity.java b/src/github/daneren2005/dsub/activity/HelpActivity.java
new file mode 100644
index 00000000..6dc516bf
--- /dev/null
+++ b/src/github/daneren2005/dsub/activity/HelpActivity.java
@@ -0,0 +1,117 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+
+package github.daneren2005.dsub.activity;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.Window;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import android.widget.Button;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.util.Util;
+
+/**
+ * An HTML-based help screen with Back and Done buttons at the bottom.
+ *
+ * @author Sindre Mehus
+ */
+public final class HelpActivity extends Activity {
+
+ private WebView webView;
+ private Button backButton;
+
+ @Override
+ protected void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ getWindow().requestFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
+
+ setContentView(R.layout.help);
+
+ webView = (WebView) findViewById(R.id.help_contents);
+ webView.getSettings().setJavaScriptEnabled(true);
+ webView.setWebViewClient(new HelpClient());
+ if (bundle != null) {
+ webView.restoreState(bundle);
+ } else {
+ webView.loadUrl(getResources().getString(R.string.help_url));
+ }
+
+ backButton = (Button) findViewById(R.id.help_back);
+ backButton.setOnClickListener(new Button.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ webView.goBack();
+ }
+ });
+
+ Button doneButton = (Button) findViewById(R.id.help_close);
+ doneButton.setOnClickListener(new Button.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ finish();
+ }
+ });
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle state) {
+ webView.saveState(state);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ if (webView.canGoBack()) {
+ webView.goBack();
+ return true;
+ }
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+ private final class HelpClient extends WebViewClient {
+ @Override
+ public void onLoadResource(WebView webView, String url) {
+ setProgressBarIndeterminateVisibility(true);
+ setTitle(getResources().getString(R.string.help_loading));
+ super.onLoadResource(webView, url);
+ }
+
+ @Override
+ public void onPageFinished(WebView view, String url) {
+ setProgressBarIndeterminateVisibility(false);
+ setTitle(view.getTitle());
+ backButton.setEnabled(view.canGoBack());
+ }
+
+ @Override
+ public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
+ Util.toast(HelpActivity.this, description);
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/activity/MainActivity.java b/src/github/daneren2005/dsub/activity/MainActivity.java
new file mode 100644
index 00000000..c077c87c
--- /dev/null
+++ b/src/github/daneren2005/dsub/activity/MainActivity.java
@@ -0,0 +1,320 @@
+package github.daneren2005.dsub.activity;
+
+import android.app.AlertDialog;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.Handler;
+import android.preference.PreferenceManager;
+import com.actionbarsherlock.app.ActionBar;
+import com.actionbarsherlock.view.Menu;
+import android.support.v4.view.ViewPager;
+import android.util.Log;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.TextView;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.PlayerState;
+import github.daneren2005.dsub.fragments.ChatFragment;
+import github.daneren2005.dsub.fragments.MainFragment;
+import github.daneren2005.dsub.fragments.SelectArtistFragment;
+import github.daneren2005.dsub.fragments.SelectDirectoryFragment;
+import github.daneren2005.dsub.fragments.SelectPlaylistFragment;
+import github.daneren2005.dsub.fragments.SelectPodcastsFragment;
+import github.daneren2005.dsub.fragments.SubsonicFragment;
+import github.daneren2005.dsub.service.DownloadFile;
+import github.daneren2005.dsub.service.DownloadServiceImpl;
+import github.daneren2005.dsub.updates.Updater;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.SilentBackgroundTask;
+import github.daneren2005.dsub.util.Util;
+import github.daneren2005.dsub.view.ChangeLog;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+public class MainActivity extends SubsonicActivity {
+ private static final String TAG = MainActivity.class.getSimpleName();
+ private static boolean infoDialogDisplayed;
+ private ScheduledExecutorService executorService;
+ private View bottomBar;
+ private View coverArtView;
+ private TextView trackView;
+ private TextView artistView;
+ private ImageButton startButton;
+ private long lastBackPressTime = 0;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (getIntent().hasExtra(Constants.INTENT_EXTRA_NAME_EXIT)) {
+ stopService(new Intent(this, DownloadServiceImpl.class));
+ finish();
+ } else if(getIntent().hasExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD)) {
+ getIntent().removeExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD);
+ Intent intent = new Intent();
+ intent.setClass(this, DownloadActivity.class);
+ startActivity(intent);
+ }
+ setContentView(R.layout.main);
+ loadSettings();
+
+ bottomBar = findViewById(R.id.bottom_bar);
+ bottomBar.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ Intent intent = new Intent();
+ intent.setClass(v.getContext(), DownloadActivity.class);
+ startActivity(intent);
+ }
+ });
+ coverArtView = bottomBar.findViewById(R.id.album_art);
+ trackView = (TextView) bottomBar.findViewById(R.id.track_name);
+ artistView = (TextView) bottomBar.findViewById(R.id.artist_name);
+
+ ImageButton previousButton = (ImageButton) findViewById(R.id.download_previous);
+ previousButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ new SilentBackgroundTask<Void>(MainActivity.this) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ if(getDownloadService() == null) {
+ return null;
+ }
+
+ getDownloadService().previous();
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ update();
+ }
+ }.execute();
+ }
+ });
+
+ startButton = (ImageButton) findViewById(R.id.download_start);
+ startButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ new SilentBackgroundTask<Void>(MainActivity.this) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ PlayerState state = getDownloadService().getPlayerState();
+ if(state == PlayerState.STARTED) {
+ getDownloadService().pause();
+ } else {
+ getDownloadService().start();
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ update();
+ }
+ }.execute();
+ }
+ });
+
+ ImageButton nextButton = (ImageButton) findViewById(R.id.download_next);
+ nextButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ new SilentBackgroundTask<Void>(MainActivity.this) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ if(getDownloadService() == null) {
+ return null;
+ }
+
+ if (getDownloadService().getCurrentPlayingIndex() < getDownloadService().size() - 1) {
+ getDownloadService().next();
+ }
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ update();
+ }
+ }.execute();
+ }
+ });
+
+ viewPager = (ViewPager) findViewById(R.id.pager);
+ viewPager.setOffscreenPageLimit(4);
+ pagerAdapter = new TabPagerAdapter(this, viewPager);
+ viewPager.setAdapter(pagerAdapter);
+ viewPager.setOnPageChangeListener(pagerAdapter);
+
+ addTab(R.string.button_bar_home, MainFragment.class, null);
+ addTab(R.string.button_bar_browse, SelectArtistFragment.class, null);
+ addTab(R.string.button_bar_playlists, SelectPlaylistFragment.class, null);
+ addTab(R.string.button_bar_podcasts, SelectPodcastsFragment.class, null);
+ SharedPreferences prefs = Util.getPreferences(this);
+ if(prefs.getBoolean(Constants.PREFERENCES_KEY_CHAT_ENABLED, true)) {
+ addTab(R.string.button_bar_chat, ChatFragment.class, null);
+ }
+
+ getSupportActionBar().setDisplayHomeAsUpEnabled(false);
+ getSupportActionBar().setHomeButtonEnabled(false);
+ getSupportActionBar().setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
+ }
+
+ @Override
+ protected void onPostCreate(Bundle bundle) {
+ super.onPostCreate(bundle);
+
+ showInfoDialog();
+ checkUpdates();
+
+ ChangeLog changeLog = new ChangeLog(this, Util.getPreferences(this));
+ if(changeLog.isFirstRun()) {
+ changeLog.getLogDialog().show();
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ final Handler handler = new Handler();
+ Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ update();
+ }
+ });
+ }
+ };
+
+ if(getIntent().hasExtra(Constants.INTENT_EXTRA_VIEW_ALBUM)) {
+ viewPager.setCurrentItem(1);
+
+ int fragmentID = R.id.select_artist_layout;
+ if(getIntent().hasExtra(Constants.INTENT_EXTRA_NAME_PARENT_ID)) {
+ SubsonicFragment fragment = new SelectDirectoryFragment();
+ Bundle args = new Bundle();
+ args.putString(Constants.INTENT_EXTRA_NAME_ID, getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_PARENT_ID));
+ args.putString(Constants.INTENT_EXTRA_NAME_NAME, getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_PARENT_NAME));
+ fragment.setArguments(args);
+
+ pagerAdapter.queueFragment(fragment, R.id.select_artist_layout);
+ fragmentID = fragment.getRootId();
+ }
+
+ SubsonicFragment fragment = new SelectDirectoryFragment();
+ Bundle args = new Bundle();
+ args.putString(Constants.INTENT_EXTRA_NAME_ID, getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_ID));
+ args.putString(Constants.INTENT_EXTRA_NAME_NAME, getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_NAME));
+ fragment.setArguments(args);
+
+ pagerAdapter.queueFragment(fragment, fragmentID);
+ getIntent().removeExtra(Constants.INTENT_EXTRA_VIEW_ALBUM);
+ }
+
+ executorService = Executors.newSingleThreadScheduledExecutor();
+ executorService.scheduleWithFixedDelay(runnable, 0L, 1000L, TimeUnit.MILLISECONDS);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ executorService.shutdown();
+ }
+
+ @Override
+ public void onBackPressed() {
+ if(onBackPressedSupport()) {
+ if(lastBackPressTime < (System.currentTimeMillis() - 4000)) {
+ lastBackPressTime = System.currentTimeMillis();
+ Util.toast(this, R.string.main_back_confirm);
+ } else {
+ finish();
+ }
+ }
+ }
+
+ private void update() {
+ if (getDownloadService() == null) {
+ return;
+ }
+
+ DownloadFile current = getDownloadService().getCurrentPlaying();
+ if(current == null) {
+ trackView.setText("Title");
+ artistView.setText("Artist");
+ getImageLoader().loadImage(coverArtView, null, false, false);
+ return;
+ }
+
+ MusicDirectory.Entry song = current.getSong();
+ trackView.setText(song.getTitle());
+ artistView.setText(song.getArtist());
+ getImageLoader().loadImage(coverArtView, song, false, false);
+ int[] attrs = new int[] {(getDownloadService().getPlayerState() == PlayerState.STARTED) ? R.attr.media_button_pause : R.attr.media_button_start};
+ TypedArray typedArray = this.obtainStyledAttributes(attrs);
+ Drawable drawable = typedArray.getDrawable(0);
+ startButton.setImageDrawable(drawable);
+ typedArray.recycle();
+ }
+
+ public void checkUpdates() {
+ try {
+ String version = getPackageManager().getPackageInfo(getPackageName(), 0).versionName;
+ int ver = Integer.parseInt(version.replace(".", ""));
+ Updater updater = new Updater(ver);
+ updater.checkUpdates(this);
+ }
+ catch(Exception e) {
+
+ }
+ }
+
+ private void loadSettings() {
+ PreferenceManager.setDefaultValues(this, R.xml.settings, false);
+ SharedPreferences prefs = Util.getPreferences(this);
+ if (!prefs.contains(Constants.PREFERENCES_KEY_CACHE_LOCATION)) {
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putString(Constants.PREFERENCES_KEY_CACHE_LOCATION, FileUtil.getDefaultMusicDirectory().getPath());
+ editor.commit();
+ }
+
+ if (!prefs.contains(Constants.PREFERENCES_KEY_OFFLINE)) {
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putBoolean(Constants.PREFERENCES_KEY_OFFLINE, false);
+
+ editor.putString(Constants.PREFERENCES_KEY_SERVER_NAME + 1, "Demo Server");
+ editor.putString(Constants.PREFERENCES_KEY_SERVER_URL + 1, "http://demo.subsonic.org");
+ editor.putString(Constants.PREFERENCES_KEY_USERNAME + 1, "android-guest");
+ editor.putString(Constants.PREFERENCES_KEY_PASSWORD + 1, "guest");
+ editor.putInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1);
+ editor.commit();
+ }
+ if(!prefs.contains(Constants.PREFERENCES_KEY_SERVER_COUNT)) {
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putInt(Constants.PREFERENCES_KEY_SERVER_COUNT, 3);
+ editor.commit();
+ }
+ }
+
+ private void showInfoDialog() {
+ if (!infoDialogDisplayed) {
+ infoDialogDisplayed = true;
+ Log.i(TAG, Util.getRestUrl(this, null));
+ if (Util.getRestUrl(this, null).contains("demo.subsonic.org")) {
+ Util.info(this, R.string.main_welcome_title, R.string.main_welcome_text);
+ }
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/activity/QueryReceiverActivity.java b/src/github/daneren2005/dsub/activity/QueryReceiverActivity.java
new file mode 100644
index 00000000..15d0c6a6
--- /dev/null
+++ b/src/github/daneren2005/dsub/activity/QueryReceiverActivity.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 2009 (C) Sindre Mehus
+ */
+
+package github.daneren2005.dsub.activity;
+
+import android.app.Activity;
+import android.app.SearchManager;
+import android.content.Intent;
+import android.os.Bundle;
+import android.provider.SearchRecentSuggestions;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.Util;
+import github.daneren2005.dsub.provider.DSubSearchProvider;
+
+/**
+ * Receives search queries and forwards to the SelectAlbumActivity.
+ *
+ * @author Sindre Mehus
+ */
+public class QueryReceiverActivity extends Activity {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ String query = getIntent().getStringExtra(SearchManager.QUERY);
+
+ if (query != null) {
+ SearchRecentSuggestions suggestions = new SearchRecentSuggestions(this, DSubSearchProvider.AUTHORITY,
+ DSubSearchProvider.MODE);
+ suggestions.saveRecentQuery(query, null);
+
+ Intent intent = new Intent(QueryReceiverActivity.this, SearchActivity.class);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_QUERY, query);
+ Util.startActivityWithoutTransition(QueryReceiverActivity.this, intent);
+ }
+ finish();
+ Util.disablePendingTransition(this);
+ }
+} \ No newline at end of file
diff --git a/src/github/daneren2005/dsub/activity/SearchActivity.java b/src/github/daneren2005/dsub/activity/SearchActivity.java
new file mode 100644
index 00000000..aeddcf4f
--- /dev/null
+++ b/src/github/daneren2005/dsub/activity/SearchActivity.java
@@ -0,0 +1,91 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+
+package github.daneren2005.dsub.activity;
+
+import github.daneren2005.dsub.R;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import github.daneren2005.dsub.fragments.SearchFragment;
+import github.daneren2005.dsub.util.Constants;
+import com.actionbarsherlock.view.MenuItem;
+
+public class SearchActivity extends SubsonicActivity {
+ private static final String TAG = SearchActivity.class.getSimpleName();
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.download_activity);
+
+ if (findViewById(R.id.download_container) != null && savedInstanceState == null) {
+ currentFragment = new SearchFragment();
+ currentFragment.setPrimaryFragment(true);
+ getSupportFragmentManager().beginTransaction().add(R.id.download_container, currentFragment, currentFragment.getSupportTag() + "").commit();
+ }
+
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ getSupportActionBar().setHomeButtonEnabled(true);
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+
+ if(currentFragment != null && currentFragment instanceof SearchFragment) {
+ String query = intent.getStringExtra(Constants.INTENT_EXTRA_NAME_QUERY);
+ boolean autoplay = intent.getBooleanExtra(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false);
+ boolean requestsearch = intent.getBooleanExtra(Constants.INTENT_EXTRA_REQUEST_SEARCH, false);
+
+ if (query != null) {
+ ((SearchFragment)currentFragment).search(query, autoplay);
+ } else {
+ ((SearchFragment)currentFragment).populateList();
+ if (requestsearch) {
+ onSearchRequested();
+ }
+ }
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if(item.getItemId() == android.R.id.home) {
+ Intent i = new Intent();
+ i.setClass(this, MainActivity.class);
+ i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(i);
+ return true;
+ } else {
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ public void onSupportNewIntent(Intent intent) {
+ onNewIntent(intent);
+ }
+
+ @Override
+ public void onBackPressed() {
+ if(onBackPressedSupport()) {
+ super.onBackPressed();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/github/daneren2005/dsub/activity/SettingsActivity.java b/src/github/daneren2005/dsub/activity/SettingsActivity.java
new file mode 100644
index 00000000..fc56281e
--- /dev/null
+++ b/src/github/daneren2005/dsub/activity/SettingsActivity.java
@@ -0,0 +1,539 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.activity;
+
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.Bundle;
+import android.preference.EditTextPreference;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.PreferenceActivity;
+import android.preference.PreferenceCategory;
+import android.preference.PreferenceScreen;
+import android.provider.SearchRecentSuggestions;
+import android.text.InputType;
+import android.util.Log;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.provider.DSubSearchProvider;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.service.DownloadServiceImpl;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.LoadingTask;
+import github.daneren2005.dsub.view.ErrorDialog;
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.Util;
+
+import java.io.File;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public class SettingsActivity extends PreferenceActivity implements SharedPreferences.OnSharedPreferenceChangeListener {
+ private static final String TAG = SettingsActivity.class.getSimpleName();
+ private final Map<String, ServerSettings> serverSettings = new LinkedHashMap<String, ServerSettings>();
+ private boolean testingConnection;
+ private ListPreference theme;
+ private ListPreference maxBitrateWifi;
+ private ListPreference maxBitrateMobile;
+ private ListPreference maxVideoBitrateWifi;
+ private ListPreference maxVideoBitrateMobile;
+ private ListPreference networkTimeout;
+ private EditTextPreference cacheSize;
+ private EditTextPreference cacheLocation;
+ private ListPreference preloadCountWifi;
+ private ListPreference preloadCountMobile;
+ private EditTextPreference randomSize;
+ private ListPreference tempLoss;
+ private EditTextPreference bufferLength;
+ private Preference addServerPreference;
+ private PreferenceCategory serversCategory;
+ private EditTextPreference chatRefreshRate;
+ private ListPreference videoPlayer;
+
+ private int serverCount = 3;
+ private SharedPreferences settings;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ applyTheme();
+ super.onCreate(savedInstanceState);
+ addPreferencesFromResource(R.xml.settings);
+
+ theme = (ListPreference) findPreference(Constants.PREFERENCES_KEY_THEME);
+ maxBitrateWifi = (ListPreference) findPreference(Constants.PREFERENCES_KEY_MAX_BITRATE_WIFI);
+ maxBitrateMobile = (ListPreference) findPreference(Constants.PREFERENCES_KEY_MAX_BITRATE_MOBILE);
+ maxVideoBitrateWifi = (ListPreference) findPreference(Constants.PREFERENCES_KEY_MAX_VIDEO_BITRATE_WIFI);
+ maxVideoBitrateMobile = (ListPreference) findPreference(Constants.PREFERENCES_KEY_MAX_VIDEO_BITRATE_MOBILE);
+ networkTimeout = (ListPreference) findPreference(Constants.PREFERENCES_KEY_NETWORK_TIMEOUT);
+ cacheSize = (EditTextPreference) findPreference(Constants.PREFERENCES_KEY_CACHE_SIZE);
+ cacheLocation = (EditTextPreference) findPreference(Constants.PREFERENCES_KEY_CACHE_LOCATION);
+ preloadCountWifi = (ListPreference) findPreference(Constants.PREFERENCES_KEY_PRELOAD_COUNT_WIFI);
+ preloadCountMobile = (ListPreference) findPreference(Constants.PREFERENCES_KEY_PRELOAD_COUNT_MOBILE);
+ randomSize = (EditTextPreference) findPreference(Constants.PREFERENCES_KEY_RANDOM_SIZE);
+ tempLoss = (ListPreference) findPreference(Constants.PREFERENCES_KEY_TEMP_LOSS);
+ bufferLength = (EditTextPreference) findPreference(Constants.PREFERENCES_KEY_BUFFER_LENGTH);
+ addServerPreference = (Preference) findPreference(Constants.PREFERENCES_KEY_SERVER_ADD);
+ serversCategory = (PreferenceCategory) findPreference(Constants.PREFERENCES_KEY_SERVER_KEY);
+ chatRefreshRate = (EditTextPreference) findPreference(Constants.PREFERENCES_KEY_CHAT_REFRESH);
+ videoPlayer = (ListPreference) findPreference(Constants.PREFERENCES_KEY_VIDEO_PLAYER);
+
+ settings = Util.getPreferences(this);
+ serverCount = settings.getInt(Constants.PREFERENCES_KEY_SERVER_COUNT, 3);
+
+ findPreference("clearSearchHistory").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ SearchRecentSuggestions suggestions = new SearchRecentSuggestions(SettingsActivity.this, DSubSearchProvider.AUTHORITY, DSubSearchProvider.MODE);
+ suggestions.clearHistory();
+ Util.toast(SettingsActivity.this, R.string.settings_search_history_cleared);
+ return false;
+ }
+ });
+ findPreference("clearCache").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ Util.confirmDialog(SettingsActivity.this, R.string.common_delete, "cache", new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ new LoadingTask<Void>(SettingsActivity.this, false) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ FileUtil.deleteMusicDirectory(SettingsActivity.this);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ Util.toast(SettingsActivity.this, R.string.settings_cache_clear_complete);
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ Util.toast(SettingsActivity.this, getErrorMessage(error), false);
+ }
+ }.execute();
+ }
+ });
+ return false;
+ }
+ });
+
+ addServerPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ serverCount++;
+ String instance = String.valueOf(serverCount);
+
+ Preference addServerPreference = findPreference(Constants.PREFERENCES_KEY_SERVER_ADD);
+ serversCategory.removePreference(addServerPreference);
+ serversCategory.addPreference(addServer(serverCount));
+ serversCategory.addPreference(addServerPreference);
+
+ SharedPreferences.Editor editor = settings.edit();
+ editor.putInt(Constants.PREFERENCES_KEY_SERVER_COUNT, serverCount);
+ editor.commit();
+
+ serverSettings.put(instance, new ServerSettings(instance));
+
+ return true;
+ }
+ });
+
+ serversCategory.removePreference(addServerPreference);
+ for (int i = 1; i <= serverCount; i++) {
+ String instance = String.valueOf(i);
+ serversCategory.addPreference(addServer(i));
+ serverSettings.put(instance, new ServerSettings(instance));
+ }
+ serversCategory.addPreference(addServerPreference);
+
+ SharedPreferences prefs = Util.getPreferences(this);
+ prefs.registerOnSharedPreferenceChangeListener(this);
+
+ update();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+
+ SharedPreferences prefs = Util.getPreferences(this);
+ prefs.unregisterOnSharedPreferenceChangeListener(this);
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+ Log.d(TAG, "Preference changed: " + key);
+ update();
+
+ if (Constants.PREFERENCES_KEY_HIDE_MEDIA.equals(key)) {
+ setHideMedia(sharedPreferences.getBoolean(key, false));
+ }
+ else if (Constants.PREFERENCES_KEY_MEDIA_BUTTONS.equals(key)) {
+ setMediaButtonsEnabled(sharedPreferences.getBoolean(key, true));
+ }
+ else if (Constants.PREFERENCES_KEY_CACHE_LOCATION.equals(key)) {
+ setCacheLocation(sharedPreferences.getString(key, ""));
+ }
+ else if (Constants.PREFERENCES_KEY_SLEEP_TIMER_DURATION.equals(key)){
+ DownloadService downloadService = DownloadServiceImpl.getInstance();
+ downloadService.setSleepTimerDuration(Integer.parseInt(sharedPreferences.getString(key, "60")));
+ }
+
+ scheduleBackup();
+ }
+
+ private void scheduleBackup() {
+ try {
+ Class managerClass = Class.forName("android.app.backup.BackupManager");
+ Constructor managerConstructor = managerClass.getConstructor(Context.class);
+ Object manager = managerConstructor.newInstance(this);
+ Method m = managerClass.getMethod("dataChanged");
+ m.invoke(manager);
+ Log.d(TAG, "Backup requested");
+ } catch(ClassNotFoundException e) {
+ Log.d(TAG, "No backup manager found");
+ } catch(Throwable t) {
+ Log.d(TAG, "Scheduling backup failed " + t);
+ t.printStackTrace();
+ }
+ }
+
+ private void update() {
+ if (testingConnection) {
+ return;
+ }
+
+ theme.setSummary(theme.getEntry());
+ maxBitrateWifi.setSummary(maxBitrateWifi.getEntry());
+ maxBitrateMobile.setSummary(maxBitrateMobile.getEntry());
+ maxVideoBitrateWifi.setSummary(maxVideoBitrateWifi.getEntry());
+ maxVideoBitrateMobile.setSummary(maxVideoBitrateMobile.getEntry());
+ networkTimeout.setSummary(networkTimeout.getEntry());
+ cacheSize.setSummary(cacheSize.getText());
+ cacheLocation.setSummary(cacheLocation.getText());
+ preloadCountWifi.setSummary(preloadCountWifi.getEntry());
+ preloadCountMobile.setSummary(preloadCountMobile.getEntry());
+ randomSize.setSummary(randomSize.getText());
+ tempLoss.setSummary(tempLoss.getEntry());
+ bufferLength.setSummary(bufferLength.getText() + " seconds");
+ chatRefreshRate.setSummary(chatRefreshRate.getText());
+ videoPlayer.setSummary(videoPlayer.getEntry());
+ for (ServerSettings ss : serverSettings.values()) {
+ ss.update();
+ }
+ }
+
+ private PreferenceScreen addServer(final int instance) {
+ final PreferenceScreen screen = getPreferenceManager().createPreferenceScreen(this);
+ screen.setTitle(R.string.settings_server_unused);
+ screen.setKey(Constants.PREFERENCES_KEY_SERVER_KEY + instance);
+
+ final EditTextPreference serverNamePreference = new EditTextPreference(this);
+ serverNamePreference.setKey(Constants.PREFERENCES_KEY_SERVER_NAME + instance);
+ serverNamePreference.setDefaultValue(getResources().getString(R.string.settings_server_unused));
+ serverNamePreference.setTitle(R.string.settings_server_name);
+
+ if (serverNamePreference.getText() == null) {
+ serverNamePreference.setText(getResources().getString(R.string.settings_server_unused));
+ }
+
+ serverNamePreference.setSummary(serverNamePreference.getText());
+
+ final EditTextPreference serverUrlPreference = new EditTextPreference(this);
+ serverUrlPreference.setKey(Constants.PREFERENCES_KEY_SERVER_URL + instance);
+ serverUrlPreference.getEditText().setInputType(InputType.TYPE_TEXT_VARIATION_URI);
+ serverUrlPreference.setDefaultValue("http://yourhost");
+ serverUrlPreference.setTitle(R.string.settings_server_address);
+
+ if (serverUrlPreference.getText() == null) {
+ serverUrlPreference.setText("http://yourhost");
+ }
+
+ serverUrlPreference.setSummary(serverUrlPreference.getText());
+
+ screen.setSummary(serverUrlPreference.getText());
+
+ final EditTextPreference serverUsernamePreference = new EditTextPreference(this);
+ serverUsernamePreference.setKey(Constants.PREFERENCES_KEY_USERNAME + instance);
+ serverUsernamePreference.setTitle(R.string.settings_server_username);
+
+ final EditTextPreference serverPasswordPreference = new EditTextPreference(this);
+ serverPasswordPreference.setKey(Constants.PREFERENCES_KEY_PASSWORD + instance);
+ serverPasswordPreference.getEditText().setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
+ serverPasswordPreference.setSummary("***");
+ serverPasswordPreference.setTitle(R.string.settings_server_password);
+
+ final Preference serverOpenBrowser = new Preference(this);
+ serverOpenBrowser.setKey(Constants.PREFERENCES_KEY_OPEN_BROWSER);
+ serverOpenBrowser.setPersistent(false);
+ serverOpenBrowser.setTitle(R.string.settings_server_open_browser);
+ serverOpenBrowser.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ openInBrowser(instance);
+ return true;
+ }
+ });
+
+ Preference serverRemoveServerPreference = new Preference(this);
+ serverRemoveServerPreference.setKey(Constants.PREFERENCES_KEY_SERVER_REMOVE + instance);
+ serverRemoveServerPreference.setPersistent(false);
+ serverRemoveServerPreference.setTitle(R.string.settings_servers_remove);
+
+ serverRemoveServerPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ Util.confirmDialog(SettingsActivity.this, R.string.common_delete, screen.getTitle().toString(), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ // Reset values to null so when we ask for them again they are new
+ serverNamePreference.setText(null);
+ serverUrlPreference.setText(null);
+ serverUsernamePreference.setText(null);
+ serverPasswordPreference.setText(null);
+
+ int activeServer = Util.getActiveServer(SettingsActivity.this);
+ for (int i = instance; i <= serverCount; i++) {
+ Util.removeInstanceName(SettingsActivity.this, i, activeServer);
+ }
+
+ serverCount--;
+ SharedPreferences.Editor editor = settings.edit();
+ editor.putInt(Constants.PREFERENCES_KEY_SERVER_COUNT, serverCount);
+ editor.commit();
+
+ serversCategory.removePreference(screen);
+ screen.getDialog().dismiss();
+ }
+ });
+
+ return true;
+ }
+ });
+
+ Preference serverTestConnectionPreference = new Preference(this);
+ serverTestConnectionPreference.setKey(Constants.PREFERENCES_KEY_TEST_CONNECTION + instance);
+ serverTestConnectionPreference.setPersistent(false);
+ serverTestConnectionPreference.setTitle(R.string.settings_test_connection_title);
+ serverTestConnectionPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ testConnection(instance);
+ return false;
+ }
+ });
+
+ screen.addPreference(serverNamePreference);
+ screen.addPreference(serverUrlPreference);
+ screen.addPreference(serverUsernamePreference);
+ screen.addPreference(serverPasswordPreference);
+ screen.addPreference(serverRemoveServerPreference);
+ screen.addPreference(serverTestConnectionPreference);
+ screen.addPreference(serverOpenBrowser);
+
+ return screen;
+ }
+
+ private void applyTheme() {
+ String activeTheme = Util.getTheme(this);
+ if ("dark".equals(activeTheme)) {
+ setTheme(R.style.Theme_DSub_Dark);
+ } else if ("black".equals(activeTheme)) {
+ setTheme(R.style.Theme_DSub_Black);
+ } else if ("light".equals(activeTheme)) {
+ setTheme(R.style.Theme_DSub_Light);
+ } else if ("dark_fullscreen".equals(activeTheme)) {
+ setTheme(R.style.Theme_DSub_Dark_Fullscreen);
+ } else if ("black_fullscreen".equals(activeTheme)) {
+ setTheme(R.style.Theme_DSub_Black_Fullscreen);
+ } else if ("light_fullscreen".equals(activeTheme)) {
+ setTheme(R.style.Theme_DSub_Light_Fullscreen);
+ } else if("holo".equals(activeTheme)) {
+ setTheme(R.style.Theme_DSub_Holo);
+ } else if("holo_fullscreen".equals(activeTheme)) {
+ setTheme(R.style.Theme_DSub_Holo_Fullscreen);
+ }else {
+ setTheme(R.style.Theme_DSub_Holo);
+ }
+ }
+
+ private void setHideMedia(boolean hide) {
+ File nomediaDir = new File(FileUtil.getSubsonicDirectory(), ".nomedia");
+ if (hide && !nomediaDir.exists()) {
+ try {
+ if (!nomediaDir.createNewFile()) {
+ Log.w(TAG, "Failed to create " + nomediaDir);
+ }
+ } catch(Exception e) {
+ Log.w(TAG, "Failed to create " + nomediaDir);
+ }
+ } else if (nomediaDir.exists()) {
+ if (!nomediaDir.delete()) {
+ Log.w(TAG, "Failed to delete " + nomediaDir);
+ }
+ }
+ Util.toast(this, R.string.settings_hide_media_toast, false);
+ }
+
+ private void setMediaButtonsEnabled(boolean enabled) {
+ if (enabled) {
+ Util.registerMediaButtonEventReceiver(this);
+ } else {
+ Util.unregisterMediaButtonEventReceiver(this);
+ }
+ }
+
+ private void setCacheLocation(String path) {
+ File dir = new File(path);
+ if (!FileUtil.ensureDirectoryExistsAndIsReadWritable(dir)) {
+ Util.toast(this, R.string.settings_cache_location_error, false);
+
+ // Reset it to the default.
+ String defaultPath = FileUtil.getDefaultMusicDirectory().getPath();
+ if (!defaultPath.equals(path)) {
+ SharedPreferences prefs = Util.getPreferences(this);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putString(Constants.PREFERENCES_KEY_CACHE_LOCATION, defaultPath);
+ editor.commit();
+ cacheLocation.setSummary(defaultPath);
+ cacheLocation.setText(defaultPath);
+ }
+
+ // Clear download queue.
+ DownloadService downloadService = DownloadServiceImpl.getInstance();
+ downloadService.clear();
+ }
+ }
+
+ private void testConnection(final int instance) {
+ LoadingTask<Boolean> task = new LoadingTask<Boolean>(this) {
+ private int previousInstance;
+
+ @Override
+ protected Boolean doInBackground() throws Throwable {
+ updateProgress(R.string.settings_testing_connection);
+
+ previousInstance = Util.getActiveServer(SettingsActivity.this);
+ testingConnection = true;
+ Util.setActiveServer(SettingsActivity.this, instance);
+ try {
+ MusicService musicService = MusicServiceFactory.getMusicService(SettingsActivity.this);
+ musicService.ping(SettingsActivity.this, this);
+ return musicService.isLicenseValid(SettingsActivity.this, null);
+ } finally {
+ Util.setActiveServer(SettingsActivity.this, previousInstance);
+ testingConnection = false;
+ }
+ }
+
+ @Override
+ protected void done(Boolean licenseValid) {
+ if (licenseValid) {
+ Util.toast(SettingsActivity.this, R.string.settings_testing_ok);
+ } else {
+ Util.toast(SettingsActivity.this, R.string.settings_testing_unlicensed);
+ }
+ }
+
+ @Override
+ protected void cancel() {
+ super.cancel();
+ Util.setActiveServer(SettingsActivity.this, previousInstance);
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ Log.w(TAG, error.toString(), error);
+ new ErrorDialog(SettingsActivity.this, getResources().getString(R.string.settings_connection_failure) +
+ " " + getErrorMessage(error), false);
+ }
+ };
+ task.execute();
+ }
+
+ private void openInBrowser(final int instance) {
+ SharedPreferences prefs = Util.getPreferences(this);
+ String url = prefs.getString(Constants.PREFERENCES_KEY_SERVER_URL + instance, null);
+ Uri uriServer = Uri.parse(url);
+
+ Intent browserIntent = new Intent(Intent.ACTION_VIEW, uriServer);
+ startActivity(browserIntent);
+ }
+
+ private class ServerSettings {
+ private EditTextPreference serverName;
+ private EditTextPreference serverUrl;
+ private EditTextPreference username;
+ private PreferenceScreen screen;
+
+ private ServerSettings(String instance) {
+
+ screen = (PreferenceScreen) findPreference("server" + instance);
+ serverName = (EditTextPreference) findPreference(Constants.PREFERENCES_KEY_SERVER_NAME + instance);
+ serverUrl = (EditTextPreference) findPreference(Constants.PREFERENCES_KEY_SERVER_URL + instance);
+ username = (EditTextPreference) findPreference(Constants.PREFERENCES_KEY_USERNAME + instance);
+
+ serverUrl.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object value) {
+ try {
+ String url = (String) value;
+ new URL(url);
+ if (!url.equals(url.trim()) || url.contains("@") || url.contains("_")) {
+ throw new Exception();
+ }
+ } catch (Exception x) {
+ new ErrorDialog(SettingsActivity.this, R.string.settings_invalid_url, false);
+ return false;
+ }
+ return true;
+ }
+ });
+
+ username.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object value) {
+ String username = (String) value;
+ if (username == null || !username.equals(username.trim())) {
+ new ErrorDialog(SettingsActivity.this, R.string.settings_invalid_username, false);
+ return false;
+ }
+ return true;
+ }
+ });
+ }
+
+ public void update() {
+ serverName.setSummary(serverName.getText());
+ serverUrl.setSummary(serverUrl.getText());
+ username.setSummary(username.getText());
+ screen.setSummary(serverUrl.getText());
+ screen.setTitle(serverName.getText());
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/activity/SubsonicActivity.java b/src/github/daneren2005/dsub/activity/SubsonicActivity.java
new file mode 100644
index 00000000..d8158f7d
--- /dev/null
+++ b/src/github/daneren2005/dsub/activity/SubsonicActivity.java
@@ -0,0 +1,641 @@
+package github.daneren2005.dsub.activity;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.media.AudioManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Environment;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentPagerAdapter;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v4.view.ViewPager;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.ArrayAdapter;
+import android.widget.Spinner;
+import com.actionbarsherlock.app.ActionBar;
+import com.actionbarsherlock.app.ActionBar.Tab;
+import com.actionbarsherlock.app.ActionBar.TabListener;
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.actionbarsherlock.app.SherlockFragment;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.fragments.SubsonicFragment;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.service.DownloadServiceImpl;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.ImageLoader;
+import github.daneren2005.dsub.util.Util;
+import java.io.File;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+public class SubsonicActivity extends SherlockFragmentActivity implements OnItemSelectedListener {
+ private static final String TAG = SubsonicActivity.class.getSimpleName();
+ private static ImageLoader IMAGE_LOADER;
+ protected static String theme;
+ private boolean destroyed = false;
+ protected TabPagerAdapter pagerAdapter;
+ protected ViewPager viewPager;
+ protected List<SubsonicFragment> backStack = new ArrayList<SubsonicFragment>();
+ protected SubsonicFragment currentFragment;
+ Spinner actionBarSpinner;
+ ArrayAdapter<CharSequence> spinnerAdapter;
+
+ @Override
+ protected void onCreate(Bundle bundle) {
+ setUncaughtExceptionHandler();
+ applyTheme();
+ super.onCreate(bundle);
+ startService(new Intent(this, DownloadServiceImpl.class));
+ setVolumeControlStream(AudioManager.STREAM_MUSIC);
+
+ View actionbar = getLayoutInflater().inflate(R.layout.actionbar_spinner, null);
+ actionBarSpinner = (Spinner)actionbar.findViewById(R.id.spinner);
+ spinnerAdapter = new ArrayAdapter(this, android.R.layout.simple_spinner_item);
+ spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ actionBarSpinner.setOnItemSelectedListener(this);
+ actionBarSpinner.setAdapter(spinnerAdapter);
+
+ getSupportActionBar().setCustomView(actionbar);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ Util.registerMediaButtonEventReceiver(this);
+
+ // Make sure to update theme
+ if (theme != null && !theme.equals(Util.getTheme(this))) {
+ restart();
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ destroyed = true;
+ getImageLoader().clear();
+ }
+
+ @Override
+ public void finish() {
+ super.finish();
+ Util.disablePendingTransition(this);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle savedInstanceState) {
+ super.onSaveInstanceState(savedInstanceState);
+ if(viewPager == null) {
+ String[] ids = new String[backStack.size() + 1];
+ ids[0] = currentFragment.getTag();
+ int i = 1;
+ for(SubsonicFragment frag: backStack) {
+ ids[i] = frag.getTag();
+ i++;
+ }
+ savedInstanceState.putStringArray(Constants.MAIN_BACK_STACK, ids);
+ savedInstanceState.putInt(Constants.MAIN_BACK_STACK_SIZE, backStack.size() + 1);
+ } else {
+ pagerAdapter.onSaveInstanceState(savedInstanceState);
+ }
+ }
+ @Override
+ public void onRestoreInstanceState(Bundle savedInstanceState) {
+ if(viewPager == null) {
+ super.onRestoreInstanceState(savedInstanceState);
+ int size = savedInstanceState.getInt(Constants.MAIN_BACK_STACK_SIZE);
+ String[] ids = savedInstanceState.getStringArray(Constants.MAIN_BACK_STACK);
+ FragmentManager fm = getSupportFragmentManager();
+ currentFragment = (SubsonicFragment)fm.findFragmentByTag(ids[0]);
+ currentFragment.setPrimaryFragment(true);
+ invalidateOptionsMenu();
+ for(int i = 1; i < size; i++) {
+ SubsonicFragment frag = (SubsonicFragment)fm.findFragmentByTag(ids[i]);
+ backStack.add(frag);
+ }
+ recreateSpinner();
+ } else {
+ pagerAdapter.onRestoreInstanceState(savedInstanceState);
+ super.onRestoreInstanceState(savedInstanceState);
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ com.actionbarsherlock.view.MenuInflater menuInflater = getSupportMenuInflater();
+ if(pagerAdapter != null) {
+ pagerAdapter.onCreateOptionsMenu(menu, menuInflater);
+ } else if(currentFragment != null) {
+ currentFragment.onCreateOptionsMenu(menu, menuInflater);
+ }
+ return true;
+ }
+ @Override
+ public boolean onOptionsItemSelected(com.actionbarsherlock.view.MenuItem item) {
+ if(pagerAdapter != null) {
+ return pagerAdapter.onOptionsItemSelected(item);
+ } else if(currentFragment != null) {
+ return currentFragment.onOptionsItemSelected(item);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ boolean isVolumeDown = keyCode == KeyEvent.KEYCODE_VOLUME_DOWN;
+ boolean isVolumeUp = keyCode == KeyEvent.KEYCODE_VOLUME_UP;
+ boolean isVolumeAdjust = isVolumeDown || isVolumeUp;
+ boolean isJukebox = getDownloadService() != null && getDownloadService().isJukeboxEnabled();
+
+ if (isVolumeAdjust && isJukebox) {
+ getDownloadService().adjustJukeboxVolume(isVolumeUp);
+ return true;
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public void setTitle(CharSequence title) {
+ super.setTitle(title);
+ if(pagerAdapter != null) {
+ pagerAdapter.recreateSpinner();
+ } else {
+ recreateSpinner();
+ }
+ }
+ public void setSubtitle(CharSequence title) {
+ getSupportActionBar().setSubtitle(title);
+ }
+
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ int top = spinnerAdapter.getCount() - 1;
+ if(position < top) {
+ for(int i = top; i > position; i--) {
+ if(pagerAdapter != null) {
+ pagerAdapter.removeCurrent();
+ } else {
+ removeCurrent();
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {
+
+ }
+
+ public boolean onBackPressedSupport() {
+ if(pagerAdapter != null) {
+ return pagerAdapter.onBackPressed();
+ } else {
+ if(backStack.size() > 0) {
+ removeCurrent();
+ return false;
+ } else {
+ return true;
+ }
+ }
+ }
+
+ public void replaceFragment(SubsonicFragment fragment, int id, int tag) {
+ if(pagerAdapter != null) {
+ pagerAdapter.replaceCurrent(fragment, id, tag);
+ } else {
+ if(currentFragment != null) {
+ currentFragment.setPrimaryFragment(false);
+ }
+ backStack.add(currentFragment);
+
+ currentFragment = fragment;
+ currentFragment.setPrimaryFragment(true);
+ invalidateOptionsMenu();
+
+ FragmentTransaction trans = getSupportFragmentManager().beginTransaction();
+ trans.add(id, fragment, tag + "");
+ trans.commit();
+ recreateSpinner();
+ }
+ }
+ private void removeCurrent() {
+ if(currentFragment != null) {
+ currentFragment.setPrimaryFragment(false);
+ }
+ Fragment oldFrag = (Fragment)currentFragment;
+
+ currentFragment = (SubsonicFragment) backStack.remove(backStack.size() - 1);
+ currentFragment.setPrimaryFragment(true);
+ invalidateOptionsMenu();
+
+ FragmentTransaction trans = getSupportFragmentManager().beginTransaction();
+ trans.remove(oldFrag);
+ trans.commit();
+ recreateSpinner();
+ }
+
+ private void recreateSpinner() {
+ if(backStack.size() > 0) {
+ spinnerAdapter.clear();
+ for(int i = 0; i < backStack.size(); i++) {
+ spinnerAdapter.add(backStack.get(i).getTitle());
+ }
+ spinnerAdapter.add(currentFragment.getTitle());
+ spinnerAdapter.notifyDataSetChanged();
+ actionBarSpinner.setSelection(spinnerAdapter.getCount() - 1);
+ getSupportActionBar().setDisplayShowCustomEnabled(true);
+ } else {
+ getSupportActionBar().setDisplayShowCustomEnabled(false);
+ }
+ }
+
+ protected void addTab(int titleRes, Class fragmentClass, Bundle args) {
+ pagerAdapter.addTab(getString(titleRes), fragmentClass, args);
+ }
+ protected void addTab(CharSequence title, Class fragmentClass, Bundle args) {
+ pagerAdapter.addTab(title, fragmentClass, args);
+ }
+
+ protected void restart() {
+ Intent intent = new Intent(this, this.getClass());
+ intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ intent.putExtras(getIntent());
+ Util.startActivityWithoutTransition(this, intent);
+ }
+
+ private void applyTheme() {
+ theme = Util.getTheme(this);
+ if ("dark".equals(theme)) {
+ setTheme(R.style.Theme_DSub_Dark);
+ } else if ("black".equals(theme)) {
+ setTheme(R.style.Theme_DSub_Black);
+ } else if ("light".equals(theme)) {
+ setTheme(R.style.Theme_DSub_Light);
+ } else if ("dark_fullscreen".equals(theme)) {
+ setTheme(R.style.Theme_DSub_Dark_Fullscreen);
+ } else if ("black_fullscreen".equals(theme)) {
+ setTheme(R.style.Theme_DSub_Black_Fullscreen);
+ } else if ("light_fullscreen".equals(theme)) {
+ setTheme(R.style.Theme_DSub_Light_Fullscreen);
+ } else if("holo".equals(theme)) {
+ setTheme(R.style.Theme_DSub_Holo);
+ } else if("holo_fullscreen".equals(theme)) {
+ setTheme(R.style.Theme_DSub_Holo_Fullscreen);
+ }else {
+ setTheme(R.style.Theme_DSub_Holo);
+ }
+ }
+
+ public boolean isDestroyed() {
+ return destroyed;
+ }
+
+ public synchronized ImageLoader getImageLoader() {
+ if (IMAGE_LOADER == null) {
+ IMAGE_LOADER = new ImageLoader(this);
+ }
+ return IMAGE_LOADER;
+ }
+ public synchronized static ImageLoader getStaticImageLoader(Context context) {
+ if (IMAGE_LOADER == null) {
+ IMAGE_LOADER = new ImageLoader(context);
+ }
+ return IMAGE_LOADER;
+ }
+
+ public DownloadService getDownloadService() {
+ // If service is not available, request it to start and wait for it.
+ for (int i = 0; i < 5; i++) {
+ DownloadService downloadService = DownloadServiceImpl.getInstance();
+ if (downloadService != null) {
+ return downloadService;
+ }
+ Log.w(TAG, "DownloadService not running. Attempting to start it.");
+ startService(new Intent(this, DownloadServiceImpl.class));
+ Util.sleepQuietly(50L);
+ }
+ return DownloadServiceImpl.getInstance();
+ }
+
+ public ViewPager getViewPager() {
+ return viewPager;
+ }
+ public TabPagerAdapter getPagerAdapter() {
+ return pagerAdapter;
+ }
+
+ public static String getThemeName() {
+ return theme;
+ }
+
+ private void setUncaughtExceptionHandler() {
+ Thread.UncaughtExceptionHandler handler = Thread.getDefaultUncaughtExceptionHandler();
+ if (!(handler instanceof SubsonicActivity.SubsonicUncaughtExceptionHandler)) {
+ Thread.setDefaultUncaughtExceptionHandler(new SubsonicActivity.SubsonicUncaughtExceptionHandler(this));
+ }
+ }
+
+ /**
+ * Logs the stack trace of uncaught exceptions to a file on the SD card.
+ */
+ private static class SubsonicUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
+
+ private final Thread.UncaughtExceptionHandler defaultHandler;
+ private final Context context;
+
+ private SubsonicUncaughtExceptionHandler(Context context) {
+ this.context = context;
+ defaultHandler = Thread.getDefaultUncaughtExceptionHandler();
+ }
+
+ @Override
+ public void uncaughtException(Thread thread, Throwable throwable) {
+ File file = null;
+ PrintWriter printWriter = null;
+ try {
+
+ PackageInfo packageInfo = context.getPackageManager().getPackageInfo("github.daneren2005.dsub", 0);
+ file = new File(Environment.getExternalStorageDirectory(), "subsonic-stacktrace.txt");
+ printWriter = new PrintWriter(file);
+ printWriter.println("Android API level: " + Build.VERSION.SDK);
+ printWriter.println("Subsonic version name: " + packageInfo.versionName);
+ printWriter.println("Subsonic version code: " + packageInfo.versionCode);
+ printWriter.println();
+ throwable.printStackTrace(printWriter);
+ Log.i(TAG, "Stack trace written to " + file);
+ } catch (Throwable x) {
+ Log.e(TAG, "Failed to write stack trace to " + file, x);
+ } finally {
+ Util.close(printWriter);
+ if (defaultHandler != null) {
+ defaultHandler.uncaughtException(thread, throwable);
+ }
+
+ }
+ }
+ }
+
+ public class TabPagerAdapter extends FragmentPagerAdapter implements TabListener, ViewPager.OnPageChangeListener {
+ private SherlockFragmentActivity activity;
+ private ViewPager pager;
+ private ActionBar actionBar;
+ private SubsonicFragment currentFragment;
+ private List<TabInfo> tabs = new ArrayList<TabInfo>();
+ private List<List<SubsonicFragment>> frags = new ArrayList<List<SubsonicFragment>>();
+ private List<QueuedFragment> queue = new ArrayList<QueuedFragment>();
+ private int currentPosition;
+
+ public TabPagerAdapter(SherlockFragmentActivity activity, ViewPager pager) {
+ super(activity.getSupportFragmentManager());
+ this.activity = activity;
+ this.actionBar = activity.getSupportActionBar();
+ this.pager = pager;
+ this.currentPosition = 0;
+ }
+
+ @Override
+ public Fragment getItem(int i) {
+ final TabInfo tabInfo = tabs.get(i);
+ SubsonicFragment frag = (SubsonicFragment) Fragment.instantiate(activity, tabInfo.fragmentClass.getName(), tabInfo.args);
+ List<SubsonicFragment> fragStack = new ArrayList<SubsonicFragment>();
+ fragStack.add(frag);
+ while(i > frags.size()) {
+ frags.add(null);
+ }
+ if(i == frags.size()) {
+ frags.add(i, fragStack);
+ } else {
+ frags.set(i, fragStack);
+ }
+ if(currentFragment == null || currentPosition == i) {
+ currentFragment = frag;
+ currentFragment.setPrimaryFragment(true);
+ }
+ return frag;
+ }
+
+ @Override
+ public int getCount() {
+ return tabs.size();
+ }
+
+ public void onCreateOptionsMenu(Menu menu, com.actionbarsherlock.view.MenuInflater menuInflater) {
+ if(currentFragment != null) {
+ currentFragment.onCreateOptionsMenu(menu, menuInflater);
+
+ for(QueuedFragment addFragment: queue) {
+ replaceFragment(addFragment.fragment, addFragment.id, currentFragment.getSupportTag());
+ currentFragment = addFragment.fragment;
+ }
+ currentFragment.setPrimaryFragment(true);
+ queue.clear();
+ }
+ }
+ public boolean onOptionsItemSelected(com.actionbarsherlock.view.MenuItem item) {
+ if(currentFragment != null) {
+ return currentFragment.onOptionsItemSelected(item);
+ } else {
+ return false;
+ }
+ }
+
+ public void onTabSelected(Tab tab, FragmentTransaction ft) {
+ TabInfo tabInfo = (TabInfo) tab.getTag();
+ for (int i = 0; i < tabs.size(); i++) {
+ if ( tabs.get(i) == tabInfo ) {
+ pager.setCurrentItem(i);
+ break;
+ }
+ }
+ }
+
+ public void onTabUnselected(Tab tab, FragmentTransaction ft) {}
+
+ public void onTabReselected(Tab tab, FragmentTransaction ft) {}
+
+ public void onPageScrollStateChanged(int arg0) {}
+
+ public void onPageScrolled(int arg0, float arg1, int arg2) {}
+
+ public void onPageSelected(int position) {
+ currentPosition = position;
+ actionBar.setSelectedNavigationItem(position);
+ if(currentFragment != null) {
+ currentFragment.setPrimaryFragment(false);
+ }
+ if(position <= frags.size()) {
+ List<SubsonicFragment> fragStack = frags.get(position);
+ currentFragment = fragStack.get(fragStack.size() - 1);
+ if(currentFragment != null) {
+ currentFragment.setPrimaryFragment(true);
+ }
+ activity.invalidateOptionsMenu();
+ recreateSpinner();
+ }
+ }
+
+ public void addTab(CharSequence title, Class fragmentClass, Bundle args) {
+ final TabInfo tabInfo = new TabInfo(fragmentClass, args);
+
+ Tab tab = actionBar.newTab();
+ tab.setText(title);
+ tab.setTabListener(this);
+ tab.setTag(tabInfo);
+
+ tabs.add(tabInfo);
+
+ actionBar.addTab(tab);
+ notifyDataSetChanged();
+ }
+ public void queueFragment(SubsonicFragment fragment, int id) {
+ QueuedFragment frag = new QueuedFragment();
+ frag.fragment = fragment;
+ frag.id = id;
+ queue.add(frag);
+ }
+ public void replaceCurrent(SubsonicFragment fragment, int id, int tag) {
+ if(currentFragment != null) {
+ currentFragment.setPrimaryFragment(false);
+ }
+ List<SubsonicFragment> fragStack = frags.get(currentPosition);
+ fragStack.add(fragment);
+
+ currentFragment = fragment;
+ currentFragment.setPrimaryFragment(true);
+ activity.invalidateOptionsMenu();
+
+ FragmentTransaction trans = getSupportFragmentManager().beginTransaction();
+ trans.add(id, fragment, tag + "");
+ trans.commit();
+ recreateSpinner();
+ }
+
+ public void removeCurrent() {
+ if(currentFragment != null) {
+ currentFragment.setPrimaryFragment(false);
+ }
+ List<SubsonicFragment> fragStack = frags.get(currentPosition);
+ Fragment oldFrag = (Fragment)fragStack.remove(fragStack.size() - 1);
+
+ currentFragment = fragStack.get(fragStack.size() - 1);
+ currentFragment.setPrimaryFragment(true);
+ activity.invalidateOptionsMenu();
+
+ FragmentTransaction trans = getSupportFragmentManager().beginTransaction();
+ trans.remove(oldFrag);
+ trans.commit();
+ }
+
+ public boolean onBackPressed() {
+ List<SubsonicFragment> fragStack = frags.get(currentPosition);
+ if(fragStack.size() > 1) {
+ removeCurrent();
+ recreateSpinner();
+ return false;
+ } else {
+ if(currentPosition == 0) {
+ return true;
+ } else {
+ viewPager.setCurrentItem(0);
+ return false;
+ }
+ }
+ }
+
+ private void recreateSpinner() {
+ if(frags.isEmpty()) {
+ return;
+ }
+
+ List<SubsonicFragment> fragStack = frags.get(currentPosition);
+ if(fragStack.size() > 1) {
+ spinnerAdapter.clear();
+ for(int i = 0; i < fragStack.size(); i++) {
+ SubsonicFragment frag = fragStack.get(i);
+ spinnerAdapter.add(frag.getTitle());
+ }
+ spinnerAdapter.notifyDataSetChanged();
+ actionBarSpinner.setSelection(spinnerAdapter.getCount() - 1);
+ actionBar.setDisplayShowCustomEnabled(true);
+ } else {
+ actionBar.setDisplayShowCustomEnabled(false);
+ }
+ }
+
+ public void invalidate() {
+ FragmentTransaction trans = getSupportFragmentManager().beginTransaction();
+ for (int i = 0; i < frags.size(); i++) {
+ List<SubsonicFragment> fragStack = frags.get(i);
+
+ for(int j = fragStack.size() - 1; j > 0; j--) {
+ SubsonicFragment oldFrag = fragStack.remove(j);
+ trans.remove((Fragment)oldFrag);
+ }
+
+ SubsonicFragment frag = (SubsonicFragment)fragStack.get(0);
+ frag.invalidate();
+ }
+ trans.commit();
+ }
+
+ public void onSaveInstanceState(Bundle savedInstanceState) {
+ for(int i = 0; i < frags.size(); i++) {
+ List<SubsonicFragment> fragStack = frags.get(i);
+ String[] ids = new String[fragStack.size()];
+
+ for(int j = 0; j < fragStack.size(); j++) {
+ ids[j] = fragStack.get(j).getTag();
+ }
+ savedInstanceState.putStringArray(Constants.MAIN_BACK_STACK + i, ids);
+ savedInstanceState.putInt(Constants.MAIN_BACK_STACK_SIZE + i, fragStack.size());
+ }
+ savedInstanceState.putInt(Constants.MAIN_BACK_STACK_TABS, frags.size());
+ savedInstanceState.putInt(Constants.MAIN_BACK_STACK_POSITION, currentPosition);
+ }
+
+ public void onRestoreInstanceState(Bundle savedInstanceState) {
+ int tabCount = savedInstanceState.getInt(Constants.MAIN_BACK_STACK_TABS);
+ FragmentManager fm = activity.getSupportFragmentManager();
+ for(int i = 0; i < tabCount; i++) {
+ int stackSize = savedInstanceState.getInt(Constants.MAIN_BACK_STACK_SIZE + i);
+ String[] ids = savedInstanceState.getStringArray(Constants.MAIN_BACK_STACK + i);
+ List<SubsonicFragment> fragStack = new ArrayList<SubsonicFragment>();
+
+ for(int j = 0; j < stackSize; j++) {
+ SubsonicFragment frag = (SubsonicFragment)fm.findFragmentByTag(ids[j]);
+ fragStack.add(frag);
+ }
+
+ frags.add(i, fragStack);
+ }
+ currentPosition = savedInstanceState.getInt(Constants.MAIN_BACK_STACK_POSITION);
+ List<SubsonicFragment> fragStack = frags.get(currentPosition);
+ currentFragment = fragStack.get(fragStack.size() - 1);
+ currentFragment.setPrimaryFragment(true);
+ activity.invalidateOptionsMenu();
+ }
+
+ private class TabInfo {
+ public final Class fragmentClass;
+ public final Bundle args;
+ public TabInfo(Class fragmentClass, Bundle args) {
+ this.fragmentClass = fragmentClass;
+ this.args = args;
+ }
+ }
+ private class QueuedFragment {
+ public SubsonicFragment fragment;
+ public int id;
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/activity/VoiceQueryReceiverActivity.java b/src/github/daneren2005/dsub/activity/VoiceQueryReceiverActivity.java
new file mode 100644
index 00000000..5cda9ee5
--- /dev/null
+++ b/src/github/daneren2005/dsub/activity/VoiceQueryReceiverActivity.java
@@ -0,0 +1,59 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+
+package github.daneren2005.dsub.activity;
+
+import android.app.Activity;
+import android.app.SearchManager;
+import android.content.Intent;
+import android.os.Bundle;
+import android.provider.SearchRecentSuggestions;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.Util;
+import github.daneren2005.dsub.provider.DSubSearchProvider;
+
+/**
+ * Receives voice search queries and forwards to the SearchActivity.
+ *
+ * http://android-developers.blogspot.com/2010/09/supporting-new-music-voice-action.html
+ *
+ * @author Sindre Mehus
+ */
+public class VoiceQueryReceiverActivity extends Activity {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ String query = getIntent().getStringExtra(SearchManager.QUERY);
+
+ if (query != null) {
+ SearchRecentSuggestions suggestions = new SearchRecentSuggestions(this, DSubSearchProvider.AUTHORITY,
+ DSubSearchProvider.MODE);
+ suggestions.saveRecentQuery(query, null);
+
+ Intent intent = new Intent(VoiceQueryReceiverActivity.this, SearchActivity.class);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_QUERY, query);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_AUTOPLAY, true);
+ Util.startActivityWithoutTransition(VoiceQueryReceiverActivity.this, intent);
+ }
+ finish();
+ Util.disablePendingTransition(this);
+ }
+} \ No newline at end of file
diff --git a/src/github/daneren2005/dsub/audiofx/EqualizerController.java b/src/github/daneren2005/dsub/audiofx/EqualizerController.java
new file mode 100644
index 00000000..0dcee863
--- /dev/null
+++ b/src/github/daneren2005/dsub/audiofx/EqualizerController.java
@@ -0,0 +1,151 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2011 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.audiofx;
+
+import java.io.Serializable;
+
+import android.content.Context;
+import android.media.MediaPlayer;
+import android.media.audiofx.Equalizer;
+import android.util.Log;
+import github.daneren2005.dsub.util.FileUtil;
+
+/**
+ * Backward-compatible wrapper for {@link Equalizer}, which is API Level 9.
+ *
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class EqualizerController {
+
+ private static final String TAG = EqualizerController.class.getSimpleName();
+
+ private final Context context;
+ private Equalizer equalizer;
+ private boolean released = false;
+ private int audioSessionId = 0;
+
+ // Class initialization fails when this throws an exception.
+ static {
+ try {
+ Class.forName("android.media.audiofx.Equalizer");
+ } catch (Exception ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ /**
+ * Throws an exception if the {@link Equalizer} class is not available.
+ */
+ public static void checkAvailable() throws Throwable {
+ // Calling here forces class initialization.
+ }
+
+ public EqualizerController(Context context, MediaPlayer mediaPlayer) {
+ this.context = context;
+ try {
+ audioSessionId = mediaPlayer.getAudioSessionId();
+ equalizer = new Equalizer(0, audioSessionId);
+ } catch (Throwable x) {
+ Log.w(TAG, "Failed to create equalizer.", x);
+ }
+ }
+
+ public void saveSettings() {
+ try {
+ if (isAvailable()) {
+ FileUtil.serialize(context, new EqualizerSettings(equalizer), "equalizer.dat");
+ }
+ } catch (Throwable x) {
+ Log.w(TAG, "Failed to save equalizer settings.", x);
+ }
+ }
+
+ public void loadSettings() {
+ try {
+ if (isAvailable()) {
+ EqualizerSettings settings = FileUtil.deserialize(context, "equalizer.dat");
+ if (settings != null) {
+ settings.apply(equalizer);
+ }
+ }
+ } catch (Throwable x) {
+ Log.w(TAG, "Failed to load equalizer settings.", x);
+ }
+ }
+
+ public boolean isAvailable() {
+ return equalizer != null;
+ }
+
+ public boolean isEnabled() {
+ return isAvailable() && equalizer.getEnabled();
+ }
+
+ public void release() {
+ if (isAvailable()) {
+ released = true;
+ equalizer.release();
+ }
+ }
+
+ public Equalizer getEqualizer() {
+ if(released) {
+ released = false;
+ try {
+ equalizer = new Equalizer(0, audioSessionId);
+ } catch (Throwable x) {
+ equalizer = null;
+ Log.w(TAG, "Failed to create equalizer.", x);
+ }
+ }
+ return equalizer;
+ }
+
+ private static class EqualizerSettings implements Serializable {
+
+ private final short[] bandLevels;
+ private short preset;
+ private final boolean enabled;
+
+ public EqualizerSettings(Equalizer equalizer) {
+ enabled = equalizer.getEnabled();
+ bandLevels = new short[equalizer.getNumberOfBands()];
+ for (short i = 0; i < equalizer.getNumberOfBands(); i++) {
+ bandLevels[i] = equalizer.getBandLevel(i);
+ }
+ try {
+ preset = equalizer.getCurrentPreset();
+ } catch (Exception x) {
+ preset = -1;
+ }
+ }
+
+ public void apply(Equalizer equalizer) {
+ for (short i = 0; i < bandLevels.length; i++) {
+ equalizer.setBandLevel(i, bandLevels[i]);
+ }
+ if (preset >= 0 && preset < equalizer.getNumberOfPresets()) {
+ equalizer.usePreset(preset);
+ }
+ equalizer.setEnabled(enabled);
+ }
+ }
+}
+
diff --git a/src/github/daneren2005/dsub/audiofx/VisualizerController.java b/src/github/daneren2005/dsub/audiofx/VisualizerController.java
new file mode 100644
index 00000000..b32245f4
--- /dev/null
+++ b/src/github/daneren2005/dsub/audiofx/VisualizerController.java
@@ -0,0 +1,104 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2011 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.audiofx;
+
+import android.content.Context;
+import android.media.MediaPlayer;
+import android.media.audiofx.Visualizer;
+import android.util.Log;
+
+/**
+ * Backward-compatible wrapper for {@link Visualizer}, which is API Level 9.
+ *
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class VisualizerController {
+
+ private static final String TAG = VisualizerController.class.getSimpleName();
+ private static final int PREFERRED_CAPTURE_SIZE = 128; // Must be a power of two.
+
+ private final Context context;
+ private Visualizer visualizer;
+ private boolean released = false;
+ private int audioSessionId = 0;
+
+ // Class initialization fails when this throws an exception.
+ static {
+ try {
+ Class.forName("android.media.audiofx.Visualizer");
+ } catch (Exception ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ /**
+ * Throws an exception if the {@link Visualizer} class is not available.
+ */
+ public static void checkAvailable() throws Throwable {
+ // Calling here forces class initialization.
+ }
+
+ public VisualizerController(Context context, MediaPlayer mediaPlayer) {
+ this.context = context;
+ try {
+ audioSessionId = mediaPlayer.getAudioSessionId();
+ visualizer = new Visualizer(audioSessionId);
+ } catch (Throwable x) {
+ Log.w(TAG, "Failed to create visualizer.", x);
+ }
+
+ if (visualizer != null) {
+ int[] captureSizeRange = Visualizer.getCaptureSizeRange();
+ int captureSize = Math.max(PREFERRED_CAPTURE_SIZE, captureSizeRange[0]);
+ captureSize = Math.min(captureSize, captureSizeRange[1]);
+ visualizer.setCaptureSize(captureSize);
+ }
+ }
+
+ public boolean isAvailable() {
+ return visualizer != null;
+ }
+
+ public boolean isEnabled() {
+ return isAvailable() && visualizer.getEnabled();
+ }
+
+ public void release() {
+ if (isAvailable()) {
+ visualizer.release();
+ released = true;
+ }
+ }
+
+ public Visualizer getVisualizer() {
+ if(released) {
+ released = false;
+ try {
+ visualizer = new Visualizer(audioSessionId);
+ } catch (Throwable x) {
+ visualizer = null;
+ Log.w(TAG, "Failed to create visualizer.", x);
+ }
+ }
+
+ return visualizer;
+ }
+}
+
diff --git a/src/github/daneren2005/dsub/domain/Artist.java b/src/github/daneren2005/dsub/domain/Artist.java
new file mode 100644
index 00000000..e4a9001b
--- /dev/null
+++ b/src/github/daneren2005/dsub/domain/Artist.java
@@ -0,0 +1,78 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.domain;
+
+import java.io.Serializable;
+
+/**
+ * @author Sindre Mehus
+ */
+public class Artist implements Serializable {
+
+ private String id;
+ private String name;
+ private String index;
+ private boolean starred;
+ private int closeness;
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getIndex() {
+ return index;
+ }
+
+ public void setIndex(String index) {
+ this.index = index;
+ }
+
+ public boolean isStarred() {
+ return starred;
+ }
+
+ public void setStarred(boolean starred) {
+ this.starred = starred;
+ }
+
+ public int getCloseness() {
+ return closeness;
+ }
+
+ public void setCloseness(int closeness) {
+ this.closeness = closeness;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+} \ No newline at end of file
diff --git a/src/github/daneren2005/dsub/domain/ChatMessage.java b/src/github/daneren2005/dsub/domain/ChatMessage.java
new file mode 100644
index 00000000..471594e9
--- /dev/null
+++ b/src/github/daneren2005/dsub/domain/ChatMessage.java
@@ -0,0 +1,51 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.domain;
+
+import java.io.Serializable;
+
+public class ChatMessage implements Serializable {
+ private String username;
+ private Long time;
+ private String message;
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public Long getTime() {
+ return time;
+ }
+
+ public void setTime(Long time) {
+ this.time = time;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public void setMessage(String message) {
+ this.message = message;
+ }
+}
diff --git a/src/github/daneren2005/dsub/domain/Genre.java b/src/github/daneren2005/dsub/domain/Genre.java
new file mode 100644
index 00000000..8c705e31
--- /dev/null
+++ b/src/github/daneren2005/dsub/domain/Genre.java
@@ -0,0 +1,29 @@
+package github.daneren2005.dsub.domain;
+
+import java.io.Serializable;
+
+public class Genre implements Serializable {
+ private String name;
+ private String index;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getIndex() {
+ return index;
+ }
+
+ public void setIndex(String index) {
+ this.index = index;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+}
diff --git a/src/github/daneren2005/dsub/domain/Indexes.java b/src/github/daneren2005/dsub/domain/Indexes.java
new file mode 100644
index 00000000..0bc44158
--- /dev/null
+++ b/src/github/daneren2005/dsub/domain/Indexes.java
@@ -0,0 +1,50 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.domain;
+
+import java.util.List;
+import java.io.Serializable;
+
+/**
+ * @author Sindre Mehus
+ */
+public class Indexes implements Serializable {
+
+ private final long lastModified;
+ private final List<Artist> shortcuts;
+ private final List<Artist> artists;
+
+ public Indexes(long lastModified, List<Artist> shortcuts, List<Artist> artists) {
+ this.lastModified = lastModified;
+ this.shortcuts = shortcuts;
+ this.artists = artists;
+ }
+
+ public long getLastModified() {
+ return lastModified;
+ }
+
+ public List<Artist> getShortcuts() {
+ return shortcuts;
+ }
+
+ public List<Artist> getArtists() {
+ return artists;
+ }
+} \ No newline at end of file
diff --git a/src/github/daneren2005/dsub/domain/JukeboxStatus.java b/src/github/daneren2005/dsub/domain/JukeboxStatus.java
new file mode 100644
index 00000000..7b229f49
--- /dev/null
+++ b/src/github/daneren2005/dsub/domain/JukeboxStatus.java
@@ -0,0 +1,63 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.domain;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class JukeboxStatus {
+
+ private Integer positionSeconds;
+ private Integer currentPlayingIndex;
+ private Float gain;
+ private boolean playing;
+
+ public Integer getPositionSeconds() {
+ return positionSeconds;
+ }
+
+ public void setPositionSeconds(Integer positionSeconds) {
+ this.positionSeconds = positionSeconds;
+ }
+
+ public Integer getCurrentPlayingIndex() {
+ return currentPlayingIndex;
+ }
+
+ public void setCurrentIndex(Integer currentPlayingIndex) {
+ this.currentPlayingIndex = currentPlayingIndex;
+ }
+
+ public boolean isPlaying() {
+ return playing;
+ }
+
+ public void setPlaying(boolean playing) {
+ this.playing = playing;
+ }
+
+ public Float getGain() {
+ return gain;
+ }
+
+ public void setGain(float gain) {
+ this.gain = gain;
+ }
+}
diff --git a/src/github/daneren2005/dsub/domain/Lyrics.java b/src/github/daneren2005/dsub/domain/Lyrics.java
new file mode 100644
index 00000000..feb75cd6
--- /dev/null
+++ b/src/github/daneren2005/dsub/domain/Lyrics.java
@@ -0,0 +1,55 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.domain;
+
+/**
+ * Song lyrics.
+ *
+ * @author Sindre Mehus
+ */
+public class Lyrics {
+
+ private String artist;
+ private String title;
+ private String text;
+
+ public String getArtist() {
+ return artist;
+ }
+
+ public void setArtist(String artist) {
+ this.artist = artist;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getText() {
+ return text;
+ }
+
+ public void setText(String text) {
+ this.text = text;
+ }
+}
diff --git a/src/github/daneren2005/dsub/domain/MusicDirectory.java b/src/github/daneren2005/dsub/domain/MusicDirectory.java
new file mode 100644
index 00000000..bb49378a
--- /dev/null
+++ b/src/github/daneren2005/dsub/domain/MusicDirectory.java
@@ -0,0 +1,374 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.domain;
+
+import android.util.Log;
+import java.util.ArrayList;
+import java.util.List;
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.Comparator;
+
+/**
+ * @author Sindre Mehus
+ */
+public class MusicDirectory {
+ private static final String TAG = MusicDirectory.class.getSimpleName();
+
+ private String name;
+ private String id;
+ private String parent;
+ private List<Entry> children = new ArrayList<Entry>();
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getParent() {
+ return parent;
+ }
+
+ public void setParent(String parent) {
+ this.parent = parent;
+ }
+
+ public void addChild(Entry child) {
+ children.add(child);
+ }
+
+ public List<Entry> getChildren() {
+ return getChildren(true, true);
+ }
+
+ public List<Entry> getChildren(boolean includeDirs, boolean includeFiles) {
+ if (includeDirs && includeFiles) {
+ return children;
+ }
+
+ List<Entry> result = new ArrayList<Entry>(children.size());
+ for (Entry child : children) {
+ if (child.isDirectory() && includeDirs || !child.isDirectory() && includeFiles) {
+ result.add(child);
+ }
+ }
+ return result;
+ }
+
+ public int getChildrenSize() {
+ return children.size();
+ }
+
+ public void sortChildren() {
+ EntryComparator.sort(children);
+ }
+
+ public static class Entry implements Serializable {
+ private String id;
+ private String parent;
+ private String grandParent;
+ private boolean directory;
+ private String title;
+ private String album;
+ private String artist;
+ private Integer track;
+ private Integer year;
+ private String genre;
+ private String contentType;
+ private String suffix;
+ private String transcodedContentType;
+ private String transcodedSuffix;
+ private String coverArt;
+ private Long size;
+ private Integer duration;
+ private Integer bitRate;
+ private String path;
+ private boolean video;
+ private Integer discNumber;
+ private boolean starred;
+ private int closeness;
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getParent() {
+ return parent;
+ }
+
+ public void setParent(String parent) {
+ this.parent = parent;
+ }
+
+ public String getGrandParent() {
+ return grandParent;
+ }
+
+ public void setGrandParent(String grandParent) {
+ this.grandParent = grandParent;
+ }
+
+ public boolean isDirectory() {
+ return directory;
+ }
+
+ public void setDirectory(boolean directory) {
+ this.directory = directory;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getAlbum() {
+ return album;
+ }
+
+ public void setAlbum(String album) {
+ this.album = album;
+ }
+
+ public String getArtist() {
+ return artist;
+ }
+
+ public void setArtist(String artist) {
+ this.artist = artist;
+ }
+
+ public Integer getTrack() {
+ return track;
+ }
+
+ public void setTrack(Integer track) {
+ this.track = track;
+ }
+
+ public Integer getYear() {
+ return year;
+ }
+
+ public void setYear(Integer year) {
+ this.year = year;
+ }
+
+ public String getGenre() {
+ return genre;
+ }
+
+ public void setGenre(String genre) {
+ this.genre = genre;
+ }
+
+ public String getContentType() {
+ return contentType;
+ }
+
+ public void setContentType(String contentType) {
+ this.contentType = contentType;
+ }
+
+ public String getSuffix() {
+ return suffix;
+ }
+
+ public void setSuffix(String suffix) {
+ this.suffix = suffix;
+ }
+
+ public String getTranscodedContentType() {
+ return transcodedContentType;
+ }
+
+ public void setTranscodedContentType(String transcodedContentType) {
+ this.transcodedContentType = transcodedContentType;
+ }
+
+ public String getTranscodedSuffix() {
+ return transcodedSuffix;
+ }
+
+ public void setTranscodedSuffix(String transcodedSuffix) {
+ this.transcodedSuffix = transcodedSuffix;
+ }
+
+ public Long getSize() {
+ return size;
+ }
+
+ public void setSize(Long size) {
+ this.size = size;
+ }
+
+ public Integer getDuration() {
+ return duration;
+ }
+
+ public void setDuration(Integer duration) {
+ this.duration = duration;
+ }
+
+ public Integer getBitRate() {
+ return bitRate;
+ }
+
+ public void setBitRate(Integer bitRate) {
+ this.bitRate = bitRate;
+ }
+
+ public String getCoverArt() {
+ return coverArt;
+ }
+
+ public void setCoverArt(String coverArt) {
+ this.coverArt = coverArt;
+ }
+
+ public String getPath() {
+ return path;
+ }
+
+ public void setPath(String path) {
+ this.path = path;
+ }
+
+ public boolean isVideo() {
+ return video;
+ }
+
+ public void setVideo(boolean video) {
+ this.video = video;
+ }
+
+ public Integer getDiscNumber() {
+ return discNumber;
+ }
+
+ public void setDiscNumber(Integer discNumber) {
+ this.discNumber = discNumber;
+ }
+
+ public boolean isStarred() {
+ return starred;
+ }
+
+ public void setStarred(boolean starred) {
+ this.starred = starred;
+ }
+
+ public int getCloseness() {
+ return closeness;
+ }
+
+ public void setCloseness(int closeness) {
+ this.closeness = closeness;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ Entry entry = (Entry) o;
+ return id.equals(entry.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return id.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return title;
+ }
+ }
+
+ public static class EntryComparator implements Comparator<Entry> {
+ public int compare(Entry lhs, Entry rhs) {
+ if(lhs.isDirectory() && !rhs.isDirectory()) {
+ return -1;
+ } else if(!lhs.isDirectory() && rhs.isDirectory()) {
+ return 1;
+ }
+
+ Integer lhsDisc = lhs.getDiscNumber();
+ Integer rhsDisc = rhs.getDiscNumber();
+
+ if(lhsDisc != null && rhsDisc != null) {
+ if(lhsDisc < rhsDisc) {
+ return -1;
+ } else if(lhsDisc > rhsDisc) {
+ return 1;
+ }
+ } else if(lhsDisc != null) {
+ return -1;
+ } else if(rhsDisc != null) {
+ return 1;
+ }
+
+ Integer lhsTrack = lhs.getTrack();
+ Integer rhsTrack = rhs.getTrack();
+ if(lhsTrack != null && rhsTrack != null) {
+ if(lhsTrack < rhsTrack) {
+ return -1;
+ } else if(lhsTrack > rhsTrack) {
+ return 1;
+ }
+ } else if(lhsTrack != null) {
+ return -1;
+ } else if(rhsTrack != null) {
+ return 1;
+ }
+
+ return lhs.getTitle().compareToIgnoreCase(rhs.getTitle());
+ }
+
+ public static void sort(List<Entry> entries) {
+ try {
+ Collections.sort(entries, new EntryComparator());
+ } catch (Exception e) {
+ Log.w(TAG, "Failed to sort MusicDirectory");
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/github/daneren2005/dsub/domain/MusicFolder.java b/src/github/daneren2005/dsub/domain/MusicFolder.java
new file mode 100644
index 00000000..68a22bcc
--- /dev/null
+++ b/src/github/daneren2005/dsub/domain/MusicFolder.java
@@ -0,0 +1,46 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.domain;
+
+import java.io.Serializable;
+
+/**
+ * Represents a top level directory in which music or other media is stored.
+ *
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class MusicFolder implements Serializable {
+
+ private final String id;
+ private final String name;
+
+ public MusicFolder(String id, String name) {
+ this.id = id;
+ this.name = name;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+}
diff --git a/src/github/daneren2005/dsub/domain/PlayerState.java b/src/github/daneren2005/dsub/domain/PlayerState.java
new file mode 100644
index 00000000..2b63077b
--- /dev/null
+++ b/src/github/daneren2005/dsub/domain/PlayerState.java
@@ -0,0 +1,46 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.domain;
+
+import android.media.RemoteControlClient;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public enum PlayerState {
+ IDLE(RemoteControlClient.PLAYSTATE_STOPPED),
+ DOWNLOADING(RemoteControlClient.PLAYSTATE_BUFFERING),
+ PREPARING(RemoteControlClient.PLAYSTATE_BUFFERING),
+ PREPARED(RemoteControlClient.PLAYSTATE_STOPPED),
+ STARTED(RemoteControlClient.PLAYSTATE_PLAYING),
+ STOPPED(RemoteControlClient.PLAYSTATE_STOPPED),
+ PAUSED(RemoteControlClient.PLAYSTATE_PAUSED),
+ COMPLETED(RemoteControlClient.PLAYSTATE_STOPPED);
+
+ private final int mRemoteControlClientPlayState;
+
+ private PlayerState(int playState) {
+ mRemoteControlClientPlayState = playState;
+ }
+
+ public int getRemoteControlClientPlayState() {
+ return mRemoteControlClientPlayState;
+ }
+}
diff --git a/src/github/daneren2005/dsub/domain/Playlist.java b/src/github/daneren2005/dsub/domain/Playlist.java
new file mode 100644
index 00000000..c97659c7
--- /dev/null
+++ b/src/github/daneren2005/dsub/domain/Playlist.java
@@ -0,0 +1,109 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.domain;
+
+import java.io.Serializable;
+
+/**
+ * @author Sindre Mehus
+ */
+public class Playlist implements Serializable {
+
+ private String id;
+ private String name;
+ private String owner;
+ private String comment;
+ private String songCount;
+ private String created;
+ private Boolean pub;
+
+ public Playlist(String id, String name) {
+ this.id = id;
+ this.name = name;
+ }
+ public Playlist(String id, String name, String owner, String comment, String songCount, String created, String pub) {
+ this.id = id;
+ this.name = name;
+ this.owner = (owner == null) ? "" : owner;
+ this.comment = (comment == null) ? "" : comment;
+ this.songCount = (songCount == null) ? "" : songCount;
+ this.created = (created == null) ? "" : created;
+ this.pub = (pub == null) ? null : (pub.equals("true"));
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getOwner() {
+ return this.owner;
+ }
+
+ public void setOwner(String owner) {
+ this.owner = owner;
+ }
+
+ public String getComment() {
+ return this.comment;
+ }
+
+ public void setComment(String comment) {
+ this.comment = comment;
+ }
+
+ public String getSongCount() {
+ return this.songCount;
+ }
+
+ public void setSongCount(String songCount) {
+ this.songCount = songCount;
+ }
+
+ public String getCreated() {
+ return this.created;
+ }
+
+ public void setCreated(String created) {
+ this.created = created;
+ }
+
+ public Boolean getPublic() {
+ return this.pub;
+ }
+ public void setPublic(Boolean pub) {
+ this.pub = pub;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+} \ No newline at end of file
diff --git a/src/github/daneren2005/dsub/domain/PodcastChannel.java b/src/github/daneren2005/dsub/domain/PodcastChannel.java
new file mode 100644
index 00000000..a39e8d04
--- /dev/null
+++ b/src/github/daneren2005/dsub/domain/PodcastChannel.java
@@ -0,0 +1,80 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.domain;
+
+import java.io.Serializable;
+
+/**
+ *
+ * @author Scott
+ */
+public class PodcastChannel implements Serializable {
+ private String id;
+ private String name;
+ private String url;
+ private String description;
+ private String status;
+ private String errorMessage;
+
+ public PodcastChannel() {
+
+ }
+
+ public String getId() {
+ return id;
+ }
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+ public void setStatus(String status) {
+ this.status = status;
+ }
+
+ public String getErrorMessage() {
+ return errorMessage;
+ }
+ public void setErrorMessage(String errorMessage) {
+ this.errorMessage = errorMessage;
+ }
+}
diff --git a/src/github/daneren2005/dsub/domain/PodcastEpisode.java b/src/github/daneren2005/dsub/domain/PodcastEpisode.java
new file mode 100644
index 00000000..01821072
--- /dev/null
+++ b/src/github/daneren2005/dsub/domain/PodcastEpisode.java
@@ -0,0 +1,54 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.domain;
+
+/**
+ *
+ * @author Scott
+ */
+public class PodcastEpisode extends MusicDirectory.Entry {
+ private String episodeId;
+ private String date;
+ private String status;
+
+ public PodcastEpisode() {
+ setDirectory(false);
+ }
+
+ public String getEpisodeId() {
+ return episodeId;
+ }
+ public void setEpisodeId(String episodeId) {
+ this.episodeId = episodeId;
+ }
+
+ public String getDate() {
+ return date;
+ }
+ public void setDate(String date) {
+ this.date = date;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+ public void setStatus(String status) {
+ this.status = status;
+ }
+}
diff --git a/src/github/daneren2005/dsub/domain/RepeatMode.java b/src/github/daneren2005/dsub/domain/RepeatMode.java
new file mode 100644
index 00000000..7139029c
--- /dev/null
+++ b/src/github/daneren2005/dsub/domain/RepeatMode.java
@@ -0,0 +1,28 @@
+package github.daneren2005.dsub.domain;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public enum RepeatMode {
+ OFF {
+ @Override
+ public RepeatMode next() {
+ return ALL;
+ }
+ },
+ ALL {
+ @Override
+ public RepeatMode next() {
+ return SINGLE;
+ }
+ },
+ SINGLE {
+ @Override
+ public RepeatMode next() {
+ return OFF;
+ }
+ };
+
+ public abstract RepeatMode next();
+}
diff --git a/src/github/daneren2005/dsub/domain/SearchCritera.java b/src/github/daneren2005/dsub/domain/SearchCritera.java
new file mode 100644
index 00000000..20d46aa0
--- /dev/null
+++ b/src/github/daneren2005/dsub/domain/SearchCritera.java
@@ -0,0 +1,55 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.domain;
+
+/**
+ * The criteria for a music search.
+ *
+ * @author Sindre Mehus
+ */
+public class SearchCritera {
+
+ private final String query;
+ private final int artistCount;
+ private final int albumCount;
+ private final int songCount;
+
+ public SearchCritera(String query, int artistCount, int albumCount, int songCount) {
+ this.query = query;
+ this.artistCount = artistCount;
+ this.albumCount = albumCount;
+ this.songCount = songCount;
+ }
+
+ public String getQuery() {
+ return query;
+ }
+
+ public int getArtistCount() {
+ return artistCount;
+ }
+
+ public int getAlbumCount() {
+ return albumCount;
+ }
+
+ public int getSongCount() {
+ return songCount;
+ }
+} \ No newline at end of file
diff --git a/src/github/daneren2005/dsub/domain/SearchResult.java b/src/github/daneren2005/dsub/domain/SearchResult.java
new file mode 100644
index 00000000..11a56540
--- /dev/null
+++ b/src/github/daneren2005/dsub/domain/SearchResult.java
@@ -0,0 +1,51 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.domain;
+
+import java.util.List;
+
+/**
+ * The result of a search. Contains matching artists, albums and songs.
+ *
+ * @author Sindre Mehus
+ */
+public class SearchResult {
+
+ private final List<Artist> artists;
+ private final List<MusicDirectory.Entry> albums;
+ private final List<MusicDirectory.Entry> songs;
+
+ public SearchResult(List<Artist> artists, List<MusicDirectory.Entry> albums, List<MusicDirectory.Entry> songs) {
+ this.artists = artists;
+ this.albums = albums;
+ this.songs = songs;
+ }
+
+ public List<Artist> getArtists() {
+ return artists;
+ }
+
+ public List<MusicDirectory.Entry> getAlbums() {
+ return albums;
+ }
+
+ public List<MusicDirectory.Entry> getSongs() {
+ return songs;
+ }
+} \ No newline at end of file
diff --git a/src/github/daneren2005/dsub/domain/ServerInfo.java b/src/github/daneren2005/dsub/domain/ServerInfo.java
new file mode 100644
index 00000000..43c7319a
--- /dev/null
+++ b/src/github/daneren2005/dsub/domain/ServerInfo.java
@@ -0,0 +1,46 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.domain;
+
+/**
+ * Information about the Subsonic server.
+ *
+ * @author Sindre Mehus
+ */
+public class ServerInfo {
+
+ private boolean isLicenseValid;
+ private Version restVersion;
+
+ public boolean isLicenseValid() {
+ return isLicenseValid;
+ }
+
+ public void setLicenseValid(boolean licenseValid) {
+ isLicenseValid = licenseValid;
+ }
+
+ public Version getRestVersion() {
+ return restVersion;
+ }
+
+ public void setRestVersion(Version restVersion) {
+ this.restVersion = restVersion;
+ }
+}
diff --git a/src/github/daneren2005/dsub/domain/Share.java b/src/github/daneren2005/dsub/domain/Share.java
new file mode 100644
index 00000000..d19496f9
--- /dev/null
+++ b/src/github/daneren2005/dsub/domain/Share.java
@@ -0,0 +1,140 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.domain;
+
+import github.daneren2005.dsub.domain.MusicDirectory.Entry;
+import java.io.Serializable;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+
+public class Share implements Serializable {
+ private String id;
+ private String url;
+ private String description;
+ private String username;
+ private Date created;
+ private Date lastVisited;
+ private Date expires;
+ private Long visitCount;
+ private List<Entry> entries;
+
+ public Share() {
+ entries = new ArrayList<Entry>();
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public Date getCreated() {
+ return created;
+ }
+
+ public void setCreated(String created) {
+ if (created != null) {
+ try {
+ this.created = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH).parse(created);
+ } catch (ParseException e) {
+ this.created = null;
+ }
+ } else {
+ this.created = null;
+ }
+ }
+
+ public Date getLastVisited() {
+ return lastVisited;
+ }
+
+ public void setLastVisited(String lastVisited) {
+ if (lastVisited != null) {
+ try {
+ this.lastVisited = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH).parse(lastVisited);
+ } catch (ParseException e) {
+ this.lastVisited = null;
+ }
+ } else {
+ this.lastVisited = null;
+ }
+ }
+
+ public Date getExpires() {
+ return expires;
+ }
+
+ public void setExpires(String expires) {
+ if (expires != null) {
+ try {
+ this.expires = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH).parse(expires);
+ } catch (ParseException e) {
+ this.expires = null;
+ }
+ } else {
+ this.expires = null;
+ }
+ }
+
+ public Long getVisitCount() {
+ return visitCount;
+ }
+
+ public void setVisitCount(Long visitCount) {
+ this.visitCount = visitCount;
+ }
+
+ public List<Entry> getEntries() {
+ return this.entries;
+ }
+
+ public void addEntry(Entry entry) {
+ entries.add(entry);
+ }
+ }
diff --git a/src/github/daneren2005/dsub/domain/Version.java b/src/github/daneren2005/dsub/domain/Version.java
new file mode 100644
index 00000000..40edf563
--- /dev/null
+++ b/src/github/daneren2005/dsub/domain/Version.java
@@ -0,0 +1,171 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.domain;
+
+/**
+ * Represents the version number of the Subsonic Android app.
+ *
+ * @author Sindre Mehus
+ * @version $Revision: 1.3 $ $Date: 2006/01/20 21:25:16 $
+ */
+public class Version implements Comparable<Version> {
+ private int major;
+ private int minor;
+ private int beta;
+ private int bugfix;
+
+ /**
+ * Creates a new version instance by parsing the given string.
+ * @param version A string of the format "1.27", "1.27.2" or "1.27.beta3".
+ */
+ public Version(String version) {
+ String[] s = version.split("\\.");
+ major = Integer.valueOf(s[0]);
+ minor = Integer.valueOf(s[1]);
+
+ if (s.length > 2) {
+ if (s[2].contains("beta")) {
+ beta = Integer.valueOf(s[2].replace("beta", ""));
+ } else {
+ bugfix = Integer.valueOf(s[2]);
+ }
+ }
+ }
+
+ public int getMajor() {
+ return major;
+ }
+
+ public int getMinor() {
+ return minor;
+ }
+
+ public String getVersion() {
+ switch(major) {
+ case 1:
+ switch(minor) {
+ case 0:
+ return "3.8";
+ case 1:
+ return "3.9";
+ case 2:
+ return "4.0";
+ case 3:
+ return "4.1";
+ case 4:
+ return "4.2";
+ case 5:
+ return "4.3.1";
+ case 6:
+ return "4.5";
+ case 7:
+ return "4.6";
+ case 8:
+ return "4.7";
+ case 9:
+ return "4.8";
+ }
+ }
+ return "";
+ }
+
+ /**
+ * Return whether this object is equal to another.
+ * @param o Object to compare to.
+ * @return Whether this object is equals to another.
+ */
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ final Version version = (Version) o;
+
+ if (beta != version.beta) return false;
+ if (bugfix != version.bugfix) return false;
+ if (major != version.major) return false;
+ return minor == version.minor;
+ }
+
+ /**
+ * Returns a hash code for this object.
+ * @return A hash code for this object.
+ */
+ public int hashCode() {
+ int result;
+ result = major;
+ result = 29 * result + minor;
+ result = 29 * result + beta;
+ result = 29 * result + bugfix;
+ return result;
+ }
+
+ /**
+ * Returns a string representation of the form "1.27", "1.27.2" or "1.27.beta3".
+ * @return A string representation of the form "1.27", "1.27.2" or "1.27.beta3".
+ */
+ public String toString() {
+ StringBuffer buf = new StringBuffer();
+ buf.append(major).append('.').append(minor);
+ if (beta != 0) {
+ buf.append(".beta").append(beta);
+ } else if (bugfix != 0) {
+ buf.append('.').append(bugfix);
+ }
+
+ return buf.toString();
+ }
+
+ /**
+ * Compares this object with the specified object for order.
+ * @param version The object to compare to.
+ * @return A negative integer, zero, or a positive integer as this object is less than, equal to, or
+ * greater than the specified object.
+ */
+ @Override
+ public int compareTo(Version version) {
+ if (major < version.major) {
+ return -1;
+ } else if (major > version.major) {
+ return 1;
+ }
+
+ if (minor < version.minor) {
+ return -1;
+ } else if (minor > version.minor) {
+ return 1;
+ }
+
+ if (bugfix < version.bugfix) {
+ return -1;
+ } else if (bugfix > version.bugfix) {
+ return 1;
+ }
+
+ int thisBeta = beta == 0 ? Integer.MAX_VALUE : beta;
+ int otherBeta = version.beta == 0 ? Integer.MAX_VALUE : version.beta;
+
+ if (thisBeta < otherBeta) {
+ return -1;
+ } else if (thisBeta > otherBeta) {
+ return 1;
+ }
+
+ return 0;
+ }
+} \ No newline at end of file
diff --git a/src/github/daneren2005/dsub/fragments/ChatFragment.java b/src/github/daneren2005/dsub/fragments/ChatFragment.java
new file mode 100644
index 00000000..4f373442
--- /dev/null
+++ b/src/github/daneren2005/dsub/fragments/ChatFragment.java
@@ -0,0 +1,223 @@
+package github.daneren2005.dsub.fragments;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import android.os.Bundle;
+import android.os.Handler;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.ListView;
+import android.widget.TextView;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.ChatMessage;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import github.daneren2005.dsub.util.BackgroundTask;
+import github.daneren2005.dsub.util.TabBackgroundTask;
+import github.daneren2005.dsub.util.Util;
+import github.daneren2005.dsub.view.ChatAdapter;
+import com.actionbarsherlock.view.Menu;
+import github.daneren2005.dsub.util.Constants;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author Joshua Bahnsen
+ */
+public class ChatFragment extends SubsonicFragment {
+ private static final String TAG = ChatFragment.class.getSimpleName();
+ private ListView chatListView;
+ private EditText messageEditText;
+ private ImageButton sendButton;
+ private Long lastChatMessageTime = (long) 0;
+ private ArrayList<ChatMessage> messageList = new ArrayList<ChatMessage>();
+ private ScheduledExecutorService executorService;
+
+ @Override
+ public void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
+ rootView = inflater.inflate(R.layout.chat, container, false);
+
+ messageEditText = (EditText) rootView.findViewById(R.id.chat_edittext);
+ sendButton = (ImageButton) rootView.findViewById(R.id.chat_send);
+
+ sendButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ sendMessage();
+ }
+ });
+
+ chatListView = (ListView) rootView.findViewById(R.id.chat_entries);
+
+ messageEditText.setImeActionLabel("Send", KeyEvent.KEYCODE_ENTER);
+ messageEditText.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+ }
+
+ @Override
+ public void afterTextChanged(Editable editable) {
+ sendButton.setEnabled(!Util.isNullOrWhiteSpace(editable.toString()));
+ }
+ });
+
+ messageEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
+
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (actionId == EditorInfo.IME_ACTION_DONE || (actionId == EditorInfo.IME_NULL && event.getAction() == KeyEvent.ACTION_DOWN)) {
+ sendMessage();
+ return true;
+ }
+
+ return false;
+ }
+ });
+
+ invalidated = true;
+ return rootView;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ final Handler handler = new Handler();
+ Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ if(primaryFragment) {
+ load(false);
+ } else {
+ invalidated = true;
+ }
+ }
+ });
+ }
+ };
+
+ SharedPreferences prefs = Util.getPreferences(context);
+ long refreshRate = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_CHAT_REFRESH, "30"));
+ if(refreshRate > 0) {
+ executorService = Executors.newSingleThreadScheduledExecutor();
+ executorService.scheduleWithFixedDelay(runnable, refreshRate * 1000L, refreshRate * 1000L, TimeUnit.MILLISECONDS);
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ if(executorService != null) {
+ executorService.shutdown();
+ executorService = null;
+ }
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, com.actionbarsherlock.view.MenuInflater menuInflater) {
+ menuInflater.inflate(R.menu.chat, menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(com.actionbarsherlock.view.MenuItem item) {
+ if(super.onOptionsItemSelected(item)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ protected void refresh(boolean refresh) {
+ load(refresh);
+ }
+
+ private synchronized void load(final boolean refresh) {
+ Log.i(TAG, "Loading: " + refresh);
+ setTitle(R.string.button_bar_chat);
+ BackgroundTask<List<ChatMessage>> task = new TabBackgroundTask<List<ChatMessage>>(this) {
+ @Override
+ protected List<ChatMessage> doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ return musicService.getChatMessages(refresh ? 0L : lastChatMessageTime, context, this);
+ }
+
+ @Override
+ protected void done(List<ChatMessage> result) {
+ if (result != null && !result.isEmpty()) {
+ if(refresh) {
+ messageList.clear();
+ }
+
+ // Reset lastChatMessageTime if we have a newer message
+ for (ChatMessage message : result) {
+ if (message.getTime() > lastChatMessageTime) {
+ lastChatMessageTime = message.getTime();
+ }
+ }
+
+ // Reverse results to show them on the bottom
+ Collections.reverse(result);
+ messageList.addAll(result);
+
+ ChatAdapter chatAdapter = new ChatAdapter(context, messageList);
+ chatListView.setAdapter(chatAdapter);
+ }
+ }
+ };
+
+ task.execute();
+ }
+
+ private void sendMessage() {
+ final String message = messageEditText.getText().toString();
+
+ if (!Util.isNullOrWhiteSpace(message)) {
+ messageEditText.setText("");
+ InputMethodManager mgr = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
+ mgr.hideSoftInputFromWindow(messageEditText.getWindowToken(), 0);
+
+ BackgroundTask<Void> task = new TabBackgroundTask<Void>(this) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.addChatMessage(message, context, this);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ load(false);
+ }
+ };
+
+ task.execute();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/github/daneren2005/dsub/fragments/DownloadFragment.java b/src/github/daneren2005/dsub/fragments/DownloadFragment.java
new file mode 100644
index 00000000..56accc30
--- /dev/null
+++ b/src/github/daneren2005/dsub/fragments/DownloadFragment.java
@@ -0,0 +1,1169 @@
+package github.daneren2005.dsub.fragments;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.graphics.Color;
+import android.graphics.Typeface;
+import android.os.Bundle;
+import android.os.Handler;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.Display;
+import android.view.GestureDetector;
+import android.view.GestureDetector.OnGestureListener;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.view.animation.AnimationUtils;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.SeekBar;
+import android.widget.TextView;
+import android.widget.ViewFlipper;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+import com.actionbarsherlock.view.MenuInflater;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.PlayerState;
+import github.daneren2005.dsub.domain.RepeatMode;
+import github.daneren2005.dsub.service.DownloadFile;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.SilentBackgroundTask;
+import github.daneren2005.dsub.view.FadeOutAnimation;
+import github.daneren2005.dsub.view.SongView;
+import github.daneren2005.dsub.util.Util;
+import github.daneren2005.dsub.view.VisualizerView;
+
+import static github.daneren2005.dsub.domain.PlayerState.*;
+import github.daneren2005.dsub.util.*;
+import github.daneren2005.dsub.view.AutoRepeatButton;
+import java.util.ArrayList;
+import java.util.concurrent.ScheduledFuture;
+import com.mobeta.android.dslv.*;
+import github.daneren2005.dsub.activity.EqualizerActivity;
+import github.daneren2005.dsub.activity.MainActivity;
+import github.daneren2005.dsub.activity.SubsonicActivity;
+
+public class DownloadFragment extends SubsonicFragment implements OnGestureListener {
+ private static final String TAG = DownloadFragment.class.getSimpleName();
+
+ public static final int DIALOG_SAVE_PLAYLIST = 100;
+ private static final int PERCENTAGE_OF_SCREEN_FOR_SWIPE = 10;
+ private static final int COLOR_BUTTON_ENABLED = Color.rgb(51, 181, 229);
+ private static final int COLOR_BUTTON_DISABLED = Color.rgb(206, 213, 211);
+ private static final int INCREMENT_TIME = 5000;
+
+ private ViewFlipper playlistFlipper;
+ private TextView emptyTextView;
+ private TextView songTitleTextView;
+ private ImageView albumArtImageView;
+ private DragSortListView playlistView;
+ private TextView positionTextView;
+ private TextView durationTextView;
+ private TextView statusTextView;
+ private SeekBar progressBar;
+ private AutoRepeatButton previousButton;
+ private AutoRepeatButton nextButton;
+ private View pauseButton;
+ private View stopButton;
+ private View startButton;
+ private ImageButton repeatButton;
+ private Button equalizerButton;
+ private Button visualizerButton;
+ private Button jukeboxButton;
+ private View toggleListButton;
+ private ImageButton starButton;
+ private View mainLayout;
+ private ScheduledExecutorService executorService;
+ private DownloadFile currentPlaying;
+ private long currentRevision;
+ private GestureDetector gestureScanner;
+ private int swipeDistance;
+ private int swipeVelocity;
+ private VisualizerView visualizerView;
+ private boolean nowPlaying = true;
+ private ScheduledFuture<?> hideControlsFuture;
+ private SongListAdapter songListAdapter;
+ private SilentBackgroundTask<Void> onProgressChangedTask;
+
+ /**
+ * Called when the activity is first created.
+ */
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
+ rootView = inflater.inflate(R.layout.download, container, false);
+ setTitle(nowPlaying ? "Now Playing" : "Downloading");
+
+ mainLayout = rootView.findViewById(R.id.download_layout);
+ if(!primaryFragment) {
+ mainLayout.setVisibility(View.GONE);
+ }
+
+ WindowManager w = context.getWindowManager();
+ Display d = w.getDefaultDisplay();
+ swipeDistance = (d.getWidth() + d.getHeight()) * PERCENTAGE_OF_SCREEN_FOR_SWIPE / 100;
+ swipeVelocity = (d.getWidth() + d.getHeight()) * PERCENTAGE_OF_SCREEN_FOR_SWIPE / 100;
+ gestureScanner = new GestureDetector(this);
+
+ playlistFlipper = (ViewFlipper)rootView.findViewById(R.id.download_playlist_flipper);
+ emptyTextView = (TextView)rootView.findViewById(R.id.download_empty);
+ songTitleTextView = (TextView)rootView.findViewById(R.id.download_song_title);
+ albumArtImageView = (ImageView)rootView.findViewById(R.id.download_album_art_image);
+ positionTextView = (TextView)rootView.findViewById(R.id.download_position);
+ durationTextView = (TextView)rootView.findViewById(R.id.download_duration);
+ statusTextView = (TextView)rootView.findViewById(R.id.download_status);
+ progressBar = (SeekBar)rootView.findViewById(R.id.download_progress_bar);
+ playlistView = (DragSortListView)rootView.findViewById(R.id.download_list);
+ previousButton = (AutoRepeatButton)rootView.findViewById(R.id.download_previous);
+ nextButton = (AutoRepeatButton)rootView.findViewById(R.id.download_next);
+ pauseButton =rootView.findViewById(R.id.download_pause);
+ stopButton =rootView.findViewById(R.id.download_stop);
+ startButton =rootView.findViewById(R.id.download_start);
+ repeatButton = (ImageButton)rootView.findViewById(R.id.download_repeat);
+ equalizerButton = (Button)rootView.findViewById(R.id.download_equalizer);
+ visualizerButton = (Button)rootView.findViewById(R.id.download_visualizer);
+ jukeboxButton = (Button)rootView.findViewById(R.id.download_jukebox);
+ LinearLayout visualizerViewLayout = (LinearLayout)rootView.findViewById(R.id.download_visualizer_view_layout);
+ toggleListButton =rootView.findViewById(R.id.download_toggle_list);
+
+ starButton = (ImageButton)rootView.findViewById(R.id.download_star);
+ starButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ DownloadFile currentDownload = getDownloadService().getCurrentPlaying();
+ if (currentDownload != null) {
+ MusicDirectory.Entry currentSong = currentDownload.getSong();
+ toggleStarred(currentSong);
+ starButton.setImageResource(currentSong.isStarred() ? android.R.drawable.btn_star_big_on : android.R.drawable.btn_star_big_off);
+ }
+ }
+ });
+
+ View.OnTouchListener touchListener = new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent me) {
+ return gestureScanner.onTouchEvent(me);
+ }
+ };
+ pauseButton.setOnTouchListener(touchListener);
+ stopButton.setOnTouchListener(touchListener);
+ startButton.setOnTouchListener(touchListener);
+ equalizerButton.setOnTouchListener(touchListener);
+ visualizerButton.setOnTouchListener(touchListener);
+ jukeboxButton.setOnTouchListener(touchListener);
+ emptyTextView.setOnTouchListener(touchListener);
+ albumArtImageView.setOnTouchListener(touchListener);
+
+ previousButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ warnIfNetworkOrStorageUnavailable();
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ getDownloadService().previous();
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ onCurrentChanged();
+ onProgressChanged();
+ }
+ }.execute();
+ setControlsVisible(true);
+ }
+ });
+ previousButton.setOnRepeatListener(new Runnable() {
+ public void run() {
+ changeProgress(-INCREMENT_TIME);
+ }
+ });
+
+ nextButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ warnIfNetworkOrStorageUnavailable();
+ new SilentBackgroundTask<Boolean>(context) {
+ @Override
+ protected Boolean doInBackground() throws Throwable {
+ if (getDownloadService().getCurrentPlayingIndex() < getDownloadService().size() - 1) {
+ getDownloadService().next();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ protected void done(Boolean result) {
+ if(result) {
+ onCurrentChanged();
+ onProgressChanged();
+ }
+ }
+ }.execute();
+ setControlsVisible(true);
+ }
+ });
+ nextButton.setOnRepeatListener(new Runnable() {
+ public void run() {
+ changeProgress(INCREMENT_TIME);
+ }
+ });
+
+ pauseButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ getDownloadService().pause();
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ onCurrentChanged();
+ onProgressChanged();
+ }
+ }.execute();
+ }
+ });
+
+ stopButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ getDownloadService().reset();
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ onCurrentChanged();
+ onProgressChanged();
+ }
+ }.execute();
+ }
+ });
+
+ startButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ warnIfNetworkOrStorageUnavailable();
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ start();
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ onCurrentChanged();
+ onProgressChanged();
+ }
+ }.execute();
+ }
+ });
+
+ repeatButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ RepeatMode repeatMode = getDownloadService().getRepeatMode().next();
+ getDownloadService().setRepeatMode(repeatMode);
+ onDownloadListChanged();
+ switch (repeatMode) {
+ case OFF:
+ Util.toast(context, R.string.download_repeat_off);
+ break;
+ case ALL:
+ Util.toast(context, R.string.download_repeat_all);
+ break;
+ case SINGLE:
+ Util.toast(context, R.string.download_repeat_single);
+ break;
+ default:
+ break;
+ }
+ setControlsVisible(true);
+ }
+ });
+
+ equalizerButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ DownloadService downloadService = getDownloadService();
+ if(downloadService != null && downloadService.getEqualizerController() != null
+ && downloadService.getEqualizerController().getEqualizer() != null) {
+ context.startActivity(new Intent(context, EqualizerActivity.class));
+ setControlsVisible(true);
+ } else {
+ Util.toast(context, "Failed to start equalizer. Try restarting.");
+ }
+ }
+ });
+
+ visualizerButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ boolean active = !visualizerView.isActive();
+ visualizerView.setActive(active);
+ boolean isActive = visualizerView.isActive();
+ getDownloadService().setShowVisualization(isActive);
+ updateButtons();
+ if(active == isActive) {
+ Util.toast(context, active ? R.string.download_visualizer_on : R.string.download_visualizer_off);
+ } else {
+ Util.toast(context, "Failed to start visualizer. Try restarting.");
+ }
+ setControlsVisible(true);
+ }
+ });
+
+ jukeboxButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ boolean jukeboxEnabled = !getDownloadService().isJukeboxEnabled();
+ getDownloadService().setJukeboxEnabled(jukeboxEnabled);
+ updateButtons();
+ Util.toast(context, jukeboxEnabled ? R.string.download_jukebox_on : R.string.download_jukebox_off, false);
+ setControlsVisible(true);
+ }
+ });
+
+ toggleListButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ toggleFullscreenAlbumArt();
+ setControlsVisible(true);
+ }
+ });
+
+ progressBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onStopTrackingTouch(final SeekBar seekBar) {
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ getDownloadService().seekTo(progressBar.getProgress());
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ DownloadFragment.this.onProgressChanged();
+ }
+ }.execute();
+ }
+
+ @Override
+ public void onStartTrackingTouch(final SeekBar seekBar) {
+
+ }
+
+ @Override
+ public void onProgressChanged(final SeekBar seekBar, final int progress, final boolean fromUser) {
+
+ }
+ });
+ playlistView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, final int position, long id) {
+ if(nowPlaying) {
+ warnIfNetworkOrStorageUnavailable();
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ getDownloadService().play(position);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ onCurrentChanged();
+ onProgressChanged();
+ }
+ }.execute();
+ }
+ }
+ });
+ playlistView.setDropListener(new DragSortListView.DropListener() {
+ @Override
+ public void drop(int from, int to) {
+ getDownloadService().swap(nowPlaying, from, to);
+ onDownloadListChanged();
+ }
+ });
+ playlistView.setRemoveListener(new DragSortListView.RemoveListener() {
+ @Override
+ public void remove(int which) {
+ getDownloadService().remove(which);
+ onDownloadListChanged();
+ }
+ });
+
+ registerForContextMenu(playlistView);
+
+ DownloadService downloadService = getDownloadService();
+ if (downloadService != null && context.getIntent().getBooleanExtra(Constants.INTENT_EXTRA_NAME_SHUFFLE, false)) {
+ context.getIntent().removeExtra(Constants.INTENT_EXTRA_NAME_SHUFFLE);
+ warnIfNetworkOrStorageUnavailable();
+ downloadService.setShufflePlayEnabled(true);
+ }
+
+ boolean visualizerAvailable = downloadService != null && downloadService.getVisualizerAvailable();
+ boolean equalizerAvailable = downloadService != null && downloadService.getEqualizerAvailable();
+
+ if (!equalizerAvailable) {
+ equalizerButton.setVisibility(View.GONE);
+ }
+ if (!visualizerAvailable) {
+ visualizerButton.setVisibility(View.GONE);
+ } else {
+ visualizerView = new VisualizerView(context);
+ visualizerViewLayout.addView(visualizerView, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.FILL_PARENT, LinearLayout.LayoutParams.FILL_PARENT));
+ }
+
+ // TODO: Extract to utility method and cache.
+ Typeface typeface = Typeface.createFromAsset(context.getAssets(), "fonts/Storopia.ttf");
+ equalizerButton.setTypeface(typeface);
+ visualizerButton.setTypeface(typeface);
+ jukeboxButton.setTypeface(typeface);
+
+ return rootView;
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) {
+ if(Util.isOffline(context)) {
+ menuInflater.inflate(R.menu.nowplaying_offline, menu);
+ } else {
+ if(nowPlaying) {
+ menuInflater.inflate(R.menu.nowplaying, menu);
+ }
+ else {
+ menuInflater.inflate(R.menu.nowplaying_downloading, menu);
+ }
+
+ if(getDownloadService() != null && getDownloadService().getSleepTimer()) {
+ menu.findItem(R.id.menu_toggle_timer).setTitle(R.string.download_stop_timer);
+ }
+ }
+ if(getDownloadService() != null && getDownloadService().getKeepScreenOn()) {
+ menu.findItem(R.id.menu_screen_on_off).setTitle(R.string.download_menu_screen_off);
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem menuItem) {
+ if(menuItemSelected(menuItem.getItemId(), null)) {
+ return true;
+ }
+
+ return super.onOptionsItemSelected(menuItem);
+ }
+
+ @Override
+ public void onCreateContextMenu(android.view.ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, view, menuInfo);
+ if (view == playlistView) {
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
+ DownloadFile downloadFile = (DownloadFile) playlistView.getItemAtPosition(info.position);
+
+ android.view.MenuInflater inflater = context.getMenuInflater();
+ if(Util.isOffline(context)) {
+ inflater.inflate(R.menu.nowplaying_context_offline, menu);
+ } else {
+ inflater.inflate(R.menu.nowplaying_context, menu);
+ menu.findItem(R.id.menu_star).setTitle(downloadFile.getSong().isStarred() ? R.string.common_unstar : R.string.common_star);
+ }
+
+ if (downloadFile.getSong().getParent() == null) {
+ menu.findItem(R.id.menu_show_album).setVisible(false);
+ }
+ }
+ }
+
+ @Override
+ public boolean onContextItemSelected(android.view.MenuItem menuItem) {
+ if(!primaryFragment) {
+ return false;
+ }
+
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
+ DownloadFile downloadFile = (DownloadFile) playlistView.getItemAtPosition(info.position);
+ return menuItemSelected(menuItem.getItemId(), downloadFile) || super.onContextItemSelected(menuItem);
+ }
+
+ private boolean menuItemSelected(int menuItemId, final DownloadFile song) {
+ switch (menuItemId) {
+ case R.id.menu_show_album:
+ MusicDirectory.Entry entry = song.getSong();
+
+ Intent intent = new Intent(context, MainActivity.class);
+ intent.putExtra(Constants.INTENT_EXTRA_VIEW_ALBUM, true);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_ID, entry.getParent());
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_NAME, entry.getAlbum());
+
+ if(entry.getGrandParent() != null) {
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_PARENT_ID, entry.getGrandParent());
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_PARENT_NAME, entry.getArtist());
+ }
+
+ if(Util.isOffline(context)) {
+ try {
+ // This should only be succesful if this is a online song in offline mode
+ Integer.parseInt(entry.getParent());
+ String root = FileUtil.getMusicDirectory(context).getPath();
+ String id = root + "/" + entry.getPath();
+ id = id.substring(0, id.lastIndexOf("/"));
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_ID, id);
+ id = id.substring(0, id.lastIndexOf("/"));
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_PARENT_ID, id);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_PARENT_NAME, entry.getArtist());
+ } catch(Exception e) {
+ // Do nothing, entry.getParent() is fine
+ }
+ }
+
+ intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ Util.startActivityWithoutTransition(context, intent);
+ return true;
+ case R.id.menu_lyrics:
+ SubsonicFragment fragment = new LyricsFragment();
+ Bundle args = new Bundle();
+ args.putString(Constants.INTENT_EXTRA_NAME_ARTIST, song.getSong().getArtist());
+ args.putString(Constants.INTENT_EXTRA_NAME_TITLE, song.getSong().getTitle());
+ fragment.setArguments(args);
+
+ replaceFragment(fragment, R.id.download_layout_container);
+ return true;
+ case R.id.menu_remove:
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ getDownloadService().remove(song);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ onDownloadListChanged();
+ }
+ }.execute();
+ return true;
+ case R.id.menu_delete:
+ List<MusicDirectory.Entry> songs = new ArrayList<MusicDirectory.Entry>(1);
+ songs.add(song.getSong());
+ getDownloadService().delete(songs);
+ return true;
+ case R.id.menu_remove_all:
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ getDownloadService().setShufflePlayEnabled(false);
+ if(nowPlaying) {
+ getDownloadService().clear();
+ }
+ else {
+ getDownloadService().clearBackground();
+ }
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ onDownloadListChanged();
+ }
+ }.execute();
+ return true;
+ case R.id.menu_screen_on_off:
+ if (getDownloadService().getKeepScreenOn()) {
+ context.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ getDownloadService().setKeepScreenOn(false);
+ } else {
+ context.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ getDownloadService().setKeepScreenOn(true);
+ }
+ context.invalidateOptionsMenu();
+ return true;
+ case R.id.menu_shuffle:
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ getDownloadService().shuffle();
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ Util.toast(context, R.string.download_menu_shuffle_notification);
+ }
+ }.execute();
+ return true;
+ case R.id.menu_save_playlist:
+ List<MusicDirectory.Entry> entries = new LinkedList<MusicDirectory.Entry>();
+ for (DownloadFile downloadFile : getDownloadService().getSongs()) {
+ entries.add(downloadFile.getSong());
+ }
+ createNewPlaylist(entries, true);
+ return true;
+ case R.id.menu_star:
+ toggleStarred(song.getSong());
+ return true;
+ case R.id.menu_toggle_now_playing:
+ toggleNowPlaying();
+ context.invalidateOptionsMenu();
+ return true;
+ case R.id.menu_toggle_timer:
+ if(getDownloadService().getSleepTimer()) {
+ getDownloadService().stopSleepTimer();
+ context.invalidateOptionsMenu();
+ } else {
+ startTimer();
+ }
+ return true;
+ case R.id.menu_add_playlist:
+ songs = new ArrayList<MusicDirectory.Entry>(1);
+ songs.add(song.getSong());
+ addToPlaylist(songs);
+ return true;
+ case R.id.menu_info:
+ displaySongInfo(song.getSong());
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ final Handler handler = new Handler();
+ Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ update();
+ }
+ });
+ }
+ };
+
+ executorService = Executors.newSingleThreadScheduledExecutor();
+ executorService.scheduleWithFixedDelay(runnable, 0L, 1000L, TimeUnit.MILLISECONDS);
+
+ setControlsVisible(true);
+
+ DownloadService downloadService = getDownloadService();
+ if (downloadService == null || downloadService.getCurrentPlaying() == null) {
+ playlistFlipper.setDisplayedChild(1);
+ }
+
+ scrollToCurrent();
+ if (downloadService != null && downloadService.getKeepScreenOn()) {
+ context.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ } else {
+ context.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ }
+
+ if (visualizerView != null && downloadService != null && downloadService.getShowVisualization()) {
+ visualizerView.setActive(true);
+ }
+
+ updateButtons();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ executorService.shutdown();
+ if (visualizerView != null && visualizerView.isActive()) {
+ visualizerView.setActive(false);
+ }
+ }
+
+ @Override
+ public void setPrimaryFragment(boolean primary) {
+ super.setPrimaryFragment(primary);
+ if(rootView != null) {
+ if(primary) {
+ mainLayout.setVisibility(View.VISIBLE);
+ } else {
+ mainLayout.setVisibility(View.GONE);
+ }
+ }
+ }
+
+ private void scheduleHideControls() {
+ if (hideControlsFuture != null) {
+ hideControlsFuture.cancel(false);
+ }
+
+ final Handler handler = new Handler();
+ Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ setControlsVisible(false);
+ }
+ });
+ }
+ };
+ hideControlsFuture = executorService.schedule(runnable, 3000L, TimeUnit.MILLISECONDS);
+ }
+
+ private void setControlsVisible(boolean visible) {
+ try {
+ long duration = 1700L;
+ FadeOutAnimation.createAndStart(rootView.findViewById(R.id.download_overlay_buttons), !visible, duration);
+
+ if (visible) {
+ scheduleHideControls();
+ }
+ } catch(Exception e) {
+
+ }
+ }
+
+ private void updateButtons() {
+ SharedPreferences prefs = Util.getPreferences(context);
+ boolean equalizerOn = prefs.getBoolean(Constants.PREFERENCES_EQUALIZER_ON, false);
+ if(equalizerOn && getDownloadService() != null && getDownloadService().getEqualizerController() != null &&
+ getDownloadService().getEqualizerController().isEnabled()) {
+ equalizerButton.setTextColor(COLOR_BUTTON_ENABLED);
+ } else {
+ equalizerButton.setTextColor(COLOR_BUTTON_DISABLED);
+ }
+
+ if (visualizerView != null) {
+ visualizerButton.setTextColor(visualizerView.isActive() ? COLOR_BUTTON_ENABLED : COLOR_BUTTON_DISABLED);
+ }
+
+ boolean jukeboxEnabled = getDownloadService() != null && getDownloadService().isJukeboxEnabled();
+ jukeboxButton.setTextColor(jukeboxEnabled ? COLOR_BUTTON_ENABLED : COLOR_BUTTON_DISABLED);
+ }
+
+ // Scroll to current playing/downloading.
+ private void scrollToCurrent() {
+ if (getDownloadService() == null || songListAdapter == null) {
+ return;
+ }
+
+ for (int i = 0; i < songListAdapter.getCount(); i++) {
+ if (currentPlaying == playlistView.getItemAtPosition(i)) {
+ playlistView.setSelectionFromTop(i, 40);
+ return;
+ }
+ }
+ DownloadFile currentDownloading = getDownloadService().getCurrentDownloading();
+ for (int i = 0; i < songListAdapter.getCount(); i++) {
+ if (currentDownloading == playlistView.getItemAtPosition(i)) {
+ playlistView.setSelectionFromTop(i, 40);
+ return;
+ }
+ }
+ }
+
+ private void update() {
+ if (getDownloadService() == null) {
+ return;
+ }
+
+ if (currentRevision != getDownloadService().getDownloadListUpdateRevision()) {
+ onDownloadListChanged();
+ }
+
+ if (currentPlaying != getDownloadService().getCurrentPlaying()) {
+ onCurrentChanged();
+ }
+
+ onProgressChanged();
+ }
+
+ protected void startTimer() {
+ View dialogView = context.getLayoutInflater().inflate(R.layout.start_timer, null);
+ final EditText lengthBox = (EditText)dialogView.findViewById(R.id.timer_length);
+
+ final SharedPreferences prefs = Util.getPreferences(context);
+ lengthBox.setText(prefs.getString(Constants.PREFERENCES_KEY_SLEEP_TIMER_DURATION, ""));
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.menu_set_timer)
+ .setView(dialogView)
+ .setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ String length = lengthBox.getText().toString();
+
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putString(Constants.PREFERENCES_KEY_SLEEP_TIMER_DURATION, length);
+ editor.commit();
+
+ getDownloadService().setSleepTimerDuration(Integer.parseInt(length));
+ getDownloadService().startSleepTimer();
+ context.invalidateOptionsMenu();
+ }
+ })
+ .setNegativeButton(R.string.common_cancel, null);
+ AlertDialog dialog = builder.create();
+ dialog.show();
+ }
+
+ private void toggleFullscreenAlbumArt() {
+ if (playlistFlipper.getDisplayedChild() == 1) {
+ playlistFlipper.setInAnimation(AnimationUtils.loadAnimation(context, R.anim.push_down_in));
+ playlistFlipper.setOutAnimation(AnimationUtils.loadAnimation(context, R.anim.push_down_out));
+ playlistFlipper.setDisplayedChild(0);
+ } else {
+ scrollToCurrent();
+ playlistFlipper.setInAnimation(AnimationUtils.loadAnimation(context, R.anim.push_up_in));
+ playlistFlipper.setOutAnimation(AnimationUtils.loadAnimation(context, R.anim.push_up_out));
+ playlistFlipper.setDisplayedChild(1);
+ }
+ }
+
+ private void start() {
+ DownloadService service = getDownloadService();
+ PlayerState state = service.getPlayerState();
+ if (state == PAUSED || state == COMPLETED || state == STOPPED) {
+ service.start();
+ } else if (state == STOPPED || state == IDLE) {
+ warnIfNetworkOrStorageUnavailable();
+ int current = service.getCurrentPlayingIndex();
+ // TODO: Use play() method.
+ if (current == -1) {
+ service.play(0);
+ } else {
+ service.play(current);
+ }
+ }
+ }
+ private void onDownloadListChanged() {
+ onDownloadListChanged(false);
+ }
+ private void onDownloadListChanged(boolean refresh) {
+ DownloadService downloadService = getDownloadService();
+ if (downloadService == null) {
+ return;
+ }
+
+ List<DownloadFile> list;
+ if(nowPlaying) {
+ list = downloadService.getSongs();
+ }
+ else {
+ list = downloadService.getBackgroundDownloads();
+ }
+
+ if(downloadService.isShufflePlayEnabled()) {
+ emptyTextView.setText(R.string.download_shuffle_loading);
+ }
+ else {
+ emptyTextView.setText(R.string.download_empty);
+ }
+
+ if(songListAdapter == null || refresh) {
+ playlistView.setAdapter(songListAdapter = new SongListAdapter(list));
+ } else {
+ songListAdapter.notifyDataSetChanged();
+ }
+ emptyTextView.setVisibility(list.isEmpty() ? View.VISIBLE : View.GONE);
+ currentRevision = downloadService.getDownloadListUpdateRevision();
+
+ switch (downloadService.getRepeatMode()) {
+ case OFF:
+ if("light".equals(SubsonicActivity.getThemeName()) | "light_fullscreen".equals(SubsonicActivity.getThemeName())) {
+ repeatButton.setImageResource(R.drawable.media_repeat_off_light);
+ } else {
+ repeatButton.setImageResource(R.drawable.media_repeat_off);
+ }
+ break;
+ case ALL:
+ repeatButton.setImageResource(R.drawable.media_repeat_all);
+ break;
+ case SINGLE:
+ repeatButton.setImageResource(R.drawable.media_repeat_single);
+ break;
+ default:
+ break;
+ }
+
+ setSubtitle(context.getResources().getString(R.string.download_playing_out_of, downloadService.getCurrentPlayingIndex() + 1, downloadService.size()));
+ }
+
+ private void onCurrentChanged() {
+ DownloadService downloadService = getDownloadService();
+ if (downloadService == null) {
+ return;
+ }
+
+ currentPlaying = downloadService.getCurrentPlaying();
+ if (currentPlaying != null) {
+ MusicDirectory.Entry song = currentPlaying.getSong();
+ songTitleTextView.setText(song.getTitle());
+ getImageLoader().loadImage(albumArtImageView, song, true, true);
+ starButton.setImageResource(song.isStarred() ? android.R.drawable.btn_star_big_on : android.R.drawable.btn_star_big_off);
+ setSubtitle(context.getResources().getString(R.string.download_playing_out_of, downloadService.getCurrentPlayingIndex() + 1, downloadService.size()));
+ } else {
+ songTitleTextView.setText(null);
+ getImageLoader().loadImage(albumArtImageView, null, true, false);
+ starButton.setImageResource(android.R.drawable.btn_star_big_off);
+ setSubtitle(null);
+ }
+ }
+
+ private void onProgressChanged() {
+ // Make sure to only be trying to run one of these at a time
+ if (getDownloadService() == null || onProgressChangedTask != null) {
+ return;
+ }
+
+ onProgressChangedTask = new SilentBackgroundTask<Void>(context) {
+ DownloadService downloadService;
+ boolean isJukeboxEnabled;
+ int millisPlayed;
+ Integer duration;
+ PlayerState playerState;
+
+ @Override
+ protected Void doInBackground() throws Throwable {
+ downloadService = getDownloadService();
+ isJukeboxEnabled = downloadService.isJukeboxEnabled();
+ millisPlayed = Math.max(0, downloadService.getPlayerPosition());
+ duration = downloadService.getPlayerDuration();
+ playerState = getDownloadService().getPlayerState();
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ if (currentPlaying != null) {
+ int millisTotal = duration == null ? 0 : duration;
+
+ positionTextView.setText(Util.formatDuration(millisPlayed / 1000));
+ durationTextView.setText(Util.formatDuration(millisTotal / 1000));
+ progressBar.setMax(millisTotal == 0 ? 100 : millisTotal); // Work-around for apparent bug.
+ progressBar.setProgress(millisPlayed);
+ progressBar.setEnabled(currentPlaying.isWorkDone() || isJukeboxEnabled);
+ } else {
+ positionTextView.setText("0:00");
+ durationTextView.setText("-:--");
+ progressBar.setProgress(0);
+ progressBar.setEnabled(false);
+ }
+
+ switch (playerState) {
+ case DOWNLOADING:
+ long bytes = currentPlaying.getPartialFile().length();
+ statusTextView.setText(context.getResources().getString(R.string.download_playerstate_downloading, Util.formatLocalizedBytes(bytes, context)));
+ break;
+ case PREPARING:
+ statusTextView.setText(R.string.download_playerstate_buffering);
+ break;
+ default:
+ if(currentPlaying != null) {
+ String artist = "";
+ if(currentPlaying.getSong().getArtist() != null) {
+ artist = currentPlaying.getSong().getArtist() + " - ";
+ }
+ statusTextView.setText(artist + currentPlaying.getSong().getAlbum());
+ } else {
+ statusTextView.setText(null);
+ }
+ break;
+ }
+
+ switch (playerState) {
+ case STARTED:
+ pauseButton.setVisibility(View.VISIBLE);
+ stopButton.setVisibility(View.INVISIBLE);
+ startButton.setVisibility(View.INVISIBLE);
+ break;
+ case DOWNLOADING:
+ case PREPARING:
+ pauseButton.setVisibility(View.INVISIBLE);
+ stopButton.setVisibility(View.VISIBLE);
+ startButton.setVisibility(View.INVISIBLE);
+ break;
+ default:
+ pauseButton.setVisibility(View.INVISIBLE);
+ stopButton.setVisibility(View.INVISIBLE);
+ startButton.setVisibility(View.VISIBLE);
+ break;
+ }
+
+ jukeboxButton.setTextColor(isJukeboxEnabled ? COLOR_BUTTON_ENABLED : COLOR_BUTTON_DISABLED);
+ onProgressChangedTask = null;
+ }
+ };
+ onProgressChangedTask.execute();
+ }
+
+ private void changeProgress(final int ms) {
+ final DownloadService downloadService = getDownloadService();
+ if(downloadService == null) {
+ return;
+ }
+
+ new SilentBackgroundTask<Void>(context) {
+ boolean isJukeboxEnabled;
+ int msPlayed;
+ Integer duration;
+ PlayerState playerState;
+ int seekTo;
+
+ @Override
+ protected Void doInBackground() throws Throwable {
+ msPlayed = Math.max(0, downloadService.getPlayerPosition());
+ duration = downloadService.getPlayerDuration();
+ playerState = getDownloadService().getPlayerState();
+ int msTotal = duration == null ? 0 : duration;
+ if(msPlayed + ms > msTotal) {
+ seekTo = msTotal;
+ } else {
+ seekTo = msPlayed + ms;
+ }
+ downloadService.seekTo(seekTo);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ progressBar.setProgress(seekTo);
+ }
+ }.execute();
+ }
+
+ private class SongListAdapter extends ArrayAdapter<DownloadFile> {
+ public SongListAdapter(List<DownloadFile> entries) {
+ super(context, android.R.layout.simple_list_item_1, entries);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ SongView view;
+ if (convertView != null && convertView instanceof SongView) {
+ view = (SongView) convertView;
+ } else {
+ view = new SongView(context);
+ }
+ DownloadFile downloadFile = getItem(position);
+ view.setSong(downloadFile.getSong(), false);
+ return view;
+ }
+ }
+
+ @Override
+ public boolean onDown(MotionEvent me) {
+ setControlsVisible(true);
+ return false;
+ }
+
+ public GestureDetector getGestureDetector() {
+ return gestureScanner;
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+ DownloadService downloadService = getDownloadService();
+ if (downloadService == null) {
+ return false;
+ }
+ Log.d(TAG, "onFling");
+
+ // Right to Left swipe
+ if (e1.getX() - e2.getX() > swipeDistance && Math.abs(velocityX) > swipeVelocity) {
+ warnIfNetworkOrStorageUnavailable();
+ if (downloadService.getCurrentPlayingIndex() < downloadService.size() - 1) {
+ downloadService.next();
+ onCurrentChanged();
+ onProgressChanged();
+ }
+ return true;
+ }
+
+ // Left to Right swipe
+ else if (e2.getX() - e1.getX() > swipeDistance && Math.abs(velocityX) > swipeVelocity) {
+ warnIfNetworkOrStorageUnavailable();
+ downloadService.previous();
+ onCurrentChanged();
+ onProgressChanged();
+ return true;
+ }
+
+ // Top to Bottom swipe
+ else if (e2.getY() - e1.getY() > swipeDistance && Math.abs(velocityY) > swipeVelocity) {
+ warnIfNetworkOrStorageUnavailable();
+ downloadService.seekTo(downloadService.getPlayerPosition() + 30000);
+ onProgressChanged();
+ return true;
+ }
+
+ // Bottom to Top swipe
+ else if (e1.getY() - e2.getY() > swipeDistance && Math.abs(velocityY) > swipeVelocity) {
+ warnIfNetworkOrStorageUnavailable();
+ downloadService.seekTo(downloadService.getPlayerPosition() - 8000);
+ onProgressChanged();
+ return true;
+ }
+
+ return false;
+ }
+
+ private void toggleNowPlaying() {
+ nowPlaying = !nowPlaying;
+ setTitle(nowPlaying ? "Now Playing" : "Downloading");
+ onDownloadListChanged(true);
+ }
+
+ @Override
+ public void onLongPress(MotionEvent e) {
+ }
+
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+ return false;
+ }
+
+ @Override
+ public void onShowPress(MotionEvent e) {
+ }
+
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ return false;
+ }
+}
diff --git a/src/github/daneren2005/dsub/fragments/LyricsFragment.java b/src/github/daneren2005/dsub/fragments/LyricsFragment.java
new file mode 100644
index 00000000..0b247986
--- /dev/null
+++ b/src/github/daneren2005/dsub/fragments/LyricsFragment.java
@@ -0,0 +1,81 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+
+package github.daneren2005.dsub.fragments;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.Lyrics;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import github.daneren2005.dsub.util.BackgroundTask;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.TabBackgroundTask;
+
+/**
+ * Displays song lyrics.
+ *
+ * @author Sindre Mehus
+ */
+public final class LyricsFragment extends SubsonicFragment {
+ @Override
+ public void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ setTitle(R.string.download_menu_lyrics);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
+ rootView = inflater.inflate(R.layout.lyrics, container, false);
+ load();
+
+ return rootView;
+ }
+
+ private void load() {
+ BackgroundTask<Lyrics> task = new TabBackgroundTask<Lyrics>(this) {
+ @Override
+ protected Lyrics doInBackground() throws Throwable {
+ String artist = getArguments().getString(Constants.INTENT_EXTRA_NAME_ARTIST);
+ String title = getArguments().getString(Constants.INTENT_EXTRA_NAME_TITLE);
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ return musicService.getLyrics(artist, title, context, this);
+ }
+
+ @Override
+ protected void done(Lyrics result) {
+ TextView artistView = (TextView) rootView.findViewById(R.id.lyrics_artist);
+ TextView titleView = (TextView) rootView.findViewById(R.id.lyrics_title);
+ TextView textView = (TextView) rootView.findViewById(R.id.lyrics_text);
+ if (result != null && result.getArtist() != null) {
+ artistView.setText(result.getArtist());
+ titleView.setText(result.getTitle());
+ textView.setText(result.getText());
+ } else {
+ artistView.setText(R.string.lyrics_nomatch);
+ }
+ }
+ };
+ task.execute();
+ }
+} \ No newline at end of file
diff --git a/src/github/daneren2005/dsub/fragments/MainFragment.java b/src/github/daneren2005/dsub/fragments/MainFragment.java
new file mode 100644
index 00000000..9a48acd5
--- /dev/null
+++ b/src/github/daneren2005/dsub/fragments/MainFragment.java
@@ -0,0 +1,373 @@
+package github.daneren2005.dsub.fragments;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.StatFs;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.CheckBox;
+import android.widget.ListView;
+import android.widget.TextView;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.LoadingTask;
+import github.daneren2005.dsub.view.MergeAdapter;
+import github.daneren2005.dsub.util.Util;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+import com.actionbarsherlock.view.MenuInflater;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import github.daneren2005.dsub.util.SilentBackgroundTask;
+import github.daneren2005.dsub.view.ChangeLog;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class MainFragment extends SubsonicFragment {
+ private static final String TAG = MainFragment.class.getSimpleName();
+ private LayoutInflater inflater;
+
+ private static final int MENU_GROUP_SERVER = 10;
+ private static final int MENU_ITEM_SERVER_BASE = 100;
+
+ @Override
+ public void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
+ this.inflater = inflater;
+ rootView = inflater.inflate(R.layout.home, container, false);
+
+ createLayout();
+
+ return rootView;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) {
+ menuInflater.inflate(R.menu.main, menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if(super.onOptionsItemSelected(item)) {
+ return true;
+ }
+
+ switch (item.getItemId()) {
+ case R.id.menu_log:
+ getLogs();
+ return true;
+ case R.id.menu_about:
+ showAboutDialog();
+ return true;
+ case R.id.menu_changelog:
+ ChangeLog changeLog = new ChangeLog(context, Util.getPreferences(context));
+ changeLog.getFullLogDialog().show();
+ }
+
+ return false;
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, view, menuInfo);
+
+ int serverCount = Util.getServerCount(context);
+ int activeServer = Util.getActiveServer(context);
+ for(int i = 1; i <= serverCount; i++) {
+ android.view.MenuItem menuItem = menu.add(MENU_GROUP_SERVER, MENU_ITEM_SERVER_BASE + i, MENU_ITEM_SERVER_BASE + i, Util.getServerName(context, i));
+ if(i == activeServer) {
+ menuItem.setChecked(true);
+ }
+ }
+ menu.setGroupCheckable(MENU_GROUP_SERVER, true, true);
+ menu.setHeaderTitle(R.string.main_select_server);
+ }
+
+ @Override
+ public boolean onContextItemSelected(android.view.MenuItem menuItem) {
+ if(!primaryFragment) {
+ return false;
+ }
+
+ int activeServer = menuItem.getItemId() - MENU_ITEM_SERVER_BASE;
+ setActiveServer(activeServer);
+ context.getPagerAdapter().invalidate();
+ return true;
+ }
+
+ @Override
+ protected void refresh(boolean refresh) {
+ createLayout();
+ }
+
+ private void createLayout() {
+ View buttons = inflater.inflate(R.layout.main_buttons, null);
+
+ final View serverButton = buttons.findViewById(R.id.main_select_server);
+ final TextView serverTextView = (TextView) serverButton.findViewById(R.id.main_select_server_2);
+ final TextView offlineButton = (TextView) buttons.findViewById(R.id.main_offline);
+ offlineButton.setText(Util.isOffline(context) ? R.string.main_online : R.string.main_offline);
+
+ final View albumsTitle = buttons.findViewById(R.id.main_albums);
+ final View albumsNewestButton = buttons.findViewById(R.id.main_albums_newest);
+ final View albumsRandomButton = buttons.findViewById(R.id.main_albums_random);
+ final View albumsHighestButton = buttons.findViewById(R.id.main_albums_highest);
+ final View albumsRecentButton = buttons.findViewById(R.id.main_albums_recent);
+ final View albumsFrequentButton = buttons.findViewById(R.id.main_albums_frequent);
+ final View albumsStarredButton = buttons.findViewById(R.id.main_albums_starred);
+ final View albumsGenresButton = buttons.findViewById(R.id.main_albums_genres);
+
+ final View dummyView = rootView.findViewById(R.id.main_dummy);
+
+ int instance = Util.getActiveServer(context);
+ String name = Util.getServerName(context, instance);
+ serverTextView.setText(name);
+
+ ListView list = (ListView) rootView.findViewById(R.id.main_list);
+
+ MergeAdapter adapter = new MergeAdapter();
+ if (!Util.isOffline(context)) {
+ adapter.addViews(Arrays.asList(serverButton), true);
+ }
+ adapter.addView(offlineButton, true);
+ if (!Util.isOffline(context)) {
+ adapter.addView(albumsTitle, false);
+ adapter.addViews(Arrays.asList(albumsNewestButton, albumsRandomButton, albumsHighestButton, albumsStarredButton, albumsGenresButton, albumsRecentButton, albumsFrequentButton), true);
+ }
+ list.setAdapter(adapter);
+ registerForContextMenu(dummyView);
+
+ list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ if (view == serverButton) {
+ dummyView.showContextMenu();
+ } else if (view == offlineButton) {
+ toggleOffline();
+ } else if (view == albumsNewestButton) {
+ showAlbumList("newest");
+ } else if (view == albumsRandomButton) {
+ showAlbumList("random");
+ } else if (view == albumsHighestButton) {
+ showAlbumList("highest");
+ } else if (view == albumsRecentButton) {
+ showAlbumList("recent");
+ } else if (view == albumsFrequentButton) {
+ showAlbumList("frequent");
+ } else if (view == albumsStarredButton) {
+ showAlbumList("starred");
+ } else if(view == albumsGenresButton) {
+ showAlbumList("genres");
+ }
+ }
+ });
+ }
+
+ private void setActiveServer(int instance) {
+ if (Util.getActiveServer(context) != instance) {
+ DownloadService service = getDownloadService();
+ if (service != null) {
+ service.clearIncomplete();
+ }
+ Util.setActiveServer(context, instance);
+ }
+ }
+
+ private void toggleOffline() {
+ boolean isOffline = Util.isOffline(context);
+ Util.setOffline(context, !isOffline);
+ context.getPagerAdapter().invalidate();
+
+ if(isOffline) {
+ int scrobblesCount = Util.offlineScrobblesCount(context);
+ int starsCount = Util.offlineStarsCount(context);
+ if(scrobblesCount > 0 || starsCount > 0){
+ showOfflineSyncDialog(scrobblesCount, starsCount);
+ }
+ }
+ }
+
+ private void showAlbumList(String type) {
+ if("genres".equals(type)) {
+ SubsonicFragment fragment = new SelectGenreFragment();
+ replaceFragment(fragment, R.id.home_layout);
+ } else {
+ SubsonicFragment fragment = new SelectDirectoryFragment();
+ Bundle args = new Bundle();
+ args.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, type);
+ args.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 20);
+ args.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0);
+ fragment.setArguments(args);
+
+ replaceFragment(fragment, R.id.home_layout);
+ }
+ }
+
+ private void showOfflineSyncDialog(final int scrobbleCount, final int starsCount) {
+ String syncDefault = Util.getSyncDefault(context);
+ if(syncDefault != null) {
+ if("sync".equals(syncDefault)) {
+ syncOffline(scrobbleCount, starsCount);
+ return;
+ } else if("delete".equals(syncDefault)) {
+ deleteOffline();
+ return;
+ }
+ }
+
+ View checkBoxView = context.getLayoutInflater().inflate(R.layout.sync_dialog, null);
+ final CheckBox checkBox = (CheckBox)checkBoxView.findViewById(R.id.sync_default);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setIcon(android.R.drawable.ic_dialog_info)
+ .setTitle(R.string.offline_sync_dialog_title)
+ .setMessage(context.getResources().getString(R.string.offline_sync_dialog_message, scrobbleCount, starsCount))
+ .setView(checkBoxView)
+ .setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ if(checkBox.isChecked()) {
+ Util.setSyncDefault(context, "sync");
+ }
+ syncOffline(scrobbleCount, starsCount);
+ }
+ }).setNeutralButton(R.string.common_cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ dialogInterface.dismiss();
+ }
+ }).setNegativeButton(R.string.common_delete, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ if(checkBox.isChecked()) {
+ Util.setSyncDefault(context, "delete");
+ }
+ deleteOffline();
+ }
+ });
+
+ builder.create().show();
+ }
+
+ private void syncOffline(final int scrobbleCount, final int starsCount) {
+ new SilentBackgroundTask<Integer>(context) {
+ @Override
+ protected Integer doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ return musicService.processOfflineSyncs(context, null);
+ }
+
+ @Override
+ protected void done(Integer result) {
+ if(result == scrobbleCount) {
+ Util.toast(context, context.getResources().getString(R.string.offline_sync_success, result));
+ } else {
+ Util.toast(context, context.getResources().getString(R.string.offline_sync_partial, result, scrobbleCount + starsCount));
+ }
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg = context.getResources().getString(R.string.offline_sync_error) + " " + getErrorMessage(error);
+ Util.toast(context, msg);
+ }
+ }.execute();
+ }
+ private void deleteOffline() {
+ SharedPreferences.Editor offline = Util.getOfflineSync(context).edit();
+ offline.putInt(Constants.OFFLINE_SCROBBLE_COUNT, 0);
+ offline.commit();
+ }
+
+ private void showAboutDialog() {
+ try {
+ File rootFolder = FileUtil.getMusicDirectory(context);
+ StatFs stat = new StatFs(rootFolder.getPath());
+ long bytesTotalFs = (long) stat.getBlockCount() * (long) stat.getBlockSize();
+ long bytesAvailableFs = (long) stat.getAvailableBlocks() * (long) stat.getBlockSize();
+
+ String msg = getResources().getString(R.string.main_about_text,
+ context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName,
+ Util.formatBytes(FileUtil.getUsedSize(context, rootFolder)),
+ Util.formatBytes(Util.getCacheSizeMB(context) * 1024L * 1024L),
+ Util.formatBytes(bytesAvailableFs),
+ Util.formatBytes(bytesTotalFs));
+ Util.info(context, R.string.main_about_title, msg);
+ } catch(Exception e) {
+ Util.toast(context, "Failed to open dialog");
+ }
+ }
+
+ private void getLogs() {
+ try {
+ final String version = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName;
+ new LoadingTask<File>(context) {
+ @Override
+ protected File doInBackground() throws Throwable {
+ updateProgress("Gathering Logs");
+ File logcat = new File(FileUtil.getSubsonicDirectory(), "logcat.txt");
+ Process logcatProc = null;
+
+ try {
+ List<String> progs = new ArrayList<String>();
+ progs.add("logcat");
+ progs.add("-v");
+ progs.add("time");
+ progs.add("-d");
+ progs.add("-f");
+ progs.add(logcat.getPath());
+ progs.add("*:I");
+
+ logcatProc = Runtime.getRuntime().exec(progs.toArray(new String[0]));
+ logcatProc.waitFor();
+ } catch(Exception e) {
+ Util.toast(context, "Failed to gather logs");
+ } finally {
+ if(logcatProc != null) {
+ logcatProc.destroy();
+ }
+ }
+
+ return logcat;
+ }
+
+ @Override
+ protected void done(File logcat) {
+ Intent email = new Intent(android.content.Intent.ACTION_SEND);
+ email.setType("text/plain");
+ email.putExtra(Intent.EXTRA_EMAIL, new String[] {"dsub.android@gmail.com"});
+ email.putExtra(Intent.EXTRA_SUBJECT, "DSub " + version + " Error Logs");
+ email.putExtra(Intent.EXTRA_TEXT, "Describe the problem here");
+ Uri attachment = Uri.fromFile(logcat);
+ email.putExtra(Intent.EXTRA_STREAM, attachment);
+ startActivity(email);
+ }
+ }.execute();
+ } catch(Exception e) {}
+ }
+}
diff --git a/src/github/daneren2005/dsub/fragments/SearchFragment.java b/src/github/daneren2005/dsub/fragments/SearchFragment.java
new file mode 100644
index 00000000..1c35d38c
--- /dev/null
+++ b/src/github/daneren2005/dsub/fragments/SearchFragment.java
@@ -0,0 +1,333 @@
+package github.daneren2005.dsub.fragments;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Arrays;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.MenuItem;
+import android.widget.AdapterView;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.net.Uri;
+import android.view.ViewGroup;
+import com.actionbarsherlock.view.Menu;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.activity.SearchActivity;
+import github.daneren2005.dsub.domain.Artist;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.SearchCritera;
+import github.daneren2005.dsub.domain.SearchResult;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.view.ArtistAdapter;
+import github.daneren2005.dsub.util.BackgroundTask;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.view.EntryAdapter;
+import github.daneren2005.dsub.view.MergeAdapter;
+import github.daneren2005.dsub.util.TabBackgroundTask;
+import github.daneren2005.dsub.util.Util;
+
+public class SearchFragment extends SubsonicFragment {
+ private static final int DEFAULT_ARTISTS = 3;
+ private static final int DEFAULT_ALBUMS = 5;
+ private static final int DEFAULT_SONGS = 10;
+
+ private static final int MAX_ARTISTS = 10;
+ private static final int MAX_ALBUMS = 20;
+ private static final int MAX_SONGS = 25;
+ private ListView list;
+
+ private View artistsHeading;
+ private View albumsHeading;
+ private View songsHeading;
+ private TextView searchButton;
+ private View moreArtistsButton;
+ private View moreAlbumsButton;
+ private View moreSongsButton;
+ private SearchResult searchResult;
+ private MergeAdapter mergeAdapter;
+ private ArtistAdapter artistAdapter;
+ private ListAdapter moreArtistsAdapter;
+ private EntryAdapter albumAdapter;
+ private ListAdapter moreAlbumsAdapter;
+ private ListAdapter moreSongsAdapter;
+ private EntryAdapter songAdapter;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
+ rootView = inflater.inflate(R.layout.search, container, false);
+ setTitle(R.string.search_title);
+
+ View buttons = inflater.inflate(R.layout.search_buttons, null);
+
+ artistsHeading = buttons.findViewById(R.id.search_artists);
+ albumsHeading = buttons.findViewById(R.id.search_albums);
+ songsHeading = buttons.findViewById(R.id.search_songs);
+
+ searchButton = (TextView) buttons.findViewById(R.id.search_search);
+ moreArtistsButton = buttons.findViewById(R.id.search_more_artists);
+ moreAlbumsButton = buttons.findViewById(R.id.search_more_albums);
+ moreSongsButton = buttons.findViewById(R.id.search_more_songs);
+
+ list = (ListView) rootView.findViewById(R.id.search_list);
+
+ list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ if (view == searchButton) {
+ context.onSearchRequested();
+ } else if (view == moreArtistsButton) {
+ expandArtists();
+ } else if (view == moreAlbumsButton) {
+ expandAlbums();
+ } else if (view == moreSongsButton) {
+ expandSongs();
+ } else {
+ Object item = parent.getItemAtPosition(position);
+ if (item instanceof Artist) {
+ onArtistSelected((Artist) item);
+ } else if (item instanceof MusicDirectory.Entry) {
+ MusicDirectory.Entry entry = (MusicDirectory.Entry) item;
+ if (entry.isDirectory()) {
+ onAlbumSelected(entry, false);
+ } else if (entry.isVideo()) {
+ onVideoSelected(entry);
+ } else {
+ onSongSelected(entry, false, true, true, false);
+ }
+
+ }
+ }
+ }
+ });
+ registerForContextMenu(list);
+ ((SearchActivity)context).onSupportNewIntent(context.getIntent());
+ return rootView;
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, com.actionbarsherlock.view.MenuInflater menuInflater) {
+ menuInflater.inflate(R.menu.search, menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(com.actionbarsherlock.view.MenuItem item) {
+ if(super.onOptionsItemSelected(item)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, view, menuInfo);
+
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
+ Object selectedItem = list.getItemAtPosition(info.position);
+ onCreateContextMenu(menu, view, menuInfo, selectedItem);
+ if(selectedItem instanceof MusicDirectory.Entry && !((MusicDirectory.Entry) selectedItem).isVideo() && !Util.isOffline(context)) {
+ menu.removeItem(R.id.song_menu_remove_playlist);
+ }
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem menuItem) {
+ if(!primaryFragment) {
+ return false;
+ }
+
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
+ Object selectedItem = list.getItemAtPosition(info.position);
+
+ if(onContextItemSelected(menuItem, selectedItem)) {
+ return true;
+ }
+
+ return true;
+ }
+
+ @Override
+ public void setPrimaryFragment(boolean primary) {
+ super.setPrimaryFragment(primary);
+ if(rootView != null && primary) {
+ ((SearchActivity)context).onSupportNewIntent(context.getIntent());
+ }
+ }
+
+ public void search(final String query, final boolean autoplay) {
+ mergeAdapter = new MergeAdapter();
+ list.setAdapter(mergeAdapter);
+
+ BackgroundTask<SearchResult> task = new TabBackgroundTask<SearchResult>(this) {
+ @Override
+ protected SearchResult doInBackground() throws Throwable {
+ SearchCritera criteria = new SearchCritera(query, MAX_ARTISTS, MAX_ALBUMS, MAX_SONGS);
+ MusicService service = MusicServiceFactory.getMusicService(context);
+ return service.search(criteria, context, this);
+ }
+
+ @Override
+ protected void done(SearchResult result) {
+ searchResult = result;
+ populateList();
+ if (autoplay) {
+ autoplay();
+ }
+
+ }
+ };
+ task.execute();
+ }
+
+ public void populateList() {
+ mergeAdapter = new MergeAdapter();
+ mergeAdapter.addView(searchButton, true);
+
+ if (searchResult != null) {
+ List<Artist> artists = searchResult.getArtists();
+ if (!artists.isEmpty()) {
+ mergeAdapter.addView(artistsHeading);
+ List<Artist> displayedArtists = new ArrayList<Artist>(artists.subList(0, Math.min(DEFAULT_ARTISTS, artists.size())));
+ artistAdapter = new ArtistAdapter(context, displayedArtists);
+ mergeAdapter.addAdapter(artistAdapter);
+ if (artists.size() > DEFAULT_ARTISTS) {
+ moreArtistsAdapter = mergeAdapter.addView(moreArtistsButton, true);
+ }
+ }
+
+ List<MusicDirectory.Entry> albums = searchResult.getAlbums();
+ if (!albums.isEmpty()) {
+ mergeAdapter.addView(albumsHeading);
+ List<MusicDirectory.Entry> displayedAlbums = new ArrayList<MusicDirectory.Entry>(albums.subList(0, Math.min(DEFAULT_ALBUMS, albums.size())));
+ albumAdapter = new EntryAdapter(context, getImageLoader(), displayedAlbums, false);
+ mergeAdapter.addAdapter(albumAdapter);
+ if (albums.size() > DEFAULT_ALBUMS) {
+ moreAlbumsAdapter = mergeAdapter.addView(moreAlbumsButton, true);
+ }
+ }
+
+ List<MusicDirectory.Entry> songs = searchResult.getSongs();
+ if (!songs.isEmpty()) {
+ mergeAdapter.addView(songsHeading);
+ List<MusicDirectory.Entry> displayedSongs = new ArrayList<MusicDirectory.Entry>(songs.subList(0, Math.min(DEFAULT_SONGS, songs.size())));
+ songAdapter = new EntryAdapter(context, getImageLoader(), displayedSongs, false);
+ mergeAdapter.addAdapter(songAdapter);
+ if (songs.size() > DEFAULT_SONGS) {
+ moreSongsAdapter = mergeAdapter.addView(moreSongsButton, true);
+ }
+ }
+
+ boolean empty = searchResult.getArtists().isEmpty() && searchResult.getAlbums().isEmpty() && searchResult.getSongs().isEmpty();
+ searchButton.setText(empty ? R.string.search_no_match : R.string.search_search);
+ }
+
+ list.setAdapter(mergeAdapter);
+ }
+
+ private void expandArtists() {
+ artistAdapter.clear();
+ for (Artist artist : searchResult.getArtists()) {
+ artistAdapter.add(artist);
+ }
+ artistAdapter.notifyDataSetChanged();
+ mergeAdapter.removeAdapter(moreArtistsAdapter);
+ mergeAdapter.notifyDataSetChanged();
+ }
+
+ private void expandAlbums() {
+ albumAdapter.clear();
+ for (MusicDirectory.Entry album : searchResult.getAlbums()) {
+ albumAdapter.add(album);
+ }
+ albumAdapter.notifyDataSetChanged();
+ mergeAdapter.removeAdapter(moreAlbumsAdapter);
+ mergeAdapter.notifyDataSetChanged();
+ }
+
+ private void expandSongs() {
+ songAdapter.clear();
+ for (MusicDirectory.Entry song : searchResult.getSongs()) {
+ songAdapter.add(song);
+ }
+ songAdapter.notifyDataSetChanged();
+ mergeAdapter.removeAdapter(moreSongsAdapter);
+ mergeAdapter.notifyDataSetChanged();
+ }
+
+ private void onArtistSelected(Artist artist) {
+ 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());
+ fragment.setArguments(args);
+
+ replaceFragment(fragment, R.id.search_layout);
+ }
+
+ private void onAlbumSelected(MusicDirectory.Entry album, boolean autoplay) {
+ int id = R.id.search_layout;
+ Bundle args;
+ if(album.getParent() != null) {
+ SubsonicFragment parentFragment = new SelectDirectoryFragment();
+ args = new Bundle();
+ args.putString(Constants.INTENT_EXTRA_NAME_ID, album.getParent());
+ args.putString(Constants.INTENT_EXTRA_NAME_NAME, album.getArtist());
+ parentFragment.setArguments(args);
+
+ replaceFragment(parentFragment, R.id.search_layout);
+ id = parentFragment.getRootId();
+ }
+
+ SubsonicFragment fragment = new SelectDirectoryFragment();
+ args = new Bundle();
+ args.putString(Constants.INTENT_EXTRA_NAME_ID, album.getId());
+ args.putString(Constants.INTENT_EXTRA_NAME_NAME, album.getTitle());
+ fragment.setArguments(args);
+
+ replaceFragment(fragment, id);
+ }
+
+ private void onSongSelected(MusicDirectory.Entry song, boolean save, boolean append, boolean autoplay, boolean playNext) {
+ DownloadService downloadService = getDownloadService();
+ if (downloadService != null) {
+ if (!append) {
+ downloadService.clear();
+ }
+ downloadService.download(Arrays.asList(song), save, false, playNext, false);
+ if (autoplay) {
+ downloadService.play(downloadService.size() - 1);
+ }
+
+ Util.toast(context, getResources().getQuantityString(R.plurals.select_album_n_songs_added, 1, 1));
+ }
+ }
+
+ private void onVideoSelected(MusicDirectory.Entry entry) {
+ int maxBitrate = Util.getMaxVideoBitrate(context);
+
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(Uri.parse(MusicServiceFactory.getMusicService(context).getVideoUrl(maxBitrate, context, entry.getId())));
+ startActivity(intent);
+ }
+
+ private void autoplay() {
+ if (!searchResult.getSongs().isEmpty()) {
+ onSongSelected(searchResult.getSongs().get(0), false, false, true, false);
+ } else if (!searchResult.getAlbums().isEmpty()) {
+ onAlbumSelected(searchResult.getAlbums().get(0), true);
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/fragments/SelectArtistFragment.java b/src/github/daneren2005/dsub/fragments/SelectArtistFragment.java
new file mode 100644
index 00000000..0a35233e
--- /dev/null
+++ b/src/github/daneren2005/dsub/fragments/SelectArtistFragment.java
@@ -0,0 +1,208 @@
+package github.daneren2005.dsub.fragments;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v4.app.FragmentTransaction;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import com.actionbarsherlock.view.Menu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ListView;
+import android.widget.TextView;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.Artist;
+import github.daneren2005.dsub.domain.Indexes;
+import github.daneren2005.dsub.domain.MusicFolder;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import github.daneren2005.dsub.util.BackgroundTask;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.TabBackgroundTask;
+import github.daneren2005.dsub.util.Util;
+import github.daneren2005.dsub.view.ArtistAdapter;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+public class SelectArtistFragment extends SubsonicFragment implements AdapterView.OnItemClickListener {
+ private static final String TAG = SelectArtistFragment.class.getSimpleName();
+ private static final int MENU_GROUP_MUSIC_FOLDER = 10;
+
+ private ListView artistList;
+ private View folderButtonParent;
+ private View folderButton;
+ private TextView folderName;
+ private List<MusicFolder> musicFolders = null;
+
+ @Override
+ public void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
+ rootView = inflater.inflate(R.layout.select_artist, container, false);
+
+ artistList = (ListView) rootView.findViewById(R.id.select_artist_list);
+ artistList.setOnItemClickListener(this);
+
+ folderButtonParent = inflater.inflate(R.layout.select_artist_header, artistList, false);
+ folderName = (TextView) folderButtonParent.findViewById(R.id.select_artist_folder_2);
+ artistList.addHeaderView(folderButtonParent);
+ folderButton = folderButtonParent.findViewById(R.id.select_artist_folder);
+
+ registerForContextMenu(artistList);
+ if(!primaryFragment) {
+ invalidated = true;
+ } else {
+ refresh(false);
+ }
+
+ return rootView;
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, com.actionbarsherlock.view.MenuInflater menuInflater) {
+ menuInflater.inflate(R.menu.select_artist, menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(com.actionbarsherlock.view.MenuItem item) {
+ if(super.onOptionsItemSelected(item)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, view, menuInfo);
+
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
+ Object entry = artistList.getItemAtPosition(info.position);
+
+ if (entry instanceof Artist) {
+ onCreateContextMenu(menu, view, menuInfo, entry);
+ } else if (info.position == 0) {
+ String musicFolderId = Util.getSelectedMusicFolderId(context);
+ MenuItem menuItem = menu.add(MENU_GROUP_MUSIC_FOLDER, -1, 0, R.string.select_artist_all_folders);
+ if (musicFolderId == null) {
+ menuItem.setChecked(true);
+ }
+ if (musicFolders != null) {
+ for (int i = 0; i < musicFolders.size(); i++) {
+ MusicFolder musicFolder = musicFolders.get(i);
+ menuItem = menu.add(MENU_GROUP_MUSIC_FOLDER, i, i + 1, musicFolder.getName());
+ if (musicFolder.getId().equals(musicFolderId)) {
+ menuItem.setChecked(true);
+ }
+ }
+ }
+ menu.setGroupCheckable(MENU_GROUP_MUSIC_FOLDER, true, true);
+ }
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem menuItem) {
+ if(!primaryFragment) {
+ return false;
+ }
+
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
+ Artist artist = (Artist) artistList.getItemAtPosition(info.position);
+
+ if (artist != null) {
+ return onContextItemSelected(menuItem, artist);
+ } else if (info.position == 0) {
+ MusicFolder selectedFolder = menuItem.getItemId() == -1 ? null : musicFolders.get(menuItem.getItemId());
+ String musicFolderId = selectedFolder == null ? null : selectedFolder.getId();
+ String musicFolderName = selectedFolder == null ? context.getString(R.string.select_artist_all_folders)
+ : selectedFolder.getName();
+ Util.setSelectedMusicFolderId(context, musicFolderId);
+ folderName.setText(musicFolderName);
+ refresh();
+ }
+
+ return true;
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ if (view == folderButtonParent) {
+ selectFolder();
+ } else {
+ Artist artist = (Artist) parent.getItemAtPosition(position);
+ SubsonicFragment fragment = new SelectDirectoryFragment();
+ Bundle args = new Bundle();
+ args.putString(Constants.INTENT_EXTRA_NAME_ID, artist.getId());
+ args.putString(Constants.INTENT_EXTRA_NAME_NAME, artist.getName());
+ fragment.setArguments(args);
+
+ replaceFragment(fragment, R.id.select_artist_layout);
+ }
+ }
+
+ @Override
+ protected void refresh(boolean refresh) {
+ load(refresh);
+ }
+
+ private void load(final boolean refresh) {
+ setTitle(R.string.search_artists);
+
+ if (Util.isOffline(context)) {
+ folderButton.setVisibility(View.GONE);
+ } else {
+ folderButton.setVisibility(View.VISIBLE);
+ }
+ artistList.setVisibility(View.INVISIBLE);
+
+ BackgroundTask<Indexes> task = new TabBackgroundTask<Indexes>(this) {
+ @Override
+ protected Indexes doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ if (!Util.isOffline(context)) {
+ musicFolders = musicService.getMusicFolders(refresh, context, this);
+ }
+ String musicFolderId = Util.getSelectedMusicFolderId(context);
+ return musicService.getIndexes(musicFolderId, refresh, context, this);
+ }
+
+ @Override
+ protected void done(Indexes result) {
+ List<Artist> artists = new ArrayList<Artist>(result.getShortcuts().size() + result.getArtists().size());
+ artists.addAll(result.getShortcuts());
+ artists.addAll(result.getArtists());
+ artistList.setAdapter(new ArtistAdapter(context, artists));
+
+ // Display selected music folder
+ if (musicFolders != null) {
+ String musicFolderId = Util.getSelectedMusicFolderId(context);
+ if (musicFolderId == null) {
+ folderName.setText(R.string.select_artist_all_folders);
+ } else {
+ for (MusicFolder musicFolder : musicFolders) {
+ if (musicFolder.getId().equals(musicFolderId)) {
+ folderName.setText(musicFolder.getName());
+ break;
+ }
+ }
+ }
+ }
+ artistList.setVisibility(View.VISIBLE);
+ }
+ };
+ task.execute();
+ }
+
+ private void selectFolder() {
+ folderButton.showContextMenu();
+ }
+}
diff --git a/src/github/daneren2005/dsub/fragments/SelectDirectoryFragment.java b/src/github/daneren2005/dsub/fragments/SelectDirectoryFragment.java
new file mode 100644
index 00000000..c3771492
--- /dev/null
+++ b/src/github/daneren2005/dsub/fragments/SelectDirectoryFragment.java
@@ -0,0 +1,818 @@
+package github.daneren2005.dsub.fragments;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.Button;
+import android.widget.ListView;
+import android.widget.TextView;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.view.EntryAdapter;
+import java.util.List;
+import com.mobeta.android.dslv.*;
+import github.daneren2005.dsub.activity.DownloadActivity;
+import github.daneren2005.dsub.activity.SearchActivity;
+import github.daneren2005.dsub.domain.PodcastEpisode;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import github.daneren2005.dsub.service.OfflineException;
+import github.daneren2005.dsub.service.ServerTooOldException;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.LoadingTask;
+import github.daneren2005.dsub.util.Pair;
+import github.daneren2005.dsub.util.TabBackgroundTask;
+import github.daneren2005.dsub.util.Util;
+import github.daneren2005.dsub.view.AlbumListAdapter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+public class SelectDirectoryFragment extends SubsonicFragment implements AdapterView.OnItemClickListener {
+ private static final String TAG = SelectDirectoryFragment.class.getSimpleName();
+
+ private DragSortListView entryList;
+ int rootId;
+ private View footer;
+ private View emptyView;
+ private boolean hideButtons = false;
+ private Boolean licenseValid;
+ private boolean showHeader = true;
+ private EntryAdapter entryAdapter;
+ private List<MusicDirectory.Entry> entries;
+
+ String id;
+ String name;
+ String playlistId;
+ String playlistName;
+ String podcastId;
+ String podcastName;
+ String podcastDescription;
+ String albumListType;
+ String albumListExtra;
+ int albumListSize;
+
+
+ public SelectDirectoryFragment() {
+ super();
+ rootId = getNewId();
+ }
+
+ @Override
+ public void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ if(bundle != null) {
+ int tmp = bundle.getInt(Constants.FRAGMENT_ID, -1);
+ if(tmp > 0) {
+ rootId = tmp;
+ }
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putInt(Constants.FRAGMENT_ID, rootId);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
+ rootView = inflater.inflate(R.layout.select_album, container, false);
+ rootView.setId(rootId);
+
+ entryList = (DragSortListView) rootView.findViewById(R.id.select_album_entries);
+ footer = LayoutInflater.from(context).inflate(R.layout.select_album_footer, entryList, false);
+ entryList.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
+ entryList.setOnItemClickListener(this);
+ entryList.setDropListener(new DragSortListView.DropListener() {
+ @Override
+ public void drop(int from, int to) {
+ int max = entries.size();
+ if(to >= max) {
+ to = max - 1;
+ }
+ else if(to < 0) {
+ to = 0;
+ }
+ entries.add(to, entries.remove(from));
+ entryAdapter.notifyDataSetChanged();
+ }
+ });
+
+ emptyView = rootView.findViewById(R.id.select_album_empty);
+
+ registerForContextMenu(entryList);
+
+ Bundle args = getArguments();
+ if(args != null) {
+ id = args.getString(Constants.INTENT_EXTRA_NAME_ID);
+ name = args.getString(Constants.INTENT_EXTRA_NAME_NAME);
+ playlistId = args.getString(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID);
+ playlistName = args.getString(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME);
+ podcastId = args.getString(Constants.INTENT_EXTRA_NAME_PODCAST_ID);
+ podcastName = args.getString(Constants.INTENT_EXTRA_NAME_PODCAST_NAME);
+ podcastDescription = args.getString(Constants.INTENT_EXTRA_NAME_PODCAST_DESCRIPTION);
+ albumListType = args.getString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE);
+ albumListExtra = args.getString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_EXTRA);
+ albumListSize = args.getInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0);
+ }
+ if(primaryFragment) {
+ load(false);
+ } else {
+ invalidated = true;
+ }
+ if(name != null) {
+ setTitle(name);
+ }
+
+ return rootView;
+ }
+
+ @Override
+ public void onCreateOptionsMenu(com.actionbarsherlock.view.Menu menu, com.actionbarsherlock.view.MenuInflater menuInflater) {
+ if(licenseValid == null) {
+ menuInflater.inflate(R.menu.empty, menu);
+ }
+ else if(hideButtons) {
+ if(albumListType != null) {
+ menuInflater.inflate(R.menu.select_album_list, menu);
+ } else {
+ menuInflater.inflate(R.menu.select_album, menu);
+ }
+ } else {
+ if(podcastId == null) {
+ if(Util.isOffline(context)) {
+ menuInflater.inflate(R.menu.select_song_offline, menu);
+ }
+ else {
+ menuInflater.inflate(R.menu.select_song, menu);
+
+ if(playlistId == null) {
+ menu.removeItem(R.id.menu_remove_playlist);
+ }
+ }
+ } else {
+ if(Util.isOffline(context)) {
+ menuInflater.inflate(R.menu.select_podcast_episode_offline, menu);
+ }
+ else {
+ menuInflater.inflate(R.menu.select_podcast_episode, menu);
+ }
+ }
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(com.actionbarsherlock.view.MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.menu_play_now:
+ playNow(false, false);
+ return true;
+ case R.id.menu_play_last:
+ playNow(false, true);
+ return true;
+ case R.id.menu_shuffle:
+ playNow(true, false);
+ return true;
+ case R.id.menu_select:
+ selectAllOrNone();
+ return true;
+ case R.id.menu_download:
+ downloadBackground(false);
+ selectAll(false, false);
+ return true;
+ case R.id.menu_cache:
+ downloadBackground(true);
+ selectAll(false, false);
+ return true;
+ case R.id.menu_delete:
+ delete();
+ selectAll(false, false);
+ return true;
+ case R.id.menu_add_playlist:
+ addToPlaylist(getSelectedSongs());
+ return true;
+ case R.id.menu_remove_playlist:
+ removeFromPlaylist(playlistId, playlistName, getSelectedIndexes());
+ return true;
+ }
+
+ if(super.onOptionsItemSelected(item)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, view, menuInfo);
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
+
+ MusicDirectory.Entry entry = (MusicDirectory.Entry) entryList.getItemAtPosition(info.position);
+ onCreateContextMenu(menu, view, menuInfo, entry);
+ if(!entry.isVideo() && !Util.isOffline(context) && playlistId == null && (podcastId == null || Util.isOffline(context) && podcastId != null)) {
+ menu.removeItem(R.id.song_menu_remove_playlist);
+ }
+ if(podcastId != null && !Util.isOffline(context)) {
+ String status = ((PodcastEpisode)entry).getStatus();
+ if("completed".equals(status)) {
+ menu.removeItem(R.id.song_menu_server_download);
+ }
+ }
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem menuItem) {
+ if(!primaryFragment) {
+ return false;
+ }
+
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
+ Object selectedItem = entries.get(showHeader ? (info.position - 1) : info.position);
+
+ if(onContextItemSelected(menuItem, selectedItem)) {
+ return true;
+ }
+
+ switch (menuItem.getItemId()) {
+ case R.id.song_menu_remove_playlist:
+ removeFromPlaylist(playlistId, playlistName, Arrays.<Integer>asList(info.position - 1));
+ break;
+ case R.id.song_menu_server_download:
+ downloadPodcastEpisode((PodcastEpisode)selectedItem);
+ break;
+ case R.id.song_menu_server_delete:
+ deletePodcastEpisode((PodcastEpisode)selectedItem);
+ break;
+ }
+
+ return true;
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ if (position >= 0) {
+ MusicDirectory.Entry entry = (MusicDirectory.Entry) parent.getItemAtPosition(position);
+ if (entry.isDirectory()) {
+ int fragId = rootId;
+ if(albumListType != null && entry.getParent() != null) {
+ SubsonicFragment parentFragment = new SelectDirectoryFragment();
+ Bundle args = new Bundle();
+ args.putString(Constants.INTENT_EXTRA_NAME_ID, entry.getParent());
+ args.putString(Constants.INTENT_EXTRA_NAME_NAME, entry.getArtist());
+ parentFragment.setArguments(args);
+
+ replaceFragment(parentFragment, fragId);
+ fragId = parentFragment.getRootId();
+ }
+
+ SubsonicFragment fragment = new SelectDirectoryFragment();
+ Bundle args = new Bundle();
+ args.putString(Constants.INTENT_EXTRA_NAME_ID, entry.getId());
+ args.putString(Constants.INTENT_EXTRA_NAME_NAME, entry.getTitle());
+ fragment.setArguments(args);
+
+ replaceFragment(fragment, fragId);
+ } else if (entry.isVideo()) {
+ playVideo(entry);
+ } else if(entry instanceof PodcastEpisode) {
+ String status = ((PodcastEpisode)entry).getStatus();
+ if("error".equals(status)) {
+ Util.toast(context, R.string.select_podcasts_error);
+ return;
+ } else if(!"completed".equals(status)) {
+ Util.toast(context, R.string.select_podcasts_skipped);
+ return;
+ }
+
+ getDownloadService().clear();
+ List<MusicDirectory.Entry> podcasts = new ArrayList<MusicDirectory.Entry>(1);
+ podcasts.add(entry);
+ getDownloadService().download(podcasts, false, true, true, false);
+ Util.startActivityWithoutTransition(context, DownloadActivity.class);
+ }
+ }
+ }
+
+ @Override
+ protected void refresh(boolean refresh) {
+ load(refresh);
+ }
+
+ @Override
+ public int getRootId() {
+ return rootId;
+ }
+
+ private void load(boolean refresh) {
+ entryList.setVisibility(View.INVISIBLE);
+ emptyView.setVisibility(View.INVISIBLE);
+ if (playlistId != null) {
+ getPlaylist(playlistId, playlistName);
+ } else if(podcastId != null) {
+ getPodcast(podcastId, podcastName);
+ } else if (albumListType != null) {
+ getAlbumList(albumListType, albumListSize);
+ } else {
+ getMusicDirectory(id, name, refresh);
+ }
+ }
+
+ private void getMusicDirectory(final String id, final String name, final boolean refresh) {
+ setTitle(name);
+
+ new LoadTask() {
+ @Override
+ protected MusicDirectory load(MusicService service) throws Exception {
+ return service.getMusicDirectory(id, name, refresh, context, this);
+ }
+ }.execute();
+ }
+
+ private void getPlaylist(final String playlistId, final String playlistName) {
+ setTitle(playlistName);
+
+ new LoadTask() {
+ @Override
+ protected MusicDirectory load(MusicService service) throws Exception {
+ return service.getPlaylist(playlistId, playlistName, context, this);
+ }
+ }.execute();
+ }
+
+ private void getPodcast(final String podcastId, final String podcastName) {
+ setTitle(podcastName);
+
+ new LoadTask() {
+ @Override
+ protected MusicDirectory load(MusicService service) throws Exception {
+ return service.getPodcastEpisodes(podcastId, context, this);
+ }
+ }.execute();
+ }
+
+ private void getAlbumList(final String albumListType, final int size) {
+ showHeader = false;
+
+ if ("newest".equals(albumListType)) {
+ setTitle(R.string.main_albums_newest);
+ } else if ("random".equals(albumListType)) {
+ setTitle(R.string.main_albums_random);
+ } else if ("highest".equals(albumListType)) {
+ setTitle(R.string.main_albums_highest);
+ } else if ("recent".equals(albumListType)) {
+ setTitle(R.string.main_albums_recent);
+ } else if ("frequent".equals(albumListType)) {
+ setTitle(R.string.main_albums_frequent);
+ } else if ("starred".equals(albumListType)) {
+ setTitle(R.string.main_albums_starred);
+ } else if("genres".equals(albumListType)) {
+ setTitle(albumListExtra);
+ }
+
+ new LoadTask() {
+ @Override
+ protected MusicDirectory load(MusicService service) throws Exception {
+ MusicDirectory result;
+ if ("starred".equals(albumListType)) {
+ result = service.getStarredList(context, this);
+ } else if("genres".equals(albumListType)) {
+ result = service.getSongsByGenre(albumListExtra, size, 0, context, this);
+ } else {
+ result = service.getAlbumList(albumListType, size, 0, context, this);
+ }
+ return result;
+ }
+ }.execute();
+ }
+
+ private abstract class LoadTask extends TabBackgroundTask<Pair<MusicDirectory, Boolean>> {
+
+ public LoadTask() {
+ super(SelectDirectoryFragment.this);
+ }
+
+ protected abstract MusicDirectory load(MusicService service) throws Exception;
+
+ @Override
+ protected Pair<MusicDirectory, Boolean> doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ MusicDirectory dir = load(musicService);
+ boolean valid = musicService.isLicenseValid(context, this);
+ return new Pair<MusicDirectory, Boolean>(dir, valid);
+ }
+
+ @Override
+ protected void done(Pair<MusicDirectory, Boolean> result) {
+ entries = result.getFirst().getChildren();
+
+ int songCount = 0;
+ for (MusicDirectory.Entry entry : entries) {
+ if (!entry.isDirectory()) {
+ songCount++;
+ }
+ }
+
+ if (songCount > 0) {
+ if(showHeader) {
+ View header = createHeader(entries);
+ if(header != null) {
+ entryList.addHeaderView(header, null, false);
+ }
+ }
+ } else {
+ showHeader = false;
+ hideButtons = true;
+ }
+
+ emptyView.setVisibility(entries.isEmpty() ? View.VISIBLE : View.GONE);
+ entryAdapter = new EntryAdapter(context, getImageLoader(), entries, (podcastId == null) ? true : false);
+ if(albumListType == null || "starred".equals(albumListType)) {
+ entryList.setAdapter(entryAdapter);
+ } else {
+ entryList.setAdapter(new AlbumListAdapter(context, entryAdapter, albumListType, albumListExtra, albumListSize));
+ }
+ entryList.setVisibility(View.VISIBLE);
+ licenseValid = result.getSecond();
+ context.invalidateOptionsMenu();
+
+ Bundle args = getArguments();
+ boolean playAll = args.getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false);
+ if (playAll && songCount > 0) {
+ playAll(args.getBoolean(Constants.INTENT_EXTRA_NAME_SHUFFLE, false), false);
+ }
+ }
+ }
+
+ private void playNow(final boolean shuffle, final boolean append) {
+ if(getSelectedSongs().size() > 0) {
+ download(append, false, !append, false, shuffle);
+ selectAll(false, false);
+ }
+ else {
+ playAll(shuffle, append);
+ }
+ }
+ private void playAll(final boolean shuffle, final boolean append) {
+ boolean hasSubFolders = false;
+ for (int i = 0; i < entryList.getCount(); i++) {
+ MusicDirectory.Entry entry = (MusicDirectory.Entry) entryList.getItemAtPosition(i);
+ if (entry != null && entry.isDirectory()) {
+ hasSubFolders = true;
+ break;
+ }
+ }
+
+ if (hasSubFolders && id != null) {
+ downloadRecursively(id, false, append, !append, shuffle, false);
+ } else {
+ selectAll(true, false);
+ download(append, false, !append, false, shuffle);
+ selectAll(false, false);
+ }
+ }
+
+ private void selectAllOrNone() {
+ boolean someUnselected = false;
+ int count = entryList.getCount();
+ for (int i = 0; i < count; i++) {
+ if (!entryList.isItemChecked(i) && entryList.getItemAtPosition(i) instanceof MusicDirectory.Entry) {
+ someUnselected = true;
+ break;
+ }
+ }
+ selectAll(someUnselected, true);
+ }
+
+ private void selectAll(boolean selected, boolean toast) {
+ int count = entryList.getCount();
+ int selectedCount = 0;
+ for (int i = 0; i < count; i++) {
+ MusicDirectory.Entry entry = (MusicDirectory.Entry) entryList.getItemAtPosition(i);
+ if (entry != null && !entry.isDirectory() && !entry.isVideo()) {
+ entryList.setItemChecked(i, selected);
+ selectedCount++;
+ }
+ }
+
+ // Display toast: N tracks selected / N tracks unselected
+ if (toast) {
+ int toastResId = selected ? R.string.select_album_n_selected
+ : R.string.select_album_n_unselected;
+ Util.toast(context, context.getString(toastResId, selectedCount));
+ }
+ }
+
+ private List<MusicDirectory.Entry> getSelectedSongs() {
+ List<MusicDirectory.Entry> songs = new ArrayList<MusicDirectory.Entry>(10);
+ int count = entryList.getCount();
+ for (int i = 0; i < count; i++) {
+ if (entryList.isItemChecked(i)) {
+ songs.add((MusicDirectory.Entry) entryList.getItemAtPosition(i));
+ }
+ }
+ return songs;
+ }
+
+ private List<Integer> getSelectedIndexes() {
+ List<Integer> indexes = new ArrayList<Integer>();
+
+ int count = entryList.getCount();
+ for (int i = 0; i < count; i++) {
+ if (entryList.isItemChecked(i)) {
+ indexes.add(i - 1);
+ }
+ }
+
+ return indexes;
+ }
+
+ private void download(final boolean append, final boolean save, final boolean autoplay, final boolean playNext, final boolean shuffle) {
+ if (getDownloadService() == null) {
+ return;
+ }
+
+ final List<MusicDirectory.Entry> songs = getSelectedSongs();
+ Runnable onValid = new Runnable() {
+ @Override
+ public void run() {
+ if (!append) {
+ getDownloadService().clear();
+ }
+
+ warnIfNetworkOrStorageUnavailable();
+ getDownloadService().download(songs, save, autoplay, playNext, shuffle);
+ if (playlistName != null) {
+ getDownloadService().setSuggestedPlaylistName(playlistName, playlistId);
+ } else {
+ getDownloadService().setSuggestedPlaylistName(null, null);
+ }
+ if (autoplay) {
+ Util.startActivityWithoutTransition(context, DownloadActivity.class);
+ if(context instanceof SearchActivity) {
+ context.finish();
+ }
+ } else if (save) {
+ Util.toast(context,
+ context.getResources().getQuantityString(R.plurals.select_album_n_songs_downloading, songs.size(), songs.size()));
+ } else if (append) {
+ Util.toast(context,
+ context.getResources().getQuantityString(R.plurals.select_album_n_songs_added, songs.size(), songs.size()));
+ }
+ }
+ };
+
+ checkLicenseAndTrialPeriod(onValid);
+ }
+ private void downloadBackground(final boolean save) {
+ List<MusicDirectory.Entry> songs = getSelectedSongs();
+ if(songs.isEmpty()) {
+ selectAll(true, false);
+ songs = getSelectedSongs();
+ }
+ downloadBackground(save, songs);
+ }
+ private void downloadBackground(final boolean save, final List<MusicDirectory.Entry> songs) {
+ if (getDownloadService() == null) {
+ return;
+ }
+
+ Runnable onValid = new Runnable() {
+ @Override
+ public void run() {
+ warnIfNetworkOrStorageUnavailable();
+ getDownloadService().downloadBackground(songs, save);
+
+ Util.toast(context,
+ context.getResources().getQuantityString(R.plurals.select_album_n_songs_downloading, songs.size(), songs.size()));
+ }
+ };
+
+ checkLicenseAndTrialPeriod(onValid);
+ }
+
+ private void delete() {
+ List<MusicDirectory.Entry> songs = getSelectedSongs();
+ if(songs.isEmpty()) {
+ selectAll(true, false);
+ songs = getSelectedSongs();
+ }
+ if (getDownloadService() != null) {
+ getDownloadService().delete(songs);
+ }
+ }
+
+ public void removeFromPlaylist(final String id, final String name, final List<Integer> indexes) {
+ new LoadingTask<Void>(context, true) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.removeFromPlaylist(id, indexes, context, null);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ for(int i = indexes.size() - 1; i >= 0; i--) {
+ entryList.setItemChecked(indexes.get(i) + 1, false);
+ entryAdapter.removeAt(indexes.get(i));
+ }
+ entryAdapter.notifyDataSetChanged();
+ Util.toast(context, context.getResources().getString(R.string.removed_playlist, indexes.size(), name));
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.updated_playlist_error, name) + " " + getErrorMessage(error);
+ }
+
+ Util.toast(context, msg, false);
+ }
+ }.execute();
+ }
+
+ public void downloadPodcastEpisode(final PodcastEpisode episode) {
+ new LoadingTask<Void>(context, true) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.downloadPodcastEpisode(episode.getEpisodeId(), context, null);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ Util.toast(context, context.getResources().getString(R.string.select_podcasts_downloading, episode.getTitle()));
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ Util.toast(context, getErrorMessage(error), false);
+ }
+ }.execute();
+ }
+
+ public void deletePodcastEpisode(final PodcastEpisode episode) {
+ Util.confirmDialog(context, R.string.common_delete, episode.getTitle(), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ new LoadingTask<Void>(context, true) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.deletePodcastEpisode(episode.getEpisodeId(), context, null);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ entries.remove(episode);
+ entryAdapter.notifyDataSetChanged();
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ Util.toast(context, getErrorMessage(error), false);
+ }
+ }.execute();
+ }
+ });
+ }
+
+ private void checkLicenseAndTrialPeriod(Runnable onValid) {
+ if (licenseValid) {
+ onValid.run();
+ return;
+ }
+
+ int trialDaysLeft = Util.getRemainingTrialDays(context);
+ Log.i(TAG, trialDaysLeft + " trial days left.");
+
+ if (trialDaysLeft == 0) {
+ showDonationDialog(trialDaysLeft, null);
+ } else if (trialDaysLeft < Constants.FREE_TRIAL_DAYS / 2) {
+ showDonationDialog(trialDaysLeft, onValid);
+ } else {
+ Util.toast(context, context.getResources().getString(R.string.select_album_not_licensed, trialDaysLeft));
+ onValid.run();
+ }
+ }
+
+ private void showDonationDialog(int trialDaysLeft, final Runnable onValid) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setIcon(android.R.drawable.ic_dialog_info);
+
+ if (trialDaysLeft == 0) {
+ builder.setTitle(R.string.select_album_donate_dialog_0_trial_days_left);
+ } else {
+ builder.setTitle(context.getResources().getQuantityString(R.plurals.select_album_donate_dialog_n_trial_days_left,
+ trialDaysLeft, trialDaysLeft));
+ }
+
+ builder.setMessage(R.string.select_album_donate_dialog_message);
+
+ builder.setPositiveButton(R.string.select_album_donate_dialog_now,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(Constants.DONATION_URL)));
+ }
+ });
+
+ builder.setNegativeButton(R.string.select_album_donate_dialog_later,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ dialogInterface.dismiss();
+ if (onValid != null) {
+ onValid.run();
+ }
+ }
+ });
+
+ builder.create().show();
+ }
+
+ private View createHeader(List<MusicDirectory.Entry> entries) {
+ View header = entryList.findViewById(R.id.select_album_header);
+ boolean add = false;
+ if(header == null) {
+ header = LayoutInflater.from(context).inflate(R.layout.select_album_header, entryList, false);
+ add = true;
+ }
+
+ View coverArtView = header.findViewById(R.id.select_album_art);
+ getImageLoader().loadImage(coverArtView, entries.get(random.nextInt(entries.size())), false, true);
+
+ TextView titleView = (TextView) header.findViewById(R.id.select_album_title);
+ if(playlistName != null) {
+ titleView.setText(playlistName);
+ } else if(podcastName != null) {
+ titleView.setText(podcastName);
+ titleView.setPadding(0, 6, 4, 8);
+ } else if(name != null) {
+ titleView.setText(name);
+ }
+
+ int songCount = 0;
+
+ Set<String> artists = new HashSet<String>();
+ Integer totalDuration = 0;
+ for (MusicDirectory.Entry entry : entries) {
+ if (!entry.isDirectory()) {
+ songCount++;
+ if (entry.getArtist() != null) {
+ artists.add(entry.getArtist());
+ }
+ Integer duration = entry.getDuration();
+ if(duration != null) {
+ totalDuration += duration;
+ }
+ }
+ }
+
+ TextView artistView = (TextView) header.findViewById(R.id.select_album_artist);
+ if(podcastDescription != null) {
+ artistView.setText(podcastDescription);
+ artistView.setSingleLine(false);
+ artistView.setLines(5);
+ } else if (artists.size() == 1) {
+ artistView.setText(artists.iterator().next());
+ artistView.setVisibility(View.VISIBLE);
+ } else {
+ artistView.setVisibility(View.GONE);
+ }
+
+ TextView songCountView = (TextView) header.findViewById(R.id.select_album_song_count);
+ TextView songLengthView = (TextView) header.findViewById(R.id.select_album_song_length);
+ if(podcastDescription == null) {
+ String s = context.getResources().getQuantityString(R.plurals.select_album_n_songs, songCount, songCount);
+ songCountView.setText(s.toUpperCase());
+ songLengthView.setText(Util.formatDuration(totalDuration));
+ } else {
+ songCountView.setVisibility(View.GONE);
+ songLengthView.setVisibility(View.GONE);
+ }
+
+ if(add) {
+ return header;
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/fragments/SelectGenreFragment.java b/src/github/daneren2005/dsub/fragments/SelectGenreFragment.java
new file mode 100644
index 00000000..623aba4e
--- /dev/null
+++ b/src/github/daneren2005/dsub/fragments/SelectGenreFragment.java
@@ -0,0 +1,142 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.fragments;
+
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ListView;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.Genre;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import github.daneren2005.dsub.util.BackgroundTask;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.TabBackgroundTask;
+import github.daneren2005.dsub.view.GenreAdapter;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+import com.actionbarsherlock.view.MenuInflater;
+import java.util.ArrayList;
+import java.util.List;
+
+public class SelectGenreFragment extends SubsonicFragment implements AdapterView.OnItemClickListener {
+ private static final String TAG = SelectGenreFragment.class.getSimpleName();
+ private ListView genreListView;
+ private View emptyView;
+
+ @Override
+ public void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
+ rootView = inflater.inflate(R.layout.select_genres, container, false);
+
+ genreListView = (ListView)rootView.findViewById(R.id.select_genre_list);
+ genreListView.setOnItemClickListener(this);
+ emptyView = rootView.findViewById(R.id.select_genre_empty);
+ refresh();
+
+ return rootView;
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) {
+ menuInflater.inflate(R.menu.select_genres, menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if(super.onOptionsItemSelected(item)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public void setPrimaryFragment(boolean primary) {
+ super.setPrimaryFragment(primary);
+ if(rootView != null) {
+ if(primary) {
+ ((ViewGroup)rootView).getChildAt(0).setVisibility(View.VISIBLE);
+ } else {
+ ((ViewGroup)rootView).getChildAt(0).setVisibility(View.GONE);
+ }
+ }
+ }
+
+ @Override
+ protected void refresh(boolean refresh) {
+ load(refresh);
+ }
+
+ private void load(final boolean refresh) {
+ setTitle(R.string.main_albums_genres);
+ genreListView.setVisibility(View.INVISIBLE);
+
+ BackgroundTask<List<Genre>> task = new TabBackgroundTask<List<Genre>>(this) {
+ @Override
+ protected List<Genre> doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+
+ List<Genre> genres = new ArrayList<Genre>();
+
+ try {
+ genres = musicService.getGenres(refresh, context, this);
+ } catch (Exception x) {
+ Log.e(TAG, "Failed to load genres", x);
+ }
+
+ return genres;
+ }
+
+ @Override
+ protected void done(List<Genre> result) {
+ emptyView.setVisibility(result == null || result.isEmpty() ? View.VISIBLE : View.GONE);
+
+ if (result != null) {
+ genreListView.setAdapter(new GenreAdapter(context, result));
+ genreListView.setVisibility(View.VISIBLE);
+ }
+ }
+ };
+ task.execute();
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ Genre genre = (Genre) parent.getItemAtPosition(position);
+
+ SubsonicFragment fragment = new SelectDirectoryFragment();
+ Bundle args = new Bundle();
+ args.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, "genres");
+ args.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 20);
+ args.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0);
+ args.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_EXTRA, genre.getName());
+ fragment.setArguments(args);
+
+ replaceFragment(fragment, R.id.select_genre_layout);
+ }
+}
diff --git a/src/github/daneren2005/dsub/fragments/SelectPlaylistFragment.java b/src/github/daneren2005/dsub/fragments/SelectPlaylistFragment.java
new file mode 100644
index 00000000..de74cdb2
--- /dev/null
+++ b/src/github/daneren2005/dsub/fragments/SelectPlaylistFragment.java
@@ -0,0 +1,286 @@
+package github.daneren2005.dsub.fragments;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v4.app.FragmentTransaction;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.ListView;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.Playlist;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import github.daneren2005.dsub.service.OfflineException;
+import github.daneren2005.dsub.service.ServerTooOldException;
+import github.daneren2005.dsub.util.BackgroundTask;
+import github.daneren2005.dsub.util.CacheCleaner;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.LoadingTask;
+import github.daneren2005.dsub.util.TabBackgroundTask;
+import github.daneren2005.dsub.util.Util;
+import github.daneren2005.dsub.view.PlaylistAdapter;
+import java.util.List;
+
+public class SelectPlaylistFragment extends SubsonicFragment implements AdapterView.OnItemClickListener {
+ private static final String TAG = SelectPlaylistFragment.class.getSimpleName();
+
+ private ListView list;
+ private View emptyTextView;
+ private PlaylistAdapter playlistAdapter;
+
+ @Override
+ public void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
+ rootView = inflater.inflate(R.layout.select_playlist, container, false);
+
+ list = (ListView) rootView.findViewById(R.id.select_playlist_list);
+ emptyTextView = rootView.findViewById(R.id.select_playlist_empty);
+ list.setOnItemClickListener(this);
+ registerForContextMenu(list);
+ if(!primaryFragment) {
+ invalidated = true;
+ } else {
+ refresh(false);
+ }
+
+ return rootView;
+ }
+
+ @Override
+ public void onCreateOptionsMenu(com.actionbarsherlock.view.Menu menu, com.actionbarsherlock.view.MenuInflater menuInflater) {
+ menuInflater.inflate(R.menu.select_playlist, menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(com.actionbarsherlock.view.MenuItem item) {
+ if(super.onOptionsItemSelected(item)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, view, menuInfo);
+
+ MenuInflater inflater = context.getMenuInflater();
+ if (Util.isOffline(context)) {
+ inflater.inflate(R.menu.select_playlist_context_offline, menu);
+ }
+ else {
+ inflater.inflate(R.menu.select_playlist_context, menu);
+ }
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem menuItem) {
+ if(!primaryFragment) {
+ return false;
+ }
+
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
+ Playlist playlist = (Playlist) list.getItemAtPosition(info.position);
+
+ SubsonicFragment fragment;
+ Bundle args;
+ FragmentTransaction trans;
+ switch (menuItem.getItemId()) {
+ case R.id.playlist_menu_download:
+ downloadPlaylist(playlist.getId(), playlist.getName(), false, true, false, false, true);
+ break;
+ case R.id.playlist_menu_pin:
+ downloadPlaylist(playlist.getId(), playlist.getName(), true, true, false, false, true);
+ break;
+ case R.id.playlist_menu_play_now:
+ fragment = new SelectDirectoryFragment();
+ args = new Bundle();
+ args.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, playlist.getId());
+ args.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME, playlist.getName());
+ args.putBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, true);
+ fragment.setArguments(args);
+
+ replaceFragment(fragment, R.id.select_playlist_layout);
+ break;
+ case R.id.playlist_menu_play_shuffled:
+ fragment = new SelectDirectoryFragment();
+ args = new Bundle();
+ args.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, playlist.getId());
+ args.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME, playlist.getName());
+ args.putBoolean(Constants.INTENT_EXTRA_NAME_SHUFFLE, true);
+ args.putBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, true);
+ fragment.setArguments(args);
+
+ replaceFragment(fragment, R.id.select_playlist_layout);
+ break;
+ case R.id.playlist_menu_delete:
+ deletePlaylist(playlist);
+ break;
+ case R.id.playlist_info:
+ displayPlaylistInfo(playlist);
+ break;
+ case R.id.playlist_update_info:
+ updatePlaylistInfo(playlist);
+ break;
+ default:
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ Playlist playlist = (Playlist) parent.getItemAtPosition(position);
+
+ SubsonicFragment fragment = new SelectDirectoryFragment();
+ Bundle args = new Bundle();
+ args.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, playlist.getId());
+ args.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME, playlist.getName());
+ fragment.setArguments(args);
+
+ replaceFragment(fragment, R.id.select_playlist_layout);
+ }
+
+ @Override
+ protected void refresh(boolean refresh) {
+ load(refresh);
+ }
+
+ private void load(final boolean refresh) {
+ setTitle(R.string.playlist_label);
+ list.setVisibility(View.INVISIBLE);
+
+ BackgroundTask<List<Playlist>> task = new TabBackgroundTask<List<Playlist>>(this) {
+ @Override
+ protected List<Playlist> doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ List<Playlist> playlists = musicService.getPlaylists(refresh, context, this);
+ if(!Util.isOffline(context) && refresh) {
+ new CacheCleaner(context, getDownloadService()).cleanPlaylists(playlists);
+ }
+ return playlists;
+ }
+
+ @Override
+ protected void done(List<Playlist> result) {
+ list.setAdapter(playlistAdapter = new PlaylistAdapter(context, result));
+ emptyTextView.setVisibility(result.isEmpty() ? View.VISIBLE : View.GONE);
+ list.setVisibility(View.VISIBLE);
+ }
+ };
+ task.execute();
+ }
+
+ private void deletePlaylist(final Playlist playlist) {
+ Util.confirmDialog(context, R.string.common_delete, playlist.getName(), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ new LoadingTask<Void>(context, false) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.deletePlaylist(playlist.getId(), context, null);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ playlistAdapter.remove(playlist);
+ playlistAdapter.notifyDataSetChanged();
+ Util.toast(context, context.getResources().getString(R.string.menu_deleted_playlist, playlist.getName()));
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.menu_deleted_playlist_error, playlist.getName()) + " " + getErrorMessage(error);
+ }
+
+ Util.toast(context, msg, false);
+ }
+ }.execute();
+ }
+ });
+ }
+
+ private void displayPlaylistInfo(final Playlist playlist) {
+ String message = "Owner: " + playlist.getOwner() + "\nComments: " +
+ ((playlist.getComment() == null) ? "" : playlist.getComment()) +
+ "\nSong Count: " + playlist.getSongCount() +
+ ((playlist.getPublic() == null) ? "" : ("\nPublic: " + playlist.getPublic())) +
+ "\nCreation Date: " + playlist.getCreated().replace('T', ' ');
+ Util.info(context, playlist.getName(), message);
+ }
+
+ private void updatePlaylistInfo(final Playlist playlist) {
+ View dialogView = context.getLayoutInflater().inflate(R.layout.update_playlist, null);
+ final EditText nameBox = (EditText)dialogView.findViewById(R.id.get_playlist_name);
+ final EditText commentBox = (EditText)dialogView.findViewById(R.id.get_playlist_comment);
+ final CheckBox publicBox = (CheckBox)dialogView.findViewById(R.id.get_playlist_public);
+
+ nameBox.setText(playlist.getName());
+ commentBox.setText(playlist.getComment());
+ Boolean pub = playlist.getPublic();
+ if(pub == null) {
+ publicBox.setEnabled(false);
+ } else {
+ publicBox.setChecked(pub);
+ }
+
+ new AlertDialog.Builder(context)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setTitle(R.string.playlist_update_info)
+ .setView(dialogView)
+ .setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ new LoadingTask<Void>(context, false) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.updatePlaylist(playlist.getId(), nameBox.getText().toString(), commentBox.getText().toString(), publicBox.isChecked(), context, null);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ refresh();
+ Util.toast(context, context.getResources().getString(R.string.playlist_updated_info, playlist.getName()));
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.playlist_updated_info_error, playlist.getName()) + " " + getErrorMessage(error);
+ }
+
+ Util.toast(context, msg, false);
+ }
+ }.execute();
+ }
+
+ })
+ .setNegativeButton(R.string.common_cancel, null)
+ .show();
+ }
+}
diff --git a/src/github/daneren2005/dsub/fragments/SelectPodcastsFragment.java b/src/github/daneren2005/dsub/fragments/SelectPodcastsFragment.java
new file mode 100644
index 00000000..f0f78569
--- /dev/null
+++ b/src/github/daneren2005/dsub/fragments/SelectPodcastsFragment.java
@@ -0,0 +1,310 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.fragments;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.text.SpannableString;
+import android.text.method.LinkMovementMethod;
+import android.text.util.Linkify;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ListView;
+import android.widget.TextView;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuInflater;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.PodcastChannel;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import github.daneren2005.dsub.service.OfflineException;
+import github.daneren2005.dsub.service.ServerTooOldException;
+import github.daneren2005.dsub.util.BackgroundTask;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.LoadingTask;
+import github.daneren2005.dsub.util.SilentBackgroundTask;
+import github.daneren2005.dsub.util.TabBackgroundTask;
+import github.daneren2005.dsub.util.Util;
+import github.daneren2005.dsub.view.PodcastChannelAdapter;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ *
+ * @author Scott
+ */
+public class SelectPodcastsFragment extends SubsonicFragment implements AdapterView.OnItemClickListener {
+ private static final String TAG = SelectPodcastsFragment.class.getSimpleName();
+ private ListView podcastListView;
+ private PodcastChannelAdapter podcastAdapter;
+ private View emptyView;
+
+ @Override
+ public void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
+ rootView = inflater.inflate(R.layout.select_podcasts, container, false);
+
+ podcastListView = (ListView)rootView.findViewById(R.id.select_podcasts_list);
+ podcastListView.setOnItemClickListener(this);
+ registerForContextMenu(podcastListView);
+ emptyView = rootView.findViewById(R.id.select_podcasts_empty);
+ if(!primaryFragment) {
+ invalidated = true;
+ } else {
+ refresh(false);
+ }
+
+ return rootView;
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) {
+ menuInflater.inflate(R.menu.select_podcasts, menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(com.actionbarsherlock.view.MenuItem item) {
+ if(super.onOptionsItemSelected(item)) {
+ return true;
+ }
+
+ switch (item.getItemId()) {
+ case R.id.menu_check:
+ refreshPodcasts();
+ break;
+ case R.id.menu_add_podcast:
+ addNewPodcast();
+ break;
+ }
+
+ return false;
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, view, menuInfo);
+ if(!Util.isOffline(context)) {
+ android.view.MenuInflater inflater = context.getMenuInflater();
+ inflater.inflate(R.menu.select_podcasts_context, menu);
+ }
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem menuItem) {
+ if(!primaryFragment) {
+ return false;
+ }
+
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
+ PodcastChannel channel = (PodcastChannel) podcastListView.getItemAtPosition(info.position);
+
+ switch (menuItem.getItemId()) {
+ case R.id.podcast_channel_info:
+ displayPodcastInfo(channel);
+ break;
+ case R.id.podcast_channel_delete:
+ deletePodcast(channel);
+ break;
+ }
+
+ return true;
+ }
+
+ @Override
+ protected void refresh(final boolean refresh) {
+ setTitle(R.string.button_bar_podcasts);
+ podcastListView.setVisibility(View.INVISIBLE);
+
+ BackgroundTask<List<PodcastChannel>> task = new TabBackgroundTask<List<PodcastChannel>>(this) {
+ @Override
+ protected List<PodcastChannel> doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+
+ List<PodcastChannel> channels = new ArrayList<PodcastChannel>();
+
+ try {
+ channels = musicService.getPodcastChannels(refresh, context, this);
+ } catch (Exception x) {
+ Log.e(TAG, "Failed to load podcasts", x);
+ }
+
+ return channels;
+ }
+
+ @Override
+ protected void done(List<PodcastChannel> result) {
+ emptyView.setVisibility(result == null || result.isEmpty() ? View.VISIBLE : View.GONE);
+
+ if (result != null) {
+ podcastListView.setAdapter(podcastAdapter = new PodcastChannelAdapter(context, result));
+ podcastListView.setVisibility(View.VISIBLE);
+ }
+ }
+ };
+ task.execute();
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ PodcastChannel channel = (PodcastChannel) parent.getItemAtPosition(position);
+
+ if("error".equals(channel.getStatus())) {
+ Util.toast(context, context.getResources().getString(R.string.select_podcasts_invalid_podcast_channel, channel.getErrorMessage() == null ? "error" : channel.getErrorMessage()));
+ } else if("downloading".equals(channel.getStatus())) {
+ Util.toast(context, R.string.select_podcasts_initializing);
+ } else {
+ SubsonicFragment fragment = new SelectDirectoryFragment();
+ Bundle args = new Bundle();
+ args.putString(Constants.INTENT_EXTRA_NAME_PODCAST_ID, channel.getId());
+ args.putString(Constants.INTENT_EXTRA_NAME_PODCAST_NAME, channel.getName());
+ args.putString(Constants.INTENT_EXTRA_NAME_PODCAST_DESCRIPTION, channel.getDescription());
+ fragment.setArguments(args);
+
+ replaceFragment(fragment, R.id.select_podcasts_layout);
+ }
+ }
+
+ public void refreshPodcasts() {
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.refreshPodcasts(context, null);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ Util.toast(context, R.string.select_podcasts_refreshing);
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ Util.toast(context, getErrorMessage(error), false);
+ }
+ }.execute();
+ }
+
+ private void addNewPodcast() {
+ View dialogView = context.getLayoutInflater().inflate(R.layout.create_podcast, null);
+ final TextView urlBox = (TextView) dialogView.findViewById(R.id.create_podcast_url);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.menu_add_podcast)
+ .setView(dialogView)
+ .setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ addNewPodcast(urlBox.getText().toString());
+ }
+ })
+ .setNegativeButton(R.string.common_cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.cancel();
+ }
+ })
+ .setCancelable(true);
+
+ AlertDialog dialog = builder.create();
+ dialog.show();
+ }
+ private void addNewPodcast(final String url) {
+ new LoadingTask<Void>(context, false) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.createPodcastChannel(url, context, null);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ refresh();
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.select_podcasts_created_error) + " " + getErrorMessage(error);
+ }
+
+ Util.toast(context, msg, false);
+ }
+ }.execute();
+ }
+
+ private void displayPodcastInfo(final PodcastChannel channel) {
+ String message = ((channel.getName()) == null ? "" : "Title: " + channel.getName()) +
+ "\nURL: " + channel.getUrl() +
+ "\nStatus: " + channel.getStatus() +
+ ((channel.getErrorMessage()) == null ? "" : "\nError Message: " + channel.getErrorMessage()) +
+ ((channel.getDescription()) == null ? "" : "\nDescription: " + channel.getDescription());
+
+ Util.info(context, channel.getName(), message);
+ }
+
+ private void deletePodcast(final PodcastChannel channel) {
+ Util.confirmDialog(context, R.string.common_delete, channel.getName(), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ new LoadingTask<Void>(context, false) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.deletePodcastChannel(channel.getId(), context, null);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ podcastAdapter.remove(channel);
+ podcastAdapter.notifyDataSetChanged();
+ Util.toast(context, context.getResources().getString(R.string.select_podcasts_deleted, channel.getName()));
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.select_podcasts_deleted_error, channel.getName()) + " " + getErrorMessage(error);
+ }
+
+ Util.toast(context, msg, false);
+ }
+ }.execute();
+ }
+ });
+ }
+}
diff --git a/src/github/daneren2005/dsub/fragments/SubsonicFragment.java b/src/github/daneren2005/dsub/fragments/SubsonicFragment.java
new file mode 100644
index 00000000..9e8ec29c
--- /dev/null
+++ b/src/github/daneren2005/dsub/fragments/SubsonicFragment.java
@@ -0,0 +1,968 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.fragments;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.media.MediaMetadataRetriever;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.TextView;
+import com.actionbarsherlock.app.SherlockFragment;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.activity.DownloadActivity;
+import github.daneren2005.dsub.activity.HelpActivity;
+import github.daneren2005.dsub.activity.MainActivity;
+import github.daneren2005.dsub.activity.SearchActivity;
+import github.daneren2005.dsub.activity.SettingsActivity;
+import github.daneren2005.dsub.activity.SubsonicActivity;
+import github.daneren2005.dsub.domain.Artist;
+import github.daneren2005.dsub.domain.Genre;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.Playlist;
+import github.daneren2005.dsub.domain.PodcastEpisode;
+import github.daneren2005.dsub.service.DownloadFile;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.service.DownloadServiceImpl;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import github.daneren2005.dsub.service.OfflineException;
+import github.daneren2005.dsub.service.ServerTooOldException;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.ImageLoader;
+import github.daneren2005.dsub.util.SilentBackgroundTask;
+import github.daneren2005.dsub.util.LoadingTask;
+import github.daneren2005.dsub.util.Util;
+import java.io.File;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Random;
+
+public class SubsonicFragment extends SherlockFragment {
+ private static final String TAG = SubsonicFragment.class.getSimpleName();
+ private static int internalID = Integer.MAX_VALUE;
+ private static int TAG_INC = 10;
+ private int tag;
+
+ protected SubsonicActivity context;
+ protected CharSequence title = "DSub";
+ protected CharSequence subtitle = null;
+ protected View rootView;
+ protected boolean primaryFragment = false;
+ protected boolean invalidated = false;
+ protected static Random random = new Random();
+
+ public SubsonicFragment() {
+ super();
+ tag = TAG_INC++;
+ }
+
+ @Override
+ public void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ context = (SubsonicActivity)activity;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(com.actionbarsherlock.view.MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.menu_refresh:
+ refresh(true);
+ return true;
+ case R.id.menu_shuffle:
+ onShuffleRequested();
+ return true;
+ case R.id.menu_search:
+ context.onSearchRequested();
+ return true;
+ case R.id.menu_exit:
+ exit();
+ return true;
+ case R.id.menu_settings:
+ startActivity(new Intent(context, SettingsActivity.class));
+ return true;
+ case R.id.menu_help:
+ startActivity(new Intent(context, HelpActivity.class));
+ return true;
+ }
+
+ return false;
+ }
+
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo, Object selected) {
+ MenuInflater inflater = context.getMenuInflater();
+
+ if(selected instanceof MusicDirectory.Entry) {
+ MusicDirectory.Entry entry = (MusicDirectory.Entry) selected;
+ if(entry instanceof PodcastEpisode && !entry.isVideo()) {
+ if(Util.isOffline(context)) {
+ inflater.inflate(R.menu.select_podcast_episode_context_offline, menu);
+ }
+ else {
+ inflater.inflate(R.menu.select_podcast_episode_context, menu);
+ }
+ }
+ else if (entry.isDirectory()) {
+ if(Util.isOffline(context)) {
+ inflater.inflate(R.menu.select_album_context_offline, menu);
+ }
+ else {
+ inflater.inflate(R.menu.select_album_context, menu);
+ }
+ menu.findItem(entry.isDirectory() ? R.id.album_menu_star : R.id.song_menu_star).setTitle(entry.isStarred() ? R.string.common_unstar : R.string.common_star);
+ } else if(!entry.isVideo()) {
+ if(Util.isOffline(context)) {
+ inflater.inflate(R.menu.select_song_context_offline, menu);
+ }
+ else {
+ inflater.inflate(R.menu.select_song_context, menu);
+ }
+ menu.findItem(entry.isDirectory() ? R.id.album_menu_star : R.id.song_menu_star).setTitle(entry.isStarred() ? R.string.common_unstar : R.string.common_star);
+ } else {
+ if(Util.isOffline(context)) {
+ inflater.inflate(R.menu.select_video_context_offline, menu);
+ }
+ else {
+ inflater.inflate(R.menu.select_video_context, menu);
+ }
+ }
+ } else if(selected instanceof Artist) {
+ if(Util.isOffline(context)) {
+ inflater.inflate(R.menu.select_artist_context_offline, menu);
+ }
+ else {
+ inflater.inflate(R.menu.select_artist_context, menu);
+ }
+ }
+ }
+
+ public boolean onContextItemSelected(MenuItem menuItem, Object selectedItem) {
+ Artist artist = selectedItem instanceof Artist ? (Artist) selectedItem : null;
+ MusicDirectory.Entry entry = selectedItem instanceof MusicDirectory.Entry ? (MusicDirectory.Entry) selectedItem : null;
+ List<MusicDirectory.Entry> songs = new ArrayList<MusicDirectory.Entry>(10);
+ songs.add(entry);
+
+ switch (menuItem.getItemId()) {
+ case R.id.artist_menu_play_now:
+ downloadRecursively(artist.getId(), false, false, true, false, false);
+ break;
+ case R.id.artist_menu_play_shuffled:
+ downloadRecursively(artist.getId(), false, false, true, true, false);
+ break;
+ case R.id.artist_menu_play_last:
+ downloadRecursively(artist.getId(), false, true, false, false, false);
+ break;
+ case R.id.artist_menu_download:
+ downloadRecursively(artist.getId(), false, true, false, false, true);
+ break;
+ case R.id.artist_menu_pin:
+ downloadRecursively(artist.getId(), true, true, false, false, true);
+ break;
+ case R.id.artist_menu_delete:
+ deleteRecursively(artist);
+ break;
+ case R.id.album_menu_play_now:
+ downloadRecursively(entry.getId(), false, false, true, false, false);
+ break;
+ case R.id.album_menu_play_shuffled:
+ downloadRecursively(entry.getId(), false, false, true, true, false);
+ break;
+ case R.id.album_menu_play_last:
+ downloadRecursively(entry.getId(), false, true, false, false, false);
+ break;
+ case R.id.album_menu_download:
+ downloadRecursively(entry.getId(), false, true, false, false, true);
+ break;
+ case R.id.album_menu_pin:
+ downloadRecursively(entry.getId(), true, true, false, false, true);
+ break;
+ case R.id.album_menu_star:
+ toggleStarred(entry);
+ break;
+ case R.id.album_menu_delete:
+ deleteRecursively(entry);
+ break;
+ case R.id.song_menu_play_now:
+ getDownloadService().clear();
+ getDownloadService().download(songs, false, true, true, false);
+ Util.startActivityWithoutTransition(context, DownloadActivity.class);
+ if(context instanceof SearchActivity) {
+ context.finish();
+ }
+ break;
+ case R.id.song_menu_play_next:
+ getDownloadService().download(songs, false, false, true, false);
+ break;
+ case R.id.song_menu_play_last:
+ getDownloadService().download(songs, false, false, false, false);
+ break;
+ case R.id.song_menu_download:
+ getDownloadService().downloadBackground(songs, false);
+ break;
+ case R.id.song_menu_pin:
+ getDownloadService().downloadBackground(songs, true);
+ break;
+ case R.id.song_menu_delete:
+ getDownloadService().delete(songs);
+ break;
+ case R.id.song_menu_add_playlist:
+ addToPlaylist(songs);
+ break;
+ case R.id.song_menu_star:
+ toggleStarred(entry);
+ break;
+ case R.id.song_menu_play_external:
+ playExternalPlayer(entry);
+ break;
+ case R.id.song_menu_info:
+ displaySongInfo(entry);
+ break;
+ case R.id.song_menu_stream_external:
+ streamExternalPlayer(entry);
+ break;
+ default:
+ return false;
+ }
+
+ return true;
+ }
+
+ public void replaceFragment(SubsonicFragment fragment, int id) {
+ context.replaceFragment(fragment, id, fragment.getSupportTag());
+ }
+
+ protected int getNewId() {
+ internalID--;
+ return internalID;
+ }
+ public int getRootId() {
+ return rootView.getId();
+ }
+
+ public int getSupportTag() {
+ return tag;
+ }
+
+ public void setPrimaryFragment(boolean primary) {
+ primaryFragment = primary;
+ if(primary) {
+ if(context != null) {
+ context.setTitle(title);
+ context.setSubtitle(subtitle);
+ }
+ if(invalidated) {
+ invalidated = false;
+ refresh(false);
+ }
+ }
+ }
+
+ public void invalidate() {
+ if(primaryFragment) {
+ refresh(false);
+ } else {
+ invalidated = true;
+ }
+ }
+
+ public DownloadService getDownloadService() {
+ return context != null ? context.getDownloadService() : null;
+ }
+
+ protected void refresh() {
+ refresh(true);
+ }
+ protected void refresh(boolean refresh) {
+
+ }
+
+ protected void exit() {
+ if(context.getClass() != MainActivity.class) {
+ Intent intent = new Intent(context, MainActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_EXIT, true);
+ Util.startActivityWithoutTransition(context, intent);
+ } else {
+ context.stopService(new Intent(context, DownloadServiceImpl.class));
+ context.finish();
+ }
+ }
+
+ public void setProgressVisible(boolean visible) {
+ View view = rootView.findViewById(R.id.tab_progress);
+ if (view != null) {
+ view.setVisibility(visible ? View.VISIBLE : View.GONE);
+ }
+ }
+
+ public void updateProgress(String message) {
+ TextView view = (TextView) rootView.findViewById(R.id.tab_progress_message);
+ if (view != null) {
+ view.setText(message);
+ }
+ }
+
+ protected synchronized ImageLoader getImageLoader() {
+ return context.getImageLoader();
+ }
+ public synchronized static ImageLoader getStaticImageLoader(Context context) {
+ return SubsonicActivity.getStaticImageLoader(context);
+ }
+
+ public void setTitle(CharSequence title) {
+ this.title = title;
+ context.setTitle(title);
+ }
+ public void setTitle(int title) {
+ this.title = context.getResources().getString(title);
+ context.setTitle(this.title);
+ }
+ public void setSubtitle(CharSequence title) {
+ this.subtitle = title;
+ context.setSubtitle(title);
+ }
+ public CharSequence getTitle() {
+ return this.title;
+ }
+
+ protected void warnIfNetworkOrStorageUnavailable() {
+ if (!Util.isExternalStoragePresent()) {
+ Util.toast(context, R.string.select_album_no_sdcard);
+ } else if (!Util.isOffline(context) && !Util.isNetworkConnected(context)) {
+ Util.toast(context, R.string.select_album_no_network);
+ }
+ }
+
+ protected void onShuffleRequested() {
+ if(Util.isOffline(context)) {
+ Intent intent = new Intent(context, DownloadActivity.class);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_SHUFFLE, true);
+ Util.startActivityWithoutTransition(context, intent);
+ return;
+ }
+
+ View dialogView = context.getLayoutInflater().inflate(R.layout.shuffle_dialog, null);
+ final EditText startYearBox = (EditText)dialogView.findViewById(R.id.start_year);
+ final EditText endYearBox = (EditText)dialogView.findViewById(R.id.end_year);
+ final EditText genreBox = (EditText)dialogView.findViewById(R.id.genre);
+ final Button genreCombo = (Button)dialogView.findViewById(R.id.genre_combo);
+
+ final SharedPreferences prefs = Util.getPreferences(context);
+ final String oldStartYear = prefs.getString(Constants.PREFERENCES_KEY_SHUFFLE_START_YEAR, "");
+ final String oldEndYear = prefs.getString(Constants.PREFERENCES_KEY_SHUFFLE_END_YEAR, "");
+ final String oldGenre = prefs.getString(Constants.PREFERENCES_KEY_SHUFFLE_GENRE, "");
+
+ boolean _useCombo = false;
+ if(Util.checkServerVersion(context, "1.9.0")) {
+ genreBox.setVisibility(View.GONE);
+ genreCombo.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ new LoadingTask<List<Genre>>(context, true) {
+ @Override
+ protected List<Genre> doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ return musicService.getGenres(false, context, this);
+ }
+
+ @Override
+ protected void done(final List<Genre> genres) {
+ List<String> names = new ArrayList<String>();
+ String blank = context.getResources().getString(R.string.select_genre_blank);
+ names.add(blank);
+ for(Genre genre: genres) {
+ names.add(genre.getName());
+ }
+ final List<String> finalNames = names;
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.shuffle_pick_genre)
+ .setItems(names.toArray(new CharSequence[names.size()]), new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ if(which == 0) {
+ genreCombo.setText("");
+ } else {
+ genreCombo.setText(finalNames.get(which));
+ }
+ }
+ });
+ AlertDialog dialog = builder.create();
+ dialog.show();
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.playlist_error) + " " + getErrorMessage(error);
+ }
+
+ Util.toast(context, msg, false);
+ }
+ }.execute();
+ }
+ });
+ _useCombo = true;
+ } else {
+ genreCombo.setVisibility(View.GONE);
+ }
+ final boolean useCombo = _useCombo;
+
+ startYearBox.setText(oldStartYear);
+ endYearBox.setText(oldEndYear);
+ genreBox.setText(oldGenre);
+ genreCombo.setText(oldGenre);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.shuffle_title)
+ .setView(dialogView)
+ .setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ Intent intent = new Intent(context, DownloadActivity.class);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_SHUFFLE, true);
+ String genre;
+ if(useCombo) {
+ genre = genreCombo.getText().toString();
+ } else {
+ genre = genreBox.getText().toString();
+ }
+ String startYear = startYearBox.getText().toString();
+ String endYear = endYearBox.getText().toString();
+
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putString(Constants.PREFERENCES_KEY_SHUFFLE_START_YEAR, startYear);
+ editor.putString(Constants.PREFERENCES_KEY_SHUFFLE_END_YEAR, endYear);
+ editor.putString(Constants.PREFERENCES_KEY_SHUFFLE_GENRE, genre);
+ editor.commit();
+
+ Util.startActivityWithoutTransition(context, intent);
+ }
+ })
+ .setNegativeButton(R.string.common_cancel, null);
+ AlertDialog dialog = builder.create();
+ dialog.show();
+ }
+
+ public void toggleStarred(final MusicDirectory.Entry entry) {
+ final boolean starred = !entry.isStarred();
+ entry.setStarred(starred);
+
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.setStarred(entry.getId(), starred, context, null);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ // UpdateView
+ Util.toast(context, context.getResources().getString(starred ? R.string.starring_content_starred : R.string.starring_content_unstarred, entry.getTitle()));
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ entry.setStarred(!starred);
+
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.starring_content_error, entry.getTitle()) + " " + getErrorMessage(error);
+ }
+
+ Util.toast(context, msg, false);
+ }
+ }.execute();
+ }
+ public void toggleStarred(final Artist entry) {
+ final boolean starred = !entry.isStarred();
+ entry.setStarred(starred);
+
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.setStarred(entry.getId(), starred, context, null);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ // UpdateView
+ Util.toast(context, context.getResources().getString(starred ? R.string.starring_content_starred : R.string.starring_content_unstarred, entry.getName()));
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ entry.setStarred(!starred);
+
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.starring_content_error, entry.getName()) + " " + getErrorMessage(error);
+ }
+
+ Util.toast(context, msg, false);
+ }
+ }.execute();
+ }
+
+ protected void downloadRecursively(final String id, final boolean save, final boolean append, final boolean autoplay, final boolean shuffle, final boolean background) {
+ downloadRecursively(id, "", true, save, append, autoplay, shuffle, background);
+ }
+ protected void downloadPlaylist(final String id, final String name, final boolean save, final boolean append, final boolean autoplay, final boolean shuffle, final boolean background) {
+ downloadRecursively(id, name, false, save, append, autoplay, shuffle, background);
+ }
+ protected void downloadRecursively(final String id, final String name, final boolean isDirectory, final boolean save, final boolean append, final boolean autoplay, final boolean shuffle, final boolean background) {
+ LoadingTask<List<MusicDirectory.Entry>> task = new LoadingTask<List<MusicDirectory.Entry>>(context) {
+ private static final int MAX_SONGS = 500;
+
+ @Override
+ protected List<MusicDirectory.Entry> doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ MusicDirectory root;
+ if(isDirectory)
+ root = musicService.getMusicDirectory(id, name, false, context, this);
+ else
+ root = musicService.getPlaylist(id, name, context, this);
+ List<MusicDirectory.Entry> songs = new LinkedList<MusicDirectory.Entry>();
+ getSongsRecursively(root, songs);
+ return songs;
+ }
+
+ private void getSongsRecursively(MusicDirectory parent, List<MusicDirectory.Entry> songs) throws Exception {
+ if (songs.size() > MAX_SONGS) {
+ return;
+ }
+
+ for (MusicDirectory.Entry song : parent.getChildren(false, true)) {
+ if (!song.isVideo()) {
+ songs.add(song);
+ }
+ }
+ for (MusicDirectory.Entry dir : parent.getChildren(true, false)) {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ getSongsRecursively(musicService.getMusicDirectory(dir.getId(), dir.getTitle(), false, context, this), songs);
+ }
+ }
+
+ @Override
+ protected void done(List<MusicDirectory.Entry> songs) {
+ DownloadService downloadService = getDownloadService();
+ if (!songs.isEmpty() && downloadService != null) {
+ if (!append) {
+ downloadService.clear();
+ }
+ warnIfNetworkOrStorageUnavailable();
+ if(!background) {
+ downloadService.download(songs, save, autoplay, false, shuffle);
+ if(!append) {
+ Util.startActivityWithoutTransition(context, DownloadActivity.class);
+ if(context instanceof SearchActivity) {
+ context.finish();
+ }
+ }
+ }
+ else {
+ downloadService.downloadBackground(songs, save);
+ }
+ }
+ }
+ };
+
+ task.execute();
+ }
+
+ protected void addToPlaylist(final List<MusicDirectory.Entry> songs) {
+ if(songs.isEmpty()) {
+ Util.toast(context, "No songs selected");
+ return;
+ }
+
+ new LoadingTask<List<Playlist>>(context, true) {
+ @Override
+ protected List<Playlist> doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ return musicService.getPlaylists(false, context, this);
+ }
+
+ @Override
+ protected void done(final List<Playlist> playlists) {
+ List<String> names = new ArrayList<String>();
+ String createNew = context.getResources().getString(R.string.playlist_create_new);
+ names.add(createNew);
+ for(Playlist playlist: playlists) {
+ names.add(playlist.getName());
+ }
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.playlist_add_to)
+ .setItems(names.toArray(new CharSequence[names.size()]), new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+
+ if(which > 0) {
+ addToPlaylist(playlists.get(which - 1), songs);
+ } else {
+ createNewPlaylist(songs, false);
+ }
+ }
+ });
+ AlertDialog dialog = builder.create();
+ dialog.show();
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.playlist_error) + " " + getErrorMessage(error);
+ }
+
+ Util.toast(context, msg, false);
+ }
+ }.execute();
+ }
+
+ private void addToPlaylist(final Playlist playlist, final List<MusicDirectory.Entry> songs) {
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.addToPlaylist(playlist.getId(), songs, context, null);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ Util.toast(context, context.getResources().getString(R.string.updated_playlist, songs.size(), playlist.getName()));
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.updated_playlist_error, playlist.getName()) + " " + getErrorMessage(error);
+ }
+
+ Util.toast(context, msg, false);
+ }
+ }.execute();
+ }
+
+ protected void createNewPlaylist(final List<MusicDirectory.Entry> songs, boolean getSuggestion) {
+ View layout = context.getLayoutInflater().inflate(R.layout.save_playlist, null);
+ final EditText playlistNameView = (EditText) layout.findViewById(R.id.save_playlist_name);
+ final CheckBox overwriteCheckBox = (CheckBox) layout.findViewById(R.id.save_playlist_overwrite);
+ if(getSuggestion) {
+ String playlistName = (getDownloadService() != null) ? getDownloadService().getSuggestedPlaylistName() : null;
+ if (playlistName != null) {
+ playlistNameView.setText(playlistName);
+ try {
+ if(Util.checkServerVersion(context, "1.8.0") && Integer.parseInt(getDownloadService().getSuggestedPlaylistId()) != -1) {
+ overwriteCheckBox.setChecked(true);
+ overwriteCheckBox.setVisibility(View.VISIBLE);
+ }
+ } catch(Exception e) {
+ Log.d(TAG, "Playlist id isn't a integer, probably MusicCabinet");
+ }
+ } else {
+ DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
+ playlistNameView.setText(dateFormat.format(new Date()));
+ }
+ } else {
+ DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
+ playlistNameView.setText(dateFormat.format(new Date()));
+ }
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.download_playlist_title)
+ .setMessage(R.string.download_playlist_name)
+ .setView(layout)
+ .setPositiveButton(R.string.common_save, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ if(overwriteCheckBox.isChecked()) {
+ overwritePlaylist(songs, String.valueOf(playlistNameView.getText()), getDownloadService().getSuggestedPlaylistId());
+ } else {
+ createNewPlaylist(songs, String.valueOf(playlistNameView.getText()));
+ }
+ }
+ })
+ .setNegativeButton(R.string.common_cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.cancel();
+ }
+ })
+ .setCancelable(true);
+
+ AlertDialog dialog = builder.create();
+ dialog.show();
+ }
+ private void createNewPlaylist(final List<MusicDirectory.Entry> songs, final String name) {
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.createPlaylist(null, name, songs, context, null);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ Util.toast(context, R.string.download_playlist_done);
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg = context.getResources().getString(R.string.download_playlist_error) + " " + getErrorMessage(error);
+ Util.toast(context, msg);
+ }
+ }.execute();
+ }
+ private void overwritePlaylist(final List<MusicDirectory.Entry> songs, final String name, final String id) {
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ MusicDirectory playlist = musicService.getPlaylist(id, name, context, null);
+ List<MusicDirectory.Entry> toDelete = playlist.getChildren();
+ musicService.overwritePlaylist(id, name, toDelete.size(), songs, context, null);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ Util.toast(context, R.string.download_playlist_done);
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.download_playlist_error) + " " + getErrorMessage(error);
+ }
+
+ Util.toast(context, msg, false);
+ }
+ }.execute();
+ }
+
+ public void displaySongInfo(final MusicDirectory.Entry song) {
+ Integer bitrate = null;
+ String format = null;
+ long size = 0;
+ try {
+ DownloadFile downloadFile = new DownloadFile(context, song, false);
+ File file = downloadFile.getCompleteFile();
+ if(file.exists()) {
+ MediaMetadataRetriever metadata = new MediaMetadataRetriever();
+ metadata.setDataSource(file.getAbsolutePath());
+ String tmp = metadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE);
+ bitrate = Integer.parseInt((tmp != null) ? tmp : "0") / 1000;
+ format = FileUtil.getExtension(file.getName());
+ size = file.length();
+
+ if(Util.isOffline(context)) {
+ song.setGenre(metadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_GENRE));
+ String year = metadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_YEAR);
+ song.setYear(Integer.parseInt((year != null) ? year : "0"));
+ }
+ }
+ } catch(Exception e) {
+ Log.i(TAG, "Device doesn't properly support MediaMetadataRetreiver");
+ }
+
+ String msg = "";
+ if(song instanceof PodcastEpisode) {
+ msg += "Podcast: " + song.getArtist() + "\nStatus: " + ((PodcastEpisode)song).getStatus();
+ } else if(!song.isVideo()) {
+ msg += "Artist: " + song.getArtist() + "\nAlbum: " + song.getAlbum();
+ }
+ if(song.getTrack() != null && song.getTrack() != 0) {
+ msg += "\nTrack: " + song.getTrack();
+ }
+ if(song.getGenre() != null && !"".equals(song.getGenre())) {
+ msg += "\nGenre: " + song.getGenre();
+ }
+ if(song.getYear() != null && song.getYear() != 0) {
+ msg += "\nYear: " + song.getYear();
+ }
+ if(!Util.isOffline(context) && song.getSuffix() != null) {
+ msg += "\nServer Format: " + song.getSuffix();
+ if(song.getBitRate() != null && song.getBitRate() != 0) {
+ msg += "\nServer Bitrate: " + song.getBitRate() + " kpbs";
+ }
+ }
+ if(format != null && !"".equals(format)) {
+ msg += "\nCached Format: " + format;
+ }
+ if(bitrate != null && bitrate != 0) {
+ msg += "\nCached Bitrate: " + bitrate + " kpbs";
+ }
+ if(size != 0) {
+ msg += "\nSize: " + Util.formatBytes(size);
+ }
+ if(song.getDuration() != null && song.getDuration() != 0) {
+ msg += "\nLength: " + Util.formatDuration(song.getDuration());
+ }
+
+ Util.info(context, song.getTitle(), msg);
+ }
+
+ protected void playVideo(MusicDirectory.Entry entry) {
+ if(entryExists(entry)) {
+ playExternalPlayer(entry);
+ } else {
+ streamExternalPlayer(entry);
+ }
+ }
+
+ protected void playWebView(MusicDirectory.Entry entry) {
+ int maxBitrate = Util.getMaxVideoBitrate(context);
+
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(Uri.parse(MusicServiceFactory.getMusicService(context).getVideoUrl(maxBitrate, context, entry.getId())));
+
+ startActivity(intent);
+ }
+ protected void playExternalPlayer(MusicDirectory.Entry entry) {
+ if(!entryExists(entry)) {
+ Util.toast(context, R.string.download_need_download);
+ } else {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setDataAndType(Uri.parse(entry.getPath()), "video/*");
+
+ List<ResolveInfo> intents = context.getPackageManager()
+ .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
+ if(intents != null && intents.size() > 0) {
+ startActivity(intent);
+ }else {
+ Util.toast(context, R.string.download_no_streaming_player);
+ }
+ }
+ }
+ protected void streamExternalPlayer(MusicDirectory.Entry entry) {
+ String videoPlayerType = Util.getVideoPlayerType(context);
+ if("flash".equals(videoPlayerType)) {
+ playWebView(entry);
+ } else if("hls".equals(videoPlayerType)) {
+ streamExternalPlayer(entry, "hls");
+ } else if("raw".equals(videoPlayerType)) {
+ streamExternalPlayer(entry, "raw");
+ } else {
+ streamExternalPlayer(entry, entry.getTranscodedSuffix());
+ }
+ }
+ protected void streamExternalPlayer(MusicDirectory.Entry entry, String format) {
+ try {
+ int maxBitrate = Util.getMaxVideoBitrate(context);
+
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ if("hls".equals(format)) {
+ intent.setDataAndType(Uri.parse(MusicServiceFactory.getMusicService(context).getHlsUrl(entry.getId(), maxBitrate, context)), "video/*");
+ } else {
+ intent.setDataAndType(Uri.parse(MusicServiceFactory.getMusicService(context).getVideoStreamUrl(format, maxBitrate, context, entry.getId())), "video/*");
+ }
+ intent.putExtra("title", entry.getTitle());
+
+ List<ResolveInfo> intents = context.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
+ if(intents != null && intents.size() > 0) {
+ startActivity(intent);
+ } else {
+ Util.toast(context, R.string.download_no_streaming_player);
+ }
+ } catch(Exception error) {
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = error.getMessage();
+ } else {
+ msg = context.getResources().getString(R.string.download_no_streaming_player) + " " + error.getMessage();
+ }
+
+ Util.toast(context, msg, false);
+ }
+ }
+
+ protected boolean entryExists(MusicDirectory.Entry entry) {
+ DownloadFile check = new DownloadFile(context, entry, false);
+ return check.isCompleteFileAvailable();
+ }
+
+ public void deleteRecursively(Artist artist) {
+ File dir = FileUtil.getArtistDirectory(context, artist);
+ Util.recursiveDelete(dir);
+ if(Util.isOffline(context)) {
+ refresh();
+ }
+ }
+
+ public void deleteRecursively(MusicDirectory.Entry album) {
+ File dir = FileUtil.getAlbumDirectory(context, album);
+ Util.recursiveDelete(dir);
+ if(Util.isOffline(context)) {
+ refresh();
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/provider/DSubSearchProvider.java b/src/github/daneren2005/dsub/provider/DSubSearchProvider.java
new file mode 100644
index 00000000..5ddec0f4
--- /dev/null
+++ b/src/github/daneren2005/dsub/provider/DSubSearchProvider.java
@@ -0,0 +1,36 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.provider;
+
+import android.content.SearchRecentSuggestionsProvider;
+
+/**
+ * Provides search suggestions based on recent searches.
+ *
+ * @author Sindre Mehus
+ */
+public class DSubSearchProvider extends SearchRecentSuggestionsProvider {
+
+ public static final String AUTHORITY = DSubSearchProvider.class.getName();
+ public static final int MODE = DATABASE_MODE_QUERIES;
+
+ public DSubSearchProvider() {
+ setupSuggestions(AUTHORITY, MODE);
+ }
+} \ No newline at end of file
diff --git a/src/github/daneren2005/dsub/provider/DSubWidget4x1.java b/src/github/daneren2005/dsub/provider/DSubWidget4x1.java
new file mode 100644
index 00000000..e00bf02d
--- /dev/null
+++ b/src/github/daneren2005/dsub/provider/DSubWidget4x1.java
@@ -0,0 +1,29 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.provider;
+
+import android.appwidget.AppWidgetManager;
+import github.daneren2005.dsub.R;
+
+public class DSubWidget4x1 extends DSubWidgetProvider {
+ @Override
+ protected int getLayout() {
+ return R.layout.appwidget4x1;
+ }
+}
diff --git a/src/github/daneren2005/dsub/provider/DSubWidget4x2.java b/src/github/daneren2005/dsub/provider/DSubWidget4x2.java
new file mode 100644
index 00000000..4908f632
--- /dev/null
+++ b/src/github/daneren2005/dsub/provider/DSubWidget4x2.java
@@ -0,0 +1,29 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.provider;
+
+import android.appwidget.AppWidgetManager;
+import github.daneren2005.dsub.R;
+
+public class DSubWidget4x2 extends DSubWidgetProvider {
+ @Override
+ protected int getLayout() {
+ return R.layout.appwidget4x2;
+ }
+}
diff --git a/src/github/daneren2005/dsub/provider/DSubWidget4x3.java b/src/github/daneren2005/dsub/provider/DSubWidget4x3.java
new file mode 100644
index 00000000..f1908d0d
--- /dev/null
+++ b/src/github/daneren2005/dsub/provider/DSubWidget4x3.java
@@ -0,0 +1,29 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.provider;
+
+import android.appwidget.AppWidgetManager;
+import github.daneren2005.dsub.R;
+
+public class DSubWidget4x3 extends DSubWidgetProvider {
+ @Override
+ protected int getLayout() {
+ return R.layout.appwidget4x3;
+ }
+}
diff --git a/src/github/daneren2005/dsub/provider/DSubWidget4x4.java b/src/github/daneren2005/dsub/provider/DSubWidget4x4.java
new file mode 100644
index 00000000..7fee2747
--- /dev/null
+++ b/src/github/daneren2005/dsub/provider/DSubWidget4x4.java
@@ -0,0 +1,29 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.provider;
+
+import android.appwidget.AppWidgetManager;
+import github.daneren2005.dsub.R;
+
+public class DSubWidget4x4 extends DSubWidgetProvider {
+ @Override
+ protected int getLayout() {
+ return R.layout.appwidget4x4;
+ }
+}
diff --git a/src/github/daneren2005/dsub/provider/DSubWidgetProvider.java b/src/github/daneren2005/dsub/provider/DSubWidgetProvider.java
new file mode 100644
index 00000000..7215040c
--- /dev/null
+++ b/src/github/daneren2005/dsub/provider/DSubWidgetProvider.java
@@ -0,0 +1,277 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.provider;
+
+import android.app.PendingIntent;
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProvider;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Environment;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.widget.RemoteViews;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.activity.DownloadActivity;
+import github.daneren2005.dsub.activity.MainActivity;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.service.DownloadServiceImpl;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.FileUtil;
+import java.util.HashMap;
+
+/**
+ * Simple widget to show currently playing album art along
+ * with play/pause and next track buttons.
+ * <p/>
+ * Based on source code from the stock Android Music app.
+ *
+ * @author Sindre Mehus
+ */
+public class DSubWidgetProvider extends AppWidgetProvider {
+ private static final String TAG = DSubWidgetProvider.class.getSimpleName();
+ private static DSubWidget4x1 instance4x1;
+ private static DSubWidget4x2 instance4x2;
+ private static DSubWidget4x3 instance4x3;
+ private static DSubWidget4x4 instance4x4;
+
+ public static synchronized void notifyInstances(Context context, DownloadService service, boolean playing) {
+ if(instance4x1 == null) {
+ instance4x1 = new DSubWidget4x1();
+ }
+ if(instance4x2 == null) {
+ instance4x2 = new DSubWidget4x2();
+ }
+ if(instance4x3 == null) {
+ instance4x3 = new DSubWidget4x3();
+ }
+ if(instance4x4 == null) {
+ instance4x4 = new DSubWidget4x4();
+ }
+
+ instance4x1.notifyChange(context, service, playing);
+ instance4x2.notifyChange(context, service, playing);
+ instance4x3.notifyChange(context, service, playing);
+ instance4x4.notifyChange(context, service, playing);
+ }
+
+ @Override
+ public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
+ defaultAppWidget(context, appWidgetIds);
+ }
+
+ protected int getLayout() {
+ return 0;
+ }
+
+ /**
+ * Initialize given widgets to default state, where we launch Subsonic on default click
+ * and hide actions if service not running.
+ */
+ private void defaultAppWidget(Context context, int[] appWidgetIds) {
+ final Resources res = context.getResources();
+ final RemoteViews views = new RemoteViews(context.getPackageName(), getLayout());
+
+ views.setTextViewText(R.id.artist, res.getText(R.string.widget_initial_text));
+ if(getLayout() == R.layout.appwidget4x2) {
+ views.setTextViewText(R.id.album, "");
+ }
+
+ linkButtons(context, views, false);
+ pushUpdate(context, appWidgetIds, views);
+ }
+
+ private void pushUpdate(Context context, int[] appWidgetIds, RemoteViews views) {
+ // Update specific list of appWidgetIds if given, otherwise default to all
+ final AppWidgetManager manager = AppWidgetManager.getInstance(context);
+ if (appWidgetIds != null) {
+ manager.updateAppWidget(appWidgetIds, views);
+ } else {
+ manager.updateAppWidget(new ComponentName(context, this.getClass()), views);
+ }
+ }
+
+ /**
+ * Handle a change notification coming over from {@link DownloadService}
+ */
+ public void notifyChange(Context context, DownloadService service, boolean playing) {
+ if (hasInstances(context)) {
+ performUpdate(context, service, null, playing);
+ }
+ }
+
+ /**
+ * Check against {@link AppWidgetManager} if there are any instances of this widget.
+ */
+ private boolean hasInstances(Context context) {
+ AppWidgetManager manager = AppWidgetManager.getInstance(context);
+ int[] appWidgetIds = manager.getAppWidgetIds(new ComponentName(context, getClass()));
+ return (appWidgetIds.length > 0);
+ }
+
+ /**
+ * Update all active widget instances by pushing changes
+ */
+ private void performUpdate(Context context, DownloadService service, int[] appWidgetIds, boolean playing) {
+ final Resources res = context.getResources();
+ final RemoteViews views = new RemoteViews(context.getPackageName(), getLayout());
+
+ MusicDirectory.Entry currentPlaying = service.getCurrentPlaying() == null ? null : service.getCurrentPlaying().getSong();
+ String title = currentPlaying == null ? null : currentPlaying.getTitle();
+ CharSequence artist = currentPlaying == null ? null : currentPlaying.getArtist();
+ CharSequence album = currentPlaying == null ? null : currentPlaying.getAlbum();
+ CharSequence errorState = null;
+
+ // Show error message?
+ String status = Environment.getExternalStorageState();
+ if (status.equals(Environment.MEDIA_SHARED) ||
+ status.equals(Environment.MEDIA_UNMOUNTED)) {
+ errorState = res.getText(R.string.widget_sdcard_busy);
+ } else if (status.equals(Environment.MEDIA_REMOVED)) {
+ errorState = res.getText(R.string.widget_sdcard_missing);
+ } else if (currentPlaying == null) {
+ errorState = res.getText(R.string.widget_initial_text);
+ }
+
+ if (errorState != null) {
+ // Show error state to user
+ views.setTextViewText(R.id.title,null);
+ views.setTextViewText(R.id.artist, errorState);
+ views.setTextViewText(R.id.album, "");
+ if(getLayout() != R.layout.appwidget4x1) {
+ views.setImageViewResource(R.id.appwidget_coverart, R.drawable.appwidget_art_default);
+ }
+ } else {
+ // No error, so show normal titles
+ views.setTextViewText(R.id.title, title);
+ views.setTextViewText(R.id.artist, artist);
+ if(getLayout() != R.layout.appwidget4x1) {
+ views.setTextViewText(R.id.album, album);
+ }
+ }
+
+ // Set correct drawable for pause state
+ if (playing) {
+ views.setImageViewResource(R.id.control_play, R.drawable.ic_appwidget_music_pause);
+ } else {
+ views.setImageViewResource(R.id.control_play, R.drawable.ic_appwidget_music_play);
+ }
+
+ // Set the cover art
+ try {
+ int size;
+ if(getLayout() != R.layout.appwidget4x1 && getLayout() != R.layout.appwidget4x2) {
+ size = context.getResources().getDrawable(R.drawable.unknown_album_large).getIntrinsicHeight();
+ } else {
+ size = context.getResources().getDrawable(R.drawable.appwidget_art_default).getIntrinsicHeight();
+ }
+ Bitmap bitmap = currentPlaying == null ? null : FileUtil.getAlbumArtBitmap(context, currentPlaying, size);
+
+ if (bitmap == null) {
+ // Set default cover art
+ views.setImageViewResource(R.id.appwidget_coverart, R.drawable.appwidget_art_unknown);
+ } else {
+ bitmap = getRoundedCornerBitmap(bitmap);
+ views.setImageViewBitmap(R.id.appwidget_coverart, bitmap);
+ }
+ } catch (Exception x) {
+ Log.e(TAG, "Failed to load cover art", x);
+ views.setImageViewResource(R.id.appwidget_coverart, R.drawable.appwidget_art_unknown);
+ }
+
+ // Link actions buttons to intents
+ linkButtons(context, views, currentPlaying != null);
+
+ pushUpdate(context, appWidgetIds, views);
+ }
+
+ /**
+ * Round the corners of a bitmap for the cover art image
+ */
+ private static Bitmap getRoundedCornerBitmap(Bitmap bitmap) {
+ Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Config.ARGB_8888);
+ Canvas canvas = new Canvas(output);
+
+ final int color = 0xff424242;
+ final Paint paint = new Paint();
+ final float roundPx = 10;
+
+ // Add extra width to the rect so the right side wont be rounded.
+ final Rect rect = new Rect(0, 0, bitmap.getWidth() + (int) roundPx, bitmap.getHeight());
+ final RectF rectF = new RectF(rect);
+
+ paint.setAntiAlias(true);
+ canvas.drawARGB(0, 0, 0, 0);
+ paint.setColor(color);
+ canvas.drawRoundRect(rectF, roundPx, roundPx, paint);
+
+ paint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN));
+ canvas.drawBitmap(bitmap, rect, rect, paint);
+
+ return output;
+ }
+
+ /**
+ * Link up various button actions using {@link PendingIntent}.
+ *
+ * @param playerActive True if player is active in background, which means
+ * widget click will launch {@link DownloadActivity},
+ * otherwise we launch {@link MainActivity}.
+ */
+ private void linkButtons(Context context, RemoteViews views, boolean playerActive) {
+ Intent intent = new Intent(context, MainActivity.class);
+ if(playerActive) {
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD, true);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ }
+ PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
+ views.setOnClickPendingIntent(R.id.appwidget_coverart, pendingIntent);
+ views.setOnClickPendingIntent(R.id.appwidget_top, pendingIntent);
+
+ // Emulate media button clicks.
+ intent = new Intent("DSub.PLAY_PAUSE");
+ intent.setComponent(new ComponentName(context, DownloadServiceImpl.class));
+ intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE));
+ pendingIntent = PendingIntent.getService(context, 0, intent, 0);
+ views.setOnClickPendingIntent(R.id.control_play, pendingIntent);
+
+ intent = new Intent("DSub.NEXT"); // Use a unique action name to ensure a different PendingIntent to be created.
+ intent.setComponent(new ComponentName(context, DownloadServiceImpl.class));
+ intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_NEXT));
+ pendingIntent = PendingIntent.getService(context, 0, intent, 0);
+ views.setOnClickPendingIntent(R.id.control_next, pendingIntent);
+
+ intent = new Intent("DSub.PREVIOUS"); // Use a unique action name to ensure a different PendingIntent to be created.
+ intent.setComponent(new ComponentName(context, DownloadServiceImpl.class));
+ intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PREVIOUS));
+ pendingIntent = PendingIntent.getService(context, 0, intent, 0);
+ views.setOnClickPendingIntent(R.id.control_previous, pendingIntent);
+ }
+}
diff --git a/src/github/daneren2005/dsub/receiver/A2dpIntentReceiver.java b/src/github/daneren2005/dsub/receiver/A2dpIntentReceiver.java
new file mode 100644
index 00000000..c8c3a1f9
--- /dev/null
+++ b/src/github/daneren2005/dsub/receiver/A2dpIntentReceiver.java
@@ -0,0 +1,48 @@
+package github.daneren2005.dsub.receiver;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.service.DownloadServiceImpl;
+
+public class A2dpIntentReceiver extends BroadcastReceiver {
+ private static final String PLAYSTATUS_RESPONSE = "com.android.music.playstatusresponse";
+ private String TAG = A2dpIntentReceiver.class.getSimpleName();
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.i(TAG, "GOT INTENT " + intent);
+
+ DownloadService downloadService = DownloadServiceImpl.getInstance();
+
+ if (downloadService != null){
+
+ Intent avrcpIntent = new Intent(PLAYSTATUS_RESPONSE);
+
+ avrcpIntent.putExtra("duration", (long) downloadService.getPlayerDuration());
+ avrcpIntent.putExtra("position", (long) downloadService.getPlayerPosition());
+ avrcpIntent.putExtra("ListSize", (long) downloadService.getSongs().size());
+
+ switch (downloadService.getPlayerState()){
+ case STARTED:
+ avrcpIntent.putExtra("playing", true);
+ break;
+ case STOPPED:
+ avrcpIntent.putExtra("playing", false);
+ break;
+ case PAUSED:
+ avrcpIntent.putExtra("playing", false);
+ break;
+ case COMPLETED:
+ avrcpIntent.putExtra("playing", false);
+ break;
+ default:
+ return;
+ }
+
+ context.sendBroadcast(avrcpIntent);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/github/daneren2005/dsub/receiver/BluetoothIntentReceiver.java b/src/github/daneren2005/dsub/receiver/BluetoothIntentReceiver.java
new file mode 100644
index 00000000..567cf8f4
--- /dev/null
+++ b/src/github/daneren2005/dsub/receiver/BluetoothIntentReceiver.java
@@ -0,0 +1,78 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.receiver;
+
+import android.bluetooth.BluetoothDevice;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+import github.daneren2005.dsub.service.DownloadServiceImpl;
+import github.daneren2005.dsub.util.Util;
+
+/**
+ * Request media button focus when connected to Bluetooth A2DP.
+ *
+ * @author Sindre Mehus
+ */
+public class BluetoothIntentReceiver extends BroadcastReceiver {
+ private static final String TAG = BluetoothIntentReceiver.class.getSimpleName();
+ // Same as constants in android.bluetooth.BluetoothProfile, which is API level 11.
+ private static final int STATE_DISCONNECTED = 0;
+ private static final int STATE_CONNECTED = 2;
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.i(TAG, "GOT INTENT " + intent);
+ if (isConnected(intent)) {
+ Log.i(TAG, "Connected to Bluetooth A2DP, requesting media button focus.");
+ Util.registerMediaButtonEventReceiver(context);
+ } else if (isDisconnected(intent)) {
+ Log.i(TAG, "Disconnected from Bluetooth A2DP, requesting pause.");
+ context.sendBroadcast(new Intent(DownloadServiceImpl.CMD_PAUSE));
+ }
+ }
+ private boolean isConnected(Intent intent) {
+ if ("android.bluetooth.a2dp.action.SINK_STATE_CHANGED".equals(intent.getAction()) &&
+ intent.getIntExtra("android.bluetooth.a2dp.extra.SINK_STATE", -1) == STATE_CONNECTED) {
+ return true;
+ }
+ else if ("android.bluetooth.headset.profile.action.CONNECTION_STATE_CHANGED".equals(intent.getAction()) &&
+ intent.getIntExtra("android.bluetooth.profile.extra.STATE", -1) == STATE_CONNECTED) {
+ return true;
+ }
+ else if (BluetoothDevice.ACTION_ACL_CONNECTED.equals(intent.getAction())) {
+ return true;
+ }
+ return false;
+ }
+ private boolean isDisconnected(Intent intent) {
+ if ("android.bluetooth.a2dp.action.SINK_STATE_CHANGED".equals(intent.getAction()) &&
+ intent.getIntExtra("android.bluetooth.a2dp.extra.SINK_STATE", -1) == STATE_DISCONNECTED) {
+ return true;
+ }
+ else if ("android.bluetooth.headset.profile.action.CONNECTION_STATE_CHANGED".equals(intent.getAction()) &&
+ intent.getIntExtra("android.bluetooth.profile.extra.STATE", -1) == STATE_DISCONNECTED) {
+ return true;
+ }
+ else if (BluetoothDevice.ACTION_ACL_DISCONNECTED.equals(intent.getAction())) {
+ return true;
+ }
+ return false;
+ }
+} \ No newline at end of file
diff --git a/src/github/daneren2005/dsub/receiver/MediaButtonIntentReceiver.java b/src/github/daneren2005/dsub/receiver/MediaButtonIntentReceiver.java
new file mode 100644
index 00000000..9ea04474
--- /dev/null
+++ b/src/github/daneren2005/dsub/receiver/MediaButtonIntentReceiver.java
@@ -0,0 +1,52 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.receiver;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+import android.view.KeyEvent;
+import github.daneren2005.dsub.service.DownloadServiceImpl;
+
+/**
+ * @author Sindre Mehus
+ */
+public class MediaButtonIntentReceiver extends BroadcastReceiver {
+
+ private static final String TAG = MediaButtonIntentReceiver.class.getSimpleName();
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ KeyEvent event = (KeyEvent) intent.getExtras().get(Intent.EXTRA_KEY_EVENT);
+ Log.i(TAG, "Got MEDIA_BUTTON key event: " + event);
+
+ Intent serviceIntent = new Intent(context, DownloadServiceImpl.class);
+ serviceIntent.putExtra(Intent.EXTRA_KEY_EVENT, event);
+ context.startService(serviceIntent);
+ if (isOrderedBroadcast())
+ {
+ try {
+ abortBroadcast();
+ } catch (Exception x) {
+ // Ignored.
+ }
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/service/CachedMusicService.java b/src/github/daneren2005/dsub/service/CachedMusicService.java
new file mode 100644
index 00000000..943b5eb2
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/CachedMusicService.java
@@ -0,0 +1,373 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.service;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.http.HttpResponse;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.support.v4.util.LruCache;
+import github.daneren2005.dsub.domain.ChatMessage;
+import github.daneren2005.dsub.domain.Genre;
+import github.daneren2005.dsub.domain.Indexes;
+import github.daneren2005.dsub.domain.JukeboxStatus;
+import github.daneren2005.dsub.domain.Lyrics;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.MusicFolder;
+import github.daneren2005.dsub.domain.Playlist;
+import github.daneren2005.dsub.domain.PodcastChannel;
+import github.daneren2005.dsub.domain.SearchCritera;
+import github.daneren2005.dsub.domain.SearchResult;
+import github.daneren2005.dsub.domain.Share;
+import github.daneren2005.dsub.domain.Version;
+import github.daneren2005.dsub.util.CancellableTask;
+import github.daneren2005.dsub.util.ProgressListener;
+import github.daneren2005.dsub.util.TimeLimitedCache;
+import github.daneren2005.dsub.util.Util;
+
+/**
+ * @author Sindre Mehus
+ */
+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
+
+ private final MusicService musicService;
+ private final LruCache<String, TimeLimitedCache<MusicDirectory>> cachedMusicDirectories;
+ private final TimeLimitedCache<Boolean> cachedLicenseValid = new TimeLimitedCache<Boolean>(120, TimeUnit.SECONDS);
+ private final TimeLimitedCache<Indexes> cachedIndexes = new TimeLimitedCache<Indexes>(60 * 60, TimeUnit.SECONDS);
+ private final TimeLimitedCache<List<Playlist>> cachedPlaylists = new TimeLimitedCache<List<Playlist>>(3600, TimeUnit.SECONDS);
+ private final TimeLimitedCache<List<MusicFolder>> cachedMusicFolders = new TimeLimitedCache<List<MusicFolder>>(10 * 3600, TimeUnit.SECONDS);
+ private final TimeLimitedCache<List<Genre>> cachedGenres = new TimeLimitedCache<List<Genre>>(10 * 3600, TimeUnit.SECONDS);
+ private final TimeLimitedCache<List<PodcastChannel>> cachedPodcastChannels = new TimeLimitedCache<List<PodcastChannel>>(10 * 3600, TimeUnit.SECONDS);
+ private String restUrl;
+
+ public CachedMusicService(MusicService musicService) {
+ this.musicService = musicService;
+ cachedMusicDirectories = new LruCache<String, TimeLimitedCache<MusicDirectory>>(MUSIC_DIR_CACHE_SIZE);
+ }
+
+ @Override
+ public void ping(Context context, ProgressListener progressListener) throws Exception {
+ checkSettingsChanged(context);
+ musicService.ping(context, progressListener);
+ }
+
+ @Override
+ public boolean isLicenseValid(Context context, ProgressListener progressListener) throws Exception {
+ checkSettingsChanged(context);
+ Boolean result = cachedLicenseValid.get();
+ if (result == null) {
+ result = musicService.isLicenseValid(context, progressListener);
+ cachedLicenseValid.set(result, result ? 30L * 60L : 2L * 60L, TimeUnit.SECONDS);
+ }
+ return result;
+ }
+
+ @Override
+ public List<MusicFolder> getMusicFolders(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ checkSettingsChanged(context);
+ if (refresh) {
+ cachedMusicFolders.clear();
+ }
+ List<MusicFolder> result = cachedMusicFolders.get();
+ if (result == null) {
+ result = musicService.getMusicFolders(refresh, context, progressListener);
+ cachedMusicFolders.set(result);
+ }
+ return result;
+ }
+
+ @Override
+ public Indexes getIndexes(String musicFolderId, boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ checkSettingsChanged(context);
+ if (refresh) {
+ cachedIndexes.clear();
+ cachedMusicFolders.clear();
+ cachedMusicDirectories.evictAll();
+ }
+ Indexes result = cachedIndexes.get();
+ if (result == null) {
+ result = musicService.getIndexes(musicFolderId, refresh, context, progressListener);
+ cachedIndexes.set(result);
+ }
+ return result;
+ }
+
+ @Override
+ public MusicDirectory getMusicDirectory(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ checkSettingsChanged(context);
+ TimeLimitedCache<MusicDirectory> cache = refresh ? null : cachedMusicDirectories.get(id);
+ MusicDirectory dir = cache == null ? null : cache.get();
+ if (dir == null) {
+ dir = musicService.getMusicDirectory(id, name, refresh, context, progressListener);
+ cache = new TimeLimitedCache<MusicDirectory>(TTL_MUSIC_DIR, TimeUnit.SECONDS);
+ cache.set(dir);
+ cachedMusicDirectories.put(id, cache);
+ }
+ return dir;
+ }
+
+ @Override
+ public SearchResult search(SearchCritera criteria, Context context, ProgressListener progressListener) throws Exception {
+ return musicService.search(criteria, context, progressListener);
+ }
+
+ @Override
+ public MusicDirectory getPlaylist(String id, String name, Context context, ProgressListener progressListener) throws Exception {
+ return musicService.getPlaylist(id, name, context, progressListener);
+ }
+
+ @Override
+ public List<Playlist> getPlaylists(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ checkSettingsChanged(context);
+ List<Playlist> result = refresh ? null : cachedPlaylists.get();
+ if (result == null) {
+ result = musicService.getPlaylists(refresh, context, progressListener);
+ cachedPlaylists.set(result);
+ }
+ return result;
+ }
+
+ @Override
+ public void createPlaylist(String id, String name, List<MusicDirectory.Entry> entries, Context context, ProgressListener progressListener) throws Exception {
+ cachedPlaylists.clear();
+ musicService.createPlaylist(id, name, entries, context, progressListener);
+ }
+
+ @Override
+ public void deletePlaylist(String id, Context context, ProgressListener progressListener) throws Exception {
+ musicService.deletePlaylist(id, context, progressListener);
+ }
+
+ @Override
+ public void addToPlaylist(String id, List<MusicDirectory.Entry> toAdd, Context context, ProgressListener progressListener) throws Exception {
+ musicService.addToPlaylist(id, toAdd, context, progressListener);
+ }
+
+ @Override
+ public void removeFromPlaylist(String id, List<Integer> toRemove, Context context, ProgressListener progressListener) throws Exception {
+ musicService.removeFromPlaylist(id, toRemove, context, progressListener);
+ }
+
+ @Override
+ public void overwritePlaylist(String id, String name, int toRemove, List<MusicDirectory.Entry> toAdd, Context context, ProgressListener progressListener) throws Exception {
+ musicService.overwritePlaylist(id, name, toRemove, toAdd, context, progressListener);
+ }
+
+ @Override
+ public void updatePlaylist(String id, String name, String comment, boolean pub, Context context, ProgressListener progressListener) throws Exception {
+ musicService.updatePlaylist(id, name, comment, pub, context, progressListener);
+ }
+
+ @Override
+ public Lyrics getLyrics(String artist, String title, Context context, ProgressListener progressListener) throws Exception {
+ return musicService.getLyrics(artist, title, context, progressListener);
+ }
+
+ @Override
+ public void scrobble(String id, boolean submission, Context context, ProgressListener progressListener) throws Exception {
+ musicService.scrobble(id, submission, context, progressListener);
+ }
+
+ @Override
+ public MusicDirectory getAlbumList(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception {
+ return musicService.getAlbumList(type, size, offset, context, progressListener);
+ }
+
+ @Override
+ public MusicDirectory getStarredList(Context context, ProgressListener progressListener) throws Exception {
+ return musicService.getStarredList(context, progressListener);
+ }
+
+ @Override
+ public MusicDirectory getRandomSongs(int size, String folder, String genre, String startYear, String endYear, Context context, ProgressListener progressListener) throws Exception {
+ return musicService.getRandomSongs(size, folder, genre, startYear, endYear, context, progressListener);
+ }
+
+ @Override
+ public Bitmap getCoverArt(Context context, MusicDirectory.Entry entry, int size, int saveSize, ProgressListener progressListener) throws Exception {
+ return musicService.getCoverArt(context, entry, size, saveSize, progressListener);
+ }
+
+ @Override
+ public HttpResponse getDownloadInputStream(Context context, MusicDirectory.Entry song, long offset, int maxBitrate, CancellableTask task) throws Exception {
+ return musicService.getDownloadInputStream(context, song, offset, maxBitrate, task);
+ }
+
+ @Override
+ public Version getLocalVersion(Context context) throws Exception {
+ return musicService.getLocalVersion(context);
+ }
+
+ @Override
+ public Version getLatestVersion(Context context, ProgressListener progressListener) throws Exception {
+ return musicService.getLatestVersion(context, progressListener);
+ }
+
+ @Override
+ public String getVideoUrl(int maxBitrate, Context context, String id) {
+ return musicService.getVideoUrl(maxBitrate, context, id);
+ }
+
+ @Override
+ public String getVideoStreamUrl(String format, int maxBitrate, Context context, String id) throws Exception {
+ return musicService.getVideoStreamUrl(format, maxBitrate, context, id);
+ }
+
+ @Override
+ public String getHlsUrl(String id, int bitRate, Context context) throws Exception {
+ return musicService.getHlsUrl(id, bitRate, context);
+ }
+
+ @Override
+ public JukeboxStatus updateJukeboxPlaylist(List<String> ids, Context context, ProgressListener progressListener) throws Exception {
+ return musicService.updateJukeboxPlaylist(ids, context, progressListener);
+ }
+
+ @Override
+ public JukeboxStatus skipJukebox(int index, int offsetSeconds, Context context, ProgressListener progressListener) throws Exception {
+ return musicService.skipJukebox(index, offsetSeconds, context, progressListener);
+ }
+
+ @Override
+ public JukeboxStatus stopJukebox(Context context, ProgressListener progressListener) throws Exception {
+ return musicService.stopJukebox(context, progressListener);
+ }
+
+ @Override
+ public JukeboxStatus startJukebox(Context context, ProgressListener progressListener) throws Exception {
+ return musicService.startJukebox(context, progressListener);
+ }
+
+ @Override
+ public JukeboxStatus getJukeboxStatus(Context context, ProgressListener progressListener) throws Exception {
+ return musicService.getJukeboxStatus(context, progressListener);
+ }
+
+ @Override
+ public JukeboxStatus setJukeboxGain(float gain, Context context, ProgressListener progressListener) throws Exception {
+ return musicService.setJukeboxGain(gain, context, progressListener);
+ }
+
+ @Override
+ public void setStarred(String id, boolean starred, Context context, ProgressListener progressListener) throws Exception {
+ musicService.setStarred(id, starred, context, progressListener);
+ }
+
+ @Override
+ public List<Share> getShares(Context context, ProgressListener progressListener) throws Exception {
+ return musicService.getShares(context, progressListener);
+ }
+
+ @Override
+ public List<ChatMessage> getChatMessages(Long since, Context context, ProgressListener progressListener) throws Exception {
+ return musicService.getChatMessages(since, context, progressListener);
+ }
+
+ @Override
+ public void addChatMessage(String message, Context context, ProgressListener progressListener) throws Exception {
+ musicService.addChatMessage(message, context, progressListener);
+ }
+
+ @Override
+ public List<Genre> getGenres(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ checkSettingsChanged(context);
+ List<Genre> result = refresh ? null : cachedGenres.get();
+
+ if (result == null) {
+ result = musicService.getGenres(refresh, context, progressListener);
+ cachedGenres.set(result);
+ }
+
+ return result;
+ }
+
+ @Override
+ public MusicDirectory getSongsByGenre(String genre, int count, int offset, Context context, ProgressListener progressListener) throws Exception {
+ return musicService.getSongsByGenre(genre, count, offset, context, progressListener);
+ }
+
+ @Override
+ public List<PodcastChannel> getPodcastChannels(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ checkSettingsChanged(context);
+ List<PodcastChannel> result = refresh ? null : cachedPodcastChannels.get();
+
+ if (result == null) {
+ result = musicService.getPodcastChannels(refresh, context, progressListener);
+ cachedPodcastChannels.set(result);
+ }
+
+ return result;
+ }
+
+ @Override
+ public MusicDirectory getPodcastEpisodes(String id, Context context, ProgressListener progressListener) throws Exception {
+ return musicService.getPodcastEpisodes(id, context, progressListener);
+ }
+
+ @Override
+ public void refreshPodcasts(Context context, ProgressListener progressListener) throws Exception {
+ musicService.refreshPodcasts(context, progressListener);
+ }
+
+ @Override
+ public void createPodcastChannel(String url, Context context, ProgressListener progressListener) throws Exception{
+ musicService.createPodcastChannel(url, context, progressListener);
+ }
+
+ @Override
+ public void deletePodcastChannel(String id, Context context, ProgressListener progressListener) throws Exception{
+ musicService.deletePodcastChannel(id, context, progressListener);
+ }
+
+ @Override
+ public void downloadPodcastEpisode(String id, Context context, ProgressListener progressListener) throws Exception{
+ musicService.downloadPodcastEpisode(id, context, progressListener);
+ }
+
+ @Override
+ public void deletePodcastEpisode(String id, Context context, ProgressListener progressListener) throws Exception{
+ musicService.deletePodcastEpisode(id, context, progressListener);
+ }
+
+ @Override
+ public int processOfflineSyncs(final Context context, final ProgressListener progressListener) throws Exception{
+ return musicService.processOfflineSyncs(context, progressListener);
+ }
+
+
+
+ private void checkSettingsChanged(Context context) {
+ String newUrl = Util.getRestUrl(context, null);
+ if (!Util.equals(newUrl, restUrl)) {
+ cachedMusicFolders.clear();
+ cachedMusicDirectories.evictAll();
+ cachedLicenseValid.clear();
+ cachedIndexes.clear();
+ cachedPlaylists.clear();
+ cachedPodcastChannels.clear();
+ restUrl = newUrl;
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/service/DownloadFile.java b/src/github/daneren2005/dsub/service/DownloadFile.java
new file mode 100644
index 00000000..5ab7ad70
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/DownloadFile.java
@@ -0,0 +1,398 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.service;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import android.content.Context;
+import android.net.wifi.WifiManager;
+import android.os.PowerManager;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.util.CancellableTask;
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.Util;
+import github.daneren2005.dsub.util.CacheCleaner;
+import org.apache.http.Header;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class DownloadFile {
+
+ private static final String TAG = DownloadFile.class.getSimpleName();
+ private final Context context;
+ private final MusicDirectory.Entry song;
+ private final File partialFile;
+ private final File completeFile;
+ private final File saveFile;
+
+ private final MediaStoreService mediaStoreService;
+ private CancellableTask downloadTask;
+ private boolean save;
+ private boolean failed;
+ private int bitRate;
+ private boolean isPlaying = false;
+ private boolean saveWhenDone = false;
+ private boolean completeWhenDone = false;
+ private Integer contentLength = null;
+
+ public DownloadFile(Context context, MusicDirectory.Entry song, boolean save) {
+ this.context = context;
+ this.song = song;
+ this.save = save;
+ saveFile = FileUtil.getSongFile(context, song);
+ bitRate = Util.getMaxBitrate(context);
+ partialFile = new File(saveFile.getParent(), FileUtil.getBaseName(saveFile.getName()) +
+ ".partial." + FileUtil.getExtension(saveFile.getName()));
+ completeFile = new File(saveFile.getParent(), FileUtil.getBaseName(saveFile.getName()) +
+ ".complete." + FileUtil.getExtension(saveFile.getName()));
+ mediaStoreService = new MediaStoreService(context);
+ }
+
+ public MusicDirectory.Entry getSong() {
+ return song;
+ }
+
+ /**
+ * Returns the effective bit rate.
+ */
+ public int getBitRate() {
+ if(!partialFile.exists()) {
+ bitRate = Util.getMaxBitrate(context);
+ }
+ if (bitRate > 0) {
+ return bitRate;
+ }
+ return song.getBitRate() == null ? 160 : song.getBitRate();
+ }
+
+ public Integer getContentLength() {
+ return contentLength;
+ }
+
+ public synchronized void download() {
+ FileUtil.createDirectoryForParent(saveFile);
+ failed = false;
+ if(!partialFile.exists()) {
+ bitRate = Util.getMaxBitrate(context);
+ }
+ downloadTask = new DownloadTask();
+ downloadTask.start();
+ }
+
+ public synchronized void cancelDownload() {
+ if (downloadTask != null) {
+ downloadTask.cancel();
+ }
+ }
+
+ public File getCompleteFile() {
+ if (saveFile.exists()) {
+ return saveFile;
+ }
+
+ if (completeFile.exists()) {
+ return completeFile;
+ }
+
+ return saveFile;
+ }
+
+ public File getPartialFile() {
+ return partialFile;
+ }
+
+ public boolean isSaved() {
+ return saveFile.exists();
+ }
+
+ public synchronized boolean isCompleteFileAvailable() {
+ return saveFile.exists() || completeFile.exists();
+ }
+
+ public synchronized boolean isWorkDone() {
+ return saveFile.exists() || (completeFile.exists() && !save) || saveWhenDone || completeWhenDone;
+ }
+
+ public synchronized boolean isDownloading() {
+ return downloadTask != null && downloadTask.isRunning();
+ }
+
+ public synchronized boolean isDownloadCancelled() {
+ return downloadTask != null && downloadTask.isCancelled();
+ }
+
+ public boolean shouldSave() {
+ return save;
+ }
+
+ public boolean isFailed() {
+ return failed;
+ }
+
+ public void delete() {
+ cancelDownload();
+ Util.delete(partialFile);
+ Util.delete(completeFile);
+ Util.delete(saveFile);
+ mediaStoreService.deleteFromMediaStore(this);
+ }
+
+ public void unpin() {
+ if (saveFile.exists()) {
+ saveFile.renameTo(completeFile);
+ }
+ }
+
+ public boolean cleanup() {
+ boolean ok = true;
+ if (completeFile.exists() || saveFile.exists()) {
+ ok = Util.delete(partialFile);
+ }
+ if (saveFile.exists()) {
+ ok &= Util.delete(completeFile);
+ }
+ return ok;
+ }
+
+ // In support of LRU caching.
+ public void updateModificationDate() {
+ updateModificationDate(saveFile);
+ updateModificationDate(partialFile);
+ updateModificationDate(completeFile);
+ }
+
+ private void updateModificationDate(File file) {
+ if (file.exists()) {
+ boolean ok = file.setLastModified(System.currentTimeMillis());
+ if (!ok) {
+ Log.w(TAG, "Failed to set last-modified date on " + file);
+ }
+ }
+ }
+
+ public void setPlaying(boolean isPlaying) {
+ try {
+ if(saveWhenDone && isPlaying == false) {
+ Util.renameFile(completeFile, saveFile);
+ saveWhenDone = false;
+ } else if(completeWhenDone && isPlaying == false) {
+ if(save) {
+ Util.renameFile(partialFile, saveFile);
+ mediaStoreService.saveInMediaStore(DownloadFile.this);
+ } else {
+ Util.renameFile(partialFile, completeFile);
+ }
+ completeWhenDone = false;
+ }
+ } catch(IOException ex) {
+ Log.w(TAG, "Failed to rename file " + completeFile + " to " + saveFile);
+ }
+
+ this.isPlaying = isPlaying;
+ }
+ public boolean getPlaying() {
+ return isPlaying;
+ }
+
+ @Override
+ public String toString() {
+ return "DownloadFile (" + song + ")";
+ }
+
+ private class DownloadTask extends CancellableTask {
+
+ @Override
+ public void execute() {
+
+ InputStream in = null;
+ FileOutputStream out = null;
+ PowerManager.WakeLock wakeLock = null;
+ WifiManager.WifiLock wifiLock = null;
+ try {
+
+ if (Util.isScreenLitOnDownload(context)) {
+ PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+ wakeLock = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK | PowerManager.ON_AFTER_RELEASE, toString());
+ wakeLock.acquire();
+ Log.i(TAG, "Acquired wake lock " + wakeLock);
+ }
+
+ wifiLock = Util.createWifiLock(context, toString());
+ wifiLock.acquire();
+
+ if (saveFile.exists()) {
+ Log.i(TAG, saveFile + " already exists. Skipping.");
+ return;
+ }
+ if (completeFile.exists()) {
+ if (save) {
+ if(isPlaying) {
+ saveWhenDone = true;
+ } else {
+ Util.renameFile(completeFile, saveFile);
+ }
+ } else {
+ Log.i(TAG, completeFile + " already exists. Skipping.");
+ }
+ return;
+ }
+
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+
+ // Some devices seem to throw error on partial file which doesn't exist
+ boolean compare;
+ try {
+ compare = (bitRate == 0) || (song.getDuration() == 0) || (partialFile.length() == 0) || (bitRate * song.getDuration() * 1000 / 8) > partialFile.length();
+ } catch(Exception e) {
+ compare = true;
+ }
+ if(compare) {
+ // Attempt partial HTTP GET, appending to the file if it exists.
+ HttpResponse response = musicService.getDownloadInputStream(context, song, partialFile.length(), bitRate, DownloadTask.this);
+ Header contentLengthHeader = response.getFirstHeader("Content-Length");
+ if(contentLengthHeader != null) {
+ String contentLengthString = contentLengthHeader.getValue();
+ if(contentLengthString != null) {
+ Log.i(TAG, "Content Length: " + contentLengthString);
+ contentLength = Integer.parseInt(contentLengthString);
+ }
+ }
+ in = response.getEntity().getContent();
+ boolean partial = response.getStatusLine().getStatusCode() == HttpStatus.SC_PARTIAL_CONTENT;
+ if (partial) {
+ Log.i(TAG, "Executed partial HTTP GET, skipping " + partialFile.length() + " bytes");
+ }
+
+ out = new FileOutputStream(partialFile, partial);
+ long n = copy(in, out);
+ Log.i(TAG, "Downloaded " + n + " bytes to " + partialFile);
+ out.flush();
+ out.close();
+
+ if (isCancelled()) {
+ throw new Exception("Download of '" + song + "' was cancelled");
+ }
+
+ downloadAndSaveCoverArt(musicService);
+ }
+
+ if(isPlaying) {
+ completeWhenDone = true;
+ } else {
+ if(save) {
+ Util.renameFile(partialFile, saveFile);
+ mediaStoreService.saveInMediaStore(DownloadFile.this);
+ } else {
+ Util.renameFile(partialFile, completeFile);
+ }
+ }
+
+ } catch (Exception x) {
+ Util.close(out);
+ Util.delete(completeFile);
+ Util.delete(saveFile);
+ if (!isCancelled()) {
+ failed = true;
+ Log.w(TAG, "Failed to download '" + song + "'.", x);
+ }
+
+ } finally {
+ Util.close(in);
+ Util.close(out);
+ if (wakeLock != null) {
+ wakeLock.release();
+ Log.i(TAG, "Released wake lock " + wakeLock);
+ }
+ if (wifiLock != null) {
+ wifiLock.release();
+ }
+ new CacheCleaner(context, DownloadServiceImpl.getInstance()).cleanSpace();
+ if(DownloadServiceImpl.getInstance() != null) {
+ ((DownloadServiceImpl)DownloadServiceImpl.getInstance()).checkDownloads();
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "DownloadTask (" + song + ")";
+ }
+
+ private void downloadAndSaveCoverArt(MusicService musicService) throws Exception {
+ try {
+ if (song.getCoverArt() != null) {
+ DisplayMetrics metrics = context.getResources().getDisplayMetrics();
+ int size = Math.min(metrics.widthPixels, metrics.heightPixels);
+ musicService.getCoverArt(context, song, size, size, null);
+ }
+ } catch (Exception x) {
+ Log.e(TAG, "Failed to get cover art.", x);
+ }
+ }
+
+ private long copy(final InputStream in, OutputStream out) throws IOException, InterruptedException {
+
+ // Start a thread that will close the input stream if the task is
+ // cancelled, thus causing the copy() method to return.
+ new Thread() {
+ @Override
+ public void run() {
+ while (true) {
+ Util.sleepQuietly(3000L);
+ if (isCancelled()) {
+ Util.close(in);
+ return;
+ }
+ if (!isRunning()) {
+ return;
+ }
+ }
+ }
+ }.start();
+
+ byte[] buffer = new byte[1024 * 16];
+ long count = 0;
+ int n;
+ long lastLog = System.currentTimeMillis();
+
+ while (!isCancelled() && (n = in.read(buffer)) != -1) {
+ out.write(buffer, 0, n);
+ count += n;
+
+ long now = System.currentTimeMillis();
+ if (now - lastLog > 3000L) { // Only every so often.
+ Log.i(TAG, "Downloaded " + Util.formatBytes(count) + " of " + song);
+ lastLog = now;
+ }
+ }
+ return count;
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/service/DownloadService.java b/src/github/daneren2005/dsub/service/DownloadService.java
new file mode 100644
index 00000000..328cc962
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/DownloadService.java
@@ -0,0 +1,141 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.service;
+
+import java.util.List;
+
+import github.daneren2005.dsub.audiofx.EqualizerController;
+import github.daneren2005.dsub.audiofx.VisualizerController;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.PlayerState;
+import github.daneren2005.dsub.domain.RepeatMode;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public interface DownloadService {
+
+ void download(List<MusicDirectory.Entry> songs, boolean save, boolean autoplay, boolean playNext, boolean shuffle);
+ void downloadBackground(List<MusicDirectory.Entry> songs, boolean save);
+
+ void setShufflePlayEnabled(boolean enabled);
+
+ boolean isShufflePlayEnabled();
+
+ void shuffle();
+
+ RepeatMode getRepeatMode();
+
+ void setRepeatMode(RepeatMode repeatMode);
+
+ boolean getKeepScreenOn();
+
+ void setKeepScreenOn(boolean screenOn);
+
+ boolean getShowVisualization();
+
+ void setShowVisualization(boolean showVisualization);
+
+ void clear();
+
+ void clearBackground();
+
+ void clearIncomplete();
+
+ int size();
+
+ void remove(int which);
+
+ void remove(DownloadFile downloadFile);
+
+ List<DownloadFile> getSongs();
+
+ List<DownloadFile> getDownloads();
+
+ List<DownloadFile> getBackgroundDownloads();
+
+ int getCurrentPlayingIndex();
+
+ DownloadFile getCurrentPlaying();
+
+ DownloadFile getCurrentDownloading();
+
+ void play(int index);
+
+ void seekTo(int position);
+
+ void previous();
+
+ void next();
+
+ void pause();
+
+ void stop();
+
+ void start();
+
+ void reset();
+
+ PlayerState getPlayerState();
+
+ int getPlayerPosition();
+
+ int getPlayerDuration();
+
+ void delete(List<MusicDirectory.Entry> songs);
+
+ void unpin(List<MusicDirectory.Entry> songs);
+
+ DownloadFile forSong(MusicDirectory.Entry song);
+
+ long getDownloadListUpdateRevision();
+
+ void setSuggestedPlaylistName(String name, String id);
+
+ String getSuggestedPlaylistName();
+
+ String getSuggestedPlaylistId();
+
+ boolean getEqualizerAvailable();
+
+ boolean getVisualizerAvailable();
+
+ EqualizerController getEqualizerController();
+
+ VisualizerController getVisualizerController();
+
+ boolean isJukeboxEnabled();
+
+ void setJukeboxEnabled(boolean b);
+
+ void adjustJukeboxVolume(boolean up);
+
+ void setSleepTimerDuration(int duration);
+
+ void startSleepTimer();
+
+ void stopSleepTimer();
+
+ boolean getSleepTimer();
+
+ void setVolume(float volume);
+
+ void swap(boolean mainList, int from, int to);
+}
diff --git a/src/github/daneren2005/dsub/service/DownloadServiceImpl.java b/src/github/daneren2005/dsub/service/DownloadServiceImpl.java
new file mode 100644
index 00000000..04875f34
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/DownloadServiceImpl.java
@@ -0,0 +1,1539 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.service;
+
+import static github.daneren2005.dsub.domain.PlayerState.COMPLETED;
+import static github.daneren2005.dsub.domain.PlayerState.DOWNLOADING;
+import static github.daneren2005.dsub.domain.PlayerState.IDLE;
+import static github.daneren2005.dsub.domain.PlayerState.PAUSED;
+import static github.daneren2005.dsub.domain.PlayerState.PREPARED;
+import static github.daneren2005.dsub.domain.PlayerState.PREPARING;
+import static github.daneren2005.dsub.domain.PlayerState.STARTED;
+import static github.daneren2005.dsub.domain.PlayerState.STOPPED;
+import github.daneren2005.dsub.audiofx.EqualizerController;
+import github.daneren2005.dsub.audiofx.VisualizerController;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.PlayerState;
+import github.daneren2005.dsub.domain.RepeatMode;
+import github.daneren2005.dsub.receiver.MediaButtonIntentReceiver;
+import github.daneren2005.dsub.util.CancellableTask;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.ShufflePlayBuffer;
+import github.daneren2005.dsub.util.SimpleServiceBinder;
+import github.daneren2005.dsub.util.Util;
+import github.daneren2005.dsub.util.compat.RemoteControlClientHelper;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Timer;
+import java.util.TimerTask;
+
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.media.AudioManager;
+import android.media.MediaPlayer;
+import android.media.audiofx.AudioEffect;
+import android.os.Build;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.PowerManager;
+import android.util.Log;
+import android.support.v4.util.LruCache;
+import java.net.URLEncoder;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class DownloadServiceImpl extends Service implements DownloadService {
+
+ private static final String TAG = DownloadServiceImpl.class.getSimpleName();
+
+ public static final String CMD_PLAY = "github.daneren2005.dsub.CMD_PLAY";
+ public static final String CMD_TOGGLEPAUSE = "github.daneren2005.dsub.CMD_TOGGLEPAUSE";
+ public static final String CMD_PAUSE = "github.daneren2005.dsub.CMD_PAUSE";
+ public static final String CMD_STOP = "github.daneren2005.dsub.CMD_STOP";
+ public static final String CMD_PREVIOUS = "github.daneren2005.dsub.CMD_PREVIOUS";
+ public static final String CMD_NEXT = "github.daneren2005.dsub.CMD_NEXT";
+
+
+ private RemoteControlClientHelper mRemoteControl;
+
+ private final IBinder binder = new SimpleServiceBinder<DownloadService>(this);
+ private Looper mediaPlayerLooper;
+ private MediaPlayer mediaPlayer;
+ private MediaPlayer nextMediaPlayer;
+ private boolean nextSetup = false;
+ private boolean isPartial = true;
+ private final List<DownloadFile> downloadList = new ArrayList<DownloadFile>();
+ private final List<DownloadFile> backgroundDownloadList = new ArrayList<DownloadFile>();
+ private final Handler handler = new Handler();
+ private Handler mediaPlayerHandler;
+ private final DownloadServiceLifecycleSupport lifecycleSupport = new DownloadServiceLifecycleSupport(this);
+ private final ShufflePlayBuffer shufflePlayBuffer = new ShufflePlayBuffer(this);
+
+ private final LruCache<MusicDirectory.Entry, DownloadFile> downloadFileCache = new LruCache<MusicDirectory.Entry, DownloadFile>(100);
+ private final List<DownloadFile> cleanupCandidates = new ArrayList<DownloadFile>();
+ private final Scrobbler scrobbler = new Scrobbler();
+ private final JukeboxService jukeboxService = new JukeboxService(this);
+ private DownloadFile currentPlaying;
+ private DownloadFile nextPlaying;
+ private DownloadFile currentDownloading;
+ private CancellableTask bufferTask;
+ private CancellableTask nextPlayingTask;
+ private PlayerState playerState = IDLE;
+ private PlayerState nextPlayerState = IDLE;
+ private boolean shufflePlay;
+ private long revision;
+ private static DownloadService instance;
+ private String suggestedPlaylistName;
+ private String suggestedPlaylistId;
+ private PowerManager.WakeLock wakeLock;
+ private boolean keepScreenOn;
+ private int cachedPosition = 0;
+
+ private static boolean equalizerAvailable;
+ private static boolean visualizerAvailable;
+ private EqualizerController equalizerController;
+ private VisualizerController visualizerController;
+ private boolean showVisualization;
+ private boolean jukeboxEnabled;
+ private PositionCache positionCache;
+ private StreamProxy proxy;
+
+ private Timer sleepTimer;
+ private int timerDuration;
+ private boolean autoPlayStart = false;
+
+ static {
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
+ equalizerAvailable = true;
+ visualizerAvailable = true;
+ }
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ new Thread(new Runnable() {
+ public void run() {
+ Looper.prepare();
+
+ mediaPlayer = new MediaPlayer();
+ mediaPlayer.setWakeMode(DownloadServiceImpl.this, PowerManager.PARTIAL_WAKE_LOCK);
+
+ mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
+ @Override
+ public boolean onError(MediaPlayer mediaPlayer, int what, int more) {
+ handleError(new Exception("MediaPlayer error: " + what + " (" + more + ")"));
+ return false;
+ }
+ });
+
+ try {
+ Intent i = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION);
+ i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, mediaPlayer.getAudioSessionId());
+ i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName());
+ sendBroadcast(i);
+ } catch(Throwable e) {
+ // Froyo or lower
+ }
+
+ mediaPlayerLooper = Looper.myLooper();
+ mediaPlayerHandler = new Handler(mediaPlayerLooper);
+ Looper.loop();
+ }
+ }).start();
+
+ Util.registerMediaButtonEventReceiver(this);
+
+ if (mRemoteControl == null) {
+ // Use the remote control APIs (if available) to set the playback state
+ mRemoteControl = RemoteControlClientHelper.createInstance();
+ ComponentName mediaButtonReceiverComponent = new ComponentName(getPackageName(), MediaButtonIntentReceiver.class.getName());
+ mRemoteControl.register(this, mediaButtonReceiverComponent);
+ }
+
+ PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
+ wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, this.getClass().getName());
+ wakeLock.setReferenceCounted(false);
+
+ SharedPreferences prefs = Util.getPreferences(this);
+ try {
+ timerDuration = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_SLEEP_TIMER_DURATION, "5"));
+ } catch(Throwable e) {
+ timerDuration = 5;
+ }
+ sleepTimer = null;
+
+ keepScreenOn = prefs.getBoolean(Constants.PREFERENCES_KEY_KEEP_SCREEN_ON, false);
+
+ instance = this;
+ lifecycleSupport.onCreate();
+
+ if(prefs.getBoolean(Constants.PREFERENCES_EQUALIZER_ON, false)) {
+ getEqualizerController();
+ }
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ super.onStartCommand(intent, flags, startId);
+ lifecycleSupport.onStart(intent);
+ return START_NOT_STICKY;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ instance = null;
+
+ if(currentPlaying != null) currentPlaying.setPlaying(false);
+ if(sleepTimer != null){
+ sleepTimer.cancel();
+ sleepTimer.purge();
+ }
+ lifecycleSupport.onDestroy();
+
+ try {
+ Intent i = new Intent(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION);
+ i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, mediaPlayer.getAudioSessionId());
+ i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName());
+ sendBroadcast(i);
+ } catch(Throwable e) {
+ // Froyo or lower
+ }
+
+ mediaPlayer.release();
+ if(nextMediaPlayer != null) {
+ nextMediaPlayer.release();
+ }
+ mediaPlayerLooper.quit();
+ shufflePlayBuffer.shutdown();
+ if (equalizerController != null) {
+ equalizerController.release();
+ }
+ if (visualizerController != null) {
+ visualizerController.release();
+ }
+ if (mRemoteControl != null) {
+ mRemoteControl.unregister(this);
+ mRemoteControl = null;
+ }
+
+ if(bufferTask != null) {
+ bufferTask.cancel();
+ }
+ if(nextPlayingTask != null) {
+ nextPlayingTask.cancel();
+ }
+ Util.hidePlayingNotification(this, this, handler);
+ }
+
+ public static DownloadService getInstance() {
+ return instance;
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return binder;
+ }
+
+ @Override
+ public synchronized void download(List<MusicDirectory.Entry> songs, boolean save, boolean autoplay, boolean playNext, boolean shuffle) {
+ setShufflePlayEnabled(false);
+ int offset = 1;
+
+ if (songs.isEmpty()) {
+ return;
+ }
+ if (playNext) {
+ if (autoplay && getCurrentPlayingIndex() >= 0) {
+ offset = 0;
+ }
+ for (MusicDirectory.Entry song : songs) {
+ DownloadFile downloadFile = new DownloadFile(this, song, save);
+ downloadList.add(getCurrentPlayingIndex() + offset, downloadFile);
+ offset++;
+ }
+ revision++;
+ } else {
+ int size = size();
+ int index = getCurrentPlayingIndex();
+ for (MusicDirectory.Entry song : songs) {
+ DownloadFile downloadFile = new DownloadFile(this, song, save);
+ downloadList.add(downloadFile);
+ }
+ if(!autoplay && (size - 1) == index) {
+ setNextPlaying();
+ }
+ revision++;
+ }
+ updateJukeboxPlaylist();
+
+ if(shuffle) {
+ shuffle();
+ }
+
+ if (autoplay) {
+ play(0);
+ } else {
+ if (currentPlaying == null) {
+ currentPlaying = downloadList.get(0);
+ currentPlaying.setPlaying(true);
+ }
+ checkDownloads();
+ }
+ lifecycleSupport.serializeDownloadQueue();
+ }
+ public synchronized void downloadBackground(List<MusicDirectory.Entry> songs, boolean save) {
+ for (MusicDirectory.Entry song : songs) {
+ DownloadFile downloadFile = new DownloadFile(this, song, save);
+ backgroundDownloadList.add(downloadFile);
+ }
+ revision++;
+
+ checkDownloads();
+ lifecycleSupport.serializeDownloadQueue();
+ }
+
+ private void updateJukeboxPlaylist() {
+ if (jukeboxEnabled) {
+ jukeboxService.updatePlaylist();
+ }
+ }
+
+ public void restore(List<MusicDirectory.Entry> songs, int currentPlayingIndex, int currentPlayingPosition) {
+ SharedPreferences prefs = Util.getPreferences(this);
+ boolean startShufflePlay = prefs.getBoolean(Constants.PREFERENCES_KEY_SHUFFLE_MODE, false);
+ download(songs, false, false, false, false);
+ if(startShufflePlay) {
+ shufflePlay = true;
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putBoolean(Constants.PREFERENCES_KEY_SHUFFLE_MODE, true);
+ editor.commit();
+ }
+ if (currentPlayingIndex != -1) {
+ while(mediaPlayer == null) {
+ Util.sleepQuietly(50L);
+ }
+
+ play(currentPlayingIndex, false);
+ if (currentPlaying != null && currentPlaying.isCompleteFileAvailable()) {
+ doPlay(currentPlaying, currentPlayingPosition, autoPlayStart);
+ }
+ autoPlayStart = false;
+ }
+ }
+
+ @Override
+ public synchronized void setShufflePlayEnabled(boolean enabled) {
+ shufflePlay = enabled;
+ if (shufflePlay) {
+ clear();
+ checkDownloads();
+ }
+ SharedPreferences.Editor editor = Util.getPreferences(this).edit();
+ editor.putBoolean(Constants.PREFERENCES_KEY_SHUFFLE_MODE, enabled);
+ editor.commit();
+ }
+
+ @Override
+ public boolean isShufflePlayEnabled() {
+ return shufflePlay;
+ }
+
+ @Override
+ public synchronized void shuffle() {
+ Collections.shuffle(downloadList);
+ if (currentPlaying != null) {
+ downloadList.remove(getCurrentPlayingIndex());
+ downloadList.add(0, currentPlaying);
+ }
+ revision++;
+ lifecycleSupport.serializeDownloadQueue();
+ updateJukeboxPlaylist();
+ setNextPlaying();
+ }
+
+ @Override
+ public RepeatMode getRepeatMode() {
+ return Util.getRepeatMode(this);
+ }
+
+ @Override
+ public void setRepeatMode(RepeatMode repeatMode) {
+ Util.setRepeatMode(this, repeatMode);
+ setNextPlaying();
+ }
+
+ @Override
+ public boolean getKeepScreenOn() {
+ return keepScreenOn;
+ }
+
+ @Override
+ public void setKeepScreenOn(boolean keepScreenOn) {
+ this.keepScreenOn = keepScreenOn;
+
+ SharedPreferences prefs = Util.getPreferences(this);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putBoolean(Constants.PREFERENCES_KEY_KEEP_SCREEN_ON, keepScreenOn);
+ editor.commit();
+ }
+
+ @Override
+ public boolean getShowVisualization() {
+ return showVisualization;
+ }
+
+ @Override
+ public void setShowVisualization(boolean showVisualization) {
+ this.showVisualization = showVisualization;
+ }
+
+ @Override
+ public synchronized DownloadFile forSong(MusicDirectory.Entry song) {
+ for (DownloadFile downloadFile : downloadList) {
+ if (downloadFile.getSong().equals(song) &&
+ ((downloadFile.isDownloading() && !downloadFile.isDownloadCancelled() && downloadFile.getPartialFile().exists())
+ || downloadFile.isWorkDone())) {
+ return downloadFile;
+ }
+ }
+ for (DownloadFile downloadFile : backgroundDownloadList) {
+ if (downloadFile.getSong().equals(song)) {
+ return downloadFile;
+ }
+ }
+
+ DownloadFile downloadFile = downloadFileCache.get(song);
+ if (downloadFile == null) {
+ downloadFile = new DownloadFile(this, song, false);
+ downloadFileCache.put(song, downloadFile);
+ }
+ return downloadFile;
+ }
+
+ @Override
+ public synchronized void clear() {
+ clear(true);
+ }
+
+ @Override
+ public synchronized void clearBackground() {
+ if(currentDownloading != null && backgroundDownloadList.contains(currentDownloading)) {
+ currentDownloading.cancelDownload();
+ currentDownloading = null;
+ }
+ backgroundDownloadList.clear();
+ }
+
+ @Override
+ public synchronized void clearIncomplete() {
+ reset();
+ Iterator<DownloadFile> iterator = downloadList.iterator();
+ while (iterator.hasNext()) {
+ DownloadFile downloadFile = iterator.next();
+ if (!downloadFile.isCompleteFileAvailable()) {
+ iterator.remove();
+ }
+ }
+ lifecycleSupport.serializeDownloadQueue();
+ updateJukeboxPlaylist();
+ }
+
+ @Override
+ public synchronized int size() {
+ return downloadList.size();
+ }
+
+ public synchronized void clear(boolean serialize) {
+ reset();
+ downloadList.clear();
+ revision++;
+ if (currentDownloading != null) {
+ currentDownloading.cancelDownload();
+ currentDownloading = null;
+ }
+ setCurrentPlaying(null, false);
+
+ if (serialize) {
+ lifecycleSupport.serializeDownloadQueue();
+ }
+ updateJukeboxPlaylist();
+ setNextPlaying();
+ }
+
+ @Override
+ public synchronized void remove(int which) {
+ downloadList.remove(which);
+ }
+
+ @Override
+ public synchronized void remove(DownloadFile downloadFile) {
+ if (downloadFile == currentDownloading) {
+ currentDownloading.cancelDownload();
+ currentDownloading = null;
+ }
+ if (downloadFile == currentPlaying) {
+ reset();
+ setCurrentPlaying(null, false);
+ }
+ downloadList.remove(downloadFile);
+ backgroundDownloadList.remove(downloadFile);
+ revision++;
+ lifecycleSupport.serializeDownloadQueue();
+ updateJukeboxPlaylist();
+ if(downloadFile == nextPlaying) {
+ setNextPlaying();
+ }
+ }
+
+ @Override
+ public synchronized void delete(List<MusicDirectory.Entry> songs) {
+ for (MusicDirectory.Entry song : songs) {
+ forSong(song).delete();
+ }
+ }
+
+ @Override
+ public synchronized void unpin(List<MusicDirectory.Entry> songs) {
+ for (MusicDirectory.Entry song : songs) {
+ forSong(song).unpin();
+ }
+ }
+
+ synchronized void setCurrentPlaying(int currentPlayingIndex, boolean showNotification) {
+ try {
+ setCurrentPlaying(downloadList.get(currentPlayingIndex), showNotification);
+ } catch (IndexOutOfBoundsException x) {
+ // Ignored
+ }
+ }
+
+ synchronized void setCurrentPlaying(DownloadFile currentPlaying, boolean showNotification) {
+ if(this.currentPlaying != null) {
+ this.currentPlaying.setPlaying(false);
+ }
+ this.currentPlaying = currentPlaying;
+
+ if (currentPlaying != null) {
+ Util.broadcastNewTrackInfo(this, currentPlaying.getSong());
+ mRemoteControl.updateMetadata(this, currentPlaying.getSong());
+ } else {
+ Util.broadcastNewTrackInfo(this, null);
+ Util.hidePlayingNotification(this, this, handler);
+ }
+ }
+
+ synchronized void setNextPlaying() {
+ SharedPreferences prefs = Util.getPreferences(DownloadServiceImpl.this);
+ boolean gaplessPlayback = prefs.getBoolean(Constants.PREFERENCES_KEY_GAPLESS_PLAYBACK, true);
+ if(!gaplessPlayback) {
+ nextPlaying = null;
+ nextPlayerState = IDLE;
+ return;
+ }
+
+ int index = getCurrentPlayingIndex();
+ if (index != -1) {
+ switch (getRepeatMode()) {
+ case OFF:
+ index = index + 1;
+ break;
+ case ALL:
+ index = (index + 1) % size();
+ break;
+ case SINGLE:
+ break;
+ default:
+ break;
+ }
+ }
+
+ nextSetup = false;
+ if(nextPlayingTask != null) {
+ nextPlayingTask.cancel();
+ nextPlayingTask = null;
+ }
+ if(index < size() && index != -1) {
+ nextPlaying = downloadList.get(index);
+ nextPlayingTask = new CheckCompletionTask(nextPlaying);
+ nextPlayingTask.start();
+ } else {
+ nextPlaying = null;
+ setNextPlayerState(IDLE);
+ }
+ }
+
+ @Override
+ public synchronized int getCurrentPlayingIndex() {
+ return downloadList.indexOf(currentPlaying);
+ }
+
+ @Override
+ public DownloadFile getCurrentPlaying() {
+ return currentPlaying;
+ }
+
+ @Override
+ public DownloadFile getCurrentDownloading() {
+ return currentDownloading;
+ }
+
+ @Override
+ public List<DownloadFile> getSongs() {
+ return downloadList;
+ }
+
+ @Override
+ public synchronized List<DownloadFile> getDownloads() {
+ List<DownloadFile> temp = new ArrayList<DownloadFile>();
+ temp.addAll(downloadList);
+ temp.addAll(backgroundDownloadList);
+ return temp;
+ }
+
+ @Override
+ public List<DownloadFile> getBackgroundDownloads() {
+ return backgroundDownloadList;
+ }
+
+ /** Plays either the current song (resume) or the first/next one in queue. */
+ public synchronized void play()
+ {
+ int current = getCurrentPlayingIndex();
+ if (current == -1) {
+ play(0);
+ } else {
+ play(current);
+ }
+ }
+
+ @Override
+ public synchronized void play(int index) {
+ play(index, true);
+ }
+
+ private synchronized void play(int index, boolean start) {
+ if (index < 0 || index >= size()) {
+ reset();
+ setCurrentPlaying(null, false);
+ lifecycleSupport.serializeDownloadQueue();
+ } else {
+ if(nextPlayingTask != null) {
+ nextPlayingTask.cancel();
+ nextPlayingTask = null;
+ }
+ setCurrentPlaying(index, start);
+ if (start) {
+ if (jukeboxEnabled) {
+ jukeboxService.skip(getCurrentPlayingIndex(), 0);
+ setPlayerState(STARTED);
+ } else {
+ bufferAndPlay();
+ }
+ }
+ checkDownloads();
+ setNextPlaying();
+ }
+ }
+ private synchronized void playNext() {
+ if(nextPlaying != null && nextPlayerState == PlayerState.PREPARED) {
+ if(!nextSetup) {
+ playNext(true);
+ } else {
+ nextSetup = false;
+ playNext(false);
+ }
+ } else {
+ onSongCompleted();
+ }
+ }
+ private synchronized void playNext(boolean start) {
+ // Swap the media players since nextMediaPlayer is ready to play
+ if(start) {
+ nextMediaPlayer.start();
+ } else {
+ Log.i(TAG, "nextMediaPlayer already playing");
+ }
+ MediaPlayer tmp = mediaPlayer;
+ mediaPlayer = nextMediaPlayer;
+ nextMediaPlayer = tmp;
+ setCurrentPlaying(nextPlaying, true);
+ setPlayerState(PlayerState.STARTED);
+ setupHandlers(currentPlaying, false);
+ setNextPlaying();
+
+ // Proxy should not be being used here since the next player was already setup to play
+ if(proxy != null) {
+ proxy.stop();
+ proxy = null;
+ }
+ }
+
+ /** Plays or resumes the playback, depending on the current player state. */
+ public synchronized void togglePlayPause() {
+ if (playerState == PAUSED || playerState == COMPLETED || playerState == STOPPED) {
+ start();
+ } else if (playerState == STOPPED || playerState == IDLE) {
+ autoPlayStart = true;
+ play();
+ } else if (playerState == STARTED) {
+ pause();
+ }
+ }
+
+ @Override
+ public synchronized void seekTo(int position) {
+ try {
+ if (jukeboxEnabled) {
+ jukeboxService.skip(getCurrentPlayingIndex(), position / 1000);
+ } else {
+ mediaPlayer.seekTo(position);
+ cachedPosition = position;
+ }
+ } catch (Exception x) {
+ handleError(x);
+ }
+ }
+
+ @Override
+ public synchronized void previous() {
+ int index = getCurrentPlayingIndex();
+ if (index == -1) {
+ return;
+ }
+
+ // Restart song if played more than five seconds.
+ if (getPlayerPosition() > 5000 || index == 0) {
+ play(index);
+ } else {
+ play(index - 1);
+ }
+ }
+
+ @Override
+ public synchronized void next() {
+ int index = getCurrentPlayingIndex();
+ if (index != -1) {
+ play(index + 1);
+ }
+ }
+
+ private void onSongCompleted() {
+ int index = getCurrentPlayingIndex();
+ if (index != -1) {
+ switch (getRepeatMode()) {
+ case OFF:
+ play(index + 1);
+ break;
+ case ALL:
+ play((index + 1) % size());
+ break;
+ case SINGLE:
+ play(index);
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ @Override
+ public synchronized void pause() {
+ try {
+ if (playerState == STARTED) {
+ if (jukeboxEnabled) {
+ jukeboxService.stop();
+ } else {
+ mediaPlayer.pause();
+ }
+ setPlayerState(PAUSED);
+ }
+ } catch (Exception x) {
+ handleError(x);
+ }
+ }
+
+ @Override
+ public synchronized void stop() {
+ try {
+ if (playerState == STARTED) {
+ if (jukeboxEnabled) {
+ jukeboxService.stop();
+ } else {
+ mediaPlayer.pause();
+ }
+ setPlayerState(STOPPED);
+ } else if(playerState == PAUSED) {
+ setPlayerState(STOPPED);
+ }
+ } catch(Exception x) {
+ handleError(x);
+ }
+ }
+
+ @Override
+ public synchronized void start() {
+ try {
+ if (jukeboxEnabled) {
+ jukeboxService.start();
+ } else {
+ mediaPlayer.start();
+ }
+ setPlayerState(STARTED);
+ } catch (Exception x) {
+ handleError(x);
+ }
+ }
+
+ @Override
+ public synchronized void reset() {
+ if (bufferTask != null) {
+ bufferTask.cancel();
+ }
+ try {
+ setPlayerState(IDLE);
+ mediaPlayer.setOnErrorListener(null);
+ mediaPlayer.setOnCompletionListener(null);
+ mediaPlayer.reset();
+ } catch (Exception x) {
+ handleError(x);
+ }
+ }
+
+ @Override
+ public synchronized int getPlayerPosition() {
+ try {
+ if (playerState == IDLE || playerState == DOWNLOADING || playerState == PREPARING) {
+ return 0;
+ }
+ if (jukeboxEnabled) {
+ return jukeboxService.getPositionSeconds() * 1000;
+ } else {
+ return cachedPosition;
+ }
+ } catch (Exception x) {
+ handleError(x);
+ return 0;
+ }
+ }
+
+ @Override
+ public synchronized int getPlayerDuration() {
+ if (currentPlaying != null) {
+ Integer duration = currentPlaying.getSong().getDuration();
+ if (duration != null) {
+ return duration * 1000;
+ }
+ }
+ if (playerState != IDLE && playerState != DOWNLOADING && playerState != PlayerState.PREPARING) {
+ try {
+ return mediaPlayer.getDuration();
+ } catch (Exception x) {
+ handleError(x);
+ }
+ }
+ return 0;
+ }
+
+ @Override
+ public PlayerState getPlayerState() {
+ return playerState;
+ }
+
+ public synchronized void setPlayerState(final PlayerState playerState) {
+ Log.i(TAG, this.playerState.name() + " -> " + playerState.name() + " (" + currentPlaying + ")");
+
+ if (playerState == PAUSED) {
+ lifecycleSupport.serializeDownloadQueue();
+ }
+
+ boolean show = playerState == PlayerState.STARTED;
+ boolean pause = playerState == PlayerState.PAUSED;
+ boolean hide = playerState == PlayerState.STOPPED;
+ Util.broadcastPlaybackStatusChange(this, playerState);
+
+ this.playerState = playerState;
+
+ if(playerState == STARTED) {
+ Util.requestAudioFocus(this);
+ }
+
+ if (show) {
+ Util.showPlayingNotification(this, this, handler, currentPlaying.getSong());
+ } else if (pause) {
+ SharedPreferences prefs = Util.getPreferences(this);
+ if(prefs.getBoolean(Constants.PREFERENCES_KEY_PERSISTENT_NOTIFICATION, false)) {
+ Util.showPlayingNotification(this, this, handler, currentPlaying.getSong());
+ } else {
+ Util.hidePlayingNotification(this, this, handler);
+ }
+ } else if(hide) {
+ Util.hidePlayingNotification(this, this, handler);
+ }
+ mRemoteControl.setPlaybackState(playerState.getRemoteControlClientPlayState());
+
+ if (playerState == STARTED) {
+ scrobbler.scrobble(this, currentPlaying, false);
+ } else if (playerState == COMPLETED) {
+ scrobbler.scrobble(this, currentPlaying, true);
+ }
+
+ if(playerState == STARTED && positionCache == null) {
+ positionCache = new PositionCache();
+ Thread thread = new Thread(positionCache);
+ thread.start();
+ } else if(playerState != STARTED && positionCache != null) {
+ positionCache.stop();
+ positionCache = null;
+ }
+ }
+
+ private class PositionCache implements Runnable {
+ boolean isRunning = true;
+
+ public void stop() {
+ isRunning = false;
+ }
+
+ @Override
+ public void run() {
+ // Stop checking position before the song reaches completion
+ while(isRunning) {
+ try {
+ if(mediaPlayer != null && playerState == STARTED) {
+ cachedPosition = mediaPlayer.getCurrentPosition();
+ }
+ Thread.sleep(200L);
+ }
+ catch(Exception e) {
+ Log.w(TAG, "Crashed getting current position", e);
+ isRunning = false;
+ positionCache = null;
+ }
+ }
+ }
+ }
+
+ private void setPlayerStateCompleted() {
+ Log.i(TAG, this.playerState.name() + " -> " + PlayerState.COMPLETED + " (" + currentPlaying + ")");
+ this.playerState = PlayerState.COMPLETED;
+ if(positionCache != null) {
+ positionCache.stop();
+ positionCache = null;
+ }
+ scrobbler.scrobble(this, currentPlaying, true);
+ }
+
+ private synchronized void setNextPlayerState(PlayerState playerState) {
+ Log.i(TAG, "Next: " + this.nextPlayerState.name() + " -> " + playerState.name() + " (" + nextPlaying + ")");
+ this.nextPlayerState = playerState;
+ }
+
+ @Override
+ public void setSuggestedPlaylistName(String name, String id) {
+ this.suggestedPlaylistName = name;
+ this.suggestedPlaylistId = id;
+ }
+
+ @Override
+ public String getSuggestedPlaylistName() {
+ return suggestedPlaylistName;
+ }
+
+ @Override
+ public String getSuggestedPlaylistId() {
+ return suggestedPlaylistId;
+ }
+
+ @Override
+ public boolean getEqualizerAvailable() {
+ return equalizerAvailable;
+ }
+
+ @Override
+ public boolean getVisualizerAvailable() {
+ return visualizerAvailable;
+ }
+
+ @Override
+ public EqualizerController getEqualizerController() {
+ if (equalizerAvailable && equalizerController == null) {
+ equalizerController = new EqualizerController(this, mediaPlayer);
+ if (!equalizerController.isAvailable()) {
+ equalizerController = null;
+ } else {
+ equalizerController.loadSettings();
+ }
+ }
+ return equalizerController;
+ }
+
+ @Override
+ public VisualizerController getVisualizerController() {
+ if (visualizerAvailable && visualizerController == null) {
+ visualizerController = new VisualizerController(this, mediaPlayer);
+ if (!visualizerController.isAvailable()) {
+ visualizerController = null;
+ }
+ }
+ return visualizerController;
+ }
+
+ @Override
+ public boolean isJukeboxEnabled() {
+ return jukeboxEnabled;
+ }
+
+ @Override
+ public void setJukeboxEnabled(boolean jukeboxEnabled) {
+ this.jukeboxEnabled = jukeboxEnabled;
+ jukeboxService.setEnabled(jukeboxEnabled);
+ if (jukeboxEnabled) {
+ reset();
+
+ // Cancel current download, if necessary.
+ if (currentDownloading != null) {
+ currentDownloading.cancelDownload();
+ }
+ }
+ }
+
+ @Override
+ public void adjustJukeboxVolume(boolean up) {
+ jukeboxService.adjustVolume(up);
+ }
+
+ private synchronized void bufferAndPlay() {
+ if(playerState != PREPARED) {
+ reset();
+
+ bufferTask = new BufferTask(currentPlaying, 0);
+ bufferTask.start();
+ } else {
+ doPlay(currentPlaying, 0, true);
+ }
+ }
+
+ 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();
+ isPartial = file.equals(downloadFile.getPartialFile());
+ downloadFile.updateModificationDate();
+
+ mediaPlayer.setOnCompletionListener(null);
+ mediaPlayer.reset();
+ setPlayerState(IDLE);
+ mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
+ String dataSource = file.getPath();
+ if(isPartial) {
+ if (proxy == null) {
+ proxy = new StreamProxy(this);
+ proxy.start();
+ }
+ dataSource = String.format("http://127.0.0.1:%d/%s", proxy.getPort(), URLEncoder.encode(dataSource, Constants.UTF_8));
+ Log.i(TAG, "Data Source: " + dataSource);
+ } else if(proxy != null) {
+ proxy.stop();
+ proxy = null;
+ }
+ mediaPlayer.setDataSource(dataSource);
+ setPlayerState(PREPARING);
+
+ mediaPlayer.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() {
+ public void onBufferingUpdate(MediaPlayer mp, int percent) {
+ Log.i(TAG, "Buffered " + percent + "%");
+ if(percent == 100) {
+ mediaPlayer.setOnBufferingUpdateListener(null);
+ }
+ }
+ });
+
+ mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
+ public void onPrepared(MediaPlayer mediaPlayer) {
+ try {
+ setPlayerState(PREPARED);
+
+ synchronized (DownloadServiceImpl.this) {
+ if (position != 0) {
+ Log.i(TAG, "Restarting player from position " + position);
+ mediaPlayer.seekTo(position);
+ }
+ cachedPosition = position;
+
+ if (start) {
+ mediaPlayer.start();
+ setPlayerState(STARTED);
+ } else {
+ setPlayerState(PAUSED);
+ }
+ }
+
+ lifecycleSupport.serializeDownloadQueue();
+ } catch (Exception x) {
+ handleError(x);
+ }
+ }
+ });
+
+ setupHandlers(downloadFile, isPartial);
+
+ mediaPlayer.prepareAsync();
+ } catch (Exception x) {
+ handleError(x);
+ }
+ }
+
+ private synchronized void setupNext(final DownloadFile downloadFile) {
+ try {
+ final File file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteFile() : downloadFile.getPartialFile();
+ if(nextMediaPlayer != null) {
+ nextMediaPlayer.setOnCompletionListener(null);
+ nextMediaPlayer.release();
+ nextMediaPlayer = null;
+ }
+ nextMediaPlayer = new MediaPlayer();
+ nextMediaPlayer.setWakeMode(DownloadServiceImpl.this, PowerManager.PARTIAL_WAKE_LOCK);
+ try {
+ nextMediaPlayer.setAudioSessionId(mediaPlayer.getAudioSessionId());
+ } catch(Throwable e) {
+ nextMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
+ }
+ nextMediaPlayer.setDataSource(file.getPath());
+ setNextPlayerState(PREPARING);
+
+ nextMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
+ public void onPrepared(MediaPlayer mp) {
+ try {
+ setNextPlayerState(PREPARED);
+
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && (playerState == PlayerState.STARTED || playerState == PlayerState.PAUSED)) {
+ mediaPlayer.setNextMediaPlayer(nextMediaPlayer);
+ nextSetup = true;
+ }
+ } catch (Exception x) {
+ handleErrorNext(x);
+ }
+ }
+ });
+
+ nextMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
+ public boolean onError(MediaPlayer mediaPlayer, int what, int extra) {
+ Log.w(TAG, "Error on playing next " + "(" + what + ", " + extra + "): " + downloadFile);
+ return true;
+ }
+ });
+
+ nextMediaPlayer.prepareAsync();
+ } catch (Exception x) {
+ handleErrorNext(x);
+ }
+ }
+
+ private void setupHandlers(final DownloadFile downloadFile, final boolean isPartial) {
+ final int duration = downloadFile.getSong().getDuration() == null ? 0 : downloadFile.getSong().getDuration() * 1000;
+ mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
+ public boolean onError(MediaPlayer mediaPlayer, int what, int extra) {
+ Log.w(TAG, "Error on playing file " + "(" + what + ", " + extra + "): " + downloadFile);
+ int pos = cachedPosition;
+ reset();
+ if (!isPartial || (downloadFile.isWorkDone() && (Math.abs(duration - pos) < 10000))) {
+ playNext();
+ } else {
+ downloadFile.setPlaying(false);
+ doPlay(downloadFile, pos, true);
+ downloadFile.setPlaying(true);
+ }
+ return true;
+ }
+ });
+
+ mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
+ @Override
+ public void onCompletion(MediaPlayer mediaPlayer) {
+ // Acquire a temporary wakelock, since when we return from
+ // this callback the MediaPlayer will release its wakelock
+ // and allow the device to go to sleep.
+ wakeLock.acquire(60000);
+
+ setPlayerStateCompleted();
+
+ int pos = cachedPosition;
+ Log.i(TAG, "Ending position " + pos + " of " + duration);
+ if (!isPartial || (downloadFile.isWorkDone() && (Math.abs(duration - pos) < 10000))) {
+ playNext();
+ return;
+ }
+
+ // If file is not completely downloaded, restart the playback from the current position.
+ synchronized (DownloadServiceImpl.this) {
+ if(downloadFile.isWorkDone()) {
+ // Complete was called early even though file is fully buffered
+ Log.i(TAG, "Requesting restart from " + pos + " of " + duration);
+ reset();
+ downloadFile.setPlaying(false);
+ doPlay(downloadFile, pos, true);
+ downloadFile.setPlaying(true);
+ } else {
+ Log.i(TAG, "Requesting restart from " + pos + " of " + duration);
+ reset();
+ bufferTask = new BufferTask(downloadFile, pos);
+ bufferTask.start();
+ }
+ }
+ }
+ });
+ }
+
+ @Override
+ public void setSleepTimerDuration(int duration){
+ timerDuration = duration;
+ }
+
+ @Override
+ public void startSleepTimer(){
+ if(sleepTimer != null){
+ sleepTimer.cancel();
+ sleepTimer.purge();
+ }
+
+ sleepTimer = new Timer();
+
+ sleepTimer.schedule(new TimerTask() {
+ @Override
+ public void run() {
+ pause();
+ sleepTimer.cancel();
+ sleepTimer.purge();
+ sleepTimer = null;
+ }
+
+ }, timerDuration * 60 * 1000);
+ }
+
+ @Override
+ public void stopSleepTimer() {
+ if(sleepTimer != null){
+ sleepTimer.cancel();
+ sleepTimer.purge();
+ }
+ sleepTimer = null;
+ }
+
+ @Override
+ public boolean getSleepTimer() {
+ return sleepTimer != null;
+ }
+
+ @Override
+ public void setVolume(float volume) {
+ if(mediaPlayer != null) {
+ mediaPlayer.setVolume(volume, volume);
+ }
+ }
+
+ @Override
+ public synchronized void swap(boolean mainList, int from, int to) {
+ List<DownloadFile> list = mainList ? downloadList : backgroundDownloadList;
+ int max = list.size();
+ if(to >= max) {
+ to = max - 1;
+ }
+ else if(to < 0) {
+ to = 0;
+ }
+
+ int currentPlayingIndex = getCurrentPlayingIndex();
+ DownloadFile movedSong = list.remove(from);
+ list.add(to, movedSong);
+ if(jukeboxEnabled && mainList) {
+ updateJukeboxPlaylist();
+ } else if(mainList && (movedSong == nextPlaying || (currentPlayingIndex + 1) == to)) {
+ // Moving next playing or moving a song to be next playing
+ setNextPlaying();
+ }
+ }
+
+ private void handleError(Exception x) {
+ Log.w(TAG, "Media player error: " + x, x);
+ mediaPlayer.reset();
+ setPlayerState(IDLE);
+ }
+ private void handleErrorNext(Exception x) {
+ Log.w(TAG, "Next Media player error: " + x, x);
+ nextMediaPlayer.reset();
+ setNextPlayerState(IDLE);
+ }
+
+ protected synchronized void checkDownloads() {
+ if (!Util.isExternalStoragePresent() || !lifecycleSupport.isExternalStorageAvailable()) {
+ return;
+ }
+
+ if (shufflePlay) {
+ checkShufflePlay();
+ }
+
+ if (jukeboxEnabled || !Util.isNetworkConnected(this)) {
+ return;
+ }
+
+ if (downloadList.isEmpty() && backgroundDownloadList.isEmpty()) {
+ return;
+ }
+
+ // Need to download current playing?
+ if (currentPlaying != null && currentPlaying != currentDownloading && !currentPlaying.isWorkDone()) {
+ // Cancel current download, if necessary.
+ if (currentDownloading != null) {
+ currentDownloading.cancelDownload();
+ }
+
+ currentDownloading = currentPlaying;
+ currentDownloading.download();
+ cleanupCandidates.add(currentDownloading);
+ }
+
+ // Find a suitable target for download.
+ else if (currentDownloading == null || currentDownloading.isWorkDone() || currentDownloading.isFailed() && (!downloadList.isEmpty() || !backgroundDownloadList.isEmpty())) {
+ currentDownloading = null;
+ int n = size();
+
+ int preloaded = 0;
+
+ if(n != 0) {
+ int start = currentPlaying == null ? 0 : getCurrentPlayingIndex();
+ if(start == -1) {
+ start = 0;
+ }
+ int i = start;
+ do {
+ DownloadFile downloadFile = downloadList.get(i);
+ if (!downloadFile.isWorkDone()) {
+ if (downloadFile.shouldSave() || preloaded < Util.getPreloadCount(this)) {
+ currentDownloading = downloadFile;
+ currentDownloading.download();
+ cleanupCandidates.add(currentDownloading);
+ if(i == (start + 1)) {
+ setNextPlayerState(DOWNLOADING);
+ }
+ break;
+ }
+ } else if (currentPlaying != downloadFile) {
+ preloaded++;
+ }
+
+ i = (i + 1) % n;
+ } while (i != start);
+ }
+
+ if((preloaded + 1 == n || preloaded >= Util.getPreloadCount(this) || downloadList.isEmpty()) && !backgroundDownloadList.isEmpty()) {
+ for(int i = 0; i < backgroundDownloadList.size(); i++) {
+ DownloadFile downloadFile = backgroundDownloadList.get(i);
+ if(downloadFile.isWorkDone() && (!downloadFile.shouldSave() || downloadFile.isSaved())) {
+ // Don't need to keep list like active song list
+ backgroundDownloadList.remove(i);
+ revision++;
+ i--;
+ } else {
+ currentDownloading = downloadFile;
+ currentDownloading.download();
+ cleanupCandidates.add(currentDownloading);
+ break;
+ }
+ }
+ }
+ }
+
+ // Delete obsolete .partial and .complete files.
+ cleanup();
+ }
+
+ private synchronized void checkShufflePlay() {
+
+ // Get users desired random playlist size
+ SharedPreferences prefs = Util.getPreferences(this);
+ int listSize = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_RANDOM_SIZE, "20"));
+ boolean wasEmpty = downloadList.isEmpty();
+
+ long revisionBefore = revision;
+
+ // First, ensure that list is at least 20 songs long.
+ int size = size();
+ if (size < listSize) {
+ for (MusicDirectory.Entry song : shufflePlayBuffer.get(listSize - size)) {
+ DownloadFile downloadFile = new DownloadFile(this, song, false);
+ downloadList.add(downloadFile);
+ revision++;
+ }
+ }
+
+ int currIndex = currentPlaying == null ? 0 : getCurrentPlayingIndex();
+
+ // Only shift playlist if playing song #5 or later.
+ if (currIndex > 4) {
+ int songsToShift = currIndex - 2;
+ for (MusicDirectory.Entry song : shufflePlayBuffer.get(songsToShift)) {
+ downloadList.add(new DownloadFile(this, song, false));
+ downloadList.get(0).cancelDownload();
+ downloadList.remove(0);
+ revision++;
+ }
+ }
+
+ if (revisionBefore != revision) {
+ updateJukeboxPlaylist();
+ }
+
+ if (wasEmpty && !downloadList.isEmpty()) {
+ play(0);
+ }
+ }
+
+ public long getDownloadListUpdateRevision() {
+ return revision;
+ }
+
+ private synchronized void cleanup() {
+ Iterator<DownloadFile> iterator = cleanupCandidates.iterator();
+ while (iterator.hasNext()) {
+ DownloadFile downloadFile = iterator.next();
+ if (downloadFile != currentPlaying && downloadFile != currentDownloading) {
+ if (downloadFile.cleanup()) {
+ iterator.remove();
+ }
+ }
+ }
+ }
+
+ private class BufferTask extends CancellableTask {
+ private final DownloadFile downloadFile;
+ private final int position;
+ private final long expectedFileSize;
+ private final File partialFile;
+
+ public BufferTask(DownloadFile downloadFile, int position) {
+ this.downloadFile = downloadFile;
+ this.position = position;
+ partialFile = downloadFile.getPartialFile();
+
+ SharedPreferences prefs = Util.getPreferences(DownloadServiceImpl.this);
+ long bufferLength = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_BUFFER_LENGTH, "5"));
+ if(bufferLength == 0) {
+ // Set to seconds in a day, basically infinity
+ bufferLength = 86400L;
+ }
+
+ // Calculate roughly how many bytes BUFFER_LENGTH_SECONDS corresponds to.
+ int bitRate = downloadFile.getBitRate();
+ long byteCount = Math.max(100000, bitRate * 1024L / 8L * bufferLength);
+
+ // Find out how large the file should grow before resuming playback.
+ Log.i(TAG, "Buffering from position " + position + " and bitrate " + bitRate);
+ expectedFileSize = (position * bitRate / 8) + byteCount;
+ }
+
+ @Override
+ public void execute() {
+ setPlayerState(DOWNLOADING);
+
+ while (!bufferComplete()) {
+ Util.sleepQuietly(1000L);
+ if (isCancelled()) {
+ return;
+ }
+ }
+ doPlay(downloadFile, position, true);
+ }
+
+ private boolean bufferComplete() {
+ boolean completeFileAvailable = downloadFile.isWorkDone();
+ long size = partialFile.length();
+
+ Log.i(TAG, "Buffering " + partialFile + " (" + size + "/" + expectedFileSize + ", " + completeFileAvailable + ")");
+ return completeFileAvailable || size >= expectedFileSize;
+ }
+
+ @Override
+ public String toString() {
+ return "BufferTask (" + downloadFile + ")";
+ }
+ }
+
+ private class CheckCompletionTask extends CancellableTask {
+ private final DownloadFile downloadFile;
+ private final File partialFile;
+
+ public CheckCompletionTask(DownloadFile downloadFile) {
+ setNextPlayerState(PlayerState.IDLE);
+ this.downloadFile = downloadFile;
+ if(downloadFile != null) {
+ partialFile = downloadFile.getPartialFile();
+ } else {
+ partialFile = null;
+ }
+ }
+
+ @Override
+ public void execute() {
+ if(downloadFile == null) {
+ return;
+ }
+
+ // Do an initial sleep so this prepare can't compete with main prepare
+ Util.sleepQuietly(5000L);
+ while (!bufferComplete()) {
+ Util.sleepQuietly(5000L);
+ if (isCancelled()) {
+ return;
+ }
+ }
+
+ // Start the setup of the next media player
+ mediaPlayerHandler.post(new Runnable() {
+ public void run() {
+ setupNext(downloadFile);
+ }
+ });
+ }
+
+ private boolean bufferComplete() {
+ boolean completeFileAvailable = downloadFile.isWorkDone();
+ Log.i(TAG, "Buffering next " + partialFile + " (" + partialFile.length() + ")");
+ return completeFileAvailable && (playerState == PlayerState.STARTED || playerState == PlayerState.PAUSED);
+ }
+
+ @Override
+ public String toString() {
+ return "CheckCompletionTask (" + downloadFile + ")";
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/service/DownloadServiceLifecycleSupport.java b/src/github/daneren2005/dsub/service/DownloadServiceLifecycleSupport.java
new file mode 100644
index 00000000..ae378865
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/DownloadServiceLifecycleSupport.java
@@ -0,0 +1,352 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.service;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.RemoteControlClient;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.telephony.PhoneStateListener;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+import android.view.KeyEvent;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.PlayerState;
+import github.daneren2005.dsub.util.CacheCleaner;
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.Util;
+
+/**
+ * @author Sindre Mehus
+ */
+public class DownloadServiceLifecycleSupport {
+
+ private static final String TAG = DownloadServiceLifecycleSupport.class.getSimpleName();
+ private static final String FILENAME_DOWNLOADS_SER = "downloadstate.ser";
+
+ private final DownloadServiceImpl downloadService;
+ private ScheduledExecutorService executorService;
+ private BroadcastReceiver headsetEventReceiver;
+ private BroadcastReceiver ejectEventReceiver;
+ private PhoneStateListener phoneStateListener;
+ private boolean externalStorageAvailable= true;
+ private ReentrantLock lock = new ReentrantLock();
+ private final AtomicBoolean setup = new AtomicBoolean(false);
+ private long lastPressTime = 0;
+
+ /**
+ * This receiver manages the intent that could come from other applications.
+ */
+ private BroadcastReceiver intentReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ Log.i(TAG, "intentReceiver.onReceive: " + action);
+ if (DownloadServiceImpl.CMD_PLAY.equals(action)) {
+ downloadService.play();
+ } else if (DownloadServiceImpl.CMD_NEXT.equals(action)) {
+ downloadService.next();
+ } else if (DownloadServiceImpl.CMD_PREVIOUS.equals(action)) {
+ downloadService.previous();
+ } else if (DownloadServiceImpl.CMD_TOGGLEPAUSE.equals(action)) {
+ downloadService.togglePlayPause();
+ } else if (DownloadServiceImpl.CMD_PAUSE.equals(action)) {
+ downloadService.pause();
+ } else if (DownloadServiceImpl.CMD_STOP.equals(action)) {
+ downloadService.pause();
+ downloadService.seekTo(0);
+ }
+ }
+ };
+
+
+ public DownloadServiceLifecycleSupport(DownloadServiceImpl downloadService) {
+ this.downloadService = downloadService;
+ }
+
+ public void onCreate() {
+ Runnable downloadChecker = new Runnable() {
+ @Override
+ public void run() {
+ try {
+ downloadService.checkDownloads();
+ } catch (Throwable x) {
+ Log.e(TAG, "checkDownloads() failed.", x);
+ }
+ }
+ };
+
+ executorService = Executors.newSingleThreadScheduledExecutor();
+ executorService.scheduleWithFixedDelay(downloadChecker, 5, 5, TimeUnit.SECONDS);
+
+ // Pause when headset is unplugged.
+ headsetEventReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.i(TAG, "Headset event for: " + intent.getExtras().get("name"));
+ if (intent.getExtras().getInt("state") == 0) {
+ if(!downloadService.isJukeboxEnabled()) {
+ downloadService.pause();
+ }
+ }
+ }
+ };
+ downloadService.registerReceiver(headsetEventReceiver, new IntentFilter(Intent.ACTION_HEADSET_PLUG));
+
+ // Stop when SD card is ejected.
+ ejectEventReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ externalStorageAvailable = Intent.ACTION_MEDIA_MOUNTED.equals(intent.getAction());
+ if (!externalStorageAvailable) {
+ Log.i(TAG, "External media is ejecting. Stopping playback.");
+ downloadService.reset();
+ } else {
+ Log.i(TAG, "External media is available.");
+ }
+ }
+ };
+ IntentFilter ejectFilter = new IntentFilter(Intent.ACTION_MEDIA_EJECT);
+ ejectFilter.addAction(Intent.ACTION_MEDIA_MOUNTED);
+ ejectFilter.addDataScheme("file");
+ downloadService.registerReceiver(ejectEventReceiver, ejectFilter);
+
+ // React to media buttons.
+ Util.registerMediaButtonEventReceiver(downloadService);
+
+ // Pause temporarily on incoming phone calls.
+ phoneStateListener = new MyPhoneStateListener();
+ TelephonyManager telephonyManager = (TelephonyManager) downloadService.getSystemService(Context.TELEPHONY_SERVICE);
+ telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
+
+ // Register the handler for outside intents.
+ IntentFilter commandFilter = new IntentFilter();
+ commandFilter.addAction(DownloadServiceImpl.CMD_PLAY);
+ commandFilter.addAction(DownloadServiceImpl.CMD_TOGGLEPAUSE);
+ commandFilter.addAction(DownloadServiceImpl.CMD_PAUSE);
+ commandFilter.addAction(DownloadServiceImpl.CMD_STOP);
+ commandFilter.addAction(DownloadServiceImpl.CMD_PREVIOUS);
+ commandFilter.addAction(DownloadServiceImpl.CMD_NEXT);
+ downloadService.registerReceiver(intentReceiver, commandFilter);
+
+ deserializeDownloadQueue();
+
+ new CacheCleaner(downloadService, downloadService).clean();
+ }
+
+ public void onStart(Intent intent) {
+ if (intent != null && intent.getExtras() != null) {
+ KeyEvent event = (KeyEvent) intent.getExtras().get(Intent.EXTRA_KEY_EVENT);
+ if (event != null) {
+ handleKeyEvent(event);
+ }
+ }
+ }
+
+ public void onDestroy() {
+ executorService.shutdown();
+ serializeDownloadQueueNow();
+ downloadService.clear(false);
+ downloadService.unregisterReceiver(ejectEventReceiver);
+ downloadService.unregisterReceiver(headsetEventReceiver);
+ downloadService.unregisterReceiver(intentReceiver);
+
+ TelephonyManager telephonyManager = (TelephonyManager) downloadService.getSystemService(Context.TELEPHONY_SERVICE);
+ telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE);
+ }
+
+ public boolean isExternalStorageAvailable() {
+ return externalStorageAvailable;
+ }
+
+ public void serializeDownloadQueue() {
+ if(!setup.get()) {
+ return;
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ new SerializeTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ } else {
+ new SerializeTask().execute();
+ }
+ }
+
+ public void serializeDownloadQueueNow() {
+ List<DownloadFile> songs = new ArrayList<DownloadFile>(downloadService.getSongs());
+ State state = new State();
+ for (DownloadFile downloadFile : songs) {
+ state.songs.add(downloadFile.getSong());
+ }
+ state.currentPlayingIndex = downloadService.getCurrentPlayingIndex();
+ state.currentPlayingPosition = downloadService.getPlayerPosition();
+
+ Log.i(TAG, "Serialized currentPlayingIndex: " + state.currentPlayingIndex + ", currentPlayingPosition: " + state.currentPlayingPosition);
+ FileUtil.serialize(downloadService, state, FILENAME_DOWNLOADS_SER);
+ }
+
+ private void deserializeDownloadQueue() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ new DeserializeTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ } else {
+ new DeserializeTask().execute();
+ }
+ }
+ private void deserializeDownloadQueueNow() {
+ State state = FileUtil.deserialize(downloadService, FILENAME_DOWNLOADS_SER);
+ if (state == null) {
+ return;
+ }
+ Log.i(TAG, "Deserialized currentPlayingIndex: " + state.currentPlayingIndex + ", currentPlayingPosition: " + state.currentPlayingPosition);
+ downloadService.restore(state.songs, state.currentPlayingIndex, state.currentPlayingPosition);
+
+ // Work-around: Serialize again, as the restore() method creates a serialization without current playing info.
+ serializeDownloadQueue();
+ }
+
+ private 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.seekTo(downloadService.getPlayerPosition() - 10000);
+ break;
+ case RemoteControlClient.FLAG_KEY_MEDIA_NEXT:
+ case KeyEvent.KEYCODE_MEDIA_NEXT:
+ downloadService.seekTo(downloadService.getPlayerPosition() + 10000);
+ break;
+ }
+ } else if(event.getAction() == KeyEvent.ACTION_UP) {
+ switch (event.getKeyCode()) {
+ case RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE:
+ downloadService.togglePlayPause();
+ break;
+ case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
+ case KeyEvent.KEYCODE_HEADSETHOOK:
+ if(lastPressTime < (System.currentTimeMillis() - 500)) {
+ lastPressTime = System.currentTimeMillis();
+ downloadService.togglePlayPause();
+ } else {
+ downloadService.next();
+ }
+ break;
+ case RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS:
+ case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
+ downloadService.previous();
+ break;
+ case RemoteControlClient.FLAG_KEY_MEDIA_NEXT:
+ case KeyEvent.KEYCODE_MEDIA_NEXT:
+ if (downloadService.getCurrentPlayingIndex() < downloadService.size() - 1) {
+ downloadService.next();
+ }
+ break;
+ case RemoteControlClient.FLAG_KEY_MEDIA_STOP:
+ case KeyEvent.KEYCODE_MEDIA_STOP:
+ downloadService.stop();
+ break;
+ case RemoteControlClient.FLAG_KEY_MEDIA_PLAY:
+ case KeyEvent.KEYCODE_MEDIA_PLAY:
+ if(downloadService.getPlayerState() != PlayerState.STARTED) {
+ downloadService.start();
+ }
+ break;
+ case RemoteControlClient.FLAG_KEY_MEDIA_PAUSE:
+ case KeyEvent.KEYCODE_MEDIA_PAUSE:
+ downloadService.pause();
+ default:
+ break;
+ }
+ }
+ }
+
+ /**
+ * Logic taken from packages/apps/Music. Will pause when an incoming
+ * call rings or if a call (incoming or outgoing) is connected.
+ */
+ private class MyPhoneStateListener extends PhoneStateListener {
+ private boolean resumeAfterCall;
+
+ @Override
+ public void onCallStateChanged(int state, String incomingNumber) {
+ switch (state) {
+ case TelephonyManager.CALL_STATE_RINGING:
+ case TelephonyManager.CALL_STATE_OFFHOOK:
+ if (downloadService.getPlayerState() == PlayerState.STARTED && !downloadService.isJukeboxEnabled()) {
+ resumeAfterCall = true;
+ downloadService.pause();
+ }
+ break;
+ case TelephonyManager.CALL_STATE_IDLE:
+ if (resumeAfterCall) {
+ resumeAfterCall = false;
+ downloadService.start();
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ private static class State implements Serializable {
+ private static final long serialVersionUID = -6346438781062572270L;
+
+ private List<MusicDirectory.Entry> songs = new ArrayList<MusicDirectory.Entry>();
+ private int currentPlayingIndex;
+ private int currentPlayingPosition;
+ }
+
+ private class SerializeTask extends AsyncTask<Void, Void, Void> {
+ @Override
+ protected Void doInBackground(Void... params) {
+ if(lock.tryLock()) {
+ try {
+ serializeDownloadQueueNow();
+ } finally {
+ lock.unlock();
+ }
+ }
+ return null;
+ }
+ }
+ private class DeserializeTask extends AsyncTask<Void, Void, Void> {
+ @Override
+ protected Void doInBackground(Void... params) {
+ try {
+ lock.lock();
+ deserializeDownloadQueueNow();
+ setup.set(true);
+ } finally {
+ lock.unlock();
+ }
+ return null;
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/service/JukeboxService.java b/src/github/daneren2005/dsub/service/JukeboxService.java
new file mode 100644
index 00000000..96b82336
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/JukeboxService.java
@@ -0,0 +1,358 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.service;
+
+import android.content.Context;
+import android.os.Handler;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ProgressBar;
+import android.widget.Toast;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.JukeboxStatus;
+import github.daneren2005.dsub.domain.PlayerState;
+import github.daneren2005.dsub.service.parser.SubsonicRESTException;
+import github.daneren2005.dsub.util.Util;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Provides an asynchronous interface to the remote jukebox on the Subsonic server.
+ *
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class JukeboxService {
+
+ private static final String TAG = JukeboxService.class.getSimpleName();
+ private static final long STATUS_UPDATE_INTERVAL_SECONDS = 5L;
+
+ private final Handler handler = new Handler();
+ private final TaskQueue tasks = new TaskQueue();
+ private final DownloadServiceImpl downloadService;
+ private final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
+ private ScheduledFuture<?> statusUpdateFuture;
+ private final AtomicLong timeOfLastUpdate = new AtomicLong();
+ private JukeboxStatus jukeboxStatus;
+ private float gain = 0.5f;
+ private VolumeToast volumeToast;
+
+ // TODO: Report warning if queue fills up.
+ // TODO: Create shutdown method?
+ // TODO: Disable repeat.
+ // TODO: Persist RC state?
+ // TODO: Minimize status updates.
+
+ public JukeboxService(DownloadServiceImpl downloadService) {
+ this.downloadService = downloadService;
+ new Thread() {
+ @Override
+ public void run() {
+ processTasks();
+ }
+ }.start();
+ }
+
+ private synchronized void startStatusUpdate() {
+ stopStatusUpdate();
+ Runnable updateTask = new Runnable() {
+ @Override
+ public void run() {
+ tasks.remove(GetStatus.class);
+ tasks.add(new GetStatus());
+ }
+ };
+ statusUpdateFuture = executorService.scheduleWithFixedDelay(updateTask, STATUS_UPDATE_INTERVAL_SECONDS,
+ STATUS_UPDATE_INTERVAL_SECONDS, TimeUnit.SECONDS);
+ }
+
+ private synchronized void stopStatusUpdate() {
+ if (statusUpdateFuture != null) {
+ statusUpdateFuture.cancel(false);
+ statusUpdateFuture = null;
+ }
+ }
+
+ private void processTasks() {
+ while (true) {
+ JukeboxTask task = null;
+ try {
+ task = tasks.take();
+ JukeboxStatus status = task.execute();
+ onStatusUpdate(status);
+ } catch (Throwable x) {
+ onError(task, x);
+ }
+ }
+ }
+
+ private void onStatusUpdate(JukeboxStatus jukeboxStatus) {
+ timeOfLastUpdate.set(System.currentTimeMillis());
+ this.jukeboxStatus = jukeboxStatus;
+
+ // Track change?
+ Integer index = jukeboxStatus.getCurrentPlayingIndex();
+ if (index != null && index != -1 && index != downloadService.getCurrentPlayingIndex()) {
+ downloadService.setPlayerState(PlayerState.COMPLETED);
+ downloadService.setCurrentPlaying(index, true);
+ downloadService.setPlayerState(PlayerState.STARTED);
+ }
+ }
+
+ private void onError(JukeboxTask task, Throwable x) {
+ if (x instanceof ServerTooOldException && !(task instanceof Stop)) {
+ disableJukeboxOnError(x, R.string.download_jukebox_server_too_old);
+ } else if (x instanceof OfflineException && !(task instanceof Stop)) {
+ disableJukeboxOnError(x, R.string.download_jukebox_offline);
+ } else if (x instanceof SubsonicRESTException && ((SubsonicRESTException) x).getCode() == 50 && !(task instanceof Stop)) {
+ disableJukeboxOnError(x, R.string.download_jukebox_not_authorized);
+ } else {
+ Log.e(TAG, "Failed to process jukebox task: " + x, x);
+ }
+ }
+
+ private void disableJukeboxOnError(Throwable x, final int resourceId) {
+ Log.w(TAG, x.toString());
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ Util.toast(downloadService, resourceId, false);
+ }
+ });
+ downloadService.setJukeboxEnabled(false);
+ }
+
+ public void updatePlaylist() {
+ tasks.remove(Skip.class);
+ tasks.remove(Stop.class);
+ tasks.remove(Start.class);
+
+ List<String> ids = new ArrayList<String>();
+ for (DownloadFile file : downloadService.getDownloads()) {
+ ids.add(file.getSong().getId());
+ }
+ tasks.add(new SetPlaylist(ids));
+ }
+
+ public void skip(final int index, final int offsetSeconds) {
+ tasks.remove(Skip.class);
+ tasks.remove(Stop.class);
+ tasks.remove(Start.class);
+
+ startStatusUpdate();
+ if (jukeboxStatus != null) {
+ jukeboxStatus.setPositionSeconds(offsetSeconds);
+ }
+ tasks.add(new Skip(index, offsetSeconds));
+ downloadService.setPlayerState(PlayerState.STARTED);
+ }
+
+ public void stop() {
+ tasks.remove(Stop.class);
+ tasks.remove(Start.class);
+
+ stopStatusUpdate();
+ tasks.add(new Stop());
+ }
+
+ public void start() {
+ tasks.remove(Stop.class);
+ tasks.remove(Start.class);
+
+ startStatusUpdate();
+ tasks.add(new Start());
+ }
+
+ public synchronized void adjustVolume(boolean up) {
+ float delta = up ? 0.1f : -0.1f;
+ gain += delta;
+ gain = Math.max(gain, 0.0f);
+ gain = Math.min(gain, 1.0f);
+
+ tasks.remove(SetGain.class);
+ tasks.add(new SetGain(gain));
+
+ if (volumeToast == null) {
+ volumeToast = new VolumeToast(downloadService);
+ }
+ volumeToast.setVolume(gain);
+ }
+
+ private MusicService getMusicService() {
+ return MusicServiceFactory.getMusicService(downloadService);
+ }
+
+ public int getPositionSeconds() {
+ if (jukeboxStatus == null || jukeboxStatus.getPositionSeconds() == null || timeOfLastUpdate.get() == 0) {
+ return 0;
+ }
+
+ if (jukeboxStatus.isPlaying()) {
+ int secondsSinceLastUpdate = (int) ((System.currentTimeMillis() - timeOfLastUpdate.get()) / 1000L);
+ return jukeboxStatus.getPositionSeconds() + secondsSinceLastUpdate;
+ }
+
+ return jukeboxStatus.getPositionSeconds();
+ }
+
+ public void setEnabled(boolean enabled) {
+ tasks.clear();
+ if (enabled) {
+ updatePlaylist();
+ }
+ stop();
+ downloadService.setPlayerState(PlayerState.IDLE);
+ }
+
+ private static class TaskQueue {
+
+ private final LinkedBlockingQueue<JukeboxTask> queue = new LinkedBlockingQueue<JukeboxTask>();
+
+ void add(JukeboxTask jukeboxTask) {
+ queue.add(jukeboxTask);
+ }
+
+ JukeboxTask take() throws InterruptedException {
+ return queue.take();
+ }
+
+ void remove(Class<? extends JukeboxTask> clazz) {
+ try {
+ Iterator<JukeboxTask> iterator = queue.iterator();
+ while (iterator.hasNext()) {
+ JukeboxTask task = iterator.next();
+ if (clazz.equals(task.getClass())) {
+ iterator.remove();
+ }
+ }
+ } catch (Throwable x) {
+ Log.w(TAG, "Failed to clean-up task queue.", x);
+ }
+ }
+
+ void clear() {
+ queue.clear();
+ }
+ }
+
+ private abstract class JukeboxTask {
+
+ abstract JukeboxStatus execute() throws Exception;
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName();
+ }
+ }
+
+ private class GetStatus extends JukeboxTask {
+ @Override
+ JukeboxStatus execute() throws Exception {
+ return getMusicService().getJukeboxStatus(downloadService, null);
+ }
+ }
+
+ private class SetPlaylist extends JukeboxTask {
+
+ private final List<String> ids;
+
+ SetPlaylist(List<String> ids) {
+ this.ids = ids;
+ }
+
+ @Override
+ JukeboxStatus execute() throws Exception {
+ return getMusicService().updateJukeboxPlaylist(ids, downloadService, null);
+ }
+ }
+
+ private class Skip extends JukeboxTask {
+ private final int index;
+ private final int offsetSeconds;
+
+ Skip(int index, int offsetSeconds) {
+ this.index = index;
+ this.offsetSeconds = offsetSeconds;
+ }
+
+ @Override
+ JukeboxStatus execute() throws Exception {
+ return getMusicService().skipJukebox(index, offsetSeconds, downloadService, null);
+ }
+ }
+
+ private class Stop extends JukeboxTask {
+ @Override
+ JukeboxStatus execute() throws Exception {
+ return getMusicService().stopJukebox(downloadService, null);
+ }
+ }
+
+ private class Start extends JukeboxTask {
+ @Override
+ JukeboxStatus execute() throws Exception {
+ return getMusicService().startJukebox(downloadService, null);
+ }
+ }
+
+ private class SetGain extends JukeboxTask {
+
+ private final float gain;
+
+ private SetGain(float gain) {
+ this.gain = gain;
+ }
+
+ @Override
+ JukeboxStatus execute() throws Exception {
+ return getMusicService().setJukeboxGain(gain, downloadService, null);
+ }
+ }
+
+ private static class VolumeToast extends Toast {
+
+ private final ProgressBar progressBar;
+
+ public VolumeToast(Context context) {
+ super(context);
+ setDuration(Toast.LENGTH_SHORT);
+ LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View view = inflater.inflate(R.layout.jukebox_volume, null);
+ progressBar = (ProgressBar) view.findViewById(R.id.jukebox_volume_progress_bar);
+
+ setView(view);
+ setGravity(Gravity.TOP, 0, 0);
+ }
+
+ public void setVolume(float volume) {
+ progressBar.setProgress(Math.round(100 * volume));
+ show();
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/service/MediaStoreService.java b/src/github/daneren2005/dsub/service/MediaStoreService.java
new file mode 100644
index 00000000..4de77bf2
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/MediaStoreService.java
@@ -0,0 +1,109 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.service;
+
+import java.io.File;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.MediaStore;
+import android.util.Log;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.util.FileUtil;
+
+/**
+ * @author Sindre Mehus
+ */
+public class MediaStoreService {
+
+ private static final String TAG = MediaStoreService.class.getSimpleName();
+ private static final Uri ALBUM_ART_URI = Uri.parse("content://media/external/audio/albumart");
+
+ private final Context context;
+
+ public MediaStoreService(Context context) {
+ this.context = context;
+ }
+
+ public void saveInMediaStore(DownloadFile downloadFile) {
+ MusicDirectory.Entry song = downloadFile.getSong();
+ File songFile = downloadFile.getCompleteFile();
+
+ // Delete existing row in case the song has been downloaded before.
+ deleteFromMediaStore(downloadFile);
+
+ ContentResolver contentResolver = context.getContentResolver();
+ ContentValues values = new ContentValues();
+ values.put(MediaStore.MediaColumns.TITLE, song.getTitle());
+ values.put(MediaStore.Audio.AudioColumns.ARTIST, song.getArtist());
+ values.put(MediaStore.Audio.AudioColumns.ALBUM, song.getAlbum());
+ values.put(MediaStore.Audio.AudioColumns.TRACK, song.getTrack());
+ values.put(MediaStore.Audio.AudioColumns.YEAR, song.getYear());
+ values.put(MediaStore.MediaColumns.DATA, songFile.getAbsolutePath());
+ values.put(MediaStore.MediaColumns.MIME_TYPE, song.getContentType());
+ values.put(MediaStore.Audio.AudioColumns.IS_MUSIC, 1);
+
+ Uri uri = contentResolver.insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, values);
+
+ // Look up album, and add cover art if found.
+ Cursor cursor = contentResolver.query(uri, new String[]{MediaStore.Audio.AudioColumns.ALBUM_ID}, null, null, null);
+ if (cursor.moveToFirst()) {
+ int albumId = cursor.getInt(0);
+ insertAlbumArt(albumId, downloadFile);
+ }
+ cursor.close();
+ }
+
+ public void deleteFromMediaStore(DownloadFile downloadFile) {
+ ContentResolver contentResolver = context.getContentResolver();
+ MusicDirectory.Entry song = downloadFile.getSong();
+ File file = downloadFile.getCompleteFile();
+
+ int n = contentResolver.delete(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+ MediaStore.Audio.AudioColumns.TITLE_KEY + "=? AND " +
+ MediaStore.MediaColumns.DATA + "=?",
+ new String[]{MediaStore.Audio.keyFor(song.getTitle()), file.getAbsolutePath()});
+ if (n > 0) {
+ Log.i(TAG, "Deleting media store row for " + song);
+ }
+ }
+
+ private void insertAlbumArt(int albumId, DownloadFile downloadFile) {
+ ContentResolver contentResolver = context.getContentResolver();
+
+ Cursor cursor = contentResolver.query(Uri.withAppendedPath(ALBUM_ART_URI, String.valueOf(albumId)), null, null, null, null);
+ if (!cursor.moveToFirst()) {
+
+ // No album art found, add it.
+ File albumArtFile = FileUtil.getAlbumArtFile(context, downloadFile.getSong());
+ if (albumArtFile.exists()) {
+ ContentValues values = new ContentValues();
+ values.put(MediaStore.Audio.AlbumColumns.ALBUM_ID, albumId);
+ values.put(MediaStore.MediaColumns.DATA, albumArtFile.getPath());
+ contentResolver.insert(ALBUM_ART_URI, values);
+ Log.i(TAG, "Added album art: " + albumArtFile);
+ }
+ }
+ cursor.close();
+ }
+
+} \ No newline at end of file
diff --git a/src/github/daneren2005/dsub/service/MusicService.java b/src/github/daneren2005/dsub/service/MusicService.java
new file mode 100644
index 00000000..7aa878ab
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/MusicService.java
@@ -0,0 +1,140 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.service;
+
+import java.util.List;
+
+import org.apache.http.HttpResponse;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import github.daneren2005.dsub.domain.ChatMessage;
+import github.daneren2005.dsub.domain.Genre;
+import github.daneren2005.dsub.domain.Indexes;
+import github.daneren2005.dsub.domain.JukeboxStatus;
+import github.daneren2005.dsub.domain.Lyrics;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.MusicFolder;
+import github.daneren2005.dsub.domain.Playlist;
+import github.daneren2005.dsub.domain.PodcastChannel;
+import github.daneren2005.dsub.domain.PodcastEpisode;
+import github.daneren2005.dsub.domain.SearchCritera;
+import github.daneren2005.dsub.domain.SearchResult;
+import github.daneren2005.dsub.domain.Share;
+import github.daneren2005.dsub.domain.Version;
+import github.daneren2005.dsub.util.CancellableTask;
+import github.daneren2005.dsub.util.ProgressListener;
+
+/**
+ * @author Sindre Mehus
+ */
+public interface MusicService {
+
+ void ping(Context context, ProgressListener progressListener) throws Exception;
+
+ boolean isLicenseValid(Context context, ProgressListener progressListener) throws Exception;
+
+ List<MusicFolder> getMusicFolders(boolean refresh, Context context, ProgressListener progressListener) throws Exception;
+
+ Indexes getIndexes(String musicFolderId, boolean refresh, Context context, ProgressListener progressListener) throws Exception;
+
+ MusicDirectory getMusicDirectory(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception;
+
+ SearchResult search(SearchCritera criteria, Context context, ProgressListener progressListener) throws Exception;
+
+ MusicDirectory getStarredList(Context context, ProgressListener progressListener) throws Exception;
+
+ MusicDirectory getPlaylist(String id, String name, Context context, ProgressListener progressListener) throws Exception;
+
+ List<Playlist> getPlaylists(boolean refresh, Context context, ProgressListener progressListener) throws Exception;
+
+ void createPlaylist(String id, String name, List<MusicDirectory.Entry> entries, Context context, ProgressListener progressListener) throws Exception;
+
+ void deletePlaylist(String id, Context context, ProgressListener progressListener) throws Exception;
+
+ void addToPlaylist(String id, List<MusicDirectory.Entry> toAdd, Context context, ProgressListener progressListener) throws Exception;
+
+ void removeFromPlaylist(String id, List<Integer> toRemove, Context context, ProgressListener progressListener) throws Exception;
+
+ void overwritePlaylist(String id, String name, int toRemove, List<MusicDirectory.Entry> toAdd, Context context, ProgressListener progressListener) throws Exception;
+
+ void updatePlaylist(String id, String name, String comment, boolean pub, Context context, ProgressListener progressListener) throws Exception;
+
+ Lyrics getLyrics(String artist, String title, Context context, ProgressListener progressListener) throws Exception;
+
+ void scrobble(String id, boolean submission, Context context, ProgressListener progressListener) throws Exception;
+
+ MusicDirectory getAlbumList(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception;
+
+ MusicDirectory getRandomSongs(int size, String folder, String genre, String startYear, String endYear, Context context, ProgressListener progressListener) throws Exception;
+
+ Bitmap getCoverArt(Context context, MusicDirectory.Entry entry, int size, int saveSize, ProgressListener progressListener) throws Exception;
+
+ HttpResponse getDownloadInputStream(Context context, MusicDirectory.Entry song, long offset, int maxBitrate, CancellableTask task) throws Exception;
+
+ Version getLocalVersion(Context context) throws Exception;
+
+ Version getLatestVersion(Context context, ProgressListener progressListener) throws Exception;
+
+ String getVideoUrl(int maxBitrate, Context context, String id);
+
+ String getVideoStreamUrl(String format, int Bitrate, Context context, String id) throws Exception;
+
+ String getHlsUrl(String id, int bitRate, Context context) throws Exception;
+
+ JukeboxStatus updateJukeboxPlaylist(List<String> ids, Context context, ProgressListener progressListener) throws Exception;
+
+ JukeboxStatus skipJukebox(int index, int offsetSeconds, Context context, ProgressListener progressListener) throws Exception;
+
+ JukeboxStatus stopJukebox(Context context, ProgressListener progressListener) throws Exception;
+
+ JukeboxStatus startJukebox(Context context, ProgressListener progressListener) throws Exception;
+
+ JukeboxStatus getJukeboxStatus(Context context, ProgressListener progressListener) throws Exception;
+
+ JukeboxStatus setJukeboxGain(float gain, Context context, ProgressListener progressListener) throws Exception;
+
+ void setStarred(String id, boolean starred, Context context, ProgressListener progressListener) throws Exception;
+
+ List<Share> getShares(Context context, ProgressListener progressListener) throws Exception;
+
+ List<ChatMessage> getChatMessages(Long since, Context context, ProgressListener progressListener) throws Exception;
+
+ void addChatMessage(String message, Context context, ProgressListener progressListener) throws Exception;
+
+ List<Genre> getGenres(boolean refresh, Context context, ProgressListener progressListener) throws Exception;
+
+ MusicDirectory getSongsByGenre(String genre, int count, int offset, Context context, ProgressListener progressListener) throws Exception;
+
+ List<PodcastChannel> getPodcastChannels(boolean refresh, Context context, ProgressListener progressListener) throws Exception;
+
+ MusicDirectory getPodcastEpisodes(String id, Context context, ProgressListener progressListener) throws Exception;
+
+ void refreshPodcasts(Context context, ProgressListener progressListener) throws Exception;
+
+ void createPodcastChannel(String url, Context context, ProgressListener progressListener) throws Exception;
+
+ void deletePodcastChannel(String id, Context context, ProgressListener progressListener) throws Exception;
+
+ void downloadPodcastEpisode(String id, Context context, ProgressListener progressListener) throws Exception;
+
+ void deletePodcastEpisode(String id, Context context, ProgressListener progressListener) throws Exception;
+
+ int processOfflineSyncs(final Context context, final ProgressListener progressListener) throws Exception;
+} \ No newline at end of file
diff --git a/src/github/daneren2005/dsub/service/MusicServiceFactory.java b/src/github/daneren2005/dsub/service/MusicServiceFactory.java
new file mode 100644
index 00000000..e04522ff
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/MusicServiceFactory.java
@@ -0,0 +1,36 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.service;
+
+import android.content.Context;
+import github.daneren2005.dsub.util.Util;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class MusicServiceFactory {
+
+ private static final MusicService REST_MUSIC_SERVICE = new CachedMusicService(new RESTMusicService());
+ private static final MusicService OFFLINE_MUSIC_SERVICE = new OfflineMusicService();
+
+ public static MusicService getMusicService(Context context) {
+ return Util.isOffline(context) ? OFFLINE_MUSIC_SERVICE : REST_MUSIC_SERVICE;
+ }
+}
diff --git a/src/github/daneren2005/dsub/service/OfflineException.java b/src/github/daneren2005/dsub/service/OfflineException.java
new file mode 100644
index 00000000..e3a8d460
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/OfflineException.java
@@ -0,0 +1,32 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.service;
+
+/**
+ * Thrown by service methods that are not available in offline mode.
+ *
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class OfflineException extends Exception {
+
+ public OfflineException(String message) {
+ super(message);
+ }
+}
diff --git a/src/github/daneren2005/dsub/service/OfflineMusicService.java b/src/github/daneren2005/dsub/service/OfflineMusicService.java
new file mode 100644
index 00000000..22fdcd9b
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/OfflineMusicService.java
@@ -0,0 +1,676 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.service;
+
+import java.io.File;
+import java.io.Reader;
+import java.io.FileReader;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Random;
+import java.util.Set;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.graphics.Bitmap;
+import android.media.MediaMetadataRetriever;
+import android.util.Log;
+import github.daneren2005.dsub.domain.Artist;
+import github.daneren2005.dsub.domain.Genre;
+import github.daneren2005.dsub.domain.Indexes;
+import github.daneren2005.dsub.domain.JukeboxStatus;
+import github.daneren2005.dsub.domain.Lyrics;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.MusicFolder;
+import github.daneren2005.dsub.domain.Playlist;
+import github.daneren2005.dsub.domain.PodcastChannel;
+import github.daneren2005.dsub.domain.PodcastEpisode;
+import github.daneren2005.dsub.domain.SearchCritera;
+import github.daneren2005.dsub.domain.SearchResult;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.ProgressListener;
+import github.daneren2005.dsub.util.Util;
+import java.io.*;
+import java.util.Comparator;
+import java.util.SortedSet;
+
+/**
+ * @author Sindre Mehus
+ */
+public class OfflineMusicService extends RESTMusicService {
+ private static final String TAG = OfflineMusicService.class.getSimpleName();
+
+ @Override
+ public boolean isLicenseValid(Context context, ProgressListener progressListener) throws Exception {
+ return true;
+ }
+
+ @Override
+ public Indexes getIndexes(String musicFolderId, boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ List<Artist> artists = new ArrayList<Artist>();
+ File root = FileUtil.getMusicDirectory(context);
+ for (File file : FileUtil.listFiles(root)) {
+ if (file.isDirectory()) {
+ Artist artist = new Artist();
+ artist.setId(file.getPath());
+ artist.setIndex(file.getName().substring(0, 1));
+ artist.setName(file.getName());
+ artists.add(artist);
+ }
+ }
+
+ SharedPreferences prefs = Util.getPreferences(context);
+ String ignoredArticlesString = prefs.getString(Constants.CACHE_KEY_IGNORE, "The El La Los Las Le Les");
+ final String[] ignoredArticles = ignoredArticlesString.split(" ");
+
+ Collections.sort(artists, new Comparator<Artist>() {
+ public int compare(Artist lhsArtist, Artist rhsArtist) {
+ String lhs = lhsArtist.getName().toLowerCase();
+ String rhs = rhsArtist.getName().toLowerCase();
+
+ char lhs1 = lhs.charAt(0);
+ char rhs1 = rhs.charAt(0);
+
+ if(Character.isDigit(lhs1) && !Character.isDigit(rhs1)) {
+ return 1;
+ } else if(Character.isDigit(rhs1) && !Character.isDigit(lhs1)) {
+ return -1;
+ }
+
+ for(String article: ignoredArticles) {
+ int index = lhs.indexOf(article.toLowerCase() + " ");
+ if(index == 0) {
+ lhs = lhs.substring(article.length() + 1);
+ }
+ index = rhs.indexOf(article.toLowerCase() + " ");
+ if(index == 0) {
+ rhs = rhs.substring(article.length() + 1);
+ }
+ }
+
+ return lhs.compareTo(rhs);
+ }
+ });
+
+ return new Indexes(0L, Collections.<Artist>emptyList(), artists);
+ }
+
+ @Override
+ public MusicDirectory getMusicDirectory(String id, String artistName, boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ File dir = new File(id);
+ MusicDirectory result = new MusicDirectory();
+ result.setName(dir.getName());
+
+ Set<String> names = new HashSet<String>();
+
+ for (File file : FileUtil.listMediaFiles(dir)) {
+ String name = getName(file);
+ if (name != null & !names.contains(name)) {
+ names.add(name);
+ result.addChild(createEntry(context, file, name));
+ }
+ }
+ result.sortChildren();
+ return result;
+ }
+
+ private String getName(File file) {
+ String name = file.getName();
+ if (file.isDirectory()) {
+ return name;
+ }
+
+ if (name.endsWith(".partial") || name.contains(".partial.") || name.equals(Constants.ALBUM_ART_FILE)) {
+ return null;
+ }
+
+ name = name.replace(".complete", "");
+ return FileUtil.getBaseName(name);
+ }
+
+ private MusicDirectory.Entry createEntry(Context context, File file, String name) {
+ MusicDirectory.Entry entry = new MusicDirectory.Entry();
+ entry.setDirectory(file.isDirectory());
+ entry.setId(file.getPath());
+ entry.setParent(file.getParent());
+ entry.setSize(file.length());
+ String root = FileUtil.getMusicDirectory(context).getPath();
+ if(!file.getParentFile().getParentFile().getPath().equals(root)) {
+ entry.setGrandParent(file.getParentFile().getParent());
+ }
+ entry.setPath(file.getPath().replaceFirst("^" + root + "/" , ""));
+ String title = name;
+ if (file.isFile()) {
+ File artistFolder = file.getParentFile().getParentFile();
+ File albumFolder = file.getParentFile();
+ if(artistFolder.getPath().equals(root)) {
+ entry.setArtist(albumFolder.getName());
+ } else {
+ entry.setArtist(artistFolder.getName());
+ }
+ entry.setAlbum(albumFolder.getName());
+
+ int index = name.indexOf('-');
+ if(index != -1) {
+ try {
+ entry.setTrack(Integer.parseInt(name.substring(0, index)));
+ title = title.substring(index + 1);
+ } catch(Exception e) {
+ // Failed parseInt, just means track filled out
+ }
+ }
+
+ try {
+ MediaMetadataRetriever metadata = new MediaMetadataRetriever();
+ metadata.setDataSource(file.getAbsolutePath());
+ String discNumber = metadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DISC_NUMBER);
+ if(discNumber == null) {
+ discNumber = "1/1";
+ }
+ int slashIndex = discNumber.indexOf("/");
+ if(slashIndex > 0) {
+ discNumber = discNumber.substring(0, slashIndex);
+ }
+ entry.setDiscNumber(Integer.parseInt(discNumber));
+ String bitrate = metadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE);
+ entry.setBitRate(Integer.parseInt((bitrate != null) ? bitrate : "0") / 1000);
+ String length = metadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
+ entry.setDuration(Integer.parseInt(length) / 1000);
+ String artist = metadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST);
+ if(artist != null) {
+ entry.setArtist(artist);
+ }
+ String album = metadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM);
+ if(album != null) {
+ entry.setAlbum(album);
+ }
+ metadata.release();
+ } catch(Exception e) {
+ Log.i(TAG, "Device doesn't properly support MediaMetadataRetreiver");
+ }
+ }
+
+ entry.setTitle(title);
+ entry.setSuffix(FileUtil.getExtension(file.getName().replace(".complete", "")));
+
+ File albumArt = FileUtil.getAlbumArtFile(context, entry);
+ if (albumArt.exists()) {
+ entry.setCoverArt(albumArt.getPath());
+ }
+ if(FileUtil.isVideoFile(file)) {
+ entry.setVideo(true);
+ }
+ return entry;
+ }
+
+ @Override
+ public Bitmap getCoverArt(Context context, MusicDirectory.Entry entry, int size, int saveSize, ProgressListener progressListener) throws Exception {
+ try {
+ return FileUtil.getAlbumArtBitmap(context, entry, size);
+ } catch(Exception e) {
+ return null;
+ }
+ }
+
+ @Override
+ public List<MusicFolder> getMusicFolders(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException("Music folders not available in offline mode");
+ }
+
+ @Override
+ public SearchResult search(SearchCritera criteria, Context context, ProgressListener progressListener) throws Exception {
+ List<Artist> artists = new ArrayList<Artist>();
+ List<MusicDirectory.Entry> albums = new ArrayList<MusicDirectory.Entry>();
+ List<MusicDirectory.Entry> songs = new ArrayList<MusicDirectory.Entry>();
+ File root = FileUtil.getMusicDirectory(context);
+ int closeness = 0;
+ for (File artistFile : FileUtil.listFiles(root)) {
+ String artistName = artistFile.getName();
+ if (artistFile.isDirectory()) {
+ if((closeness = matchCriteria(criteria, artistName)) > 0) {
+ Artist artist = new Artist();
+ artist.setId(artistFile.getPath());
+ artist.setIndex(artistFile.getName().substring(0, 1));
+ artist.setName(artistName);
+ artist.setCloseness(closeness);
+ artists.add(artist);
+ }
+
+ recursiveAlbumSearch(artistName, artistFile, criteria, context, albums, songs);
+ }
+ }
+
+ Collections.sort(artists, new Comparator<Artist>() {
+ public int compare(Artist lhs, Artist rhs) {
+ if(lhs.getCloseness() == rhs.getCloseness()) {
+ return 0;
+ }
+ else if(lhs.getCloseness() > rhs.getCloseness()) {
+ return -1;
+ }
+ else {
+ return 1;
+ }
+ }
+ });
+ Collections.sort(albums, new Comparator<MusicDirectory.Entry>() {
+ public int compare(MusicDirectory.Entry lhs, MusicDirectory.Entry rhs) {
+ if(lhs.getCloseness() == rhs.getCloseness()) {
+ return 0;
+ }
+ else if(lhs.getCloseness() > rhs.getCloseness()) {
+ return -1;
+ }
+ else {
+ return 1;
+ }
+ }
+ });
+ Collections.sort(songs, new Comparator<MusicDirectory.Entry>() {
+ public int compare(MusicDirectory.Entry lhs, MusicDirectory.Entry rhs) {
+ if(lhs.getCloseness() == rhs.getCloseness()) {
+ return 0;
+ }
+ else if(lhs.getCloseness() > rhs.getCloseness()) {
+ return -1;
+ }
+ else {
+ return 1;
+ }
+ }
+ });
+
+ return new SearchResult(artists, albums, songs);
+ }
+
+ private void recursiveAlbumSearch(String artistName, File file, SearchCritera criteria, Context context, List<MusicDirectory.Entry> albums, List<MusicDirectory.Entry> songs) {
+ int closeness;
+ for(File albumFile : FileUtil.listMediaFiles(file)) {
+ if(albumFile.isDirectory()) {
+ String albumName = getName(albumFile);
+ if((closeness = matchCriteria(criteria, albumName)) > 0) {
+ MusicDirectory.Entry album = createEntry(context, albumFile, albumName);
+ album.setArtist(artistName);
+ album.setCloseness(closeness);
+ albums.add(album);
+ }
+
+ for(File songFile : FileUtil.listMediaFiles(albumFile)) {
+ String songName = getName(songFile);
+ if(songFile.isDirectory()) {
+ recursiveAlbumSearch(artistName, songFile, criteria, context, albums, songs);
+ }
+ else if((closeness = matchCriteria(criteria, songName)) > 0){
+ MusicDirectory.Entry song = createEntry(context, albumFile, songName);
+ song.setArtist(artistName);
+ song.setAlbum(albumName);
+ song.setCloseness(closeness);
+ songs.add(song);
+ }
+ }
+ }
+ else {
+ String songName = getName(albumFile);
+ if((closeness = matchCriteria(criteria, songName)) > 0) {
+ MusicDirectory.Entry song = createEntry(context, albumFile, songName);
+ song.setArtist(artistName);
+ song.setAlbum(songName);
+ song.setCloseness(closeness);
+ songs.add(song);
+ }
+ }
+ }
+ }
+ private int matchCriteria(SearchCritera criteria, String name) {
+ String query = criteria.getQuery().toLowerCase();
+ String[] queryParts = query.split(" ");
+ String[] nameParts = name.toLowerCase().split(" ");
+
+ int closeness = 0;
+ for(String queryPart : queryParts) {
+ for(String namePart : nameParts) {
+ if(namePart.equals(queryPart)) {
+ closeness++;
+ }
+ }
+ }
+
+ return closeness;
+ }
+
+ @Override
+ public List<Playlist> getPlaylists(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ List<Playlist> playlists = new ArrayList<Playlist>();
+ File root = FileUtil.getPlaylistDirectory();
+ String lastServer = null;
+ boolean removeServer = true;
+ for (File folder : FileUtil.listFiles(root)) {
+ if(folder.isDirectory()) {
+ String server = folder.getName();
+ SortedSet<File> fileList = FileUtil.listFiles(folder);
+ for(File file: fileList) {
+ if(FileUtil.isPlaylistFile(file)) {
+ String id = file.getName();
+ String filename = server + ": " + FileUtil.getBaseName(id);
+ Playlist playlist = new Playlist(server, filename);
+ playlists.add(playlist);
+ }
+ }
+
+ if(!server.equals(lastServer) && fileList.size() > 0) {
+ if(lastServer != null) {
+ removeServer = false;
+ }
+ lastServer = server;
+ }
+ } else {
+ // Delete legacy playlist files
+ try {
+ folder.delete();
+ } catch(Exception e) {
+ Log.w(TAG, "Failed to delete old playlist file: " + folder.getName());
+ }
+ }
+ }
+
+ if(removeServer) {
+ for(Playlist playlist: playlists) {
+ playlist.setName(playlist.getName().substring(playlist.getId().length() + 2));
+ }
+ }
+ return playlists;
+ }
+
+ @Override
+ public MusicDirectory getPlaylist(String id, String name, Context context, ProgressListener progressListener) throws Exception {
+ DownloadService downloadService = DownloadServiceImpl.getInstance();
+ if (downloadService == null) {
+ return new MusicDirectory();
+ }
+
+ Reader reader = null;
+ BufferedReader buffer = null;
+ try {
+ int firstIndex = name.indexOf(id);
+ if(firstIndex != -1) {
+ name = name.substring(id.length() + 2);
+ }
+
+ File playlistFile = FileUtil.getPlaylistFile(id, name);
+ reader = new FileReader(playlistFile);
+ buffer = new BufferedReader(reader);
+
+ MusicDirectory playlist = new MusicDirectory();
+ String line = buffer.readLine();
+ if(!"#EXTM3U".equals(line)) return playlist;
+
+ while( (line = buffer.readLine()) != null ){
+ File entryFile = new File(line);
+ String entryName = getName(entryFile);
+ if(entryFile.exists() && entryName != null){
+ playlist.addChild(createEntry(context, entryFile, entryName));
+ }
+ }
+
+ return playlist;
+ } finally {
+ Util.close(buffer);
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public void createPlaylist(String id, String name, List<MusicDirectory.Entry> entries, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException("Playlists not available in offline mode");
+ }
+
+ @Override
+ public void deletePlaylist(String id, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException("Playlists not available in offline mode");
+ }
+
+ @Override
+ public void addToPlaylist(String id, List<MusicDirectory.Entry> toAdd, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException("Updating playlist not available in offline mode");
+ }
+
+ @Override
+ public void removeFromPlaylist(String id, List<Integer> toRemove, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException("Removing from playlist not available in offline mode");
+ }
+
+ @Override
+ public void overwritePlaylist(String id, String name, int toRemove, List<MusicDirectory.Entry> toAdd, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException("Overwriting playlist not available in offline mode");
+ }
+
+ @Override
+ public void updatePlaylist(String id, String name, String comment, boolean pub, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException("Updating playlist not available in offline mode");
+ }
+
+ @Override
+ public Lyrics getLyrics(String artist, String title, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException("Lyrics not available in offline mode");
+ }
+
+ @Override
+ public void scrobble(String id, boolean submission, Context context, ProgressListener progressListener) throws Exception {
+ if(!submission) {
+ return;
+ }
+
+ SharedPreferences prefs = Util.getPreferences(context);
+ String cacheLocn = prefs.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, null);
+
+ SharedPreferences offline = Util.getOfflineSync(context);
+ int scrobbles = offline.getInt(Constants.OFFLINE_SCROBBLE_COUNT, 0);
+ scrobbles++;
+ SharedPreferences.Editor offlineEditor = offline.edit();
+
+ if(id.indexOf(cacheLocn) != -1) {
+ String scrobbleSearchCriteria = Util.parseOfflineIDSearch(context, id, cacheLocn);
+ offlineEditor.putString(Constants.OFFLINE_SCROBBLE_SEARCH + scrobbles, scrobbleSearchCriteria);
+ offlineEditor.remove(Constants.OFFLINE_SCROBBLE_ID + scrobbles);
+ } else {
+ offlineEditor.putString(Constants.OFFLINE_SCROBBLE_ID + scrobbles, id);
+ offlineEditor.remove(Constants.OFFLINE_SCROBBLE_SEARCH + scrobbles);
+ }
+
+ offlineEditor.putLong(Constants.OFFLINE_SCROBBLE_TIME + scrobbles, System.currentTimeMillis());
+ offlineEditor.putInt(Constants.OFFLINE_SCROBBLE_COUNT, scrobbles);
+ offlineEditor.commit();
+ }
+
+ @Override
+ public MusicDirectory getAlbumList(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException("Album lists not available in offline mode");
+ }
+
+ @Override
+ public String getVideoUrl(int maxBitrate, Context context, String id) {
+ return null;
+ }
+
+ @Override
+ public String getVideoStreamUrl(String format, int maxBitrate, Context context, String id) throws Exception {
+ return null;
+ }
+
+ @Override
+ public String getHlsUrl(String id, int bitRate, Context context) throws Exception {
+ return null;
+ }
+
+ @Override
+ public JukeboxStatus updateJukeboxPlaylist(List<String> ids, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException("Jukebox not available in offline mode");
+ }
+
+ @Override
+ public JukeboxStatus skipJukebox(int index, int offsetSeconds, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException("Jukebox not available in offline mode");
+ }
+
+ @Override
+ public JukeboxStatus stopJukebox(Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException("Jukebox not available in offline mode");
+ }
+
+ @Override
+ public JukeboxStatus startJukebox(Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException("Jukebox not available in offline mode");
+ }
+
+ @Override
+ public JukeboxStatus getJukeboxStatus(Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException("Jukebox not available in offline mode");
+ }
+
+ @Override
+ public JukeboxStatus setJukeboxGain(float gain, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException("Jukebox not available in offline mode");
+ }
+
+ @Override
+ public void setStarred(String id, boolean starred, Context context, ProgressListener progressListener) throws Exception {
+ SharedPreferences prefs = Util.getPreferences(context);
+ String cacheLocn = prefs.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, null);
+
+ SharedPreferences offline = Util.getOfflineSync(context);
+ int stars = offline.getInt(Constants.OFFLINE_STAR_COUNT, 0);
+ stars++;
+ SharedPreferences.Editor offlineEditor = offline.edit();
+
+ if(id.indexOf(cacheLocn) != -1) {
+ String searchCriteria = Util.parseOfflineIDSearch(context, id, cacheLocn);
+ offlineEditor.putString(Constants.OFFLINE_STAR_SEARCH + stars, searchCriteria);
+ offlineEditor.remove(Constants.OFFLINE_STAR_ID + stars);
+ } else {
+ offlineEditor.putString(Constants.OFFLINE_STAR_ID + stars, id);
+ offlineEditor.remove(Constants.OFFLINE_STAR_SEARCH + stars);
+ }
+
+ offlineEditor.putBoolean(Constants.OFFLINE_STAR_SETTING + stars, starred);
+ offlineEditor.putInt(Constants.OFFLINE_STAR_COUNT, stars);
+ offlineEditor.commit();
+ }
+
+ @Override
+ public List<Genre> getGenres(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException("Getting Genres not available in offline mode");
+ }
+
+ @Override
+ public MusicDirectory getSongsByGenre(String genre, int count, int offset, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException("Getting Songs By Genre not available in offline mode");
+ }
+
+ @Override
+ public MusicDirectory getRandomSongs(int size, String folder, String genre, String startYear, String endYear, Context context, ProgressListener progressListener) throws Exception {
+ File root = FileUtil.getMusicDirectory(context);
+ List<File> children = new LinkedList<File>();
+ listFilesRecursively(root, children);
+ MusicDirectory result = new MusicDirectory();
+
+ if (children.isEmpty()) {
+ return result;
+ }
+ Random random = new Random();
+ for (int i = 0; i < size; i++) {
+ File file = children.get(random.nextInt(children.size()));
+ result.addChild(createEntry(context, file, getName(file)));
+ }
+
+ return result;
+ }
+
+ @Override
+ public List<PodcastChannel> getPodcastChannels(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ List<PodcastChannel> channels = new ArrayList<PodcastChannel>();
+
+ File dir = FileUtil.getPodcastDirectory(context);
+ String line;
+ for(File file: dir.listFiles()) {
+ BufferedReader br = new BufferedReader(new FileReader(file));
+ while ((line = br.readLine()) != null && !"".equals(line)) {
+ PodcastChannel channel = new PodcastChannel();
+ channel.setId(line);
+ channel.setName(line);
+ channel.setStatus("completed");
+
+ if(FileUtil.getPodcastDirectory(context, channel).exists()) {
+ channels.add(channel);
+ }
+ }
+ br.close();
+ }
+
+ return channels;
+ }
+
+ @Override
+ public MusicDirectory getPodcastEpisodes(String id, Context context, ProgressListener progressListener) throws Exception {
+ return getMusicDirectory(FileUtil.getPodcastDirectory(context, id).getPath(), null, false, context, progressListener);
+ }
+
+ @Override
+ public void refreshPodcasts(Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException("Getting Podcasts not available in offline mode");
+ }
+
+ @Override
+ public void createPodcastChannel(String url, Context context, ProgressListener progressListener) throws Exception{
+ throw new OfflineException("Getting Podcasts not available in offline mode");
+ }
+
+ @Override
+ public void deletePodcastChannel(String id, Context context, ProgressListener progressListener) throws Exception{
+ throw new OfflineException("Getting Podcasts not available in offline mode");
+ }
+
+ @Override
+ public void downloadPodcastEpisode(String id, Context context, ProgressListener progressListener) throws Exception{
+ throw new OfflineException("Getting Podcasts not available in offline mode");
+ }
+
+ @Override
+ public void deletePodcastEpisode(String id, Context context, ProgressListener progressListener) throws Exception{
+ throw new OfflineException("Getting Podcasts not available in offline mode");
+ }
+
+ @Override
+ public int processOfflineSyncs(final Context context, final ProgressListener progressListener) throws Exception{
+ throw new OfflineException("Offline scrobble cached can not be processes while in offline mode");
+ }
+
+ private void listFilesRecursively(File parent, List<File> children) {
+ for (File file : FileUtil.listMediaFiles(parent)) {
+ if (file.isFile()) {
+ children.add(file);
+ } else {
+ listFilesRecursively(file, children);
+ }
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/service/RESTMusicService.java b/src/github/daneren2005/dsub/service/RESTMusicService.java
new file mode 100644
index 00000000..e81c2b95
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/RESTMusicService.java
@@ -0,0 +1,1296 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.service;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.FileReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpHost;
+import org.apache.http.HttpResponse;
+import org.apache.http.NameValuePair;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.conn.params.ConnManagerParams;
+import org.apache.http.conn.params.ConnPerRouteBean;
+import org.apache.http.conn.scheme.PlainSocketFactory;
+import org.apache.http.conn.scheme.Scheme;
+import org.apache.http.conn.scheme.SchemeRegistry;
+import org.apache.http.conn.scheme.SocketFactory;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
+import org.apache.http.message.BasicHeader;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.params.BasicHttpParams;
+import org.apache.http.params.HttpConnectionParams;
+import org.apache.http.params.HttpParams;
+import org.apache.http.protocol.BasicHttpContext;
+import org.apache.http.protocol.ExecutionContext;
+import org.apache.http.protocol.HttpContext;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.pm.PackageInfo;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.util.Log;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.*;
+import github.daneren2005.dsub.domain.MusicDirectory.Entry;
+import github.daneren2005.dsub.service.parser.AlbumListParser;
+import github.daneren2005.dsub.service.parser.ChatMessageParser;
+import github.daneren2005.dsub.service.parser.ErrorParser;
+import github.daneren2005.dsub.service.parser.GenreParser;
+import github.daneren2005.dsub.service.parser.IndexesParser;
+import github.daneren2005.dsub.service.parser.JukeboxStatusParser;
+import github.daneren2005.dsub.service.parser.LicenseParser;
+import github.daneren2005.dsub.service.parser.LyricsParser;
+import github.daneren2005.dsub.service.parser.MusicDirectoryParser;
+import github.daneren2005.dsub.service.parser.MusicFoldersParser;
+import github.daneren2005.dsub.service.parser.PlaylistParser;
+import github.daneren2005.dsub.service.parser.PlaylistsParser;
+import github.daneren2005.dsub.service.parser.PodcastChannelParser;
+import github.daneren2005.dsub.service.parser.PodcastEntryParser;
+import github.daneren2005.dsub.service.parser.RandomSongsParser;
+import github.daneren2005.dsub.service.parser.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.VersionParser;
+import github.daneren2005.dsub.service.ssl.SSLSocketFactory;
+import github.daneren2005.dsub.service.ssl.TrustSelfSignedStrategy;
+import github.daneren2005.dsub.util.CancellableTask;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.ProgressListener;
+import github.daneren2005.dsub.util.Util;
+import java.io.*;
+
+/**
+ * @author Sindre Mehus
+ */
+public class RESTMusicService implements MusicService {
+
+ private static final String TAG = RESTMusicService.class.getSimpleName();
+
+ private static final int SOCKET_CONNECT_TIMEOUT = 10 * 1000;
+ private static final int SOCKET_READ_TIMEOUT_DEFAULT = 10 * 1000;
+ private static final int SOCKET_READ_TIMEOUT_DOWNLOAD = 30 * 1000;
+ private static final int SOCKET_READ_TIMEOUT_GET_RANDOM_SONGS = 60 * 1000;
+ private static final int SOCKET_READ_TIMEOUT_GET_PLAYLIST = 60 * 1000;
+
+ // Allow 20 seconds extra timeout per MB offset.
+ private static final double TIMEOUT_MILLIS_PER_OFFSET_BYTE = 20000.0 / 1000000.0;
+
+ /**
+ * URL from which to fetch latest versions.
+ */
+ private static final String VERSION_URL = "http://subsonic.org/backend/version.view";
+
+ private static final int HTTP_REQUEST_MAX_ATTEMPTS = 5;
+ private static final long REDIRECTION_CHECK_INTERVAL_MILLIS = 60L * 60L * 1000L;
+
+ private final DefaultHttpClient httpClient;
+ private long redirectionLastChecked;
+ private int redirectionNetworkType = -1;
+ private String redirectFrom;
+ private String redirectTo;
+ private final ThreadSafeClientConnManager connManager;
+
+ public RESTMusicService() {
+
+ // Create and initialize default HTTP parameters
+ HttpParams params = new BasicHttpParams();
+ ConnManagerParams.setMaxTotalConnections(params, 20);
+ ConnManagerParams.setMaxConnectionsPerRoute(params, new ConnPerRouteBean(20));
+ HttpConnectionParams.setConnectionTimeout(params, SOCKET_CONNECT_TIMEOUT);
+ HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_DEFAULT);
+
+ // Turn off stale checking. Our connections break all the time anyway,
+ // and it's not worth it to pay the penalty of checking every time.
+ HttpConnectionParams.setStaleCheckingEnabled(params, false);
+
+ // Create and initialize scheme registry
+ SchemeRegistry schemeRegistry = new SchemeRegistry();
+ schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
+ schemeRegistry.register(new Scheme("https", createSSLSocketFactory(), 443));
+
+ // Create an HttpClient with the ThreadSafeClientConnManager.
+ // This connection manager must be used if more than one thread will
+ // be using the HttpClient.
+ connManager = new ThreadSafeClientConnManager(params, schemeRegistry);
+ httpClient = new DefaultHttpClient(connManager, params);
+ }
+
+ private SocketFactory createSSLSocketFactory() {
+ try {
+ return new SSLSocketFactory(new TrustSelfSignedStrategy(), SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
+ } catch (Throwable x) {
+ Log.e(TAG, "Failed to create custom SSL socket factory, using default.", x);
+ return org.apache.http.conn.ssl.SSLSocketFactory.getSocketFactory();
+ }
+ }
+
+ @Override
+ public void ping(Context context, ProgressListener progressListener) throws Exception {
+ Reader reader = getReader(context, progressListener, "ping", null);
+ try {
+ new ErrorParser(context).parse(reader);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public boolean isLicenseValid(Context context, ProgressListener progressListener) throws Exception {
+
+ Reader reader = getReader(context, progressListener, "getLicense", null);
+ try {
+ ServerInfo serverInfo = new LicenseParser(context).parse(reader);
+ return serverInfo.isLicenseValid();
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ public List<MusicFolder> getMusicFolders(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+
+ List<MusicFolder> cachedMusicFolders = readCachedMusicFolders(context);
+ if (cachedMusicFolders != null && !refresh) {
+ return cachedMusicFolders;
+ }
+
+ Reader reader = getReader(context, progressListener, "getMusicFolders", null);
+ try {
+ List<MusicFolder> musicFolders = new MusicFoldersParser(context).parse(reader, progressListener);
+ writeCachedMusicFolders(context, musicFolders);
+ return musicFolders;
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public Indexes getIndexes(String musicFolderId, boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ Indexes cachedIndexes = readCachedIndexes(context, musicFolderId);
+ if (cachedIndexes != null && !refresh) {
+ return cachedIndexes;
+ }
+
+ long lastModified = (cachedIndexes == null || refresh) ? 0L : cachedIndexes.getLastModified();
+
+ List<String> parameterNames = new ArrayList<String>();
+ List<Object> parameterValues = new ArrayList<Object>();
+
+ if(lastModified != 0L) {
+ parameterNames.add("ifModifiedSince");
+ parameterValues.add(lastModified);
+ }
+
+ if (musicFolderId != null) {
+ parameterNames.add("musicFolderId");
+ parameterValues.add(musicFolderId);
+ }
+
+ Reader reader = getReader(context, progressListener, "getIndexes", null, parameterNames, parameterValues);
+ try {
+ Indexes indexes = new IndexesParser(context).parse(reader, progressListener);
+ if (indexes != null) {
+ writeCachedIndexes(context, indexes, musicFolderId);
+ return indexes;
+ }
+ if(cachedIndexes != null) {
+ return cachedIndexes;
+ } else {
+ return new Indexes(0, new ArrayList<Artist>(), new ArrayList<Artist>());
+ }
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ private Indexes readCachedIndexes(Context context, String musicFolderId) {
+ String filename = getCachedIndexesFilename(context, musicFolderId);
+ return FileUtil.deserialize(context, filename);
+ }
+
+ private void writeCachedIndexes(Context context, Indexes indexes, String musicFolderId) {
+ String filename = getCachedIndexesFilename(context, musicFolderId);
+ FileUtil.serialize(context, indexes, filename);
+ }
+
+ private String getCachedIndexesFilename(Context context, String musicFolderId) {
+ String s = Util.getRestUrl(context, null) + musicFolderId;
+ return "indexes-" + Math.abs(s.hashCode()) + ".ser";
+ }
+
+ private ArrayList<MusicFolder> readCachedMusicFolders(Context context) {
+ String filename = getCachedMusicFoldersFilename(context);
+ return FileUtil.deserialize(context, filename);
+ }
+
+ private void writeCachedMusicFolders(Context context, List<MusicFolder> musicFolders) {
+ String filename = getCachedMusicFoldersFilename(context);
+ FileUtil.serialize(context, new ArrayList<MusicFolder>(musicFolders), filename);
+ }
+
+ private String getCachedMusicFoldersFilename(Context context) {
+ String s = Util.getRestUrl(context, null);
+ return "musicFolders-" + Math.abs(s.hashCode()) + ".ser";
+ }
+
+ @Override
+ public MusicDirectory getMusicDirectory(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ SharedPreferences prefs = Util.getPreferences(context);
+ String cacheLocn = prefs.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, null);
+ if(id.indexOf(cacheLocn) != -1) {
+ String search = Util.parseOfflineIDSearch(context, id, cacheLocn);
+ SearchCritera critera = new SearchCritera(search, 1, 1, 0);
+ SearchResult result = searchNew(critera, context, progressListener);
+ if(result.getArtists().size() == 1) {
+ id = result.getArtists().get(0).getId();
+ } else if(result.getAlbums().size() == 1) {
+ id = result.getAlbums().get(0).getId();
+ }
+ }
+
+ Reader reader = getReader(context, progressListener, "getMusicDirectory", null, "id", id);
+ try {
+ return new MusicDirectoryParser(context).parse(name, reader, progressListener);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public SearchResult search(SearchCritera critera, Context context, ProgressListener progressListener) throws Exception {
+ try {
+ return searchNew(critera, context, progressListener);
+ } catch (ServerTooOldException x) {
+ // Ensure backward compatibility with REST 1.3.
+ return searchOld(critera, context, progressListener);
+ }
+ }
+
+ /**
+ * Search using the "search" REST method.
+ */
+ private SearchResult searchOld(SearchCritera critera, Context context, ProgressListener progressListener) throws Exception {
+ List<String> parameterNames = Arrays.asList("any", "songCount");
+ List<Object> parameterValues = Arrays.<Object>asList(critera.getQuery(), critera.getSongCount());
+ Reader reader = getReader(context, progressListener, "search", null, parameterNames, parameterValues);
+ try {
+ return new SearchResultParser(context).parse(reader, progressListener);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ /**
+ * Search using the "search2" REST method, available in 1.4.0 and later.
+ */
+ private SearchResult searchNew(SearchCritera critera, Context context, ProgressListener progressListener) throws Exception {
+ checkServerVersion(context, "1.4", null);
+
+ List<String> parameterNames = Arrays.asList("query", "artistCount", "albumCount", "songCount");
+ List<Object> parameterValues = Arrays.<Object>asList(critera.getQuery(), critera.getArtistCount(),
+ critera.getAlbumCount(), critera.getSongCount());
+ Reader reader = getReader(context, progressListener, "search2", null, parameterNames, parameterValues);
+ try {
+ return new SearchResult2Parser(context).parse(reader, progressListener);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public MusicDirectory getPlaylist(String id, String name, Context context, ProgressListener progressListener) throws Exception {
+ HttpParams params = new BasicHttpParams();
+ HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_GET_PLAYLIST);
+
+ Reader reader = getReader(context, progressListener, "getPlaylist", params, "id", id);
+ try {
+ MusicDirectory playlist = new PlaylistParser(context).parse(reader, progressListener);
+
+ File playlistFile = FileUtil.getPlaylistFile(Util.getServerName(context), name);
+ FileWriter fw = new FileWriter(playlistFile);
+ BufferedWriter bw = new BufferedWriter(fw);
+ try {
+ fw.write("#EXTM3U\n");
+ for (MusicDirectory.Entry e : playlist.getChildren()) {
+ String filePath = FileUtil.getSongFile(context, e).getAbsolutePath();
+ if(! new File(filePath).exists()){
+ String ext = FileUtil.getExtension(filePath);
+ String base = FileUtil.getBaseName(filePath);
+ filePath = base + ".complete." + ext;
+ }
+ fw.write(filePath + "\n");
+ }
+ } catch(Exception e) {
+ Log.w(TAG, "Failed to save playlist: " + name);
+ } finally {
+ bw.close();
+ fw.close();
+ }
+
+ return playlist;
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public List<Playlist> getPlaylists(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ Reader reader = getReader(context, progressListener, "getPlaylists", null);
+ try {
+ return new PlaylistsParser(context).parse(reader, progressListener);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public void createPlaylist(String id, String name, List<MusicDirectory.Entry> entries, Context context, ProgressListener progressListener) throws Exception {
+ List<String> parameterNames = new LinkedList<String>();
+ List<Object> parameterValues = new LinkedList<Object>();
+
+ if (id != null) {
+ parameterNames.add("playlistId");
+ parameterValues.add(id);
+ }
+ if (name != null) {
+ parameterNames.add("name");
+ parameterValues.add(name);
+ }
+ for (MusicDirectory.Entry entry : entries) {
+ parameterNames.add("songId");
+ parameterValues.add(getOfflineSongId(entry.getId(), context, progressListener));
+ }
+
+ Reader reader = getReader(context, progressListener, "createPlaylist", null, parameterNames, parameterValues);
+ try {
+ new ErrorParser(context).parse(reader);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public void deletePlaylist(String id, Context context, ProgressListener progressListener) throws Exception {
+ Reader reader = getReader(context, progressListener, "deletePlaylist", null, "id", id);
+ try {
+ new ErrorParser(context).parse(reader);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public void addToPlaylist(String id, List<MusicDirectory.Entry> toAdd, Context context, ProgressListener progressListener) throws Exception {
+ checkServerVersion(context, "1.8", "Updating playlists is not supported.");
+ List<String> names = new ArrayList<String>();
+ List<Object> values = new ArrayList<Object>();
+ names.add("playlistId");
+ values.add(id);
+ for(MusicDirectory.Entry song: toAdd) {
+ names.add("songIdToAdd");
+ values.add(getOfflineSongId(song.getId(), context, progressListener));
+ }
+ Reader reader = getReader(context, progressListener, "updatePlaylist", null, names, values);
+ try {
+ new ErrorParser(context).parse(reader);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public void removeFromPlaylist(String id, List<Integer> toRemove, Context context, ProgressListener progressListener) throws Exception {
+ checkServerVersion(context, "1.8", "Updating playlists is not supported.");
+ List<String> names = new ArrayList<String>();
+ List<Object> values = new ArrayList<Object>();
+ names.add("playlistId");
+ values.add(id);
+ for(Integer song: toRemove) {
+ names.add("songIndexToRemove");
+ values.add(song);
+ }
+ Reader reader = getReader(context, progressListener, "updatePlaylist", null, names, values);
+ try {
+ new ErrorParser(context).parse(reader);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public void overwritePlaylist(String id, String name, int toRemove, List<MusicDirectory.Entry> toAdd, Context context, ProgressListener progressListener) throws Exception {
+ checkServerVersion(context, "1.8", "Updating playlists is not supported.");
+ List<String> names = new ArrayList<String>();
+ List<Object> values = new ArrayList<Object>();
+ names.add("playlistId");
+ values.add(id);
+ names.add("name");
+ values.add(name);
+ for(MusicDirectory.Entry song: toAdd) {
+ names.add("songIdToAdd");
+ values.add(song.getId());
+ }
+ for(int i = 0; i < toRemove; i++) {
+ names.add("songIndexToRemove");
+ values.add(i);
+ }
+ Reader reader = getReader(context, progressListener, "updatePlaylist", null, names, values);
+ try {
+ new ErrorParser(context).parse(reader);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public void updatePlaylist(String id, String name, String comment, boolean pub, Context context, ProgressListener progressListener) throws Exception {
+ checkServerVersion(context, "1.8", "Updating playlists is not supported.");
+ Reader reader = getReader(context, progressListener, "updatePlaylist", null, Arrays.asList("playlistId", "name", "comment", "public"), Arrays.<Object>asList(id, name, comment, pub));
+ try {
+ new ErrorParser(context).parse(reader);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public Lyrics getLyrics(String artist, String title, Context context, ProgressListener progressListener) throws Exception {
+ Reader reader = getReader(context, progressListener, "getLyrics", null, Arrays.asList("artist", "title"), Arrays.<Object>asList(artist, title));
+ try {
+ return new LyricsParser(context).parse(reader, progressListener);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public void scrobble(String id, boolean submission, Context context, ProgressListener progressListener) throws Exception {
+ id = getOfflineSongId(id, context, progressListener);
+ scrobble(id, submission, 0, context, progressListener);
+ }
+
+ public void scrobble(String id, boolean submission, long time, Context context, ProgressListener progressListener) throws Exception {
+ checkServerVersion(context, "1.5", "Scrobbling not supported.");
+ Reader reader;
+ if(time > 0){
+ checkServerVersion(context, "1.8", "Scrobbling with a time not supported.");
+ reader = getReader(context, progressListener, "scrobble", null, Arrays.asList("id", "submission", "time"), Arrays.<Object>asList(id, submission, time));
+ }
+ else
+ reader = getReader(context, progressListener, "scrobble", null, Arrays.asList("id", "submission"), Arrays.<Object>asList(id, submission));
+ try {
+ new ErrorParser(context).parse(reader);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public MusicDirectory getAlbumList(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception {
+ Reader reader = getReader(context, progressListener, "getAlbumList",
+ null, Arrays.asList("type", "size", "offset"), Arrays.<Object>asList(type, size, offset));
+ try {
+ return new AlbumListParser(context).parse(reader, progressListener);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public MusicDirectory getStarredList(Context context, ProgressListener progressListener) throws Exception {
+ Reader reader = getReader(context, progressListener, "getStarred", null);
+ try {
+ return new StarredListParser(context).parse(reader, progressListener);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public MusicDirectory getRandomSongs(int size, String musicFolderId, String genre, String startYear, String endYear, Context context, ProgressListener progressListener) throws Exception {
+ HttpParams params = new BasicHttpParams();
+ HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_GET_RANDOM_SONGS);
+
+ List<String> names = new ArrayList<String>();
+ List<Object> values = new ArrayList<Object>();
+
+ names.add("size");
+ values.add(size);
+
+ if (musicFolderId != null && !"".equals(musicFolderId)) {
+ names.add("musicFolderId");
+ values.add(musicFolderId);
+ }
+ if(genre != null && !"".equals(genre)) {
+ names.add("genre");
+ values.add(genre);
+ }
+ if(startYear != null && !"".equals(startYear)) {
+ names.add("fromYear");
+ values.add(startYear);
+ }
+ if(endYear != null && !"".equals(endYear)) {
+ names.add("toYear");
+ values.add(endYear);
+ }
+
+ Reader reader = getReader(context, progressListener, "getRandomSongs", params, names, values);
+ try {
+ return new RandomSongsParser(context).parse(reader, progressListener);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public Version getLocalVersion(Context context) throws Exception {
+ PackageInfo packageInfo = context.getPackageManager().getPackageInfo("github.daneren2005.dsub", 0);
+ return new Version(packageInfo.versionName);
+ }
+
+ @Override
+ public Version getLatestVersion(Context context, ProgressListener progressListener) throws Exception {
+ Reader reader = getReaderForURL(context, VERSION_URL, null, null, null, progressListener);
+ try {
+ return new VersionParser().parse(reader);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ private void checkServerVersion(Context context, String version, String text) throws ServerTooOldException {
+ Version serverVersion = Util.getServerRestVersion(context);
+ Version requiredVersion = new Version(version);
+ boolean ok = serverVersion == null || serverVersion.compareTo(requiredVersion) >= 0;
+
+ if (!ok) {
+ throw new ServerTooOldException(text, serverVersion, requiredVersion);
+ }
+ }
+
+ @Override
+ public Bitmap getCoverArt(Context context, MusicDirectory.Entry entry, int size, int saveSize, ProgressListener progressListener) throws Exception {
+
+ // Synchronize on the entry so that we don't download concurrently for the same song.
+ synchronized (entry) {
+
+ // Use cached file, if existing.
+ Bitmap bitmap = FileUtil.getAlbumArtBitmap(context, entry, size);
+ if (bitmap != null) {
+ return bitmap;
+ }
+
+ String url = Util.getRestUrl(context, "getCoverArt");
+
+ InputStream in = null;
+ try {
+ List<String> parameterNames = Arrays.asList("id", "size");
+ List<Object> parameterValues = Arrays.<Object>asList(entry.getCoverArt(), saveSize);
+ HttpEntity entity = getEntityForURL(context, url, null, parameterNames, parameterValues, progressListener);
+ in = entity.getContent();
+
+ // If content type is XML, an error occured. Get it.
+ String contentType = Util.getContentType(entity);
+ if (contentType != null && contentType.startsWith("text/xml")) {
+ new ErrorParser(context).parse(new InputStreamReader(in, Constants.UTF_8));
+ return null; // Never reached.
+ }
+
+ byte[] bytes = Util.toByteArray(in);
+
+ File albumDir = FileUtil.getAlbumDirectory(context, entry);
+ if (albumDir.exists()) {
+ OutputStream out = null;
+ try {
+ out = new FileOutputStream(FileUtil.getAlbumArtFile(albumDir));
+ out.write(bytes);
+ } finally {
+ Util.close(out);
+ }
+ }
+
+ bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
+ if(size != saveSize) {
+ bitmap = Bitmap.createScaledBitmap(bitmap, size, size, true);
+ }
+ return bitmap;
+
+ } finally {
+ Util.close(in);
+ }
+ }
+ }
+
+ @Override
+ public HttpResponse getDownloadInputStream(Context context, MusicDirectory.Entry song, long offset, int maxBitrate, CancellableTask task) throws Exception {
+
+ String url = Util.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 = Arrays.asList("id", "maxBitRate");
+ List<Object> parameterValues = Arrays.<Object>asList(song.getId(), maxBitrate);
+ HttpResponse response = getResponseForURL(context, url, params, parameterNames, parameterValues, headers, null, task);
+
+ // If content type is XML, an error occurred. Get it.
+ String contentType = Util.getContentType(response.getEntity());
+ if (contentType != null && contentType.startsWith("text/xml")) {
+ InputStream in = response.getEntity().getContent();
+ try {
+ new ErrorParser(context).parse(new InputStreamReader(in, Constants.UTF_8));
+ } finally {
+ Util.close(in);
+ }
+ }
+
+ return response;
+ }
+
+ @Override
+ public String getVideoUrl(int maxBitrate, Context context, String id) {
+ StringBuilder builder = new StringBuilder(Util.getRestUrl(context, "videoPlayer"));
+ builder.append("&id=").append(id);
+ builder.append("&maxBitRate=").append(maxBitrate);
+ builder.append("&autoplay=true");
+
+ String url = rewriteUrlWithRedirect(context, builder.toString());
+ Log.i(TAG, "Using video URL: " + url);
+ return url;
+ }
+
+ @Override
+ public String getVideoStreamUrl(String format, int maxBitrate, Context context, String id) throws Exception {
+ StringBuilder builder = new StringBuilder(Util.getRestUrl(context, "stream"));
+ builder.append("&id=").append(id);
+ if(!"raw".equals(format)) {
+ checkServerVersion(context, "1.9", "Video streaming not supported.");
+ builder.append("&maxBitRate=").append(maxBitrate);
+ }
+ builder.append("&format=").append(format);
+
+ String url = rewriteUrlWithRedirect(context, builder.toString());
+ Log.i(TAG, "Using video URL: " + url);
+ return url;
+ }
+
+ @Override
+ public String getHlsUrl(String id, int bitRate, Context context) throws Exception {
+ checkServerVersion(context, "1.9", "HLS video streaming not supported.");
+
+ StringBuilder builder = new StringBuilder(Util.getRestUrl(context, "hls"));
+ builder.append("&id=").append(id);
+ if(bitRate > 0) {
+ builder.append("&bitRate=").append(bitRate);
+ }
+
+ String url = rewriteUrlWithRedirect(context, builder.toString());
+ Log.i(TAG, "Using hls URL: " + url);
+ return url;
+ }
+
+ @Override
+ public JukeboxStatus updateJukeboxPlaylist(List<String> ids, Context context, ProgressListener progressListener) throws Exception {
+ int n = ids.size();
+ List<String> parameterNames = new ArrayList<String>(n + 1);
+ parameterNames.add("action");
+ for (int i = 0; i < n; i++) {
+ parameterNames.add("id");
+ }
+ List<Object> parameterValues = new ArrayList<Object>();
+ parameterValues.add("set");
+ parameterValues.addAll(ids);
+
+ return executeJukeboxCommand(context, progressListener, parameterNames, parameterValues);
+ }
+
+ @Override
+ public JukeboxStatus skipJukebox(int index, int offsetSeconds, Context context, ProgressListener progressListener) throws Exception {
+ List<String> parameterNames = Arrays.asList("action", "index", "offset");
+ List<Object> parameterValues = Arrays.<Object>asList("skip", index, offsetSeconds);
+ return executeJukeboxCommand(context, progressListener, parameterNames, parameterValues);
+ }
+
+ @Override
+ public JukeboxStatus stopJukebox(Context context, ProgressListener progressListener) throws Exception {
+ return executeJukeboxCommand(context, progressListener, Arrays.asList("action"), Arrays.<Object>asList("stop"));
+ }
+
+ @Override
+ public JukeboxStatus startJukebox(Context context, ProgressListener progressListener) throws Exception {
+ return executeJukeboxCommand(context, progressListener, Arrays.asList("action"), Arrays.<Object>asList("start"));
+ }
+
+ @Override
+ public JukeboxStatus getJukeboxStatus(Context context, ProgressListener progressListener) throws Exception {
+ return executeJukeboxCommand(context, progressListener, Arrays.asList("action"), Arrays.<Object>asList("status"));
+ }
+
+ @Override
+ public JukeboxStatus setJukeboxGain(float gain, Context context, ProgressListener progressListener) throws Exception {
+ List<String> parameterNames = Arrays.asList("action", "gain");
+ List<Object> parameterValues = Arrays.<Object>asList("setGain", gain);
+ return executeJukeboxCommand(context, progressListener, parameterNames, parameterValues);
+
+ }
+
+ private JukeboxStatus executeJukeboxCommand(Context context, ProgressListener progressListener, List<String> parameterNames, List<Object> parameterValues) throws Exception {
+ checkServerVersion(context, "1.7", "Jukebox not supported.");
+ Reader reader = getReader(context, progressListener, "jukeboxControl", null, parameterNames, parameterValues);
+ try {
+ return new JukeboxStatusParser(context).parse(reader);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public void setStarred(String id, boolean starred, Context context, ProgressListener progressListener) throws Exception {
+ checkServerVersion(context, "1.8", "Starring is not supported.");
+ id = getOfflineSongId(id, context, progressListener);
+
+ Reader reader = getReader(context, progressListener, starred ? "star" : "unstar", null, "id", id);
+ try {
+ new ErrorParser(context).parse(reader);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public List<Share> getShares(Context context, ProgressListener progressListener) throws Exception {
+ checkServerVersion(context, "1.6", "Shares not supported.");
+
+ Reader reader = getReader(context, progressListener, "getShares", null);
+ try {
+ return new ShareParser(context).parse(reader, progressListener);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public List<ChatMessage> getChatMessages(Long since, Context context, ProgressListener progressListener) throws Exception {
+ checkServerVersion(context, "1.2", "Chat not supported.");
+
+ HttpParams params = new BasicHttpParams();
+ HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_GET_RANDOM_SONGS);
+
+ List<String> parameterNames = new ArrayList<String>();
+ List<Object> parameterValues = new ArrayList<Object>();
+
+ parameterNames.add("since");
+ parameterValues.add(since);
+
+ Reader reader = getReader(context, progressListener, "getChatMessages", params, parameterNames, parameterValues);
+
+ try {
+ return new ChatMessageParser(context).parse(reader, progressListener);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public void addChatMessage(String message, Context context, ProgressListener progressListener) throws Exception {
+ checkServerVersion(context, "1.2", "Chat not supported.");
+
+ HttpParams params = new BasicHttpParams();
+ HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_GET_RANDOM_SONGS);
+
+ List<String> parameterNames = new ArrayList<String>();
+ List<Object> parameterValues = new ArrayList<Object>();
+
+ parameterNames.add("message");
+ parameterValues.add(message);
+
+ Reader reader = getReader(context, progressListener, "addChatMessage", params, parameterNames, parameterValues);
+
+ try {
+ new ErrorParser(context).parse(reader);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public List<Genre> getGenres(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ checkServerVersion(context, "1.9", "Genres not supported.");
+
+ Reader reader = getReader(context, progressListener, "getGenres", null);
+ try {
+ return new GenreParser(context).parse(reader, progressListener);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public MusicDirectory getSongsByGenre(String genre, int count, int offset, Context context, ProgressListener progressListener) throws Exception {
+ checkServerVersion(context, "1.9", "Genres not supported.");
+
+ HttpParams params = new BasicHttpParams();
+ HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_GET_RANDOM_SONGS);
+
+ List<String> parameterNames = new ArrayList<String>();
+ List<Object> parameterValues = new ArrayList<Object>();
+
+ parameterNames.add("genre");
+ parameterValues.add(genre);
+ parameterNames.add("count");
+ parameterValues.add(count);
+ parameterNames.add("offset");
+ parameterValues.add(offset);
+
+ Reader reader = getReader(context, progressListener, "getSongsByGenre", params, parameterNames, parameterValues);
+
+ try {
+ return new RandomSongsParser(context).parse(reader, progressListener);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public List<PodcastChannel> getPodcastChannels(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ checkServerVersion(context, "1.9", "Podcasts not supported.");
+
+ Reader reader = getReader(context, progressListener, "getPodcasts", null, Arrays.asList("includeEpisodes"), Arrays.<Object>asList("false"));
+ try {
+ List<PodcastChannel> channels = new PodcastChannelParser(context).parse(reader, progressListener);
+
+ String content = "";
+ for(PodcastChannel channel: channels) {
+ content += channel.getName() + "\n";
+ }
+
+ File file = FileUtil.getPodcastFile(context, Util.getServerName(context));
+ BufferedWriter bw = new BufferedWriter(new FileWriter(file));
+ bw.write(content);
+ bw.close();
+
+ return channels;
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public MusicDirectory getPodcastEpisodes(String id, Context context, ProgressListener progressListener) throws Exception {
+ Reader reader = getReader(context, progressListener, "getPodcasts", null, Arrays.asList("id"), Arrays.<Object>asList(id));
+ try {
+ return new PodcastEntryParser(context).parse(id, reader, progressListener);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public void refreshPodcasts(Context context, ProgressListener progressListener) throws Exception {
+ checkServerVersion(context, "1.9", "Refresh podcasts not supported.");
+
+ Reader reader = getReader(context, progressListener, "refreshPodcasts", null);
+ try {
+ new ErrorParser(context).parse(reader);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public void createPodcastChannel(String url, Context context, ProgressListener progressListener) throws Exception{
+ checkServerVersion(context, "1.9", "Creating podcasts not supported.");
+
+ Reader reader = getReader(context, progressListener, "createPodcastChannel", null, "url", url);
+ try {
+ new ErrorParser(context).parse(reader);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public void deletePodcastChannel(String id, Context context, ProgressListener progressListener) throws Exception {
+ checkServerVersion(context, "1.9", "Deleting podcasts not supported.");
+
+ Reader reader = getReader(context, progressListener, "deletePodcastChannel", null, "id", id);
+ try {
+ new ErrorParser(context).parse(reader);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public void downloadPodcastEpisode(String id, Context context, ProgressListener progressListener) throws Exception{
+ checkServerVersion(context, "1.9", "Downloading podcasts not supported.");
+
+ Reader reader = getReader(context, progressListener, "downloadPodcastEpisode", null, "id", id);
+ try {
+ new ErrorParser(context).parse(reader);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public void deletePodcastEpisode(String id, Context context, ProgressListener progressListener) throws Exception{
+ checkServerVersion(context, "1.9", "Deleting podcasts not supported.");
+
+ Reader reader = getReader(context, progressListener, "deletePodcastEpisode", null, "id", id);
+ try {
+ new ErrorParser(context).parse(reader);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public int processOfflineSyncs(final Context context, final ProgressListener progressListener) throws Exception{
+ return processOfflineScrobbles(context, progressListener) + processOfflineStars(context, progressListener);
+ }
+
+ public int processOfflineScrobbles(final Context context, final ProgressListener progressListener) throws Exception {
+ SharedPreferences offline = Util.getOfflineSync(context);
+ SharedPreferences.Editor offlineEditor = offline.edit();
+ int count = offline.getInt(Constants.OFFLINE_SCROBBLE_COUNT, 0);
+ int retry = 0;
+ for(int i = 1; i <= count; i++) {
+ String id = offline.getString(Constants.OFFLINE_SCROBBLE_ID + i, null);
+ long time = offline.getLong(Constants.OFFLINE_SCROBBLE_TIME + i, 0);
+ if(id != null) {
+ scrobble(id, true, time, context, progressListener);
+ } else {
+ String search = offline.getString(Constants.OFFLINE_SCROBBLE_SEARCH + i, "");
+ try{
+ SearchCritera critera = new SearchCritera(search, 0, 0, 1);
+ SearchResult result = searchNew(critera, context, progressListener);
+ if(result.getSongs().size() == 1){
+ Log.i(TAG, "Query '" + search + "' returned song " + result.getSongs().get(0).getTitle() + " by " + result.getSongs().get(0).getArtist() + " with id " + result.getSongs().get(0).getId());
+ Log.i(TAG, "Scrobbling " + result.getSongs().get(0).getId() + " with time " + time);
+ scrobble(result.getSongs().get(0).getId(), true, time, context, progressListener);
+ }
+ else{
+ throw new Exception("Song not found on server");
+ }
+ }
+ catch(Exception e){
+ Log.e(TAG, e.toString());
+ retry++;
+ }
+ }
+ }
+
+ offlineEditor.putInt(Constants.OFFLINE_SCROBBLE_COUNT, 0);
+ offlineEditor.commit();
+
+ return count - retry;
+ }
+
+ public int processOfflineStars(final Context context, final ProgressListener progressListener) throws Exception {
+ SharedPreferences offline = Util.getOfflineSync(context);
+ SharedPreferences.Editor offlineEditor = offline.edit();
+ int count = offline.getInt(Constants.OFFLINE_STAR_COUNT, 0);
+ int retry = 0;
+ for(int i = 1; i <= count; i++) {
+ String id = offline.getString(Constants.OFFLINE_STAR_ID + i, null);
+ boolean starred = offline.getBoolean(Constants.OFFLINE_STAR_SETTING + i, false);
+ if(id != null) {
+ setStarred(id, starred, context, progressListener);
+ } else {
+ String search = offline.getString(Constants.OFFLINE_STAR_SEARCH + i, "");
+ try{
+ SearchCritera critera = new SearchCritera(search, 0, 1, 1);
+ SearchResult result = searchNew(critera, context, progressListener);
+ if(result.getSongs().size() == 1){
+ Log.i(TAG, "Query '" + search + "' returned song " + result.getSongs().get(0).getTitle() + " by " + result.getSongs().get(0).getArtist() + " with id " + result.getSongs().get(0).getId());
+ setStarred(result.getSongs().get(0).getId(), starred, context, progressListener);
+ } else if(result.getAlbums().size() == 1){
+ Log.i(TAG, "Query '" + search + "' returned song " + result.getAlbums().get(0).getTitle() + " by " + result.getAlbums().get(0).getArtist() + " with id " + result.getAlbums().get(0).getId());
+ setStarred(result.getAlbums().get(0).getId(), starred, context, progressListener);
+ }
+ else{
+ throw new Exception("Song not found on server");
+ }
+ }
+ catch(Exception e){
+ Log.e(TAG, e.toString());
+ retry++;
+ }
+ }
+ }
+
+ offlineEditor.putInt(Constants.OFFLINE_STAR_COUNT, 0);
+ offlineEditor.commit();
+
+ return count - retry;
+ }
+
+ private String getOfflineSongId(String id, Context context, ProgressListener progressListener) throws Exception {
+ SharedPreferences prefs = Util.getPreferences(context);
+ String cacheLocn = prefs.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, null);
+ if(id.indexOf(cacheLocn) != -1) {
+ String searchCriteria = Util.parseOfflineIDSearch(context, id, cacheLocn);
+ SearchCritera critera = new SearchCritera(searchCriteria, 0, 0, 1);
+ SearchResult result = searchNew(critera, context, progressListener);
+ if(result.getSongs().size() == 1){
+ id = result.getSongs().get(0).getId();
+ }
+ }
+
+ return id;
+ }
+
+ private Reader getReader(Context context, ProgressListener progressListener, String method, HttpParams requestParams) throws Exception {
+ return getReader(context, progressListener, method, requestParams, Collections.<String>emptyList(), Collections.emptyList());
+ }
+
+ private Reader getReader(Context context, ProgressListener progressListener, String method,
+ HttpParams requestParams, String parameterName, Object parameterValue) throws Exception {
+ return getReader(context, progressListener, method, requestParams, Arrays.asList(parameterName), Arrays.<Object>asList(parameterValue));
+ }
+
+ private Reader getReader(Context context, ProgressListener progressListener, String method,
+ HttpParams requestParams, List<String> parameterNames, List<Object> parameterValues) throws Exception {
+
+ if (progressListener != null) {
+ progressListener.updateProgress(R.string.service_connecting);
+ }
+
+ String url = Util.getRestUrl(context, method);
+ return getReaderForURL(context, url, requestParams, parameterNames, parameterValues, progressListener);
+ }
+
+ private Reader getReaderForURL(Context context, String url, HttpParams requestParams, List<String> parameterNames,
+ List<Object> parameterValues, ProgressListener progressListener) throws Exception {
+ HttpEntity entity = getEntityForURL(context, url, requestParams, parameterNames, parameterValues, progressListener);
+ if (entity == null) {
+ throw new RuntimeException("No entity received for URL " + url);
+ }
+
+ InputStream in = entity.getContent();
+ return new InputStreamReader(in, Constants.UTF_8);
+ }
+
+ private HttpEntity getEntityForURL(Context context, String url, HttpParams requestParams, List<String> parameterNames,
+ List<Object> parameterValues, ProgressListener progressListener) throws Exception {
+ return getResponseForURL(context, url, requestParams, parameterNames, parameterValues, null, progressListener, null).getEntity();
+ }
+
+ private HttpResponse getResponseForURL(Context context, String url, HttpParams requestParams,
+ List<String> parameterNames, List<Object> parameterValues,
+ List<Header> headers, ProgressListener progressListener, CancellableTask task) throws Exception {
+ Log.d(TAG, "Connections in pool: " + connManager.getConnectionsInPool());
+
+ // 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("=");
+ builder.append(URLEncoder.encode(String.valueOf(parameterValues.get(i)), "UTF-8"));
+ }
+ url = builder.toString();
+ parameterNames = null;
+ parameterValues = null;
+ }
+
+ String rewrittenUrl = rewriteUrlWithRedirect(context, url);
+ return executeWithRetry(context, rewrittenUrl, url, requestParams, parameterNames, parameterValues, headers, progressListener, task);
+ }
+
+ private HttpResponse executeWithRetry(Context context, String url, String originalUrl, HttpParams requestParams,
+ List<String> parameterNames, List<Object> parameterValues,
+ List<Header> headers, ProgressListener progressListener, CancellableTask task) throws IOException {
+ // Strip out sensitive information from log
+ Log.i(TAG, "Using URL " + url.substring(0, url.indexOf("?u=") + 1) + url.substring(url.indexOf("&v=") + 1));
+
+ 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> cancelled = new AtomicReference<Boolean>(false);
+ int attempts = 0;
+ while (true) {
+ attempts++;
+ HttpContext httpContext = new BasicHttpContext();
+ final HttpPost request = new HttpPost(url);
+
+ if (task != null) {
+ // Attempt to abort the HTTP request if the task is cancelled.
+ task.setOnCancelListener(new CancellableTask.OnCancelListener() {
+ @Override
+ public void onCancel() {
+ new Thread(new Runnable() {
+ public void run() {
+ try {
+ cancelled.set(true);
+ request.abort();
+ } catch(Exception e) {
+ Log.e(TAG, "Failed to stop http task");
+ }
+ }
+ }).start();
+ }
+ });
+ }
+
+ if (parameterNames != null) {
+ List<NameValuePair> params = new ArrayList<NameValuePair>();
+ for (int i = 0; i < parameterNames.size(); i++) {
+ params.add(new BasicNameValuePair(parameterNames.get(i), String.valueOf(parameterValues.get(i))));
+ }
+ request.setEntity(new UrlEncodedFormEntity(params, Constants.UTF_8));
+ }
+
+ if (requestParams != null) {
+ request.setParams(requestParams);
+ Log.d(TAG, "Socket read timeout: " + HttpConnectionParams.getSoTimeout(requestParams) + " ms.");
+ }
+
+ if (headers != null) {
+ for (Header header : headers) {
+ request.addHeader(header);
+ }
+ }
+
+ // Set credentials to get through apache proxies that require authentication.
+ int instance = prefs.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1);
+ String username = prefs.getString(Constants.PREFERENCES_KEY_USERNAME + instance, null);
+ String password = prefs.getString(Constants.PREFERENCES_KEY_PASSWORD + instance, null);
+ httpClient.getCredentialsProvider().setCredentials(new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT),
+ new UsernamePasswordCredentials(username, password));
+
+ try {
+ HttpResponse response = httpClient.execute(request, httpContext);
+ detectRedirect(originalUrl, context, httpContext);
+ return response;
+ } catch (IOException x) {
+ request.abort();
+ if (attempts >= HTTP_REQUEST_MAX_ATTEMPTS || cancelled.get()) {
+ 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 (" + attempts + "), will retry", x);
+ increaseTimeouts(requestParams);
+ Util.sleepQuietly(2000L);
+ }
+ }
+ }
+
+ private void increaseTimeouts(HttpParams requestParams) {
+ if (requestParams != null) {
+ int connectTimeout = HttpConnectionParams.getConnectionTimeout(requestParams);
+ if (connectTimeout != 0) {
+ HttpConnectionParams.setConnectionTimeout(requestParams, (int) (connectTimeout * 1.3F));
+ }
+ int readTimeout = HttpConnectionParams.getSoTimeout(requestParams);
+ if (readTimeout != 0) {
+ HttpConnectionParams.setSoTimeout(requestParams, (int) (readTimeout * 1.5F));
+ }
+ }
+ }
+
+ private void detectRedirect(String originalUrl, Context context, HttpContext httpContext) {
+ 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();
+ }
+
+ redirectFrom = originalUrl.substring(0, originalUrl.indexOf("/rest/"));
+ redirectTo = redirectedUrl.substring(0, redirectedUrl.indexOf("/rest/"));
+
+ if(redirectFrom.compareTo(redirectTo) != 0) {
+ Log.i(TAG, redirectFrom + " redirects to " + redirectTo);
+ }
+ redirectionLastChecked = System.currentTimeMillis();
+ redirectionNetworkType = getCurrentNetworkType(context);
+ }
+
+ private String rewriteUrlWithRedirect(Context context, String url) {
+
+ // Only cache for a certain time.
+ if (System.currentTimeMillis() - redirectionLastChecked > REDIRECTION_CHECK_INTERVAL_MILLIS) {
+ return url;
+ }
+
+ // Ignore cache if network type has changed.
+ if (redirectionNetworkType != getCurrentNetworkType(context)) {
+ return url;
+ }
+
+ if (redirectFrom == null || redirectTo == null) {
+ return url;
+ }
+
+ return url.replace(redirectFrom, redirectTo);
+ }
+
+ private int getCurrentNetworkType(Context context) {
+ ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo networkInfo = manager.getActiveNetworkInfo();
+ return networkInfo == null ? -1 : networkInfo.getType();
+ }
+}
diff --git a/src/github/daneren2005/dsub/service/Scrobbler.java b/src/github/daneren2005/dsub/service/Scrobbler.java
new file mode 100644
index 00000000..222c78c8
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/Scrobbler.java
@@ -0,0 +1,52 @@
+package github.daneren2005.dsub.service;
+
+import android.content.Context;
+import android.util.Log;
+import github.daneren2005.dsub.util.Util;
+
+/**
+ * Scrobbles played songs to Last.fm.
+ *
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class Scrobbler {
+
+ private static final String TAG = Scrobbler.class.getSimpleName();
+
+ private String lastSubmission;
+ private String lastNowPlaying;
+
+ public void scrobble(final Context context, final DownloadFile song, final boolean submission) {
+ if (song == null || !Util.isScrobblingEnabled(context)) {
+ return;
+ }
+ final String id = song.getSong().getId();
+
+ // Avoid duplicate registrations.
+ if (submission && id.equals(lastSubmission)) {
+ return;
+ }
+ if (!submission && id.equals(lastNowPlaying)) {
+ return;
+ }
+ if (submission) {
+ lastSubmission = id;
+ } else {
+ lastNowPlaying = id;
+ }
+
+ new Thread("Scrobble " + song) {
+ @Override
+ public void run() {
+ MusicService service = MusicServiceFactory.getMusicService(context);
+ try {
+ service.scrobble(id, submission, context, null);
+ Log.i(TAG, "Scrobbled '" + (submission ? "submission" : "now playing") + "' for " + song);
+ } catch (Exception x) {
+ Log.i(TAG, "Failed to scrobble'" + (submission ? "submission" : "now playing") + "' for " + song, x);
+ }
+ }
+ }.start();
+ }
+}
diff --git a/src/github/daneren2005/dsub/service/ServerTooOldException.java b/src/github/daneren2005/dsub/service/ServerTooOldException.java
new file mode 100644
index 00000000..e4a951de
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/ServerTooOldException.java
@@ -0,0 +1,60 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.service;
+
+import github.daneren2005.dsub.domain.Version;
+
+/**
+ * Thrown if the REST API version implemented by the server is too old.
+ *
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class ServerTooOldException extends Exception {
+
+ private final String text;
+ private final Version serverVersion;
+ private final Version requiredVersion;
+
+ public ServerTooOldException(String text, Version serverVersion, Version requiredVersion) {
+ this.text = text;
+ this.serverVersion = serverVersion;
+ this.requiredVersion = requiredVersion;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ if (text != null) {
+ builder.append(text).append(" ");
+ }
+ builder.append("Server API version too old. ");
+ builder.append("Requires server version ")
+ .append(requiredVersion.getVersion())
+ .append(", but it is version ")
+ .append(serverVersion.getVersion())
+ .append(".");
+ return builder.toString();
+ }
+
+ @Override
+ public String getMessage() {
+ return this.toString();
+ }
+}
diff --git a/src/github/daneren2005/dsub/service/StreamProxy.java b/src/github/daneren2005/dsub/service/StreamProxy.java
new file mode 100644
index 00000000..24c1b201
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/StreamProxy.java
@@ -0,0 +1,248 @@
+package github.daneren2005.dsub.service;
+
+import java.io.BufferedOutputStream;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketException;
+import java.net.SocketTimeoutException;
+import java.net.URLDecoder;
+import java.net.UnknownHostException;
+import java.util.StringTokenizer;
+
+import org.apache.http.HttpRequest;
+import org.apache.http.message.BasicHttpRequest;
+
+import android.os.AsyncTask;
+import android.os.Looper;
+import android.util.Log;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.util.Constants;
+
+public class StreamProxy implements Runnable {
+ private static final String TAG = StreamProxy.class.getSimpleName();
+
+ private Thread thread;
+ private boolean isRunning;
+ private ServerSocket socket;
+ private int port;
+ private DownloadService downloadService;
+
+ public StreamProxy(DownloadService downloadService) {
+
+ // Create listening socket
+ try {
+ socket = new ServerSocket(0, 0, InetAddress.getByAddress(new byte[] { 127, 0, 0, 1 }));
+ socket.setSoTimeout(5000);
+ port = socket.getLocalPort();
+ this.downloadService = downloadService;
+ } catch (UnknownHostException e) { // impossible
+ } catch (IOException e) {
+ Log.e(TAG, "IOException initializing server", e);
+ }
+ }
+
+ public int getPort() {
+ return port;
+ }
+
+ public void start() {
+ thread = new Thread(this);
+ thread.start();
+ }
+
+ public void stop() {
+ isRunning = false;
+ thread.interrupt();
+ }
+
+ @Override
+ public void run() {
+ isRunning = true;
+ while (isRunning) {
+ try {
+ Socket client = socket.accept();
+ if (client == null) {
+ continue;
+ }
+ Log.i(TAG, "client connected");
+
+ StreamToMediaPlayerTask task = new StreamToMediaPlayerTask(client);
+ if (task.processRequest()) {
+ new Thread(task).start();
+ }
+
+ } catch (SocketTimeoutException e) {
+ // Do nothing
+ } catch (IOException e) {
+ Log.e(TAG, "Error connecting to client", e);
+ }
+ }
+ Log.i(TAG, "Proxy interrupted. Shutting down.");
+ }
+
+ private class StreamToMediaPlayerTask implements Runnable {
+
+ String localPath;
+ Socket client;
+ int cbSkip;
+
+ public StreamToMediaPlayerTask(Socket client) {
+ this.client = client;
+ }
+
+ private HttpRequest readRequest() {
+ HttpRequest request = null;
+ InputStream is;
+ String firstLine;
+ try {
+ is = client.getInputStream();
+ BufferedReader reader = new BufferedReader(new InputStreamReader(is), 8192);
+ firstLine = reader.readLine();
+ } catch (IOException e) {
+ Log.e(TAG, "Error parsing request", e);
+ return request;
+ }
+
+ if (firstLine == null) {
+ Log.i(TAG, "Proxy client closed connection without a request.");
+ return request;
+ }
+
+ StringTokenizer st = new StringTokenizer(firstLine);
+ String method = st.nextToken();
+ String uri = st.nextToken();
+ String realUri = uri.substring(1);
+ Log.i(TAG, realUri);
+ request = new BasicHttpRequest(method, realUri);
+ return request;
+ }
+
+ public boolean processRequest() {
+ HttpRequest request = readRequest();
+ if (request == null) {
+ return false;
+ }
+
+ // Read HTTP headers
+ Log.i(TAG, "Processing request");
+
+ try {
+ localPath = URLDecoder.decode(request.getRequestLine().getUri(), Constants.UTF_8);
+ } catch (UnsupportedEncodingException e) {
+ Log.e(TAG, "Unsupported encoding", e);
+ return false;
+ }
+
+ Log.i(TAG, "Processing request for file " + localPath);
+ File file = new File(localPath);
+ if (!file.exists()) {
+ Log.e(TAG, "File " + localPath + " does not exist");
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public void run() {
+ Log.i(TAG, "Streaming song in background");
+ DownloadFile downloadFile = downloadService.getCurrentPlaying();
+ MusicDirectory.Entry song = downloadFile.getSong();
+
+ // Create HTTP header
+ String headers = "HTTP/1.0 200 OK\r\n";
+ headers += "Content-Type: " + "application/octet-stream" + "\r\n";
+
+ Integer contentLength = downloadFile.getContentLength();
+ long fileSize;
+ if(contentLength == null) {
+ fileSize = downloadFile.getBitRate() * ((song.getDuration() != null) ? song.getDuration() : 0) * 1000 / 8;
+ } else {
+ fileSize = contentLength;
+ headers += "Content-Length: " + fileSize + "\r\n";
+ }
+ Log.i(TAG, "Streaming fileSize: " + fileSize);
+
+ headers += "Connection: close\r\n";
+ headers += "\r\n";
+
+ long cbToSend = fileSize - cbSkip;
+ OutputStream output = null;
+ byte[] buff = new byte[64 * 1024];
+ try {
+ output = new BufferedOutputStream(client.getOutputStream(), 32*1024);
+ output.write(headers.getBytes());
+
+ if(!downloadFile.isWorkDone()) {
+ // Loop as long as there's stuff to send
+ while (isRunning && !client.isClosed()) {
+
+ // See if there's more to send
+ File file = new File(localPath);
+ int cbSentThisBatch = 0;
+ if (file.exists()) {
+ FileInputStream input = new FileInputStream(file);
+ input.skip(cbSkip);
+ int cbToSendThisBatch = input.available();
+ while (cbToSendThisBatch > 0) {
+ int cbToRead = Math.min(cbToSendThisBatch, buff.length);
+ int cbRead = input.read(buff, 0, cbToRead);
+ if (cbRead == -1) {
+ break;
+ }
+ cbToSendThisBatch -= cbRead;
+ cbToSend -= cbRead;
+ output.write(buff, 0, cbRead);
+ output.flush();
+ cbSkip += cbRead;
+ cbSentThisBatch += cbRead;
+ }
+ input.close();
+ }
+
+ // Done regardless of whether or not it thinks it is
+ if(downloadFile.isWorkDone() && cbSkip >= file.length()) {
+ break;
+ }
+
+ // If we did nothing this batch, block for a second
+ if (cbSentThisBatch == 0) {
+ Log.d(TAG, "Blocking until more data appears (" + cbToSend + ")");
+ Thread.sleep(1000);
+ }
+ }
+ } else {
+ Log.w(TAG, "Requesting data for completely downloaded file");
+ }
+ }
+ catch (SocketException socketException) {
+ Log.e(TAG, "SocketException() thrown, proxy client has probably closed. This can exit harmlessly");
+ }
+ catch (Exception e) {
+ Log.e(TAG, "Exception thrown from streaming task:");
+ Log.e(TAG, e.getClass().getName() + " : " + e.getLocalizedMessage());
+ }
+
+ // Cleanup
+ try {
+ if (output != null) {
+ output.close();
+ }
+ client.close();
+ }
+ catch (IOException e) {
+ Log.e(TAG, "IOException while cleaning up streaming task:");
+ Log.e(TAG, e.getClass().getName() + " : " + e.getLocalizedMessage());
+ }
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/service/parser/AbstractParser.java b/src/github/daneren2005/dsub/service/parser/AbstractParser.java
new file mode 100644
index 00000000..1a457754
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/parser/AbstractParser.java
@@ -0,0 +1,138 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.service.parser;
+
+import java.io.Reader;
+
+import org.xmlpull.v1.XmlPullParser;
+
+import android.content.Context;
+import android.util.Xml;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.Version;
+import github.daneren2005.dsub.util.ProgressListener;
+import github.daneren2005.dsub.util.Util;
+
+/**
+ * @author Sindre Mehus
+ */
+public abstract class AbstractParser {
+
+ protected final Context context;
+ private XmlPullParser parser;
+ private boolean rootElementFound;
+
+ public AbstractParser(Context context) {
+ this.context = context;
+ }
+
+ protected Context getContext() {
+ return context;
+ }
+
+ protected void handleError() throws Exception {
+ int code = getInteger("code");
+ String message;
+ switch (code) {
+ case 20:
+ message = context.getResources().getString(R.string.parser_upgrade_client);
+ break;
+ case 30:
+ message = context.getResources().getString(R.string.parser_upgrade_server);
+ break;
+ case 40:
+ message = context.getResources().getString(R.string.parser_not_authenticated);
+ break;
+ case 50:
+ message = context.getResources().getString(R.string.parser_not_authorized);
+ break;
+ default:
+ message = get("message");
+ break;
+ }
+ throw new SubsonicRESTException(code, message);
+ }
+
+ protected void updateProgress(ProgressListener progressListener, int messageId) {
+ if (progressListener != null) {
+ progressListener.updateProgress(messageId);
+ }
+ }
+
+ protected void updateProgress(ProgressListener progressListener, String message) {
+ if (progressListener != null) {
+ progressListener.updateProgress(message);
+ }
+ }
+
+ protected String getText() {
+ return parser.getText();
+ }
+
+ protected String get(String name) {
+ return parser.getAttributeValue(null, name);
+ }
+
+ protected boolean getBoolean(String name) {
+ return "true".equals(get(name));
+ }
+
+ protected Integer getInteger(String name) {
+ String s = get(name);
+ return s == null ? null : Integer.valueOf(s);
+ }
+
+ protected Long getLong(String name) {
+ String s = get(name);
+ return s == null ? null : Long.valueOf(s);
+ }
+
+ protected Float getFloat(String name) {
+ String s = get(name);
+ return s == null ? null : Float.valueOf(s);
+ }
+
+ protected void init(Reader reader) throws Exception {
+ parser = Xml.newPullParser();
+ parser.setInput(reader);
+ rootElementFound = false;
+ }
+
+ protected int nextParseEvent() throws Exception {
+ return parser.next();
+ }
+
+ protected String getElementName() {
+ String name = parser.getName();
+ if ("subsonic-response".equals(name)) {
+ rootElementFound = true;
+ String version = get("version");
+ if (version != null) {
+ Util.setServerRestVersion(context, new Version(version));
+ }
+ }
+ return name;
+ }
+
+ protected void validate() throws Exception {
+ if (!rootElementFound) {
+ throw new Exception(context.getResources().getString(R.string.background_task_parse_error));
+ }
+ }
+} \ No newline at end of file
diff --git a/src/github/daneren2005/dsub/service/parser/AlbumListParser.java b/src/github/daneren2005/dsub/service/parser/AlbumListParser.java
new file mode 100644
index 00000000..64145d67
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/parser/AlbumListParser.java
@@ -0,0 +1,62 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.service.parser;
+
+import android.content.Context;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.util.ProgressListener;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.Reader;
+
+/**
+ * @author Sindre Mehus
+ */
+public class AlbumListParser extends MusicDirectoryEntryParser {
+
+ public AlbumListParser(Context context) {
+ super(context);
+ }
+
+ public MusicDirectory parse(Reader reader, ProgressListener progressListener) throws Exception {
+
+ updateProgress(progressListener, R.string.parser_reading);
+ init(reader);
+
+ MusicDirectory dir = new MusicDirectory();
+ int eventType;
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = getElementName();
+ if ("album".equals(name)) {
+ dir.addChild(parseEntry(""));
+ } else if ("error".equals(name)) {
+ handleError();
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+ updateProgress(progressListener, R.string.parser_reading_done);
+
+ return dir;
+ }
+} \ No newline at end of file
diff --git a/src/github/daneren2005/dsub/service/parser/ChatMessageParser.java b/src/github/daneren2005/dsub/service/parser/ChatMessageParser.java
new file mode 100644
index 00000000..1425a734
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/parser/ChatMessageParser.java
@@ -0,0 +1,67 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.service.parser;
+
+import android.content.Context;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.ChatMessage;
+import github.daneren2005.dsub.util.ProgressListener;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author Joshua Bahnsen
+ */
+public class ChatMessageParser extends AbstractParser {
+
+ public ChatMessageParser(Context context) {
+ super(context);
+ }
+
+ public List<ChatMessage> parse(Reader reader, ProgressListener progressListener) throws Exception {
+ updateProgress(progressListener, R.string.parser_reading);
+ init(reader);
+ List<ChatMessage> result = new ArrayList<ChatMessage>();
+ int eventType;
+
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = getElementName();
+ if ("chatMessage".equals(name)) {
+ ChatMessage chatMessage = new ChatMessage();
+ chatMessage.setUsername(get("username"));
+ chatMessage.setTime(getLong("time"));
+ chatMessage.setMessage(get("message"));
+ result.add(chatMessage);
+ } else if ("error".equals(name)) {
+ handleError();
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+ updateProgress(progressListener, R.string.parser_reading_done);
+
+ return result;
+ }
+}
diff --git a/src/github/daneren2005/dsub/service/parser/ErrorParser.java b/src/github/daneren2005/dsub/service/parser/ErrorParser.java
new file mode 100644
index 00000000..3463687d
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/parser/ErrorParser.java
@@ -0,0 +1,49 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.service.parser;
+
+import android.content.Context;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.Reader;
+
+/**
+ * @author Sindre Mehus
+ */
+public class ErrorParser extends AbstractParser {
+
+ public ErrorParser(Context context) {
+ super(context);
+ }
+
+ public void parse(Reader reader) throws Exception {
+
+ init(reader);
+
+ int eventType;
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG && "error".equals(getElementName())) {
+ handleError();
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+ }
+} \ No newline at end of file
diff --git a/src/github/daneren2005/dsub/service/parser/GenreParser.java b/src/github/daneren2005/dsub/service/parser/GenreParser.java
new file mode 100644
index 00000000..1062d3af
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/parser/GenreParser.java
@@ -0,0 +1,122 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.service.parser;
+
+import android.content.Context;
+import android.util.Log;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.Genre;
+import github.daneren2005.dsub.util.ProgressListener;
+
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author Joshua Bahnsen
+ */
+public class GenreParser extends AbstractParser {
+ private static final String TAG = GenreParser.class.getSimpleName();
+
+ public GenreParser(Context context) {
+ super(context);
+ }
+
+ public List<Genre> parse(Reader reader, ProgressListener progressListener) throws Exception {
+ updateProgress(progressListener, R.string.parser_reading);
+
+ List<Genre> result = new ArrayList<Genre>();
+ StringReader sr = null;
+
+ try {
+ BufferedReader br = new BufferedReader(reader);
+ String xml = null;
+ String line = null;
+
+ while ((line = br.readLine()) != null) {
+ if (xml == null) {
+ xml = line;
+ } else {
+ xml += line;
+ }
+ }
+ br.close();
+
+ // Replace double escaped ampersand (&amp;apos;)
+ xml = xml.replaceAll("(?:&amp;)(amp;|lt;|gt;|#37;|apos;)", "&$1");
+
+ // Replace unescaped ampersand
+ xml = xml.replaceAll("&(?!amp;|lt;|gt;|#37;|apos;)", "&amp;");
+
+ // Replace unescaped percent symbol
+ // No replacements for <> at this time
+ xml = xml.replaceAll("%", "&#37;");
+
+ xml = xml.replaceAll("'", "&apos;");
+
+ sr = new StringReader(xml);
+ } catch (IOException ioe) {
+ Log.e(TAG, "Error parsing Genre XML", ioe);
+ }
+
+ if (sr == null) {
+ Log.w(TAG, "Unable to parse Genre XML, returning empty list");
+ return result;
+ }
+
+ init(sr);
+
+ Genre genre = null;
+
+ int eventType;
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = getElementName();
+ if ("genre".equals(name)) {
+ genre = new Genre();
+ } else if ("error".equals(name)) {
+ handleError();
+ } else {
+ genre = null;
+ }
+ } else if (eventType == XmlPullParser.TEXT) {
+ if (genre != null) {
+ String value = getText();
+ if (genre != null) {
+ genre.setName(value);
+ genre.setIndex(value.substring(0, 1));
+ result.add(genre);
+ genre = null;
+ }
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+ updateProgress(progressListener, R.string.parser_reading_done);
+
+ return result;
+ }
+}
diff --git a/src/github/daneren2005/dsub/service/parser/IndexesParser.java b/src/github/daneren2005/dsub/service/parser/IndexesParser.java
new file mode 100644
index 00000000..6196411d
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/parser/IndexesParser.java
@@ -0,0 +1,120 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.service.parser;
+
+import java.io.Reader;
+import java.util.List;
+import java.util.ArrayList;
+
+import org.xmlpull.v1.XmlPullParser;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.Artist;
+import github.daneren2005.dsub.domain.Indexes;
+import github.daneren2005.dsub.util.ProgressListener;
+import android.util.Log;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.Util;
+
+/**
+ * @author Sindre Mehus
+ */
+public class IndexesParser extends AbstractParser {
+ private static final String TAG = IndexesParser.class.getSimpleName();
+
+ private Context context;
+
+ public IndexesParser(Context context) {
+ super(context);
+ this.context = context;
+ }
+
+ public Indexes parse(Reader reader, ProgressListener progressListener) throws Exception {
+
+ long t0 = System.currentTimeMillis();
+ updateProgress(progressListener, R.string.parser_reading);
+ init(reader);
+
+ List<Artist> artists = new ArrayList<Artist>();
+ List<Artist> shortcuts = new ArrayList<Artist>();
+ Long lastModified = null;
+ int eventType;
+ String index = "#";
+ String ignoredArticles = null;
+ boolean changed = false;
+
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = getElementName();
+ if ("indexes".equals(name)) {
+ changed = true;
+ lastModified = getLong("lastModified");
+ ignoredArticles = get("ignoredArticles");
+ } else if ("index".equals(name)) {
+ index = get("name");
+
+ } else if ("artist".equals(name)) {
+ Artist artist = new Artist();
+ artist.setId(get("id"));
+ artist.setName(get("name"));
+ artist.setIndex(index);
+ artist.setStarred(get("starred") != null);
+ artists.add(artist);
+
+ if (artists.size() % 10 == 0) {
+ String msg = getContext().getResources().getString(R.string.parser_artist_count, artists.size());
+ updateProgress(progressListener, msg);
+ }
+ } else if ("shortcut".equals(name)) {
+ Artist shortcut = new Artist();
+ shortcut.setId(get("id"));
+ shortcut.setName(get("name"));
+ shortcut.setIndex("*");
+ shortcut.setStarred(get("starred") != null);
+ shortcuts.add(shortcut);
+ } else if ("error".equals(name)) {
+ handleError();
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+
+ if(ignoredArticles != null) {
+ SharedPreferences.Editor prefs = Util.getPreferences(context).edit();
+ prefs.putString(Constants.CACHE_KEY_IGNORE, ignoredArticles);
+ prefs.commit();
+ }
+
+ if (!changed) {
+ return null;
+ }
+
+ long t1 = System.currentTimeMillis();
+ Log.d(TAG, "Got " + artists.size() + " artist(s) in " + (t1 - t0) + "ms.");
+
+ String msg = getContext().getResources().getString(R.string.parser_artist_count, artists.size());
+ updateProgress(progressListener, msg);
+
+ return new Indexes(lastModified == null ? 0L : lastModified, shortcuts, artists);
+ }
+} \ No newline at end of file
diff --git a/src/github/daneren2005/dsub/service/parser/JukeboxStatusParser.java b/src/github/daneren2005/dsub/service/parser/JukeboxStatusParser.java
new file mode 100644
index 00000000..8526e635
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/parser/JukeboxStatusParser.java
@@ -0,0 +1,62 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.service.parser;
+
+import java.io.Reader;
+
+import org.xmlpull.v1.XmlPullParser;
+
+import android.content.Context;
+import github.daneren2005.dsub.domain.JukeboxStatus;
+
+/**
+ * @author Sindre Mehus
+ */
+public class JukeboxStatusParser extends AbstractParser {
+
+ public JukeboxStatusParser(Context context) {
+ super(context);
+ }
+
+ public JukeboxStatus parse(Reader reader) throws Exception {
+
+ init(reader);
+
+ JukeboxStatus jukeboxStatus = new JukeboxStatus();
+ int eventType;
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = getElementName();
+ if ("jukeboxPlaylist".equals(name) || "jukeboxStatus".equals(name)) {
+ jukeboxStatus.setPositionSeconds(getInteger("position"));
+ jukeboxStatus.setCurrentIndex(getInteger("currentIndex"));
+ jukeboxStatus.setPlaying(getBoolean("playing"));
+ jukeboxStatus.setGain(getFloat("gain"));
+ } else if ("error".equals(name)) {
+ handleError();
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+
+ return jukeboxStatus;
+ }
+} \ No newline at end of file
diff --git a/src/github/daneren2005/dsub/service/parser/LicenseParser.java b/src/github/daneren2005/dsub/service/parser/LicenseParser.java
new file mode 100644
index 00000000..e7b200fd
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/parser/LicenseParser.java
@@ -0,0 +1,62 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.service.parser;
+
+import android.content.Context;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.Reader;
+
+import github.daneren2005.dsub.domain.ServerInfo;
+import github.daneren2005.dsub.domain.Version;
+
+/**
+ * @author Sindre Mehus
+ */
+public class LicenseParser extends AbstractParser {
+
+ public LicenseParser(Context context) {
+ super(context);
+ }
+
+ public ServerInfo parse(Reader reader) throws Exception {
+
+ init(reader);
+
+ ServerInfo serverInfo = new ServerInfo();
+ int eventType;
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = getElementName();
+ if ("subsonic-response".equals(name)) {
+ serverInfo.setRestVersion(new Version(get("version")));
+ } else if ("license".equals(name)) {
+ serverInfo.setLicenseValid(getBoolean("valid"));
+ } else if ("error".equals(name)) {
+ handleError();
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+
+ return serverInfo;
+ }
+} \ No newline at end of file
diff --git a/src/github/daneren2005/dsub/service/parser/LyricsParser.java b/src/github/daneren2005/dsub/service/parser/LyricsParser.java
new file mode 100644
index 00000000..98b0f6a0
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/parser/LyricsParser.java
@@ -0,0 +1,65 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.service.parser;
+
+import android.content.Context;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.Lyrics;
+import github.daneren2005.dsub.util.ProgressListener;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.Reader;
+
+/**
+ * @author Sindre Mehus
+ */
+public class LyricsParser extends AbstractParser {
+
+ public LyricsParser(Context context) {
+ super(context);
+ }
+
+ public Lyrics parse(Reader reader, ProgressListener progressListener) throws Exception {
+ updateProgress(progressListener, R.string.parser_reading);
+ init(reader);
+
+ Lyrics lyrics = null;
+ int eventType;
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = getElementName();
+ if ("lyrics".equals(name)) {
+ lyrics = new Lyrics();
+ lyrics.setArtist(get("artist"));
+ lyrics.setTitle(get("title"));
+ } else if ("error".equals(name)) {
+ handleError();
+ }
+ } else if (eventType == XmlPullParser.TEXT) {
+ if (lyrics != null && lyrics.getText() == null) {
+ lyrics.setText(getText());
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+ return lyrics;
+ }
+}
diff --git a/src/github/daneren2005/dsub/service/parser/MusicDirectoryEntryParser.java b/src/github/daneren2005/dsub/service/parser/MusicDirectoryEntryParser.java
new file mode 100644
index 00000000..b0434aca
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/parser/MusicDirectoryEntryParser.java
@@ -0,0 +1,74 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.service.parser;
+
+import android.content.Context;
+import github.daneren2005.dsub.domain.MusicDirectory;
+
+/**
+ * @author Sindre Mehus
+ */
+public class MusicDirectoryEntryParser extends AbstractParser {
+ public MusicDirectoryEntryParser(Context context) {
+ super(context);
+ }
+
+ protected MusicDirectory.Entry parseEntry(String artist) {
+ MusicDirectory.Entry entry = new MusicDirectory.Entry();
+ entry.setId(get("id"));
+ entry.setParent(get("parent"));
+ entry.setTitle(get("title"));
+ entry.setDirectory(getBoolean("isDir"));
+ entry.setCoverArt(get("coverArt"));
+ entry.setArtist(get("artist"));
+ entry.setStarred(get("starred") != null);
+
+ if (!entry.isDirectory()) {
+ entry.setAlbum(get("album"));
+ entry.setTrack(getInteger("track"));
+ entry.setYear(getInteger("year"));
+ entry.setGenre(get("genre"));
+ entry.setContentType(get("contentType"));
+ entry.setSuffix(get("suffix"));
+ entry.setTranscodedContentType(get("transcodedContentType"));
+ entry.setTranscodedSuffix(get("transcodedSuffix"));
+ entry.setSize(getLong("size"));
+ entry.setDuration(getInteger("duration"));
+ entry.setBitRate(getInteger("bitRate"));
+ entry.setPath(get("path"));
+ entry.setVideo(getBoolean("isVideo"));
+ entry.setDiscNumber(getInteger("discNumber"));
+ } else if(!"".equals(artist)) {
+ entry.setPath(artist + "/" + entry.getTitle());
+ }
+ return entry;
+ }
+
+ protected MusicDirectory.Entry parseArtist() {
+ MusicDirectory.Entry entry = new MusicDirectory.Entry();
+
+ entry.setId(get("id"));
+ entry.setTitle(get("name"));
+ entry.setPath(entry.getTitle());
+ entry.setStarred(true);
+ entry.setDirectory(true);
+
+ return entry;
+ }
+} \ No newline at end of file
diff --git a/src/github/daneren2005/dsub/service/parser/MusicDirectoryParser.java b/src/github/daneren2005/dsub/service/parser/MusicDirectoryParser.java
new file mode 100644
index 00000000..17b09805
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/parser/MusicDirectoryParser.java
@@ -0,0 +1,83 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.service.parser;
+
+import android.content.Context;
+import android.util.Log;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.Version;
+import github.daneren2005.dsub.util.ProgressListener;
+import github.daneren2005.dsub.util.Util;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.Reader;
+
+/**
+ * @author Sindre Mehus
+ */
+public class MusicDirectoryParser extends MusicDirectoryEntryParser {
+
+ private static final String TAG = MusicDirectoryParser.class.getSimpleName();
+ private Context context;
+
+ public MusicDirectoryParser(Context context) {
+ super(context);
+ this.context = context;
+ }
+
+ public MusicDirectory parse(String artist, Reader reader, ProgressListener progressListener) throws Exception {
+ long t0 = System.currentTimeMillis();
+ updateProgress(progressListener, R.string.parser_reading);
+ init(reader);
+
+ MusicDirectory dir = new MusicDirectory();
+ int eventType;
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = getElementName();
+ if ("child".equals(name)) {
+ MusicDirectory.Entry entry = parseEntry(artist);
+ entry.setGrandParent(dir.getParent());
+ dir.addChild(entry);
+ } else if ("directory".equals(name)) {
+ dir.setName(get("name"));
+ dir.setId(get("id"));
+ dir.setParent(get("parent"));
+ } else if ("error".equals(name)) {
+ handleError();
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+ updateProgress(progressListener, R.string.parser_reading_done);
+
+ // Only apply sorting on server version 4.7 and greater, where disc is supported
+ if(Util.checkServerVersion(context, "1.8.0")) {
+ dir.sortChildren();
+ }
+
+ long t1 = System.currentTimeMillis();
+ Log.d(TAG, "Got music directory in " + (t1 - t0) + "ms.");
+
+ return dir;
+ }
+} \ No newline at end of file
diff --git a/src/github/daneren2005/dsub/service/parser/MusicFoldersParser.java b/src/github/daneren2005/dsub/service/parser/MusicFoldersParser.java
new file mode 100644
index 00000000..5dfebf27
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/parser/MusicFoldersParser.java
@@ -0,0 +1,69 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.service.parser;
+
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.xmlpull.v1.XmlPullParser;
+
+import android.content.Context;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicFolder;
+import github.daneren2005.dsub.domain.Playlist;
+import github.daneren2005.dsub.util.ProgressListener;
+
+/**
+ * @author Sindre Mehus
+ */
+public class MusicFoldersParser extends AbstractParser {
+
+ public MusicFoldersParser(Context context) {
+ super(context);
+ }
+
+ public List<MusicFolder> parse(Reader reader, ProgressListener progressListener) throws Exception {
+
+ updateProgress(progressListener, R.string.parser_reading);
+ init(reader);
+
+ List<MusicFolder> result = new ArrayList<MusicFolder>();
+ int eventType;
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG) {
+ String tag = getElementName();
+ if ("musicFolder".equals(tag)) {
+ String id = get("id");
+ String name = get("name");
+ result.add(new MusicFolder(id, name));
+ } else if ("error".equals(tag)) {
+ handleError();
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+ updateProgress(progressListener, R.string.parser_reading_done);
+
+ return result;
+ }
+
+} \ No newline at end of file
diff --git a/src/github/daneren2005/dsub/service/parser/PlaylistParser.java b/src/github/daneren2005/dsub/service/parser/PlaylistParser.java
new file mode 100644
index 00000000..8c6cfc6f
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/parser/PlaylistParser.java
@@ -0,0 +1,62 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.service.parser;
+
+import android.content.Context;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.util.ProgressListener;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.Reader;
+
+/**
+ * @author Sindre Mehus
+ */
+public class PlaylistParser extends MusicDirectoryEntryParser {
+
+ public PlaylistParser(Context context) {
+ super(context);
+ }
+
+ public MusicDirectory parse(Reader reader, ProgressListener progressListener) throws Exception {
+ updateProgress(progressListener, R.string.parser_reading);
+ init(reader);
+
+ MusicDirectory dir = new MusicDirectory();
+ int eventType;
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = getElementName();
+ if ("entry".equals(name)) {
+ dir.addChild(parseEntry(""));
+ } else if ("error".equals(name)) {
+ handleError();
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+ updateProgress(progressListener, R.string.parser_reading_done);
+
+ return dir;
+ }
+
+} \ No newline at end of file
diff --git a/src/github/daneren2005/dsub/service/parser/PlaylistsParser.java b/src/github/daneren2005/dsub/service/parser/PlaylistsParser.java
new file mode 100644
index 00000000..a5bf2497
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/parser/PlaylistsParser.java
@@ -0,0 +1,73 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.service.parser;
+
+import android.content.Context;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.Playlist;
+import github.daneren2005.dsub.util.ProgressListener;
+import github.daneren2005.dsub.view.PlaylistAdapter;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author Sindre Mehus
+ */
+public class PlaylistsParser extends AbstractParser {
+
+ public PlaylistsParser(Context context) {
+ super(context);
+ }
+
+ public List<Playlist> parse(Reader reader, ProgressListener progressListener) throws Exception {
+
+ updateProgress(progressListener, R.string.parser_reading);
+ init(reader);
+
+ List<Playlist> result = new ArrayList<Playlist>();
+ int eventType;
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG) {
+ String tag = getElementName();
+ if ("playlist".equals(tag)) {
+ String id = get("id");
+ String name = get("name");
+ String owner = get("owner");
+ String comment = get("comment");
+ String songCount = get("songCount");
+ String created = get("created");
+ String pub = get("public");
+ result.add(new Playlist(id, name, owner, comment, songCount, created, pub));
+ } else if ("error".equals(tag)) {
+ handleError();
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+ updateProgress(progressListener, R.string.parser_reading_done);
+
+ return PlaylistAdapter.PlaylistComparator.sort(result);
+ }
+
+} \ No newline at end of file
diff --git a/src/github/daneren2005/dsub/service/parser/PodcastChannelParser.java b/src/github/daneren2005/dsub/service/parser/PodcastChannelParser.java
new file mode 100644
index 00000000..b091aefa
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/parser/PodcastChannelParser.java
@@ -0,0 +1,67 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.service.parser;
+
+import android.content.Context;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.PodcastChannel;
+import github.daneren2005.dsub.util.ProgressListener;
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.List;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ *
+ * @author Scott
+ */
+public class PodcastChannelParser extends AbstractParser {
+ public PodcastChannelParser(Context context) {
+ super(context);
+ }
+
+ public List<PodcastChannel> parse(Reader reader, ProgressListener progressListener) throws Exception {
+ updateProgress(progressListener, R.string.parser_reading);
+ init(reader);
+
+ List<PodcastChannel> channels = new ArrayList<PodcastChannel>();
+ int eventType;
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = getElementName();
+ if ("channel".equals(name)) {
+ PodcastChannel channel = new PodcastChannel();
+ channel.setId(get("id"));
+ channel.setUrl(get("url"));
+ channel.setName(get("title"));
+ channel.setDescription(get("description"));
+ channel.setStatus(get("status"));
+ channel.setErrorMessage(get("errorMessage"));
+ channels.add(channel);
+ } else if ("error".equals(name)) {
+ handleError();
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+ return channels;
+ }
+}
diff --git a/src/github/daneren2005/dsub/service/parser/PodcastEntryParser.java b/src/github/daneren2005/dsub/service/parser/PodcastEntryParser.java
new file mode 100644
index 00000000..585b3057
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/parser/PodcastEntryParser.java
@@ -0,0 +1,105 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.service.parser;
+
+import android.content.Context;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.PodcastEpisode;
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.ProgressListener;
+import java.io.Reader;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ *
+ * @author Scott
+ */
+public class PodcastEntryParser extends AbstractParser {
+ private static int bogusId = -1;
+
+ public PodcastEntryParser(Context context) {
+ super(context);
+ }
+
+ public MusicDirectory parse(String channel, Reader reader, ProgressListener progressListener) throws Exception {
+ updateProgress(progressListener, R.string.parser_reading);
+ init(reader);
+
+ MusicDirectory episodes = new MusicDirectory();
+ int eventType;
+ boolean valid = false;
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = getElementName();
+ if ("channel".equals(name)) {
+ String id = get("id");
+ if(id.equals(channel)) {
+ episodes.setId(id);
+ episodes.setName(get("title"));
+ valid = true;
+ } else {
+ valid = false;
+ }
+ }
+ else if ("episode".equals(name) && valid) {
+ PodcastEpisode episode = new PodcastEpisode();
+ episode.setEpisodeId(get("id"));
+ episode.setId(get("streamId"));
+ episode.setTitle(get("title"));
+ episode.setArtist(episodes.getName());
+ episode.setAlbum(get("description"));
+ episode.setDate(get("publishDate"));
+ if(episode.getDate() == null) {
+ episode.setDate(get("created"));
+ }
+ if(episode.getDate() != null && episode.getDate().indexOf("T") != -1) {
+ episode.setDate(episode.getDate().replace("T", " "));
+ }
+ episode.setStatus(get("status"));
+ episode.setCoverArt(get("coverArt"));
+ episode.setSize(getLong("size"));
+ episode.setContentType(get("contentType"));
+ episode.setSuffix(get("suffix"));
+ episode.setDuration(getInteger("duration"));
+ episode.setBitRate(getInteger("bitRate"));
+ episode.setVideo(getBoolean("isVideo"));
+ episode.setPath(get("path"));
+ if(episode.getPath() == null) {
+ episode.setPath(FileUtil.getPodcastPath(context, episode));
+ } else if(episode.getPath().indexOf("Podcasts/") == 0) {
+ episode.setPath(episode.getPath().substring("Podcasts/".length()));
+ }
+
+ if("error".equals(episode.getStatus()) || "skipped".equals(episode.getStatus())) {
+ episode.setId(String.valueOf(bogusId));
+ bogusId--;
+ }
+ episodes.addChild(episode);
+ } else if ("error".equals(name)) {
+ handleError();
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+ return episodes;
+ }
+}
diff --git a/src/github/daneren2005/dsub/service/parser/RandomSongsParser.java b/src/github/daneren2005/dsub/service/parser/RandomSongsParser.java
new file mode 100644
index 00000000..3e62d3dc
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/parser/RandomSongsParser.java
@@ -0,0 +1,62 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.service.parser;
+
+import android.content.Context;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.util.ProgressListener;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.Reader;
+
+/**
+ * @author Sindre Mehus
+ */
+public class RandomSongsParser extends MusicDirectoryEntryParser {
+
+ public RandomSongsParser(Context context) {
+ super(context);
+ }
+
+ public MusicDirectory parse(Reader reader, ProgressListener progressListener) throws Exception {
+ updateProgress(progressListener, R.string.parser_reading);
+ init(reader);
+
+ MusicDirectory dir = new MusicDirectory();
+ int eventType;
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = getElementName();
+ if ("song".equals(name)) {
+ dir.addChild(parseEntry(""));
+ } else if ("error".equals(name)) {
+ handleError();
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+ updateProgress(progressListener, R.string.parser_reading_done);
+
+ return dir;
+ }
+
+} \ No newline at end of file
diff --git a/src/github/daneren2005/dsub/service/parser/SearchResult2Parser.java b/src/github/daneren2005/dsub/service/parser/SearchResult2Parser.java
new file mode 100644
index 00000000..a0be07ac
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/parser/SearchResult2Parser.java
@@ -0,0 +1,75 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.service.parser;
+
+import android.content.Context;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.SearchResult;
+import github.daneren2005.dsub.domain.Artist;
+import github.daneren2005.dsub.util.ProgressListener;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.Reader;
+import java.util.List;
+import java.util.ArrayList;
+
+/**
+ * @author Sindre Mehus
+ */
+public class SearchResult2Parser extends MusicDirectoryEntryParser {
+
+ public SearchResult2Parser(Context context) {
+ super(context);
+ }
+
+ public SearchResult parse(Reader reader, ProgressListener progressListener) throws Exception {
+ updateProgress(progressListener, R.string.parser_reading);
+ init(reader);
+
+ List<Artist> artists = new ArrayList<Artist>();
+ List<MusicDirectory.Entry> albums = new ArrayList<MusicDirectory.Entry>();
+ List<MusicDirectory.Entry> songs = new ArrayList<MusicDirectory.Entry>();
+ int eventType;
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = getElementName();
+ if ("artist".equals(name)) {
+ Artist artist = new Artist();
+ artist.setId(get("id"));
+ artist.setName(get("name"));
+ artists.add(artist);
+ } else if ("album".equals(name)) {
+ albums.add(parseEntry(""));
+ } else if ("song".equals(name)) {
+ songs.add(parseEntry(""));
+ } else if ("error".equals(name)) {
+ handleError();
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+ updateProgress(progressListener, R.string.parser_reading_done);
+
+ return new SearchResult(artists, albums, songs);
+ }
+
+} \ No newline at end of file
diff --git a/src/github/daneren2005/dsub/service/parser/SearchResultParser.java b/src/github/daneren2005/dsub/service/parser/SearchResultParser.java
new file mode 100644
index 00000000..c8ef4031
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/parser/SearchResultParser.java
@@ -0,0 +1,67 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.service.parser;
+
+import android.content.Context;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.SearchResult;
+import github.daneren2005.dsub.domain.Artist;
+import github.daneren2005.dsub.util.ProgressListener;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.Reader;
+import java.util.Collections;
+import java.util.List;
+import java.util.ArrayList;
+
+/**
+ * @author Sindre Mehus
+ */
+public class SearchResultParser extends MusicDirectoryEntryParser {
+
+ public SearchResultParser(Context context) {
+ super(context);
+ }
+
+ public SearchResult parse(Reader reader, ProgressListener progressListener) throws Exception {
+ updateProgress(progressListener, R.string.parser_reading);
+ init(reader);
+
+ List<MusicDirectory.Entry> songs = new ArrayList<MusicDirectory.Entry>();
+ int eventType;
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = getElementName();
+ if ("match".equals(name)) {
+ songs.add(parseEntry(""));
+ } else if ("error".equals(name)) {
+ handleError();
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+ updateProgress(progressListener, R.string.parser_reading_done);
+
+ return new SearchResult(Collections.<Artist>emptyList(), Collections.<MusicDirectory.Entry>emptyList(), songs);
+ }
+
+}
diff --git a/src/github/daneren2005/dsub/service/parser/ShareParser.java b/src/github/daneren2005/dsub/service/parser/ShareParser.java
new file mode 100644
index 00000000..c317e799
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/parser/ShareParser.java
@@ -0,0 +1,77 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.service.parser;
+
+import android.content.Context;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.Share;
+import github.daneren2005.dsub.util.ProgressListener;
+import org.xmlpull.v1.XmlPullParser;
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author Joshua Bahnsen
+ */
+public class ShareParser extends MusicDirectoryEntryParser {
+
+ public ShareParser(Context context) {
+ super(context);
+ }
+
+ public List<Share> parse(Reader reader, ProgressListener progressListener) throws Exception {
+
+ updateProgress(progressListener, R.string.parser_reading);
+ init(reader);
+
+ List<Share> dir = new ArrayList<Share>();
+ Share share = null;
+ int eventType;
+
+ do {
+ eventType = nextParseEvent();
+
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = getElementName();
+
+ if ("share".equals(name)) {
+ share = new Share();
+ share.setCreated(get("created"));
+ share.setDescription(get("description"));
+ share.setExpires(get("expires"));
+ share.setId(get("id"));
+ share.setLastVisited(get("lastVisited"));
+ share.setUrl(get("url"));
+ share.setUsername(get("username"));
+ share.setVisitCount(getLong("visitCount"));
+ } else if ("entry".equals(name)) {
+ share.addEntry(parseEntry(null));
+ } else if ("error".equals(name)) {
+ handleError();
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+ updateProgress(progressListener, R.string.parser_reading_done);
+
+ return dir;
+ }
+} \ No newline at end of file
diff --git a/src/github/daneren2005/dsub/service/parser/StarredListParser.java b/src/github/daneren2005/dsub/service/parser/StarredListParser.java
new file mode 100644
index 00000000..fc4cd175
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/parser/StarredListParser.java
@@ -0,0 +1,64 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.service.parser;
+
+import android.content.Context;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.util.ProgressListener;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.Reader;
+
+/**
+ * @author Kurt Hardin
+ */
+public class StarredListParser extends MusicDirectoryEntryParser {
+
+ public StarredListParser(Context context) {
+ super(context);
+ }
+
+ public MusicDirectory parse(Reader reader, ProgressListener progressListener) throws Exception {
+
+ updateProgress(progressListener, R.string.parser_reading);
+ init(reader);
+
+ MusicDirectory dir = new MusicDirectory();
+ int eventType;
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = getElementName();
+ if ("album".equals(name) || "song".equals(name)) {
+ dir.addChild(parseEntry(""));
+ } else if("artist".equals(name)) {
+ dir.addChild(parseArtist());
+ } else if ("error".equals(name)) {
+ handleError();
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+ updateProgress(progressListener, R.string.parser_reading_done);
+
+ return dir;
+ }
+} \ No newline at end of file
diff --git a/src/github/daneren2005/dsub/service/parser/SubsonicRESTException.java b/src/github/daneren2005/dsub/service/parser/SubsonicRESTException.java
new file mode 100644
index 00000000..096597a1
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/parser/SubsonicRESTException.java
@@ -0,0 +1,19 @@
+package github.daneren2005.dsub.service.parser;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class SubsonicRESTException extends Exception {
+
+ private final int code;
+
+ public SubsonicRESTException(int code, String message) {
+ super(message);
+ this.code = code;
+ }
+
+ public int getCode() {
+ return code;
+ }
+}
diff --git a/src/github/daneren2005/dsub/service/parser/VersionParser.java b/src/github/daneren2005/dsub/service/parser/VersionParser.java
new file mode 100644
index 00000000..1b646206
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/parser/VersionParser.java
@@ -0,0 +1,47 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.service.parser;
+
+import github.daneren2005.dsub.domain.Version;
+
+import java.io.BufferedReader;
+import java.io.Reader;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * @author Sindre Mehus
+ */
+public class VersionParser {
+
+ public Version parse(Reader reader) throws Exception {
+
+ BufferedReader bufferedReader = new BufferedReader(reader);
+ Pattern pattern = Pattern.compile("SUBSONIC_ANDROID_VERSION_BEGIN(.*)SUBSONIC_ANDROID_VERSION_END");
+ String line = bufferedReader.readLine();
+ while (line != null) {
+ Matcher finalMatcher = pattern.matcher(line);
+ if (finalMatcher.find()) {
+ return new Version(finalMatcher.group(1));
+ }
+ line = bufferedReader.readLine();
+ }
+ return null;
+ }
+} \ No newline at end of file
diff --git a/src/github/daneren2005/dsub/service/ssl/SSLSocketFactory.java b/src/github/daneren2005/dsub/service/ssl/SSLSocketFactory.java
new file mode 100644
index 00000000..2ffed048
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/ssl/SSLSocketFactory.java
@@ -0,0 +1,497 @@
+/*
+ * ====================================================================
+ * 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 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.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.SecureRandom;
+import java.security.UnrecoverableKeyException;
+
+/**
+ * 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 {
+
+ public static final String TLS = "TLS";
+ public static final String SSL = "SSL";
+ public static final String SSLV2 = "SSLv2";
+
+ 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
+ return (SSLSocket) this.socketfactory.createSocket();
+ }
+
+ @SuppressWarnings("cast")
+ public Socket createSocket() throws IOException {
+ // the cast makes sure that the factory is working as expected
+ return (SSLSocket) this.socketfactory.createSocket();
+ }
+
+ /**
+ * @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);
+ }
+
+ 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
+ );
+ 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 {
+ return createLayeredSocket(socket, host, port, autoClose);
+ }
+
+}
diff --git a/src/github/daneren2005/dsub/service/ssl/TrustManagerDecorator.java b/src/github/daneren2005/dsub/service/ssl/TrustManagerDecorator.java
new file mode 100644
index 00000000..f2364368
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/ssl/TrustManagerDecorator.java
@@ -0,0 +1,65 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package github.daneren2005.dsub.service.ssl;
+
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+
+import javax.net.ssl.X509TrustManager;
+
+
+/**
+ * @since 4.1
+ */
+class TrustManagerDecorator implements X509TrustManager {
+
+ private final X509TrustManager trustManager;
+ private final TrustStrategy trustStrategy;
+
+ TrustManagerDecorator(final X509TrustManager trustManager, final TrustStrategy trustStrategy) {
+ super();
+ this.trustManager = trustManager;
+ this.trustStrategy = trustStrategy;
+ }
+
+ public void checkClientTrusted(
+ final X509Certificate[] chain, final String authType) throws CertificateException {
+ this.trustManager.checkClientTrusted(chain, authType);
+ }
+
+ public void checkServerTrusted(
+ final X509Certificate[] chain, final String authType) throws CertificateException {
+ if (!this.trustStrategy.isTrusted(chain, authType)) {
+ this.trustManager.checkServerTrusted(chain, authType);
+ }
+ }
+
+ public X509Certificate[] getAcceptedIssuers() {
+ return this.trustManager.getAcceptedIssuers();
+ }
+
+}
diff --git a/src/github/daneren2005/dsub/service/ssl/TrustSelfSignedStrategy.java b/src/github/daneren2005/dsub/service/ssl/TrustSelfSignedStrategy.java
new file mode 100644
index 00000000..637a8931
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/ssl/TrustSelfSignedStrategy.java
@@ -0,0 +1,44 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package github.daneren2005.dsub.service.ssl;
+
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+
+/**
+ * A trust strategy that accepts self-signed certificates as trusted. Verification of all other
+ * certificates is done by the trust manager configured in the SSL context.
+ *
+ * @since 4.1
+ */
+public class TrustSelfSignedStrategy implements TrustStrategy {
+
+ public boolean isTrusted(final X509Certificate[] chain, final String authType) throws CertificateException {
+ return true;
+ }
+
+}
diff --git a/src/github/daneren2005/dsub/service/ssl/TrustStrategy.java b/src/github/daneren2005/dsub/service/ssl/TrustStrategy.java
new file mode 100644
index 00000000..334a97c5
--- /dev/null
+++ b/src/github/daneren2005/dsub/service/ssl/TrustStrategy.java
@@ -0,0 +1,57 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package github.daneren2005.dsub.service.ssl;
+
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+
+/**
+ * A strategy to establish trustworthiness of certificates without consulting the trust manager
+ * configured in the actual SSL context. This interface can be used to override the standard
+ * JSSE certificate verification process.
+ *
+ * @since 4.1
+ */
+public interface TrustStrategy {
+
+ /**
+ * Determines whether the certificate chain can be trusted without consulting the trust manager
+ * configured in the actual SSL context. This method can be used to override the standard JSSE
+ * certificate verification process.
+ * <p>
+ * Please note that, if this method returns <code>false</code>, the trust manager configured
+ * in the actual SSL context can still clear the certificate as trusted.
+ *
+ * @param chain the peer certificate chain
+ * @param authType the authentication type based on the client certificate
+ * @return <code>true</code> if the certificate can be trusted without verification by
+ * the trust manager, <code>false</code> otherwise.
+ * @throws CertificateException thrown if the certificate is not trusted or invalid.
+ */
+ boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException;
+
+}
diff --git a/src/github/daneren2005/dsub/updates/Updater.java b/src/github/daneren2005/dsub/updates/Updater.java
new file mode 100644
index 00000000..60a17b67
--- /dev/null
+++ b/src/github/daneren2005/dsub/updates/Updater.java
@@ -0,0 +1,91 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.updates;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.AsyncTask;
+import android.util.Log;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.Util;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ *
+ * @author Scott
+ */
+public class Updater {
+ protected String TAG = Updater.class.getSimpleName();
+ protected int version;
+ protected Context context;
+
+ public Updater(int version) {
+ this.version = version;
+ }
+
+ public void checkUpdates(Context context) {
+ this.context = context;
+ List<Updater> updaters = new ArrayList<Updater>();
+ updaters.add(new Updater403());
+
+ SharedPreferences prefs = Util.getPreferences(context);
+ int lastVersion = prefs.getInt(Constants.LAST_VERSION, 0);
+ if(lastVersion == 0) {
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putInt(Constants.LAST_VERSION, version);
+ editor.commit();
+ }
+ else if(version > lastVersion) {
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putInt(Constants.LAST_VERSION, version);
+ editor.commit();
+
+ Log.i(TAG, "Updating from version " + lastVersion + " to " + version);
+ for(Updater updater: updaters) {
+ if(updater.shouldUpdate(lastVersion)) {
+ new BackgroundUpdate().execute(updater);
+ }
+ }
+ }
+ }
+
+ public String getName() {
+ return this.TAG;
+ }
+
+ private class BackgroundUpdate extends AsyncTask<Updater, Void, Void> {
+ @Override
+ protected Void doInBackground(Updater... params) {
+ try {
+ params[0].update(context);
+ } catch(Exception e) {
+ Log.w(TAG, "Failed to run update for " + params[0].getName());
+ }
+ return null;
+ }
+ }
+
+ public boolean shouldUpdate(int version) {
+ return this.version > version;
+ }
+ public void update(Context context) {
+
+ }
+}
diff --git a/src/github/daneren2005/dsub/updates/Updater403.java b/src/github/daneren2005/dsub/updates/Updater403.java
new file mode 100644
index 00000000..17947ce5
--- /dev/null
+++ b/src/github/daneren2005/dsub/updates/Updater403.java
@@ -0,0 +1,58 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.updates;
+
+import android.content.Context;
+import android.util.Log;
+import github.daneren2005.dsub.updates.Updater;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.FileUtil;
+import java.io.File;
+
+/**
+ *
+ * @author Scott
+ */
+public class Updater403 extends Updater {
+ public Updater403() {
+ super(403);
+ TAG = Updater403.class.getSimpleName();
+ }
+
+ @Override
+ public void update(Context context) {
+ // Rename cover.jpeg to cover.jpg
+ Log.i(TAG, "Running Updater403: updating cover.jpg to albumart.jpg");
+ File dir = FileUtil.getMusicDirectory(context);
+ if(dir != null) {
+ moveArt(dir);
+ }
+ }
+
+ private void moveArt(File dir) {
+ for(File file: dir.listFiles()) {
+ if(file.isDirectory()) {
+ moveArt(file);
+ } else if("cover.jpg".equals(file.getName()) || "cover.jpeg".equals(file.getName())) {
+ File renamed = new File(dir, Constants.ALBUM_ART_FILE);
+ file.renameTo(renamed);
+ }
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/util/BackgroundTask.java b/src/github/daneren2005/dsub/util/BackgroundTask.java
new file mode 100644
index 00000000..547bbd1e
--- /dev/null
+++ b/src/github/daneren2005/dsub/util/BackgroundTask.java
@@ -0,0 +1,97 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.util;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.app.Activity;
+import android.os.Handler;
+import android.util.Log;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.view.ErrorDialog;
+
+/**
+ * @author Sindre Mehus
+ */
+public abstract class BackgroundTask<T> implements ProgressListener {
+
+ private static final String TAG = BackgroundTask.class.getSimpleName();
+ private final Activity activity;
+ private final Handler handler;
+
+ public BackgroundTask(Activity activity) {
+ this.activity = activity;
+ handler = new Handler();
+ }
+
+ protected Activity getActivity() {
+ return activity;
+ }
+
+ protected Handler getHandler() {
+ return handler;
+ }
+
+ public abstract void execute();
+
+ protected abstract T doInBackground() throws Throwable;
+
+ protected abstract void done(T result);
+
+ protected void error(Throwable error) {
+ Log.w(TAG, "Got exception: " + error, error);
+ new ErrorDialog(activity, getErrorMessage(error), true);
+ }
+
+ protected String getErrorMessage(Throwable error) {
+
+ if (error instanceof IOException && !Util.isNetworkConnected(activity)) {
+ return activity.getResources().getString(R.string.background_task_no_network);
+ }
+
+ if (error instanceof FileNotFoundException) {
+ return activity.getResources().getString(R.string.background_task_not_found);
+ }
+
+ if (error instanceof IOException) {
+ return activity.getResources().getString(R.string.background_task_network_error);
+ }
+
+ if (error instanceof XmlPullParserException) {
+ return activity.getResources().getString(R.string.background_task_parse_error);
+ }
+
+ String message = error.getMessage();
+ if (message != null) {
+ return message;
+ }
+ return error.getClass().getSimpleName();
+ }
+
+ @Override
+ public abstract void updateProgress(final String message);
+
+ @Override
+ public void updateProgress(int messageId) {
+ updateProgress(activity.getResources().getString(messageId));
+ }
+} \ No newline at end of file
diff --git a/src/github/daneren2005/dsub/util/CacheCleaner.java b/src/github/daneren2005/dsub/util/CacheCleaner.java
new file mode 100644
index 00000000..62204c76
--- /dev/null
+++ b/src/github/daneren2005/dsub/util/CacheCleaner.java
@@ -0,0 +1,239 @@
+package github.daneren2005.dsub.util;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import android.content.Context;
+import android.os.AsyncTask;
+import android.util.Log;
+import android.os.StatFs;
+import github.daneren2005.dsub.domain.Playlist;
+import github.daneren2005.dsub.service.DownloadFile;
+import github.daneren2005.dsub.service.DownloadService;
+import java.util.*;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class CacheCleaner {
+
+ private static final String TAG = CacheCleaner.class.getSimpleName();
+ private static final long MIN_FREE_SPACE = 500 * 1024L * 1024L;
+
+ private final Context context;
+ private final DownloadService downloadService;
+
+ public CacheCleaner(Context context, DownloadService downloadService) {
+ this.context = context;
+ this.downloadService = downloadService;
+ }
+
+ public void clean() {
+ new BackgroundCleanup().execute();
+ }
+ public void cleanSpace() {
+ new BackgroundSpaceCleanup().execute();
+ }
+ public void cleanPlaylists(List<Playlist> playlists) {
+ new BackgroundPlaylistsCleanup().execute(playlists);
+ }
+
+ private void deleteEmptyDirs(List<File> dirs, Set<File> undeletable) {
+ for (File dir : dirs) {
+ if (undeletable.contains(dir)) {
+ continue;
+ }
+
+ File[] children = dir.listFiles();
+
+ // No songs left in the folder
+ if(children.length == 1 && children[0].getPath().equals(FileUtil.getAlbumArtFile(dir).getPath())) {
+ Util.delete(children[0]);
+ children = dir.listFiles();
+ }
+
+ // Delete empty directory
+ if (children.length == 0) {
+ Util.delete(dir);
+ }
+ }
+ }
+
+ private long getMinimumDelete(List<File> files) {
+ if(files.size() == 0) {
+ return 0L;
+ }
+
+ long cacheSizeBytes = Util.getCacheSizeMB(context) * 1024L * 1024L;
+
+ long bytesUsedBySubsonic = 0L;
+ for (File file : files) {
+ bytesUsedBySubsonic += file.length();
+ }
+
+ // Ensure that file system is not more than 95% full.
+ StatFs stat = new StatFs(files.get(0).getPath());
+ long bytesTotalFs = (long) stat.getBlockCount() * (long) stat.getBlockSize();
+ long bytesAvailableFs = (long) stat.getAvailableBlocks() * (long) stat.getBlockSize();
+ long bytesUsedFs = bytesTotalFs - bytesAvailableFs;
+ long minFsAvailability = bytesTotalFs - MIN_FREE_SPACE;
+
+ long bytesToDeleteCacheLimit = Math.max(bytesUsedBySubsonic - cacheSizeBytes, 0L);
+ long bytesToDeleteFsLimit = Math.max(bytesUsedFs - minFsAvailability, 0L);
+ long bytesToDelete = Math.max(bytesToDeleteCacheLimit, bytesToDeleteFsLimit);
+
+ Log.i(TAG, "File system : " + Util.formatBytes(bytesAvailableFs) + " of " + Util.formatBytes(bytesTotalFs) + " available");
+ Log.i(TAG, "Cache limit : " + Util.formatBytes(cacheSizeBytes));
+ Log.i(TAG, "Cache size before : " + Util.formatBytes(bytesUsedBySubsonic));
+ Log.i(TAG, "Minimum to delete : " + Util.formatBytes(bytesToDelete));
+
+ return bytesToDelete;
+ }
+
+ private void deleteFiles(List<File> files, Set<File> undeletable, long bytesToDelete, boolean deletePartials) {
+ if (files.isEmpty()) {
+ return;
+ }
+
+ long bytesDeleted = 0L;
+ for (File file : files) {
+ if(!deletePartials && bytesDeleted > bytesToDelete) break;
+
+ if (bytesToDelete > bytesDeleted || (deletePartials && (file.getName().endsWith(".partial") || file.getName().contains(".partial.")))) {
+ if (!undeletable.contains(file) && !file.getName().equals(Constants.ALBUM_ART_FILE)) {
+ long size = file.length();
+ if (Util.delete(file)) {
+ bytesDeleted += size;
+ }
+ }
+ }
+ }
+
+ Log.i(TAG, "Deleted : " + Util.formatBytes(bytesDeleted));
+ }
+
+ private void findCandidatesForDeletion(File file, List<File> files, List<File> dirs) {
+ if (file.isFile()) {
+ String name = file.getName();
+ boolean isCacheFile = name.endsWith(".partial") || name.contains(".partial.") || name.endsWith(".complete") || name.contains(".complete.");
+ if (isCacheFile) {
+ files.add(file);
+ }
+ } else {
+ // Depth-first
+ for (File child : FileUtil.listFiles(file)) {
+ findCandidatesForDeletion(child, files, dirs);
+ }
+ dirs.add(file);
+ }
+ }
+
+ private void sortByAscendingModificationTime(List<File> files) {
+ Collections.sort(files, new Comparator<File>() {
+ @Override
+ public int compare(File a, File b) {
+ if (a.lastModified() < b.lastModified()) {
+ return -1;
+ }
+ if (a.lastModified() > b.lastModified()) {
+ return 1;
+ }
+ return 0;
+ }
+ });
+ }
+
+ private Set<File> findUndeletableFiles() {
+ Set<File> undeletable = new HashSet<File>(5);
+
+ for (DownloadFile downloadFile : downloadService.getDownloads()) {
+ undeletable.add(downloadFile.getPartialFile());
+ undeletable.add(downloadFile.getCompleteFile());
+ }
+
+ undeletable.add(FileUtil.getMusicDirectory(context));
+ return undeletable;
+ }
+
+ private class BackgroundCleanup extends AsyncTask<Void, Void, Void> {
+ @Override
+ protected Void doInBackground(Void... params) {
+ if (downloadService == null) {
+ Log.e(TAG, "DownloadService not set. Aborting cache cleaning.");
+ return null;
+ }
+
+ try {
+ List<File> files = new ArrayList<File>();
+ List<File> dirs = new ArrayList<File>();
+
+ findCandidatesForDeletion(FileUtil.getMusicDirectory(context), files, dirs);
+ sortByAscendingModificationTime(files);
+
+ Set<File> undeletable = findUndeletableFiles();
+
+ deleteFiles(files, undeletable, getMinimumDelete(files), true);
+ deleteEmptyDirs(dirs, undeletable);
+ } catch (RuntimeException x) {
+ Log.e(TAG, "Error in cache cleaning.", x);
+ }
+
+ return null;
+ }
+ }
+
+ private class BackgroundSpaceCleanup extends AsyncTask<Void, Void, Void> {
+ @Override
+ protected Void doInBackground(Void... params) {
+ if (downloadService == null) {
+ Log.e(TAG, "DownloadService not set. Aborting cache cleaning.");
+ return null;
+ }
+
+ try {
+ List<File> files = new ArrayList<File>();
+ List<File> dirs = new ArrayList<File>();
+ findCandidatesForDeletion(FileUtil.getMusicDirectory(context), files, dirs);
+
+ long bytesToDelete = getMinimumDelete(files);
+ if(bytesToDelete > 0L) {
+ sortByAscendingModificationTime(files);
+ Set<File> undeletable = findUndeletableFiles();
+ deleteFiles(files, undeletable, bytesToDelete, false);
+ }
+ } catch (RuntimeException x) {
+ Log.e(TAG, "Error in cache cleaning.", x);
+ }
+
+ return null;
+ }
+ }
+
+ private class BackgroundPlaylistsCleanup extends AsyncTask<List<Playlist>, Void, Void> {
+ @Override
+ protected Void doInBackground(List<Playlist>... params) {
+ try {
+ String server = Util.getServerName(context);
+ SortedSet<File> playlistFiles = FileUtil.listFiles(FileUtil.getPlaylistDirectory(server));
+ List<Playlist> playlists = params[0];
+ for (Playlist playlist : playlists) {
+ playlistFiles.remove(FileUtil.getPlaylistFile(server, playlist.getName()));
+ }
+
+ for(File playlist : playlistFiles) {
+ playlist.delete();
+ }
+ } catch (RuntimeException x) {
+ Log.e(TAG, "Error in playlist cache cleaning.", x);
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/util/CancellableTask.java b/src/github/daneren2005/dsub/util/CancellableTask.java
new file mode 100644
index 00000000..933bc0c1
--- /dev/null
+++ b/src/github/daneren2005/dsub/util/CancellableTask.java
@@ -0,0 +1,87 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.util;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+import android.util.Log;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public abstract class CancellableTask {
+
+ private static final String TAG = CancellableTask.class.getSimpleName();
+
+ private final AtomicBoolean running = new AtomicBoolean(false);
+ private final AtomicBoolean cancelled = new AtomicBoolean(false);
+ private final AtomicReference<Thread> thread = new AtomicReference<Thread>();
+ private final AtomicReference<OnCancelListener> cancelListener = new AtomicReference<OnCancelListener>();
+
+ public void cancel() {
+ Log.i(TAG, "Cancelling " + CancellableTask.this);
+ cancelled.set(true);
+
+ OnCancelListener listener = cancelListener.get();
+ if (listener != null) {
+ try {
+ listener.onCancel();
+ } catch (Throwable x) {
+ Log.w(TAG, "Error when invoking OnCancelListener.", x);
+ }
+ }
+ }
+
+ public boolean isCancelled() {
+ return cancelled.get();
+ }
+
+ public void setOnCancelListener(OnCancelListener listener) {
+ cancelListener.set(listener);
+ }
+
+ public boolean isRunning() {
+ return running.get();
+ }
+
+ public abstract void execute();
+
+ public void start() {
+ thread.set(new Thread() {
+ @Override
+ public void run() {
+ running.set(true);
+ Log.i(TAG, "Starting thread for " + CancellableTask.this);
+ try {
+ execute();
+ } finally {
+ running.set(false);
+ Log.i(TAG, "Stopping thread for " + CancellableTask.this);
+ }
+ }
+ });
+ thread.get().start();
+ }
+
+ public static interface OnCancelListener {
+ void onCancel();
+ }
+}
diff --git a/src/github/daneren2005/dsub/util/Constants.java b/src/github/daneren2005/dsub/util/Constants.java
new file mode 100644
index 00000000..550749b9
--- /dev/null
+++ b/src/github/daneren2005/dsub/util/Constants.java
@@ -0,0 +1,149 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.util;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public final class Constants {
+
+ // Character encoding used throughout.
+ public static final String UTF_8 = "UTF-8";
+
+ // REST protocol version and client ID.
+ // Note: Keep it as low as possible to maintain compatibility with older servers.
+ public static final String REST_PROTOCOL_VERSION = "1.2.0";
+ public static final String REST_CLIENT_ID = "DSub";
+ public static final String LAST_VERSION = "subsonic.version";
+
+ // Names for intent extras.
+ public static final String INTENT_EXTRA_NAME_ID = "subsonic.id";
+ public static final String INTENT_EXTRA_NAME_NAME = "subsonic.name";
+ public static final String INTENT_EXTRA_NAME_PARENT_ID = "subsonic.parent_id";
+ public static final String INTENT_EXTRA_NAME_PARENT_NAME = "subsonic.parent_name";
+ public static final String INTENT_EXTRA_NAME_ARTIST = "subsonic.artist";
+ public static final String INTENT_EXTRA_NAME_TITLE = "subsonic.title";
+ public static final String INTENT_EXTRA_NAME_AUTOPLAY = "subsonic.playall";
+ public static final String INTENT_EXTRA_NAME_ERROR = "subsonic.error";
+ public static final String INTENT_EXTRA_NAME_QUERY = "subsonic.query";
+ public static final String INTENT_EXTRA_NAME_PLAYLIST_ID = "subsonic.playlist.id";
+ public static final String INTENT_EXTRA_NAME_PLAYLIST_NAME = "subsonic.playlist.name";
+ public static final String INTENT_EXTRA_NAME_ALBUM_LIST_TYPE = "subsonic.albumlisttype";
+ public static final String INTENT_EXTRA_NAME_ALBUM_LIST_EXTRA = "subsonic.albumlistextra";
+ public static final String INTENT_EXTRA_NAME_ALBUM_LIST_SIZE = "subsonic.albumlistsize";
+ public static final String INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET = "subsonic.albumlistoffset";
+ public static final String INTENT_EXTRA_NAME_SHUFFLE = "subsonic.shuffle";
+ public static final String INTENT_EXTRA_NAME_REFRESH = "subsonic.refresh";
+ public static final String INTENT_EXTRA_REQUEST_SEARCH = "subsonic.requestsearch";
+ public static final String INTENT_EXTRA_NAME_EXIT = "subsonic.exit" ;
+ public static final String INTENT_EXTRA_NAME_DOWNLOAD = "subsonic.download";
+ public static final String INTENT_EXTRA_VIEW_ALBUM = "subsonic.view_album";
+ public static final String INTENT_EXTRA_NAME_PODCAST_ID = "subsonic.podcast.id";
+ public static final String INTENT_EXTRA_NAME_PODCAST_NAME = "subsonic.podcast.name";
+ public static final String INTENT_EXTRA_NAME_PODCAST_DESCRIPTION = "subsonic.podcast.description";
+
+ // Notification IDs.
+ public static final int NOTIFICATION_ID_PLAYING = 100;
+ public static final int NOTIFICATION_ID_ERROR = 101;
+
+ // Preferences keys.
+ public static final String PREFERENCES_KEY_SERVER_KEY = "server";
+ public static final String PREFERENCES_KEY_SERVER_COUNT = "serverCount";
+ public static final String PREFERENCES_KEY_SERVER_ADD = "serverAdd";
+ public static final String PREFERENCES_KEY_SERVER_REMOVE = "serverRemove";
+ public static final String PREFERENCES_KEY_SERVER_INSTANCE = "serverInstanceId";
+ public static final String PREFERENCES_KEY_SERVER_NAME = "serverName";
+ public static final String PREFERENCES_KEY_SERVER_URL = "serverUrl";
+ public static final String PREFERENCES_KEY_SERVER_VERSION = "serverVersion";
+ public static final String PREFERENCES_KEY_TEST_CONNECTION = "serverTestConnection";
+ public static final String PREFERENCES_KEY_OPEN_BROWSER = "openBrowser";
+ public static final String PREFERENCES_KEY_MUSIC_FOLDER_ID = "musicFolderId";
+ public static final String PREFERENCES_KEY_USERNAME = "username";
+ public static final String PREFERENCES_KEY_PASSWORD = "password";
+ public static final String PREFERENCES_KEY_INSTALL_TIME = "installTime";
+ public static final String PREFERENCES_KEY_THEME = "theme";
+ public static final String PREFERENCES_KEY_DISPLAY_TRACK = "displayTrack";
+ public static final String PREFERENCES_KEY_MAX_BITRATE_WIFI = "maxBitrateWifi";
+ public static final String PREFERENCES_KEY_MAX_BITRATE_MOBILE = "maxBitrateMobile";
+ public static final String PREFERENCES_KEY_MAX_VIDEO_BITRATE_WIFI = "maxVideoBitrateWifi";
+ public static final String PREFERENCES_KEY_MAX_VIDEO_BITRATE_MOBILE = "maxVideoBitrateMobile";
+ public static final String PREFERENCES_KEY_NETWORK_TIMEOUT = "networkTimeout";
+ public static final String PREFERENCES_KEY_CACHE_SIZE = "cacheSize";
+ public static final String PREFERENCES_KEY_CACHE_LOCATION = "cacheLocation";
+ public static final String PREFERENCES_KEY_PRELOAD_COUNT_WIFI = "preloadCountWifi";
+ public static final String PREFERENCES_KEY_PRELOAD_COUNT_MOBILE = "preloadCountMobile";
+ public static final String PREFERENCES_KEY_HIDE_MEDIA = "hideMedia";
+ public static final String PREFERENCES_KEY_MEDIA_BUTTONS = "mediaButtons";
+ public static final String PREFERENCES_KEY_SCREEN_LIT_ON_DOWNLOAD = "screenLitOnDownload";
+ public static final String PREFERENCES_KEY_SCROBBLE = "scrobble";
+ public static final String PREFERENCES_KEY_REPEAT_MODE = "repeatMode";
+ public static final String PREFERENCES_KEY_WIFI_REQUIRED_FOR_DOWNLOAD = "wifiRequiredForDownload";
+ public static final String PREFERENCES_KEY_RANDOM_SIZE = "randomSize";
+ public static final String PREFERENCES_KEY_SLEEP_TIMER = "sleepTimer";
+ public static final String PREFERENCES_KEY_SLEEP_TIMER_DURATION = "sleepTimerDuration";
+ public static final String PREFERENCES_KEY_OFFLINE = "offline";
+ public static final String PREFERENCES_KEY_TEMP_LOSS = "tempLoss";
+ public static final String PREFERENCES_KEY_SHUFFLE_START_YEAR = "startYear";
+ public static final String PREFERENCES_KEY_SHUFFLE_END_YEAR = "endYear";
+ public static final String PREFERENCES_KEY_SHUFFLE_GENRE = "genre";
+ public static final String PREFERENCES_KEY_KEEP_SCREEN_ON = "keepScreenOn";
+ public static final String PREFERENCES_KEY_BUFFER_LENGTH = "bufferLength";
+ public static final String PREFERENCES_EQUALIZER_ON = "equalizerOn";
+ public static final String PREFERENCES_EQUALIZER_SETTINGS = "equalizerSettings";
+ public static final String PREFERENCES_KEY_PERSISTENT_NOTIFICATION = "persistentNotification";
+ public static final String PREFERENCES_KEY_GAPLESS_PLAYBACK = "gaplessPlayback";
+ public static final String PREFERENCES_KEY_SHUFFLE_MODE = "shuffleMode";
+ public static final String PREFERENCES_KEY_CHAT_REFRESH = "chatRefreshRate";
+ public static final String PREFERENCES_KEY_CHAT_ENABLED = "chatEnabled";
+ public static final String PREFERENCES_KEY_VIDEO_PLAYER = "videoPlayer";
+
+ public static final String OFFLINE_SCROBBLE_COUNT = "scrobbleCount";
+ public static final String OFFLINE_SCROBBLE_ID = "scrobbleID";
+ public static final String OFFLINE_SCROBBLE_SEARCH = "scrobbleTitle";
+ public static final String OFFLINE_SCROBBLE_TIME = "scrobbleTime";
+ public static final String OFFLINE_STAR_COUNT = "starCount";
+ public static final String OFFLINE_STAR_ID = "starID";
+ public static final String OFFLINE_STAR_SEARCH = "starTitle";
+ public static final String OFFLINE_STAR_SETTING = "starSetting";
+
+ public static final String CACHE_KEY_IGNORE = "ignoreArticles";
+
+ public static final String MAIN_BACK_STACK = "backStackIds";
+ public static final String MAIN_BACK_STACK_SIZE = "backStackIdsSize";
+ public static final String MAIN_BACK_STACK_TABS = "backStackTabs";
+ public static final String MAIN_BACK_STACK_POSITION = "backStackPosition";
+ public static final String FRAGMENT_ID = "fragmentId";
+
+ // Name of the preferences file.
+ public static final String PREFERENCES_FILE_NAME = "github.daneren2005.dsub_preferences";
+ public static final String OFFLINE_SYNC_NAME = "github.daneren2005.dsub.offline";
+ public static final String OFFLINE_SYNC_DEFAULT = "syncDefaults";
+
+ // Number of free trial days for non-licensed servers.
+ public static final int FREE_TRIAL_DAYS = 30;
+
+ // URL for project donations.
+ public static final String DONATION_URL = "http://subsonic.org/pages/android-donation.jsp";
+
+ public static final String ALBUM_ART_FILE = "albumart.jpg";
+
+ private Constants() {
+ }
+}
diff --git a/src/github/daneren2005/dsub/util/FileUtil.java b/src/github/daneren2005/dsub/util/FileUtil.java
new file mode 100644
index 00000000..34bc82bd
--- /dev/null
+++ b/src/github/daneren2005/dsub/util/FileUtil.java
@@ -0,0 +1,396 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.util;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.Iterator;
+import java.util.List;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.os.Environment;
+import android.util.Log;
+import github.daneren2005.dsub.domain.Artist;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.PodcastChannel;
+import github.daneren2005.dsub.domain.PodcastEpisode;
+
+/**
+ * @author Sindre Mehus
+ */
+public class FileUtil {
+
+ private static final String TAG = FileUtil.class.getSimpleName();
+ private static final String[] FILE_SYSTEM_UNSAFE = {"/", "\\", "..", ":", "\"", "?", "*", "<", ">", "|"};
+ private static final String[] FILE_SYSTEM_UNSAFE_DIR = {"\\", "..", ":", "\"", "?", "*", "<", ">", "|"};
+ private static final List<String> MUSIC_FILE_EXTENSIONS = Arrays.asList("mp3", "ogg", "aac", "flac", "m4a", "wav", "wma");
+ private static final List<String> VIDEO_FILE_EXTENSIONS = Arrays.asList("flv", "mp4", "m4v", "wmv", "avi", "mov", "mpg", "mkv");
+ private static final List<String> PLAYLIST_FILE_EXTENSIONS = Arrays.asList("m3u");
+ private static final File DEFAULT_MUSIC_DIR = createDirectory("music");
+
+ public static File getAnySong(Context context) {
+ File dir = getMusicDirectory(context);
+ return getAnySong(context, dir);
+ }
+ private static File getAnySong(Context context, File dir) {
+ for(File file: dir.listFiles()) {
+ if(file.isDirectory()) {
+ return getAnySong(context, file);
+ }
+
+ String extension = getExtension(file.getName());
+ if(MUSIC_FILE_EXTENSIONS.contains(extension)) {
+ return file;
+ }
+ }
+
+ return null;
+ }
+
+ public static File getSongFile(Context context, MusicDirectory.Entry song) {
+ File dir = getAlbumDirectory(context, song);
+
+ StringBuilder fileName = new StringBuilder();
+ Integer track = song.getTrack();
+ if (track != null) {
+ if (track < 10) {
+ fileName.append("0");
+ }
+ fileName.append(track).append("-");
+ }
+
+ fileName.append(fileSystemSafe(song.getTitle())).append(".");
+
+ if (song.getTranscodedSuffix() != null) {
+ fileName.append(song.getTranscodedSuffix());
+ } else {
+ fileName.append(song.getSuffix());
+ }
+
+ return new File(dir, fileName.toString());
+ }
+
+ public static File getPlaylistFile(String server, String name) {
+ File playlistDir = getPlaylistDirectory(server);
+ return new File(playlistDir, fileSystemSafe(name) + ".m3u");
+ }
+ public static File getPlaylistDirectory() {
+ File playlistDir = new File(getSubsonicDirectory(), "playlists");
+ ensureDirectoryExistsAndIsReadWritable(playlistDir);
+ return playlistDir;
+ }
+ public static File getPlaylistDirectory(String server) {
+ File playlistDir = new File(getPlaylistDirectory(), server);
+ ensureDirectoryExistsAndIsReadWritable(playlistDir);
+ return playlistDir;
+ }
+
+ public static File getAlbumArtFile(Context context, MusicDirectory.Entry entry) {
+ File albumDir = getAlbumDirectory(context, entry);
+ return getAlbumArtFile(albumDir);
+ }
+
+ public static File getAlbumArtFile(File albumDir) {
+ return new File(albumDir, Constants.ALBUM_ART_FILE);
+ }
+
+ public static Bitmap getAlbumArtBitmap(Context context, MusicDirectory.Entry entry, int size) {
+ File albumArtFile = getAlbumArtFile(context, entry);
+ if (albumArtFile.exists()) {
+ Bitmap bitmap = BitmapFactory.decodeFile(albumArtFile.getPath());
+ return (bitmap == null) ? null : Bitmap.createScaledBitmap(bitmap, size, size, true);
+ }
+ return null;
+ }
+
+ public static File getArtistDirectory(Context context, Artist artist) {
+ File dir = new File(getMusicDirectory(context).getPath() + "/" + fileSystemSafe(artist.getName()));
+ return dir;
+ }
+ public static File getArtistDirectory(Context context, MusicDirectory.Entry artist) {
+ File dir = new File(getMusicDirectory(context).getPath() + "/" + fileSystemSafe(artist.getTitle()));
+ return dir;
+ }
+
+ public static File getAlbumDirectory(Context context, MusicDirectory.Entry entry) {
+ File dir;
+ if (entry.getPath() != null) {
+ File f = new File(fileSystemSafeDir(entry.getPath()));
+ dir = new File(getMusicDirectory(context).getPath() + "/" + (entry.isDirectory() ? f.getPath() : f.getParent()));
+ } else {
+ String artist = fileSystemSafe(entry.getArtist());
+ String album = fileSystemSafe(entry.getAlbum());
+ if("unnamed".equals(album)) {
+ album = fileSystemSafe(entry.getTitle());
+ }
+ dir = new File(getMusicDirectory(context).getPath() + "/" + artist + "/" + album);
+ }
+ return dir;
+ }
+
+ public static String getPodcastPath(Context context, PodcastEpisode episode) {
+ return fileSystemSafe(episode.getArtist()) + "/" + fileSystemSafe(episode.getTitle());
+ }
+ public static File getPodcastFile(Context context, String server) {
+ File dir = getPodcastDirectory(context);
+ return new File(dir.getPath() + "/" + fileSystemSafe(server));
+ }
+ public static File getPodcastDirectory(Context context) {
+ File dir = new File(getSubsonicDirectory(), "podcasts");
+ ensureDirectoryExistsAndIsReadWritable(dir);
+ return dir;
+ }
+ public static File getPodcastDirectory(Context context, PodcastChannel channel) {
+ File dir = new File(getMusicDirectory(context).getPath() + "/" + fileSystemSafe(channel.getName()));
+ return dir;
+ }
+ public static File getPodcastDirectory(Context context, String channel) {
+ File dir = new File(getMusicDirectory(context).getPath() + "/" + fileSystemSafe(channel));
+ return dir;
+ }
+
+ public static void createDirectoryForParent(File file) {
+ File dir = file.getParentFile();
+ if (!dir.exists()) {
+ if (!dir.mkdirs()) {
+ Log.e(TAG, "Failed to create directory " + dir);
+ }
+ }
+ }
+
+ private static File createDirectory(String name) {
+ File dir = new File(getSubsonicDirectory(), name);
+ if (!dir.exists() && !dir.mkdirs()) {
+ Log.e(TAG, "Failed to create " + name);
+ }
+ return dir;
+ }
+
+ public static File getSubsonicDirectory() {
+ return new File(Environment.getExternalStorageDirectory(), "subsonic");
+ }
+
+ public static File getDefaultMusicDirectory() {
+ return DEFAULT_MUSIC_DIR;
+ }
+
+ public static File getMusicDirectory(Context context) {
+ String path = Util.getPreferences(context).getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, DEFAULT_MUSIC_DIR.getPath());
+ File dir = new File(path);
+ return ensureDirectoryExistsAndIsReadWritable(dir) ? dir : getDefaultMusicDirectory();
+ }
+ public static boolean deleteMusicDirectory(Context context) {
+ File musicDirectory = FileUtil.getMusicDirectory(context);
+ return Util.recursiveDelete(musicDirectory);
+ }
+
+ public static boolean ensureDirectoryExistsAndIsReadWritable(File dir) {
+ if (dir == null) {
+ return false;
+ }
+
+ if (dir.exists()) {
+ if (!dir.isDirectory()) {
+ Log.w(TAG, dir + " exists but is not a directory.");
+ return false;
+ }
+ } else {
+ if (dir.mkdirs()) {
+ Log.i(TAG, "Created directory " + dir);
+ } else {
+ Log.w(TAG, "Failed to create directory " + dir);
+ return false;
+ }
+ }
+
+ if (!dir.canRead()) {
+ Log.w(TAG, "No read permission for directory " + dir);
+ return false;
+ }
+
+ if (!dir.canWrite()) {
+ Log.w(TAG, "No write permission for directory " + dir);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Makes a given filename safe by replacing special characters like slashes ("/" and "\")
+ * with dashes ("-").
+ *
+ * @param filename The filename in question.
+ * @return The filename with special characters replaced by hyphens.
+ */
+ private static String fileSystemSafe(String filename) {
+ if (filename == null || filename.trim().length() == 0) {
+ return "unnamed";
+ }
+
+ for (String s : FILE_SYSTEM_UNSAFE) {
+ filename = filename.replace(s, "-");
+ }
+ return filename;
+ }
+
+ /**
+ * Makes a given filename safe by replacing special characters like colons (":")
+ * with dashes ("-").
+ *
+ * @param path The path of the directory in question.
+ * @return The the directory name with special characters replaced by hyphens.
+ */
+ private static String fileSystemSafeDir(String path) {
+ if (path == null || path.trim().length() == 0) {
+ return "";
+ }
+
+ for (String s : FILE_SYSTEM_UNSAFE_DIR) {
+ path = path.replace(s, "-");
+ }
+ return path;
+ }
+
+ /**
+ * Similar to {@link File#listFiles()}, but returns a sorted set.
+ * Never returns {@code null}, instead a warning is logged, and an empty set is returned.
+ */
+ public static SortedSet<File> listFiles(File dir) {
+ File[] files = dir.listFiles();
+ if (files == null) {
+ Log.w(TAG, "Failed to list children for " + dir.getPath());
+ return new TreeSet<File>();
+ }
+
+ return new TreeSet<File>(Arrays.asList(files));
+ }
+
+ public static SortedSet<File> listMediaFiles(File dir) {
+ SortedSet<File> files = listFiles(dir);
+ Iterator<File> iterator = files.iterator();
+ while (iterator.hasNext()) {
+ File file = iterator.next();
+ if (!file.isDirectory() && !isMediaFile(file)) {
+ iterator.remove();
+ }
+ }
+ return files;
+ }
+
+ private static boolean isMediaFile(File file) {
+ String extension = getExtension(file.getName());
+ return MUSIC_FILE_EXTENSIONS.contains(extension) || VIDEO_FILE_EXTENSIONS.contains(extension);
+ }
+
+ public static boolean isMusicFile(File file) {
+ String extension = getExtension(file.getName());
+ return MUSIC_FILE_EXTENSIONS.contains(extension);
+ }
+ public static boolean isVideoFile(File file) {
+ String extension = getExtension(file.getName());
+ return VIDEO_FILE_EXTENSIONS.contains(extension);
+ }
+
+ public static boolean isPlaylistFile(File file) {
+ String extension = getExtension(file.getName());
+ return PLAYLIST_FILE_EXTENSIONS.contains(extension);
+ }
+
+ /**
+ * Returns the extension (the substring after the last dot) of the given file. The dot
+ * is not included in the returned extension.
+ *
+ * @param name The filename in question.
+ * @return The extension, or an empty string if no extension is found.
+ */
+ public static String getExtension(String name) {
+ int index = name.lastIndexOf('.');
+ return index == -1 ? "" : name.substring(index + 1).toLowerCase();
+ }
+
+ /**
+ * Returns the base name (the substring before the last dot) of the given file. The dot
+ * is not included in the returned basename.
+ *
+ * @param name The filename in question.
+ * @return The base name, or an empty string if no basename is found.
+ */
+ public static String getBaseName(String name) {
+ int index = name.lastIndexOf('.');
+ return index == -1 ? name : name.substring(0, index);
+ }
+
+ public static long getUsedSize(Context context, File file) {
+ long size = 0L;
+
+ if(file.isFile()) {
+ return file.length();
+ } else {
+ for (File child : FileUtil.listFiles(file)) {
+ size += getUsedSize(context, child);
+ }
+ return size;
+ }
+ }
+
+ public static <T extends Serializable> boolean serialize(Context context, T obj, String fileName) {
+ File file = new File(context.getCacheDir(), fileName);
+ ObjectOutputStream out = null;
+ try {
+ out = new ObjectOutputStream(new FileOutputStream(file));
+ out.writeObject(obj);
+ Log.i(TAG, "Serialized object to " + file);
+ return true;
+ } catch (Throwable x) {
+ Log.w(TAG, "Failed to serialize object to " + file);
+ return false;
+ } finally {
+ Util.close(out);
+ }
+ }
+
+ public static <T extends Serializable> T deserialize(Context context, String fileName) {
+ File file = new File(context.getCacheDir(), fileName);
+ if (!file.exists() || !file.isFile()) {
+ return null;
+ }
+
+ ObjectInputStream in = null;
+ try {
+ in = new ObjectInputStream(new FileInputStream(file));
+ T result = (T) in.readObject();
+ Log.i(TAG, "Deserialized object from " + file);
+ return result;
+ } catch (Throwable x) {
+ Log.w(TAG, "Failed to deserialize object from " + file, x);
+ return null;
+ } finally {
+ Util.close(in);
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/util/ImageLoader.java b/src/github/daneren2005/dsub/util/ImageLoader.java
new file mode 100644
index 00000000..331bc629
--- /dev/null
+++ b/src/github/daneren2005/dsub/util/ImageLoader.java
@@ -0,0 +1,332 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.util;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.TransitionDrawable;
+import android.media.RemoteControlClient;
+import android.os.Handler;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.support.v4.util.LruCache;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+
+/**
+ * Asynchronous loading of images, with caching.
+ * <p/>
+ * There should normally be only one instance of this class.
+ *
+ * @author Sindre Mehus
+ */
+@TargetApi(14)
+public class ImageLoader implements Runnable {
+
+ private static final String TAG = ImageLoader.class.getSimpleName();
+ private static final int CONCURRENCY = 5;
+
+ private Handler mHandler = new Handler();
+ private Context context;
+ private LruCache<String, Bitmap> cache;
+ private Bitmap nowPlaying;
+ private final BlockingQueue<Task> queue;
+ private final int imageSizeDefault;
+ private final int imageSizeLarge;
+ private Drawable largeUnknownImage;
+
+ public ImageLoader(Context context) {
+ this.context = context;
+ final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
+ final int cacheSize = maxMemory / 4;
+ cache = new LruCache<String, Bitmap>(cacheSize) {
+ @Override
+ protected int sizeOf(String key, Bitmap bitmap) {
+ return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
+ }
+
+ @Override
+ protected void entryRemoved(boolean evicted, String key, Bitmap oldBitmap, Bitmap newBitmap) {
+ if(evicted && oldBitmap != nowPlaying) {
+ oldBitmap.recycle();
+ }
+ }
+ };
+
+ queue = new LinkedBlockingQueue<Task>(500);
+
+ // Determine the density-dependent image sizes.
+ imageSizeDefault = context.getResources().getDrawable(R.drawable.unknown_album).getIntrinsicHeight();
+ DisplayMetrics metrics = context.getResources().getDisplayMetrics();
+ imageSizeLarge = (int) Math.round(Math.min(metrics.widthPixels, metrics.heightPixels));
+
+ for (int i = 0; i < CONCURRENCY; i++) {
+ new Thread(this, "ImageLoader").start();
+ }
+
+ createLargeUnknownImage(context);
+ }
+
+ private void createLargeUnknownImage(Context context) {
+ BitmapDrawable drawable = (BitmapDrawable) context.getResources().getDrawable(R.drawable.unknown_album_large);
+ Bitmap bitmap = Bitmap.createScaledBitmap(drawable.getBitmap(), imageSizeLarge, imageSizeLarge, true);
+ largeUnknownImage = Util.createDrawableFromBitmap(context, bitmap);
+ }
+
+ public void loadImage(View view, MusicDirectory.Entry entry, boolean large, boolean crossfade) {
+ if (largeUnknownImage != null && ((BitmapDrawable)largeUnknownImage).getBitmap().isRecycled())
+ createLargeUnknownImage(view.getContext());
+
+ if (entry == null || entry.getCoverArt() == null) {
+ setUnknownImage(view, large);
+ return;
+ }
+
+ int size = large ? imageSizeLarge : imageSizeDefault;
+ Bitmap bitmap = cache.get(getKey(entry.getCoverArt(), size));
+ if (bitmap != null && !bitmap.isRecycled()) {
+ final Drawable drawable = Util.createDrawableFromBitmap(this.context, bitmap);
+ setImage(view, drawable, large);
+ if(large) {
+ nowPlaying = bitmap;
+ }
+ return;
+ }
+
+ if (!large) {
+ setUnknownImage(view, large);
+ }
+ queue.offer(new Task(view.getContext(), entry, size, imageSizeLarge, large, new ViewTaskHandler(view, crossfade)));
+ }
+
+ public void loadImage(Context context, RemoteControlClient remoteControl, MusicDirectory.Entry entry) {
+ if (largeUnknownImage != null && ((BitmapDrawable)largeUnknownImage).getBitmap().isRecycled())
+ createLargeUnknownImage(context);
+
+ if (entry == null || entry.getCoverArt() == null) {
+ setUnknownImage(remoteControl);
+ return;
+ }
+
+ Bitmap bitmap = cache.get(getKey(entry.getCoverArt(), imageSizeLarge));
+ if (bitmap != null && !bitmap.isRecycled()) {
+ Drawable drawable = Util.createDrawableFromBitmap(this.context, bitmap);
+ setImage(remoteControl, drawable);
+ return;
+ }
+
+ setUnknownImage(remoteControl);
+ queue.offer(new Task(context, entry, imageSizeLarge, imageSizeLarge, false, new RemoteControlClientTaskHandler(remoteControl)));
+ }
+
+ private String getKey(String coverArtId, int size) {
+ return coverArtId + size;
+ }
+
+ private void setImage(View view, Drawable drawable, boolean crossfade) {
+ if (view instanceof TextView) {
+ // Cross-fading is not implemented for TextView since it's not in use. It would be easy to add it, though.
+ TextView textView = (TextView) view;
+ textView.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null);
+ } else if (view instanceof ImageView) {
+ ImageView imageView = (ImageView) view;
+ if (crossfade) {
+
+ Drawable existingDrawable = imageView.getDrawable();
+ if (existingDrawable == null) {
+ Bitmap emptyImage;
+ if(drawable.getIntrinsicWidth() > 0 && drawable.getIntrinsicHeight() > 0) {
+ emptyImage = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
+ } else {
+ emptyImage = Bitmap.createBitmap(imageSizeDefault, imageSizeDefault, Bitmap.Config.ARGB_8888);
+ }
+ existingDrawable = new BitmapDrawable(emptyImage);
+ } else {
+ // Try to get rid of old transitions
+ try {
+ TransitionDrawable tmp = (TransitionDrawable) existingDrawable;
+ int layers = tmp.getNumberOfLayers();
+ existingDrawable = tmp.getDrawable(layers - 1);
+ } catch(Exception e) {
+ // Do nothing, just means that the drawable is a flat image
+ }
+ }
+ if (!(((BitmapDrawable)existingDrawable).getBitmap().isRecycled()))
+ { // We will flow through to the non-transition if the old image is recycled... Yay 4.3
+ Drawable[] layers = new Drawable[]{existingDrawable, drawable};
+
+ TransitionDrawable transitionDrawable = new TransitionDrawable(layers);
+ imageView.setImageDrawable(transitionDrawable);
+ transitionDrawable.startTransition(250);
+ return;
+ }
+ }
+ imageView.setImageDrawable(drawable);
+ return;
+ }
+ }
+
+ private void setImage(RemoteControlClient remoteControl, Drawable drawable) {
+ if(remoteControl != null && drawable != null) {
+ Bitmap origBitmap = ((BitmapDrawable)drawable).getBitmap();
+ if ( origBitmap != null && !origBitmap.isRecycled()) {
+ remoteControl.editMetadata(false)
+ .putBitmap(
+ RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK,
+ origBitmap)
+ .apply();
+ } else {
+ Log.e(TAG, "Tried to load a recycled bitmap.");
+ remoteControl.editMetadata(false)
+ .putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, null)
+ .apply();
+ }
+ }
+ }
+
+ private void setUnknownImage(View view, boolean large) {
+ if (large) {
+ setImage(view, largeUnknownImage, false);
+ } else {
+ if (view instanceof TextView) {
+ ((TextView) view).setCompoundDrawablesWithIntrinsicBounds(R.drawable.unknown_album, 0, 0, 0);
+ } else if (view instanceof ImageView) {
+ ((ImageView) view).setImageResource(R.drawable.unknown_album);
+ }
+ }
+ }
+
+ private void setUnknownImage(RemoteControlClient remoteControl) {
+ setImage(remoteControl, largeUnknownImage);
+ }
+
+ public void clear() {
+ queue.clear();
+ }
+
+ @Override
+ public void run() {
+ while (true) {
+ try {
+ Task task = queue.take();
+ task.execute();
+ } catch (Throwable x) {
+ Log.e(TAG, "Unexpected exception in ImageLoader.", x);
+ }
+ }
+ }
+
+ private class Task {
+ private final Context mContext;
+ private final MusicDirectory.Entry mEntry;
+ private final int mSize;
+ private final int mSaveSize;
+ private final boolean mIsNowPlaying;
+ private ImageLoaderTaskHandler mTaskHandler;
+
+ public Task(Context context, MusicDirectory.Entry entry, int size, int saveSize, boolean isNowPlaying, ImageLoaderTaskHandler taskHandler) {
+ mContext = context;
+ mEntry = entry;
+ mSize = size;
+ mSaveSize = saveSize;
+ mIsNowPlaying = isNowPlaying;
+ mTaskHandler = taskHandler;
+ }
+
+ public void execute() {
+ try {
+ loadImage();
+ } catch(OutOfMemoryError e) {
+ Log.w(TAG, "Ran out of memory trying to load image, try cleanup and retry");
+ cache.evictAll();
+ System.gc();
+ }
+ }
+ public void loadImage() {
+ try {
+ MusicService musicService = MusicServiceFactory.getMusicService(mContext);
+ Bitmap bitmap = musicService.getCoverArt(mContext, mEntry, mSize, mSaveSize, null);
+ String key = getKey(mEntry.getCoverArt(), mSize);
+ cache.put(key, bitmap);
+ // Make sure key is the most recently "used"
+ cache.get(key);
+ if(mIsNowPlaying) {
+ nowPlaying = bitmap;
+ }
+
+ final Drawable drawable = Util.createDrawableFromBitmap(mContext, bitmap);
+ mTaskHandler.setDrawable(drawable);
+ mHandler.post(mTaskHandler);
+ } catch (Throwable x) {
+ Log.e(TAG, "Failed to download album art.", x);
+ }
+ }
+ }
+
+ private abstract class ImageLoaderTaskHandler implements Runnable {
+
+ protected Drawable mDrawable;
+
+ public void setDrawable(Drawable drawable) {
+ mDrawable = drawable;
+ }
+
+ }
+
+ private class ViewTaskHandler extends ImageLoaderTaskHandler {
+
+ protected boolean mCrossfade;
+ private View mView;
+
+ public ViewTaskHandler(View view, boolean crossfade) {
+ mCrossfade = crossfade;
+ mView = view;
+ }
+
+ @Override
+ public void run() {
+ setImage(mView, mDrawable, mCrossfade);
+ }
+ }
+
+ private class RemoteControlClientTaskHandler extends ImageLoaderTaskHandler {
+
+ private RemoteControlClient mRemoteControl;
+
+ public RemoteControlClientTaskHandler(RemoteControlClient remoteControl) {
+ mRemoteControl = remoteControl;
+ }
+
+ @Override
+ public void run() {
+ setImage(mRemoteControl, mDrawable);
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/util/LoadingTask.java b/src/github/daneren2005/dsub/util/LoadingTask.java
new file mode 100644
index 00000000..9ab5c86d
--- /dev/null
+++ b/src/github/daneren2005/dsub/util/LoadingTask.java
@@ -0,0 +1,97 @@
+package github.daneren2005.dsub.util;
+
+import android.app.Activity;
+import android.app.ProgressDialog;
+import android.content.DialogInterface;
+
+import github.daneren2005.dsub.activity.SubsonicActivity;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public abstract class LoadingTask<T> extends BackgroundTask<T> {
+
+ private final Activity tabActivity;
+ private ProgressDialog loading;
+ private Thread thread;
+ private final boolean cancellable;
+ private boolean cancelled = false;
+
+ public LoadingTask(Activity activity) {
+ super(activity);
+ tabActivity = activity;
+ this.cancellable = true;
+ }
+ public LoadingTask(Activity activity, final boolean cancellable) {
+ super(activity);
+ tabActivity = activity;
+ this.cancellable = cancellable;
+ }
+
+ @Override
+ public void execute() {
+ loading = ProgressDialog.show(tabActivity, "", "Loading. Please Wait...", true, cancellable, new DialogInterface.OnCancelListener() {
+ public void onCancel(DialogInterface dialog) {
+ cancel();
+ }
+
+ });
+
+ thread = new Thread() {
+ @Override
+ public void run() {
+ try {
+ final T result = doInBackground();
+ if (isCancelled()) {
+ return;
+ }
+
+ getHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ loading.cancel();
+ done(result);
+ }
+ });
+ } catch (final Throwable t) {
+ if (isCancelled()) {
+ return;
+ }
+
+ getHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ loading.cancel();
+ error(t);
+ }
+ });
+ }
+ }
+ };
+ thread.start();
+ }
+
+ protected void cancel() {
+ cancelled = true;
+ if (thread != null) {
+ thread.interrupt();
+ }
+ }
+
+ private boolean isCancelled() {
+ return (tabActivity instanceof SubsonicActivity && ((SubsonicActivity)tabActivity).isDestroyed()) || cancelled;
+ }
+
+ @Override
+ public void updateProgress(final String message) {
+ if(!cancelled) {
+ getHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ loading.setMessage(message);
+ }
+ });
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/util/Pair.java b/src/github/daneren2005/dsub/util/Pair.java
new file mode 100644
index 00000000..54386a62
--- /dev/null
+++ b/src/github/daneren2005/dsub/util/Pair.java
@@ -0,0 +1,54 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.util;
+
+import java.io.Serializable;
+
+/**
+ * @author Sindre Mehus
+ */
+public class Pair<S, T> implements Serializable {
+
+ private S first;
+ private T second;
+
+ public Pair() {
+ }
+
+ public Pair(S first, T second) {
+ this.first = first;
+ this.second = second;
+ }
+
+ public S getFirst() {
+ return first;
+ }
+
+ public void setFirst(S first) {
+ this.first = first;
+ }
+
+ public T getSecond() {
+ return second;
+ }
+
+ public void setSecond(T second) {
+ this.second = second;
+ }
+}
diff --git a/src/github/daneren2005/dsub/util/ProgressListener.java b/src/github/daneren2005/dsub/util/ProgressListener.java
new file mode 100644
index 00000000..c6d58f42
--- /dev/null
+++ b/src/github/daneren2005/dsub/util/ProgressListener.java
@@ -0,0 +1,27 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.util;
+
+/**
+ * @author Sindre Mehus
+ */
+public interface ProgressListener {
+ void updateProgress(String message);
+ void updateProgress(int messageId);
+}
diff --git a/src/github/daneren2005/dsub/util/SettingsBackupAgent.java b/src/github/daneren2005/dsub/util/SettingsBackupAgent.java
new file mode 100644
index 00000000..7eb6d137
--- /dev/null
+++ b/src/github/daneren2005/dsub/util/SettingsBackupAgent.java
@@ -0,0 +1,31 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+*/
+package github.daneren2005.dsub.util;
+
+import android.app.backup.BackupAgentHelper;
+import android.app.backup.SharedPreferencesBackupHelper;
+import github.daneren2005.dsub.util.Constants;
+
+public class SettingsBackupAgent extends BackupAgentHelper {
+ public void onCreate() {
+ super.onCreate();
+ SharedPreferencesBackupHelper helper = new SharedPreferencesBackupHelper(this, Constants.PREFERENCES_FILE_NAME);
+ addHelper("mypreferences", helper);
+ }
+ }
diff --git a/src/github/daneren2005/dsub/util/ShufflePlayBuffer.java b/src/github/daneren2005/dsub/util/ShufflePlayBuffer.java
new file mode 100644
index 00000000..195fe913
--- /dev/null
+++ b/src/github/daneren2005/dsub/util/ShufflePlayBuffer.java
@@ -0,0 +1,127 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.util;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.Log;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class ShufflePlayBuffer {
+
+ private static final String TAG = ShufflePlayBuffer.class.getSimpleName();
+ private static final int CAPACITY = 50;
+ private static final int REFILL_THRESHOLD = 40;
+
+ private final ScheduledExecutorService executorService;
+ private final List<MusicDirectory.Entry> buffer = new ArrayList<MusicDirectory.Entry>();
+ private int lastCount = -1;
+ private Context context;
+ private int currentServer;
+ private String currentFolder = "";
+
+ private String genre = "";
+ private String startYear = "";
+ private String endYear = "";
+
+ public ShufflePlayBuffer(Context context) {
+ this.context = context;
+ executorService = Executors.newSingleThreadScheduledExecutor();
+ Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ refill();
+ }
+ };
+ executorService.scheduleWithFixedDelay(runnable, 1, 10, TimeUnit.SECONDS);
+ }
+
+ public List<MusicDirectory.Entry> get(int size) {
+ clearBufferIfnecessary();
+
+ List<MusicDirectory.Entry> result = new ArrayList<MusicDirectory.Entry>(size);
+ synchronized (buffer) {
+ while (!buffer.isEmpty() && result.size() < size) {
+ result.add(buffer.remove(buffer.size() - 1));
+ }
+ }
+ Log.i(TAG, "Taking " + result.size() + " songs from shuffle play buffer. " + buffer.size() + " remaining.");
+ return result;
+ }
+
+ public void shutdown() {
+ executorService.shutdown();
+ }
+
+ private void refill() {
+
+ // Check if active server has changed.
+ clearBufferIfnecessary();
+
+ if (buffer.size() > REFILL_THRESHOLD || (!Util.isNetworkConnected(context) && !Util.isOffline(context)) || lastCount == 0) {
+ return;
+ }
+
+ try {
+ MusicService service = MusicServiceFactory.getMusicService(context);
+ int n = CAPACITY - buffer.size();
+ String folder = Util.getSelectedMusicFolderId(context);
+ MusicDirectory songs = service.getRandomSongs(n, folder, genre, startYear, endYear, context, null);
+
+ synchronized (buffer) {
+ buffer.addAll(songs.getChildren());
+ Log.i(TAG, "Refilled shuffle play buffer with " + songs.getChildrenSize() + " songs.");
+ lastCount = songs.getChildrenSize();
+ }
+ } catch (Exception x) {
+ Log.w(TAG, "Failed to refill shuffle play buffer.", x);
+ }
+ }
+
+ private void clearBufferIfnecessary() {
+ synchronized (buffer) {
+ final SharedPreferences prefs = Util.getPreferences(context);
+ if (currentServer != Util.getActiveServer(context)
+ || (currentFolder != null && !currentFolder.equals(Util.getSelectedMusicFolderId(context)))
+ || (genre != null && !genre.equals(prefs.getString(Constants.PREFERENCES_KEY_SHUFFLE_GENRE, "")))
+ || (startYear != null && !startYear.equals(prefs.getString(Constants.PREFERENCES_KEY_SHUFFLE_START_YEAR, "")))
+ || (endYear != null && !endYear.equals(prefs.getString(Constants.PREFERENCES_KEY_SHUFFLE_END_YEAR, "")))) {
+ lastCount = -1;
+ currentServer = Util.getActiveServer(context);
+ currentFolder = Util.getSelectedMusicFolderId(context);
+ genre = prefs.getString(Constants.PREFERENCES_KEY_SHUFFLE_GENRE, "");
+ startYear = prefs.getString(Constants.PREFERENCES_KEY_SHUFFLE_START_YEAR, "");
+ endYear = prefs.getString(Constants.PREFERENCES_KEY_SHUFFLE_END_YEAR, "");
+ buffer.clear();
+ }
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/util/SilentBackgroundTask.java b/src/github/daneren2005/dsub/util/SilentBackgroundTask.java
new file mode 100644
index 00000000..7bceb467
--- /dev/null
+++ b/src/github/daneren2005/dsub/util/SilentBackgroundTask.java
@@ -0,0 +1,67 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.util;
+
+import android.app.Activity;
+
+/**
+ * @author Sindre Mehus
+ */
+public abstract class SilentBackgroundTask<T> extends BackgroundTask<T> {
+
+ public SilentBackgroundTask(Activity activity) {
+ super(activity);
+ }
+
+ @Override
+ public void execute() {
+ Thread thread = new Thread() {
+ @Override
+ public void run() {
+ try {
+ final T result = doInBackground();
+
+ getHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ done(result);
+ }
+ });
+
+ } catch (final Throwable t) {
+ getHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ error(t);
+ }
+ });
+ }
+ }
+ };
+ thread.start();
+ }
+
+ @Override
+ public void updateProgress(int messageId) {
+ }
+
+ @Override
+ public void updateProgress(String message) {
+ }
+}
diff --git a/src/github/daneren2005/dsub/util/SimpleServiceBinder.java b/src/github/daneren2005/dsub/util/SimpleServiceBinder.java
new file mode 100644
index 00000000..9c0b36a9
--- /dev/null
+++ b/src/github/daneren2005/dsub/util/SimpleServiceBinder.java
@@ -0,0 +1,37 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.util;
+
+import android.os.Binder;
+
+/**
+ * @author Sindre Mehus
+ */
+public class SimpleServiceBinder<S> extends Binder {
+
+ private final S service;
+
+ public SimpleServiceBinder(S service) {
+ this.service = service;
+ }
+
+ public S getService() {
+ return service;
+ }
+}
diff --git a/src/github/daneren2005/dsub/util/TabBackgroundTask.java b/src/github/daneren2005/dsub/util/TabBackgroundTask.java
new file mode 100644
index 00000000..c345b982
--- /dev/null
+++ b/src/github/daneren2005/dsub/util/TabBackgroundTask.java
@@ -0,0 +1,67 @@
+package github.daneren2005.dsub.util;
+
+import github.daneren2005.dsub.fragments.SubsonicFragment;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public abstract class TabBackgroundTask<T> extends BackgroundTask<T> {
+
+ private final SubsonicFragment tabFragment;
+
+ public TabBackgroundTask(SubsonicFragment fragment) {
+ super(fragment.getActivity());
+ tabFragment = fragment;
+ }
+
+ @Override
+ public void execute() {
+ tabFragment.setProgressVisible(true);
+
+ new Thread() {
+ @Override
+ public void run() {
+ try {
+ final T result = doInBackground();
+ if (isCancelled()) {
+ return;
+ }
+
+ getHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ tabFragment.setProgressVisible(false);
+ done(result);
+ }
+ });
+ } catch (final Throwable t) {
+ if (isCancelled()) {
+ return;
+ }
+ getHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ tabFragment.setProgressVisible(false);
+ error(t);
+ }
+ });
+ }
+ }
+ }.start();
+ }
+
+ private boolean isCancelled() {
+ return !tabFragment.isAdded();
+ }
+
+ @Override
+ public void updateProgress(final String message) {
+ getHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ tabFragment.updateProgress(message);
+ }
+ });
+ }
+}
diff --git a/src/github/daneren2005/dsub/util/TimeLimitedCache.java b/src/github/daneren2005/dsub/util/TimeLimitedCache.java
new file mode 100644
index 00000000..8b7df783
--- /dev/null
+++ b/src/github/daneren2005/dsub/util/TimeLimitedCache.java
@@ -0,0 +1,55 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.util;
+
+import java.lang.ref.SoftReference;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class TimeLimitedCache<T> {
+
+ private SoftReference<T> value;
+ private final long ttlMillis;
+ private long expires;
+
+ public TimeLimitedCache(long ttl, TimeUnit timeUnit) {
+ this.ttlMillis = TimeUnit.MILLISECONDS.convert(ttl, timeUnit);
+ }
+
+ public T get() {
+ return System.currentTimeMillis() < expires ? value.get() : null;
+ }
+
+ public void set(T value) {
+ set(value, ttlMillis, TimeUnit.MILLISECONDS);
+ }
+
+ public void set(T value, long ttl, TimeUnit timeUnit) {
+ this.value = new SoftReference<T>(value);
+ expires = System.currentTimeMillis() + timeUnit.toMillis(ttl);
+ }
+
+ public void clear() {
+ expires = 0L;
+ value = null;
+ }
+}
diff --git a/src/github/daneren2005/dsub/util/Util.java b/src/github/daneren2005/dsub/util/Util.java
new file mode 100644
index 00000000..a6fdd987
--- /dev/null
+++ b/src/github/daneren2005/dsub/util/Util.java
@@ -0,0 +1,1173 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.util;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.media.AudioManager;
+import android.media.AudioManager.OnAudioFocusChangeListener;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.wifi.WifiManager;
+import android.os.Build;
+import android.os.Environment;
+import android.os.Handler;
+import android.support.v4.app.NotificationCompat;
+import android.text.SpannableString;
+import android.text.method.LinkMovementMethod;
+import android.text.util.Linkify;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.ViewGroup;
+import android.view.KeyEvent;
+import android.widget.LinearLayout;
+import android.widget.RemoteViews;
+import android.widget.TextView;
+import android.widget.Toast;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.activity.MainActivity;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.PlayerState;
+import github.daneren2005.dsub.domain.RepeatMode;
+import github.daneren2005.dsub.domain.Version;
+import github.daneren2005.dsub.provider.DSubWidgetProvider;
+import github.daneren2005.dsub.receiver.MediaButtonIntentReceiver;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.service.DownloadServiceImpl;
+import org.apache.http.HttpEntity;
+
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.security.MessageDigest;
+import java.text.DecimalFormat;
+import java.text.NumberFormat;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public final class Util {
+ private static final String TAG = Util.class.getSimpleName();
+
+ private static final DecimalFormat GIGA_BYTE_FORMAT = new DecimalFormat("0.00 GB");
+ private static final DecimalFormat MEGA_BYTE_FORMAT = new DecimalFormat("0.00 MB");
+ private static final DecimalFormat KILO_BYTE_FORMAT = new DecimalFormat("0 KB");
+
+ private static DecimalFormat GIGA_BYTE_LOCALIZED_FORMAT = null;
+ private static DecimalFormat MEGA_BYTE_LOCALIZED_FORMAT = null;
+ private static DecimalFormat KILO_BYTE_LOCALIZED_FORMAT = null;
+ private static DecimalFormat BYTE_LOCALIZED_FORMAT = null;
+
+ public static final String EVENT_META_CHANGED = "github.daneren2005.dsub.EVENT_META_CHANGED";
+ public static final String EVENT_PLAYSTATE_CHANGED = "github.daneren2005.dsub.EVENT_PLAYSTATE_CHANGED";
+
+ public static final String AVRCP_PLAYSTATE_CHANGED = "com.android.music.playstatechanged";
+ public static final String AVRCP_METADATA_CHANGED = "com.android.music.metachanged";
+
+ private static boolean hasFocus = false;
+ private static boolean pauseFocus = false;
+ private static boolean lowerFocus = false;
+
+ private static final Map<Integer, Version> SERVER_REST_VERSIONS = new ConcurrentHashMap<Integer, Version>();
+
+ // Used by hexEncode()
+ private static final char[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
+
+ private final static Pair<Integer, Integer> NOTIFICATION_TEXT_COLORS = new Pair<Integer, Integer>();
+ private static Toast toast;
+
+ private Util() {
+ }
+
+ public static boolean isOffline(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getBoolean(Constants.PREFERENCES_KEY_OFFLINE, false);
+ }
+
+ public static void setOffline(Context context, boolean offline) {
+ SharedPreferences prefs = getPreferences(context);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putBoolean(Constants.PREFERENCES_KEY_OFFLINE, offline);
+ editor.commit();
+ }
+
+ public static boolean isScreenLitOnDownload(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getBoolean(Constants.PREFERENCES_KEY_SCREEN_LIT_ON_DOWNLOAD, false);
+ }
+
+ public static RepeatMode getRepeatMode(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return RepeatMode.valueOf(prefs.getString(Constants.PREFERENCES_KEY_REPEAT_MODE, RepeatMode.OFF.name()));
+ }
+
+ public static void setRepeatMode(Context context, RepeatMode repeatMode) {
+ SharedPreferences prefs = getPreferences(context);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putString(Constants.PREFERENCES_KEY_REPEAT_MODE, repeatMode.name());
+ editor.commit();
+ }
+
+ public static boolean isScrobblingEnabled(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getBoolean(Constants.PREFERENCES_KEY_SCROBBLE, false);
+ }
+
+ public static void setActiveServer(Context context, int instance) {
+ SharedPreferences prefs = getPreferences(context);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, instance);
+ editor.putString(Constants.PREFERENCES_KEY_MUSIC_FOLDER_ID + instance, null);
+ editor.commit();
+ }
+
+ public static int getActiveServer(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1);
+ }
+
+ public static boolean checkServerVersion(Context context, String requiredVersion) {
+ Version version = Util.getServerRestVersion(context);
+ Version required = new Version(requiredVersion);
+ if(version != null && version.compareTo(required) >= 0) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public static int getServerCount(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getInt(Constants.PREFERENCES_KEY_SERVER_COUNT, 1);
+ }
+
+ public static void removeInstanceName(Context context, int instance, int activeInstance) {
+ SharedPreferences prefs = getPreferences(context);
+ SharedPreferences.Editor editor = prefs.edit();
+
+ int newInstance = instance + 1;
+
+ String server = prefs.getString(Constants.PREFERENCES_KEY_SERVER_KEY + newInstance, null);
+ String serverName = prefs.getString(Constants.PREFERENCES_KEY_SERVER_NAME + newInstance, null);
+ String serverUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_URL + newInstance, null);
+ String userName = prefs.getString(Constants.PREFERENCES_KEY_USERNAME + newInstance, null);
+ String password = prefs.getString(Constants.PREFERENCES_KEY_PASSWORD + newInstance, null);
+
+ editor.putString(Constants.PREFERENCES_KEY_SERVER_KEY + instance, server);
+ editor.putString(Constants.PREFERENCES_KEY_SERVER_NAME + instance, serverName);
+ editor.putString(Constants.PREFERENCES_KEY_SERVER_URL + instance, serverUrl);
+ editor.putString(Constants.PREFERENCES_KEY_USERNAME + instance, userName);
+ editor.putString(Constants.PREFERENCES_KEY_PASSWORD + instance, password);
+
+ editor.putString(Constants.PREFERENCES_KEY_SERVER_KEY + newInstance, null);
+ editor.putString(Constants.PREFERENCES_KEY_SERVER_NAME + newInstance, null);
+ editor.putString(Constants.PREFERENCES_KEY_SERVER_URL + newInstance, null);
+ editor.putString(Constants.PREFERENCES_KEY_USERNAME + newInstance, null);
+ editor.putString(Constants.PREFERENCES_KEY_PASSWORD + newInstance, null);
+ editor.commit();
+
+ if (instance == activeInstance) {
+ if(instance != 1) {
+ Util.setActiveServer(context, 1);
+ } else {
+ Util.setOffline(context, true);
+ }
+ } else if (newInstance == activeInstance) {
+ Util.setActiveServer(context, instance);
+ }
+ }
+
+ public static String getServerName(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ int instance = prefs.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1);
+ return prefs.getString(Constants.PREFERENCES_KEY_SERVER_NAME + instance, null);
+ }
+ public static String getServerName(Context context, int instance) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getString(Constants.PREFERENCES_KEY_SERVER_NAME + instance, null);
+ }
+
+ public static String getUserName(Context context, int instance) {
+ if (instance == 0) {
+ return context.getResources().getString(R.string.main_offline);
+ }
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getString(Constants.PREFERENCES_KEY_USERNAME + instance, null);
+ }
+
+ public static void setServerRestVersion(Context context, Version version) {
+ int instance = getActiveServer(context);
+ Version current = SERVER_REST_VERSIONS.get(instance);
+ if(current != version) {
+ SERVER_REST_VERSIONS.put(instance, version);
+ SharedPreferences.Editor editor = getPreferences(context).edit();
+ editor.putString(Constants.PREFERENCES_KEY_SERVER_VERSION + instance, version.getVersion());
+ editor.commit();
+ }
+ }
+
+ public static Version getServerRestVersion(Context context) {
+ int instance = getActiveServer(context);
+ Version version = SERVER_REST_VERSIONS.get(instance);
+ if(version == null) {
+ SharedPreferences prefs = getPreferences(context);
+ String versionString = prefs.getString(Constants.PREFERENCES_KEY_SERVER_VERSION + instance, null);
+ if(versionString != null) {
+ version = new Version(versionString);
+ SERVER_REST_VERSIONS.put(instance, version);
+ }
+ }
+ return version;
+ }
+
+ public static void setSelectedMusicFolderId(Context context, String musicFolderId) {
+ int instance = getActiveServer(context);
+ SharedPreferences prefs = getPreferences(context);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putString(Constants.PREFERENCES_KEY_MUSIC_FOLDER_ID + instance, musicFolderId);
+ editor.commit();
+ }
+
+ public static String getSelectedMusicFolderId(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ int instance = getActiveServer(context);
+ return prefs.getString(Constants.PREFERENCES_KEY_MUSIC_FOLDER_ID + instance, null);
+ }
+
+ public static String getTheme(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getString(Constants.PREFERENCES_KEY_THEME, null);
+ }
+
+ public static boolean getDisplayTrack(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getBoolean(Constants.PREFERENCES_KEY_DISPLAY_TRACK, false);
+ }
+
+ public static int getMaxBitrate(Context context) {
+ ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo networkInfo = manager.getActiveNetworkInfo();
+ if (networkInfo == null) {
+ return 0;
+ }
+
+ boolean wifi = networkInfo.getType() == ConnectivityManager.TYPE_WIFI;
+ SharedPreferences prefs = getPreferences(context);
+ return Integer.parseInt(prefs.getString(wifi ? Constants.PREFERENCES_KEY_MAX_BITRATE_WIFI : Constants.PREFERENCES_KEY_MAX_BITRATE_MOBILE, "0"));
+ }
+
+ public static int getMaxVideoBitrate(Context context) {
+ ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo networkInfo = manager.getActiveNetworkInfo();
+ if (networkInfo == null) {
+ return 0;
+ }
+
+ boolean wifi = networkInfo.getType() == ConnectivityManager.TYPE_WIFI;
+ SharedPreferences prefs = getPreferences(context);
+ return Integer.parseInt(prefs.getString(wifi ? Constants.PREFERENCES_KEY_MAX_VIDEO_BITRATE_WIFI : Constants.PREFERENCES_KEY_MAX_VIDEO_BITRATE_MOBILE, "0"));
+ }
+
+ public static int getPreloadCount(Context context) {
+ ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo networkInfo = manager.getActiveNetworkInfo();
+ if (networkInfo == null) {
+ return 3;
+ }
+
+ SharedPreferences prefs = getPreferences(context);
+ boolean wifi = networkInfo.getType() == ConnectivityManager.TYPE_WIFI;
+ int preloadCount = Integer.parseInt(prefs.getString(wifi ? Constants.PREFERENCES_KEY_PRELOAD_COUNT_WIFI : Constants.PREFERENCES_KEY_PRELOAD_COUNT_MOBILE, "-1"));
+ return preloadCount == -1 ? Integer.MAX_VALUE : preloadCount;
+ }
+
+ public static int getCacheSizeMB(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ int cacheSize = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_CACHE_SIZE, "-1"));
+ return cacheSize == -1 ? Integer.MAX_VALUE : cacheSize;
+ }
+
+ public static String getRestUrl(Context context, String method) {
+ StringBuilder builder = new StringBuilder();
+
+ SharedPreferences prefs = getPreferences(context);
+
+ int instance = prefs.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1);
+ String serverUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_URL + instance, null);
+ String username = prefs.getString(Constants.PREFERENCES_KEY_USERNAME + instance, null);
+ String password = prefs.getString(Constants.PREFERENCES_KEY_PASSWORD + instance, null);
+
+ // Slightly obfuscate password
+ password = "enc:" + Util.utf8HexEncode(password);
+
+ builder.append(serverUrl);
+ if (builder.charAt(builder.length() - 1) != '/') {
+ builder.append("/");
+ }
+ builder.append("rest/").append(method).append(".view");
+ builder.append("?u=").append(username);
+ builder.append("&p=").append(password);
+ builder.append("&v=").append(Constants.REST_PROTOCOL_VERSION);
+ builder.append("&c=").append(Constants.REST_CLIENT_ID);
+
+ return builder.toString();
+ }
+
+ public static String getVideoPlayerType(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getString(Constants.PREFERENCES_KEY_VIDEO_PLAYER, "raw");
+ }
+
+ public static SharedPreferences getPreferences(Context context) {
+ return context.getSharedPreferences(Constants.PREFERENCES_FILE_NAME, 0);
+ }
+ public static SharedPreferences getOfflineSync(Context context) {
+ return context.getSharedPreferences(Constants.OFFLINE_SYNC_NAME, 0);
+ }
+
+ public static String getSyncDefault(Context context) {
+ SharedPreferences prefs = Util.getOfflineSync(context);
+ return prefs.getString(Constants.OFFLINE_SYNC_DEFAULT, null);
+ }
+ public static void setSyncDefault(Context context, String defaultValue) {
+ SharedPreferences.Editor editor = Util.getOfflineSync(context).edit();
+ editor.putString(Constants.OFFLINE_SYNC_DEFAULT, defaultValue);
+ editor.commit();
+ }
+
+ public static int offlineScrobblesCount(Context context) {
+ SharedPreferences offline = getOfflineSync(context);
+ return offline.getInt(Constants.OFFLINE_SCROBBLE_COUNT, 0);
+ }
+ public static int offlineStarsCount(Context context) {
+ SharedPreferences offline = getOfflineSync(context);
+ return offline.getInt(Constants.OFFLINE_STAR_COUNT, 0);
+ }
+
+ public static String parseOfflineIDSearch(Context context, String id, String cacheLocation) {
+ String name = id.replace(cacheLocation, "");
+ if(name.startsWith("/")) {
+ name = name.substring(1);
+ }
+ name = name.replace(".complete", "").replace(".partial", "");
+ int index = name.lastIndexOf(".");
+ name = index == -1 ? name : name.substring(0, index);
+ String[] details = name.split("/");
+
+ String title = details[details.length - 1];
+ if(index == -1) {
+ if(details.length > 1) {
+ String artist = "artist:\"" + details[details.length - 2] + "\"";
+ String simpleArtist = "artist:\"" + title + "\"";
+ title = "album:\"" + title + "\"";
+ if(details[details.length - 1].equals(details[details.length - 2])) {
+ name = title;
+ } else {
+ name = "(" + artist + " AND " + title + ")" + " OR " + simpleArtist;
+ }
+ } else {
+ name = "artist:\"" + title + "\" OR album:\"" + title + "\"";
+ }
+ } else {
+ String artist;
+ if(details.length > 2) {
+ artist = "artist:\"" + details[details.length - 3] + "\"";
+ } else {
+ artist = "(artist:\"" + details[0] + "\" OR album:\"" + details[0] + "\")";
+ }
+ title = "title:\"" + title.substring(title.indexOf('-') + 1) + "\"";
+ name = artist + " AND " + title;
+ }
+
+ return name;
+ }
+
+ public static String getContentType(HttpEntity entity) {
+ if (entity == null || entity.getContentType() == null) {
+ return null;
+ }
+ return entity.getContentType().getValue();
+ }
+
+ public static int getRemainingTrialDays(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ long installTime = prefs.getLong(Constants.PREFERENCES_KEY_INSTALL_TIME, 0L);
+
+ if (installTime == 0L) {
+ installTime = System.currentTimeMillis();
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putLong(Constants.PREFERENCES_KEY_INSTALL_TIME, installTime);
+ editor.commit();
+ }
+
+ long now = System.currentTimeMillis();
+ long millisPerDay = 24L * 60L * 60L * 1000L;
+ int daysSinceInstall = (int) ((now - installTime) / millisPerDay);
+ return Math.max(0, Constants.FREE_TRIAL_DAYS - daysSinceInstall);
+ }
+
+ /**
+ * Get the contents of an <code>InputStream</code> as a <code>byte[]</code>.
+ * <p/>
+ * This method buffers the input internally, so there is no need to use a
+ * <code>BufferedInputStream</code>.
+ *
+ * @param input the <code>InputStream</code> to read from
+ * @return the requested byte array
+ * @throws NullPointerException if the input is null
+ * @throws IOException if an I/O error occurs
+ */
+ public static byte[] toByteArray(InputStream input) throws IOException {
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ copy(input, output);
+ return output.toByteArray();
+ }
+
+ public static long copy(InputStream input, OutputStream output)
+ throws IOException {
+ byte[] buffer = new byte[1024 * 4];
+ long count = 0;
+ int n;
+ while (-1 != (n = input.read(buffer))) {
+ output.write(buffer, 0, n);
+ count += n;
+ }
+ return count;
+ }
+
+ public static void atomicCopy(File from, File to) throws IOException {
+ FileInputStream in = null;
+ FileOutputStream out = null;
+ File tmp = null;
+ try {
+ tmp = new File(to.getPath() + ".tmp");
+ in = new FileInputStream(from);
+ out = new FileOutputStream(tmp);
+ in.getChannel().transferTo(0, from.length(), out.getChannel());
+ out.close();
+ if (!tmp.renameTo(to)) {
+ throw new IOException("Failed to rename " + tmp + " to " + to);
+ }
+ Log.i(TAG, "Copied " + from + " to " + to);
+ } catch (IOException x) {
+ close(out);
+ delete(to);
+ throw x;
+ } finally {
+ close(in);
+ close(out);
+ delete(tmp);
+ }
+ }
+ public static void renameFile(File from, File to) throws IOException {
+ if(from.renameTo(to)) {
+ Log.i(TAG, "Renamed " + from + " to " + to);
+ } else {
+ atomicCopy(from, to);
+ }
+ }
+
+ public static void close(Closeable closeable) {
+ try {
+ if (closeable != null) {
+ closeable.close();
+ }
+ } catch (Throwable x) {
+ // Ignored
+ }
+ }
+
+ public static boolean delete(File file) {
+ if (file != null && file.exists()) {
+ if (!file.delete()) {
+ Log.w(TAG, "Failed to delete file " + file);
+ return false;
+ }
+ Log.i(TAG, "Deleted file " + file);
+ }
+ return true;
+ }
+ public static boolean recursiveDelete(File dir) {
+ if (dir != null && dir.exists()) {
+ for(File file: dir.listFiles()) {
+ if(file.isDirectory()) {
+ if(!recursiveDelete(file)) {
+ return false;
+ }
+ } else if(file.exists()) {
+ if(!file.delete()) {
+ return false;
+ }
+ }
+ }
+ return dir.delete();
+ }
+ return false;
+ }
+
+ public static void toast(Context context, int messageId) {
+ toast(context, messageId, true);
+ }
+
+ public static void toast(Context context, int messageId, boolean shortDuration) {
+ toast(context, context.getString(messageId), shortDuration);
+ }
+
+ public static void toast(Context context, String message) {
+ toast(context, message, true);
+ }
+
+ public static void toast(Context context, String message, boolean shortDuration) {
+ if (toast == null) {
+ toast = Toast.makeText(context, message, shortDuration ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG);
+ toast.setGravity(Gravity.CENTER, 0, 0);
+ } else {
+ toast.setText(message);
+ toast.setDuration(shortDuration ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG);
+ }
+ toast.show();
+ }
+
+ public static void confirmDialog(Context context, int action, String subject, DialogInterface.OnClickListener onClick) {
+ Util.confirmDialog(context, context.getResources().getString(action).toLowerCase(), subject, onClick);
+ }
+ public static void confirmDialog(Context context, String action, String subject, DialogInterface.OnClickListener onClick) {
+ new AlertDialog.Builder(context)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setTitle(R.string.common_confirm)
+ .setMessage(context.getResources().getString(R.string.common_confirm_message, action, subject))
+ .setPositiveButton(R.string.common_ok, onClick)
+ .setNegativeButton(R.string.common_cancel, null)
+ .show();
+ }
+
+ /**
+ * Converts a byte-count to a formatted string suitable for display to the user.
+ * For instance:
+ * <ul>
+ * <li><code>format(918)</code> returns <em>"918 B"</em>.</li>
+ * <li><code>format(98765)</code> returns <em>"96 KB"</em>.</li>
+ * <li><code>format(1238476)</code> returns <em>"1.2 MB"</em>.</li>
+ * </ul>
+ * This method assumes that 1 KB is 1024 bytes.
+ * To get a localized string, please use formatLocalizedBytes instead.
+ *
+ * @param byteCount The number of bytes.
+ * @return The formatted string.
+ */
+ public static synchronized String formatBytes(long byteCount) {
+
+ // More than 1 GB?
+ if (byteCount >= 1024 * 1024 * 1024) {
+ NumberFormat gigaByteFormat = GIGA_BYTE_FORMAT;
+ return gigaByteFormat.format((double) byteCount / (1024 * 1024 * 1024));
+ }
+
+ // More than 1 MB?
+ if (byteCount >= 1024 * 1024) {
+ NumberFormat megaByteFormat = MEGA_BYTE_FORMAT;
+ return megaByteFormat.format((double) byteCount / (1024 * 1024));
+ }
+
+ // More than 1 KB?
+ if (byteCount >= 1024) {
+ NumberFormat kiloByteFormat = KILO_BYTE_FORMAT;
+ return kiloByteFormat.format((double) byteCount / 1024);
+ }
+
+ return byteCount + " B";
+ }
+
+ /**
+ * Converts a byte-count to a formatted string suitable for display to the user.
+ * For instance:
+ * <ul>
+ * <li><code>format(918)</code> returns <em>"918 B"</em>.</li>
+ * <li><code>format(98765)</code> returns <em>"96 KB"</em>.</li>
+ * <li><code>format(1238476)</code> returns <em>"1.2 MB"</em>.</li>
+ * </ul>
+ * This method assumes that 1 KB is 1024 bytes.
+ * This version of the method returns a localized string.
+ *
+ * @param byteCount The number of bytes.
+ * @return The formatted string.
+ */
+ public static synchronized String formatLocalizedBytes(long byteCount, Context context) {
+
+ // More than 1 GB?
+ if (byteCount >= 1024 * 1024 * 1024) {
+ if (GIGA_BYTE_LOCALIZED_FORMAT == null) {
+ GIGA_BYTE_LOCALIZED_FORMAT = new DecimalFormat(context.getResources().getString(R.string.util_bytes_format_gigabyte));
+ }
+
+ return GIGA_BYTE_LOCALIZED_FORMAT.format((double) byteCount / (1024 * 1024 * 1024));
+ }
+
+ // More than 1 MB?
+ if (byteCount >= 1024 * 1024) {
+ if (MEGA_BYTE_LOCALIZED_FORMAT == null) {
+ MEGA_BYTE_LOCALIZED_FORMAT = new DecimalFormat(context.getResources().getString(R.string.util_bytes_format_megabyte));
+ }
+
+ return MEGA_BYTE_LOCALIZED_FORMAT.format((double) byteCount / (1024 * 1024));
+ }
+
+ // More than 1 KB?
+ if (byteCount >= 1024) {
+ if (KILO_BYTE_LOCALIZED_FORMAT == null) {
+ KILO_BYTE_LOCALIZED_FORMAT = new DecimalFormat(context.getResources().getString(R.string.util_bytes_format_kilobyte));
+ }
+
+ return KILO_BYTE_LOCALIZED_FORMAT.format((double) byteCount / 1024);
+ }
+
+ if (BYTE_LOCALIZED_FORMAT == null) {
+ BYTE_LOCALIZED_FORMAT = new DecimalFormat(context.getResources().getString(R.string.util_bytes_format_byte));
+ }
+
+ return BYTE_LOCALIZED_FORMAT.format((double) byteCount);
+ }
+
+ public static String formatDuration(Integer seconds) {
+ if (seconds == null) {
+ return null;
+ }
+
+ int hours = seconds / 3600;
+ int minutes = (seconds / 60) % 60;
+ int secs = seconds % 60;
+
+ StringBuilder builder = new StringBuilder(7);
+ if(hours > 0) {
+ builder.append(hours).append(":");
+ if(minutes < 10) {
+ builder.append("0");
+ }
+ }
+ builder.append(minutes).append(":");
+ if (secs < 10) {
+ builder.append("0");
+ }
+ builder.append(secs);
+ return builder.toString();
+ }
+
+ public static boolean equals(Object object1, Object object2) {
+ if (object1 == object2) {
+ return true;
+ }
+ if (object1 == null || object2 == null) {
+ return false;
+ }
+ return object1.equals(object2);
+
+ }
+
+ /**
+ * Encodes the given string by using the hexadecimal representation of its UTF-8 bytes.
+ *
+ * @param s The string to encode.
+ * @return The encoded string.
+ */
+ public static String utf8HexEncode(String s) {
+ if (s == null) {
+ return null;
+ }
+ byte[] utf8;
+ try {
+ utf8 = s.getBytes(Constants.UTF_8);
+ } catch (UnsupportedEncodingException x) {
+ throw new RuntimeException(x);
+ }
+ return hexEncode(utf8);
+ }
+
+ /**
+ * Converts an array of bytes into an array of characters representing the hexadecimal values of each byte in order.
+ * The returned array will be double the length of the passed array, as it takes two characters to represent any
+ * given byte.
+ *
+ * @param data Bytes to convert to hexadecimal characters.
+ * @return A string containing hexadecimal characters.
+ */
+ public static String hexEncode(byte[] data) {
+ int length = data.length;
+ char[] out = new char[length << 1];
+ // two characters form the hex value.
+ for (int i = 0, j = 0; i < length; i++) {
+ out[j++] = HEX_DIGITS[(0xF0 & data[i]) >>> 4];
+ out[j++] = HEX_DIGITS[0x0F & data[i]];
+ }
+ return new String(out);
+ }
+
+ /**
+ * Calculates the MD5 digest and returns the value as a 32 character hex string.
+ *
+ * @param s Data to digest.
+ * @return MD5 digest as a hex string.
+ */
+ public static String md5Hex(String s) {
+ if (s == null) {
+ return null;
+ }
+
+ try {
+ MessageDigest md5 = MessageDigest.getInstance("MD5");
+ return hexEncode(md5.digest(s.getBytes(Constants.UTF_8)));
+ } catch (Exception x) {
+ throw new RuntimeException(x.getMessage(), x);
+ }
+ }
+
+ public static boolean isNullOrWhiteSpace(String string) {
+ return string == null || string.isEmpty() || string.trim().isEmpty();
+ }
+
+ public static boolean isNetworkConnected(Context context) {
+ ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo networkInfo = manager.getActiveNetworkInfo();
+ boolean connected = networkInfo != null && networkInfo.isConnected();
+
+ boolean wifiConnected = connected && networkInfo.getType() == ConnectivityManager.TYPE_WIFI;
+ boolean wifiRequired = isWifiRequiredForDownload(context);
+
+ return connected && (!wifiRequired || wifiConnected);
+ }
+
+ public static boolean isExternalStoragePresent() {
+ return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState());
+ }
+
+ private static boolean isWifiRequiredForDownload(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getBoolean(Constants.PREFERENCES_KEY_WIFI_REQUIRED_FOR_DOWNLOAD, false);
+ }
+
+ public static void info(Context context, int titleId, int messageId) {
+ showDialog(context, android.R.drawable.ic_dialog_info, titleId, messageId);
+ }
+ public static void info(Context context, int titleId, String message) {
+ showDialog(context, android.R.drawable.ic_dialog_info, titleId, message);
+ }
+ public static void info(Context context, String title, String message) {
+ showDialog(context, android.R.drawable.ic_dialog_info, title, message);
+ }
+
+ private static void showDialog(Context context, int icon, int titleId, int messageId) {
+ showDialog(context, icon, context.getResources().getString(titleId), context.getResources().getString(messageId));
+ }
+ private static void showDialog(Context context, int icon, int titleId, String message) {
+ showDialog(context, icon, context.getResources().getString(titleId), message);
+ }
+ private static void showDialog(Context context, int icon, String title, String message) {
+ SpannableString ss = new SpannableString(message);
+ Linkify.addLinks(ss, Linkify.ALL);
+
+ AlertDialog dialog = new AlertDialog.Builder(context)
+ .setIcon(icon)
+ .setTitle(title)
+ .setMessage(ss)
+ .setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int i) {
+ dialog.dismiss();
+ }
+ })
+ .show();
+
+ ((TextView)dialog.findViewById(android.R.id.message)).setMovementMethod(LinkMovementMethod.getInstance());
+ }
+
+ public static void showPlayingNotification(final Context context, final DownloadServiceImpl downloadService, Handler handler, MusicDirectory.Entry song) {
+ // Set the icon, scrolling text and timestamp
+ final Notification notification = new Notification(R.drawable.stat_notify_playing, song.getTitle(), System.currentTimeMillis());
+ notification.flags |= Notification.FLAG_NO_CLEAR | Notification.FLAG_ONGOING_EVENT;
+
+ boolean playing = downloadService.getPlayerState() == PlayerState.STARTED;
+ if (Build.VERSION.SDK_INT>= Build.VERSION_CODES.JELLY_BEAN){
+ RemoteViews expandedContentView = new RemoteViews(context.getPackageName(), R.layout.notification_expanded);
+ setupViews(expandedContentView,context,song, playing);
+ notification.bigContentView = expandedContentView;
+ }
+
+ RemoteViews smallContentView = new RemoteViews(context.getPackageName(), R.layout.notification);
+ setupViews(smallContentView, context, song, playing);
+ notification.contentView = smallContentView;
+
+ Intent notificationIntent = new Intent(context, MainActivity.class);
+ notificationIntent.putExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD, true);
+ notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ notification.contentIntent = PendingIntent.getActivity(context, 0, notificationIntent, 0);
+
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ downloadService.startForeground(Constants.NOTIFICATION_ID_PLAYING, notification);
+ }
+ });
+
+ // Update widget
+ DSubWidgetProvider.notifyInstances(context, downloadService, true);
+ }
+
+ private static void setupViews(RemoteViews rv, Context context, MusicDirectory.Entry song, boolean playing){
+
+ // Use the same text for the ticker and the expanded notification
+ String title = song.getTitle();
+ String arist = song.getArtist();
+ String album = song.getAlbum();
+
+ // Set the album art.
+ try {
+ int size = context.getResources().getDrawable(R.drawable.unknown_album).getIntrinsicHeight();
+ Bitmap bitmap = FileUtil.getAlbumArtBitmap(context, song, size);
+ if (bitmap == null) {
+ // set default album art
+ rv.setImageViewResource(R.id.notification_image, R.drawable.unknown_album);
+ } else {
+ rv.setImageViewBitmap(R.id.notification_image, bitmap);
+ }
+ } catch (Exception x) {
+ Log.w(TAG, "Failed to get notification cover art", x);
+ rv.setImageViewResource(R.id.notification_image, R.drawable.unknown_album);
+ }
+
+ // set the text for the notifications
+ rv.setTextViewText(R.id.notification_title, title);
+ rv.setTextViewText(R.id.notification_artist, arist);
+ rv.setTextViewText(R.id.notification_album, album);
+
+ Pair<Integer, Integer> colors = getNotificationTextColors(context);
+ if (colors.getFirst() != null) {
+ rv.setTextColor(R.id.notification_title, colors.getFirst());
+ }
+ if (colors.getSecond() != null) {
+ rv.setTextColor(R.id.notification_artist, colors.getSecond());
+ }
+
+ if(!playing) {
+ rv.setImageViewResource(R.id.control_pause, R.drawable.notification_play);
+ rv.setImageViewResource(R.id.control_previous, R.drawable.notification_stop);
+ }
+
+ // Create actions for media buttons
+ PendingIntent pendingIntent;
+ if(playing) {
+ Intent prevIntent = new Intent("KEYCODE_MEDIA_PREVIOUS");
+ prevIntent.setComponent(new ComponentName(context, DownloadServiceImpl.class));
+ prevIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PREVIOUS));
+ pendingIntent = PendingIntent.getService(context, 0, prevIntent, 0);
+ rv.setOnClickPendingIntent(R.id.control_previous, pendingIntent);
+ } else {
+ Intent prevIntent = new Intent("KEYCODE_MEDIA_STOP");
+ prevIntent.setComponent(new ComponentName(context, DownloadServiceImpl.class));
+ prevIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_STOP));
+ pendingIntent = PendingIntent.getService(context, 0, prevIntent, 0);
+ rv.setOnClickPendingIntent(R.id.control_previous, pendingIntent);
+ }
+
+ Intent pauseIntent = new Intent("KEYCODE_MEDIA_PLAY_PAUSE");
+ pauseIntent.setComponent(new ComponentName(context, DownloadServiceImpl.class));
+ pauseIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE));
+ pendingIntent = PendingIntent.getService(context, 0, pauseIntent, 0);
+ rv.setOnClickPendingIntent(R.id.control_pause, pendingIntent);
+
+ Intent nextIntent = new Intent("KEYCODE_MEDIA_NEXT");
+ nextIntent.setComponent(new ComponentName(context, DownloadServiceImpl.class));
+ nextIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_NEXT));
+ pendingIntent = PendingIntent.getService(context, 0, nextIntent, 0);
+ rv.setOnClickPendingIntent(R.id.control_next, pendingIntent);
+ }
+
+ public static void hidePlayingNotification(final Context context, final DownloadServiceImpl downloadService, Handler handler) {
+ // Remove notification and remove the service from the foreground
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ downloadService.stopForeground(true);
+ }
+ });
+
+ // Update widget
+ DSubWidgetProvider.notifyInstances(context, downloadService, false);
+ }
+
+ public static void sleepQuietly(long millis) {
+ try {
+ Thread.sleep(millis);
+ } catch (InterruptedException x) {
+ Log.w(TAG, "Interrupted from sleep.", x);
+ }
+ }
+
+ public static void startActivityWithoutTransition(Activity currentActivity, Class<? extends Activity> newActivitiy) {
+ startActivityWithoutTransition(currentActivity, new Intent(currentActivity, newActivitiy));
+ }
+
+ public static void startActivityWithoutTransition(Activity currentActivity, Intent intent) {
+ currentActivity.startActivity(intent);
+ disablePendingTransition(currentActivity);
+ }
+
+ public static void disablePendingTransition(Activity activity) {
+
+ // Activity.overridePendingTransition() was introduced in Android 2.0. Use reflection to maintain
+ // compatibility with 1.5.
+ try {
+ Method method = Activity.class.getMethod("overridePendingTransition", int.class, int.class);
+ method.invoke(activity, 0, 0);
+ } catch (Throwable x) {
+ // Ignored
+ }
+ }
+
+ public static Drawable createDrawableFromBitmap(Context context, Bitmap bitmap) {
+ // BitmapDrawable(Resources, Bitmap) was introduced in Android 1.6. Use reflection to maintain
+ // compatibility with 1.5.
+ try {
+ Constructor<BitmapDrawable> constructor = BitmapDrawable.class.getConstructor(Resources.class, Bitmap.class);
+ return constructor.newInstance(context.getResources(), bitmap);
+ } catch (Throwable x) {
+ return new BitmapDrawable(bitmap);
+ }
+ }
+
+ public static void registerMediaButtonEventReceiver(Context context) {
+
+ // Only do it if enabled in the settings.
+ SharedPreferences prefs = getPreferences(context);
+ boolean enabled = prefs.getBoolean(Constants.PREFERENCES_KEY_MEDIA_BUTTONS, true);
+
+ if (enabled) {
+
+ // AudioManager.registerMediaButtonEventReceiver() was introduced in Android 2.2.
+ // Use reflection to maintain compatibility with 1.5.
+ try {
+ AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ ComponentName componentName = new ComponentName(context.getPackageName(), MediaButtonIntentReceiver.class.getName());
+ Method method = AudioManager.class.getMethod("registerMediaButtonEventReceiver", ComponentName.class);
+ method.invoke(audioManager, componentName);
+ } catch (Throwable x) {
+ // Ignored.
+ }
+ }
+ }
+
+ public static void unregisterMediaButtonEventReceiver(Context context) {
+ // AudioManager.unregisterMediaButtonEventReceiver() was introduced in Android 2.2.
+ // Use reflection to maintain compatibility with 1.5.
+ try {
+ AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ ComponentName componentName = new ComponentName(context.getPackageName(), MediaButtonIntentReceiver.class.getName());
+ Method method = AudioManager.class.getMethod("unregisterMediaButtonEventReceiver", ComponentName.class);
+ method.invoke(audioManager, componentName);
+ } catch (Throwable x) {
+ // Ignored.
+ }
+ }
+
+ @TargetApi(8)
+ public static void requestAudioFocus(final Context context) {
+ if (Build.VERSION.SDK_INT >= 8 && !hasFocus) {
+ final AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ hasFocus = true;
+ audioManager.requestAudioFocus(new OnAudioFocusChangeListener() {
+ public void onAudioFocusChange(int focusChange) {
+ DownloadServiceImpl downloadService = (DownloadServiceImpl)context;
+ if((focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT || focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) && !downloadService.isJukeboxEnabled()) {
+ if(downloadService.getPlayerState() == PlayerState.STARTED) {
+ SharedPreferences prefs = getPreferences(context);
+ int lossPref = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_TEMP_LOSS, "1"));
+ if(lossPref == 2 || (lossPref == 1 && focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK)) {
+ lowerFocus = true;
+ downloadService.setVolume(0.1f);
+ } else if(lossPref == 0 || (lossPref == 1 && focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT)) {
+ pauseFocus = true;
+ downloadService.pause();
+ }
+ }
+ } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
+ if(pauseFocus) {
+ pauseFocus = false;
+ downloadService.start();
+ } else if(lowerFocus) {
+ lowerFocus = false;
+ downloadService.setVolume(1.0f);
+ }
+ } else if(focusChange == AudioManager.AUDIOFOCUS_LOSS && !downloadService.isJukeboxEnabled()) {
+ hasFocus = false;
+ downloadService.pause();
+ audioManager.abandonAudioFocus(this);
+ }
+ }
+ }, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
+ }
+ }
+
+ /**
+ * <p>Broadcasts the given song info as the new song being played.</p>
+ */
+ public static void broadcastNewTrackInfo(Context context, MusicDirectory.Entry song) {
+ DownloadService downloadService = (DownloadServiceImpl)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);
+ avrcpIntent.putExtra("track", song.getTitle());
+ avrcpIntent.putExtra("artist", song.getArtist());
+ avrcpIntent.putExtra("album", song.getAlbum());
+ avrcpIntent.putExtra("ListSize",(long) downloadService.getSongs().size());
+ avrcpIntent.putExtra("id", (long) downloadService.getCurrentPlayingIndex()+1);
+ avrcpIntent.putExtra("duration", (long) downloadService.getPlayerDuration());
+ avrcpIntent.putExtra("position", (long) downloadService.getPlayerPosition());
+ avrcpIntent.putExtra("coverart", albumArtFile.getAbsolutePath());
+ } else {
+ intent.putExtra("title", "");
+ intent.putExtra("artist", "");
+ intent.putExtra("album", "");
+ intent.putExtra("coverart", "");
+
+ avrcpIntent.putExtra("playing", false);
+ avrcpIntent.putExtra("track", "");
+ avrcpIntent.putExtra("artist", "");
+ avrcpIntent.putExtra("album", "");
+ avrcpIntent.putExtra("ListSize",(long)0);
+ avrcpIntent.putExtra("id", (long) 0);
+ avrcpIntent.putExtra("duration", (long )0);
+ avrcpIntent.putExtra("position", (long) 0);
+ avrcpIntent.putExtra("coverart", "");
+ }
+
+ context.sendBroadcast(intent);
+ context.sendBroadcast(avrcpIntent);
+ }
+
+ /**
+ * <p>Broadcasts the given player state as the one being set.</p>
+ */
+ public static void broadcastPlaybackStatusChange(Context context, 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 COMPLETED:
+ intent.putExtra("state", "complete");
+ avrcpIntent.putExtra("playing", false);
+ break;
+ default:
+ return; // No need to broadcast.
+ }
+
+ context.sendBroadcast(intent);
+ context.sendBroadcast(avrcpIntent);
+ }
+
+ /**
+ * Resolves the default text color for notifications.
+ *
+ * Based on http://stackoverflow.com/questions/4867338/custom-notification-layouts-and-text-colors/7320604#7320604
+ */
+ private static Pair<Integer, Integer> getNotificationTextColors(Context context) {
+ if (NOTIFICATION_TEXT_COLORS.getFirst() == null && NOTIFICATION_TEXT_COLORS.getSecond() == null) {
+ try {
+ Notification notification = new Notification();
+ String title = "title";
+ String content = "content";
+ notification.setLatestEventInfo(context, title, content, null);
+ LinearLayout group = new LinearLayout(context);
+ ViewGroup event = (ViewGroup) notification.contentView.apply(context, group);
+ findNotificationTextColors(event, title, content);
+ group.removeAllViews();
+ } catch (Exception x) {
+ Log.w(TAG, "Failed to resolve notification text colors.", x);
+ }
+ }
+ return NOTIFICATION_TEXT_COLORS;
+ }
+
+ private static void findNotificationTextColors(ViewGroup group, String title, String content) {
+ for (int i = 0; i < group.getChildCount(); i++) {
+ if (group.getChildAt(i) instanceof TextView) {
+ TextView textView = (TextView) group.getChildAt(i);
+ String text = textView.getText().toString();
+ if (title.equals(text)) {
+ NOTIFICATION_TEXT_COLORS.setFirst(textView.getTextColors().getDefaultColor());
+ }
+ else if (content.equals(text)) {
+ NOTIFICATION_TEXT_COLORS.setSecond(textView.getTextColors().getDefaultColor());
+ }
+ }
+ else if (group.getChildAt(i) instanceof ViewGroup)
+ findNotificationTextColors((ViewGroup) group.getChildAt(i), title, content);
+ }
+ }
+
+ public static WifiManager.WifiLock createWifiLock(Context context, String tag) {
+ WifiManager wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+ int lockType = WifiManager.WIFI_MODE_FULL;
+ if (Build.VERSION.SDK_INT >= 12) {
+ lockType = 3;
+ }
+ return wm.createWifiLock(lockType, tag);
+ }
+}
diff --git a/src/github/daneren2005/dsub/util/compat/RemoteControlClientBase.java b/src/github/daneren2005/dsub/util/compat/RemoteControlClientBase.java
new file mode 100644
index 00000000..c3f3f70c
--- /dev/null
+++ b/src/github/daneren2005/dsub/util/compat/RemoteControlClientBase.java
@@ -0,0 +1,32 @@
+package github.daneren2005.dsub.util.compat;
+
+import github.daneren2005.dsub.domain.MusicDirectory.Entry;
+import android.content.ComponentName;
+import android.content.Context;
+import android.util.Log;
+
+public class RemoteControlClientBase extends RemoteControlClientHelper {
+
+ private static final String TAG = RemoteControlClientBase.class.getSimpleName();
+
+ @Override
+ public void register(Context context, ComponentName mediaButtonReceiverComponent) {
+ Log.w(TAG, "RemoteControlClient requires Android API level 14 or higher.");
+ }
+
+ @Override
+ public void unregister(Context context) {
+ Log.w(TAG, "RemoteControlClient requires Android API level 14 or higher.");
+ }
+
+ @Override
+ public void setPlaybackState(int state) {
+ Log.w(TAG, "RemoteControlClient requires Android API level 14 or higher.");
+ }
+
+ @Override
+ public void updateMetadata(Context context, Entry currentSong) {
+ Log.w(TAG, "RemoteControlClient requires Android API level 14 or higher.");
+ }
+
+}
diff --git a/src/github/daneren2005/dsub/util/compat/RemoteControlClientHelper.java b/src/github/daneren2005/dsub/util/compat/RemoteControlClientHelper.java
new file mode 100644
index 00000000..ddaa9f43
--- /dev/null
+++ b/src/github/daneren2005/dsub/util/compat/RemoteControlClientHelper.java
@@ -0,0 +1,27 @@
+package github.daneren2005.dsub.util.compat;
+
+import github.daneren2005.dsub.domain.MusicDirectory;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.Build;
+
+public abstract class RemoteControlClientHelper {
+
+ public static RemoteControlClientHelper createInstance() {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ return new RemoteControlClientBase();
+ } else {
+ return new RemoteControlClientICS();
+ }
+ }
+
+ protected RemoteControlClientHelper() {
+ // Avoid instantiation
+ }
+
+ public abstract void register(final Context context, final ComponentName mediaButtonReceiverComponent);
+ public abstract void unregister(final Context context);
+ public abstract void setPlaybackState(final int state);
+ public abstract void updateMetadata(final Context context, final MusicDirectory.Entry currentSong);
+
+}
diff --git a/src/github/daneren2005/dsub/util/compat/RemoteControlClientICS.java b/src/github/daneren2005/dsub/util/compat/RemoteControlClientICS.java
new file mode 100644
index 00000000..a8fed63d
--- /dev/null
+++ b/src/github/daneren2005/dsub/util/compat/RemoteControlClientICS.java
@@ -0,0 +1,79 @@
+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;
+
+@TargetApi(14)
+public class RemoteControlClientICS extends RemoteControlClientHelper {
+
+ private RemoteControlClient mRemoteControl;
+ private ImageLoader imageLoader;
+
+ public void register(final Context context, final ComponentName mediaButtonReceiverComponent) {
+ AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+
+ // build the PendingIntent for the remote control client
+ Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
+ mediaButtonIntent.setComponent(mediaButtonReceiverComponent);
+ PendingIntent mediaPendingIntent = PendingIntent.getBroadcast(context.getApplicationContext(), 0, mediaButtonIntent, 0);
+
+ // create and register the remote control client
+ mRemoteControl = new RemoteControlClient(mediaPendingIntent);
+ audioManager.registerRemoteControlClient(mRemoteControl);
+
+ mRemoteControl.setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED);
+
+ mRemoteControl.setTransportControlFlags(
+ RemoteControlClient.FLAG_KEY_MEDIA_PLAY |
+ RemoteControlClient.FLAG_KEY_MEDIA_PAUSE |
+ RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE |
+ RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS |
+ RemoteControlClient.FLAG_KEY_MEDIA_NEXT |
+ RemoteControlClient.FLAG_KEY_MEDIA_STOP);
+
+ imageLoader = SubsonicActivity.getStaticImageLoader(context);
+ }
+
+ public void unregister(final Context context) {
+ if (mRemoteControl != null) {
+ AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ audioManager.unregisterRemoteControlClient(mRemoteControl);
+ }
+ }
+
+ public void setPlaybackState(final int state) {
+ mRemoteControl.setPlaybackState(state);
+ }
+
+ public void updateMetadata(final Context context, final MusicDirectory.Entry currentSong) {
+ if(imageLoader == null) {
+ imageLoader = SubsonicActivity.getStaticImageLoader(context);
+ }
+
+ // Update the remote controls
+ mRemoteControl.editMetadata(true)
+ .putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, (currentSong == null) ? null : currentSong.getArtist())
+ .putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, (currentSong == null) ? null : currentSong.getArtist())
+ .putString(MediaMetadataRetriever.METADATA_KEY_TITLE, (currentSong) == null ? null : currentSong.getTitle())
+ .putLong(MediaMetadataRetriever.METADATA_KEY_DURATION, (currentSong == null) ?
+ 0 : ((currentSong.getDuration() == null) ? 0 : currentSong.getDuration()))
+ .apply();
+ if (currentSong == null || imageLoader == null) {
+ mRemoteControl.editMetadata(true)
+ .putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, null)
+ .apply();
+ } else {
+ imageLoader.loadImage(context, mRemoteControl, currentSong);
+ }
+ }
+
+}
diff --git a/src/github/daneren2005/dsub/view/AlbumListAdapter.java b/src/github/daneren2005/dsub/view/AlbumListAdapter.java
new file mode 100644
index 00000000..3ff8350b
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/AlbumListAdapter.java
@@ -0,0 +1,83 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.view;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import com.commonsware.cwac.endless.EndlessAdapter;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import java.util.List;
+
+public class AlbumListAdapter extends EndlessAdapter {
+ Context context;
+ ArrayAdapter<MusicDirectory.Entry> adapter;
+ String type;
+ String extra;
+ int size;
+ int offset;
+ List<MusicDirectory.Entry> entries;
+
+ public AlbumListAdapter(Context context, ArrayAdapter<MusicDirectory.Entry> adapter, String type, String extra, int size) {
+ super(adapter);
+ this.context = context;
+ this.adapter = adapter;
+ this.type = type;
+ this.extra = extra;
+ this.size = size;
+ this.offset = size;
+ }
+
+ @Override
+ protected boolean cacheInBackground() throws Exception {
+ MusicService service = MusicServiceFactory.getMusicService(context);
+ MusicDirectory result;
+ if("genres".equals(type)) {
+ result = service.getSongsByGenre(extra, size, offset, context, null);
+ } else {
+ result = service.getAlbumList(type, size, offset, context, null);
+ }
+ entries = result.getChildren();
+ if(entries.size() > 0) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ protected void appendCachedData() {
+ for(MusicDirectory.Entry entry: entries) {
+ adapter.add(entry);
+ }
+ offset += entries.size();
+ }
+
+ @Override
+ protected View getPendingView(ViewGroup parent) {
+ View progress = LayoutInflater.from(context).inflate(R.layout.tab_progress, null);
+ progress.setVisibility(View.VISIBLE);
+ return progress;
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/AlbumView.java b/src/github/daneren2005/dsub/view/AlbumView.java
new file mode 100644
index 00000000..3a7b895e
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/AlbumView.java
@@ -0,0 +1,93 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.view;
+
+import android.content.Context;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.TextView;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.ImageLoader;
+import github.daneren2005.dsub.util.Util;
+import java.io.File;
+/**
+ * Used to display albums in a {@code ListView}.
+ *
+ * @author Sindre Mehus
+ */
+public class AlbumView extends UpdateView {
+ private static final String TAG = AlbumView.class.getSimpleName();
+
+ private Context context;
+ private MusicDirectory.Entry album;
+
+ private TextView titleView;
+ private TextView artistView;
+ private View coverArtView;
+ private ImageButton starButton;
+ private ImageView moreButton;
+
+ public AlbumView(Context context) {
+ super(context);
+ this.context = context;
+ LayoutInflater.from(context).inflate(R.layout.album_list_item, this, true);
+
+ titleView = (TextView) findViewById(R.id.album_title);
+ artistView = (TextView) findViewById(R.id.album_artist);
+ coverArtView = findViewById(R.id.album_coverart);
+ starButton = (ImageButton) findViewById(R.id.album_star);
+
+ moreButton = (ImageView) findViewById(R.id.album_more);
+ moreButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ v.showContextMenu();
+ }
+ });
+ }
+
+ public void setAlbum(MusicDirectory.Entry album, ImageLoader imageLoader) {
+ this.album = album;
+
+ titleView.setText(album.getTitle());
+ artistView.setText(album.getArtist());
+ artistView.setVisibility(album.getArtist() == null ? View.GONE : View.VISIBLE);
+ imageLoader.loadImage(coverArtView, album, false, true);
+
+ starButton.setVisibility(!album.isStarred() ? View.GONE : View.VISIBLE);
+ starButton.setFocusable(false);
+
+ update();
+ }
+
+ @Override
+ protected void update() {
+ starButton.setVisibility(!album.isStarred() ? View.GONE : View.VISIBLE);
+ File file = FileUtil.getAlbumDirectory(context, album);
+ if(file.exists()) {
+ moreButton.setImageResource(R.drawable.list_item_more_shaded);
+ } else {
+ moreButton.setImageResource(R.drawable.list_item_more);
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/ArtistAdapter.java b/src/github/daneren2005/dsub/view/ArtistAdapter.java
new file mode 100644
index 00000000..7e9bf218
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/ArtistAdapter.java
@@ -0,0 +1,95 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.view;
+
+import android.content.Context;
+import github.daneren2005.dsub.R;
+import java.util.List;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.SectionIndexer;
+import github.daneren2005.dsub.domain.Artist;
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+/**
+ * @author Sindre Mehus
+ */
+public class ArtistAdapter extends ArrayAdapter<Artist> implements SectionIndexer {
+
+ private final Context activity;
+
+ // Both arrays are indexed by section ID.
+ private final Object[] sections;
+ private final Integer[] positions;
+
+ public ArtistAdapter(Context activity, List<Artist> artists) {
+ super(activity, R.layout.artist_list_item, artists);
+ this.activity = activity;
+
+ Set<String> sectionSet = new LinkedHashSet<String>(30);
+ List<Integer> positionList = new ArrayList<Integer>(30);
+ for (int i = 0; i < artists.size(); i++) {
+ Artist artist = artists.get(i);
+ String index = artist.getIndex();
+ if (!sectionSet.contains(index)) {
+ sectionSet.add(index);
+ positionList.add(i);
+ }
+ }
+ sections = sectionSet.toArray(new Object[sectionSet.size()]);
+ positions = positionList.toArray(new Integer[positionList.size()]);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ Artist entry = getItem(position);
+ ArtistView view;
+ if (convertView != null && convertView instanceof ArtistView) {
+ view = (ArtistView) convertView;
+ } else {
+ view = new ArtistView(activity);
+ }
+ view.setArtist(entry);
+ return view;
+ }
+
+ @Override
+ public Object[] getSections() {
+ return sections;
+ }
+
+ @Override
+ public int getPositionForSection(int section) {
+ section = Math.min(section, positions.length - 1);
+ return positions[section];
+ }
+
+ @Override
+ public int getSectionForPosition(int pos) {
+ for (int i = 0; i < sections.length - 1; i++) {
+ if (pos < positions[i + 1]) {
+ return i;
+ }
+ }
+ return sections.length - 1;
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/ArtistEntryView.java b/src/github/daneren2005/dsub/view/ArtistEntryView.java
new file mode 100644
index 00000000..3b6a50e4
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/ArtistEntryView.java
@@ -0,0 +1,83 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.view;
+
+import android.content.Context;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.TextView;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.ImageLoader;
+import github.daneren2005.dsub.util.Util;
+import java.io.File;
+/**
+ * Used to display albums in a {@code ListView}.
+ *
+ * @author Sindre Mehus
+ */
+public class ArtistEntryView extends UpdateView {
+ private static final String TAG = AlbumView.class.getSimpleName();
+
+ private Context context;
+ private MusicDirectory.Entry artist;
+
+ private TextView titleView;
+ private ImageButton starButton;
+ private ImageView moreButton;
+
+ public ArtistEntryView(Context context) {
+ super(context);
+ this.context = context;
+ LayoutInflater.from(context).inflate(R.layout.artist_list_item, this, true);
+
+ titleView = (TextView) findViewById(R.id.artist_name);
+ starButton = (ImageButton) findViewById(R.id.artist_star);
+ moreButton = (ImageView) findViewById(R.id.artist_more);
+ moreButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ v.showContextMenu();
+ }
+ });
+ }
+
+ public void setArtist(MusicDirectory.Entry artist) {
+ this.artist = artist;
+
+ titleView.setText(artist.getTitle());
+ starButton.setVisibility((Util.isOffline(getContext()) || !artist.isStarred()) ? View.GONE : View.VISIBLE);
+ starButton.setFocusable(false);
+ update();
+ }
+
+ @Override
+ protected void update() {
+ starButton.setVisibility((Util.isOffline(getContext()) || !artist.isStarred()) ? View.GONE : View.VISIBLE);
+ File file = FileUtil.getArtistDirectory(context, artist);
+ if(file.exists()) {
+ moreButton.setImageResource(R.drawable.list_item_more_shaded);
+ } else {
+ moreButton.setImageResource(R.drawable.list_item_more);
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/ArtistView.java b/src/github/daneren2005/dsub/view/ArtistView.java
new file mode 100644
index 00000000..353be618
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/ArtistView.java
@@ -0,0 +1,84 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.view;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.TextView;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.Artist;
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.Util;
+import java.io.File;
+
+/**
+ * Used to display albums in a {@code ListView}.
+ *
+ * @author Sindre Mehus
+ */
+public class ArtistView extends UpdateView {
+ private static final String TAG = ArtistView.class.getSimpleName();
+
+ private Context context;
+ private Artist artist;
+
+ private TextView titleView;
+ private ImageButton starButton;
+ private ImageView moreButton;
+
+ public ArtistView(Context context) {
+ super(context);
+ this.context = context;
+ LayoutInflater.from(context).inflate(R.layout.artist_list_item, this, true);
+
+ titleView = (TextView) findViewById(R.id.artist_name);
+ starButton = (ImageButton) findViewById(R.id.artist_star);
+ moreButton = (ImageView) findViewById(R.id.artist_more);
+ moreButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ v.showContextMenu();
+ }
+ });
+ }
+
+ public void setArtist(Artist artist) {
+ this.artist = artist;
+
+ titleView.setText(artist.getName());
+
+ starButton.setVisibility((Util.isOffline(getContext()) || !artist.isStarred()) ? View.GONE : View.VISIBLE);
+ starButton.setFocusable(false);
+
+ update();
+ }
+
+ @Override
+ protected void update() {
+ starButton.setVisibility((Util.isOffline(getContext()) || !artist.isStarred()) ? View.GONE : View.VISIBLE);
+ File file = FileUtil.getArtistDirectory(context, artist);
+ if(file.exists()) {
+ moreButton.setImageResource(R.drawable.list_item_more_shaded);
+ } else {
+ moreButton.setImageResource(R.drawable.list_item_more);
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/AutoRepeatButton.java b/src/github/daneren2005/dsub/view/AutoRepeatButton.java
new file mode 100644
index 00000000..798c1649
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/AutoRepeatButton.java
@@ -0,0 +1,86 @@
+package github.daneren2005.dsub.view;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.ImageButton;
+
+public class AutoRepeatButton extends ImageButton {
+
+ private long initialRepeatDelay = 1000;
+ private long repeatIntervalInMilliseconds = 300;
+ private boolean doClick = true;
+ private Runnable repeatEvent = null;
+
+ private Runnable repeatClickWhileButtonHeldRunnable = new Runnable() {
+ @Override
+ public void run() {
+ doClick = false;
+ //Perform the present repetition of the click action provided by the user
+ // in setOnClickListener().
+ if(repeatEvent != null)
+ repeatEvent.run();
+
+ //Schedule the next repetitions of the click action, using a faster repeat
+ // interval than the initial repeat delay interval.
+ postDelayed(repeatClickWhileButtonHeldRunnable, repeatIntervalInMilliseconds);
+ }
+ };
+
+ private void commonConstructorCode() {
+ this.setOnTouchListener(new OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ int action = event.getAction();
+ if(action == MotionEvent.ACTION_DOWN)
+ {
+ doClick = true;
+ //Just to be sure that we removed all callbacks,
+ // which should have occurred in the ACTION_UP
+ removeCallbacks(repeatClickWhileButtonHeldRunnable);
+
+ //Schedule the start of repetitions after a one half second delay.
+ postDelayed(repeatClickWhileButtonHeldRunnable, initialRepeatDelay);
+
+ setPressed(true);
+ }
+ else if(action == MotionEvent.ACTION_UP) {
+ //Cancel any repetition in progress.
+ removeCallbacks(repeatClickWhileButtonHeldRunnable);
+
+ if(doClick || repeatEvent == null) {
+ performClick();
+ }
+
+ setPressed(false);
+ }
+
+ //Returning true here prevents performClick() from getting called
+ // in the usual manner, which would be redundant, given that we are
+ // already calling it above.
+ return true;
+ }
+ });
+ }
+
+ public void setOnRepeatListener(Runnable runnable) {
+ repeatEvent = runnable;
+ }
+
+ public AutoRepeatButton(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ commonConstructorCode();
+ }
+
+
+ public AutoRepeatButton(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ commonConstructorCode();
+ }
+
+ public AutoRepeatButton(Context context) {
+ super(context);
+ commonConstructorCode();
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/ChangeLog.java b/src/github/daneren2005/dsub/view/ChangeLog.java
new file mode 100644
index 00000000..b847733e
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/ChangeLog.java
@@ -0,0 +1,552 @@
+/*
+ * Copyright (C) 2012 Christian Ketterer (cketti)
+ *
+ * Portions Copyright (C) 2012 Martin van Zuilekom (http://martin.cubeactive.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ *
+ * Based on android-change-log:
+ *
+ * Copyright (C) 2011, Karsten Priegnitz
+ *
+ * Permission to use, copy, modify, and distribute this piece of software
+ * for any purpose with or without fee is hereby granted, provided that
+ * the above copyright notice and this permission notice appear in the
+ * source code of all copies.
+ *
+ * It would be appreciated if you mention the author in your change log,
+ * contributors list or the like.
+ *
+ * http://code.google.com/p/android-change-log/
+ */
+package github.daneren2005.dsub.view;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.SharedPreferences;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources;
+import android.content.res.XmlResourceParser;
+import android.preference.PreferenceManager;
+import android.util.Log;
+import android.util.SparseArray;
+import android.webkit.WebView;
+import github.daneren2005.dsub.R;
+
+
+/**
+ * Display a dialog showing a full or partial (What's New) change log.
+ */
+public class ChangeLog {
+ /**
+ * Tag that is used when sending error/debug messages to the log.
+ */
+ protected static final String LOG_TAG = "ckChangeLog";
+
+ /**
+ * This is the key used when storing the version code in SharedPreferences.
+ */
+ protected static final String VERSION_KEY = "ckChangeLog_last_version_code";
+
+ /**
+ * Constant that used when no version code is available.
+ */
+ protected static final int NO_VERSION = -1;
+
+ /**
+ * Default CSS styles used to format the change log.
+ */
+ private static final String DEFAULT_CSS =
+ "div.title { margin-left: 0px; font-size: 1.2em; text-align: center;}" +
+ "div.subtitle {margin-left: 0px; font-size: .8em; text-align: center;}" +
+ "li { margin-left: 0px;}" +
+ "ul { padding-left: 2em;}";
+
+
+ /**
+ * Context that is used to access the resources and to create the ChangeLog dialogs.
+ */
+ protected final Context mContext;
+
+ /**
+ * Contains the CSS rules used to format the change log.
+ */
+ protected final String mCss;
+
+ /**
+ * Last version code read from {@code SharedPreferences} or {@link #NO_VERSION}.
+ */
+ private int mLastVersionCode;
+
+ /**
+ * Version code of the current installation.
+ */
+ private int mCurrentVersionCode;
+
+ /**
+ * Version name of the current installation.
+ */
+ private String mCurrentVersionName;
+
+
+ /**
+ * Contains constants for the root element of {@code changelog.xml}.
+ */
+ protected interface ChangeLogTag {
+ static final String NAME = "changelog";
+ }
+
+ /**
+ * Contains constants for the release element of {@code changelog.xml}.
+ */
+ protected interface ReleaseTag {
+ static final String NAME = "release";
+ static final String ATTRIBUTE_VERSION = "version";
+ static final String ATTRIBUTE_VERSION_CODE = "versioncode";
+ static final String ATTRIBUTE_RELEASE_DATE = "releasedate";
+ }
+
+ /**
+ * Contains constants for the change element of {@code changelog.xml}.
+ */
+ protected interface ChangeTag {
+ static final String NAME = "change";
+ }
+
+ /**
+ * Create a {@code ChangeLog} instance using the default {@link SharedPreferences} file.
+ *
+ * @param context
+ * Context that is used to access the resources and to create the ChangeLog dialogs.
+ */
+ public ChangeLog(Context context) {
+ this(context, PreferenceManager.getDefaultSharedPreferences(context), DEFAULT_CSS);
+ }
+
+ /**
+ * Create a {@code ChangeLog} instance using the default {@link SharedPreferences} file.
+ *
+ * @param context
+ * Context that is used to access the resources and to create the ChangeLog dialogs.
+ * @param css
+ * CSS styles that will be used to format the change log.
+ */
+ public ChangeLog(Context context, String css) {
+ this(context, PreferenceManager.getDefaultSharedPreferences(context), css);
+ }
+
+ public ChangeLog(Context context, SharedPreferences preferences) {
+ this(context, preferences, DEFAULT_CSS);
+ }
+
+ /**
+ * Create a {@code ChangeLog} instance using the supplied {@code SharedPreferences} instance.
+ *
+ * @param context
+ * Context that is used to access the resources and to create the ChangeLog dialogs.
+ * @param preferences
+ * {@code SharedPreferences} instance that is used to persist the last version code.
+ * @param css
+ * CSS styles used to format the change log (excluding {@code <style>} and
+ * {@code </style>}).
+ *
+ */
+ public ChangeLog(Context context, SharedPreferences preferences, String css) {
+ mContext = context;
+ mCss = css;
+
+ // Get last version code
+ mLastVersionCode = preferences.getInt(VERSION_KEY, NO_VERSION);
+
+ // Get current version code and version name
+ try {
+ PackageInfo packageInfo = context.getPackageManager().getPackageInfo(
+ context.getPackageName(), 0);
+
+ mCurrentVersionCode = packageInfo.versionCode;
+ mCurrentVersionName = packageInfo.versionName;
+ } catch (NameNotFoundException e) {
+ mCurrentVersionCode = NO_VERSION;
+ Log.e(LOG_TAG, "Could not get version information from manifest!", e);
+ }
+ }
+
+ /**
+ * Get version code of last installation.
+ *
+ * @return The version code of the last installation of this app (as described in the former
+ * manifest). This will be the same as returned by {@link #getCurrentVersionCode()} the
+ * second time this version of the app is launched (more precisely: the second time
+ * {@code ChangeLog} is instantiated).
+ *
+ * @see AndroidManifest.xml#android:versionCode
+ */
+ public int getLastVersionCode() {
+ return mLastVersionCode;
+ }
+
+ /**
+ * Get version code of current installation.
+ *
+ * @return The version code of this app as described in the manifest.
+ *
+ * @see AndroidManifest.xml#android:versionCode
+ */
+ public int getCurrentVersionCode() {
+ return mCurrentVersionCode;
+ }
+
+ /**
+ * Get version name of current installation.
+ *
+ * @return The version name of this app as described in the manifest.
+ *
+ * @see AndroidManifest.xml#android:versionName
+ */
+ public String getCurrentVersionName() {
+ return mCurrentVersionName;
+ }
+
+ /**
+ * Check if this is the first execution of this app version.
+ *
+ * @return {@code true} if this version of your app is started the first time.
+ */
+ public boolean isFirstRun() {
+ return mLastVersionCode < mCurrentVersionCode;
+ }
+
+ /**
+ * Check if this is a new installation.
+ *
+ * @return {@code true} if your app including {@code ChangeLog} is started the first time ever.
+ * Also {@code true} if your app was uninstalled and installed again.
+ */
+ public boolean isFirstRunEver() {
+ return mLastVersionCode == NO_VERSION;
+ }
+
+ /**
+ * Get the "What's New" dialog.
+ *
+ * @return An AlertDialog displaying the changes since the previous installed version of your
+ * app (What's New). But when this is the first run of your app including
+ * {@code ChangeLog} then the full log dialog is show.
+ */
+ public AlertDialog getLogDialog() {
+ return getDialog(isFirstRunEver());
+ }
+
+ /**
+ * Get a dialog with the full change log.
+ *
+ * @return An AlertDialog with a full change log displayed.
+ */
+ public AlertDialog getFullLogDialog() {
+ return getDialog(true);
+ }
+
+ /**
+ * Create a dialog containing (parts of the) change log.
+ *
+ * @param full
+ * If this is {@code true} the full change log is displayed. Otherwise only changes for
+ * versions newer than the last version are displayed.
+ *
+ * @return A dialog containing the (partial) change log.
+ */
+ protected AlertDialog getDialog(boolean full) {
+ WebView wv = new WebView(mContext);
+ //wv.setBackgroundColor(0); // transparent
+ wv.loadDataWithBaseURL(null, getLog(full), "text/html", "UTF-8", null);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
+ builder.setTitle(
+ mContext.getResources().getString(
+ full ? R.string.changelog_full_title : R.string.changelog_title))
+ .setView(wv)
+ .setCancelable(false)
+ // OK button
+ .setPositiveButton(
+ mContext.getResources().getString(R.string.changelog_ok_button),
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ // The user clicked "OK" so save the current version code as
+ // "last version code".
+ updateVersionInPreferences();
+ }
+ });
+
+ if (!full) {
+ // Show "More…" button if we're only displaying a partial change log.
+ builder.setNegativeButton(R.string.changelog_show_full,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ getFullLogDialog().show();
+ }
+ });
+ }
+
+ return builder.create();
+ }
+
+ /**
+ * Write current version code to the preferences.
+ */
+ protected void updateVersionInPreferences() {
+ SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext);
+ SharedPreferences.Editor editor = sp.edit();
+ editor.putInt(VERSION_KEY, mCurrentVersionCode);
+
+ // TODO: Update preferences from a background thread
+ editor.commit();
+ }
+
+ /**
+ * Get changes since last version as HTML string.
+ *
+ * @return HTML string containing the changes since the previous installed version of your app
+ * (What's New).
+ */
+ public String getLog() {
+ return getLog(false);
+ }
+
+ /**
+ * Get full change log as HTML string.
+ *
+ * @return HTML string containing the full change log.
+ */
+ public String getFullLog() {
+ return getLog(true);
+ }
+
+ /**
+ * Get (partial) change log as HTML string.
+ *
+ * @param full
+ * If this is {@code true} the full change log is returned. Otherwise only changes for
+ * versions newer than the last version are returned.
+ *
+ * @return The (partial) change log.
+ */
+ private String getLog(boolean full) {
+ StringBuilder sb = new StringBuilder();
+
+ sb.append("<html><head><style type=\"text/css\">");
+ sb.append(mCss);
+ sb.append("</style></head><body>");
+
+ Resources resources = mContext.getResources();
+
+ // Read master change log from raw/changelog.xml
+ SparseArray<ReleaseItem> defaultChangelog;
+ try {
+ XmlPullParser xml = XmlPullParserFactory.newInstance().newPullParser();
+ InputStreamReader reader = new InputStreamReader(resources.openRawResource(R.raw.changelog));
+ xml.setInput(reader);
+ try {
+ defaultChangelog = readChangeLog(xml, full);
+ } finally {
+ try { reader.close(); } catch (Exception e) { /* do nothing */ }
+ }
+ } catch (XmlPullParserException e) {
+ Log.e(LOG_TAG, "Error reading raw/changelog.xml", e);
+ return null;
+ }
+
+ // Read localized change log from xml[-lang]/changelog.xml
+ XmlResourceParser resXml = mContext.getResources().getXml(R.xml.changelog);
+ SparseArray<ReleaseItem> changelog;
+ try {
+ changelog = readChangeLog(resXml, full);
+ } finally {
+ resXml.close();
+ }
+
+ String versionFormat = resources.getString(R.string.changelog_version_format);
+
+ // Get all version codes from the master change log...
+ List<Integer> versions = new ArrayList<Integer>(defaultChangelog.size());
+ for (int i = 0, len = defaultChangelog.size(); i < len; i++) {
+ int key = defaultChangelog.keyAt(i);
+ versions.add(key);
+ }
+
+ // ... and sort them (newest version first).
+ Collections.sort(versions, Collections.reverseOrder());
+
+ for (Integer version : versions) {
+ int key = version.intValue();
+
+ // Use release information from localized change log and fall back to the master file
+ // if necessary.
+ ReleaseItem release = changelog.get(key, defaultChangelog.get(key));
+
+ sb.append("<div class='title'>");
+ sb.append(String.format(versionFormat, release.versionName));
+ sb.append("</div>");
+ if(release.releaseDate != null) {
+ sb.append("<div class='subtitle'>");
+ sb.append(release.releaseDate);
+ sb.append("</div>");
+ }
+ sb.append("<ul>");
+ for (String change : release.changes) {
+ sb.append("<li>");
+ sb.append(change);
+ sb.append("</li>");
+ }
+ sb.append("</ul>");
+ }
+
+ sb.append("</body></html>");
+
+ return sb.toString();
+ }
+
+ /**
+ * Read the change log from an XML file.
+ *
+ * @param xml
+ * The {@code XmlPullParser} instance used to read the change log.
+ * @param full
+ * If {@code true} the full change log is read. Otherwise only the changes since the
+ * last (saved) version are read.
+ *
+ * @return A {@code SparseArray} mapping the version codes to release information.
+ */
+ protected SparseArray<ReleaseItem> readChangeLog(XmlPullParser xml, boolean full) {
+ SparseArray<ReleaseItem> result = new SparseArray<ReleaseItem>();
+
+ try {
+ int eventType = xml.getEventType();
+ while (eventType != XmlPullParser.END_DOCUMENT) {
+ if (eventType == XmlPullParser.START_TAG && xml.getName().equals(ReleaseTag.NAME)) {
+ if (parseReleaseTag(xml, full, result)) {
+ // Stop reading more elements if this entry is not newer than the last
+ // version.
+ break;
+ }
+ }
+ eventType = xml.next();
+ }
+ } catch (XmlPullParserException e) {
+ Log.e(LOG_TAG, e.getMessage(), e);
+ } catch (IOException e) {
+ Log.e(LOG_TAG, e.getMessage(), e);
+ }
+
+ return result;
+ }
+
+ /**
+ * Parse the {@code release} tag of a change log XML file.
+ *
+ * @param xml
+ * The {@code XmlPullParser} instance used to read the change log.
+ * @param full
+ * If {@code true} the contents of the {@code release} tag are always added to
+ * {@code changelog}. Otherwise only if the item's {@code versioncode} attribute is
+ * higher than the last version code.
+ * @param changelog
+ * The {@code SparseArray} to add a new {@link ReleaseItem} instance to.
+ *
+ * @return {@code true} if the {@code release} element is describing changes of a version older
+ * or equal to the last version. In that case {@code changelog} won't be modified and
+ * {@link #readChangeLog(XmlPullParser, boolean)} will stop reading more elements from
+ * the change log file.
+ *
+ * @throws XmlPullParserException
+ * @throws IOException
+ */
+ private boolean parseReleaseTag(XmlPullParser xml, boolean full,
+ SparseArray<ReleaseItem> changelog) throws XmlPullParserException, IOException {
+
+ String version = xml.getAttributeValue(null, ReleaseTag.ATTRIBUTE_VERSION);
+
+ int versionCode;
+ try {
+ String versionCodeStr = xml.getAttributeValue(null, ReleaseTag.ATTRIBUTE_VERSION_CODE);
+ versionCode = Integer.parseInt(versionCodeStr);
+ } catch (NumberFormatException e) {
+ versionCode = NO_VERSION;
+ }
+
+ String releaseDate = xml.getAttributeValue(null, ReleaseTag.ATTRIBUTE_RELEASE_DATE);
+
+ if (!full && versionCode <= mLastVersionCode) {
+ return true;
+ }
+
+ int eventType = xml.getEventType();
+ List<String> changes = new ArrayList<String>();
+ while (eventType != XmlPullParser.END_TAG || xml.getName().equals(ChangeTag.NAME)) {
+ if (eventType == XmlPullParser.START_TAG && xml.getName().equals(ChangeTag.NAME)) {
+ eventType = xml.next();
+
+ changes.add(xml.getText());
+ }
+ eventType = xml.next();
+ }
+
+ ReleaseItem release = new ReleaseItem(versionCode, version, releaseDate, changes);
+ changelog.put(versionCode, release);
+
+ return false;
+ }
+
+ /**
+ * Container used to store information about a release/version.
+ */
+ protected static class ReleaseItem {
+ /**
+ * Version code of the release.
+ */
+ public final int versionCode;
+
+ /**
+ * Version name of the release.
+ */
+ public final String versionName;
+
+ public final String releaseDate;
+
+ /**
+ * List of changes introduced with that release.
+ */
+ public final List<String> changes;
+
+ ReleaseItem(int versionCode, String versionName, String releaseDate, List<String> changes) {
+ this.versionCode = versionCode;
+ this.versionName = versionName;
+ this.releaseDate = releaseDate;
+ this.changes = changes;
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/ChatAdapter.java b/src/github/daneren2005/dsub/view/ChatAdapter.java
new file mode 100644
index 00000000..518f81ef
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/ChatAdapter.java
@@ -0,0 +1,100 @@
+package github.daneren2005.dsub.view;
+
+import android.text.method.LinkMovementMethod;
+import android.text.util.Linkify;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.TextView;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.activity.SubsonicActivity;
+import github.daneren2005.dsub.domain.ChatMessage;
+import github.daneren2005.dsub.util.Util;
+
+import java.text.DateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.regex.Pattern;
+
+public class ChatAdapter extends ArrayAdapter<ChatMessage> {
+
+ private final SubsonicActivity activity;
+ private ArrayList<ChatMessage> messages;
+
+ private static final String phoneRegex = "1?\\W*([2-9][0-8][0-9])\\W*([2-9][0-9]{2})\\W*([0-9]{4})"; //you can just place your support phone here
+ private static final Pattern phoneMatcher = Pattern.compile(phoneRegex);
+
+ public ChatAdapter(SubsonicActivity activity, ArrayList<ChatMessage> messages) {
+ super(activity, R.layout.chat_item, messages);
+ this.activity = activity;
+ this.messages = messages;
+ }
+
+ @Override
+ public int getCount() {
+ return messages.size();
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ ChatMessage message = this.getItem(position);
+
+ ViewHolder holder;
+ int layout;
+
+ String messageUser = message.getUsername();
+ Date messageTime = new java.util.Date(message.getTime());
+ String messageText = message.getMessage();
+
+ String me = Util.getUserName(activity, Util.getActiveServer(activity));
+
+ if (messageUser.equals(me)) {
+ layout = R.layout.chat_item_reverse;
+ } else {
+ layout = R.layout.chat_item;
+ }
+
+ if (convertView == null)
+ {
+ holder = new ViewHolder();
+
+ convertView = LayoutInflater.from(activity).inflate(layout, parent, false);
+
+ TextView usernameView = (TextView) convertView.findViewById(R.id.chat_username);
+ TextView timeView = (TextView) convertView.findViewById(R.id.chat_time);
+ TextView messageView = (TextView) convertView.findViewById(R.id.chat_message);
+
+ messageView.setMovementMethod(LinkMovementMethod.getInstance());
+ Linkify.addLinks(messageView, Linkify.EMAIL_ADDRESSES);
+ Linkify.addLinks(messageView, Linkify.WEB_URLS);
+ Linkify.addLinks(messageView, phoneMatcher, "tel:");
+
+ holder.message = messageView;
+ holder.username = usernameView;
+ holder.time = timeView;
+
+ convertView.setTag(holder);
+ }
+ else
+ {
+ holder = (ViewHolder) convertView.getTag();
+ }
+
+ DateFormat timeFormat = android.text.format.DateFormat.getTimeFormat(activity);
+ String messageTimeFormatted = String.format("[%s]", timeFormat.format(messageTime));
+
+ holder.username.setText(messageUser);
+ holder.message.setText(messageText);
+ holder.time.setText(messageTimeFormatted);
+
+ return convertView;
+ }
+
+ private static class ViewHolder
+ {
+ TextView message;
+ TextView username;
+ TextView time;
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/EntryAdapter.java b/src/github/daneren2005/dsub/view/EntryAdapter.java
new file mode 100644
index 00000000..ff7393c6
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/EntryAdapter.java
@@ -0,0 +1,79 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.view;
+
+import android.content.Context;
+import android.util.Log;
+import java.util.List;
+
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.util.ImageLoader;
+
+/**
+ * @author Sindre Mehus
+ */
+public class EntryAdapter extends ArrayAdapter<MusicDirectory.Entry> {
+ private final static String TAG = EntryAdapter.class.getSimpleName();
+ private final Context activity;
+ private final ImageLoader imageLoader;
+ private final boolean checkable;
+ private List<MusicDirectory.Entry> entries;
+
+ public EntryAdapter(Context activity, ImageLoader imageLoader, List<MusicDirectory.Entry> entries, boolean checkable) {
+ super(activity, android.R.layout.simple_list_item_1, entries);
+ this.entries = entries;
+ this.activity = activity;
+ this.imageLoader = imageLoader;
+ this.checkable = checkable;
+ }
+
+ public void removeAt(int position) {
+ entries.remove(position);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ MusicDirectory.Entry entry = getItem(position);
+
+ if (entry.isDirectory()) {
+ if(entry.getArtist() != null || entry.getParent() != null) {
+ AlbumView view;
+ view = new AlbumView(activity);
+ view.setAlbum(entry, imageLoader);
+ return view;
+ } else {
+ ArtistEntryView view = new ArtistEntryView(activity);
+ view.setArtist(entry);
+ return view;
+ }
+ } else {
+ SongView view;
+ if (convertView != null && convertView instanceof SongView) {
+ view = (SongView) convertView;
+ } else {
+ view = new SongView(activity);
+ }
+ view.setSong(entry, checkable);
+ return view;
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/ErrorDialog.java b/src/github/daneren2005/dsub/view/ErrorDialog.java
new file mode 100644
index 00000000..e9f25a2d
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/ErrorDialog.java
@@ -0,0 +1,70 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.view;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import github.daneren2005.dsub.activity.MainActivity;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.util.Util;
+
+/**
+ * @author Sindre Mehus
+ */
+public class ErrorDialog {
+
+ public ErrorDialog(Activity activity, int messageId, boolean finishActivityOnCancel) {
+ this(activity, activity.getResources().getString(messageId), finishActivityOnCancel);
+ }
+
+ public ErrorDialog(final Activity activity, String message, final boolean finishActivityOnClose) {
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+ builder.setIcon(android.R.drawable.ic_dialog_alert);
+ builder.setTitle(R.string.error_label);
+ builder.setMessage(message);
+ builder.setCancelable(true);
+ builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialogInterface) {
+ if (finishActivityOnClose) {
+ restart(activity);
+ }
+ }
+ });
+ builder.setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ if (finishActivityOnClose) {
+ restart(activity);
+ }
+ }
+ });
+
+ builder.create().show();
+ }
+
+ private void restart(Activity context) {
+ Intent intent = new Intent(context, MainActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ Util.startActivityWithoutTransition(context, intent);
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/FadeOutAnimation.java b/src/github/daneren2005/dsub/view/FadeOutAnimation.java
new file mode 100644
index 00000000..292529e6
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/FadeOutAnimation.java
@@ -0,0 +1,77 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.view;
+
+import android.view.View;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+
+/**
+ * Fades a view out by changing its alpha value.
+ *
+ * @author Sindre Mehus
+ * @version $Id: Util.java 3203 2012-10-04 09:12:08Z sindre_mehus $
+ */
+public class FadeOutAnimation extends AlphaAnimation {
+
+ private boolean cancelled;
+
+ /**
+ * Creates and starts the fade out animation.
+ *
+ * @param view The view to fade out (or display).
+ * @param fadeOut If true, the view is faded out. Otherwise it is immediately made visible.
+ * @param durationMillis Fade duration.
+ */
+ public static void createAndStart(View view, boolean fadeOut, long durationMillis) {
+ if (fadeOut) {
+ view.clearAnimation();
+ view.startAnimation(new FadeOutAnimation(view, durationMillis));
+ } else {
+ Animation animation = view.getAnimation();
+ if (animation instanceof FadeOutAnimation) {
+ ((FadeOutAnimation) animation).cancelFadeOut();
+ }
+ view.clearAnimation();
+ view.setVisibility(View.VISIBLE);
+ }
+ }
+
+ FadeOutAnimation(final View view, long durationMillis) {
+ super(1.0F, 0.0F);
+ setDuration(durationMillis);
+ setAnimationListener(new AnimationListener() {
+ public void onAnimationStart(Animation animation) {
+ }
+
+ public void onAnimationRepeat(Animation animation) {
+ }
+
+ public void onAnimationEnd(Animation animation) {
+ if (!cancelled) {
+ view.setVisibility(View.INVISIBLE);
+ }
+ }
+ });
+ }
+
+ private void cancelFadeOut() {
+ cancelled = true;
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/GenreAdapter.java b/src/github/daneren2005/dsub/view/GenreAdapter.java
new file mode 100644
index 00000000..b98efd20
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/GenreAdapter.java
@@ -0,0 +1,59 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.view;
+
+import android.widget.ArrayAdapter;
+import android.widget.SectionIndexer;
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.Genre;
+
+import java.util.List;
+import java.util.Set;
+import java.util.LinkedHashSet;
+import java.util.ArrayList;
+
+/**
+ * @author Sindre Mehus
+*/
+public class GenreAdapter extends ArrayAdapter<Genre>{
+ private Context activity;
+ private List<Genre> genres;
+
+ public GenreAdapter(Context context, List<Genre> genres) {
+ super(context, android.R.layout.simple_list_item_1, genres);
+ this.activity = context;
+ this.genres = genres;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ Genre genre = genres.get(position);
+ GenreView view;
+ if (convertView != null && convertView instanceof GenreView) {
+ view = (GenreView) convertView;
+ } else {
+ view = new GenreView(activity);
+ }
+ view.setGenre(genre);
+ return view;
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/GenreView.java b/src/github/daneren2005/dsub/view/GenreView.java
new file mode 100644
index 00000000..dbb0248b
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/GenreView.java
@@ -0,0 +1,53 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.view;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.TextView;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.Genre;
+
+public class GenreView extends UpdateView {
+ private static final String TAG = GenreView.class.getSimpleName();
+
+ private TextView titleView;
+ private ImageButton starButton;
+ private ImageView moreButton;
+
+ public GenreView(Context context) {
+ super(context);
+ LayoutInflater.from(context).inflate(R.layout.artist_list_item, this, true);
+
+ titleView = (TextView) findViewById(R.id.artist_name);
+ starButton = (ImageButton) findViewById(R.id.artist_star);
+ moreButton = (ImageView) findViewById(R.id.artist_more);
+ moreButton.setClickable(false);
+ }
+
+ public void setGenre(Genre genre) {
+ titleView.setText(genre.getName());
+
+ starButton.setVisibility(View.GONE);
+ starButton.setFocusable(false);
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/MergeAdapter.java b/src/github/daneren2005/dsub/view/MergeAdapter.java
new file mode 100644
index 00000000..bfe777ea
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/MergeAdapter.java
@@ -0,0 +1,292 @@
+/***
+ Copyright (c) 2008-2009 CommonsWare, LLC
+ Portions (c) 2009 Google, Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License"); you may
+ not use this file except in compliance with the License. You may obtain
+ a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+package github.daneren2005.dsub.view;
+
+import android.database.DataSetObserver;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ListAdapter;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Arrays;
+
+import github.daneren2005.dsub.view.SackOfViewsAdapter;
+
+/**
+ * Adapter that merges multiple child adapters and views
+ * into a single contiguous whole.
+ * <p/>
+ * Adapters used as pieces within MergeAdapter must
+ * have view type IDs monotonically increasing from 0. Ideally,
+ * adapters also have distinct ranges for their row ids, as
+ * returned by getItemId().
+ */
+public class MergeAdapter extends BaseAdapter {
+
+ private final CascadeDataSetObserver observer = new CascadeDataSetObserver();
+ private final ArrayList<ListAdapter> pieces = new ArrayList<ListAdapter>();
+
+ /**
+ * Stock constructor, simply chaining to the superclass.
+ */
+ public MergeAdapter() {
+ super();
+ }
+
+ /**
+ * Adds a new adapter to the roster of things to appear
+ * in the aggregate list.
+ *
+ * @param adapter Source for row views for this section
+ */
+ public void addAdapter(ListAdapter adapter) {
+ pieces.add(adapter);
+ adapter.registerDataSetObserver(observer);
+ }
+
+ public void removeAdapter(ListAdapter adapter) {
+ adapter.unregisterDataSetObserver(observer);
+ pieces.remove(adapter);
+ }
+
+ /**
+ * Adds a new View to the roster of things to appear
+ * in the aggregate list.
+ *
+ * @param view Single view to add
+ */
+ public ListAdapter addView(View view) {
+ return addView(view, false);
+ }
+
+ /**
+ * Adds a new View to the roster of things to appear
+ * in the aggregate list.
+ *
+ * @param view Single view to add
+ * @param enabled false if views are disabled, true if enabled
+ */
+ public ListAdapter addView(View view, boolean enabled) {
+ return addViews(Arrays.asList(view), enabled);
+ }
+
+ /**
+ * Adds a list of views to the roster of things to appear
+ * in the aggregate list.
+ *
+ * @param views List of views to add
+ */
+ public ListAdapter addViews(List<View> views) {
+ return addViews(views, false);
+ }
+
+ /**
+ * Adds a list of views to the roster of things to appear
+ * in the aggregate list.
+ *
+ * @param views List of views to add
+ * @param enabled false if views are disabled, true if enabled
+ */
+ public ListAdapter addViews(List<View> views, boolean enabled) {
+ ListAdapter adapter = enabled ? new EnabledSackAdapter(views) : new SackOfViewsAdapter(views);
+ addAdapter(adapter);
+ return adapter;
+ }
+
+ /**
+ * Get the data item associated with the specified
+ * position in the data set.
+ *
+ * @param position Position of the item whose data we want
+ */
+ @Override
+ public Object getItem(int position) {
+ for (ListAdapter piece : pieces) {
+ int size = piece.getCount();
+
+ if (position < size) {
+ return (piece.getItem(position));
+ }
+
+ position -= size;
+ }
+
+ return (null);
+ }
+
+ /**
+ * How many items are in the data set represented by this
+ * Adapter.
+ */
+ @Override
+ public int getCount() {
+ int total = 0;
+
+ for (ListAdapter piece : pieces) {
+ total += piece.getCount();
+ }
+
+ return (total);
+ }
+
+ /**
+ * Returns the number of types of Views that will be
+ * created by getView().
+ */
+ @Override
+ public int getViewTypeCount() {
+ int total = 0;
+
+ for (ListAdapter piece : pieces) {
+ total += piece.getViewTypeCount();
+ }
+
+ return (Math.max(total, 1)); // needed for setListAdapter() before content add'
+ }
+
+ /**
+ * Get the type of View that will be created by getView()
+ * for the specified item.
+ *
+ * @param position Position of the item whose data we want
+ */
+ @Override
+ public int getItemViewType(int position) {
+ int typeOffset = 0;
+ int result = -1;
+
+ for (ListAdapter piece : pieces) {
+ int size = piece.getCount();
+
+ if (position < size) {
+ result = typeOffset + piece.getItemViewType(position);
+ break;
+ }
+
+ position -= size;
+ typeOffset += piece.getViewTypeCount();
+ }
+
+ return (result);
+ }
+
+ /**
+ * Are all items in this ListAdapter enabled? If yes it
+ * means all items are selectable and clickable.
+ */
+ @Override
+ public boolean areAllItemsEnabled() {
+ return (false);
+ }
+
+ /**
+ * Returns true if the item at the specified position is
+ * not a separator.
+ *
+ * @param position Position of the item whose data we want
+ */
+ @Override
+ public boolean isEnabled(int position) {
+ for (ListAdapter piece : pieces) {
+ int size = piece.getCount();
+
+ if (position < size) {
+ return (piece.isEnabled(position));
+ }
+
+ position -= size;
+ }
+
+ return (false);
+ }
+
+ /**
+ * Get a View that displays the data at the specified
+ * position in the data set.
+ *
+ * @param position Position of the item whose data we want
+ * @param convertView View to recycle, if not null
+ * @param parent ViewGroup containing the returned View
+ */
+ @Override
+ public View getView(int position, View convertView,
+ ViewGroup parent) {
+ for (ListAdapter piece : pieces) {
+ int size = piece.getCount();
+
+ if (position < size) {
+
+ return (piece.getView(position, convertView, parent));
+ }
+
+ position -= size;
+ }
+
+ return (null);
+ }
+
+ /**
+ * Get the row id associated with the specified position
+ * in the list.
+ *
+ * @param position Position of the item whose data we want
+ */
+ @Override
+ public long getItemId(int position) {
+ for (ListAdapter piece : pieces) {
+ int size = piece.getCount();
+
+ if (position < size) {
+ return (piece.getItemId(position));
+ }
+
+ position -= size;
+ }
+
+ return (-1);
+ }
+
+ private static class EnabledSackAdapter extends SackOfViewsAdapter {
+ public EnabledSackAdapter(List<View> views) {
+ super(views);
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ return (true);
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ return (true);
+ }
+ }
+
+ private class CascadeDataSetObserver extends DataSetObserver {
+ @Override
+ public void onChanged() {
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public void onInvalidated() {
+ notifyDataSetInvalidated();
+ }
+ }
+}
+
diff --git a/src/github/daneren2005/dsub/view/MyViewFlipper.java b/src/github/daneren2005/dsub/view/MyViewFlipper.java
new file mode 100644
index 00000000..26a3de08
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/MyViewFlipper.java
@@ -0,0 +1,53 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.view;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.ViewFlipper;
+
+/**
+ * Work-around for Android Issue 6191 (http://code.google.com/p/android/issues/detail?id=6191)
+ *
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class MyViewFlipper extends ViewFlipper {
+
+ public MyViewFlipper(Context context) {
+ super(context);
+ }
+
+ public MyViewFlipper(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+
+ @Override
+ protected void onDetachedFromWindow() {
+ try {
+ super.onDetachedFromWindow();
+ }
+ catch (IllegalArgumentException e) {
+ // Call stopFlipping() in order to kick off updateRunning()
+ stopFlipping();
+ }
+ }
+}
+
diff --git a/src/github/daneren2005/dsub/view/PlaylistAdapter.java b/src/github/daneren2005/dsub/view/PlaylistAdapter.java
new file mode 100644
index 00000000..71727c04
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/PlaylistAdapter.java
@@ -0,0 +1,68 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.view;
+
+import android.content.Context;
+import github.daneren2005.dsub.R;
+import java.util.List;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import github.daneren2005.dsub.domain.Playlist;
+import java.util.Collections;
+import java.util.Comparator;
+
+/**
+ * @author Sindre Mehus
+ */
+public class PlaylistAdapter extends ArrayAdapter<Playlist> {
+
+ private final Context activity;
+
+ public PlaylistAdapter(Context activity, List<Playlist> Playlists) {
+ super(activity, R.layout.playlist_list_item, Playlists);
+ this.activity = activity;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ Playlist entry = getItem(position);
+ PlaylistView view;
+ if (convertView != null && convertView instanceof PlaylistView) {
+ view = (PlaylistView) convertView;
+ } else {
+ view = new PlaylistView(activity);
+ }
+ view.setPlaylist(entry);
+ return view;
+ }
+
+ public static class PlaylistComparator implements Comparator<Playlist> {
+ @Override
+ public int compare(Playlist playlist1, Playlist playlist2) {
+ return playlist1.getName().compareToIgnoreCase(playlist2.getName());
+ }
+
+ public static List<Playlist> sort(List<Playlist> playlists) {
+ Collections.sort(playlists, new PlaylistComparator());
+ return playlists;
+ }
+
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/PlaylistView.java b/src/github/daneren2005/dsub/view/PlaylistView.java
new file mode 100644
index 00000000..876e0691
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/PlaylistView.java
@@ -0,0 +1,76 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.view;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.Playlist;
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.Util;
+import java.io.File;
+
+/**
+ * Used to display albums in a {@code ListView}.
+ *
+ * @author Sindre Mehus
+ */
+public class PlaylistView extends UpdateView {
+ private static final String TAG = PlaylistView.class.getSimpleName();
+
+ private Context context;
+ private Playlist playlist;
+
+ private TextView titleView;
+ private ImageView moreButton;
+
+ public PlaylistView(Context context) {
+ super(context);
+ this.context = context;
+ LayoutInflater.from(context).inflate(R.layout.playlist_list_item, this, true);
+
+ titleView = (TextView) findViewById(R.id.playlist_name);
+ moreButton = (ImageView) findViewById(R.id.playlist_more);
+ moreButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ v.showContextMenu();
+ }
+ });
+ }
+
+ public void setPlaylist(Playlist playlist) {
+ this.playlist = playlist;
+
+ titleView.setText(playlist.getName());
+ update();
+ }
+
+ @Override
+ protected void update() {
+ File file = FileUtil.getPlaylistFile(Util.getServerName(context), playlist.getName());
+ if(file.exists() || Util.isOffline(context)) {
+ moreButton.setImageResource(R.drawable.list_item_more_shaded);
+ } else {
+ moreButton.setImageResource(R.drawable.list_item_more);
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/PodcastChannelAdapter.java b/src/github/daneren2005/dsub/view/PodcastChannelAdapter.java
new file mode 100644
index 00000000..6b7af991
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/PodcastChannelAdapter.java
@@ -0,0 +1,59 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.view;
+
+import android.widget.ArrayAdapter;
+import android.widget.SectionIndexer;
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.PodcastChannel;
+
+import java.util.List;
+import java.util.Set;
+import java.util.LinkedHashSet;
+import java.util.ArrayList;
+
+/**
+ * @author Sindre Mehus
+*/
+public class PodcastChannelAdapter extends ArrayAdapter<PodcastChannel>{
+ private Context activity;
+ private List<PodcastChannel> podcasts;
+
+ public PodcastChannelAdapter(Context context, List<PodcastChannel> podcasts) {
+ super(context, android.R.layout.simple_list_item_1, podcasts);
+ this.activity = context;
+ this.podcasts = podcasts;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ PodcastChannel podcast = podcasts.get(position);
+ PodcastChannelView view;
+ if (convertView != null && convertView instanceof PodcastChannelView) {
+ view = (PodcastChannelView) convertView;
+ } else {
+ view = new PodcastChannelView(activity);
+ }
+ view.setPodcastChannel(podcast);
+ return view;
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/PodcastChannelView.java b/src/github/daneren2005/dsub/view/PodcastChannelView.java
new file mode 100644
index 00000000..94eedf2c
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/PodcastChannelView.java
@@ -0,0 +1,76 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.view;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.TextView;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.PodcastChannel;
+import github.daneren2005.dsub.util.FileUtil;
+import java.io.File;
+
+public class PodcastChannelView extends UpdateView {
+ private static final String TAG = PodcastChannelView.class.getSimpleName();
+
+ private Context context;
+ private PodcastChannel channel;
+
+ private TextView titleView;
+ private ImageView moreButton;
+
+ public PodcastChannelView(Context context) {
+ super(context);
+ this.context = context;
+ LayoutInflater.from(context).inflate(R.layout.artist_list_item, this, true);
+
+ titleView = (TextView) findViewById(R.id.artist_name);
+ ImageButton starButton = (ImageButton) findViewById(R.id.artist_star);
+ starButton.setVisibility(View.GONE);
+ starButton.setFocusable(false);
+ moreButton = (ImageView) findViewById(R.id.artist_more);
+ moreButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ v.showContextMenu();
+ }
+ });
+ }
+
+ public void setPodcastChannel(PodcastChannel podcastChannel) {
+ channel = podcastChannel;
+ if(podcastChannel.getName() != null) {
+ titleView.setText(podcastChannel.getName());
+ } else {
+ titleView.setText(podcastChannel.getUrl());
+ }
+ }
+
+ @Override
+ protected void update() {
+ File file = FileUtil.getPodcastDirectory(context, channel);
+ if(file.exists()) {
+ moreButton.setImageResource(R.drawable.list_item_more_shaded);
+ } else {
+ moreButton.setImageResource(R.drawable.list_item_more);
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/SackOfViewsAdapter.java b/src/github/daneren2005/dsub/view/SackOfViewsAdapter.java
new file mode 100644
index 00000000..ff2280c1
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/SackOfViewsAdapter.java
@@ -0,0 +1,181 @@
+/***
+ Copyright (c) 2008-2009 CommonsWare, LLC
+ Portions (c) 2009 Google, Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License"); you may
+ not use this file except in compliance with the License. You may obtain
+ a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+package github.daneren2005.dsub.view;
+
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ListView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Adapter that simply returns row views from a list.
+ * <p/>
+ * If you supply a size, you must implement newView(), to
+ * create a required view. The adapter will then cache these
+ * views.
+ * <p/>
+ * If you supply a list of views in the constructor, that
+ * list will be used directly. If any elements in the list
+ * are null, then newView() will be called just for those
+ * slots.
+ * <p/>
+ * Subclasses may also wish to override areAllItemsEnabled()
+ * (default: false) and isEnabled() (default: false), if some
+ * of their rows should be selectable.
+ * <p/>
+ * It is assumed each view is unique, and therefore will not
+ * get recycled.
+ * <p/>
+ * Note that this adapter is not designed for long lists. It
+ * is more for screens that should behave like a list. This
+ * is particularly useful if you combine this with other
+ * adapters (e.g., SectionedAdapter) that might have an
+ * arbitrary number of rows, so it all appears seamless.
+ */
+public class SackOfViewsAdapter extends BaseAdapter {
+ private List<View> views = null;
+
+ /**
+ * Constructor creating an empty list of views, but with
+ * a specified count. Subclasses must override newView().
+ */
+ public SackOfViewsAdapter(int count) {
+ super();
+
+ views = new ArrayList<View>(count);
+
+ for (int i = 0; i < count; i++) {
+ views.add(null);
+ }
+ }
+
+ /**
+ * Constructor wrapping a supplied list of views.
+ * Subclasses must override newView() if any of the elements
+ * in the list are null.
+ */
+ public SackOfViewsAdapter(List<View> views) {
+ for (View view : views) {
+ view.setLayoutParams(new ListView.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
+ }
+ this.views = views;
+ }
+
+ /**
+ * Get the data item associated with the specified
+ * position in the data set.
+ *
+ * @param position Position of the item whose data we want
+ */
+ @Override
+ public Object getItem(int position) {
+ return (views.get(position));
+ }
+
+ /**
+ * How many items are in the data set represented by this
+ * Adapter.
+ */
+ @Override
+ public int getCount() {
+ return (views.size());
+ }
+
+ /**
+ * Returns the number of types of Views that will be
+ * created by getView().
+ */
+ @Override
+ public int getViewTypeCount() {
+ return (getCount());
+ }
+
+ /**
+ * Get the type of View that will be created by getView()
+ * for the specified item.
+ *
+ * @param position Position of the item whose data we want
+ */
+ @Override
+ public int getItemViewType(int position) {
+ return (position);
+ }
+
+ /**
+ * Are all items in this ListAdapter enabled? If yes it
+ * means all items are selectable and clickable.
+ */
+ @Override
+ public boolean areAllItemsEnabled() {
+ return (false);
+ }
+
+ /**
+ * Returns true if the item at the specified position is
+ * not a separator.
+ *
+ * @param position Position of the item whose data we want
+ */
+ @Override
+ public boolean isEnabled(int position) {
+ return (false);
+ }
+
+ /**
+ * Get a View that displays the data at the specified
+ * position in the data set.
+ *
+ * @param position Position of the item whose data we want
+ * @param convertView View to recycle, if not null
+ * @param parent ViewGroup containing the returned View
+ */
+ @Override
+ public View getView(int position, View convertView,
+ ViewGroup parent) {
+ View result = views.get(position);
+
+ if (result == null) {
+ result = newView(position, parent);
+ views.set(position, result);
+ }
+
+ return (result);
+ }
+
+ /**
+ * Get the row id associated with the specified position
+ * in the list.
+ *
+ * @param position Position of the item whose data we want
+ */
+ @Override
+ public long getItemId(int position) {
+ return (position);
+ }
+
+ /**
+ * Create a new View to go into the list at the specified
+ * position.
+ *
+ * @param position Position of the item whose data we want
+ * @param parent ViewGroup containing the returned View
+ */
+ protected View newView(int position, ViewGroup parent) {
+ throw new RuntimeException("You must override newView()!");
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/SongView.java b/src/github/daneren2005/dsub/view/SongView.java
new file mode 100644
index 00000000..042d8031
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/SongView.java
@@ -0,0 +1,241 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ 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.media.MediaMetadataRetriever;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.*;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.PodcastEpisode;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.service.DownloadServiceImpl;
+import github.daneren2005.dsub.service.DownloadFile;
+import github.daneren2005.dsub.util.Util;
+
+import java.io.File;
+import java.text.DateFormat;
+
+/**
+ * Used to display songs in a {@code ListView}.
+ *
+ * @author Sindre Mehus
+ */
+public class SongView extends UpdateView implements Checkable {
+ private static final String TAG = SongView.class.getSimpleName();
+
+ private Context context;
+ private MusicDirectory.Entry song;
+
+ private CheckedTextView checkedTextView;
+ private TextView titleTextView;
+ private TextView artistTextView;
+ private TextView durationTextView;
+ private TextView statusTextView;
+ private ImageButton starButton;
+ private ImageView moreButton;
+
+ private DownloadService downloadService;
+ private long revision = -1;
+ private DownloadFile downloadFile;
+
+ private boolean playing = false;
+ private int rightImage = 0;
+ private int moreImage = 0;
+ private boolean starred = false;
+ private boolean isWorkDone = false;
+ private boolean isSaved = false;
+ private File partialFile;
+ private boolean partialFileExists = false;
+
+ public SongView(Context context) {
+ super(context);
+ this.context = context;
+ LayoutInflater.from(context).inflate(R.layout.song_list_item, this, true);
+
+ checkedTextView = (CheckedTextView) findViewById(R.id.song_check);
+ titleTextView = (TextView) findViewById(R.id.song_title);
+ artistTextView = (TextView) findViewById(R.id.song_artist);
+ durationTextView = (TextView) findViewById(R.id.song_duration);
+ statusTextView = (TextView) findViewById(R.id.song_status);
+ starButton = (ImageButton) findViewById(R.id.song_star);
+ starButton.setFocusable(false);
+ moreButton = (ImageView) findViewById(R.id.artist_more);
+ moreButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ v.showContextMenu();
+ }
+ });
+ }
+
+ public void setSong(MusicDirectory.Entry song, boolean checkable) {
+ this.song = song;
+
+ StringBuilder artist = new StringBuilder(40);
+
+ String bitRate = null;
+ if (song.getBitRate() != null) {
+ bitRate = String.format(getContext().getString(R.string.song_details_kbps), song.getBitRate());
+ }
+
+ String fileFormat = null;
+ if (song.getTranscodedSuffix() != null && !song.getTranscodedSuffix().equals(song.getSuffix())) {
+ fileFormat = String.format("%s > %s", song.getSuffix(), song.getTranscodedSuffix());
+ } else {
+ fileFormat = song.getSuffix();
+ }
+
+ if(!song.isVideo()) {
+ if(song instanceof PodcastEpisode) {
+ String date = ((PodcastEpisode)song).getDate();
+ if(date != null) {
+ int index = date.indexOf(" ");
+ artist.append(date.substring(0, index != -1 ? index : date.length()));
+ }
+ }
+ else if(song.getArtist() != null) {
+ artist.append(song.getArtist());
+ }
+
+ String status = (song instanceof PodcastEpisode) ? ((PodcastEpisode)song).getStatus() : "";
+ artist.append(" (");
+ if("error".equals(status)) {
+ artist.append(getContext().getString(R.string.song_details_error));
+ } else if("skipped".equals(status)) {
+ artist.append(getContext().getString(R.string.song_details_skipped));
+ } else if("downloading".equals(status)) {
+ artist.append(getContext().getString(R.string.song_details_downloading));
+ } else {
+ artist.append(String.format(getContext().getString(R.string.song_details_all), bitRate == null ? "" : bitRate, fileFormat));
+ }
+ artist.append(")");
+ } else {
+ artist.append(String.format(getContext().getString(R.string.song_details_all), bitRate == null ? "" : bitRate, fileFormat));
+ }
+
+ String title = song.getTitle();
+ Integer track = song.getTrack();
+ if(track != null && Util.getDisplayTrack(context)) {
+ title = String.format("%02d", track) + " " + title;
+ }
+
+ titleTextView.setText(title);
+ artistTextView.setText(artist);
+ durationTextView.setText(Util.formatDuration(song.getDuration()));
+ checkedTextView.setVisibility(checkable && !song.isVideo() ? View.VISIBLE : View.GONE);
+
+ revision = -1;
+ updateBackground();
+ update();
+ }
+
+ @Override
+ protected void updateBackground() {
+ if (downloadService == null) {
+ downloadService = DownloadServiceImpl.getInstance();
+ if(downloadService == null) {
+ return;
+ }
+ }
+
+ long newRevision = downloadService.getDownloadListUpdateRevision();
+ if(revision != newRevision || downloadFile == null) {
+ downloadFile = downloadService.forSong(song);
+ revision = newRevision;
+ }
+
+ isWorkDone = downloadFile.isWorkDone();
+ isSaved = downloadFile.isSaved();
+ partialFile = downloadFile.getPartialFile();
+ partialFileExists = partialFile.exists();
+ }
+
+ @Override
+ protected void update() {
+ if (downloadService == null) {
+ return;
+ }
+
+ if(song.isStarred()) {
+ if(!starred) {
+ starButton.setVisibility(View.VISIBLE);
+ starred = true;
+ }
+ } else {
+ if(starred) {
+ starButton.setVisibility(View.GONE);
+ starred = false;
+ }
+ }
+
+ int rightImage = 0;
+ if (isWorkDone) {
+ int moreImage = isSaved ? R.drawable.list_item_more_saved : R.drawable.list_item_more_shaded;
+ if(moreImage != this.moreImage) {
+ moreButton.setImageResource(moreImage);
+ this.moreImage = moreImage;
+ }
+ } else if(this.moreImage != R.drawable.list_item_more) {
+ moreButton.setImageResource(R.drawable.list_item_more);
+ this.moreImage = R.drawable.list_item_more;
+ }
+
+ if (downloadFile.isDownloading() && !downloadFile.isDownloadCancelled() && partialFileExists) {
+ statusTextView.setText(Util.formatLocalizedBytes(partialFile.length(), getContext()));
+ rightImage = R.drawable.downloading;
+ } else if(this.rightImage != 0) {
+ statusTextView.setText(null);
+ }
+ if(this.rightImage != rightImage) {
+ statusTextView.setCompoundDrawablesWithIntrinsicBounds(0, 0, rightImage, 0);
+ this.rightImage = rightImage;
+ }
+
+ boolean playing = downloadService.getCurrentPlaying() == downloadFile;
+ if (playing) {
+ if(!this.playing) {
+ this.playing = playing;
+ titleTextView.setCompoundDrawablesWithIntrinsicBounds(R.drawable.stat_notify_playing, 0, 0, 0);
+ }
+ } else {
+ if(this.playing) {
+ this.playing = playing;
+ titleTextView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
+ }
+ }
+ }
+
+ @Override
+ public void setChecked(boolean b) {
+ checkedTextView.setChecked(b);
+ }
+
+ @Override
+ public boolean isChecked() {
+ return checkedTextView.isChecked();
+ }
+
+ @Override
+ public void toggle() {
+ checkedTextView.toggle();
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/UpdateView.java b/src/github/daneren2005/dsub/view/UpdateView.java
new file mode 100644
index 00000000..7ce27f06
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/UpdateView.java
@@ -0,0 +1,133 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.view;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.LinearLayout;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.WeakHashMap;
+
+public class UpdateView extends LinearLayout {
+ private static final String TAG = UpdateView.class.getSimpleName();
+ private static final WeakHashMap<UpdateView, ?> INSTANCES = new WeakHashMap<UpdateView, Object>();
+
+ private static Handler backgroundHandler;
+ private static Handler uiHandler;
+ private static Runnable updateRunnable;
+
+ public UpdateView(Context context) {
+ super(context);
+
+ setLayoutParams(new AbsListView.LayoutParams(
+ ViewGroup.LayoutParams.FILL_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT));
+
+ INSTANCES.put(this, null);
+ int instanceCount = INSTANCES.size();
+ if (instanceCount > 50) {
+ Log.w(TAG, instanceCount + " live UpdateView instances");
+ }
+
+ startUpdater();
+ }
+
+ @Override
+ public void setPressed(boolean pressed) {
+
+ }
+
+ private static synchronized void startUpdater() {
+ if(uiHandler != null) {
+ return;
+ }
+
+ uiHandler = new Handler();
+ updateRunnable = new Runnable() {
+ @Override
+ public void run() {
+ updateAll();
+ }
+ };
+
+ new Thread(new Runnable() {
+ public void run() {
+ Looper.prepare();
+ backgroundHandler = new Handler(Looper.myLooper());
+ uiHandler.post(updateRunnable);
+ Looper.loop();
+ }
+ }).start();
+ }
+
+ private static void updateAll() {
+ try {
+ List<UpdateView> views = new ArrayList<UpdateView>();;
+ for (UpdateView view : INSTANCES.keySet()) {
+ if (view.isShown()) {
+ views.add(view);
+ }
+ }
+ updateAllLive(views);
+ } catch (Throwable x) {
+ Log.w(TAG, "Error when updating song views.", x);
+ }
+ }
+ private static void updateAllLive(final List<UpdateView> views) {
+ final Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ try {
+ for(UpdateView view: views) {
+ view.update();
+ }
+ } catch (Throwable x) {
+ Log.w(TAG, "Error when updating song views.", x);
+ }
+ uiHandler.postDelayed(updateRunnable, 1000L);
+ }
+ };
+
+ backgroundHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ for(UpdateView view: views) {
+ view.updateBackground();
+ }
+ uiHandler.post(runnable);
+ } catch (Throwable x) {
+ Log.w(TAG, "Error when updating song views.", x);
+ }
+ }
+ });
+ }
+
+ protected void updateBackground() {
+
+ }
+ protected void update() {
+
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/VisualizerView.java b/src/github/daneren2005/dsub/view/VisualizerView.java
new file mode 100644
index 00000000..53ebc2ec
--- /dev/null
+++ b/src/github/daneren2005/dsub/view/VisualizerView.java
@@ -0,0 +1,137 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2011 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.view;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.media.audiofx.Visualizer;
+import android.util.AttributeSet;
+import android.view.View;
+import github.daneren2005.dsub.audiofx.VisualizerController;
+import github.daneren2005.dsub.domain.PlayerState;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.service.DownloadServiceImpl;
+
+/**
+ * A simple class that draws waveform data received from a
+ * {@link Visualizer.OnDataCaptureListener#onWaveFormDataCapture}
+ *
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class VisualizerView extends View {
+
+ private static final int PREFERRED_CAPTURE_RATE_MILLIHERTZ = 20000;
+
+ private final Paint paint = new Paint();
+
+ private byte[] data;
+ private float[] points;
+ private boolean active = false;
+
+ public VisualizerView(Context context) {
+ super(context);
+
+ paint.setStrokeWidth(2f);
+ paint.setAntiAlias(true);
+ paint.setColor(Color.rgb(51, 181, 229));
+ }
+
+ public boolean isActive() {
+ return active;
+ }
+
+ public void setActive(boolean active) {
+ this.active = active;
+ VisualizerController visualizerController = getVizualiser();
+ Visualizer visualizer = visualizerController == null ? null : visualizerController.getVisualizer();
+ if (visualizer == null) {
+ this.active = false;
+ return;
+ }
+
+ int captureRate = Math.min(PREFERRED_CAPTURE_RATE_MILLIHERTZ, Visualizer.getMaxCaptureRate());
+ if (active) {
+ visualizer.setDataCaptureListener(new Visualizer.OnDataCaptureListener() {
+ @Override
+ public void onWaveFormDataCapture(Visualizer visualizer, byte[] waveform, int samplingRate) {
+ updateVisualizer(waveform);
+ }
+
+ @Override
+ public void onFftDataCapture(Visualizer visualizer, byte[] fft, int samplingRate) {
+ }
+ }, captureRate, true, false);
+ } else {
+ visualizer.setDataCaptureListener(null, captureRate, false, false);
+ }
+
+ visualizer.setEnabled(active);
+ if(!active) {
+ visualizerController.release();
+ }
+ invalidate();
+ }
+
+ private VisualizerController getVizualiser() {
+ DownloadService downloadService = DownloadServiceImpl.getInstance();
+ VisualizerController visualizerController = downloadService == null ? null : downloadService.getVisualizerController();
+ return visualizerController;
+ }
+
+ private void updateVisualizer(byte[] waveform) {
+ this.data = waveform;
+ invalidate();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ if (!active) {
+ return;
+ }
+ DownloadService downloadService = DownloadServiceImpl.getInstance();
+ if (downloadService != null && downloadService.getPlayerState() != PlayerState.STARTED) {
+ return;
+ }
+
+ if (data == null) {
+ return;
+ }
+
+ if (points == null || points.length < data.length * 4) {
+ points = new float[data.length * 4];
+ }
+
+ int w = getWidth();
+ int h = getHeight();
+
+ for (int i = 0; i < data.length - 1; i++) {
+ points[i * 4] = w * i / (data.length - 1);
+ points[i * 4 + 1] = h / 2 + ((byte) (data[i] + 128)) * (h / 2) / 128;
+ points[i * 4 + 2] = w * (i + 1) / (data.length - 1);
+ points[i * 4 + 3] = h / 2 + ((byte) (data[i + 1] + 128)) * (h / 2) / 128;
+ }
+
+ canvas.drawLines(points, paint);
+ }
+}
diff --git a/subsonic.keystore b/subsonic.keystore
new file mode 100644
index 00000000..46996d4c
--- /dev/null
+++ b/subsonic.keystore
Binary files differ
diff --git a/subsonic.png b/subsonic.png
new file mode 100644
index 00000000..e17a4540
--- /dev/null
+++ b/subsonic.png
Binary files differ
diff --git a/subsonic2.png b/subsonic2.png
new file mode 100644
index 00000000..561529a5
--- /dev/null
+++ b/subsonic2.png
Binary files differ