summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAndrew Neal <andrewdneal@gmail.com>2012-11-20 18:41:37 -0600
committerAndrew Neal <andrewdneal@gmail.com>2012-11-20 18:41:37 -0600
commitad90d968d80a7f57873d35d701147b5081b297a6 (patch)
tree507e79c4668ab247245ce36922016a65ae95786c
parent12f4713e2c39e715240b1116d32b54496f8c958f (diff)
downloadandroid_packages_apps_Eleven-ad90d968d80a7f57873d35d701147b5081b297a6.tar.gz
android_packages_apps_Eleven-ad90d968d80a7f57873d35d701147b5081b297a6.tar.bz2
android_packages_apps_Eleven-ad90d968d80a7f57873d35d701147b5081b297a6.zip
Play Store changes
-rw-r--r--AndroidManifest.xml428
-rw-r--r--assets/Frame0.pngbin2155 -> 0 bytes
-rw-r--r--assets/Frame1.pngbin2140 -> 0 bytes
-rw-r--r--assets/Frame10.pngbin2179 -> 0 bytes
-rw-r--r--assets/Frame11.pngbin2202 -> 0 bytes
-rw-r--r--assets/Frame2.pngbin2166 -> 0 bytes
-rw-r--r--assets/Frame3.pngbin2151 -> 0 bytes
-rw-r--r--assets/Frame4.pngbin2115 -> 0 bytes
-rw-r--r--assets/Frame5.pngbin2182 -> 0 bytes
-rw-r--r--assets/Frame6.pngbin2160 -> 0 bytes
-rw-r--r--assets/Frame7.pngbin2134 -> 0 bytes
-rw-r--r--assets/Frame8.pngbin2150 -> 0 bytes
-rw-r--r--assets/Frame9.pngbin2184 -> 0 bytes
-rw-r--r--assets/RobotoLight.ttfbin0 -> 162636 bytes
-rw-r--r--assets/RobotoThin.ttfbin0 -> 122512 bytes
-rw-r--r--assets/licenses.html90
-rw-r--r--libs/android-query-0.21.7.jarbin83576 -> 0 bytes
-rw-r--r--libs/android-support-v4.jarbin271788 -> 349252 bytes
-rw-r--r--libs/jaudiotagger.jarbin0 -> 870066 bytes
-rw-r--r--libs/nineoldandroids.jarbin0 -> 110746 bytes
-rw-r--r--res/anim/peak_meter_1.xml18
-rw-r--r--res/anim/peak_meter_2.xml17
-rw-r--r--res/color/tab_text_color.xml6
-rw-r--r--res/drawable-hdpi-v11/appwidget_bg.9.pngbin0 -> 489 bytes
-rw-r--r--res/drawable-hdpi-v8/stat_notify_music.pngbin0 -> 973 bytes
-rw-r--r--res/drawable-hdpi-v9/stat_notify_music.pngbin0 -> 942 bytes
-rw-r--r--res/drawable-hdpi/apollo_holo_light_favorite_normal.pngbin1512 -> 0 bytes
-rw-r--r--res/drawable-hdpi/apollo_holo_light_favorite_selected.pngbin553 -> 0 bytes
-rw-r--r--res/drawable-hdpi/apollo_holo_light_next.pngbin1477 -> 0 bytes
-rw-r--r--res/drawable-hdpi/apollo_holo_light_pause.pngbin1116 -> 0 bytes
-rw-r--r--res/drawable-hdpi/apollo_holo_light_play.pngbin1405 -> 0 bytes
-rw-r--r--res/drawable-hdpi/apollo_holo_light_previous.pngbin1509 -> 0 bytes
-rw-r--r--res/drawable-hdpi/apollo_holo_light_repeat_all.pngbin817 -> 0 bytes
-rw-r--r--res/drawable-hdpi/apollo_holo_light_repeat_normal.pngbin1753 -> 0 bytes
-rw-r--r--res/drawable-hdpi/apollo_holo_light_repeat_one.pngbin1088 -> 0 bytes
-rw-r--r--res/drawable-hdpi/apollo_holo_light_search.pngbin1759 -> 0 bytes
-rw-r--r--res/drawable-hdpi/apollo_holo_light_shuffle_normal.pngbin1945 -> 0 bytes
-rw-r--r--res/drawable-hdpi/apollo_holo_light_shuffle_on.pngbin994 -> 0 bytes
-rw-r--r--res/drawable-hdpi/apollo_settings_themes.pngbin961 -> 0 bytes
-rw-r--r--res/drawable-hdpi/appwidget_bg.9.pngbin1367 -> 4272 bytes
-rw-r--r--res/drawable-hdpi/bg_stripes_dark.pngbin0 -> 97 bytes
-rw-r--r--res/drawable-hdpi/btn_notification_collapse.png (renamed from res/drawable-hdpi/apollo_holo_dark_notifiation_bar_collapse.png)bin371 -> 371 bytes
-rw-r--r--res/drawable-hdpi/btn_playback_next.png (renamed from res/drawable-hdpi/apollo_holo_dark_next.png)bin1521 -> 1521 bytes
-rw-r--r--res/drawable-hdpi/btn_playback_pause.png (renamed from res/drawable-hdpi/apollo_holo_dark_pause.png)bin1114 -> 1114 bytes
-rw-r--r--res/drawable-hdpi/btn_playback_play.png (renamed from res/drawable-hdpi/apollo_holo_dark_play.png)bin1410 -> 1410 bytes
-rw-r--r--res/drawable-hdpi/btn_playback_previous.pngbin0 -> 1503 bytes
-rw-r--r--res/drawable-hdpi/btn_playback_repeat.pngbin0 -> 1763 bytes
-rw-r--r--res/drawable-hdpi/btn_playback_repeat_all.pngbin0 -> 1024 bytes
-rw-r--r--res/drawable-hdpi/btn_playback_repeat_one.pngbin0 -> 1215 bytes
-rw-r--r--res/drawable-hdpi/btn_playback_shuffle.pngbin0 -> 1098 bytes
-rw-r--r--res/drawable-hdpi/btn_playback_shuffle_all.pngbin0 -> 1201 bytes
-rw-r--r--res/drawable-hdpi/btn_switch_queue.pngbin0 -> 435 bytes
-rw-r--r--res/drawable-hdpi/colorstrip_shadow.9.pngbin979 -> 0 bytes
-rw-r--r--res/drawable-hdpi/dropdown_ic_arrow_normal_holo_light.pngbin551 -> 0 bytes
-rw-r--r--res/drawable-hdpi/ic_action_favorite.pngbin0 -> 874 bytes
-rw-r--r--res/drawable-hdpi/ic_action_pinn_to_home.pngbin0 -> 1625 bytes
-rw-r--r--res/drawable-hdpi/ic_action_search.png (renamed from res/drawable-hdpi/apollo_holo_light_overflow.png)bin2838 -> 3120 bytes
-rw-r--r--res/drawable-hdpi/ic_action_shop.pngbin0 -> 1202 bytes
-rw-r--r--res/drawable-hdpi/ic_launcher.pngbin6698 -> 4752 bytes
-rw-r--r--res/drawable-hdpi/indicator_playing_peak_meter_1.pngbin171 -> 0 bytes
-rw-r--r--res/drawable-hdpi/indicator_playing_peak_meter_2.pngbin218 -> 0 bytes
-rw-r--r--res/drawable-hdpi/indicator_playing_peak_meter_3.pngbin232 -> 0 bytes
-rw-r--r--res/drawable-hdpi/indicator_playing_peak_meter_4.pngbin224 -> 0 bytes
-rw-r--r--res/drawable-hdpi/indicator_playing_peak_meter_5.pngbin207 -> 0 bytes
-rw-r--r--res/drawable-hdpi/list_section_divider_holo_custom.9.pngbin129 -> 0 bytes
-rw-r--r--res/drawable-hdpi/notify_panel_notification_icon_bg.pngbin107 -> 0 bytes
-rw-r--r--res/drawable-hdpi/playlist_tile_normal.9.pngbin0 -> 285 bytes
-rw-r--r--res/drawable-hdpi/queue_thumbnail_bg.9.pngbin462 -> 0 bytes
-rw-r--r--res/drawable-hdpi/recents_thumbnail_bg_press.9.pngbin324 -> 0 bytes
-rw-r--r--res/drawable-hdpi/scrubber_primary_holo.9.pngbin0 -> 152 bytes
-rw-r--r--res/drawable-hdpi/scrubber_secondary_holo.9.pngbin0 -> 150 bytes
-rw-r--r--res/drawable-hdpi/scrubber_track_holo_dark.9.pngbin0 -> 167 bytes
-rw-r--r--res/drawable-hdpi/tab_selected_holo.9.pngbin90 -> 0 bytes
-rw-r--r--res/drawable-hdpi/tab_selected_pressed_focused_holo.9.pngbin615 -> 0 bytes
-rw-r--r--res/drawable-hdpi/tab_selected_pressed_holo.9.pngbin93 -> 0 bytes
-rw-r--r--res/drawable-hdpi/tab_unselected_focused_holo.9.pngbin93 -> 0 bytes
-rw-r--r--res/drawable-hdpi/tab_unselected_holo.9.pngbin91 -> 0 bytes
-rw-r--r--res/drawable-hdpi/tab_unselected_pressed_holo.9.pngbin93 -> 0 bytes
-rw-r--r--res/drawable-hdpi/title_bar_shadow.9.pngbin129 -> 0 bytes
-rw-r--r--res/drawable-hdpi/view_pager_background_texture.pngbin0 -> 115 bytes
-rw-r--r--res/drawable-mdpi-v11/appwidget_bg.9.pngbin0 -> 345 bytes
-rw-r--r--res/drawable-mdpi/apollo_holo_light_favorite_normal.pngbin1327 -> 0 bytes
-rw-r--r--res/drawable-mdpi/apollo_holo_light_favorite_selected.pngbin447 -> 0 bytes
-rw-r--r--res/drawable-mdpi/apollo_holo_light_next.pngbin1326 -> 0 bytes
-rw-r--r--res/drawable-mdpi/apollo_holo_light_pause.pngbin1109 -> 0 bytes
-rw-r--r--res/drawable-mdpi/apollo_holo_light_play.pngbin1261 -> 0 bytes
-rw-r--r--res/drawable-mdpi/apollo_holo_light_previous.pngbin1326 -> 0 bytes
-rw-r--r--res/drawable-mdpi/apollo_holo_light_repeat_all.pngbin596 -> 0 bytes
-rw-r--r--res/drawable-mdpi/apollo_holo_light_repeat_normal.pngbin1455 -> 0 bytes
-rw-r--r--res/drawable-mdpi/apollo_holo_light_repeat_one.pngbin709 -> 0 bytes
-rw-r--r--res/drawable-mdpi/apollo_holo_light_search.pngbin1429 -> 0 bytes
-rw-r--r--res/drawable-mdpi/apollo_holo_light_shuffle_normal.pngbin1561 -> 0 bytes
-rw-r--r--res/drawable-mdpi/apollo_holo_light_shuffle_on.pngbin715 -> 0 bytes
-rw-r--r--res/drawable-mdpi/apollo_settings_themes.pngbin630 -> 0 bytes
-rw-r--r--res/drawable-mdpi/appwidget_bg.9.pngbin1208 -> 2687 bytes
-rw-r--r--res/drawable-mdpi/bg_stripes_dark.pngbin0 -> 97 bytes
-rw-r--r--res/drawable-mdpi/btn_notification_collapse.png (renamed from res/drawable-mdpi/apollo_holo_dark_notifiation_bar_collapse.png)bin286 -> 286 bytes
-rw-r--r--res/drawable-mdpi/btn_playback_next.png (renamed from res/drawable-mdpi/apollo_holo_dark_next.png)bin1316 -> 1316 bytes
-rw-r--r--res/drawable-mdpi/btn_playback_pause.png (renamed from res/drawable-mdpi/apollo_holo_dark_pause.png)bin1107 -> 1107 bytes
-rw-r--r--res/drawable-mdpi/btn_playback_play.png (renamed from res/drawable-mdpi/apollo_holo_dark_play.png)bin1248 -> 1248 bytes
-rw-r--r--res/drawable-mdpi/btn_playback_previous.pngbin0 -> 1333 bytes
-rw-r--r--res/drawable-mdpi/btn_playback_repeat.pngbin0 -> 1435 bytes
-rw-r--r--res/drawable-mdpi/btn_playback_repeat_all.pngbin0 -> 655 bytes
-rw-r--r--res/drawable-mdpi/btn_playback_repeat_one.pngbin0 -> 731 bytes
-rw-r--r--res/drawable-mdpi/btn_playback_shuffle.pngbin0 -> 640 bytes
-rw-r--r--res/drawable-mdpi/btn_playback_shuffle_all.pngbin0 -> 736 bytes
-rw-r--r--res/drawable-mdpi/btn_switch_queue.pngbin0 -> 315 bytes
-rw-r--r--res/drawable-mdpi/dropdown_ic_arrow_normal_holo_light.pngbin468 -> 0 bytes
-rw-r--r--res/drawable-mdpi/ic_action_favorite.pngbin0 -> 585 bytes
-rw-r--r--res/drawable-mdpi/ic_action_pinn_to_home.pngbin0 -> 1379 bytes
-rw-r--r--res/drawable-mdpi/ic_action_search.png (renamed from res/drawable-hdpi/apollo_holo_dark_overflow.png)bin2825 -> 3030 bytes
-rw-r--r--res/drawable-mdpi/ic_action_shop.pngbin0 -> 887 bytes
-rw-r--r--res/drawable-mdpi/ic_launcher.pngbin3589 -> 2575 bytes
-rw-r--r--res/drawable-mdpi/indicator_playing_peak_meter_1.pngbin174 -> 0 bytes
-rw-r--r--res/drawable-mdpi/indicator_playing_peak_meter_2.pngbin202 -> 0 bytes
-rw-r--r--res/drawable-mdpi/indicator_playing_peak_meter_3.pngbin214 -> 0 bytes
-rw-r--r--res/drawable-mdpi/indicator_playing_peak_meter_4.pngbin202 -> 0 bytes
-rw-r--r--res/drawable-mdpi/indicator_playing_peak_meter_5.pngbin188 -> 0 bytes
-rw-r--r--res/drawable-mdpi/list_section_divider_holo_custom.9.pngbin122 -> 0 bytes
-rw-r--r--res/drawable-mdpi/notify_panel_notification_icon_bg.pngbin93 -> 0 bytes
-rw-r--r--res/drawable-mdpi/playlist_tile_normal.9.pngbin0 -> 220 bytes
-rw-r--r--res/drawable-mdpi/queue_thumbnail_bg.9.pngbin354 -> 0 bytes
-rw-r--r--res/drawable-mdpi/recents_thumbnail_bg_press.9.pngbin280 -> 0 bytes
-rw-r--r--res/drawable-mdpi/scrubber_primary_holo.9.pngbin0 -> 141 bytes
-rw-r--r--res/drawable-mdpi/scrubber_secondary_holo.9.pngbin0 -> 138 bytes
-rw-r--r--res/drawable-mdpi/scrubber_track_holo_dark.9.pngbin0 -> 161 bytes
-rw-r--r--res/drawable-mdpi/tab_selected_holo.9.pngbin88 -> 0 bytes
-rw-r--r--res/drawable-mdpi/tab_selected_pressed_focused_holo.9.pngbin529 -> 0 bytes
-rw-r--r--res/drawable-mdpi/tab_selected_pressed_holo.9.pngbin93 -> 0 bytes
-rw-r--r--res/drawable-mdpi/tab_unselected_focused_holo.9.pngbin95 -> 0 bytes
-rw-r--r--res/drawable-mdpi/tab_unselected_holo.9.pngbin92 -> 0 bytes
-rw-r--r--res/drawable-mdpi/tab_unselected_pressed_holo.9.pngbin96 -> 0 bytes
-rw-r--r--res/drawable-mdpi/title_bar_shadow.9.pngbin111 -> 0 bytes
-rw-r--r--res/drawable-mdpi/view_pager_background_texture.pngbin0 -> 115 bytes
-rw-r--r--res/drawable-nodpi/app_widget_large.pngbin0 -> 152610 bytes
-rw-r--r--res/drawable-nodpi/app_widget_large_alternate.pngbin0 -> 81016 bytes
-rw-r--r--res/drawable-nodpi/app_widget_recents.pngbin0 -> 176007 bytes
-rw-r--r--res/drawable-nodpi/app_widget_recents_stack_preview.pngbin0 -> 356951 bytes
-rw-r--r--res/drawable-nodpi/app_widget_small.pngbin0 -> 36970 bytes
-rw-r--r--res/drawable-nodpi/background_holo_dark.pngbin0 -> 2118 bytes
-rw-r--r--res/drawable-nodpi/colorstrip_shadow.9.pngbin979 -> 0 bytes
-rw-r--r--res/drawable-nodpi/default_artwork.pngbin0 -> 4628 bytes
-rw-r--r--res/drawable-nodpi/header_temp.pngbin0 -> 30358 bytes
-rw-r--r--res/drawable-nodpi/promo.pngbin30061 -> 0 bytes
-rw-r--r--res/drawable-nodpi/theme_preview.pngbin0 -> 42309 bytes
-rw-r--r--res/drawable-v14/pager_background.xml19
-rw-r--r--res/drawable-v14/tpi_background.xml19
-rw-r--r--res/drawable-xhdpi-v11/appwidget_bg.9.pngbin0 -> 536 bytes
-rw-r--r--res/drawable-xhdpi/apollo_holo_dark_overflow.pngbin2828 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/apollo_holo_light_favorite_normal.pngbin1773 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/apollo_holo_light_favorite_selected.pngbin827 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/apollo_holo_light_next.pngbin1732 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/apollo_holo_light_overflow.pngbin2846 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/apollo_holo_light_pause.pngbin1159 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/apollo_holo_light_play.pngbin1578 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/apollo_holo_light_previous.pngbin1742 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/apollo_holo_light_repeat_all.pngbin1274 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/apollo_holo_light_repeat_normal.pngbin2035 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/apollo_holo_light_repeat_one.pngbin1542 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/apollo_holo_light_search.pngbin2117 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/apollo_holo_light_shuffle_normal.pngbin2389 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/apollo_holo_light_shuffle_on.pngbin1585 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/apollo_settings_themes.pngbin915 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/appwidget_bg.9.pngbin1621 -> 2687 bytes
-rw-r--r--res/drawable-xhdpi/bg_stripes_dark.pngbin0 -> 123 bytes
-rw-r--r--res/drawable-xhdpi/btn_notification_collapse.png (renamed from res/drawable-xhdpi/apollo_holo_dark_notifiation_bar_collapse.png)bin435 -> 435 bytes
-rw-r--r--res/drawable-xhdpi/btn_playback_next.png (renamed from res/drawable-xhdpi/apollo_holo_dark_next.png)bin1750 -> 1750 bytes
-rw-r--r--res/drawable-xhdpi/btn_playback_pause.png (renamed from res/drawable-xhdpi/apollo_holo_dark_pause.png)bin1181 -> 1181 bytes
-rw-r--r--res/drawable-xhdpi/btn_playback_play.png (renamed from res/drawable-xhdpi/apollo_holo_dark_play.png)bin1620 -> 1620 bytes
-rw-r--r--res/drawable-xhdpi/btn_playback_previous.pngbin0 -> 1736 bytes
-rw-r--r--res/drawable-xhdpi/btn_playback_repeat.pngbin0 -> 2066 bytes
-rw-r--r--res/drawable-xhdpi/btn_playback_repeat_all.pngbin0 -> 1116 bytes
-rw-r--r--res/drawable-xhdpi/btn_playback_repeat_one.pngbin0 -> 1561 bytes
-rw-r--r--res/drawable-xhdpi/btn_playback_shuffle.pngbin0 -> 2283 bytes
-rw-r--r--res/drawable-xhdpi/btn_playback_shuffle_all.pngbin0 -> 1407 bytes
-rw-r--r--res/drawable-xhdpi/btn_switch_queue.pngbin0 -> 347 bytes
-rw-r--r--res/drawable-xhdpi/dropdown_ic_arrow_normal_holo_light.pngbin810 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/ic_action_favorite.png (renamed from res/drawable-mdpi/apollo_holo_light_overflow.png)bin2837 -> 3621 bytes
-rw-r--r--res/drawable-xhdpi/ic_action_pinn_to_home.pngbin0 -> 1903 bytes
-rw-r--r--res/drawable-xhdpi/ic_action_search.png (renamed from res/drawable-mdpi/apollo_holo_dark_overflow.png)bin2824 -> 3199 bytes
-rw-r--r--res/drawable-xhdpi/ic_action_shop.pngbin0 -> 1479 bytes
-rw-r--r--res/drawable-xhdpi/ic_launcher.pngbin10412 -> 7495 bytes
-rw-r--r--res/drawable-xhdpi/indicator_playing_peak_meter_1.pngbin222 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/indicator_playing_peak_meter_2.pngbin295 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/indicator_playing_peak_meter_3.pngbin323 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/indicator_playing_peak_meter_4.pngbin312 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/indicator_playing_peak_meter_5.pngbin276 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/list_section_divider_holo_custom.9.pngbin137 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/notify_panel_notification_icon_bg.pngbin99 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/playlist_tile_normal.9.pngbin0 -> 369 bytes
-rw-r--r--res/drawable-xhdpi/queue_thumbnail_bg.9.pngbin636 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/recents_thumbnail_bg_press.9.pngbin411 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/scrubber_primary_holo.9.pngbin0 -> 159 bytes
-rw-r--r--res/drawable-xhdpi/scrubber_secondary_holo.9.pngbin0 -> 156 bytes
-rw-r--r--res/drawable-xhdpi/scrubber_track_holo_dark.9.pngbin0 -> 174 bytes
-rw-r--r--res/drawable-xhdpi/tab_selected_holo.9.pngbin92 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/tab_selected_pressed_focused_holo.9.pngbin95 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/tab_selected_pressed_holo.9.pngbin95 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/tab_unselected_focused_holo.9.pngbin98 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/tab_unselected_holo.9.pngbin96 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/tab_unselected_pressed_holo.9.pngbin98 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/title_bar_shadow.9.pngbin188 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/view_pager_background_texture.pngbin0 -> 120 bytes
-rw-r--r--res/drawable/action_bar.xml21
-rw-r--r--res/drawable/audio_player_pager_container.xml21
-rw-r--r--res/drawable/audio_player_seekbar.xml33
-rw-r--r--res/drawable/bottom_action_bar.xml21
-rw-r--r--res/drawable/bottom_shadow.xml25
-rw-r--r--res/drawable/holo_selector.xml8
-rw-r--r--res/drawable/pager_background.xml18
-rw-r--r--res/drawable/queue_thumbnail_fg.xml23
-rw-r--r--res/drawable/right_shadow.xml25
-rw-r--r--res/drawable/status_bg.xml4
-rw-r--r--res/drawable/tab.xml11
-rw-r--r--res/drawable/top_shadow.xml25
-rw-r--r--res/drawable/tpi_background.xml18
-rw-r--r--res/drawable/viewpager_margin.xml11
-rw-r--r--res/layout-land/activity_player_base.xml189
-rw-r--r--res/layout-v11/app_widget_recents.xml106
-rw-r--r--res/layout-v11/app_widget_recents_items.xml61
-rw-r--r--res/layout-v11/notification_template_base.xml70
-rw-r--r--res/layout-v16/notification_template_expanded_base.xml123
-rw-r--r--res/layout/action_bar.xml57
-rw-r--r--res/layout/activity_base.xml30
-rw-r--r--res/layout/activity_player_base.xml206
-rw-r--r--res/layout/activity_profile_base.xml53
-rw-r--r--res/layout/app_widget_large.xml118
-rw-r--r--res/layout/app_widget_large_alternate.xml138
-rw-r--r--res/layout/app_widget_small.xml151
-rw-r--r--res/layout/audio_controls.xml75
-rw-r--r--res/layout/audio_player.xml46
-rw-r--r--res/layout/audio_player_browser.xml14
-rw-r--r--res/layout/audio_player_controls.xml94
-rw-r--r--res/layout/bottom_action_bar.xml117
-rw-r--r--res/layout/bottom_action_bar_controls.xml55
-rw-r--r--res/layout/color_scheme_dialog.xml156
-rw-r--r--res/layout/colorstrip.xml22
-rw-r--r--res/layout/context_menu.xml27
-rw-r--r--res/layout/context_menu_header.xml17
-rw-r--r--res/layout/custom_action_bar.xml45
-rw-r--r--res/layout/edit_track_list_item.xml39
-rw-r--r--res/layout/empty_view.xml4
-rw-r--r--res/layout/faux_carousel.xml24
-rw-r--r--res/layout/fourbyone_app_widget.xml86
-rw-r--r--res/layout/fourbytwo_app_widget.xml105
-rw-r--r--res/layout/fragment_music_browser_phone.xml41
-rw-r--r--res/layout/fragment_themes_base.xml47
-rw-r--r--res/layout/grid_base.xml46
-rw-r--r--res/layout/grid_items_normal.xml55
-rw-r--r--res/layout/gridview.xml22
-rw-r--r--res/layout/gridview_items.xml47
-rw-r--r--res/layout/half_and_half.xml60
-rw-r--r--res/layout/library_browser.xml28
-rw-r--r--res/layout/list_base.xml47
-rw-r--r--res/layout/list_header.xml25
-rw-r--r--res/layout/list_item_detailed.xml78
-rw-r--r--res/layout/list_item_detailed_no_background.xml77
-rw-r--r--res/layout/list_item_normal.xml64
-rw-r--r--res/layout/list_item_simple.xml55
-rw-r--r--res/layout/list_separator.xml13
-rw-r--r--res/layout/listview.xml28
-rw-r--r--res/layout/listview_items.xml64
-rw-r--r--res/layout/lyrics_base.xml46
-rw-r--r--res/layout/notification_template_base.xml54
-rw-r--r--res/layout/onebyone_app_widget.xml11
-rw-r--r--res/layout/profile_tab.xml90
-rw-r--r--res/layout/profile_tab_carousel.xml55
-rw-r--r--res/layout/quick_queue.xml19
-rw-r--r--res/layout/quick_queue_items.xml87
-rw-r--r--res/layout/shadow.xml7
-rw-r--r--res/layout/square_image_view.xml28
-rw-r--r--res/layout/status_bar.xml56
-rw-r--r--res/layout/tabs.xml4
-rw-r--r--res/layout/theme_preview.xml36
-rw-r--r--res/layout/top_shadow.xml19
-rw-r--r--res/layout/track_browser.xml27
-rw-r--r--res/menu/activity_base.xml25
-rw-r--r--res/menu/add_to_homescreen.xml26
-rw-r--r--res/menu/album_song_sort_by.xml39
-rw-r--r--res/menu/album_sort_by.xml42
-rw-r--r--res/menu/artist_album_sort_by.xml39
-rw-r--r--res/menu/artist_song_sort_by.xml44
-rw-r--r--res/menu/artist_sort_by.xml39
-rw-r--r--res/menu/audio_player.xml36
-rw-r--r--res/menu/favorite.xml26
-rw-r--r--res/menu/overflow_library.xml25
-rw-r--r--res/menu/overflow_now_playing.xml25
-rw-r--r--res/menu/queue.xml27
-rw-r--r--res/menu/search.xml27
-rw-r--r--res/menu/shuffle.xml24
-rw-r--r--res/menu/song_sort_by.xml45
-rw-r--r--res/menu/theme_shop.xml25
-rw-r--r--res/menu/view_as.xml36
-rw-r--r--res/values-hdpi/config.xml7
-rw-r--r--res/values-hdpi/dimens.xml11
-rw-r--r--res/values-sw600dp/dimens.xml22
-rw-r--r--res/values-v11/config.xml22
-rw-r--r--res/values-v11/dimens.xml24
-rw-r--r--res/values-xhdpi/dimens.xml11
-rw-r--r--res/values/arrays.xml29
-rw-r--r--res/values/attrs.xml25
-rw-r--r--res/values/colors.xml57
-rw-r--r--res/values/config.xml34
-rw-r--r--res/values/dimens.xml225
-rw-r--r--res/values/donottranslate.xml24
-rw-r--r--res/values/fractions.xml22
-rw-r--r--res/values/plurals.xml90
-rw-r--r--res/values/strings.xml315
-rw-r--r--res/values/styles.xml227
-rw-r--r--res/values/themeconfig.xml54
-rw-r--r--res/xml-v14/app_widget_recents.xml26
-rw-r--r--res/xml-v14/settings.xml89
-rw-r--r--res/xml/app_widget_large.xml23
-rw-r--r--res/xml/app_widget_large_alternate.xml23
-rw-r--r--res/xml/app_widget_small.xml23
-rw-r--r--res/xml/appwidget1x1_info.xml6
-rw-r--r--res/xml/appwidget4x1_info.xml6
-rw-r--r--res/xml/appwidget4x2_info.xml6
-rw-r--r--res/xml/searchable.xml47
-rw-r--r--res/xml/settings.xml94
-rw-r--r--src/com/andrew/apollo/ApolloApplication.java70
-rw-r--r--src/com/andrew/apollo/AudioPlayerFragment.java638
-rw-r--r--src/com/andrew/apollo/BottomActionBarControlsFragment.java283
-rw-r--r--src/com/andrew/apollo/BottomActionBarFragment.java60
-rw-r--r--src/com/andrew/apollo/Config.java66
-rw-r--r--src/com/andrew/apollo/Constants.java74
-rw-r--r--src/com/andrew/apollo/IApolloService.aidl32
-rw-r--r--src/com/andrew/apollo/MediaButtonIntentReceiver.java (renamed from src/com/andrew/apollo/service/MediaButtonIntentReceiver.java)106
-rw-r--r--src/com/andrew/apollo/MusicPlaybackService.java2981
-rw-r--r--src/com/andrew/apollo/MusicStateListener.java19
-rw-r--r--src/com/andrew/apollo/NotificationHelper.java282
-rw-r--r--src/com/andrew/apollo/NowPlayingCursor.java194
-rw-r--r--src/com/andrew/apollo/RemoteControlClientCompat.java397
-rw-r--r--src/com/andrew/apollo/RemoteControlHelper.java88
-rw-r--r--src/com/andrew/apollo/activities/AudioPlayerHolder.java291
-rw-r--r--src/com/andrew/apollo/activities/MusicLibrary.java165
-rw-r--r--src/com/andrew/apollo/activities/QueryBrowserActivity.java448
-rw-r--r--src/com/andrew/apollo/activities/QuickQueue.java35
-rw-r--r--src/com/andrew/apollo/activities/TracksBrowser.java444
-rw-r--r--src/com/andrew/apollo/adapters/AlbumAdapter.java292
-rw-r--r--src/com/andrew/apollo/adapters/ArtistAdapter.java278
-rw-r--r--src/com/andrew/apollo/adapters/ArtistAlbumAdapter.java299
-rw-r--r--src/com/andrew/apollo/adapters/GenreAdapter.java154
-rw-r--r--src/com/andrew/apollo/adapters/PagerAdapter.java213
-rw-r--r--src/com/andrew/apollo/adapters/PlaylistAdapter.java149
-rw-r--r--src/com/andrew/apollo/adapters/ProfileSongAdapter.java216
-rw-r--r--src/com/andrew/apollo/adapters/QuickQueueAdapter.java95
-rw-r--r--src/com/andrew/apollo/adapters/RecentlyAddedAdapter.java111
-rw-r--r--src/com/andrew/apollo/adapters/ScrollingTabsAdapter.java34
-rw-r--r--src/com/andrew/apollo/adapters/SongAdapter.java135
-rw-r--r--src/com/andrew/apollo/adapters/TabAdapter.java8
-rw-r--r--src/com/andrew/apollo/adapters/TrackAdapter.java97
-rw-r--r--src/com/andrew/apollo/app/widgets/AppWidget11.java145
-rw-r--r--src/com/andrew/apollo/app/widgets/AppWidget41.java196
-rw-r--r--src/com/andrew/apollo/app/widgets/AppWidget42.java240
-rw-r--r--src/com/andrew/apollo/appwidgets/AppWidgetLarge.java202
-rw-r--r--src/com/andrew/apollo/appwidgets/AppWidgetLargeAlternate.java250
-rw-r--r--src/com/andrew/apollo/appwidgets/AppWidgetSmall.java200
-rw-r--r--src/com/andrew/apollo/appwidgets/RecentWidgetProvider.java300
-rw-r--r--src/com/andrew/apollo/appwidgets/RecentWidgetService.java245
-rw-r--r--src/com/andrew/apollo/cache/DiskLruCache.java969
-rw-r--r--src/com/andrew/apollo/cache/ImageCache.java790
-rw-r--r--src/com/andrew/apollo/cache/ImageFetcher.java410
-rw-r--r--src/com/andrew/apollo/cache/ImageWorker.java454
-rw-r--r--src/com/andrew/apollo/cache/LruCache.java333
-rw-r--r--src/com/andrew/apollo/dragdrop/DragSortController.java442
-rw-r--r--src/com/andrew/apollo/dragdrop/DragSortListView.java2115
-rw-r--r--src/com/andrew/apollo/dragdrop/SimpleFloatViewManager.java77
-rw-r--r--src/com/andrew/apollo/format/Capitalize.java60
-rw-r--r--src/com/andrew/apollo/format/PrefixHighlighter.java124
-rw-r--r--src/com/andrew/apollo/grid/fragments/AlbumsFragment.java263
-rw-r--r--src/com/andrew/apollo/grid/fragments/ArtistsFragment.java272
-rw-r--r--src/com/andrew/apollo/grid/fragments/QuickQueueFragment.java270
-rw-r--r--src/com/andrew/apollo/lastfm/Album.java108
-rw-r--r--src/com/andrew/apollo/lastfm/Artist.java160
-rw-r--r--src/com/andrew/apollo/lastfm/Caller.java279
-rw-r--r--src/com/andrew/apollo/lastfm/DomElement.java175
-rw-r--r--src/com/andrew/apollo/lastfm/Image.java74
-rw-r--r--src/com/andrew/apollo/lastfm/ImageHolder.java83
-rw-r--r--src/com/andrew/apollo/lastfm/ImageSize.java31
-rw-r--r--src/com/andrew/apollo/lastfm/ItemFactory.java44
-rw-r--r--src/com/andrew/apollo/lastfm/ItemFactoryBuilder.java77
-rw-r--r--src/com/andrew/apollo/lastfm/MapUtilities.java90
-rw-r--r--src/com/andrew/apollo/lastfm/MusicEntry.java94
-rw-r--r--src/com/andrew/apollo/lastfm/PaginatedResult.java87
-rw-r--r--src/com/andrew/apollo/lastfm/ResponseBuilder.java209
-rw-r--r--src/com/andrew/apollo/lastfm/Result.java138
-rw-r--r--src/com/andrew/apollo/lastfm/StringUtilities.java176
-rw-r--r--src/com/andrew/apollo/lastfm/api/Album.java114
-rw-r--r--src/com/andrew/apollo/lastfm/api/Artist.java99
-rw-r--r--src/com/andrew/apollo/lastfm/api/CallException.java53
-rw-r--r--src/com/andrew/apollo/lastfm/api/Caller.java262
-rw-r--r--src/com/andrew/apollo/lastfm/api/Image.java80
-rw-r--r--src/com/andrew/apollo/lastfm/api/ImageHolder.java85
-rw-r--r--src/com/andrew/apollo/lastfm/api/ImageSize.java36
-rw-r--r--src/com/andrew/apollo/lastfm/api/ItemFactory.java49
-rw-r--r--src/com/andrew/apollo/lastfm/api/ItemFactoryBuilder.java77
-rw-r--r--src/com/andrew/apollo/lastfm/api/MusicEntry.java123
-rw-r--r--src/com/andrew/apollo/lastfm/api/PaginatedResult.java91
-rw-r--r--src/com/andrew/apollo/lastfm/api/ResponseBuilder.java123
-rw-r--r--src/com/andrew/apollo/lastfm/api/Result.java119
-rw-r--r--src/com/andrew/apollo/lastfm/api/Session.java121
-rw-r--r--src/com/andrew/apollo/list/fragments/ArtistAlbumsFragment.java276
-rw-r--r--src/com/andrew/apollo/list/fragments/GenresFragment.java165
-rw-r--r--src/com/andrew/apollo/list/fragments/PlaylistsFragment.java197
-rw-r--r--src/com/andrew/apollo/list/fragments/RecentlyAddedFragment.java166
-rw-r--r--src/com/andrew/apollo/list/fragments/TracksFragment.java463
-rw-r--r--src/com/andrew/apollo/loaders/AlbumLoader.java121
-rw-r--r--src/com/andrew/apollo/loaders/AlbumSongLoader.java133
-rw-r--r--src/com/andrew/apollo/loaders/ArtistAlbumLoader.java126
-rw-r--r--src/com/andrew/apollo/loaders/ArtistLoader.java121
-rw-r--r--src/com/andrew/apollo/loaders/ArtistSongLoader.java123
-rw-r--r--src/com/andrew/apollo/loaders/AsyncHandler.java43
-rw-r--r--src/com/andrew/apollo/loaders/FavoritesLoader.java108
-rw-r--r--src/com/andrew/apollo/loaders/GenreLoader.java101
-rw-r--r--src/com/andrew/apollo/loaders/GenreSongLoader.java117
-rw-r--r--src/com/andrew/apollo/loaders/LastAddedLoader.java113
-rw-r--r--src/com/andrew/apollo/loaders/NowPlayingCursor.java292
-rw-r--r--src/com/andrew/apollo/loaders/PlaylistLoader.java119
-rw-r--r--src/com/andrew/apollo/loaders/PlaylistSongLoader.java127
-rw-r--r--src/com/andrew/apollo/loaders/QueueLoader.java96
-rw-r--r--src/com/andrew/apollo/loaders/RecentLoader.java120
-rw-r--r--src/com/andrew/apollo/loaders/SearchLoader.java126
-rw-r--r--src/com/andrew/apollo/loaders/SongLoader.java114
-rw-r--r--src/com/andrew/apollo/loaders/WrappedAsyncTaskLoader.java70
-rw-r--r--src/com/andrew/apollo/lyrics/LyricsProvider.java22
-rw-r--r--src/com/andrew/apollo/lyrics/LyricsProviderFactory.java27
-rw-r--r--src/com/andrew/apollo/lyrics/LyricsWikiProvider.java116
-rw-r--r--src/com/andrew/apollo/lyrics/OfflineLyricsProvider.java138
-rw-r--r--src/com/andrew/apollo/menu/BasePlaylistDialog.java179
-rw-r--r--src/com/andrew/apollo/menu/CreateNewPlaylist.java145
-rw-r--r--src/com/andrew/apollo/menu/DeleteDialog.java105
-rw-r--r--src/com/andrew/apollo/menu/FragmentMenuItems.java79
-rw-r--r--src/com/andrew/apollo/menu/PhotoSelectionDialog.java165
-rw-r--r--src/com/andrew/apollo/menu/PlaylistDialog.java328
-rw-r--r--src/com/andrew/apollo/menu/PlaylistPicker.java121
-rw-r--r--src/com/andrew/apollo/menu/PlaylistPickerDialog.java28
-rw-r--r--src/com/andrew/apollo/menu/RenamePlaylist.java137
-rw-r--r--src/com/andrew/apollo/model/Album.java141
-rw-r--r--src/com/andrew/apollo/model/Artist.java126
-rw-r--r--src/com/andrew/apollo/model/Genre.java95
-rw-r--r--src/com/andrew/apollo/model/Playlist.java95
-rw-r--r--src/com/andrew/apollo/model/Song.java139
-rw-r--r--src/com/andrew/apollo/preferences/SettingsFragment.java22
-rw-r--r--src/com/andrew/apollo/preferences/SettingsHolder.java220
-rw-r--r--src/com/andrew/apollo/preferences/ThemePreview.java111
-rw-r--r--src/com/andrew/apollo/provider/FavoritesStore.java240
-rw-r--r--src/com/andrew/apollo/provider/RecentStore.java203
-rw-r--r--src/com/andrew/apollo/recycler/RecycleHolder.java65
-rw-r--r--src/com/andrew/apollo/service/ApolloService.java2280
-rw-r--r--src/com/andrew/apollo/service/ServiceBinder.java31
-rw-r--r--src/com/andrew/apollo/service/ServiceToken.java12
-rw-r--r--src/com/andrew/apollo/tasks/BitmapFromURL.java43
-rw-r--r--src/com/andrew/apollo/tasks/FetchAlbumImages.java122
-rw-r--r--src/com/andrew/apollo/tasks/FetchArtistImages.java86
-rw-r--r--src/com/andrew/apollo/tasks/GetCachedImages.java71
-rw-r--r--src/com/andrew/apollo/tasks/LastfmGetAlbumImages.java76
-rw-r--r--src/com/andrew/apollo/tasks/LastfmGetArtistImages.java65
-rw-r--r--src/com/andrew/apollo/tasks/LastfmGetArtistImagesOriginal.java80
-rw-r--r--src/com/andrew/apollo/tasks/ViewHolderQueueTask.java81
-rw-r--r--src/com/andrew/apollo/tasks/ViewHolderTask.java93
-rw-r--r--src/com/andrew/apollo/ui/MusicHolder.java143
-rw-r--r--src/com/andrew/apollo/ui/activities/AudioPlayerActivity.java895
-rw-r--r--src/com/andrew/apollo/ui/activities/BaseActivity.java463
-rw-r--r--src/com/andrew/apollo/ui/activities/HomeActivity.java49
-rw-r--r--src/com/andrew/apollo/ui/activities/ProfileActivity.java685
-rw-r--r--src/com/andrew/apollo/ui/activities/SearchActivity.java593
-rw-r--r--src/com/andrew/apollo/ui/activities/SettingsActivity.java291
-rw-r--r--src/com/andrew/apollo/ui/activities/ShortcutActivity.java353
-rw-r--r--src/com/andrew/apollo/ui/activities/ThemesActivity.java91
-rw-r--r--src/com/andrew/apollo/ui/fragments/AlbumFragment.java463
-rw-r--r--src/com/andrew/apollo/ui/fragments/ArtistFragment.java461
-rw-r--r--src/com/andrew/apollo/ui/fragments/GenreFragment.java238
-rw-r--r--src/com/andrew/apollo/ui/fragments/LyricsFragment.java182
-rw-r--r--src/com/andrew/apollo/ui/fragments/PlaylistFragment.java336
-rw-r--r--src/com/andrew/apollo/ui/fragments/QueueFragment.java409
-rw-r--r--src/com/andrew/apollo/ui/fragments/RecentFragment.java429
-rw-r--r--src/com/andrew/apollo/ui/fragments/SongFragment.java372
-rw-r--r--src/com/andrew/apollo/ui/fragments/ThemeFragment.java320
-rw-r--r--src/com/andrew/apollo/ui/fragments/phone/MusicBrowserPhoneFragment.java345
-rw-r--r--src/com/andrew/apollo/ui/fragments/profile/AlbumSongFragment.java331
-rw-r--r--src/com/andrew/apollo/ui/fragments/profile/ArtistAlbumFragment.java332
-rw-r--r--src/com/andrew/apollo/ui/fragments/profile/ArtistSongFragment.java331
-rw-r--r--src/com/andrew/apollo/ui/fragments/profile/FavoriteFragment.java328
-rw-r--r--src/com/andrew/apollo/ui/fragments/profile/GenreSongFragment.java338
-rw-r--r--src/com/andrew/apollo/ui/fragments/profile/LastAddedFragment.java325
-rw-r--r--src/com/andrew/apollo/ui/fragments/profile/PlaylistSongFragment.java385
-rw-r--r--src/com/andrew/apollo/ui/widgets/BottomActionBar.java126
-rw-r--r--src/com/andrew/apollo/ui/widgets/BottomActionBarItem.java154
-rw-r--r--src/com/andrew/apollo/ui/widgets/RepeatingImageButton.java137
-rw-r--r--src/com/andrew/apollo/ui/widgets/ScrollableTabView.java198
-rw-r--r--src/com/andrew/apollo/utils/ApolloUtils.java554
-rw-r--r--src/com/andrew/apollo/utils/BitmapUtils.java322
-rw-r--r--src/com/andrew/apollo/utils/DomElement.java167
-rw-r--r--src/com/andrew/apollo/utils/Lists.java52
-rw-r--r--src/com/andrew/apollo/utils/MapUtilities.java91
-rw-r--r--src/com/andrew/apollo/utils/MusicUtils.java1783
-rw-r--r--src/com/andrew/apollo/utils/NavUtils.java152
-rw-r--r--src/com/andrew/apollo/utils/PreferenceUtils.java467
-rw-r--r--src/com/andrew/apollo/utils/SharedPreferencesCompat.java63
-rw-r--r--src/com/andrew/apollo/utils/SortOrder.java154
-rw-r--r--src/com/andrew/apollo/utils/Stopwatch.java247
-rw-r--r--src/com/andrew/apollo/utils/StringUtilities.java189
-rw-r--r--src/com/andrew/apollo/utils/ThemeUtils.java550
-rw-r--r--src/com/andrew/apollo/utils/Ticker.java56
-rw-r--r--src/com/andrew/apollo/views/ViewHolderGrid.java39
-rw-r--r--src/com/andrew/apollo/views/ViewHolderList.java48
-rw-r--r--src/com/andrew/apollo/views/ViewHolderQueue.java34
-rw-r--r--src/com/andrew/apollo/widgets/AlphaPatternDrawable.java132
-rw-r--r--src/com/andrew/apollo/widgets/AlphaTouchInterceptorOverlay.java112
-rw-r--r--src/com/andrew/apollo/widgets/CarouselTab.java246
-rw-r--r--src/com/andrew/apollo/widgets/ColorPanelView.java166
-rw-r--r--src/com/andrew/apollo/widgets/ColorPickerView.java947
-rw-r--r--src/com/andrew/apollo/widgets/ColorSchemeDialog.java218
-rw-r--r--src/com/andrew/apollo/widgets/CompatActionBarNavHandler.java79
-rw-r--r--src/com/andrew/apollo/widgets/CompatActionBarNavListener.java25
-rw-r--r--src/com/andrew/apollo/widgets/FrameLayoutWithOverlay.java75
-rw-r--r--src/com/andrew/apollo/widgets/LayoutSuppressingImageView.java42
-rw-r--r--src/com/andrew/apollo/widgets/PlayPauseButton.java102
-rw-r--r--src/com/andrew/apollo/widgets/ProfileTabCarousel.java533
-rw-r--r--src/com/andrew/apollo/widgets/RepeatButton.java116
-rw-r--r--src/com/andrew/apollo/widgets/RepeatingImageButton.java216
-rw-r--r--src/com/andrew/apollo/widgets/SeparatedListAdapter.java157
-rw-r--r--src/com/andrew/apollo/widgets/ShowHideMasterLayout.java398
-rw-r--r--src/com/andrew/apollo/widgets/ShuffleButton.java110
-rw-r--r--src/com/andrew/apollo/widgets/SquareImageView.java45
-rw-r--r--src/com/andrew/apollo/widgets/SquareView.java63
-rw-r--r--src/com/andrew/apollo/widgets/VerticalScrollListener.java87
-rw-r--r--src/com/andrew/apollo/widgets/theme/BottomActionBar.java57
-rw-r--r--src/com/andrew/apollo/widgets/theme/Colorstrip.java44
-rw-r--r--src/com/andrew/apollo/widgets/theme/HoloSelector.java78
-rw-r--r--src/com/andrew/apollo/widgets/theme/ThemeableFrameLayout.java47
-rw-r--r--src/com/andrew/apollo/widgets/theme/ThemeableSeekBar.java42
-rw-r--r--src/com/andrew/apollo/widgets/theme/ThemeableTextView.java98
-rw-r--r--src/com/andrew/apollo/widgets/theme/ThemeableTitlePageIndicator.java67
535 files changed, 40102 insertions, 15680 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 7d17e87..8ff9c96 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -1,194 +1,236 @@
-<?xml version="1.0" encoding="utf-8"?>
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.andrew.apollo"
- android:versionCode="1"
- android:versionName="1.0" >
-
- <uses-sdk
- android:minSdkVersion="14"
- android:targetSdkVersion="15" />
-
- <!-- This is used for Last.fm and Google Music -->
- <uses-permission android:name="android.permission.INTERNET" />
- <!-- Used to check for a data connection -->
- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
- <!-- Used to keep the service running when the phone sleeps -->
- <uses-permission android:name="android.permission.WAKE_LOCK" />
- <!-- Stick Broadcast -->
- <uses-permission android:name="android.permission.BROADCAST_STICKY" />
- <!-- Incoming calls -->
- <uses-permission android:name="android.permission.READ_PHONE_STATE" />
- <!-- Used to set ringtone -->
- <uses-permission android:name="android.permission.WRITE_SETTINGS" />
-
- <application
- android:allowTaskReparenting="true"
- android:hardwareAccelerated="true"
- android:icon="@drawable/ic_launcher"
- android:label="@string/app_name"
- android:largeHeap="true"
- android:process=":main"
- android:taskAffinity="apollo.task.music"
- android:theme="@android:style/Theme.Holo.Light" >
- <meta-data
- android:name="android.app.default_searchable"
- android:value=".activities.QueryBrowserActivity" />
- <!-- Serach -->
- <activity
- android:name=".activities.QueryBrowserActivity"
- android:exported="true"
- android:theme="@android:style/Theme.Holo.Light" >
- <intent-filter>
- <action android:name="android.intent.action.SEARCH" />
- <action android:name="android.intent.action.MEDIA_SEARCH" />
- <action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
-
- <category android:name="android.intent.category.DEFAULT" />
- </intent-filter>
-
- <meta-data
- android:name="android.app.searchable"
- android:resource="@xml/searchable" />
- </activity>
-
- <!-- Main Activity -->
- <activity
- android:name=".activities.MusicLibrary"
- android:label="@string/app_name"
- android:windowSoftInputMode="adjustPan" >
- <intent-filter>
- <action android:name="android.intent.action.MAIN" />
- <action android:name="android.intent.action.MUSIC_PLAYER" />
-
- <category android:name="android.intent.category.DEFAULT" />
- <category android:name="android.intent.category.LAUNCHER" />
- <category android:name="android.intent.category.APP_MUSIC" />
- </intent-filter>
- </activity>
- <!-- Now Playing -->
- <activity
- android:name=".activities.AudioPlayerHolder"
- android:clearTaskOnLaunch="true"
- android:excludeFromRecents="true"
- android:label="@string/nowplaying"
- android:launchMode="singleTask"
- android:windowSoftInputMode="adjustPan" >
- <intent-filter>
- <action android:name="android.intent.action.VIEW" />
-
- <category android:name="android.intent.category.DEFAULT" />
-
- <data android:scheme="content" />
- <data android:host="media" />
- <data android:mimeType="audio/*" />
- <data android:mimeType="application/ogg" />
- <data android:mimeType="application/x-ogg" />
- <data android:mimeType="application/itunes" />
- </intent-filter>
- <intent-filter>
- <action android:name="com.andrew.apollo.PLAYBACK_VIEWER" />
-
- <category android:name="android.intent.category.DEFAULT" />
- </intent-filter>
- </activity>
- <!-- Track browser -->
- <activity
- android:name=".activities.TracksBrowser"
- android:label="@string/app_name"
- android:windowSoftInputMode="adjustPan" >
- <intent-filter>
- <action android:name="android.intent.action.EDIT" />
- <action android:name="android.intent.action.VIEW" />
-
- <category android:name="android.intent.category.DEFAULT" />
- </intent-filter>
- </activity>
-
- <!-- Quickly show the queue -->
- <activity
- android:name=".activities.QuickQueue"
- android:excludeFromRecents="true"
- android:launchMode="singleTop"
- android:noHistory="true"
- android:theme="@style/Theme.QuickQueue"
- android:windowSoftInputMode="stateUnchanged" />
- <!-- Settings -->
- <activity
- android:name=".preferences.SettingsHolder"
- android:label="@string/settings" />
- <activity
- android:name=".menu.PlaylistDialog"
- android:label="@string/rename_playlist"
- android:theme="@android:style/Theme.Holo.Light.Dialog.NoActionBar" >
- <intent-filter>
- <action android:name="com.andrew.apollo.CREATE_PLAYLIST" />
-
- <category android:name="android.intent.category.DEFAULT" />
- </intent-filter>
- <intent-filter>
- <action android:name="com.andrew.apollo.RENAME_PLAYLIST" />
-
- <category android:name="android.intent.category.DEFAULT" />
- </intent-filter>
- </activity>
- <activity
- android:name=".menu.PlaylistPicker"
- android:icon="@drawable/ic_launcher"
- android:theme="@android:style/Theme.Holo.Light.Dialog.NoActionBar" >
- <intent-filter>
- <action android:name="com.andrew.apollo.ADD_TO_PLAYLIST" />
-
- <category android:name="android.intent.category.DEFAULT" />
- </intent-filter>
- </activity>
- <!-- 1x1 App Widget -->
- <receiver
- android:name="com.andrew.apollo.app.widgets.AppWidget11"
- android:label="@string/apollo_1x1" >
- <intent-filter>
- <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
- </intent-filter>
-
- <meta-data
- android:name="android.appwidget.provider"
- android:resource="@xml/appwidget1x1_info" />
- </receiver>
- <!-- 4x1 App Widget -->
- <receiver
- android:name="com.andrew.apollo.app.widgets.AppWidget41"
- android:label="@string/apollo_4x1" >
- <intent-filter>
- <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
- </intent-filter>
-
- <meta-data
- android:name="android.appwidget.provider"
- android:resource="@xml/appwidget4x1_info" />
- </receiver>
- <!-- 4x2 App Widget -->
- <receiver
- android:name="com.andrew.apollo.app.widgets.AppWidget42"
- android:label="@string/apollo_4x2" >
- <intent-filter>
- <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
- </intent-filter>
-
- <meta-data
- android:name="android.appwidget.provider"
- android:resource="@xml/appwidget4x2_info" />
- </receiver>
- <!-- Media button receiver -->
- <receiver android:name=".service.MediaButtonIntentReceiver" >
- <intent-filter>
- <action android:name="android.intent.action.MEDIA_BUTTON" />
- <action android:name="android.media.AUDIO_BECOMING_NOISY" />
- </intent-filter>
- </receiver>
- <!-- Music service -->
- <service
- android:name=".service.ApolloService"
- android:label="@string/app_name"
- android:process=":main" />
- </application>
-
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.andrew.apollo"
+ android:versionCode="1"
+ android:versionName="1.0" >
+
+ <!-- Gingerbread to Jelly Bean -->
+ <uses-sdk
+ android:minSdkVersion="9"
+ android:targetSdkVersion="17" />
+
+ <!-- Used for caching and creating new playlists -->
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+ <!-- Used to check for a network connection -->
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ <!-- Used to download images -->
+ <uses-permission android:name="android.permission.INTERNET" />
+ <!-- Used to keep the service running when the phone sleeps -->
+ <uses-permission android:name="android.permission.WAKE_LOCK" />
+ <!-- The main service uses a sticky broadcast -->
+ <uses-permission android:name="android.permission.BROADCAST_STICKY" />
+ <!-- Lower or raise the music based on the phone state -->
+ <uses-permission android:name="android.permission.READ_PHONE_STATE" />
+ <!-- Used to set the devices's ringtone -->
+ <uses-permission android:name="android.permission.WRITE_SETTINGS" />
+ <!-- Used to create launcher shortcuts -->
+ <uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
+ <!-- Used to check if the app is in the background -->
+ <uses-permission android:name="android.permission.GET_TASKS" />
+
+ <application
+ android:name=".ApolloApplication"
+ android:allowBackup="true"
+ android:allowTaskReparenting="true"
+ android:hardwareAccelerated="@bool/config_hardwareAccelerated"
+ android:icon="@drawable/ic_launcher"
+ android:label="@string/app_name"
+ android:largeHeap="@bool/config_largeHeap"
+ android:taskAffinity="com.andrew.apollo.task" >
+
+ <!-- Searchable -->
+ <meta-data
+ android:name="android.app.default_searchable"
+ android:value=".ui.activities.SearchActivity" />
+ <!-- Main activity -->
+ <activity
+ android:name=".ui.activities.HomeActivity"
+ android:windowSoftInputMode="adjustPan" >
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <action android:name="android.intent.action.MUSIC_PLAYER" />
+
+ <category android:name="android.intent.category.LAUNCHER" />
+ <category android:name="android.intent.category.APP_MUSIC" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+ <!-- Now playing -->
+ <activity
+ android:name=".ui.activities.AudioPlayerActivity"
+ android:clearTaskOnLaunch="true"
+ android:exported="true"
+ android:launchMode="singleTask"
+ android:windowSoftInputMode="adjustPan" >
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+
+ <category android:name="android.intent.category.DEFAULT" />
+
+ <data android:scheme="content" />
+ <data android:host="media" />
+ <data android:mimeType="audio/*" />
+ <data android:mimeType="application/ogg" />
+ <data android:mimeType="application/x-ogg" />
+ <data android:mimeType="application/itunes" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+
+ <category android:name="android.intent.category.DEFAULT" />
+
+ <data android:scheme="file" />
+ <data android:mimeType="audio/*" />
+ <data android:mimeType="application/ogg" />
+ <data android:mimeType="application/x-ogg" />
+ <data android:mimeType="application/itunes" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+
+ <data android:scheme="http" />
+ <data android:mimeType="audio/*" />
+ <data android:mimeType="application/ogg" />
+ <data android:mimeType="application/x-ogg" />
+ <data android:mimeType="application/itunes" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="com.andrew.apollo.AUDIO_PLAYER" />
+
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+
+ <!-- Profile phone Activity -->
+ <activity
+ android:name=".ui.activities.ProfileActivity"
+ android:excludeFromRecents="true" />
+ <!-- Shortcut launcher Activity -->
+ <activity
+ android:name=".ui.activities.ShortcutActivity"
+ android:excludeFromRecents="true"
+ android:exported="true"
+ android:theme="@style/Theme.Transparent" >
+ <intent-filter>
+ <action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
+
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+ <!-- Search interface -->
+ <activity
+ android:name=".ui.activities.SearchActivity"
+ android:exported="true" >
+ <intent-filter>
+ <action android:name="android.intent.action.SEARCH" />
+ <action android:name="android.intent.action.MEDIA_SEARCH" />
+
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+
+ <meta-data
+ android:name="android.app.searchable"
+ android:resource="@xml/searchable" />
+ </activity>
+ <!-- Used to set options -->
+ <activity
+ android:name=".ui.activities.SettingsActivity"
+ android:label="@string/menu_settings"
+ android:theme="@style/Apollo.Theme.Dark" />
+ <!-- Themes Activity -->
+ <activity
+ android:name=".ui.activities.ThemesActivity"
+ android:excludeFromRecents="true" />
+ <!-- 4x1 App Widget -->
+ <receiver
+ android:name="com.andrew.apollo.appwidgets.AppWidgetSmall"
+ android:exported="false"
+ android:label="@string/app_widget_small" >
+ <intent-filter>
+ <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
+ </intent-filter>
+
+ <meta-data
+ android:name="android.appwidget.provider"
+ android:resource="@xml/app_widget_small" />
+ </receiver>
+ <!-- 4x2 App Widget -->
+ <receiver
+ android:name="com.andrew.apollo.appwidgets.AppWidgetLarge"
+ android:enabled="@bool/has_honeycomb"
+ android:exported="false"
+ android:label="@string/app_widget_large" >
+ <intent-filter>
+ <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
+ </intent-filter>
+
+ <meta-data
+ android:name="android.appwidget.provider"
+ android:resource="@xml/app_widget_large" />
+ </receiver>
+ <!-- 4x2 alternate App Widget -->
+ <receiver
+ android:name="com.andrew.apollo.appwidgets.AppWidgetLargeAlternate"
+ android:enabled="@bool/has_honeycomb"
+ android:exported="false"
+ android:label="@string/app_widget_large_alt" >
+ <intent-filter>
+ <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
+ </intent-filter>
+
+ <meta-data
+ android:name="android.appwidget.provider"
+ android:resource="@xml/app_widget_large_alternate" />
+ </receiver>
+ <!-- Resizable recently listened App Widget -->
+ <receiver
+ android:name="com.andrew.apollo.appwidgets.RecentWidgetProvider"
+ android:enabled="@bool/has_honeycomb"
+ android:exported="false"
+ android:label="@string/app_widget_recent" >
+ <intent-filter>
+ <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
+ </intent-filter>
+ <!-- This specifies the widget provider info -->
+ <meta-data
+ android:name="android.appwidget.provider"
+ android:resource="@xml/app_widget_recents" />
+ </receiver>
+ <!-- The service serving the RemoteViews to the recently listened App Widget -->
+ <service
+ android:name="com.andrew.apollo.appwidgets.RecentWidgetService"
+ android:enabled="@bool/has_honeycomb"
+ android:permission="android.permission.BIND_REMOTEVIEWS" />
+ <!-- Media button receiver -->
+ <receiver android:name=".MediaButtonIntentReceiver" >
+ <intent-filter>
+ <action android:name="android.intent.action.MEDIA_BUTTON" />
+ <action android:name="android.media.AUDIO_BECOMING_NOISY" />
+ </intent-filter>
+ </receiver>
+ <!-- Music service -->
+ <service
+ android:name=".MusicPlaybackService"
+ android:label="@string/app_name"
+ android:process=":main" />
+ </application>
+
</manifest> \ No newline at end of file
diff --git a/assets/Frame0.png b/assets/Frame0.png
deleted file mode 100644
index 5a977d8..0000000
--- a/assets/Frame0.png
+++ /dev/null
Binary files differ
diff --git a/assets/Frame1.png b/assets/Frame1.png
deleted file mode 100644
index a377d76..0000000
--- a/assets/Frame1.png
+++ /dev/null
Binary files differ
diff --git a/assets/Frame10.png b/assets/Frame10.png
deleted file mode 100644
index a189e9d..0000000
--- a/assets/Frame10.png
+++ /dev/null
Binary files differ
diff --git a/assets/Frame11.png b/assets/Frame11.png
deleted file mode 100644
index fed6906..0000000
--- a/assets/Frame11.png
+++ /dev/null
Binary files differ
diff --git a/assets/Frame2.png b/assets/Frame2.png
deleted file mode 100644
index 84e2a7a..0000000
--- a/assets/Frame2.png
+++ /dev/null
Binary files differ
diff --git a/assets/Frame3.png b/assets/Frame3.png
deleted file mode 100644
index c221e28..0000000
--- a/assets/Frame3.png
+++ /dev/null
Binary files differ
diff --git a/assets/Frame4.png b/assets/Frame4.png
deleted file mode 100644
index 6654af8..0000000
--- a/assets/Frame4.png
+++ /dev/null
Binary files differ
diff --git a/assets/Frame5.png b/assets/Frame5.png
deleted file mode 100644
index 92731d9..0000000
--- a/assets/Frame5.png
+++ /dev/null
Binary files differ
diff --git a/assets/Frame6.png b/assets/Frame6.png
deleted file mode 100644
index 473f1c1..0000000
--- a/assets/Frame6.png
+++ /dev/null
Binary files differ
diff --git a/assets/Frame7.png b/assets/Frame7.png
deleted file mode 100644
index df20376..0000000
--- a/assets/Frame7.png
+++ /dev/null
Binary files differ
diff --git a/assets/Frame8.png b/assets/Frame8.png
deleted file mode 100644
index 7cebd07..0000000
--- a/assets/Frame8.png
+++ /dev/null
Binary files differ
diff --git a/assets/Frame9.png b/assets/Frame9.png
deleted file mode 100644
index bde4b9d..0000000
--- a/assets/Frame9.png
+++ /dev/null
Binary files differ
diff --git a/assets/RobotoLight.ttf b/assets/RobotoLight.ttf
new file mode 100644
index 0000000..d43e943
--- /dev/null
+++ b/assets/RobotoLight.ttf
Binary files differ
diff --git a/assets/RobotoThin.ttf b/assets/RobotoThin.ttf
new file mode 100644
index 0000000..861d63a
--- /dev/null
+++ b/assets/RobotoThin.ttf
Binary files differ
diff --git a/assets/licenses.html b/assets/licenses.html
new file mode 100644
index 0000000..61a5d29
--- /dev/null
+++ b/assets/licenses.html
@@ -0,0 +1,90 @@
+<html><head><style> body { font-family: sans-serif; } pre { background-color: #eeeeee; padding: 1em; white-space: pre-wrap; } </style></head><body>
+<h3>Notices for files:</h3>
+<ul>
+<li>ActionBarSherlock</li>
+<li>ViewPagerIndicator</li>
+<li>NineOldAndroids.jar</li>
+</ul>
+<pre>
+/*
+ * Copyright 2012 Jake Wharton
+ *
+ * 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.
+ */
+</pre>
+
+<h3>Notices for file:</h3>
+<ul>
+<li>AppMsg</li>
+</ul>
+<pre>
+/*
+ * Copyright 2012 Evgeny Shishkin
+ *
+ * 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.
+ */
+</pre>
+
+<h3>Notices for files:</h3><ul>
+<li>DragSortListView</li>
+</ul>
+<pre>
+/*
+ * Copyright (C) 2012 Carl Bauer
+ *
+ * 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.
+ */
+</pre>
+</body></html>
+
+<h3>Notices for file:</h3><ul>
+<li>android-support-v4.jar</li>
+</ul>
+<pre>
+/*
+ * Copyright (C) 2012 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.
+ */
+</pre>
+</body></html> \ No newline at end of file
diff --git a/libs/android-query-0.21.7.jar b/libs/android-query-0.21.7.jar
deleted file mode 100644
index f2dd72a..0000000
--- a/libs/android-query-0.21.7.jar
+++ /dev/null
Binary files differ
diff --git a/libs/android-support-v4.jar b/libs/android-support-v4.jar
index 1fbeba0..feaf44f 100644
--- a/libs/android-support-v4.jar
+++ b/libs/android-support-v4.jar
Binary files differ
diff --git a/libs/jaudiotagger.jar b/libs/jaudiotagger.jar
new file mode 100644
index 0000000..51d7774
--- /dev/null
+++ b/libs/jaudiotagger.jar
Binary files differ
diff --git a/libs/nineoldandroids.jar b/libs/nineoldandroids.jar
new file mode 100644
index 0000000..43ee45f
--- /dev/null
+++ b/libs/nineoldandroids.jar
Binary files differ
diff --git a/res/anim/peak_meter_1.xml b/res/anim/peak_meter_1.xml
deleted file mode 100644
index c9fbfde..0000000
--- a/res/anim/peak_meter_1.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<animation-list android:oneshot="false"
- xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:duration="@integer/peak" android:drawable="@drawable/indicator_playing_peak_meter_1" />
- <item android:duration="@integer/peak" android:drawable="@drawable/indicator_playing_peak_meter_2" />
- <item android:duration="@integer/peak" android:drawable="@drawable/indicator_playing_peak_meter_4" />
- <item android:duration="@integer/peak" android:drawable="@drawable/indicator_playing_peak_meter_3" />
- <item android:duration="@integer/peak" android:drawable="@drawable/indicator_playing_peak_meter_4" />
- <item android:duration="@integer/peak" android:drawable="@drawable/indicator_playing_peak_meter_3" />
- <item android:duration="@integer/peak" android:drawable="@drawable/indicator_playing_peak_meter_2" />
- <item android:duration="@integer/peak" android:drawable="@drawable/indicator_playing_peak_meter_4" />
- <item android:duration="@integer/peak" android:drawable="@drawable/indicator_playing_peak_meter_5" />
- <item android:duration="@integer/peak" android:drawable="@drawable/indicator_playing_peak_meter_4" />
- <item android:duration="@integer/peak" android:drawable="@drawable/indicator_playing_peak_meter_3" />
- <item android:duration="@integer/peak" android:drawable="@drawable/indicator_playing_peak_meter_2" />
- <item android:duration="@integer/peak" android:drawable="@drawable/indicator_playing_peak_meter_1" />
- <item android:duration="@integer/peak" android:drawable="@drawable/indicator_playing_peak_meter_3" />
-</animation-list>
diff --git a/res/anim/peak_meter_2.xml b/res/anim/peak_meter_2.xml
deleted file mode 100644
index 5601a21..0000000
--- a/res/anim/peak_meter_2.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<animation-list android:oneshot="false"
- xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:duration="@integer/peak" android:drawable="@drawable/indicator_playing_peak_meter_1" />
- <item android:duration="@integer/peak" android:drawable="@drawable/indicator_playing_peak_meter_4" />
- <item android:duration="@integer/peak" android:drawable="@drawable/indicator_playing_peak_meter_3" />
- <item android:duration="@integer/peak" android:drawable="@drawable/indicator_playing_peak_meter_2" />
- <item android:duration="@integer/peak" android:drawable="@drawable/indicator_playing_peak_meter_1" />
- <item android:duration="@integer/peak" android:drawable="@drawable/indicator_playing_peak_meter_3" />
- <item android:duration="@integer/peak" android:drawable="@drawable/indicator_playing_peak_meter_2" />
- <item android:duration="@integer/peak" android:drawable="@drawable/indicator_playing_peak_meter_3" />
- <item android:duration="@integer/peak" android:drawable="@drawable/indicator_playing_peak_meter_4" />
- <item android:duration="@integer/peak" android:drawable="@drawable/indicator_playing_peak_meter_3" />
- <item android:duration="@integer/peak" android:drawable="@drawable/indicator_playing_peak_meter_2" />
- <item android:duration="@integer/peak" android:drawable="@drawable/indicator_playing_peak_meter_4" />
- <item android:duration="@integer/peak" android:drawable="@drawable/indicator_playing_peak_meter_5" />
-</animation-list>
diff --git a/res/color/tab_text_color.xml b/res/color/tab_text_color.xml
deleted file mode 100644
index 569f1c3..0000000
--- a/res/color/tab_text_color.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<selector
- xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:state_selected="true" android:color="@color/black" />
- <item android:color="@color/transparent_black" />
-</selector>
diff --git a/res/drawable-hdpi-v11/appwidget_bg.9.png b/res/drawable-hdpi-v11/appwidget_bg.9.png
new file mode 100644
index 0000000..6bacc7f
--- /dev/null
+++ b/res/drawable-hdpi-v11/appwidget_bg.9.png
Binary files differ
diff --git a/res/drawable-hdpi-v8/stat_notify_music.png b/res/drawable-hdpi-v8/stat_notify_music.png
new file mode 100644
index 0000000..0ddcf8e
--- /dev/null
+++ b/res/drawable-hdpi-v8/stat_notify_music.png
Binary files differ
diff --git a/res/drawable-hdpi-v9/stat_notify_music.png b/res/drawable-hdpi-v9/stat_notify_music.png
new file mode 100644
index 0000000..d6be948
--- /dev/null
+++ b/res/drawable-hdpi-v9/stat_notify_music.png
Binary files differ
diff --git a/res/drawable-hdpi/apollo_holo_light_favorite_normal.png b/res/drawable-hdpi/apollo_holo_light_favorite_normal.png
deleted file mode 100644
index 98dd2ca..0000000
--- a/res/drawable-hdpi/apollo_holo_light_favorite_normal.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/apollo_holo_light_favorite_selected.png b/res/drawable-hdpi/apollo_holo_light_favorite_selected.png
deleted file mode 100644
index 861b898..0000000
--- a/res/drawable-hdpi/apollo_holo_light_favorite_selected.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/apollo_holo_light_next.png b/res/drawable-hdpi/apollo_holo_light_next.png
deleted file mode 100644
index b4f692f..0000000
--- a/res/drawable-hdpi/apollo_holo_light_next.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/apollo_holo_light_pause.png b/res/drawable-hdpi/apollo_holo_light_pause.png
deleted file mode 100644
index 9661cfb..0000000
--- a/res/drawable-hdpi/apollo_holo_light_pause.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/apollo_holo_light_play.png b/res/drawable-hdpi/apollo_holo_light_play.png
deleted file mode 100644
index e70f041..0000000
--- a/res/drawable-hdpi/apollo_holo_light_play.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/apollo_holo_light_previous.png b/res/drawable-hdpi/apollo_holo_light_previous.png
deleted file mode 100644
index ba9d60c..0000000
--- a/res/drawable-hdpi/apollo_holo_light_previous.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/apollo_holo_light_repeat_all.png b/res/drawable-hdpi/apollo_holo_light_repeat_all.png
deleted file mode 100644
index bc4c95a..0000000
--- a/res/drawable-hdpi/apollo_holo_light_repeat_all.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/apollo_holo_light_repeat_normal.png b/res/drawable-hdpi/apollo_holo_light_repeat_normal.png
deleted file mode 100644
index 8fc95ba..0000000
--- a/res/drawable-hdpi/apollo_holo_light_repeat_normal.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/apollo_holo_light_repeat_one.png b/res/drawable-hdpi/apollo_holo_light_repeat_one.png
deleted file mode 100644
index 4656cc0..0000000
--- a/res/drawable-hdpi/apollo_holo_light_repeat_one.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/apollo_holo_light_search.png b/res/drawable-hdpi/apollo_holo_light_search.png
deleted file mode 100644
index e6b7045..0000000
--- a/res/drawable-hdpi/apollo_holo_light_search.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/apollo_holo_light_shuffle_normal.png b/res/drawable-hdpi/apollo_holo_light_shuffle_normal.png
deleted file mode 100644
index 7397176..0000000
--- a/res/drawable-hdpi/apollo_holo_light_shuffle_normal.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/apollo_holo_light_shuffle_on.png b/res/drawable-hdpi/apollo_holo_light_shuffle_on.png
deleted file mode 100644
index 1095fcc..0000000
--- a/res/drawable-hdpi/apollo_holo_light_shuffle_on.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/apollo_settings_themes.png b/res/drawable-hdpi/apollo_settings_themes.png
deleted file mode 100644
index a646ad5..0000000
--- a/res/drawable-hdpi/apollo_settings_themes.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/appwidget_bg.9.png b/res/drawable-hdpi/appwidget_bg.9.png
index 1783677..9739693 100644
--- a/res/drawable-hdpi/appwidget_bg.9.png
+++ b/res/drawable-hdpi/appwidget_bg.9.png
Binary files differ
diff --git a/res/drawable-hdpi/bg_stripes_dark.png b/res/drawable-hdpi/bg_stripes_dark.png
new file mode 100644
index 0000000..4b61fb2
--- /dev/null
+++ b/res/drawable-hdpi/bg_stripes_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/apollo_holo_dark_notifiation_bar_collapse.png b/res/drawable-hdpi/btn_notification_collapse.png
index 5b04d33..5b04d33 100644
--- a/res/drawable-hdpi/apollo_holo_dark_notifiation_bar_collapse.png
+++ b/res/drawable-hdpi/btn_notification_collapse.png
Binary files differ
diff --git a/res/drawable-hdpi/apollo_holo_dark_next.png b/res/drawable-hdpi/btn_playback_next.png
index 738aae1..738aae1 100644
--- a/res/drawable-hdpi/apollo_holo_dark_next.png
+++ b/res/drawable-hdpi/btn_playback_next.png
Binary files differ
diff --git a/res/drawable-hdpi/apollo_holo_dark_pause.png b/res/drawable-hdpi/btn_playback_pause.png
index 6b435bb..6b435bb 100644
--- a/res/drawable-hdpi/apollo_holo_dark_pause.png
+++ b/res/drawable-hdpi/btn_playback_pause.png
Binary files differ
diff --git a/res/drawable-hdpi/apollo_holo_dark_play.png b/res/drawable-hdpi/btn_playback_play.png
index df8a2ca..df8a2ca 100644
--- a/res/drawable-hdpi/apollo_holo_dark_play.png
+++ b/res/drawable-hdpi/btn_playback_play.png
Binary files differ
diff --git a/res/drawable-hdpi/btn_playback_previous.png b/res/drawable-hdpi/btn_playback_previous.png
new file mode 100644
index 0000000..ba6c5ad
--- /dev/null
+++ b/res/drawable-hdpi/btn_playback_previous.png
Binary files differ
diff --git a/res/drawable-hdpi/btn_playback_repeat.png b/res/drawable-hdpi/btn_playback_repeat.png
new file mode 100644
index 0000000..fd84738
--- /dev/null
+++ b/res/drawable-hdpi/btn_playback_repeat.png
Binary files differ
diff --git a/res/drawable-hdpi/btn_playback_repeat_all.png b/res/drawable-hdpi/btn_playback_repeat_all.png
new file mode 100644
index 0000000..d665477
--- /dev/null
+++ b/res/drawable-hdpi/btn_playback_repeat_all.png
Binary files differ
diff --git a/res/drawable-hdpi/btn_playback_repeat_one.png b/res/drawable-hdpi/btn_playback_repeat_one.png
new file mode 100644
index 0000000..1a49b49
--- /dev/null
+++ b/res/drawable-hdpi/btn_playback_repeat_one.png
Binary files differ
diff --git a/res/drawable-hdpi/btn_playback_shuffle.png b/res/drawable-hdpi/btn_playback_shuffle.png
new file mode 100644
index 0000000..6504781
--- /dev/null
+++ b/res/drawable-hdpi/btn_playback_shuffle.png
Binary files differ
diff --git a/res/drawable-hdpi/btn_playback_shuffle_all.png b/res/drawable-hdpi/btn_playback_shuffle_all.png
new file mode 100644
index 0000000..ea3cd12
--- /dev/null
+++ b/res/drawable-hdpi/btn_playback_shuffle_all.png
Binary files differ
diff --git a/res/drawable-hdpi/btn_switch_queue.png b/res/drawable-hdpi/btn_switch_queue.png
new file mode 100644
index 0000000..740e55b
--- /dev/null
+++ b/res/drawable-hdpi/btn_switch_queue.png
Binary files differ
diff --git a/res/drawable-hdpi/colorstrip_shadow.9.png b/res/drawable-hdpi/colorstrip_shadow.9.png
deleted file mode 100644
index 285f123..0000000
--- a/res/drawable-hdpi/colorstrip_shadow.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/dropdown_ic_arrow_normal_holo_light.png b/res/drawable-hdpi/dropdown_ic_arrow_normal_holo_light.png
deleted file mode 100644
index d362ec1..0000000
--- a/res/drawable-hdpi/dropdown_ic_arrow_normal_holo_light.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/ic_action_favorite.png b/res/drawable-hdpi/ic_action_favorite.png
new file mode 100644
index 0000000..6678536
--- /dev/null
+++ b/res/drawable-hdpi/ic_action_favorite.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_action_pinn_to_home.png b/res/drawable-hdpi/ic_action_pinn_to_home.png
new file mode 100644
index 0000000..e6c7bf3
--- /dev/null
+++ b/res/drawable-hdpi/ic_action_pinn_to_home.png
Binary files differ
diff --git a/res/drawable-hdpi/apollo_holo_light_overflow.png b/res/drawable-hdpi/ic_action_search.png
index 0c844f3..67de12d 100644
--- a/res/drawable-hdpi/apollo_holo_light_overflow.png
+++ b/res/drawable-hdpi/ic_action_search.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_action_shop.png b/res/drawable-hdpi/ic_action_shop.png
new file mode 100644
index 0000000..c8c5bc9
--- /dev/null
+++ b/res/drawable-hdpi/ic_action_shop.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_launcher.png b/res/drawable-hdpi/ic_launcher.png
index b0006c9..dd0ff7d 100644
--- a/res/drawable-hdpi/ic_launcher.png
+++ b/res/drawable-hdpi/ic_launcher.png
Binary files differ
diff --git a/res/drawable-hdpi/indicator_playing_peak_meter_1.png b/res/drawable-hdpi/indicator_playing_peak_meter_1.png
deleted file mode 100644
index 7539244..0000000
--- a/res/drawable-hdpi/indicator_playing_peak_meter_1.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/indicator_playing_peak_meter_2.png b/res/drawable-hdpi/indicator_playing_peak_meter_2.png
deleted file mode 100644
index 289855e..0000000
--- a/res/drawable-hdpi/indicator_playing_peak_meter_2.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/indicator_playing_peak_meter_3.png b/res/drawable-hdpi/indicator_playing_peak_meter_3.png
deleted file mode 100644
index af883b3..0000000
--- a/res/drawable-hdpi/indicator_playing_peak_meter_3.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/indicator_playing_peak_meter_4.png b/res/drawable-hdpi/indicator_playing_peak_meter_4.png
deleted file mode 100644
index 3af8e7d..0000000
--- a/res/drawable-hdpi/indicator_playing_peak_meter_4.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/indicator_playing_peak_meter_5.png b/res/drawable-hdpi/indicator_playing_peak_meter_5.png
deleted file mode 100644
index eebba7c..0000000
--- a/res/drawable-hdpi/indicator_playing_peak_meter_5.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/list_section_divider_holo_custom.9.png b/res/drawable-hdpi/list_section_divider_holo_custom.9.png
deleted file mode 100644
index 1e3e778..0000000
--- a/res/drawable-hdpi/list_section_divider_holo_custom.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/notify_panel_notification_icon_bg.png b/res/drawable-hdpi/notify_panel_notification_icon_bg.png
deleted file mode 100644
index 6f37a22..0000000
--- a/res/drawable-hdpi/notify_panel_notification_icon_bg.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/playlist_tile_normal.9.png b/res/drawable-hdpi/playlist_tile_normal.9.png
new file mode 100644
index 0000000..c6726bc
--- /dev/null
+++ b/res/drawable-hdpi/playlist_tile_normal.9.png
Binary files differ
diff --git a/res/drawable-hdpi/queue_thumbnail_bg.9.png b/res/drawable-hdpi/queue_thumbnail_bg.9.png
deleted file mode 100644
index d000f7e..0000000
--- a/res/drawable-hdpi/queue_thumbnail_bg.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/recents_thumbnail_bg_press.9.png b/res/drawable-hdpi/recents_thumbnail_bg_press.9.png
deleted file mode 100644
index 288d818..0000000
--- a/res/drawable-hdpi/recents_thumbnail_bg_press.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/scrubber_primary_holo.9.png b/res/drawable-hdpi/scrubber_primary_holo.9.png
new file mode 100644
index 0000000..eb8b3ab
--- /dev/null
+++ b/res/drawable-hdpi/scrubber_primary_holo.9.png
Binary files differ
diff --git a/res/drawable-hdpi/scrubber_secondary_holo.9.png b/res/drawable-hdpi/scrubber_secondary_holo.9.png
new file mode 100644
index 0000000..3a0ca29
--- /dev/null
+++ b/res/drawable-hdpi/scrubber_secondary_holo.9.png
Binary files differ
diff --git a/res/drawable-hdpi/scrubber_track_holo_dark.9.png b/res/drawable-hdpi/scrubber_track_holo_dark.9.png
new file mode 100644
index 0000000..0c0ccda
--- /dev/null
+++ b/res/drawable-hdpi/scrubber_track_holo_dark.9.png
Binary files differ
diff --git a/res/drawable-hdpi/tab_selected_holo.9.png b/res/drawable-hdpi/tab_selected_holo.9.png
deleted file mode 100644
index b9801b0..0000000
--- a/res/drawable-hdpi/tab_selected_holo.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/tab_selected_pressed_focused_holo.9.png b/res/drawable-hdpi/tab_selected_pressed_focused_holo.9.png
deleted file mode 100644
index 296613b..0000000
--- a/res/drawable-hdpi/tab_selected_pressed_focused_holo.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/tab_selected_pressed_holo.9.png b/res/drawable-hdpi/tab_selected_pressed_holo.9.png
deleted file mode 100644
index 6c0e61e..0000000
--- a/res/drawable-hdpi/tab_selected_pressed_holo.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/tab_unselected_focused_holo.9.png b/res/drawable-hdpi/tab_unselected_focused_holo.9.png
deleted file mode 100644
index 9967301..0000000
--- a/res/drawable-hdpi/tab_unselected_focused_holo.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/tab_unselected_holo.9.png b/res/drawable-hdpi/tab_unselected_holo.9.png
deleted file mode 100644
index 1070d6a..0000000
--- a/res/drawable-hdpi/tab_unselected_holo.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/tab_unselected_pressed_holo.9.png b/res/drawable-hdpi/tab_unselected_pressed_holo.9.png
deleted file mode 100644
index 2b6bdac..0000000
--- a/res/drawable-hdpi/tab_unselected_pressed_holo.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/title_bar_shadow.9.png b/res/drawable-hdpi/title_bar_shadow.9.png
deleted file mode 100644
index e106a4c..0000000
--- a/res/drawable-hdpi/title_bar_shadow.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/view_pager_background_texture.png b/res/drawable-hdpi/view_pager_background_texture.png
new file mode 100644
index 0000000..e8cd304
--- /dev/null
+++ b/res/drawable-hdpi/view_pager_background_texture.png
Binary files differ
diff --git a/res/drawable-mdpi-v11/appwidget_bg.9.png b/res/drawable-mdpi-v11/appwidget_bg.9.png
new file mode 100644
index 0000000..09309a9
--- /dev/null
+++ b/res/drawable-mdpi-v11/appwidget_bg.9.png
Binary files differ
diff --git a/res/drawable-mdpi/apollo_holo_light_favorite_normal.png b/res/drawable-mdpi/apollo_holo_light_favorite_normal.png
deleted file mode 100644
index f4838d4..0000000
--- a/res/drawable-mdpi/apollo_holo_light_favorite_normal.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/apollo_holo_light_favorite_selected.png b/res/drawable-mdpi/apollo_holo_light_favorite_selected.png
deleted file mode 100644
index 55e1d3b..0000000
--- a/res/drawable-mdpi/apollo_holo_light_favorite_selected.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/apollo_holo_light_next.png b/res/drawable-mdpi/apollo_holo_light_next.png
deleted file mode 100644
index 937e029..0000000
--- a/res/drawable-mdpi/apollo_holo_light_next.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/apollo_holo_light_pause.png b/res/drawable-mdpi/apollo_holo_light_pause.png
deleted file mode 100644
index 01858e3..0000000
--- a/res/drawable-mdpi/apollo_holo_light_pause.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/apollo_holo_light_play.png b/res/drawable-mdpi/apollo_holo_light_play.png
deleted file mode 100644
index 1e3bc97..0000000
--- a/res/drawable-mdpi/apollo_holo_light_play.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/apollo_holo_light_previous.png b/res/drawable-mdpi/apollo_holo_light_previous.png
deleted file mode 100644
index 4e2b588..0000000
--- a/res/drawable-mdpi/apollo_holo_light_previous.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/apollo_holo_light_repeat_all.png b/res/drawable-mdpi/apollo_holo_light_repeat_all.png
deleted file mode 100644
index 4880369..0000000
--- a/res/drawable-mdpi/apollo_holo_light_repeat_all.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/apollo_holo_light_repeat_normal.png b/res/drawable-mdpi/apollo_holo_light_repeat_normal.png
deleted file mode 100644
index a6e8935..0000000
--- a/res/drawable-mdpi/apollo_holo_light_repeat_normal.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/apollo_holo_light_repeat_one.png b/res/drawable-mdpi/apollo_holo_light_repeat_one.png
deleted file mode 100644
index 8889d93..0000000
--- a/res/drawable-mdpi/apollo_holo_light_repeat_one.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/apollo_holo_light_search.png b/res/drawable-mdpi/apollo_holo_light_search.png
deleted file mode 100644
index 3aa6440..0000000
--- a/res/drawable-mdpi/apollo_holo_light_search.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/apollo_holo_light_shuffle_normal.png b/res/drawable-mdpi/apollo_holo_light_shuffle_normal.png
deleted file mode 100644
index 5fd81e5..0000000
--- a/res/drawable-mdpi/apollo_holo_light_shuffle_normal.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/apollo_holo_light_shuffle_on.png b/res/drawable-mdpi/apollo_holo_light_shuffle_on.png
deleted file mode 100644
index 86c608a..0000000
--- a/res/drawable-mdpi/apollo_holo_light_shuffle_on.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/apollo_settings_themes.png b/res/drawable-mdpi/apollo_settings_themes.png
deleted file mode 100644
index a5ae584..0000000
--- a/res/drawable-mdpi/apollo_settings_themes.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/appwidget_bg.9.png b/res/drawable-mdpi/appwidget_bg.9.png
index 2ae3070..4523c65 100644
--- a/res/drawable-mdpi/appwidget_bg.9.png
+++ b/res/drawable-mdpi/appwidget_bg.9.png
Binary files differ
diff --git a/res/drawable-mdpi/bg_stripes_dark.png b/res/drawable-mdpi/bg_stripes_dark.png
new file mode 100644
index 0000000..4b61fb2
--- /dev/null
+++ b/res/drawable-mdpi/bg_stripes_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/apollo_holo_dark_notifiation_bar_collapse.png b/res/drawable-mdpi/btn_notification_collapse.png
index 1b4c46f..1b4c46f 100644
--- a/res/drawable-mdpi/apollo_holo_dark_notifiation_bar_collapse.png
+++ b/res/drawable-mdpi/btn_notification_collapse.png
Binary files differ
diff --git a/res/drawable-mdpi/apollo_holo_dark_next.png b/res/drawable-mdpi/btn_playback_next.png
index 28e8137..28e8137 100644
--- a/res/drawable-mdpi/apollo_holo_dark_next.png
+++ b/res/drawable-mdpi/btn_playback_next.png
Binary files differ
diff --git a/res/drawable-mdpi/apollo_holo_dark_pause.png b/res/drawable-mdpi/btn_playback_pause.png
index a5aee6f..a5aee6f 100644
--- a/res/drawable-mdpi/apollo_holo_dark_pause.png
+++ b/res/drawable-mdpi/btn_playback_pause.png
Binary files differ
diff --git a/res/drawable-mdpi/apollo_holo_dark_play.png b/res/drawable-mdpi/btn_playback_play.png
index 6a40cd5..6a40cd5 100644
--- a/res/drawable-mdpi/apollo_holo_dark_play.png
+++ b/res/drawable-mdpi/btn_playback_play.png
Binary files differ
diff --git a/res/drawable-mdpi/btn_playback_previous.png b/res/drawable-mdpi/btn_playback_previous.png
new file mode 100644
index 0000000..6c67842
--- /dev/null
+++ b/res/drawable-mdpi/btn_playback_previous.png
Binary files differ
diff --git a/res/drawable-mdpi/btn_playback_repeat.png b/res/drawable-mdpi/btn_playback_repeat.png
new file mode 100644
index 0000000..685609c
--- /dev/null
+++ b/res/drawable-mdpi/btn_playback_repeat.png
Binary files differ
diff --git a/res/drawable-mdpi/btn_playback_repeat_all.png b/res/drawable-mdpi/btn_playback_repeat_all.png
new file mode 100644
index 0000000..e708613
--- /dev/null
+++ b/res/drawable-mdpi/btn_playback_repeat_all.png
Binary files differ
diff --git a/res/drawable-mdpi/btn_playback_repeat_one.png b/res/drawable-mdpi/btn_playback_repeat_one.png
new file mode 100644
index 0000000..1c51450
--- /dev/null
+++ b/res/drawable-mdpi/btn_playback_repeat_one.png
Binary files differ
diff --git a/res/drawable-mdpi/btn_playback_shuffle.png b/res/drawable-mdpi/btn_playback_shuffle.png
new file mode 100644
index 0000000..fbbffa5
--- /dev/null
+++ b/res/drawable-mdpi/btn_playback_shuffle.png
Binary files differ
diff --git a/res/drawable-mdpi/btn_playback_shuffle_all.png b/res/drawable-mdpi/btn_playback_shuffle_all.png
new file mode 100644
index 0000000..fc687d1
--- /dev/null
+++ b/res/drawable-mdpi/btn_playback_shuffle_all.png
Binary files differ
diff --git a/res/drawable-mdpi/btn_switch_queue.png b/res/drawable-mdpi/btn_switch_queue.png
new file mode 100644
index 0000000..206dd48
--- /dev/null
+++ b/res/drawable-mdpi/btn_switch_queue.png
Binary files differ
diff --git a/res/drawable-mdpi/dropdown_ic_arrow_normal_holo_light.png b/res/drawable-mdpi/dropdown_ic_arrow_normal_holo_light.png
deleted file mode 100644
index c3fdef7..0000000
--- a/res/drawable-mdpi/dropdown_ic_arrow_normal_holo_light.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/ic_action_favorite.png b/res/drawable-mdpi/ic_action_favorite.png
new file mode 100644
index 0000000..f62b665
--- /dev/null
+++ b/res/drawable-mdpi/ic_action_favorite.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_action_pinn_to_home.png b/res/drawable-mdpi/ic_action_pinn_to_home.png
new file mode 100644
index 0000000..de9a29e
--- /dev/null
+++ b/res/drawable-mdpi/ic_action_pinn_to_home.png
Binary files differ
diff --git a/res/drawable-hdpi/apollo_holo_dark_overflow.png b/res/drawable-mdpi/ic_action_search.png
index 38aadc6..134d549 100644
--- a/res/drawable-hdpi/apollo_holo_dark_overflow.png
+++ b/res/drawable-mdpi/ic_action_search.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_action_shop.png b/res/drawable-mdpi/ic_action_shop.png
new file mode 100644
index 0000000..29a47d0
--- /dev/null
+++ b/res/drawable-mdpi/ic_action_shop.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_launcher.png b/res/drawable-mdpi/ic_launcher.png
index 34ebfd0..0352727 100644
--- a/res/drawable-mdpi/ic_launcher.png
+++ b/res/drawable-mdpi/ic_launcher.png
Binary files differ
diff --git a/res/drawable-mdpi/indicator_playing_peak_meter_1.png b/res/drawable-mdpi/indicator_playing_peak_meter_1.png
deleted file mode 100644
index 68013aa..0000000
--- a/res/drawable-mdpi/indicator_playing_peak_meter_1.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/indicator_playing_peak_meter_2.png b/res/drawable-mdpi/indicator_playing_peak_meter_2.png
deleted file mode 100644
index 4937611..0000000
--- a/res/drawable-mdpi/indicator_playing_peak_meter_2.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/indicator_playing_peak_meter_3.png b/res/drawable-mdpi/indicator_playing_peak_meter_3.png
deleted file mode 100644
index af6aa3d..0000000
--- a/res/drawable-mdpi/indicator_playing_peak_meter_3.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/indicator_playing_peak_meter_4.png b/res/drawable-mdpi/indicator_playing_peak_meter_4.png
deleted file mode 100644
index b7f92cd..0000000
--- a/res/drawable-mdpi/indicator_playing_peak_meter_4.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/indicator_playing_peak_meter_5.png b/res/drawable-mdpi/indicator_playing_peak_meter_5.png
deleted file mode 100644
index 98c1f10..0000000
--- a/res/drawable-mdpi/indicator_playing_peak_meter_5.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/list_section_divider_holo_custom.9.png b/res/drawable-mdpi/list_section_divider_holo_custom.9.png
deleted file mode 100644
index 1d8fd09..0000000
--- a/res/drawable-mdpi/list_section_divider_holo_custom.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/notify_panel_notification_icon_bg.png b/res/drawable-mdpi/notify_panel_notification_icon_bg.png
deleted file mode 100644
index 8fbf4bb..0000000
--- a/res/drawable-mdpi/notify_panel_notification_icon_bg.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/playlist_tile_normal.9.png b/res/drawable-mdpi/playlist_tile_normal.9.png
new file mode 100644
index 0000000..3d2cebf
--- /dev/null
+++ b/res/drawable-mdpi/playlist_tile_normal.9.png
Binary files differ
diff --git a/res/drawable-mdpi/queue_thumbnail_bg.9.png b/res/drawable-mdpi/queue_thumbnail_bg.9.png
deleted file mode 100644
index f19dc93..0000000
--- a/res/drawable-mdpi/queue_thumbnail_bg.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/recents_thumbnail_bg_press.9.png b/res/drawable-mdpi/recents_thumbnail_bg_press.9.png
deleted file mode 100644
index 10e4fd2..0000000
--- a/res/drawable-mdpi/recents_thumbnail_bg_press.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/scrubber_primary_holo.9.png b/res/drawable-mdpi/scrubber_primary_holo.9.png
new file mode 100644
index 0000000..aa2e382
--- /dev/null
+++ b/res/drawable-mdpi/scrubber_primary_holo.9.png
Binary files differ
diff --git a/res/drawable-mdpi/scrubber_secondary_holo.9.png b/res/drawable-mdpi/scrubber_secondary_holo.9.png
new file mode 100644
index 0000000..9a2f058
--- /dev/null
+++ b/res/drawable-mdpi/scrubber_secondary_holo.9.png
Binary files differ
diff --git a/res/drawable-mdpi/scrubber_track_holo_dark.9.png b/res/drawable-mdpi/scrubber_track_holo_dark.9.png
new file mode 100644
index 0000000..b91a4ee
--- /dev/null
+++ b/res/drawable-mdpi/scrubber_track_holo_dark.9.png
Binary files differ
diff --git a/res/drawable-mdpi/tab_selected_holo.9.png b/res/drawable-mdpi/tab_selected_holo.9.png
deleted file mode 100644
index ec093fe..0000000
--- a/res/drawable-mdpi/tab_selected_holo.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/tab_selected_pressed_focused_holo.9.png b/res/drawable-mdpi/tab_selected_pressed_focused_holo.9.png
deleted file mode 100644
index 4dda20b..0000000
--- a/res/drawable-mdpi/tab_selected_pressed_focused_holo.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/tab_selected_pressed_holo.9.png b/res/drawable-mdpi/tab_selected_pressed_holo.9.png
deleted file mode 100644
index 2b338ba..0000000
--- a/res/drawable-mdpi/tab_selected_pressed_holo.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/tab_unselected_focused_holo.9.png b/res/drawable-mdpi/tab_unselected_focused_holo.9.png
deleted file mode 100644
index d25a427..0000000
--- a/res/drawable-mdpi/tab_unselected_focused_holo.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/tab_unselected_holo.9.png b/res/drawable-mdpi/tab_unselected_holo.9.png
deleted file mode 100644
index 90a94d7..0000000
--- a/res/drawable-mdpi/tab_unselected_holo.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/tab_unselected_pressed_holo.9.png b/res/drawable-mdpi/tab_unselected_pressed_holo.9.png
deleted file mode 100644
index 8f666ba..0000000
--- a/res/drawable-mdpi/tab_unselected_pressed_holo.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/title_bar_shadow.9.png b/res/drawable-mdpi/title_bar_shadow.9.png
deleted file mode 100644
index 3b7cf57..0000000
--- a/res/drawable-mdpi/title_bar_shadow.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/view_pager_background_texture.png b/res/drawable-mdpi/view_pager_background_texture.png
new file mode 100644
index 0000000..60bbbba
--- /dev/null
+++ b/res/drawable-mdpi/view_pager_background_texture.png
Binary files differ
diff --git a/res/drawable-nodpi/app_widget_large.png b/res/drawable-nodpi/app_widget_large.png
new file mode 100644
index 0000000..db2c003
--- /dev/null
+++ b/res/drawable-nodpi/app_widget_large.png
Binary files differ
diff --git a/res/drawable-nodpi/app_widget_large_alternate.png b/res/drawable-nodpi/app_widget_large_alternate.png
new file mode 100644
index 0000000..ad37031
--- /dev/null
+++ b/res/drawable-nodpi/app_widget_large_alternate.png
Binary files differ
diff --git a/res/drawable-nodpi/app_widget_recents.png b/res/drawable-nodpi/app_widget_recents.png
new file mode 100644
index 0000000..ecf7be1
--- /dev/null
+++ b/res/drawable-nodpi/app_widget_recents.png
Binary files differ
diff --git a/res/drawable-nodpi/app_widget_recents_stack_preview.png b/res/drawable-nodpi/app_widget_recents_stack_preview.png
new file mode 100644
index 0000000..a8f7af7
--- /dev/null
+++ b/res/drawable-nodpi/app_widget_recents_stack_preview.png
Binary files differ
diff --git a/res/drawable-nodpi/app_widget_small.png b/res/drawable-nodpi/app_widget_small.png
new file mode 100644
index 0000000..ad3cc11
--- /dev/null
+++ b/res/drawable-nodpi/app_widget_small.png
Binary files differ
diff --git a/res/drawable-nodpi/background_holo_dark.png b/res/drawable-nodpi/background_holo_dark.png
new file mode 100644
index 0000000..85bd6f7
--- /dev/null
+++ b/res/drawable-nodpi/background_holo_dark.png
Binary files differ
diff --git a/res/drawable-nodpi/colorstrip_shadow.9.png b/res/drawable-nodpi/colorstrip_shadow.9.png
deleted file mode 100644
index 285f123..0000000
--- a/res/drawable-nodpi/colorstrip_shadow.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-nodpi/default_artwork.png b/res/drawable-nodpi/default_artwork.png
new file mode 100644
index 0000000..269c8de
--- /dev/null
+++ b/res/drawable-nodpi/default_artwork.png
Binary files differ
diff --git a/res/drawable-nodpi/header_temp.png b/res/drawable-nodpi/header_temp.png
new file mode 100644
index 0000000..95f66d9
--- /dev/null
+++ b/res/drawable-nodpi/header_temp.png
Binary files differ
diff --git a/res/drawable-nodpi/promo.png b/res/drawable-nodpi/promo.png
deleted file mode 100644
index 651f5eb..0000000
--- a/res/drawable-nodpi/promo.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-nodpi/theme_preview.png b/res/drawable-nodpi/theme_preview.png
new file mode 100644
index 0000000..d94bb0b
--- /dev/null
+++ b/res/drawable-nodpi/theme_preview.png
Binary files differ
diff --git a/res/drawable-v14/pager_background.xml b/res/drawable-v14/pager_background.xml
new file mode 100644
index 0000000..ac03fea
--- /dev/null
+++ b/res/drawable-v14/pager_background.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:src="@drawable/view_pager_background_texture"
+ android:tileMode="repeat" />
diff --git a/res/drawable-v14/tpi_background.xml b/res/drawable-v14/tpi_background.xml
new file mode 100644
index 0000000..d96958b
--- /dev/null
+++ b/res/drawable-v14/tpi_background.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:src="@drawable/bg_stripes_dark"
+ android:tileMode="repeat" />
diff --git a/res/drawable-xhdpi-v11/appwidget_bg.9.png b/res/drawable-xhdpi-v11/appwidget_bg.9.png
new file mode 100644
index 0000000..900859c
--- /dev/null
+++ b/res/drawable-xhdpi-v11/appwidget_bg.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/apollo_holo_dark_overflow.png b/res/drawable-xhdpi/apollo_holo_dark_overflow.png
deleted file mode 100644
index 3a48be2..0000000
--- a/res/drawable-xhdpi/apollo_holo_dark_overflow.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/apollo_holo_light_favorite_normal.png b/res/drawable-xhdpi/apollo_holo_light_favorite_normal.png
deleted file mode 100644
index e6acafd..0000000
--- a/res/drawable-xhdpi/apollo_holo_light_favorite_normal.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/apollo_holo_light_favorite_selected.png b/res/drawable-xhdpi/apollo_holo_light_favorite_selected.png
deleted file mode 100644
index 767bf0d..0000000
--- a/res/drawable-xhdpi/apollo_holo_light_favorite_selected.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/apollo_holo_light_next.png b/res/drawable-xhdpi/apollo_holo_light_next.png
deleted file mode 100644
index 61b8d59..0000000
--- a/res/drawable-xhdpi/apollo_holo_light_next.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/apollo_holo_light_overflow.png b/res/drawable-xhdpi/apollo_holo_light_overflow.png
deleted file mode 100644
index 9a62ae0..0000000
--- a/res/drawable-xhdpi/apollo_holo_light_overflow.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/apollo_holo_light_pause.png b/res/drawable-xhdpi/apollo_holo_light_pause.png
deleted file mode 100644
index 97d6f91..0000000
--- a/res/drawable-xhdpi/apollo_holo_light_pause.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/apollo_holo_light_play.png b/res/drawable-xhdpi/apollo_holo_light_play.png
deleted file mode 100644
index 2d67d31..0000000
--- a/res/drawable-xhdpi/apollo_holo_light_play.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/apollo_holo_light_previous.png b/res/drawable-xhdpi/apollo_holo_light_previous.png
deleted file mode 100644
index 5ba8441..0000000
--- a/res/drawable-xhdpi/apollo_holo_light_previous.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/apollo_holo_light_repeat_all.png b/res/drawable-xhdpi/apollo_holo_light_repeat_all.png
deleted file mode 100644
index 1cc1063..0000000
--- a/res/drawable-xhdpi/apollo_holo_light_repeat_all.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/apollo_holo_light_repeat_normal.png b/res/drawable-xhdpi/apollo_holo_light_repeat_normal.png
deleted file mode 100644
index 468415a..0000000
--- a/res/drawable-xhdpi/apollo_holo_light_repeat_normal.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/apollo_holo_light_repeat_one.png b/res/drawable-xhdpi/apollo_holo_light_repeat_one.png
deleted file mode 100644
index d9d4c20..0000000
--- a/res/drawable-xhdpi/apollo_holo_light_repeat_one.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/apollo_holo_light_search.png b/res/drawable-xhdpi/apollo_holo_light_search.png
deleted file mode 100644
index 804420a..0000000
--- a/res/drawable-xhdpi/apollo_holo_light_search.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/apollo_holo_light_shuffle_normal.png b/res/drawable-xhdpi/apollo_holo_light_shuffle_normal.png
deleted file mode 100644
index eee9d97..0000000
--- a/res/drawable-xhdpi/apollo_holo_light_shuffle_normal.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/apollo_holo_light_shuffle_on.png b/res/drawable-xhdpi/apollo_holo_light_shuffle_on.png
deleted file mode 100644
index 6b7468d..0000000
--- a/res/drawable-xhdpi/apollo_holo_light_shuffle_on.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/apollo_settings_themes.png b/res/drawable-xhdpi/apollo_settings_themes.png
deleted file mode 100644
index 57dd2a5..0000000
--- a/res/drawable-xhdpi/apollo_settings_themes.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/appwidget_bg.9.png b/res/drawable-xhdpi/appwidget_bg.9.png
index 909f498..4523c65 100644
--- a/res/drawable-xhdpi/appwidget_bg.9.png
+++ b/res/drawable-xhdpi/appwidget_bg.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/bg_stripes_dark.png b/res/drawable-xhdpi/bg_stripes_dark.png
new file mode 100644
index 0000000..76aab7c
--- /dev/null
+++ b/res/drawable-xhdpi/bg_stripes_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/apollo_holo_dark_notifiation_bar_collapse.png b/res/drawable-xhdpi/btn_notification_collapse.png
index 5dfcf1c..5dfcf1c 100644
--- a/res/drawable-xhdpi/apollo_holo_dark_notifiation_bar_collapse.png
+++ b/res/drawable-xhdpi/btn_notification_collapse.png
Binary files differ
diff --git a/res/drawable-xhdpi/apollo_holo_dark_next.png b/res/drawable-xhdpi/btn_playback_next.png
index fe6b558..fe6b558 100644
--- a/res/drawable-xhdpi/apollo_holo_dark_next.png
+++ b/res/drawable-xhdpi/btn_playback_next.png
Binary files differ
diff --git a/res/drawable-xhdpi/apollo_holo_dark_pause.png b/res/drawable-xhdpi/btn_playback_pause.png
index 333c1b2..333c1b2 100644
--- a/res/drawable-xhdpi/apollo_holo_dark_pause.png
+++ b/res/drawable-xhdpi/btn_playback_pause.png
Binary files differ
diff --git a/res/drawable-xhdpi/apollo_holo_dark_play.png b/res/drawable-xhdpi/btn_playback_play.png
index 5112499..5112499 100644
--- a/res/drawable-xhdpi/apollo_holo_dark_play.png
+++ b/res/drawable-xhdpi/btn_playback_play.png
Binary files differ
diff --git a/res/drawable-xhdpi/btn_playback_previous.png b/res/drawable-xhdpi/btn_playback_previous.png
new file mode 100644
index 0000000..5be8b46
--- /dev/null
+++ b/res/drawable-xhdpi/btn_playback_previous.png
Binary files differ
diff --git a/res/drawable-xhdpi/btn_playback_repeat.png b/res/drawable-xhdpi/btn_playback_repeat.png
new file mode 100644
index 0000000..b72f10d
--- /dev/null
+++ b/res/drawable-xhdpi/btn_playback_repeat.png
Binary files differ
diff --git a/res/drawable-xhdpi/btn_playback_repeat_all.png b/res/drawable-xhdpi/btn_playback_repeat_all.png
new file mode 100644
index 0000000..963be39
--- /dev/null
+++ b/res/drawable-xhdpi/btn_playback_repeat_all.png
Binary files differ
diff --git a/res/drawable-xhdpi/btn_playback_repeat_one.png b/res/drawable-xhdpi/btn_playback_repeat_one.png
new file mode 100644
index 0000000..7c1b347
--- /dev/null
+++ b/res/drawable-xhdpi/btn_playback_repeat_one.png
Binary files differ
diff --git a/res/drawable-xhdpi/btn_playback_shuffle.png b/res/drawable-xhdpi/btn_playback_shuffle.png
new file mode 100644
index 0000000..b5cae4f
--- /dev/null
+++ b/res/drawable-xhdpi/btn_playback_shuffle.png
Binary files differ
diff --git a/res/drawable-xhdpi/btn_playback_shuffle_all.png b/res/drawable-xhdpi/btn_playback_shuffle_all.png
new file mode 100644
index 0000000..ad17616
--- /dev/null
+++ b/res/drawable-xhdpi/btn_playback_shuffle_all.png
Binary files differ
diff --git a/res/drawable-xhdpi/btn_switch_queue.png b/res/drawable-xhdpi/btn_switch_queue.png
new file mode 100644
index 0000000..37b292d
--- /dev/null
+++ b/res/drawable-xhdpi/btn_switch_queue.png
Binary files differ
diff --git a/res/drawable-xhdpi/dropdown_ic_arrow_normal_holo_light.png b/res/drawable-xhdpi/dropdown_ic_arrow_normal_holo_light.png
deleted file mode 100644
index 36d8cf4..0000000
--- a/res/drawable-xhdpi/dropdown_ic_arrow_normal_holo_light.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/apollo_holo_light_overflow.png b/res/drawable-xhdpi/ic_action_favorite.png
index 493e1f1..a4acf1b 100644
--- a/res/drawable-mdpi/apollo_holo_light_overflow.png
+++ b/res/drawable-xhdpi/ic_action_favorite.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_action_pinn_to_home.png b/res/drawable-xhdpi/ic_action_pinn_to_home.png
new file mode 100644
index 0000000..6b197c3
--- /dev/null
+++ b/res/drawable-xhdpi/ic_action_pinn_to_home.png
Binary files differ
diff --git a/res/drawable-mdpi/apollo_holo_dark_overflow.png b/res/drawable-xhdpi/ic_action_search.png
index c37420e..d699c6b 100644
--- a/res/drawable-mdpi/apollo_holo_dark_overflow.png
+++ b/res/drawable-xhdpi/ic_action_search.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_action_shop.png b/res/drawable-xhdpi/ic_action_shop.png
new file mode 100644
index 0000000..8df476a
--- /dev/null
+++ b/res/drawable-xhdpi/ic_action_shop.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_launcher.png b/res/drawable-xhdpi/ic_launcher.png
index d22f8f3..f593286 100644
--- a/res/drawable-xhdpi/ic_launcher.png
+++ b/res/drawable-xhdpi/ic_launcher.png
Binary files differ
diff --git a/res/drawable-xhdpi/indicator_playing_peak_meter_1.png b/res/drawable-xhdpi/indicator_playing_peak_meter_1.png
deleted file mode 100644
index b5c524e..0000000
--- a/res/drawable-xhdpi/indicator_playing_peak_meter_1.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/indicator_playing_peak_meter_2.png b/res/drawable-xhdpi/indicator_playing_peak_meter_2.png
deleted file mode 100644
index 6f48de3..0000000
--- a/res/drawable-xhdpi/indicator_playing_peak_meter_2.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/indicator_playing_peak_meter_3.png b/res/drawable-xhdpi/indicator_playing_peak_meter_3.png
deleted file mode 100644
index 485f52c..0000000
--- a/res/drawable-xhdpi/indicator_playing_peak_meter_3.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/indicator_playing_peak_meter_4.png b/res/drawable-xhdpi/indicator_playing_peak_meter_4.png
deleted file mode 100644
index a148d0e..0000000
--- a/res/drawable-xhdpi/indicator_playing_peak_meter_4.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/indicator_playing_peak_meter_5.png b/res/drawable-xhdpi/indicator_playing_peak_meter_5.png
deleted file mode 100644
index e85552c..0000000
--- a/res/drawable-xhdpi/indicator_playing_peak_meter_5.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/list_section_divider_holo_custom.9.png b/res/drawable-xhdpi/list_section_divider_holo_custom.9.png
deleted file mode 100644
index 0bd8a0f..0000000
--- a/res/drawable-xhdpi/list_section_divider_holo_custom.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/notify_panel_notification_icon_bg.png b/res/drawable-xhdpi/notify_panel_notification_icon_bg.png
deleted file mode 100644
index adbe4d2..0000000
--- a/res/drawable-xhdpi/notify_panel_notification_icon_bg.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/playlist_tile_normal.9.png b/res/drawable-xhdpi/playlist_tile_normal.9.png
new file mode 100644
index 0000000..296399c
--- /dev/null
+++ b/res/drawable-xhdpi/playlist_tile_normal.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/queue_thumbnail_bg.9.png b/res/drawable-xhdpi/queue_thumbnail_bg.9.png
deleted file mode 100644
index 80fc849..0000000
--- a/res/drawable-xhdpi/queue_thumbnail_bg.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/recents_thumbnail_bg_press.9.png b/res/drawable-xhdpi/recents_thumbnail_bg_press.9.png
deleted file mode 100644
index 5bae56d..0000000
--- a/res/drawable-xhdpi/recents_thumbnail_bg_press.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/scrubber_primary_holo.9.png b/res/drawable-xhdpi/scrubber_primary_holo.9.png
new file mode 100644
index 0000000..0fc5305
--- /dev/null
+++ b/res/drawable-xhdpi/scrubber_primary_holo.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/scrubber_secondary_holo.9.png b/res/drawable-xhdpi/scrubber_secondary_holo.9.png
new file mode 100644
index 0000000..1c356da
--- /dev/null
+++ b/res/drawable-xhdpi/scrubber_secondary_holo.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/scrubber_track_holo_dark.9.png b/res/drawable-xhdpi/scrubber_track_holo_dark.9.png
new file mode 100644
index 0000000..bfb2048
--- /dev/null
+++ b/res/drawable-xhdpi/scrubber_track_holo_dark.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/tab_selected_holo.9.png b/res/drawable-xhdpi/tab_selected_holo.9.png
deleted file mode 100644
index 1d66449..0000000
--- a/res/drawable-xhdpi/tab_selected_holo.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/tab_selected_pressed_focused_holo.9.png b/res/drawable-xhdpi/tab_selected_pressed_focused_holo.9.png
deleted file mode 100644
index e9f327f..0000000
--- a/res/drawable-xhdpi/tab_selected_pressed_focused_holo.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/tab_selected_pressed_holo.9.png b/res/drawable-xhdpi/tab_selected_pressed_holo.9.png
deleted file mode 100644
index 79a1e0a..0000000
--- a/res/drawable-xhdpi/tab_selected_pressed_holo.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/tab_unselected_focused_holo.9.png b/res/drawable-xhdpi/tab_unselected_focused_holo.9.png
deleted file mode 100644
index 823638f..0000000
--- a/res/drawable-xhdpi/tab_unselected_focused_holo.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/tab_unselected_holo.9.png b/res/drawable-xhdpi/tab_unselected_holo.9.png
deleted file mode 100644
index 244f04b..0000000
--- a/res/drawable-xhdpi/tab_unselected_holo.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/tab_unselected_pressed_holo.9.png b/res/drawable-xhdpi/tab_unselected_pressed_holo.9.png
deleted file mode 100644
index a75e182..0000000
--- a/res/drawable-xhdpi/tab_unselected_pressed_holo.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/title_bar_shadow.9.png b/res/drawable-xhdpi/title_bar_shadow.9.png
deleted file mode 100644
index 45b5456..0000000
--- a/res/drawable-xhdpi/title_bar_shadow.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/view_pager_background_texture.png b/res/drawable-xhdpi/view_pager_background_texture.png
new file mode 100644
index 0000000..8b85814
--- /dev/null
+++ b/res/drawable-xhdpi/view_pager_background_texture.png
Binary files differ
diff --git a/res/drawable/action_bar.xml b/res/drawable/action_bar.xml
new file mode 100644
index 0000000..eb2c17e
--- /dev/null
+++ b/res/drawable/action_bar.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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:drawable="@color/action_bar"></item>
+
+</selector> \ No newline at end of file
diff --git a/res/drawable/audio_player_pager_container.xml b/res/drawable/audio_player_pager_container.xml
new file mode 100644
index 0000000..eb2c17e
--- /dev/null
+++ b/res/drawable/audio_player_pager_container.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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:drawable="@color/action_bar"></item>
+
+</selector> \ No newline at end of file
diff --git a/res/drawable/audio_player_seekbar.xml b/res/drawable/audio_player_seekbar.xml
new file mode 100644
index 0000000..db3416a
--- /dev/null
+++ b/res/drawable/audio_player_seekbar.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item
+ android:id="@android:id/background"
+ android:drawable="@drawable/scrubber_track_holo_dark"/>
+ <item android:id="@android:id/secondaryProgress">
+ <scale
+ android:drawable="@drawable/scrubber_secondary_holo"
+ android:scaleWidth="100%" />
+ </item>
+ <item android:id="@android:id/progress">
+ <scale
+ android:drawable="@drawable/scrubber_primary_holo"
+ android:scaleWidth="100%" />
+ </item>
+
+</layer-list> \ No newline at end of file
diff --git a/res/drawable/bottom_action_bar.xml b/res/drawable/bottom_action_bar.xml
new file mode 100644
index 0000000..2fceb5c
--- /dev/null
+++ b/res/drawable/bottom_action_bar.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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:drawable="@color/bottom_action_bar"></item>
+
+</selector> \ No newline at end of file
diff --git a/res/drawable/bottom_shadow.xml b/res/drawable/bottom_shadow.xml
new file mode 100644
index 0000000..f6cb3f5
--- /dev/null
+++ b/res/drawable/bottom_shadow.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle" >
+
+ <gradient
+ android:angle="90.0"
+ android:endColor="@color/transparent"
+ android:startColor="@color/black" />
+
+</shape> \ No newline at end of file
diff --git a/res/drawable/holo_selector.xml b/res/drawable/holo_selector.xml
deleted file mode 100644
index 46330f1..0000000
--- a/res/drawable/holo_selector.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<selector xmlns:android="http://schemas.android.com/apk/res/android" android:exitFadeDuration="@android:integer/config_mediumAnimTime">
-
- <item android:drawable="@color/holo_blue_dark" android:state_pressed="true"/>
- <item android:drawable="@color/holo_blue_dark" android:state_enabled="true" android:state_focused="true"/>
- <item android:drawable="@color/transparent"/>
-
-</selector> \ No newline at end of file
diff --git a/res/drawable/pager_background.xml b/res/drawable/pager_background.xml
new file mode 100644
index 0000000..559b986
--- /dev/null
+++ b/res/drawable/pager_background.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:src="@drawable/background_holo_dark" />
diff --git a/res/drawable/queue_thumbnail_fg.xml b/res/drawable/queue_thumbnail_fg.xml
deleted file mode 100644
index d1201c9..0000000
--- a/res/drawable/queue_thumbnail_fg.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright (C) 2011 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:drawable="@drawable/recents_thumbnail_bg_press" android:state_selected="true"/>
- <item android:drawable="@drawable/recents_thumbnail_bg_press" android:state_pressed="true"/>
- <item android:drawable="@color/transparent"/>
-
-</selector> \ No newline at end of file
diff --git a/res/drawable/right_shadow.xml b/res/drawable/right_shadow.xml
new file mode 100644
index 0000000..e22eee1
--- /dev/null
+++ b/res/drawable/right_shadow.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle" >
+
+ <gradient
+ android:angle="180"
+ android:endColor="@color/transparent"
+ android:startColor="@color/black" />
+
+</shape> \ No newline at end of file
diff --git a/res/drawable/status_bg.xml b/res/drawable/status_bg.xml
deleted file mode 100644
index 99e5ea6..0000000
--- a/res/drawable/status_bg.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
- android:src="@drawable/notify_panel_notification_icon_bg"
- android:tileMode="repeat" />
diff --git a/res/drawable/tab.xml b/res/drawable/tab.xml
deleted file mode 100644
index a6e1b55..0000000
--- a/res/drawable/tab.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-
- <item android:drawable="@drawable/tab_unselected_holo" android:state_focused="false" android:state_pressed="false" android:state_selected="false"/>
- <item android:drawable="@drawable/tab_selected_holo" android:state_focused="false" android:state_pressed="false" android:state_selected="true"/>
- <item android:drawable="@drawable/tab_unselected_focused_holo" android:state_focused="true" android:state_pressed="false" android:state_selected="false"/>
- <item android:drawable="@drawable/tab_unselected_pressed_holo" android:state_focused="false" android:state_pressed="true" android:state_selected="false"/>
- <item android:drawable="@drawable/tab_selected_pressed_holo" android:state_focused="false" android:state_pressed="true" android:state_selected="true"/>
- <item android:drawable="@drawable/tab_selected_pressed_focused_holo" android:state_focused="true" android:state_pressed="true" android:state_selected="true"/>
-
-</selector> \ No newline at end of file
diff --git a/res/drawable/top_shadow.xml b/res/drawable/top_shadow.xml
new file mode 100644
index 0000000..59e060b
--- /dev/null
+++ b/res/drawable/top_shadow.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle" >
+
+ <gradient
+ android:angle="270"
+ android:endColor="@color/transparent"
+ android:startColor="@color/black" />
+
+</shape> \ No newline at end of file
diff --git a/res/drawable/tpi_background.xml b/res/drawable/tpi_background.xml
new file mode 100644
index 0000000..cf8d123
--- /dev/null
+++ b/res/drawable/tpi_background.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<color xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="@color/action_bar_color" />
diff --git a/res/drawable/viewpager_margin.xml b/res/drawable/viewpager_margin.xml
deleted file mode 100644
index 850607c..0000000
--- a/res/drawable/viewpager_margin.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
- android:shape="rectangle" >
-
- <solid android:color="@color/transparent" />
-
- <stroke
- android:width="@dimen/viewpager_margin_stroke_width"
- android:color="@color/transparent_black" />
-
-</shape> \ No newline at end of file
diff --git a/res/layout-land/activity_player_base.xml b/res/layout-land/activity_player_base.xml
new file mode 100644
index 0000000..ac0e480
--- /dev/null
+++ b/res/layout-land/activity_player_base.xml
@@ -0,0 +1,189 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<com.andrew.apollo.widgets.theme.ThemeableFrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res/com.andrew.apollo"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ <RelativeLayout
+ android:id="@+id/audio_player_large_album_frame"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_alignParentLeft="true" >
+
+ <com.andrew.apollo.widgets.SquareImageView
+ android:id="@+id/audio_player_album_art"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignParentLeft="true"
+ android:scaleType="fitXY" />
+
+ <ImageView
+ android:layout_width="@dimen/shadow_height"
+ android:layout_height="match_parent"
+ android:layout_alignRight="@+id/audio_player_album_art"
+ android:contentDescription="@null"
+ android:src="@drawable/right_shadow" />
+ </RelativeLayout>
+
+ <FrameLayout
+ android:id="@+id/audio_player_pager_container"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_alignParentLeft="true"
+ android:layout_alignRight="@+id/audio_player_large_album_frame"
+ android:visibility="invisible" >
+
+ <android.support.v4.view.ViewPager
+ android:id="@+id/audio_player_pager"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+ <ImageView
+ android:layout_width="@dimen/shadow_height"
+ android:layout_height="match_parent"
+ android:layout_gravity="right"
+ android:contentDescription="@null"
+ android:src="@drawable/right_shadow" />
+ </FrameLayout>
+
+ <View
+ android:id="@+id/audio_player_footer"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_alignParentRight="true"
+ android:layout_toRightOf="@+id/audio_player_large_album_frame" />
+
+ <LinearLayout
+ android:id="@+id/audio_player_header"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/audio_player_header_height"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentTop="true"
+ android:layout_toRightOf="@+id/audio_player_large_album_frame"
+ android:baselineAligned="false"
+ android:orientation="horizontal"
+ android:padding="0dp" >
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:layout_weight="1"
+ android:baselineAligned="false"
+ android:orientation="vertical"
+ android:paddingLeft="@dimen/audio_player_header_padding_left"
+ android:paddingRight="@dimen/audio_player_header_padding_right" >
+
+ <com.andrew.apollo.widgets.theme.ThemeableTextView
+ android:id="@+id/audio_player_track_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:gravity="bottom"
+ android:singleLine="true"
+ android:textSize="@dimen/text_size_medium"
+ android:textStyle="bold"
+ app:themeResource="audio_player_line_one" />
+
+ <com.andrew.apollo.widgets.theme.ThemeableTextView
+ android:id="@+id/audio_player_artist_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:gravity="top"
+ android:singleLine="true"
+ android:textSize="@dimen/text_size_medium"
+ app:themeResource="audio_player_line_two" />
+ </LinearLayout>
+
+ <FrameLayout
+ android:id="@+id/audio_player_switch"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:focusable="true"
+ android:padding="@dimen/audio_player_switch_padding" >
+
+ <com.andrew.apollo.widgets.SquareImageView
+ android:id="@+id/audio_player_switch_queue"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:contentDescription="@null" />
+
+ <com.andrew.apollo.widgets.SquareImageView
+ android:id="@+id/audio_player_switch_album_art"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:visibility="invisible" />
+ </FrameLayout>
+ </LinearLayout>
+
+ <com.andrew.apollo.widgets.theme.ThemeableTextView
+ android:id="@+id/audio_player_current_time"
+ android:layout_width="@dimen/audio_player_time_width"
+ android:layout_height="wrap_content"
+ android:layout_alignBottom="@android:id/progress"
+ android:layout_alignTop="@android:id/progress"
+ android:layout_toRightOf="@+id/audio_player_large_album_frame"
+ android:gravity="center"
+ android:textSize="@dimen/text_size_micro"
+ app:themeResource="audio_player_current_time" />
+
+ <com.andrew.apollo.widgets.theme.ThemeableTextView
+ android:id="@+id/audio_player_total_time"
+ android:layout_width="@dimen/audio_player_time_width"
+ android:layout_height="wrap_content"
+ android:layout_alignBottom="@android:id/progress"
+ android:layout_alignParentRight="true"
+ android:layout_alignTop="@android:id/progress"
+ android:gravity="center"
+ android:textSize="@dimen/text_size_micro"
+ app:themeResource="audio_player_total_time" />
+
+ <com.andrew.apollo.widgets.theme.ThemeableSeekBar
+ android:id="@android:id/progress"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_above="@+id/audio_player_controlss"
+ android:layout_marginBottom="@dimen/audio_player_seek_bar_margin_bottom"
+ android:layout_toLeftOf="@+id/audio_player_total_time"
+ android:layout_toRightOf="@+id/audio_player_current_time"
+ android:background="@null"
+ android:max="1000"
+ android:thumb="@null" />
+
+ <LinearLayout
+ android:id="@+id/audio_player_controlss"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentRight="true"
+ android:layout_toRightOf="@+id/audio_player_large_album_frame"
+ android:baselineAligned="false" >
+
+ <include layout="@layout/audio_player_controls" />
+ </LinearLayout>
+ </RelativeLayout>
+
+ <include layout="@layout/colorstrip" />
+
+</com.andrew.apollo.widgets.theme.ThemeableFrameLayout> \ No newline at end of file
diff --git a/res/layout-v11/app_widget_recents.xml b/res/layout-v11/app_widget_recents.xml
new file mode 100644
index 0000000..54ce198
--- /dev/null
+++ b/res/layout-v11/app_widget_recents.xml
@@ -0,0 +1,106 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_margin="0dp"
+ android:background="@color/action_bar_color" >
+
+ <RelativeLayout
+ android:id="@+id/app_widget_recents_action_bar"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/app_widget_recents_action_bar_height" >
+
+ <ImageView
+ android:id="@+id/app_widget_recents_icon"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_centerVertical="true"
+ android:background="?android:selectableItemBackground"
+ android:contentDescription="@string/app_name"
+ android:padding="@dimen/app_widget_recents_action_bar_item_padding"
+ android:scaleType="centerInside"
+ android:src="@drawable/ic_launcher" />
+
+ <TextView
+ android:id="@+id/app_widget_recents_app_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:layout_toRightOf="@+id/app_widget_recents_icon"
+ android:text="@string/page_recent"
+ android:textColor="@color/white"
+ android:textSize="@dimen/text_size_medium" />
+
+ <ImageButton
+ android:id="@+id/app_widget_recents_previous"
+ android:layout_width="@dimen/app_widget_recents_action_bar_height"
+ android:layout_height="match_parent"
+ android:layout_centerVertical="true"
+ android:layout_toLeftOf="@+id/app_widget_recents_play"
+ android:background="?android:selectableItemBackground"
+ android:contentDescription="@null"
+ android:scaleType="center"
+ android:src="@drawable/btn_playback_previous" />
+
+ <ImageButton
+ android:id="@+id/app_widget_recents_play"
+ android:layout_width="@dimen/app_widget_recents_action_bar_height"
+ android:layout_height="match_parent"
+ android:layout_centerVertical="true"
+ android:layout_toLeftOf="@+id/app_widget_recents_next"
+ android:background="?android:selectableItemBackground"
+ android:contentDescription="@null"
+ android:scaleType="center"
+ android:src="@drawable/btn_playback_play" />
+
+ <ImageButton
+ android:id="@+id/app_widget_recents_next"
+ android:layout_width="@dimen/app_widget_recents_action_bar_height"
+ android:layout_height="match_parent"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:background="?android:selectableItemBackground"
+ android:contentDescription="@null"
+ android:scaleType="center"
+ android:src="@drawable/btn_playback_next" />
+ </RelativeLayout>
+
+ <ImageView
+ android:id="@+id/colorstrip"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/colorstrip_height"
+ android:layout_below="@+id/app_widget_recents_action_bar"
+ android:background="@color/holo_blue_light"
+ android:contentDescription="@null" />
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_below="@+id/colorstrip"
+ android:layout_centerVertical="true"
+ android:background="@drawable/appwidget_bg" >
+
+ <ListView
+ android:id="@+id/app_widget_recents_list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@null"
+ android:cacheColorHint="@color/transparent" />
+ </FrameLayout>
+
+</RelativeLayout> \ No newline at end of file
diff --git a/res/layout-v11/app_widget_recents_items.xml b/res/layout-v11/app_widget_recents_items.xml
new file mode 100644
index 0000000..1568177
--- /dev/null
+++ b/res/layout-v11/app_widget_recents_items.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/app_widget_recents_items"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ tools:ignore="ContentDescription" >
+
+ <ImageView
+ android:id="@+id/app_widget_recents_base_image"
+ android:layout_width="@dimen/item_normal_height"
+ android:layout_height="@dimen/item_normal_height"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ android:scaleType="fitXY" />
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/item_normal_height"
+ android:layout_gravity="center_vertical"
+ android:layout_toRightOf="@+id/app_widget_recents_base_image"
+ android:gravity="center_vertical"
+ android:paddingLeft="@dimen/list_preferred_item_padding" >
+
+ <TextView
+ android:id="@+id/app_widget_recents_line_one"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ android:singleLine="true"
+ android:textSize="@dimen/text_size_medium"
+ android:textStyle="bold" />
+
+ <TextView
+ android:id="@+id/app_widget_recents_line_two"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/app_widget_recents_line_one"
+ android:layout_marginTop="@dimen/list_item_line_two_margin_top"
+ android:singleLine="true"
+ android:textSize="@dimen/text_size_small" />
+ </RelativeLayout>
+
+</RelativeLayout> \ No newline at end of file
diff --git a/res/layout-v11/notification_template_base.xml b/res/layout-v11/notification_template_base.xml
new file mode 100644
index 0000000..b4d9e1c
--- /dev/null
+++ b/res/layout-v11/notification_template_base.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/notification_base"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:orientation="horizontal"
+ tools:ignore="ContentDescription" >
+
+ <ImageView
+ android:id="@+id/notification_base_image"
+ android:layout_width="@dimen/notification_big_icon_width"
+ android:layout_height="@dimen/notification_big_icon_height"
+ android:background="@drawable/default_artwork"
+ android:gravity="center" />
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:paddingLeft="@dimen/notification_info_container_padding_left"
+ android:paddingBottom="@dimen/notification_info_container_padding_bottom" >
+
+ <TextView
+ android:id="@+id/notification_base_line_one"
+ style="@style/NotificationText"
+ android:textAppearance="@android:style/TextAppearance.StatusBar.EventContent.Title" />
+
+ <TextView
+ android:id="@+id/notification_base_line_two"
+ style="@style/NotificationText"
+ android:textAppearance="@android:style/TextAppearance.StatusBar.EventContent" />
+
+ </LinearLayout>
+
+ <ImageButton
+ android:id="@+id/notification_base_previous"
+ style="@style/NotificationAction.Previous" />
+
+ <ImageButton
+ android:id="@+id/notification_base_play"
+ style="@style/NotificationAction.Play" />
+
+ <ImageButton
+ android:id="@+id/notification_base_next"
+ style="@style/NotificationAction.Next" />
+
+ <ImageButton
+ android:id="@+id/notification_base_collapse"
+ style="@style/NotificationAction.Collapse" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/res/layout-v16/notification_template_expanded_base.xml b/res/layout-v16/notification_template_expanded_base.xml
new file mode 100644
index 0000000..a8dd2f3
--- /dev/null
+++ b/res/layout-v16/notification_template_expanded_base.xml
@@ -0,0 +1,123 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="128.0dip"
+ tools:ignore="ContentDescription" >
+
+ <!-- The height cannot be specified any other way. It must read "128.0dip" and cannot be referenced. I think it's a bug. -->
+
+ <ImageView
+ android:id="@+id/notification_expanded_base_image"
+ android:layout_width="@dimen/notification_expanded_height"
+ android:layout_height="@dimen/notification_expanded_height"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentLeft="true"
+ android:background="@drawable/default_artwork"
+ android:scaleType="fitXY" />
+
+ <LinearLayout
+ android:id="@+id/notification_expanded_buttons"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentRight="true"
+ android:layout_toRightOf="@+id/notification_expanded_base_image"
+ android:divider="?android:listDivider"
+ android:dividerPadding="@dimen/notification_expanded_buttons_divider_padding"
+ android:gravity="center_vertical"
+ android:orientation="horizontal"
+ android:showDividers="middle" >
+
+ <ImageButton
+ android:id="@+id/notification_expanded_base_previous"
+ android:layout_width="0dp"
+ android:layout_height="@dimen/notification_expanded_button_height"
+ android:layout_weight="1"
+ android:background="?android:selectableItemBackground"
+ android:padding="@dimen/notification_expanded_button_padding"
+ android:scaleType="fitCenter"
+ android:src="@drawable/btn_playback_previous" />
+
+ <ImageButton
+ android:id="@+id/notification_expanded_base_play"
+ android:layout_width="0dp"
+ android:layout_height="@dimen/notification_expanded_button_height"
+ android:layout_weight="1"
+ android:background="?android:selectableItemBackground"
+ android:padding="@dimen/notification_expanded_button_padding"
+ android:scaleType="fitCenter"
+ android:src="@drawable/btn_playback_pause" />
+
+ <ImageButton
+ android:id="@+id/notification_expanded_base_next"
+ android:layout_width="0dp"
+ android:layout_height="@dimen/notification_expanded_button_height"
+ android:layout_weight="1"
+ android:background="?android:selectableItemBackground"
+ android:padding="@dimen/notification_expanded_button_padding"
+ android:scaleType="fitCenter"
+ android:src="@drawable/btn_playback_next" />
+ </LinearLayout>
+
+ <ImageView
+ android:layout_width="wrap_content"
+ android:layout_height="1dp"
+ android:layout_above="@+id/notification_expanded_buttons"
+ android:layout_alignParentRight="true"
+ android:layout_toRightOf="@+id/notification_expanded_base_image"
+ android:background="?android:dividerHorizontal" />
+
+ <ImageButton
+ android:id="@+id/notification_expanded_base_collapse"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentTop="true"
+ android:background="?android:selectableItemBackground"
+ android:padding="@dimen/notification_expanded_collapse_padding"
+ android:src="@drawable/btn_notification_collapse" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ android:layout_gravity="center_vertical"
+ android:layout_toLeftOf="@+id/notification_expanded_base_collapse"
+ android:layout_toRightOf="@+id/notification_expanded_base_image"
+ android:orientation="vertical"
+ android:paddingLeft="@dimen/notification_info_container_padding_left"
+ android:paddingTop="@dimen/notification_expanded_content_padding_top" >
+
+ <TextView
+ android:id="@+id/notification_expanded_base_line_one"
+ style="@style/NotificationText"
+ android:textAppearance="@android:style/TextAppearance.StatusBar.EventContent.Title" />
+
+ <TextView
+ android:id="@+id/notification_expanded_base_line_two"
+ style="@style/NotificationText"
+ android:textAppearance="@android:style/TextAppearance.StatusBar.EventContent" />
+
+ <TextView
+ android:id="@+id/notification_expanded_base_line_three"
+ style="@style/NotificationText"
+ android:textAppearance="@android:style/TextAppearance.StatusBar.EventContent" />
+ </LinearLayout>
+
+</RelativeLayout> \ No newline at end of file
diff --git a/res/layout/action_bar.xml b/res/layout/action_bar.xml
new file mode 100644
index 0000000..1fad70e
--- /dev/null
+++ b/res/layout/action_bar.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:enabled="false"
+ android:orientation="horizontal"
+ android:paddingEnd="8dip" >
+
+ <ImageView
+ android:id="@+id/up"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical|start"
+ android:contentDescription="@null"
+ android:visibility="gone" />
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical|start"
+ android:orientation="vertical" >
+
+ <TextView
+ android:id="@+id/action_bar_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:singleLine="true"
+ android:textSize="@dimen/text_size_medium" />
+
+ <TextView
+ android:id="@+id/action_bar_subtitle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="-3dp"
+ android:ellipsize="end"
+ android:singleLine="true"
+ android:textSize="@dimen/text_size_small"
+ android:visibility="gone" />
+ </LinearLayout>
+
+</LinearLayout> \ No newline at end of file
diff --git a/res/layout/activity_base.xml b/res/layout/activity_base.xml
new file mode 100644
index 0000000..07316d4
--- /dev/null
+++ b/res/layout/activity_base.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical" >
+
+ <com.andrew.apollo.widgets.theme.ThemeableFrameLayout
+ android:id="@+id/activity_base_content"
+ android:layout_width="match_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1" />
+
+ <include layout="@layout/bottom_action_bar"/>
+
+</LinearLayout> \ No newline at end of file
diff --git a/res/layout/activity_player_base.xml b/res/layout/activity_player_base.xml
new file mode 100644
index 0000000..e98bbb5
--- /dev/null
+++ b/res/layout/activity_player_base.xml
@@ -0,0 +1,206 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<com.andrew.apollo.widgets.theme.ThemeableFrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res/com.andrew.apollo"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ <View
+ android:id="@+id/audio_player_footer"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_alignTop="@android:id/progress" />
+
+ <LinearLayout
+ android:id="@+id/audio_player_header"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/audio_player_header_height"
+ android:layout_alignParentTop="true"
+ android:baselineAligned="false"
+ android:orientation="horizontal" >
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:paddingLeft="@dimen/audio_player_header_padding_left"
+ android:paddingRight="@dimen/audio_player_header_padding_right" >
+
+ <com.andrew.apollo.widgets.theme.ThemeableTextView
+ android:id="@+id/audio_player_track_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:singleLine="true"
+ android:textSize="@dimen/text_size_medium"
+ android:textStyle="bold"
+ app:themeResource="audio_player_line_one" />
+
+ <com.andrew.apollo.widgets.theme.ThemeableTextView
+ android:id="@+id/audio_player_artist_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:singleLine="true"
+ android:textSize="@dimen/text_size_medium"
+ app:themeResource="audio_player_line_two" />
+ </LinearLayout>
+
+ <FrameLayout
+ android:id="@+id/audio_player_switch"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:clickable="true"
+ android:focusable="true"
+ android:padding="@dimen/audio_player_switch_padding" >
+
+ <com.andrew.apollo.widgets.SquareImageView
+ android:id="@+id/audio_player_switch_queue"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:contentDescription="@null" />
+
+ <com.andrew.apollo.widgets.SquareImageView
+ android:id="@+id/audio_player_switch_album_art"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:visibility="invisible" />
+ </FrameLayout>
+ </LinearLayout>
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_above="@android:id/progress"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentRight="true"
+ android:layout_below="@+id/audio_player_header" >
+
+ <com.andrew.apollo.widgets.SquareImageView
+ android:id="@+id/audio_player_album_art"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_centerHorizontal="true"
+ android:scaleType="fitXY" />
+
+ <View
+ android:id="@+id/audio_player_footer_two"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_below="@+id/audio_player_album_art" />
+
+ <ImageView
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/shadow_height"
+ android:layout_alignTop="@+id/audio_player_album_art"
+ android:contentDescription="@null"
+ android:src="@drawable/top_shadow" />
+
+ <ImageView
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/shadow_height"
+ android:layout_alignBottom="@+id/audio_player_album_art"
+ android:contentDescription="@null"
+ android:src="@drawable/bottom_shadow" />
+
+ <FrameLayout
+ android:id="@+id/audio_player_pager_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignBottom="@+id/audio_player_album_art"
+ android:visibility="invisible" >
+
+ <android.support.v4.view.ViewPager
+ android:id="@+id/audio_player_pager"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+ <ImageView
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/shadow_height"
+ android:layout_gravity="top"
+ android:contentDescription="@null"
+ android:src="@drawable/top_shadow" />
+
+ <ImageView
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/shadow_height"
+ android:layout_gravity="bottom"
+ android:contentDescription="@null"
+ android:src="@drawable/bottom_shadow" />
+ </FrameLayout>
+ </RelativeLayout>
+
+ <com.andrew.apollo.widgets.theme.ThemeableTextView
+ android:id="@+id/audio_player_current_time"
+ android:layout_width="@dimen/audio_player_time_width"
+ android:layout_height="wrap_content"
+ android:layout_alignBottom="@android:id/progress"
+ android:layout_alignParentLeft="true"
+ android:layout_alignTop="@android:id/progress"
+ android:gravity="center"
+ android:textSize="@dimen/text_size_micro"
+ app:themeResource="audio_player_current_time" />
+
+ <com.andrew.apollo.widgets.theme.ThemeableTextView
+ android:id="@+id/audio_player_total_time"
+ android:layout_width="@dimen/audio_player_time_width"
+ android:layout_height="wrap_content"
+ android:layout_alignBottom="@android:id/progress"
+ android:layout_alignParentRight="true"
+ android:layout_alignTop="@android:id/progress"
+ android:gravity="center"
+ android:textSize="@dimen/text_size_micro"
+ app:themeResource="audio_player_total_time" />
+
+ <com.andrew.apollo.widgets.theme.ThemeableSeekBar
+ android:id="@android:id/progress"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_above="@+id/audio_player_controlss"
+ android:layout_marginBottom="@dimen/audio_player_seek_bar_margin_bottom"
+ android:layout_toLeftOf="@+id/audio_player_total_time"
+ android:layout_toRightOf="@+id/audio_player_current_time"
+ android:max="1000"
+ android:thumb="@null" />
+
+ <LinearLayout
+ android:id="@+id/audio_player_controlss"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentRight="true"
+ android:baselineAligned="false"
+ android:gravity="center" >
+
+ <include layout="@layout/audio_player_controls" />
+ </LinearLayout>
+ </RelativeLayout>
+
+ <include layout="@layout/colorstrip" />
+
+</com.andrew.apollo.widgets.theme.ThemeableFrameLayout> \ No newline at end of file
diff --git a/res/layout/activity_profile_base.xml b/res/layout/activity_profile_base.xml
new file mode 100644
index 0000000..fa8f654
--- /dev/null
+++ b/res/layout/activity_profile_base.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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. -->
+<com.andrew.apollo.widgets.theme.ThemeableFrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical" >
+
+ <include
+ android:id="@+id/shadow"
+ layout="@layout/top_shadow" />
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1" >
+
+ <android.support.v4.view.ViewPager
+ android:id="@+id/acivity_profile_base_pager"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true" />
+
+ <include
+ android:id="@+id/acivity_profile_base_tab_carousel"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ layout="@layout/profile_tab_carousel" />
+ </RelativeLayout>
+
+ <include layout="@layout/bottom_action_bar" />
+ </LinearLayout>
+
+</com.andrew.apollo.widgets.theme.ThemeableFrameLayout> \ No newline at end of file
diff --git a/res/layout/app_widget_large.xml b/res/layout/app_widget_large.xml
new file mode 100644
index 0000000..7756d5f
--- /dev/null
+++ b/res/layout/app_widget_large.xml
@@ -0,0 +1,118 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/app_widget_large_min_height"
+ android:background="@drawable/appwidget_bg"
+ tools:ignore="ContentDescription" >
+
+ <ImageView
+ android:id="@+id/app_widget_large_image"
+ android:layout_width="@dimen/notification_expanded_height"
+ android:layout_height="@dimen/notification_expanded_height"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentLeft="true"
+ android:background="@drawable/default_artwork"
+ android:scaleType="fitXY" />
+
+ <LinearLayout
+ android:id="@+id/app_widget_large_buttons"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentRight="true"
+ android:layout_toRightOf="@+id/app_widget_large_image"
+ android:divider="?android:listDivider"
+ android:dividerPadding="@dimen/notification_expanded_buttons_divider_padding"
+ android:gravity="center_vertical"
+ android:orientation="horizontal"
+ android:showDividers="middle" >
+
+ <ImageButton
+ android:id="@+id/app_widget_large_previous"
+ android:layout_width="0dp"
+ android:layout_height="@dimen/notification_expanded_button_height"
+ android:layout_weight="1"
+ android:background="?android:selectableItemBackground"
+ android:contentDescription="@string/accessibility_prev"
+ android:padding="@dimen/notification_expanded_button_padding"
+ android:scaleType="fitCenter"
+ android:src="@drawable/btn_playback_previous" />
+
+ <ImageButton
+ android:id="@+id/app_widget_large_play"
+ android:layout_width="0dp"
+ android:layout_height="@dimen/notification_expanded_button_height"
+ android:layout_weight="1"
+ android:background="?android:selectableItemBackground"
+ android:padding="@dimen/notification_expanded_button_padding"
+ android:scaleType="fitCenter"
+ android:src="@drawable/btn_playback_pause" />
+
+ <ImageButton
+ android:id="@+id/app_widget_large_next"
+ android:layout_width="0dp"
+ android:layout_height="@dimen/notification_expanded_button_height"
+ android:layout_weight="1"
+ android:background="?android:selectableItemBackground"
+ android:contentDescription="@string/accessibility_next"
+ android:padding="@dimen/notification_expanded_button_padding"
+ android:scaleType="fitCenter"
+ android:src="@drawable/btn_playback_next" />
+ </LinearLayout>
+
+ <ImageView
+ android:layout_width="wrap_content"
+ android:layout_height="1dp"
+ android:layout_above="@+id/app_widget_large_buttons"
+ android:layout_alignParentRight="true"
+ android:layout_toRightOf="@+id/app_widget_large_image"
+ android:background="?android:dividerHorizontal" />
+
+ <LinearLayout
+ android:id="@+id/app_widget_large_info_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ android:layout_gravity="center_vertical"
+ android:layout_toRightOf="@+id/app_widget_large_image"
+ android:orientation="vertical"
+ android:paddingLeft="@dimen/notification_info_container_padding_left"
+ android:paddingTop="@dimen/notification_expanded_content_padding_top" >
+
+ <TextView
+ android:id="@+id/app_widget_large_line_one"
+ style="@style/NotificationText"
+ android:textColor="@color/white"
+ android:textSize="@dimen/text_size_medium"
+ android:textStyle="bold" />
+
+ <TextView
+ android:id="@+id/app_widget_large_line_two"
+ style="@style/NotificationText"
+ android:textColor="@color/transparent_white"
+ android:textSize="@dimen/text_size_small" />
+
+ <TextView
+ android:id="@+id/app_widget_large_line_three"
+ style="@style/NotificationText"
+ android:textColor="@color/transparent_white"
+ android:textSize="@dimen/text_size_small" />
+ </LinearLayout>
+
+</RelativeLayout> \ No newline at end of file
diff --git a/res/layout/app_widget_large_alternate.xml b/res/layout/app_widget_large_alternate.xml
new file mode 100644
index 0000000..8f777dd
--- /dev/null
+++ b/res/layout/app_widget_large_alternate.xml
@@ -0,0 +1,138 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/app_widget_large_min_height"
+ android:background="@drawable/appwidget_bg"
+ tools:ignore="ContentDescription" >
+
+ <ImageView
+ android:id="@+id/app_widget_large_alternate_image"
+ android:layout_width="@dimen/app_widget_large_alternate_artwork_size"
+ android:layout_height="@dimen/app_widget_large_alternate_artwork_size"
+ android:layout_above="@+id/app_widget_large_alternate_buttons"
+ android:layout_alignParentLeft="true"
+ android:background="@drawable/default_artwork"
+ android:scaleType="fitXY" />
+
+ <LinearLayout
+ android:id="@+id/app_widget_large_alternate_buttons"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:divider="?android:listDivider"
+ android:dividerPadding="@dimen/notification_expanded_buttons_divider_padding"
+ android:gravity="center_vertical"
+ android:orientation="horizontal"
+ android:showDividers="middle" >
+
+ <ImageButton
+ android:id="@+id/app_widget_large_alternate_shuffle"
+ android:layout_width="0dp"
+ android:layout_height="@dimen/notification_expanded_button_height"
+ android:layout_weight="1"
+ android:background="?android:selectableItemBackground"
+ android:contentDescription="@string/accessibility_shuffle"
+ android:padding="@dimen/notification_expanded_button_padding"
+ android:scaleType="fitCenter"
+ android:src="@drawable/btn_playback_shuffle" />
+
+ <ImageButton
+ android:id="@+id/app_widget_large_alternate_previous"
+ android:layout_width="0dp"
+ android:layout_height="@dimen/notification_expanded_button_height"
+ android:layout_weight="1"
+ android:background="?android:selectableItemBackground"
+ android:contentDescription="@string/accessibility_prev"
+ android:padding="@dimen/notification_expanded_button_padding"
+ android:scaleType="fitCenter"
+ android:src="@drawable/btn_playback_previous" />
+
+ <ImageButton
+ android:id="@+id/app_widget_large_alternate_play"
+ android:layout_width="0dp"
+ android:layout_height="@dimen/notification_expanded_button_height"
+ android:layout_weight="1"
+ android:background="?android:selectableItemBackground"
+ android:padding="@dimen/notification_expanded_button_padding"
+ android:scaleType="fitCenter"
+ android:src="@drawable/btn_playback_pause" />
+
+ <ImageButton
+ android:id="@+id/app_widget_large_alternate_next"
+ android:layout_width="0dp"
+ android:layout_height="@dimen/notification_expanded_button_height"
+ android:layout_weight="1"
+ android:background="?android:selectableItemBackground"
+ android:contentDescription="@string/accessibility_next"
+ android:padding="@dimen/notification_expanded_button_padding"
+ android:scaleType="fitCenter"
+ android:src="@drawable/btn_playback_next" />
+
+ <ImageButton
+ android:id="@+id/app_widget_large_alternate_repeat"
+ android:layout_width="0dp"
+ android:layout_height="@dimen/notification_expanded_button_height"
+ android:layout_weight="1"
+ android:background="?android:selectableItemBackground"
+ android:contentDescription="@string/accessibility_repeat"
+ android:padding="@dimen/notification_expanded_button_padding"
+ android:scaleType="fitCenter"
+ android:src="@drawable/btn_playback_repeat" />
+ </LinearLayout>
+
+ <ImageView
+ android:layout_width="wrap_content"
+ android:layout_height="1dp"
+ android:layout_above="@+id/app_widget_large_alternate_buttons"
+ android:layout_alignParentRight="true"
+ android:layout_toRightOf="@+id/app_widget_large_alternate_image"
+ android:background="?android:dividerHorizontal" />
+
+ <LinearLayout
+ android:id="@+id/app_widget_large_alternate_info_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ android:layout_gravity="center_vertical"
+ android:layout_toRightOf="@+id/app_widget_large_alternate_image"
+ android:orientation="vertical"
+ android:paddingLeft="@dimen/notification_info_container_padding_left"
+ android:paddingTop="@dimen/notification_expanded_content_padding_top" >
+
+ <TextView
+ android:id="@+id/app_widget_large_alternate_line_one"
+ style="@style/NotificationText"
+ android:textColor="@color/white"
+ android:textSize="@dimen/text_size_medium"
+ android:textStyle="bold" />
+
+ <TextView
+ android:id="@+id/app_widget_large_alternate_line_two"
+ style="@style/NotificationText"
+ android:textColor="@color/transparent_white"
+ android:textSize="@dimen/text_size_small" />
+
+ <TextView
+ android:id="@+id/app_widget_large_alternate_line_three"
+ style="@style/NotificationText"
+ android:textColor="@color/transparent_white"
+ android:textSize="@dimen/text_size_small" />
+ </LinearLayout>
+
+</RelativeLayout> \ No newline at end of file
diff --git a/res/layout/app_widget_small.xml b/res/layout/app_widget_small.xml
new file mode 100644
index 0000000..62eb356
--- /dev/null
+++ b/res/layout/app_widget_small.xml
@@ -0,0 +1,151 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="top"
+ android:background="@drawable/appwidget_bg"
+ android:gravity="top"
+ android:padding="@dimen/app_widget_padding"
+ tools:ignore="NestedWeights" >
+
+ <LinearLayout
+ android:id="@+id/app_widget_small_buttons"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/app_widget_small_artwork_size"
+ android:baselineAligned="false"
+ android:focusable="true"
+ android:orientation="horizontal" >
+
+ <FrameLayout
+ android:layout_width="@dimen/app_widget_small_artwork_size"
+ android:layout_height="@dimen/app_widget_small_artwork_size"
+ android:focusable="true" >
+
+ <ImageView
+ android:id="@+id/app_widget_small_image"
+ android:layout_width="@dimen/app_widget_small_artwork_size"
+ android:layout_height="@dimen/app_widget_small_artwork_size"
+ android:background="@drawable/default_artwork"
+ android:contentDescription="@null"
+ android:scaleType="centerInside" />
+ </FrameLayout>
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:layout_weight="1"
+ android:focusable="true"
+ android:gravity="center_horizontal"
+ android:orientation="horizontal" >
+
+ <ImageButton
+ android:id="@+id/app_widget_small_previous"
+ android:layout_width="0dp"
+ android:layout_height="@dimen/app_widget_small_button_height"
+ android:layout_weight="1"
+ android:background="@null"
+ android:contentDescription="@string/accessibility_prev"
+ android:focusable="true"
+ android:padding="@dimen/app_widget_small_button_padding"
+ android:scaleType="fitCenter"
+ android:src="@drawable/btn_playback_previous" />
+
+ <ImageButton
+ android:id="@+id/app_widget_small_play"
+ android:layout_width="0dp"
+ android:layout_height="@dimen/app_widget_small_button_height"
+ android:layout_weight="1"
+ android:background="@null"
+ android:contentDescription="@string/accessibility_play"
+ android:focusable="true"
+ android:padding="@dimen/app_widget_small_button_padding"
+ android:scaleType="fitCenter"
+ android:src="@drawable/btn_playback_play" />
+
+ <ImageButton
+ android:id="@+id/app_widget_small_next"
+ android:layout_width="0dp"
+ android:layout_height="@dimen/app_widget_small_button_height"
+ android:layout_weight="1"
+ android:background="@null"
+ android:contentDescription="@string/accessibility_next"
+ android:focusable="true"
+ android:padding="@dimen/app_widget_small_button_padding"
+ android:scaleType="fitCenter"
+ android:src="@drawable/btn_playback_next" />
+ </LinearLayout>
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/app_widget_small_half_separator"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_below="@+id/app_widget_small_buttons"
+ android:background="@color/transparent_white" />
+
+ <RelativeLayout
+ android:id="@+id/app_widget_small_info_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_below="@+id/app_widget_small_half_separator"
+ android:focusable="true"
+ android:gravity="top"
+ android:orientation="horizontal"
+ android:paddingLeft="@dimen/app_widget_small_info_container_padding_left"
+ android:paddingRight="@dimen/app_widget_small_info_container_padding_right"
+ android:paddingTop="@dimen/app_widget_small_info_container_padding_top" >
+
+ <TextView
+ android:id="@+id/app_widget_small_line_one"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="none"
+ android:singleLine="true"
+ android:textColor="@color/white"
+ android:textSize="@dimen/text_size_small"
+ android:textStyle="bold" />
+
+ <TextView
+ android:id="@+id/app_widget_small_text_separator"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_toRightOf="@+id/app_widget_small_line_one"
+ android:ellipsize="none"
+ android:paddingLeft="5dp"
+ android:paddingRight="5dp"
+ android:singleLine="true"
+ android:text="@string/app_widget_text_separator"
+ android:textColor="@color/transparent_white"
+ android:textSize="@dimen/text_size_small"
+ android:textStyle="bold" />
+
+ <TextView
+ android:id="@+id/app_widget_small_line_two"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_toRightOf="@+id/app_widget_small_text_separator"
+ android:ellipsize="marquee"
+ android:singleLine="true"
+ android:textColor="@color/transparent_white"
+ android:textSize="@dimen/text_size_small" />
+ </RelativeLayout>
+
+</RelativeLayout> \ No newline at end of file
diff --git a/res/layout/audio_controls.xml b/res/layout/audio_controls.xml
deleted file mode 100644
index e9602b0..0000000
--- a/res/layout/audio_controls.xml
+++ /dev/null
@@ -1,75 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<merge xmlns:android="http://schemas.android.com/apk/res/android" >
-
- <RelativeLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content" >
-
- <SeekBar
- android:id="@android:id/progress"
- style="?android:attr/progressBarStyleHorizontal"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:paddingTop="@dimen/audio_player_seek_bar_padding"
- android:thumb="@null" />
-
- <TextView
- android:id="@+id/audio_player_current_time"
- style="@style/AudioPlayerText"
- android:layout_alignParentLeft="true"
- android:layout_below="@android:id/progress"
- android:paddingLeft="@dimen/audio_player_info_container_padding"
- android:paddingRight="@dimen/audio_player_info_container_padding"
- android:textColor="@color/transparent_black"
- android:textSize="@dimen/text_size_small"
- android:textStyle="bold" />
-
- <TextView
- android:id="@+id/audio_player_total_time"
- style="@style/AudioPlayerText"
- android:layout_alignParentRight="true"
- android:layout_below="@android:id/progress"
- android:paddingLeft="@dimen/audio_player_info_container_padding"
- android:paddingRight="@dimen/audio_player_info_container_padding"
- android:textColor="@color/transparent_black"
- android:textSize="@dimen/text_size_small"
- android:textStyle="bold" />
- </RelativeLayout>
-
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="@dimen/audio_player_controls_height"
- android:layout_gravity="bottom"
- android:orientation="horizontal"
- android:paddingBottom="@dimen/audio_player_button_container_padding" >
-
- <ImageButton
- android:id="@+id/audio_player_repeat"
- style="@style/AudioPlayerButton"
- android:contentDescription="@string/cd_repeat"
- android:src="@drawable/apollo_holo_light_repeat_normal" />
-
- <com.andrew.apollo.ui.widgets.RepeatingImageButton
- android:id="@+id/audio_player_prev"
- style="@style/AudioPlayerButton"
- android:src="@drawable/apollo_holo_light_previous" />
-
- <ImageButton
- android:id="@+id/audio_player_play"
- style="@style/AudioPlayerButton"
- android:contentDescription="@string/cd_play"
- android:src="@drawable/apollo_holo_light_pause" />
-
- <com.andrew.apollo.ui.widgets.RepeatingImageButton
- android:id="@+id/audio_player_next"
- style="@style/AudioPlayerButton"
- android:src="@drawable/apollo_holo_light_next" />
-
- <ImageButton
- android:id="@+id/audio_player_shuffle"
- style="@style/AudioPlayerButton"
- android:contentDescription="@string/cd_shuffle"
- android:src="@drawable/apollo_holo_light_shuffle_normal" />
- </LinearLayout>
-
-</merge> \ No newline at end of file
diff --git a/res/layout/audio_player.xml b/res/layout/audio_player.xml
deleted file mode 100644
index fa55223..0000000
--- a/res/layout/audio_player.xml
+++ /dev/null
@@ -1,46 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical"
- tools:ignore="ContentDescription" >
-
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="0dp"
- android:layout_weight="1"
- android:background="@color/black" >
-
- <ImageView
- android:id="@+id/audio_player_album_art"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:scaleType="fitXY" />
- </LinearLayout>
-
- <RelativeLayout
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:paddingLeft="@dimen/audio_player_info_container_padding"
- android:paddingRight="@dimen/audio_player_info_container_padding"
- android:paddingTop="@dimen/audio_player_artwork_padding" >
-
- <TextView
- android:id="@+id/audio_player_track"
- style="@style/AudioPlayerText"
- android:textColor="@color/black"
- android:textSize="@dimen/text_size_large"
- android:textStyle="bold" />
-
- <TextView
- android:id="@+id/audio_player_album_artist"
- style="@style/AudioPlayerText"
- android:layout_below="@+id/audio_player_track"
- android:textColor="@color/transparent_black"
- android:textSize="@dimen/text_size_small" />
- </RelativeLayout>
-
- <include layout="@layout/audio_controls" />
-
-</LinearLayout> \ No newline at end of file
diff --git a/res/layout/audio_player_browser.xml b/res/layout/audio_player_browser.xml
deleted file mode 100644
index 9c57385..0000000
--- a/res/layout/audio_player_browser.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical" >
-
- <include layout="@layout/colorstrip" />
-
- <android.support.v4.view.ViewPager
- android:id="@+id/viewPager"
- android:layout_width="match_parent"
- android:layout_height="match_parent" />
-
-</LinearLayout> \ No newline at end of file
diff --git a/res/layout/audio_player_controls.xml b/res/layout/audio_player_controls.xml
new file mode 100644
index 0000000..511aea9
--- /dev/null
+++ b/res/layout/audio_player_controls.xml
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<merge xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <FrameLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1" >
+
+ <com.andrew.apollo.widgets.ShuffleButton
+ android:id="@+id/action_button_shuffle"
+ android:layout_width="@dimen/audio_player_controls_end_button_width"
+ android:layout_height="@dimen/audio_player_controls_end_button_height"
+ android:layout_gravity="center"
+ android:contentDescription="@string/accessibility_shuffle"
+ android:scaleType="centerInside"
+ android:src="@drawable/btn_playback_shuffle" />
+ </FrameLayout>
+
+ <FrameLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1" >
+
+ <com.andrew.apollo.widgets.RepeatingImageButton
+ android:id="@+id/action_button_previous"
+ android:layout_width="@dimen/audio_player_controls_main_button_width"
+ android:layout_height="@dimen/audio_player_controls_main_button_height"
+ android:layout_gravity="center"
+ android:scaleType="centerInside"
+ android:src="@drawable/btn_playback_previous" />
+ </FrameLayout>
+
+ <FrameLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1" >
+
+ <com.andrew.apollo.widgets.PlayPauseButton
+ android:id="@+id/action_button_play"
+ android:layout_width="@dimen/audio_player_controls_main_button_width"
+ android:layout_height="@dimen/audio_player_controls_main_button_height"
+ android:layout_gravity="center"
+ android:contentDescription="@string/accessibility_play"
+ android:focusable="true"
+ android:scaleType="centerInside"
+ android:src="@drawable/btn_playback_play" />
+ </FrameLayout>
+
+ <FrameLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1" >
+
+ <com.andrew.apollo.widgets.RepeatingImageButton
+ android:id="@+id/action_button_next"
+ android:layout_width="@dimen/audio_player_controls_main_button_width"
+ android:layout_height="@dimen/audio_player_controls_main_button_height"
+ android:layout_gravity="center"
+ android:scaleType="centerInside"
+ android:src="@drawable/btn_playback_next" />
+ </FrameLayout>
+
+ <FrameLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1" >
+
+ <com.andrew.apollo.widgets.RepeatButton
+ android:id="@+id/action_button_repeat"
+ android:layout_width="@dimen/audio_player_controls_end_button_width"
+ android:layout_height="@dimen/audio_player_controls_end_button_height"
+ android:layout_gravity="center"
+ android:contentDescription="@string/accessibility_repeat"
+ android:focusable="true"
+ android:scaleType="centerInside"
+ android:src="@drawable/btn_playback_repeat" />
+ </FrameLayout>
+
+</merge> \ No newline at end of file
diff --git a/res/layout/bottom_action_bar.xml b/res/layout/bottom_action_bar.xml
index 2994335..2bcb032 100644
--- a/res/layout/bottom_action_bar.xml
+++ b/res/layout/bottom_action_bar.xml
@@ -1,69 +1,98 @@
<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/bottom_action_bar_container"
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<com.andrew.apollo.widgets.theme.BottomActionBar xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:app="http://schemas.android.com/apk/res/com.andrew.apollo"
+ android:id="@+id/bottom_action_bar_parent"
android:layout_width="match_parent"
android:layout_height="@dimen/bottom_action_bar_height"
android:layout_gravity="bottom"
- android:orientation="vertical" >
+ tools:ignore="ContentDescription" >
- <ImageView
- android:id="@+id/bottom_action_bar_info_divider"
- android:layout_width="match_parent"
- android:layout_height="@dimen/bottom_action_bar_divider_height"
- android:background="@color/holo_blue_dark" />
+ <include
+ android:id="@+id/colorstrip"
+ layout="@layout/colorstrip" />
- <com.andrew.apollo.ui.widgets.BottomActionBar
+ <LinearLayout
android:id="@+id/bottom_action_bar"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:background="@drawable/holo_selector"
+ android:layout_below="@+id/colorstrip"
+ android:clickable="true"
android:orientation="horizontal" >
- <ImageView
+ <com.andrew.apollo.widgets.SquareImageView
android:id="@+id/bottom_action_bar_album_art"
- android:layout_width="@dimen/bottom_action_bar_album_art_width_height"
- android:layout_height="@dimen/bottom_action_bar_album_art_width_height"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
android:layout_gravity="left|center"
- android:contentDescription="@string/cd_bottom_action_bar_album_art"
- android:scaleType="fitXY" />
+ android:background="@drawable/default_artwork" />
- <LinearLayout
+ <RelativeLayout
+ android:id="@+id/bottom_action_bar_info_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
- android:orientation="vertical"
android:paddingLeft="@dimen/bottom_action_bar_info_padding_left" >
- <TextView
- android:id="@+id/bottom_action_bar_track_name"
- style="@style/BottomActionBarText"
- android:textColor="@color/transparent_black"
- android:textStyle="bold" />
+ <com.andrew.apollo.widgets.theme.ThemeableTextView
+ android:id="@+id/bottom_action_bar_line_one"
+ style="@style/BottomActionBarLineOne"
+ app:themeResource="bab_line_one" />
+
+ <com.andrew.apollo.widgets.theme.ThemeableTextView
+ android:id="@+id/bottom_action_bar_line_two"
+ style="@style/BottomActionBarLineTwo"
+ android:layout_below="@+id/bottom_action_bar_line_one"
+ app:themeResource="bab_line_two" />
+ </RelativeLayout>
+
+ <HorizontalScrollView
+ android:layout_width="@dimen/bottom_action_bar_button_container_width"
+ android:layout_height="match_parent"
+ android:scrollbars="none" >
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:orientation="horizontal" >
+
+ <com.andrew.apollo.widgets.RepeatingImageButton
+ android:id="@+id/action_button_previous"
+ style="@style/BottomActionBarItem.Previous" />
- <TextView
- android:id="@+id/bottom_action_bar_artist_name"
- style="@style/BottomActionBarText"
- android:textColor="@color/transparent_black" />
- </LinearLayout>
+ <com.andrew.apollo.widgets.PlayPauseButton
+ android:id="@+id/action_button_play"
+ style="@style/BottomActionBarItem.Play" />
- <com.andrew.apollo.ui.widgets.BottomActionBarItem
- android:id="@+id/bottom_action_bar_item_one"
- style="@style/BottomActionBarItem"
- android:contentDescription="@string/cd_favorite"
- android:src="@drawable/apollo_holo_light_favorite_normal" />
+ <com.andrew.apollo.widgets.RepeatingImageButton
+ android:id="@+id/action_button_next"
+ style="@style/BottomActionBarItem.Next" />
- <com.andrew.apollo.ui.widgets.BottomActionBarItem
- android:id="@+id/bottom_action_bar_item_two"
- style="@style/BottomActionBarItem"
- android:contentDescription="@string/cd_search"
- android:src="@drawable/apollo_holo_light_search" />
+ <com.andrew.apollo.widgets.ShuffleButton
+ android:id="@+id/action_button_shuffle"
+ style="@style/BottomActionBarItem.Shuffle" />
- <com.andrew.apollo.ui.widgets.BottomActionBarItem
- android:id="@+id/bottom_action_bar_item_three"
- style="@style/BottomActionBarItem"
- android:contentDescription="@string/cd_overflow"
- android:src="@drawable/apollo_holo_light_overflow" />
- </com.andrew.apollo.ui.widgets.BottomActionBar>
+ <com.andrew.apollo.widgets.RepeatButton
+ android:id="@+id/action_button_repeat"
+ style="@style/BottomActionBarItem.Repeat" />
+ </LinearLayout>
+ </HorizontalScrollView>
+ </LinearLayout>
-</LinearLayout> \ No newline at end of file
+</com.andrew.apollo.widgets.theme.BottomActionBar> \ No newline at end of file
diff --git a/res/layout/bottom_action_bar_controls.xml b/res/layout/bottom_action_bar_controls.xml
deleted file mode 100644
index 3406c00..0000000
--- a/res/layout/bottom_action_bar_controls.xml
+++ /dev/null
@@ -1,55 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="@dimen/bottom_action_bar_height"
- android:layout_gravity="bottom"
- android:orientation="vertical" >
-
- <ImageView
- android:id="@+id/bottom_action_bar_control_divider"
- android:layout_width="match_parent"
- android:layout_height="@dimen/bottom_action_bar_divider_height"
- android:background="@color/holo_blue_dark" />
-
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="horizontal" >
-
- <ImageButton
- android:id="@+id/bottom_action_bar_repeat"
- style="@style/BottomActionBarItem"
- android:layout_weight="1"
- android:contentDescription="@string/cd_repeat"
- android:src="@drawable/apollo_holo_light_repeat_normal" />
-
- <ImageButton
- android:id="@+id/bottom_action_bar_previous"
- style="@style/BottomActionBarItem"
- android:layout_weight="1"
- android:contentDescription="@string/cd_previous"
- android:src="@drawable/apollo_holo_light_previous" />
-
- <ImageButton
- android:id="@+id/bottom_action_bar_play"
- style="@style/BottomActionBarItem"
- android:layout_weight="1"
- android:contentDescription="@string/cd_play"
- android:src="@drawable/apollo_holo_light_play" />
-
- <ImageButton
- android:id="@+id/bottom_action_bar_next"
- style="@style/BottomActionBarItem"
- android:layout_weight="1"
- android:contentDescription="@string/cd_next"
- android:src="@drawable/apollo_holo_light_next" />
-
- <ImageButton
- android:id="@+id/bottom_action_bar_shuffle"
- style="@style/BottomActionBarItem"
- android:layout_weight="1"
- android:contentDescription="@string/cd_shuffle"
- android:src="@drawable/apollo_holo_light_shuffle_normal" />
- </LinearLayout>
-
-</LinearLayout> \ No newline at end of file
diff --git a/res/layout/color_scheme_dialog.xml b/res/layout/color_scheme_dialog.xml
new file mode 100644
index 0000000..49bcaf1
--- /dev/null
+++ b/res/layout/color_scheme_dialog.xml
@@ -0,0 +1,156 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="@dimen/color_scheme_dialog_row_padding" >
+
+ <com.andrew.apollo.widgets.ColorPickerView
+ android:id="@+id/color_picker_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+ <LinearLayout
+ android:id="@+id/color_scheme_dialog_row_one"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/color_picker_view"
+ android:layout_marginBottom="4dp"
+ android:orientation="horizontal"
+ android:paddingLeft="@dimen/color_scheme_dialog_row_padding"
+ android:paddingRight="@dimen/color_scheme_dialog_row_padding" >
+
+ <Button
+ android:id="@+id/color_scheme_dialog_preset_one"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:background="@color/holo_blue_light" />
+
+ <Button
+ android:id="@+id/color_scheme_dialog_preset_two"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/color_scheme_dialog_row_padding"
+ android:layout_weight="1"
+ android:background="@color/holo_green_light" />
+
+ <Button
+ android:id="@+id/color_scheme_dialog_preset_three"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/color_scheme_dialog_row_padding"
+ android:layout_weight="1"
+ android:background="@color/holo_orange_dark" />
+
+ <Button
+ android:id="@+id/color_scheme_dialog_preset_four"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/color_scheme_dialog_row_padding"
+ android:layout_weight="1"
+ android:background="@color/holo_orange_light" />
+
+ <Button
+ android:id="@+id/color_scheme_dialog_old_color"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/color_scheme_dialog_row_padding"
+ android:layout_weight="2"
+ android:background="@color/holo_blue_light"
+ android:text="@string/current_color"
+ android:textSize="@dimen/text_size_micro"
+ android:textStyle="bold" />
+
+ <Button
+ android:id="@+id/color_scheme_dialog_new_color"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="2"
+ android:background="@color/transparent"
+ android:text="@string/new_color"
+ android:textSize="@dimen/text_size_micro"
+ android:textStyle="bold" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/color_scheme_dialog_row_one"
+ android:layout_marginBottom="4dp"
+ android:orientation="horizontal"
+ android:paddingLeft="@dimen/color_scheme_dialog_row_padding"
+ android:paddingRight="@dimen/color_scheme_dialog_row_padding" >
+
+ <Button
+ android:id="@+id/color_scheme_dialog_preset_five"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:background="@color/holo_purple" />
+
+ <Button
+ android:id="@+id/color_scheme_dialog_preset_six"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/color_scheme_dialog_row_padding"
+ android:layout_weight="1"
+ android:background="@color/holo_red_light" />
+
+ <Button
+ android:id="@+id/color_scheme_dialog_preset_seven"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/color_scheme_dialog_row_padding"
+ android:layout_weight="1"
+ android:background="@color/white" />
+
+ <Button
+ android:id="@+id/color_scheme_dialog_preset_eight"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/color_scheme_dialog_row_padding"
+ android:layout_weight="1"
+ android:background="@color/black" />
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/color_scheme_dialog_row_padding"
+ android:layout_weight="4"
+ android:gravity="center"
+ android:orientation="horizontal" >
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/hex" />
+
+ <EditText
+ android:id="@+id/color_scheme_dialog_hex_value"
+ android:layout_width="110dp"
+ android:layout_height="wrap_content"
+ android:digits="0123456789ABCDEFabcdef"
+ android:imeOptions="actionGo"
+ android:inputType="textFilter"
+ android:maxLength="8"
+ android:singleLine="true"
+ android:typeface="monospace" />
+ </LinearLayout>
+ </LinearLayout>
+
+</RelativeLayout> \ No newline at end of file
diff --git a/res/layout/colorstrip.xml b/res/layout/colorstrip.xml
index b04eb8c..4c13b7a 100644
--- a/res/layout/colorstrip.xml
+++ b/res/layout/colorstrip.xml
@@ -1,8 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<com.andrew.apollo.widgets.theme.Colorstrip xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/colorstrip"
android:layout_width="match_parent"
- android:layout_height="@dimen/colorstrip_height"
- android:foreground="@drawable/colorstrip_shadow"
- tools:ignore="Overdraw" />
+ android:layout_height="@dimen/colorstrip_height"/> \ No newline at end of file
diff --git a/res/layout/context_menu.xml b/res/layout/context_menu.xml
deleted file mode 100644
index 9802971..0000000
--- a/res/layout/context_menu.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/track_list_context_frame"
- android:layout_width="wrap_content"
- android:layout_height="@dimen/listview_item_height"
- android:layout_alignParentRight="true"
- android:background="@drawable/holo_selector"
- android:clickable="true"
- android:paddingRight="@dimen/quick_context_padding_right" >
-
- <ImageView
- android:id="@+id/quick_context_line"
- android:layout_width="@dimen/quick_context_line_width"
- android:layout_height="@dimen/quick_context_line_height"
- android:layout_gravity="center|left"
- android:background="@color/transparent_black" />
-
- <ImageView
- android:id="@+id/quick_context_tip"
- android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:layout_gravity="center"
- android:layout_marginRight="@dimen/quick_context_margin_right"
- android:scaleType="centerInside"
- android:src="@drawable/dropdown_ic_arrow_normal_holo_light" />
-
-</FrameLayout> \ No newline at end of file
diff --git a/res/layout/context_menu_header.xml b/res/layout/context_menu_header.xml
deleted file mode 100644
index ba41e56..0000000
--- a/res/layout/context_menu_header.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:background="@color/black" >
-
- <ImageView
- android:id="@+id/header_image"
- android:layout_width="match_parent"
- android:layout_height="@dimen/half_and_half_image_height" />
-
- <TextView
- android:id="@+id/header_text"
- style="@style/HeaderText"
- android:layout_alignBottom="@+id/header_image" />
-
-</RelativeLayout> \ No newline at end of file
diff --git a/res/layout/custom_action_bar.xml b/res/layout/custom_action_bar.xml
deleted file mode 100644
index 090bb1c..0000000
--- a/res/layout/custom_action_bar.xml
+++ /dev/null
@@ -1,45 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/action_bar_layout"
- android:layout_width="96dp"
- android:layout_height="wrap_content"
- android:layout_gravity="right|center"
- android:background="@drawable/holo_selector"
- android:clickable="true"
- android:focusable="true" >
-
- <ImageView
- android:id="@+id/action_bar_album_art"
- android:layout_width="48dp"
- android:layout_height="48dp"
- android:layout_alignParentBottom="true"
- android:layout_alignParentRight="true"
- android:layout_alignParentTop="true" />
-
- <TextView
- android:id="@+id/action_bar_track_name"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_alignParentTop="true"
- android:layout_toLeftOf="@+id/action_bar_album_art"
- android:ellipsize="end"
- android:paddingRight="5dp"
- android:paddingTop="10dp"
- android:singleLine="true"
- android:textColor="@color/transparent_black"
- android:textSize="@dimen/text_size_micro"
- android:textStyle="bold" />
-
- <TextView
- android:id="@+id/action_bar_album_name"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_below="@+id/action_bar_track_name"
- android:layout_toLeftOf="@+id/action_bar_album_art"
- android:ellipsize="end"
- android:paddingRight="5dp"
- android:singleLine="true"
- android:textColor="@color/transparent_black"
- android:textSize="@dimen/text_size_micro" />
-
-</RelativeLayout> \ No newline at end of file
diff --git a/res/layout/edit_track_list_item.xml b/res/layout/edit_track_list_item.xml
new file mode 100644
index 0000000..1d388d4
--- /dev/null
+++ b/res/layout/edit_track_list_item.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/edit_track_list_parent"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/item_normal_height" >
+
+ <ImageView
+ android:id="@+id/edit_track_list_item_handle"
+ android:layout_width="@dimen/drag_and_drop_handle"
+ android:layout_height="wrap_content"
+ android:scaleType="fitXY"
+ android:src="@drawable/playlist_tile_normal" />
+
+ <FrameLayout
+ android:id="@+id/edit_track_list_container"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/item_normal_height"
+ android:layout_toRightOf="@+id/edit_track_list_item_handle"
+ android:gravity="center_vertical" >
+
+ <include layout="@layout/list_item_simple" />
+ </FrameLayout>
+
+</RelativeLayout> \ No newline at end of file
diff --git a/res/layout/empty_view.xml b/res/layout/empty_view.xml
deleted file mode 100644
index aa29c7a..0000000
--- a/res/layout/empty_view.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="wrap_content"
- android:layout_height="@dimen/bottom_action_bar_height" /> \ No newline at end of file
diff --git a/res/layout/faux_carousel.xml b/res/layout/faux_carousel.xml
new file mode 100644
index 0000000..e8e7564
--- /dev/null
+++ b/res/layout/faux_carousel.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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. -->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" >
+
+ <include
+ layout="@layout/profile_tab_carousel"
+ android:visibility="invisible" />
+
+</FrameLayout> \ No newline at end of file
diff --git a/res/layout/fourbyone_app_widget.xml b/res/layout/fourbyone_app_widget.xml
deleted file mode 100644
index 8e0cf1c..0000000
--- a/res/layout/fourbyone_app_widget.xml
+++ /dev/null
@@ -1,86 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="@drawable/appwidget_bg"
- android:orientation="horizontal"
- tools:ignore="Overdraw" >
-
- <ImageView
- android:id="@+id/four_by_one_albumart"
- android:layout_width="@dimen/four_by_one_album_art_width"
- android:layout_height="match_parent"
- android:scaleType="centerCrop" />
-
- <LinearLayout
- android:id="@+id/four_by_one_album_appwidget"
- android:layout_width="0dp"
- android:layout_height="match_parent"
- android:layout_weight="2"
- android:background="@drawable/holo_selector"
- android:clickable="true"
- android:focusable="true"
- android:gravity="center"
- android:orientation="vertical"
- android:paddingLeft="4dp" >
-
- <TextView
- android:id="@+id/four_by_one_title"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:ellipsize="end"
- android:singleLine="true"
- android:textColor="@color/transparent_black"
- android:textSize="@dimen/text_size_small"
- android:textStyle="bold" />
-
- <TextView
- android:id="@+id/four_by_one_artist"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:ellipsize="end"
- android:singleLine="true"
- android:textColor="@color/transparent_black"
- android:textSize="@dimen/text_size_small" />
- </LinearLayout>
-
- <ImageButton
- android:id="@+id/four_by_one_control_prev"
- android:layout_width="0dp"
- android:layout_height="match_parent"
- android:layout_weight="1"
- android:background="@drawable/holo_selector"
- android:scaleType="center"
- android:src="@drawable/apollo_holo_light_previous"
- android:visibility="gone" />
-
- <ImageView
- android:layout_width="0.2dp"
- android:layout_height="match_parent"
- android:background="@color/transparent_black" />
-
- <ImageButton
- android:id="@+id/four_by_one_control_play"
- android:layout_width="0dp"
- android:layout_height="match_parent"
- android:layout_weight="1"
- android:background="@drawable/holo_selector"
- android:scaleType="center"
- android:src="@drawable/apollo_holo_light_play" />
-
- <ImageView
- android:layout_width="0.2dp"
- android:layout_height="match_parent"
- android:background="@color/transparent_black" />
-
- <ImageButton
- android:id="@+id/four_by_one_control_next"
- android:layout_width="0dp"
- android:layout_height="match_parent"
- android:layout_weight="1"
- android:background="@drawable/holo_selector"
- android:scaleType="center"
- android:src="@drawable/apollo_holo_light_next" />
-
-</LinearLayout> \ No newline at end of file
diff --git a/res/layout/fourbytwo_app_widget.xml b/res/layout/fourbytwo_app_widget.xml
deleted file mode 100644
index 05fda67..0000000
--- a/res/layout/fourbytwo_app_widget.xml
+++ /dev/null
@@ -1,105 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/four_by_two"
- android:layout_width="match_parent"
- android:layout_height="@dimen/four_by_two_height"
- android:background="@drawable/appwidget_bg"
- android:gravity="center"
- android:orientation="horizontal" >
-
- <LinearLayout
- android:id="@+id/four_by_two_controls"
- android:layout_width="match_parent"
- android:layout_height="@dimen/four_by_two_control_height"
- android:layout_alignParentBottom="true"
- android:orientation="horizontal" >
-
- <ImageButton
- android:id="@+id/four_by_two_control_prev"
- style="@style/FourByTwoMediaButton"
- android:src="@drawable/apollo_holo_light_previous" />
-
- <ImageButton
- android:id="@+id/four_by_two_control_play"
- style="@style/FourByTwoMediaButton"
- android:src="@drawable/apollo_holo_light_play" />
-
- <ImageButton
- android:id="@+id/four_by_two_control_next"
- style="@style/FourByTwoMediaButton"
- android:src="@drawable/apollo_holo_light_next" />
-
- <ImageButton
- android:id="@+id/four_by_two_control_shuffle"
- style="@style/FourByTwoMediaButton"
- android:src="@drawable/apollo_holo_light_shuffle_normal" />
-
- <ImageButton
- android:id="@+id/four_by_two_control_repeat"
- style="@style/FourByTwoMediaButton"
- android:src="@drawable/apollo_holo_light_repeat_normal" />
- </LinearLayout>
-
- <ImageView
- android:id="@+id/four_by_two_controls_info_divider"
- android:layout_width="match_parent"
- android:layout_height="1dp"
- android:layout_above="@id/four_by_two_controls"
- android:scaleType="fitXY" />
-
- <ImageView
- android:id="@+id/four_by_two_albumart"
- android:layout_width="@dimen/four_by_two_album_art_width"
- android:layout_height="match_parent"
- android:layout_above="@id/four_by_two_controls_info_divider"
- android:adjustViewBounds="true"
- android:scaleType="fitXY" />
-
- <ImageView
- android:layout_width="match_parent"
- android:layout_height="1dp"
- android:layout_above="@id/four_by_two_controls"
- android:background="@color/holo_blue_dark" />
-
- <LinearLayout
- android:id="@+id/four_by_two_info"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_above="@id/four_by_two_controls"
- android:layout_toRightOf="@+id/four_by_two_albumart"
- android:background="@drawable/holo_selector"
- android:clickable="true"
- android:focusable="true"
- android:gravity="center"
- android:orientation="vertical" >
-
- <TextView
- android:id="@+id/four_by_two_trackname"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:ellipsize="end"
- android:singleLine="true"
- android:textColor="@color/transparent_black"
- android:textSize="@dimen/text_size_small"
- android:textStyle="bold" />
-
- <TextView
- android:id="@+id/four_by_two_albumname"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:ellipsize="end"
- android:singleLine="true"
- android:textColor="@color/transparent_black"
- android:textSize="@dimen/text_size_small" />
-
- <TextView
- android:id="@+id/four_by_two_artistname"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:ellipsize="end"
- android:singleLine="true"
- android:textColor="@color/transparent_black"
- android:textSize="@dimen/text_size_small" />
- </LinearLayout>
-
-</RelativeLayout> \ No newline at end of file
diff --git a/res/layout/fragment_music_browser_phone.xml b/res/layout/fragment_music_browser_phone.xml
new file mode 100644
index 0000000..d45e8f6
--- /dev/null
+++ b/res/layout/fragment_music_browser_phone.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ <RelativeLayout
+ android:id="@+id/fragment_home_phone_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ <com.andrew.apollo.widgets.theme.ThemeableTitlePageIndicator
+ android:id="@+id/fragment_home_phone_pager_titles"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="@dimen/text_size_micro" />
+
+ <android.support.v4.view.ViewPager
+ android:id="@+id/fragment_home_phone_pager"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_below="@+id/fragment_home_phone_pager_titles" />
+ </RelativeLayout>
+
+ <include layout="@layout/top_shadow" />
+
+</FrameLayout> \ No newline at end of file
diff --git a/res/layout/fragment_themes_base.xml b/res/layout/fragment_themes_base.xml
new file mode 100644
index 0000000..521cce5
--- /dev/null
+++ b/res/layout/fragment_themes_base.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:padding="@dimen/grid_item_spacing" >
+
+ <ImageView
+ android:id="@+id/image"
+ android:layout_width="match_parent"
+ android:layout_height="155.0dip"
+ android:scaleType="centerCrop"
+ tools:ignore="ContentDescription" />
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_below="@+id/image" >
+
+ <TextView
+ android:id="@+id/line_one"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_centerHorizontal="true"
+ android:layout_centerVertical="true"
+ android:background="@color/transparent_black"
+ android:gravity="center"
+ android:textColor="@color/white"
+ android:textSize="@dimen/text_size_dayum" />
+ </RelativeLayout>
+
+</RelativeLayout> \ No newline at end of file
diff --git a/res/layout/grid_base.xml b/res/layout/grid_base.xml
new file mode 100644
index 0000000..6437b36
--- /dev/null
+++ b/res/layout/grid_base.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/grid_base_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:padding="@dimen/list_preferred_item_padding" >
+
+ <TextView
+ android:id="@+id/empty"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical|center"
+ android:textColor="@color/holo_blue_light"
+ android:textSize="@dimen/text_size_large"
+ android:textStyle="bold"
+ android:visibility="gone" />
+
+ <GridView
+ android:id="@+id/grid_base"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:cacheColorHint="@color/transparent"
+ android:drawSelectorOnTop="true"
+ android:fadingEdge="vertical"
+ android:fastScrollEnabled="true"
+ android:horizontalSpacing="@dimen/grid_item_spacing"
+ android:scrollbarStyle="outsideOverlay"
+ android:scrollbars="vertical"
+ android:verticalSpacing="@dimen/grid_item_spacing" />
+
+</FrameLayout> \ No newline at end of file
diff --git a/res/layout/grid_items_normal.xml b/res/layout/grid_items_normal.xml
new file mode 100644
index 0000000..290a8b0
--- /dev/null
+++ b/res/layout/grid_items_normal.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res/com.andrew.apollo"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical" >
+
+ <include layout="@layout/square_image_view" />
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/item_normal_height"
+ android:layout_alignParentBottom="true"
+ android:background="@color/transparent_black"
+ android:gravity="center_vertical"
+ android:paddingLeft="@dimen/grid_item_padding_left" >
+
+ <com.andrew.apollo.widgets.theme.ThemeableTextView
+ android:id="@+id/line_one"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ android:maxLines="2"
+ android:textColor="@color/white"
+ android:textSize="@dimen/text_size_medium"
+ android:textStyle="bold"
+ app:themeResource="@null" />
+
+ <com.andrew.apollo.widgets.theme.ThemeableTextView
+ android:id="@+id/line_two"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/line_one"
+ android:singleLine="true"
+ android:textColor="@color/transparent_white"
+ android:textSize="@dimen/text_size_small"
+ app:themeResource="@null" />
+ </RelativeLayout>
+
+</RelativeLayout> \ No newline at end of file
diff --git a/res/layout/gridview.xml b/res/layout/gridview.xml
deleted file mode 100644
index 5e3e940..0000000
--- a/res/layout/gridview.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:gravity="center_vertical"
- android:orientation="vertical" >
-
- <include layout="@layout/shadow" />
-
- <GridView
- android:id="@+id/gridview"
- android:layout_width="match_parent"
- android:layout_height="0dp"
- android:layout_weight="1"
- android:cacheColorHint="@color/transparent"
- android:drawSelectorOnTop="true"
- android:numColumns="@integer/gridview_columns"
- android:scrollbars="none" />
-
- <include layout="@layout/empty_view" />
-
-</LinearLayout> \ No newline at end of file
diff --git a/res/layout/gridview_items.xml b/res/layout/gridview_items.xml
deleted file mode 100644
index 2c6462f..0000000
--- a/res/layout/gridview_items.xml
+++ /dev/null
@@ -1,47 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="wrap_content" >
-
- <ImageView
- android:id="@+id/gridview_image"
- android:layout_width="@dimen/gridview_image_width"
- android:layout_height="@dimen/gridview_image_height"
- android:scaleType="centerCrop" />
-
- <LinearLayout
- android:id="@+id/gridview_info_holder"
- android:layout_width="match_parent"
- android:layout_height="@dimen/gridview_item_ccontainer_height"
- android:layout_alignParentBottom="true"
- android:background="@color/transparent_black"
- android:gravity="center_vertical"
- android:orientation="vertical"
- android:paddingLeft="@dimen/gridview_item_ccontainer_padding_left"
- android:paddingRight="@dimen/gridview_item_ccontainer_padding_right" >
-
- <TextView
- android:id="@+id/gridview_line_one"
- style="@style/GridViewTextItem"
- android:textStyle="bold" />
-
- <TextView
- android:id="@+id/gridview_line_two"
- style="@style/GridViewTextItem" />
- </LinearLayout>
-
- <ImageView
- android:id="@+id/peak_one"
- style="@style/PeakMeter"
- android:layout_alignParentBottom="true"
- android:paddingBottom="@dimen/peak_meter_padding_bottom"
- android:paddingRight="@dimen/peak_meter_one_padding_right" />
-
- <ImageView
- android:id="@+id/peak_two"
- style="@style/PeakMeter"
- android:layout_alignParentBottom="true"
- android:paddingBottom="@dimen/peak_meter_padding_bottom"
- android:paddingRight="@dimen/peak_meter_two_padding_right" />
-
-</RelativeLayout> \ No newline at end of file
diff --git a/res/layout/half_and_half.xml b/res/layout/half_and_half.xml
deleted file mode 100644
index e9a43d0..0000000
--- a/res/layout/half_and_half.xml
+++ /dev/null
@@ -1,60 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- tools:ignore="UnknownIdInLayout" >
-
- <include layout="@layout/colorstrip" />
-
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_below="@+id/colorstrip"
- android:baselineAligned="false"
- android:orientation="horizontal" >
-
- <RelativeLayout
- android:id="@+id/artist_half_container"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:background="@color/black"
- android:padding="@dimen/half_and_half_container_padding" >
-
- <ImageView
- android:id="@+id/half_artist_image"
- android:layout_width="match_parent"
- android:layout_height="@dimen/half_and_half_image_height"
- android:scaleType="centerCrop" />
-
- <TextView
- android:id="@+id/half_artist_image_text"
- style="@style/HalfText"
- android:layout_alignBottom="@+id/half_artist_image"
- android:visibility="gone" />
- </RelativeLayout>
-
- <RelativeLayout
- android:id="@+id/album_half_container"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:background="@color/black"
- android:padding="@dimen/half_and_half_container_padding"
- android:visibility="gone" >
-
- <ImageView
- android:id="@+id/half_album_image"
- android:layout_width="match_parent"
- android:layout_height="@dimen/half_and_half_image_height"
- android:scaleType="centerCrop" />
-
- <TextView
- android:id="@+id/half_album_image_text"
- style="@style/HalfText"
- android:layout_alignBottom="@+id/half_album_image" />
- </RelativeLayout>
- </LinearLayout>
-
-</RelativeLayout> \ No newline at end of file
diff --git a/res/layout/library_browser.xml b/res/layout/library_browser.xml
deleted file mode 100644
index 383694e..0000000
--- a/res/layout/library_browser.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="wrap_content" >
-
- <com.andrew.apollo.ui.widgets.ScrollableTabView
- android:id="@+id/scrollingTabs"
- android:layout_width="match_parent"
- android:layout_height="wrap_content" />
-
- <android.support.v4.view.ViewPager
- android:id="@+id/viewPager"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_below="@+id/scrollingTabs" />
-
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="@dimen/bottom_action_bar_height"
- android:layout_alignParentBottom="true" >
-
- <android.support.v4.view.ViewPager
- android:id="@+id/bottomActionBarPager"
- android:layout_width="match_parent"
- android:layout_height="match_parent"/>
- </LinearLayout>
-
-</RelativeLayout> \ No newline at end of file
diff --git a/res/layout/list_base.xml b/res/layout/list_base.xml
new file mode 100644
index 0000000..0bd7cc6
--- /dev/null
+++ b/res/layout/list_base.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/list_base_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingBottom="@dimen/list_preferred_item_padding"
+ android:paddingTop="@dimen/list_preferred_item_padding" >
+
+ <TextView
+ android:id="@+id/empty"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical|center"
+ android:padding="@dimen/list_preferred_item_padding"
+ android:textColor="@color/holo_blue_light"
+ android:textSize="@dimen/text_size_large"
+ android:textStyle="bold"
+ android:visibility="gone" />
+
+ <com.andrew.apollo.dragdrop.DragSortListView
+ android:id="@+id/list_base"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:cacheColorHint="@color/transparent"
+ android:drawSelectorOnTop="false"
+ android:fadingEdge="vertical"
+ android:fastScrollAlwaysVisible="true"
+ android:fastScrollEnabled="true"
+ android:paddingLeft="@dimen/fast_scroll_padding_left"
+ android:paddingRight="@dimen/fast_scroll_padding_right" />
+
+</FrameLayout> \ No newline at end of file
diff --git a/res/layout/list_header.xml b/res/layout/list_header.xml
new file mode 100644
index 0000000..b8522eb
--- /dev/null
+++ b/res/layout/list_header.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="32.0dip"
+ android:background="@null"
+ android:gravity="center_vertical"
+ android:paddingLeft="10.0dip"
+ android:textAllCaps="true"
+ android:textColor="@color/white"
+ android:textSize="@dimen/text_size_medium" />
diff --git a/res/layout/list_item_detailed.xml b/res/layout/list_item_detailed.xml
new file mode 100644
index 0000000..6af42f5
--- /dev/null
+++ b/res/layout/list_item_detailed.xml
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res/com.andrew.apollo"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical" >
+
+ <com.andrew.apollo.widgets.LayoutSuppressingImageView
+ android:id="@+id/list_item_background"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/list_item_detailed_height"
+ android:scaleType="centerCrop" />
+
+ <com.andrew.apollo.widgets.LayoutSuppressingImageView
+ android:id="@+id/image"
+ android:layout_width="@dimen/list_item_detailed_height"
+ android:layout_height="@dimen/list_item_detailed_height"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentLeft="true"
+ android:scaleType="centerCrop" />
+
+ <RelativeLayout
+ android:id="@+id/image_background"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/list_item_detailed_height"
+ android:layout_toRightOf="@+id/image"
+ android:background="@color/list_item_background"
+ android:padding="@dimen/list_preferred_item_padding" >
+
+ <com.andrew.apollo.widgets.theme.ThemeableTextView
+ android:id="@+id/line_one"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ android:maxLines="2"
+ android:textColor="@color/white"
+ android:textSize="@dimen/text_size_medium"
+ android:textStyle="bold"
+ app:themeResource="@null" />
+
+ <com.andrew.apollo.widgets.theme.ThemeableTextView
+ android:id="@+id/line_two"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/line_one"
+ android:layout_marginTop="@dimen/list_item_line_two_margin_top"
+ android:maxLines="2"
+ android:textColor="@color/transparent_white"
+ android:textSize="@dimen/text_size_small"
+ app:themeResource="@null" />
+
+ <com.andrew.apollo.widgets.theme.ThemeableTextView
+ android:id="@+id/line_three"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:singleLine="true"
+ android:textColor="@color/transparent_white"
+ android:textSize="@dimen/text_size_small"
+ app:themeResource="@null" />
+ </RelativeLayout>
+
+</RelativeLayout> \ No newline at end of file
diff --git a/res/layout/list_item_detailed_no_background.xml b/res/layout/list_item_detailed_no_background.xml
new file mode 100644
index 0000000..b0c9f56
--- /dev/null
+++ b/res/layout/list_item_detailed_no_background.xml
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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. -->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res/com.andrew.apollo"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical" >
+
+ <com.andrew.apollo.widgets.LayoutSuppressingImageView
+ android:id="@+id/list_item_background"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/list_item_detailed_height"
+ android:scaleType="centerCrop" />
+
+ <com.andrew.apollo.widgets.LayoutSuppressingImageView
+ android:id="@+id/image"
+ android:layout_width="@dimen/list_item_detailed_height"
+ android:layout_height="@dimen/list_item_detailed_height"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentLeft="true"
+ android:scaleType="centerCrop" />
+
+ <RelativeLayout
+ android:id="@+id/image_background"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/list_item_detailed_height"
+ android:layout_toRightOf="@+id/image"
+ android:background="@color/list_item_background"
+ android:padding="@dimen/list_preferred_item_padding" >
+
+ <com.andrew.apollo.widgets.theme.ThemeableTextView
+ android:id="@+id/line_one"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ android:maxLines="2"
+ android:textColor="@color/white"
+ android:textSize="@dimen/text_size_medium"
+ android:textStyle="bold"
+ app:themeResource="line_one" />
+
+ <com.andrew.apollo.widgets.theme.ThemeableTextView
+ android:id="@+id/line_two"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/line_one"
+ android:layout_marginTop="@dimen/list_item_line_two_margin_top"
+ android:maxLines="2"
+ android:textColor="@color/transparent_white"
+ android:textSize="@dimen/text_size_small"
+ app:themeResource="line_two" />
+
+ <com.andrew.apollo.widgets.theme.ThemeableTextView
+ android:id="@+id/line_three"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:singleLine="true"
+ android:textColor="@color/transparent_white"
+ android:textSize="@dimen/text_size_small"
+ app:themeResource="line_three" />
+ </RelativeLayout>
+
+</RelativeLayout> \ No newline at end of file
diff --git a/res/layout/list_item_normal.xml b/res/layout/list_item_normal.xml
new file mode 100644
index 0000000..615ab5d
--- /dev/null
+++ b/res/layout/list_item_normal.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:app="http://schemas.android.com/apk/res/com.andrew.apollo"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ tools:ignore="ContentDescription" >
+
+ <com.andrew.apollo.widgets.SquareImageView
+ android:id="@+id/image"
+ android:layout_width="@dimen/item_normal_height"
+ android:layout_height="@dimen/item_normal_height"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ android:scaleType="fitXY" />
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/item_normal_height"
+ android:layout_toRightOf="@+id/image"
+ android:gravity="center_vertical"
+ android:minHeight="@dimen/item_normal_height"
+ android:paddingLeft="@dimen/list_preferred_item_padding"
+ android:paddingRight="@dimen/list_preferred_item_padding" >
+
+ <com.andrew.apollo.widgets.theme.ThemeableTextView
+ android:id="@+id/line_one"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ android:singleLine="true"
+ android:textSize="@dimen/text_size_medium"
+ android:textStyle="bold"
+ app:themeResource="line_one" />
+
+ <com.andrew.apollo.widgets.theme.ThemeableTextView
+ android:id="@+id/line_two"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/line_one"
+ android:layout_marginTop="@dimen/list_item_line_two_margin_top"
+ android:singleLine="true"
+ android:textSize="@dimen/text_size_small"
+ app:themeResource="line_two" />
+ </RelativeLayout>
+
+</RelativeLayout> \ No newline at end of file
diff --git a/res/layout/list_item_simple.xml b/res/layout/list_item_simple.xml
new file mode 100644
index 0000000..3cbc3ed
--- /dev/null
+++ b/res/layout/list_item_simple.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res/com.andrew.apollo"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/item_normal_height"
+ android:gravity="center_vertical"
+ android:minHeight="@dimen/item_normal_height"
+ android:paddingLeft="@dimen/list_preferred_item_padding"
+ android:paddingRight="@dimen/list_preferred_item_padding" >
+
+ <com.andrew.apollo.widgets.theme.ThemeableTextView
+ android:id="@+id/line_one"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ android:singleLine="true"
+ android:textSize="@dimen/text_size_medium"
+ android:textStyle="bold"
+ app:themeResource="line_one" />
+
+ <com.andrew.apollo.widgets.theme.ThemeableTextView
+ android:id="@+id/line_two"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/line_one"
+ android:layout_marginTop="@dimen/list_item_line_two_margin_top"
+ android:singleLine="true"
+ android:textSize="@dimen/text_size_small"
+ app:themeResource="line_two" />
+
+ <com.andrew.apollo.widgets.theme.ThemeableTextView
+ android:id="@+id/line_three"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:singleLine="true"
+ android:textSize="@dimen/text_size_small"
+ app:themeResource="line_three" />
+
+</RelativeLayout> \ No newline at end of file
diff --git a/res/layout/list_separator.xml b/res/layout/list_separator.xml
deleted file mode 100644
index b8717df..0000000
--- a/res/layout/list_separator.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:focusable="false"
- android:paddingLeft="@dimen/list_separator_container_padding_left"
- android:paddingRight="@dimen/fast_scroll_padding_right" >
-
- <TextView
- android:id="@+id/title"
- style="@style/SeparatorTextViewStyle" />
-
-</FrameLayout> \ No newline at end of file
diff --git a/res/layout/listview.xml b/res/layout/listview.xml
deleted file mode 100644
index 7e4283a..0000000
--- a/res/layout/listview.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:gravity="center_vertical"
- android:orientation="vertical" >
-
- <include layout="@layout/shadow" />
-
- <include layout="@layout/list_separator" />
-
- <ListView
- android:id="@android:id/list"
- android:layout_width="match_parent"
- android:layout_height="0dp"
- android:layout_weight="1"
- android:cacheColorHint="@color/transparent"
- android:drawSelectorOnTop="false"
- android:fastScrollAlwaysVisible="true"
- android:fastScrollEnabled="true"
- android:listSelector="@drawable/holo_selector"
- android:paddingRight="@dimen/fast_scroll_padding_right" />
-
- <include
- android:id="@+id/empty_view"
- layout="@layout/empty_view" />
-
-</LinearLayout> \ No newline at end of file
diff --git a/res/layout/listview_items.xml b/res/layout/listview_items.xml
deleted file mode 100644
index d589ae2..0000000
--- a/res/layout/listview_items.xml
+++ /dev/null
@@ -1,64 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="@dimen/listview_item_height"
- android:layout_gravity="center_vertical" >
-
- <ImageView
- android:id="@+id/listview_item_image"
- android:layout_width="@dimen/listview_album_art"
- android:layout_height="@dimen/listview_album_art"
- android:layout_alignParentBottom="true"
- android:layout_alignParentLeft="true"
- android:layout_alignParentTop="true"
- android:scaleType="centerCrop" />
-
- <!-- Padding may be set on via code for some tabs -->
-
- <TextView
- android:id="@+id/listview_item_line_one"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_toRightOf="@+id/listview_item_image"
- android:ellipsize="end"
- android:maxLength="29"
- android:paddingLeft="@dimen/listview_items_padding_left_top"
- android:paddingRight="@dimen/listview_items_padding_right"
- android:paddingTop="@dimen/listview_items_padding_left_top"
- android:shadowColor="@color/black"
- android:shadowRadius="0.5"
- android:singleLine="true"
- android:textSize="@dimen/text_size_medium" />
-
- <TextView
- android:id="@+id/listview_item_line_two"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_below="@+id/listview_item_line_one"
- android:layout_toRightOf="@+id/listview_item_image"
- android:ellipsize="end"
- android:paddingLeft="@dimen/listview_items_padding_left_top"
- android:paddingRight="@dimen/listview_items_padding_right"
- android:paddingTop="@dimen/listview_items_padding_left_top"
- android:shadowColor="@color/black"
- android:shadowRadius="0.2"
- android:singleLine="true"
- android:textSize="@dimen/text_size_small" />
-
- <include layout="@layout/context_menu" />
-
- <ImageView
- android:id="@+id/peak_two"
- style="@style/PeakMeter"
- android:layout_centerVertical="true"
- android:paddingRight="@dimen/listview_peak_meter_two_padding_right"
- android:paddingTop="@dimen/peak_meter_padding_top" />
-
- <ImageView
- android:id="@+id/peak_one"
- style="@style/PeakMeter"
- android:layout_centerVertical="true"
- android:paddingRight="@dimen/listview_peak_meter_one_padding_right"
- android:paddingTop="@dimen/peak_meter_padding_top" />
-
-</RelativeLayout> \ No newline at end of file
diff --git a/res/layout/lyrics_base.xml b/res/layout/lyrics_base.xml
new file mode 100644
index 0000000..821a8f2
--- /dev/null
+++ b/res/layout/lyrics_base.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res/com.andrew.apollo"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ <ScrollView
+ android:id="@+id/audio_player_lyrics_frame"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fillViewport="true" >
+
+ <com.andrew.apollo.widgets.theme.ThemeableTextView
+ android:id="@+id/audio_player_lyrics"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:padding="@dimen/list_preferred_item_padding"
+ android:textStyle="bold"
+ app:themeResource="lyrics" />
+ </ScrollView>
+
+ <ProgressBar
+ android:id="@+id/audio_player_lyrics_progess"
+ style="?android:attr/progressBarStyleLarge"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical|center"
+ android:visibility="gone" />
+
+</FrameLayout> \ No newline at end of file
diff --git a/res/layout/notification_template_base.xml b/res/layout/notification_template_base.xml
new file mode 100644
index 0000000..87c0990
--- /dev/null
+++ b/res/layout/notification_template_base.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/notification_base"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:orientation="horizontal"
+ tools:ignore="ContentDescription" >
+
+ <ImageView
+ android:id="@+id/notification_base_image"
+ android:layout_width="@dimen/notification_big_icon_width"
+ android:layout_height="@dimen/notification_big_icon_height"
+ android:background="@drawable/default_artwork"
+ android:gravity="center"
+ android:scaleType="fitXY" />
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:paddingBottom="@dimen/notification_info_container_padding_bottom"
+ android:paddingLeft="@dimen/notification_info_container_padding_left" >
+
+ <TextView
+ android:id="@+id/notification_base_line_one"
+ style="@style/NotificationText"
+ android:textAppearance="@android:style/TextAppearance.StatusBar.EventContent.Title" />
+
+ <TextView
+ android:id="@+id/notification_base_line_two"
+ style="@style/NotificationText"
+ android:textAppearance="@android:style/TextAppearance.StatusBar.EventContent" />
+ </LinearLayout>
+
+</LinearLayout> \ No newline at end of file
diff --git a/res/layout/onebyone_app_widget.xml b/res/layout/onebyone_app_widget.xml
deleted file mode 100644
index b770eec..0000000
--- a/res/layout/onebyone_app_widget.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="@dimen/one_by_one_width"
- android:layout_height="@dimen/one_by_one_height" >
-
- <ImageView
- android:id="@+id/one_by_one_albumart"
- android:layout_width="match_parent"
- android:layout_height="match_parent" />
-
-</FrameLayout> \ No newline at end of file
diff --git a/res/layout/profile_tab.xml b/res/layout/profile_tab.xml
new file mode 100644
index 0000000..27693ba
--- /dev/null
+++ b/res/layout/profile_tab.xml
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<view xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="0dip"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ class="com.andrew.apollo.widgets.CarouselTab" >
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ <com.andrew.apollo.widgets.LayoutSuppressingImageView
+ android:id="@+id/profile_tab_photo"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ android:contentDescription="@null"
+ android:scaleType="centerCrop" />
+
+ <com.andrew.apollo.widgets.SquareImageView
+ android:id="@+id/profile_tab_album_art"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ android:contentDescription="@null"
+ android:scaleType="fitXY"
+ android:visibility="gone" />
+
+ <View
+ android:id="@+id/profile_tab_photo_overlay"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true" />
+
+ <View
+ android:id="@+id/profile_tab_label_background"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/profile_carousel_label_height"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentLeft="true"
+ android:background="@color/transparent_black" />
+
+ <com.andrew.apollo.widgets.theme.Colorstrip
+ android:id="@+id/profile_tab_colorstrip"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/profile_indicator_height"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentLeft="true" />
+
+ <View
+ android:id="@+id/profile_tab_alpha_overlay"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ android:layout_marginBottom="@dimen/profile_carousel_label_height" />
+
+ <TextView
+ android:id="@+id/profile_tab_label"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/profile_carousel_label_height"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentLeft="true"
+ android:gravity="left|center_vertical"
+ android:paddingLeft="@dimen/profile_label_padding"
+ android:paddingRight="@dimen/profile_label_padding"
+ android:singleLine="true"
+ android:textColor="@color/white"
+ android:textSize="@dimen/text_size_large" />
+ </RelativeLayout>
+
+</view> \ No newline at end of file
diff --git a/res/layout/profile_tab_carousel.xml b/res/layout/profile_tab_carousel.xml
new file mode 100644
index 0000000..18c5580
--- /dev/null
+++ b/res/layout/profile_tab_carousel.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<view xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/profile_tab_carousel"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ class="com.andrew.apollo.widgets.ProfileTabCarousel"
+ android:fadingEdge="none"
+ android:scrollbars="none" >
+
+ <LinearLayout
+ android:id="@+id/profile_tab_carousel_tab_and_shadow_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical" >
+
+ <LinearLayout
+ android:id="@+id/profile_tab_carousel_tab_container"
+ android:layout_width="match_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1"
+ android:baselineAligned="false"
+ android:orientation="horizontal" >
+
+ <include
+ android:id="@+id/profile_tab_carousel_tab_one"
+ layout="@layout/profile_tab" />
+
+ <include
+ android:id="@+id/profile_tab_carousel_tab_two"
+ layout="@layout/profile_tab" />
+ </LinearLayout>
+
+ <View
+ android:id="@+id/profile_tab_carousel_shadow"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/profile_photo_shadow_height"
+ android:background="?android:attr/windowContentOverlay" />
+ </LinearLayout>
+
+</view> \ No newline at end of file
diff --git a/res/layout/quick_queue.xml b/res/layout/quick_queue.xml
deleted file mode 100644
index 4ee60f5..0000000
--- a/res/layout/quick_queue.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/quick_queue_holder"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:gravity="center_vertical"
- android:orientation="vertical" >
-
- <GridView
- android:id="@+id/gridview"
- android:layout_width="match_parent"
- android:layout_height="0dp"
- android:layout_weight="1"
- android:cacheColorHint="@color/transparent"
- android:drawSelectorOnTop="false"
- android:listSelector="@color/transparent"
- android:scrollbars="none" />
-
-</LinearLayout> \ No newline at end of file
diff --git a/res/layout/quick_queue_items.xml b/res/layout/quick_queue_items.xml
deleted file mode 100644
index afcb7ee..0000000
--- a/res/layout/quick_queue_items.xml
+++ /dev/null
@@ -1,87 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:paddingBottom="@dimen/status_bar_recents_item_padding"
- android:paddingTop="@dimen/status_bar_recents_item_padding" >
-
- <RelativeLayout
- android:id="@+id/recent_item"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center_horizontal" >
-
- <TextView
- android:id="@+id/queue_track_name"
- android:layout_width="@dimen/status_bar_recents_app_label_width"
- android:layout_height="wrap_content"
- android:layout_alignParentLeft="true"
- android:layout_alignTop="@+id/queue_album_art"
- android:layout_marginLeft="@dimen/status_bar_recents_app_label_left_margin"
- android:ellipsize="marquee"
- android:fadingEdge="horizontal"
- android:fadingEdgeLength="@dimen/status_bar_recents_fading_edge_length"
- android:paddingTop="2dp"
- android:scrollHorizontally="true"
- android:singleLine="true"
- android:textColor="@color/white"
- android:textSize="@dimen/status_bar_recents_app_label_text_size" />
-
- <FrameLayout
- android:id="@+id/app_thumbnail"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_alignParentTop="true"
- android:layout_marginLeft="@dimen/status_bar_recents_thumbnail_left_margin"
- android:layout_toRightOf="@+id/queue_track_name"
- android:background="@drawable/queue_thumbnail_bg"
- android:foreground="@drawable/queue_thumbnail_fg" >
-
- <ImageView
- android:id="@+id/queue_artist_image"
- android:layout_width="@dimen/status_bar_recents_thumbnail_width"
- android:layout_height="@dimen/status_bar_recents_thumbnail_height"
- android:scaleType="fitXY" />
- </FrameLayout>
-
- <View
- android:id="@+id/recents_callout_line"
- android:layout_width="@dimen/status_bar_recents_app_label_width"
- android:layout_height="1dip"
- android:layout_alignParentLeft="true"
- android:layout_below="@+id/queue_track_name"
- android:layout_marginLeft="@dimen/status_bar_recents_app_label_left_margin"
- android:layout_marginRight="3dip"
- android:layout_marginTop="3dip"
- android:layout_toLeftOf="@id/app_thumbnail"
- android:background="@color/queue_callout_line" />
-
- <ImageView
- android:id="@+id/queue_album_art"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginLeft="@dimen/status_bar_recents_app_icon_left_margin"
- android:layout_marginTop="@dimen/status_bar_recents_app_icon_top_margin"
- android:layout_toRightOf="@+id/queue_track_name"
- android:adjustViewBounds="true"
- android:maxHeight="@dimen/status_bar_recents_app_icon_max_height"
- android:maxWidth="@dimen/status_bar_recents_app_icon_max_width"
- android:scaleType="centerInside" />
-
- <TextView
- android:id="@+id/app_description"
- android:layout_width="@dimen/status_bar_recents_app_label_width"
- android:layout_height="wrap_content"
- android:layout_alignParentLeft="true"
- android:layout_below="@id/recents_callout_line"
- android:layout_marginLeft="@dimen/status_bar_recents_app_label_left_margin"
- android:layout_marginTop="3dip"
- android:ellipsize="marquee"
- android:fadingEdge="horizontal"
- android:fadingEdgeLength="@dimen/status_bar_recents_fading_edge_length"
- android:scrollHorizontally="true"
- android:singleLine="true"
- android:textSize="@dimen/status_bar_recents_app_description_text_size" />
- </RelativeLayout>
-
-</FrameLayout> \ No newline at end of file
diff --git a/res/layout/shadow.xml b/res/layout/shadow.xml
deleted file mode 100644
index 37f28bb..0000000
--- a/res/layout/shadow.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/shadow"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:foreground="@drawable/title_bar_shadow"
- android:foregroundGravity="fill_horizontal|top|center" />
diff --git a/res/layout/square_image_view.xml b/res/layout/square_image_view.xml
new file mode 100644
index 0000000..ea958b5
--- /dev/null
+++ b/res/layout/square_image_view.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<com.andrew.apollo.widgets.SquareView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/square_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ <com.andrew.apollo.widgets.SquareImageView
+ android:id="@+id/image"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scaleType="centerCrop" />
+
+</com.andrew.apollo.widgets.SquareView> \ No newline at end of file
diff --git a/res/layout/status_bar.xml b/res/layout/status_bar.xml
deleted file mode 100644
index 34385e7..0000000
--- a/res/layout/status_bar.xml
+++ /dev/null
@@ -1,56 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="horizontal" >
-
- <ImageView
- android:id="@+id/status_bar_album_art"
- android:layout_width="@dimen/status_bar_album_art"
- android:layout_height="@dimen/status_bar_album_art"
- android:gravity="center" />
-
- <ImageView
- android:id="@+id/status_bar_icon"
- android:layout_width="@dimen/status_bar_album_art"
- android:layout_height="@dimen/status_bar_album_art"
- android:background="@drawable/status_bg"
- android:scaleType="center"
- android:src="@drawable/stat_notify_music"
- android:visibility="gone" />
-
- <LinearLayout
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_gravity="center_vertical"
- android:layout_weight="1"
- android:orientation="vertical"
- android:paddingLeft="@dimen/status_bar_button_info_container_padding_left" >
-
- <TextView
- android:id="@+id/status_bar_track_name"
- style="@style/StatusBarText"
- android:textColor="@color/white"
- android:textSize="@dimen/text_size_medium"
- android:textStyle="bold" />
-
- <TextView
- android:id="@+id/status_bar_artist_name"
- style="@style/StatusBarText" />
- </LinearLayout>
-
- <ImageButton
- android:id="@+id/status_bar_play"
- style="@style/StatusBarButton" />
-
- <ImageButton
- android:id="@+id/status_bar_next"
- style="@style/StatusBarButton"
- android:src="@drawable/apollo_holo_dark_next" />
-
- <ImageButton
- android:id="@+id/status_bar_collapse"
- style="@style/StatusBarButton"
- android:src="@drawable/apollo_holo_dark_notifiation_bar_collapse" />
-
-</LinearLayout> \ No newline at end of file
diff --git a/res/layout/tabs.xml b/res/layout/tabs.xml
deleted file mode 100644
index 0bfd77b..0000000
--- a/res/layout/tabs.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<Button xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/tabs"
- style="@style/Tabs" />
diff --git a/res/layout/theme_preview.xml b/res/layout/theme_preview.xml
deleted file mode 100644
index 0563b44..0000000
--- a/res/layout/theme_preview.xml
+++ /dev/null
@@ -1,36 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical" >
-
- <TextView
- android:id="@+id/themeTitle"
- android:layout_width="match_parent"
- android:layout_height="wrap_content" />
-
- <TextView
- android:id="@+id/themeDescription"
- android:layout_width="match_parent"
- android:layout_height="wrap_content" />
-
- <ImageView
- android:id="@+id/themeIcon"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content" />
-
- <Button
- android:id="@+id/themeApply"
- android:onClick="applyTheme"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="@string/apply_theme" />
-
- <Button
- android:id="@+id/themeSearch"
- android:onClick="getThemes"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="@string/get_more_themes" />
-
-</LinearLayout> \ No newline at end of file
diff --git a/res/layout/top_shadow.xml b/res/layout/top_shadow.xml
new file mode 100644
index 0000000..f37f41b
--- /dev/null
+++ b/res/layout/top_shadow.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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. -->
+<View xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/shadow_height"
+ android:background="@drawable/top_shadow" />
diff --git a/res/layout/track_browser.xml b/res/layout/track_browser.xml
deleted file mode 100644
index 4559091..0000000
--- a/res/layout/track_browser.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="wrap_content" >
-
- <include
- android:id="@+id/half"
- layout="@layout/half_and_half" />
-
- <android.support.v4.view.ViewPager
- android:id="@+id/viewPager"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_below="@+id/half" />
-
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="@dimen/bottom_action_bar_height"
- android:layout_alignParentBottom="true" >
-
- <android.support.v4.view.ViewPager
- android:id="@+id/bottomActionBarPager"
- android:layout_width="match_parent"
- android:layout_height="match_parent"/>
- </LinearLayout>
-
-</RelativeLayout> \ No newline at end of file
diff --git a/res/menu/activity_base.xml b/res/menu/activity_base.xml
new file mode 100644
index 0000000..d494081
--- /dev/null
+++ b/res/menu/activity_base.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item
+ android:id="@+id/menu_settings"
+ android:orderInCategory="4"
+ android:showAsAction="never"
+ android:title="@string/menu_settings"/>
+
+</menu> \ No newline at end of file
diff --git a/res/menu/add_to_homescreen.xml b/res/menu/add_to_homescreen.xml
new file mode 100644
index 0000000..b1a2de2
--- /dev/null
+++ b/res/menu/add_to_homescreen.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item
+ android:id="@+id/menu_add_to_homescreen"
+ android:icon="@drawable/ic_action_pinn_to_home"
+ android:orderInCategory="2"
+ android:showAsAction="ifRoom"
+ android:title="@string/menu_add_to_homescreen"/>
+
+</menu> \ No newline at end of file
diff --git a/res/menu/album_song_sort_by.xml b/res/menu/album_song_sort_by.xml
new file mode 100644
index 0000000..9300f86
--- /dev/null
+++ b/res/menu/album_song_sort_by.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item
+ android:id="@+id/menu_sort_by"
+ android:showAsAction="never"
+ android:title="@string/menu_sort_by">
+ <menu>
+ <item
+ android:id="@+id/menu_sort_by_az"
+ android:title="@string/sort_order_entry_az"/>
+ <item
+ android:id="@+id/menu_sort_by_za"
+ android:title="@string/sort_order_entry_za"/>
+ <item
+ android:id="@+id/menu_sort_by_duration"
+ android:title="@string/sort_order_entry_duration"/>
+ <item
+ android:id="@+id/menu_sort_by_track_list"
+ android:title="@string/sort_order_entry_track_list"/>
+ </menu>
+ </item>
+
+</menu> \ No newline at end of file
diff --git a/res/menu/album_sort_by.xml b/res/menu/album_sort_by.xml
new file mode 100644
index 0000000..dd4bef9
--- /dev/null
+++ b/res/menu/album_sort_by.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item
+ android:id="@+id/menu_sort_by"
+ android:showAsAction="never"
+ android:title="@string/menu_sort_by">
+ <menu>
+ <item
+ android:id="@+id/menu_sort_by_az"
+ android:title="@string/sort_order_entry_az"/>
+ <item
+ android:id="@+id/menu_sort_by_za"
+ android:title="@string/sort_order_entry_za"/>
+ <item
+ android:id="@+id/menu_sort_by_year"
+ android:title="@string/sort_order_entry_year"/>
+ <item
+ android:id="@+id/menu_sort_by_artist"
+ android:title="@string/sort_order_entry_artist"/>
+ <item
+ android:id="@+id/menu_sort_by_number_of_songs"
+ android:title="@string/sort_order_entry_number_of_songs"/>
+ </menu>
+ </item>
+
+</menu> \ No newline at end of file
diff --git a/res/menu/artist_album_sort_by.xml b/res/menu/artist_album_sort_by.xml
new file mode 100644
index 0000000..9be6ed8
--- /dev/null
+++ b/res/menu/artist_album_sort_by.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item
+ android:id="@+id/menu_sort_by"
+ android:showAsAction="never"
+ android:title="@string/menu_sort_by">
+ <menu>
+ <item
+ android:id="@+id/menu_sort_by_az"
+ android:title="@string/sort_order_entry_az"/>
+ <item
+ android:id="@+id/menu_sort_by_za"
+ android:title="@string/sort_order_entry_za"/>
+ <item
+ android:id="@+id/menu_sort_by_year"
+ android:title="@string/sort_order_entry_year"/>
+ <item
+ android:id="@+id/menu_sort_by_number_of_songs"
+ android:title="@string/sort_order_entry_number_of_songs"/>
+ </menu>
+ </item>
+
+</menu> \ No newline at end of file
diff --git a/res/menu/artist_song_sort_by.xml b/res/menu/artist_song_sort_by.xml
new file mode 100644
index 0000000..67d1a35
--- /dev/null
+++ b/res/menu/artist_song_sort_by.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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. -->
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item
+ android:id="@+id/menu_sort_by"
+ android:showAsAction="never"
+ android:title="@string/menu_sort_by">
+ <menu>
+ <item
+ android:id="@+id/menu_sort_by_az"
+ android:title="@string/sort_order_entry_az"/>
+ <item
+ android:id="@+id/menu_sort_by_za"
+ android:title="@string/sort_order_entry_za"/>
+ <item
+ android:id="@+id/menu_sort_by_year"
+ android:title="@string/sort_order_entry_year"/>
+ <item
+ android:id="@+id/menu_sort_by_album"
+ android:title="@string/sort_order_entry_album"/>
+ <item
+ android:id="@+id/menu_sort_by_duration"
+ android:title="@string/sort_order_entry_duration"/>
+ <item
+ android:id="@+id/menu_sort_by_date_added"
+ android:title="@string/sort_order_entry_date_added"/>
+ </menu>
+ </item>
+
+</menu> \ No newline at end of file
diff --git a/res/menu/artist_sort_by.xml b/res/menu/artist_sort_by.xml
new file mode 100644
index 0000000..d87923c
--- /dev/null
+++ b/res/menu/artist_sort_by.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item
+ android:id="@+id/menu_sort_by"
+ android:showAsAction="never"
+ android:title="@string/menu_sort_by">
+ <menu>
+ <item
+ android:id="@+id/menu_sort_by_az"
+ android:title="@string/sort_order_entry_az"/>
+ <item
+ android:id="@+id/menu_sort_by_za"
+ android:title="@string/sort_order_entry_za"/>
+ <item
+ android:id="@+id/menu_sort_by_number_of_songs"
+ android:title="@string/sort_order_entry_number_of_songs"/>
+ <item
+ android:id="@+id/menu_sort_by_number_of_albums"
+ android:title="@string/sort_order_entry_number_of_albums"/>
+ </menu>
+ </item>
+
+</menu> \ No newline at end of file
diff --git a/res/menu/audio_player.xml b/res/menu/audio_player.xml
new file mode 100644
index 0000000..8625074
--- /dev/null
+++ b/res/menu/audio_player.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item
+ android:id="@+id/menu_audio_player_share"
+ android:showAsAction="never"
+ android:title="@string/menu_share"/>
+ <item
+ android:id="@+id/menu_audio_player_equalizer"
+ android:showAsAction="never"
+ android:title="@string/menu_equalizer"/>
+ <item
+ android:id="@+id/menu_download_lyrics"
+ android:showAsAction="never"
+ android:title="@string/menu_download_lyrics"/>
+ <item
+ android:id="@+id/menu_audio_player_ringtone"
+ android:showAsAction="never"
+ android:title="@string/context_menu_use_as_ringtone"/>
+
+</menu> \ No newline at end of file
diff --git a/res/menu/favorite.xml b/res/menu/favorite.xml
new file mode 100644
index 0000000..baff20f
--- /dev/null
+++ b/res/menu/favorite.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item
+ android:id="@+id/menu_favorite"
+ android:icon="@drawable/ic_action_favorite"
+ android:orderInCategory="2"
+ android:showAsAction="ifRoom"
+ android:title="@string/add_to_favorites"/>
+
+</menu> \ No newline at end of file
diff --git a/res/menu/overflow_library.xml b/res/menu/overflow_library.xml
deleted file mode 100644
index 7243c8a..0000000
--- a/res/menu/overflow_library.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<menu xmlns:android="http://schemas.android.com/apk/res/android" >
-
- <item
- android:id="@+id/settings"
- android:title="@string/settings"/>
- <item
- android:id="@+id/equalizer"
- android:title="@string/eqalizer"/>
- <item
- android:id="@+id/shuffle_all"
- android:title="@string/shuffle_all"/>
- <!--
- <item
- android:id="@+id/help"
- android:title="@string/help"/>
- <item
- android:id="@+id/fetch_artwork"
- android:title="Fetch album art"/>
- <item
- android:id="@+id/fetch_artist_images"
- android:title="Fetch artist images"/>
- -->
-
-</menu> \ No newline at end of file
diff --git a/res/menu/overflow_now_playing.xml b/res/menu/overflow_now_playing.xml
deleted file mode 100644
index 554c6bc..0000000
--- a/res/menu/overflow_now_playing.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<menu xmlns:android="http://schemas.android.com/apk/res/android" >
-
- <item
- android:id="@+id/add_to_playlist"
- android:showAsAction="never"
- android:title="@string/add_to_playlist"/>
- <item
- android:id="@+id/eq"
- android:showAsAction="never"
- android:title="@string/eqalizer"/>
- <item
- android:id="@+id/play_store"
- android:showAsAction="never"
- android:title="@string/play_store"/>
- <item
- android:id="@+id/share"
- android:showAsAction="never"
- android:title="@string/share"/>
- <item
- android:id="@+id/settings"
- android:showAsAction="never"
- android:title="@string/settings"/>
-
-</menu> \ No newline at end of file
diff --git a/res/menu/queue.xml b/res/menu/queue.xml
new file mode 100644
index 0000000..4836493
--- /dev/null
+++ b/res/menu/queue.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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. -->
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item
+ android:id="@+id/menu_save_queue"
+ android:showAsAction="never"
+ android:title="@string/menu_save_queue"/>
+ <item
+ android:id="@+id/menu_clear_queue"
+ android:showAsAction="never"
+ android:title="@string/menu_clear_queue"/>
+
+</menu> \ No newline at end of file
diff --git a/res/menu/search.xml b/res/menu/search.xml
new file mode 100644
index 0000000..838e746
--- /dev/null
+++ b/res/menu/search.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item
+ android:id="@+id/menu_search"
+ android:actionViewClass="com.actionbarsherlock.widget.SearchView"
+ android:icon="@drawable/ic_action_search"
+ android:orderInCategory="1"
+ android:showAsAction="ifRoom|collapseActionView"
+ android:title="@string/menu_search"/>
+
+</menu> \ No newline at end of file
diff --git a/res/menu/shuffle.xml b/res/menu/shuffle.xml
new file mode 100644
index 0000000..cef5f75
--- /dev/null
+++ b/res/menu/shuffle.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item
+ android:id="@+id/menu_shuffle"
+ android:showAsAction="never"
+ android:title="@string/menu_shuffle"/>
+
+</menu> \ No newline at end of file
diff --git a/res/menu/song_sort_by.xml b/res/menu/song_sort_by.xml
new file mode 100644
index 0000000..32ba679
--- /dev/null
+++ b/res/menu/song_sort_by.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item
+ android:id="@+id/menu_sort_by"
+ android:showAsAction="never"
+ android:title="@string/menu_sort_by">
+ <menu>
+ <item
+ android:id="@+id/menu_sort_by_az"
+ android:title="@string/sort_order_entry_az"/>
+ <item
+ android:id="@+id/menu_sort_by_za"
+ android:title="@string/sort_order_entry_za"/>
+ <item
+ android:id="@+id/menu_sort_by_year"
+ android:title="@string/sort_order_entry_year"/>
+ <item
+ android:id="@+id/menu_sort_by_artist"
+ android:title="@string/sort_order_entry_artist"/>
+ <item
+ android:id="@+id/menu_sort_by_album"
+ android:title="@string/sort_order_entry_album"/>
+ <item
+ android:id="@+id/menu_sort_by_duration"
+ android:title="@string/sort_order_entry_duration"/>
+ </menu>
+ </item>
+
+</menu> \ No newline at end of file
diff --git a/res/menu/theme_shop.xml b/res/menu/theme_shop.xml
new file mode 100644
index 0000000..998a77c
--- /dev/null
+++ b/res/menu/theme_shop.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item
+ android:id="@+id/menu_shop"
+ android:icon="@drawable/ic_action_shop"
+ android:showAsAction="always"
+ android:title="@string/menu_shop"/>
+
+</menu> \ No newline at end of file
diff --git a/res/menu/view_as.xml b/res/menu/view_as.xml
new file mode 100644
index 0000000..0d24860
--- /dev/null
+++ b/res/menu/view_as.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item
+ android:id="@+id/menu_view_as"
+ android:showAsAction="never"
+ android:title="@string/menu_view_as">
+ <menu>
+ <item
+ android:id="@+id/menu_view_as_simple"
+ android:title="@string/menu_simple"/>
+ <item
+ android:id="@+id/menu_view_as_detailed"
+ android:title="@string/menu_detailed"/>
+ <item
+ android:id="@+id/menu_view_as_grid"
+ android:title="@string/menu_grid"/>
+ </menu>
+ </item>
+
+</menu> \ No newline at end of file
diff --git a/res/values-hdpi/config.xml b/res/values-hdpi/config.xml
deleted file mode 100644
index b727fb7..0000000
--- a/res/values-hdpi/config.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources>
-
- <!-- ListView album art size -->
- <integer name="listview_album_art">100</integer>
-
-</resources> \ No newline at end of file
diff --git a/res/values-hdpi/dimens.xml b/res/values-hdpi/dimens.xml
deleted file mode 100644
index 4aae5b1..0000000
--- a/res/values-hdpi/dimens.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources>
-
- <!-- Half and half layout -->
- <dimen name="half_and_half_image_height">130dp</dimen>
-
- <!-- GridView items -->
- <dimen name="gridview_image_height">148dp</dimen>
- <dimen name="gridview_item_ccontainer_height">54dp</dimen>
-
-</resources> \ No newline at end of file
diff --git a/res/values-sw600dp/dimens.xml b/res/values-sw600dp/dimens.xml
new file mode 100644
index 0000000..843d9f3
--- /dev/null
+++ b/res/values-sw600dp/dimens.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<resources>
+
+ <!-- List item detailed height -->
+ <dimen name="list_item_detailed_height">164.0dip</dimen>
+
+</resources> \ No newline at end of file
diff --git a/res/values-v11/config.xml b/res/values-v11/config.xml
new file mode 100644
index 0000000..ec37e0f
--- /dev/null
+++ b/res/values-v11/config.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<resources>
+
+ <!-- Runnong Honeycomb or above -->
+ <bool name="has_honeycomb">true</bool>
+
+</resources> \ No newline at end of file
diff --git a/res/values-v11/dimens.xml b/res/values-v11/dimens.xml
new file mode 100644
index 0000000..9637cfd
--- /dev/null
+++ b/res/values-v11/dimens.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<resources>
+
+ <dimen name="app_widget_padding">0.0dip</dimen>
+
+ <!-- List view fast scroll padding right -->
+ <dimen name="fast_scroll_padding_right">32.0dip</dimen>
+
+</resources> \ No newline at end of file
diff --git a/res/values-xhdpi/dimens.xml b/res/values-xhdpi/dimens.xml
deleted file mode 100644
index af3eaf7..0000000
--- a/res/values-xhdpi/dimens.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources>
-
- <!-- Half and half layout -->
- <dimen name="half_and_half_image_height">150dp</dimen>
-
- <!-- GridView items -->
- <dimen name="gridview_image_height">180dp</dimen>
- <dimen name="gridview_item_ccontainer_height">64dp</dimen>
-
-</resources> \ No newline at end of file
diff --git a/res/values/arrays.xml b/res/values/arrays.xml
new file mode 100644
index 0000000..bbd1cbf
--- /dev/null
+++ b/res/values/arrays.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<resources>
+
+ <!-- Main TitlePageIndicator titles -->
+ <string-array name="page_titles">
+ <item>@string/page_playlists</item>
+ <item>@string/page_recent</item>
+ <item>@string/page_artists</item>
+ <item>@string/page_albums</item>
+ <item>@string/page_songs</item>
+ <item>@string/page_genres</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 0000000..33eb19e
--- /dev/null
+++ b/res/values/attrs.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<resources>
+
+ <declare-styleable name="ThemeableTextView">
+
+ <!-- Used to set the themeable resource name for the text view -->
+ <attr name="themeResource" format="string" />
+ </declare-styleable>
+
+</resources> \ No newline at end of file
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 959d021..02bb26e 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -1,13 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
<resources>
- <!-- A transparent black -->
- <color name="transparent_black">#99000000</color>
-
- <!-- A semi-transparent dark Holo shade of blue -->
- <color name="holo_blue_dark">#ff0099cc</color>
-
- <!-- Transparent -->
+ <!-- See through -->
<color name="transparent">#00000000</color>
<!-- Black -->
@@ -16,7 +25,37 @@
<!-- White -->
<color name="white">#ffffffff</color>
- <!-- Quick Queue line seperator -->
- <color name="queue_callout_line">#99ffffff</color>
+ <!-- Transparent black -->
+ <color name="transparent_black">#99000000</color>
+
+ <!-- Transparent white -->
+ <color name="transparent_white">#ffcccccc</color>
+
+ <!-- Dark transparent color -->
+ <color name="list_item_background">#c1000000</color>
+
+ <!-- A darkish color used for the action bar -->
+ <color name="action_bar_color">#ff0d0d0d</color>
+
+ <!-- A light, Holo shade of blue -->
+ <color name="holo_blue_light">#ff33b5e5</color>
+
+ <!-- A light, transparent Holo shade of blue -->
+ <color name="holo_blue_light_transparent">#9933b5e5</color>
+
+ <!-- A light Holo shade of green -->
+ <color name="holo_green_light">#ff99cc00</color>
+
+ <!-- A light Holo shade of red -->
+ <color name="holo_red_light">#ffff4444</color>
+
+ <!-- A Holo shade of purple -->
+ <color name="holo_purple">#ffaa66cc</color>
+
+ <!-- A light Holo shade of orange -->
+ <color name="holo_orange_light">#ffffbb33</color>
+
+ <!-- A dark Holo shade of orange -->
+ <color name="holo_orange_dark">#ffff8800</color>
</resources> \ No newline at end of file
diff --git a/res/values/config.xml b/res/values/config.xml
index c3e8295..388765b 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -1,20 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
<resources>
- <!-- ViewPager margin width -->
- <integer name="viewpager_margin_width">30</integer>
+ <!-- Configures hardware acceleration -->
+ <bool name="config_hardwareAccelerated">true</bool>
- <!-- ListView album art size -->
- <integer name="listview_album_art">133</integer>
+ <!-- Configures a larger heap size -->
+ <bool name="config_largeHeap">true</bool>
- <!-- Now playing indicator animation time -->
- <integer name="peak">200</integer>
-
- <!-- Number of GridView coulumns -->
- <integer name="gridview_columns">2</integer>
-
- <!-- ListView padding when header is applied -->
- <integer name="listview_padding_left">16</integer>
- <integer name="listview_padding_right">32</integer>
+ <!-- Running Honeycomb or above -->
+ <bool name="has_honeycomb">false</bool>
</resources> \ No newline at end of file
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index aeacf75..69f0059 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -1,124 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
<resources>
<!-- Text sizes -->
- <dimen name="text_size_extra_micro">10sp</dimen>
- <dimen name="text_size_micro">12sp</dimen>
- <dimen name="text_size_small">14sp</dimen>
- <dimen name="text_size_medium">16sp</dimen>
- <dimen name="text_size_large">18sp</dimen>
-
- <!-- Tabs -->
- <dimen name="tab_padding_top_bottom">15dp</dimen>
- <dimen name="tab_padding_left_right">40dp</dimen>
-
- <!-- Bottom ActionBar -->
- <dimen name="bottom_action_bar_height">48dp</dimen>
- <dimen name="bottom_action_bar_item_width">56dp</dimen>
- <dimen name="bottom_action_bar_item_padding_left_right">12dp</dimen>
- <dimen name="bottom_action_bar_album_art_width_height">30dp</dimen>
- <dimen name="bottom_action_bar_divider_height">2dp</dimen>
- <dimen name="bottom_action_bar_info_padding_left">5dp</dimen>
-
- <!-- ViewPager margin stroke width -->
- <dimen name="viewpager_margin_stroke_width">0.5dp</dimen>
-
- <!-- FastScroll padding -->
- <dimen name="fast_scroll_padding_right">32dp</dimen>
-
- <!-- List separator -->
- <dimen name="list_separator_padding_left_right">8dp</dimen>
- <dimen name="list_separator_min_height">32dp</dimen>
- <dimen name="list_separator_container_padding_left">16dp</dimen>
-
- <!-- ListView items -->
- <dimen name="listview_item_height">64dp</dimen>
- <dimen name="listview_items_padding_left_top">9dp</dimen>
- <dimen name="listview_items_padding_right">85dp</dimen>
- <dimen name="listview_peak_meter_one_padding_right">80dp</dimen>
- <dimen name="listview_peak_meter_two_padding_right">70dp</dimen>
- <dimen name="listview_album_art">64dp</dimen>
-
- <!-- Quick Context Menu -->
- <dimen name="quick_context_padding_right">4dp</dimen>
- <dimen name="quick_context_line_height">30dp</dimen>
- <dimen name="quick_context_line_width">1dp</dimen>
- <dimen name="quick_context_margin_right">5dp</dimen>
-
- <!-- Nofication bar button -->
- <dimen name="status_bar_button_width_height">48dp</dimen>
- <dimen name="status_bar_album_art">64dp</dimen>
- <dimen name="status_bar_button_info_container_padding_left">11dp</dimen>
-
- <!-- Colorstrip -->
- <dimen name="colorstrip_height">4dp</dimen>
-
- <!-- Half and half layout -->
- <dimen name="half_and_half_text_padding">5dp</dimen>
- <dimen name="half_and_half_image_height">150dp</dimen>
- <dimen name="half_and_half_container_padding">3dp</dimen>
-
- <!-- ContextMenu header text padding -->
- <dimen name="header_text_padding">5dp</dimen>
- <dimen name="header_text_padding_left">15dp</dimen>
-
- <!-- GridView items -->
- <dimen name="gridview_image_width">180dp</dimen>
- <dimen name="gridview_image_height">180dp</dimen>
- <dimen name="gridview_item_ccontainer_height">64dp</dimen>
- <dimen name="gridview_item_ccontainer_padding_left">8dp</dimen>
- <dimen name="gridview_item_ccontainer_padding_right">80dp</dimen>
- <dimen name="peak_meter_one_padding_right">15dp</dimen>
- <dimen name="peak_meter_two_padding_right">5dp</dimen>
- <dimen name="peak_meter_padding_bottom">10dp</dimen>
- <dimen name="peak_meter_padding_top">8dp</dimen>
+ <dimen name="text_size_extra_micro">10.0sp</dimen>
+ <dimen name="text_size_micro">12.0sp</dimen>
+ <dimen name="text_size_small">14.0sp</dimen>
+ <dimen name="text_size_medium">16.0sp</dimen>
+ <dimen name="text_size_large">18.0sp</dimen>
+ <dimen name="text_size_x_large">24.0sp</dimen>
+ <dimen name="text_size_dayum">36.0sp</dimen>
+
+ <!-- List and grid view padding -->
+ <dimen name="list_preferred_item_padding">10.0dip</dimen>
+ <!-- List view fast scroll padding left -->
+ <dimen name="fast_scroll_padding_left">8.0dip</dimen>
+ <!-- List view fast scroll padding right -->
+ <dimen name="fast_scroll_padding_right">0.0dip</dimen>
+ <!-- grid view vertical and horizontal spacing -->
+ <dimen name="grid_item_spacing">4.0dip</dimen>
+ <!-- List item detailed height -->
+ <dimen name="list_item_detailed_height">120.0dip</dimen>
+ <!-- Top margin of "line_two" -->
+ <dimen name="list_item_line_two_margin_top">8.0dip</dimen>
+ <!-- Left padding in the grid text -->
+ <dimen name="grid_item_padding_left">8.0dip</dimen>
+ <!-- Grid and list item normal height -->
+ <dimen name="item_normal_height">64.0dip</dimen>
+
+ <!-- Bottom Action Bar -->
+ <dimen name="bottom_action_bar_height">48.0dip</dimen>
+ <dimen name="bottom_action_bar_item_width">56.0dip</dimen>
+ <dimen name="bottom_action_bar_item_padding_left">12.0dip</dimen>
+ <dimen name="bottom_action_bar_item_padding_right">12.0dip</dimen>
+ <dimen name="bottom_action_bar_album_art_width">30.0dip</dimen>
+ <dimen name="bottom_action_bar_album_art_height">30.0dip</dimen>
+ <dimen name="bottom_action_bar_info_padding_left">5.0dip</dimen>
+ <dimen name="bottom_action_bar_button_container_width">165.0dip</dimen>
+
+ <!-- Notification template -->
+ <dimen name="notification_big_icon_height">64.0dip</dimen>
+ <dimen name="notification_big_icon_width">64.0dip</dimen>
+ <dimen name="notification_info_container_padding_left">8.0dip</dimen>
+ <dimen name="notification_info_container_padding_bottom">4.0dip</dimen>
+ <dimen name="notification_action_padding">8.0dip</dimen>
+ <dimen name="notification_action_height">48.0dip</dimen>
+ <dimen name="notification_action_width">48.0dip</dimen>
+
+ <!-- Notification template expanded -->
+ <dimen name="notification_expanded_height">128.0dip</dimen>
+ <dimen name="notification_expanded_buttons_divider_padding">12.0dip</dimen>
+ <dimen name="notification_expanded_button_height">48.0dip</dimen>
+ <dimen name="notification_expanded_button_padding">10.0dip</dimen>
+ <dimen name="notification_expanded_content_padding_top">8.0dip</dimen>
+ <dimen name="notification_expanded_collapse_padding">8.0dip</dimen>
+
+ <!-- Height of the shadow asset under the photo -->
+ <dimen name="profile_photo_shadow_height">10.0dip</dimen>
+ <!-- Height of the text label in the carousel -->
+ <dimen name="profile_carousel_label_height">45.0dip</dimen>
+ <dimen name="profile_indicator_height">5.0dip</dimen>
+ <dimen name="profile_label_padding">16.0dip</dimen>
<!-- Audio player -->
- <dimen name="audio_player_info_container_padding">16dp</dimen>
- <dimen name="audio_player_artwork_padding">20dp</dimen>
- <dimen name="audio_player_controls_height">56dp</dimen>
- <dimen name="audio_player_seek_bar_padding">10dp</dimen>
- <dimen name="audio_player_button_container_padding">2dp</dimen>
-
- <!-- Recent Applications parameters -->
- <!-- How far the thumbnail for a recent app appears from left edge -->
- <dimen name="status_bar_recents_thumbnail_left_margin">20dp</dimen>
- <!-- Width of application label text -->
- <dimen name="status_bar_recents_app_label_width">88dp</dimen>
- <!-- Left margin of application label text -->
- <dimen name="status_bar_recents_app_label_left_margin">0dp</dimen>
- <!-- Padding between recents items -->
- <dimen name="status_bar_recents_item_padding">0dp</dimen>
- <!-- Where to place the app icon over the thumbnail -->
- <dimen name="status_bar_recents_app_icon_left_margin">0dp</dimen>
- <dimen name="status_bar_recents_app_icon_top_margin">8dp</dimen>
- <!-- Recent Applications parameters -->
- <!-- Upper width limit for application icon -->
- <dimen name="status_bar_recents_app_icon_max_width">48dp</dimen>
- <!-- Upper height limit for application icon -->
- <dimen name="status_bar_recents_app_icon_max_height">48dp</dimen>
+ <dimen name="audio_player_header_height">60.0dip</dimen>
+ <dimen name="audio_player_header_padding_left">16.0dip</dimen>
+ <dimen name="audio_player_header_padding_right">16.0dip</dimen>
+ <dimen name="audio_player_switch_padding">10.0dip</dimen>
+ <dimen name="audio_player_time_width">52.0dip</dimen>
+ <dimen name="audio_player_seek_bar_margin_bottom">2.0dip</dimen>
+ <dimen name="audio_player_controls_end_button_width">50.0dip</dimen>
+ <dimen name="audio_player_controls_end_button_height">50.0dip</dimen>
+ <dimen name="audio_player_controls_main_button_width">58.0dip</dimen>
+ <dimen name="audio_player_controls_main_button_height">58.0dip</dimen>
+
+ <!-- App Widgets -->
+ <dimen name="app_widget_large_min_width">250.0dip</dimen>
+ <dimen name="app_widget_large_min_height">128.0dip</dimen>
+ <dimen name="app_widget_scrollable_min_height">180.0dip</dimen>
+ <dimen name="app_widget_scrollable_min_resize_height">110.0dip</dimen>
+ <dimen name="app_widget_large_alternate_artwork_size">80.0dip</dimen>
+ <dimen name="app_widget_small_min_width">250.0dip</dimen>
+ <dimen name="app_widget_small_min_height">40.0dip</dimen>
+ <dimen name="app_widget_small_info_container_padding_left">8.0dip</dimen>
+ <dimen name="app_widget_small_info_container_padding_right">8.0dip</dimen>
+ <dimen name="app_widget_small_info_container_padding_top">5.0dip</dimen>
+ <dimen name="app_widget_small_artwork_size">48.0dip</dimen>
+ <dimen name="app_widget_small_button_padding">8.0dip</dimen>
+ <dimen name="app_widget_small_button_height">48.0dip</dimen>
+ <dimen name="app_widget_recents_action_bar_height">48.0dip</dimen>
+ <dimen name="app_widget_recents_action_bar_item_padding">8.0dip</dimen>
+ <dimen name="app_widget_tiny_height">70.0dip</dimen>
+ <dimen name="app_widget_tiny_width">70.0dip</dimen>
+ <dimen name="app_widget_padding">10.0dip</dimen>
+
+ <!-- Shadow height -->
+ <dimen name="shadow_height">5.0dip</dimen>
- <!-- Size of application thumbnail -->
- <dimen name="status_bar_recents_thumbnail_width">164dp</dimen>
- <dimen name="status_bar_recents_thumbnail_height">145dp</dimen>
-
- <!-- Size of application label text -->
- <dimen name="status_bar_recents_app_label_text_size">14dp</dimen>
- <!-- Size of application description text -->
- <dimen name="status_bar_recents_app_description_text_size">14dp</dimen>
- <!-- Size of fading edge for scroll effect -->
- <dimen name="status_bar_recents_fading_edge_length">20dp</dimen>
+ <!-- Colorstrip -->
+ <dimen name="colorstrip_height">2.0dip</dimen>
- <!-- AppWidgdt 1x1 -->
- <dimen name="one_by_one_height">62dp</dimen>
- <dimen name="one_by_one_width">72dp</dimen>
+ <!-- Drag and drop -->
+ <dimen name="drag_and_drop_handle">26.0dip</dimen>
- <!-- AppWidgdt 4x1 -->
- <dimen name="four_by_one_album_art_width">90dp</dimen>
-
- <!-- AppWidget 4x2 -->
- <dimen name="four_by_two_height">180dp</dimen>
- <dimen name="four_by_two_control_height">55dp</dimen>
- <dimen name="four_by_two_album_art_width">135dp</dimen>
+ <!-- Color scheme dialog -->
+ <dimen name="color_scheme_dialog_row_padding">8.0dip</dimen>
</resources> \ No newline at end of file
diff --git a/res/values/donottranslate.xml b/res/values/donottranslate.xml
new file mode 100644
index 0000000..6836d75
--- /dev/null
+++ b/res/values/donottranslate.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+ <!-- Do not translate. Duration format for duration < 1 hour -->
+ <string name="durationformatshort"><xliff:g id="format">%2$d:%5$02d</xliff:g></string>
+ <!-- Do not translate. Duration format for duration >= 1 hour -->
+ <string name="durationformatlong"><xliff:g id="format">%1$d:%3$02d:%5$02d</xliff:g></string>
+
+</resources> \ No newline at end of file
diff --git a/res/values/fractions.xml b/res/values/fractions.xml
new file mode 100644
index 0000000..a1e37fa
--- /dev/null
+++ b/res/values/fractions.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<resources>
+
+ <item name="tab_width_screen_percentage" type="fraction">75.0%</item>
+ <item name="tab_height_screen_percentage" type="fraction">42.0%</item>
+
+</resources> \ No newline at end of file
diff --git a/res/values/plurals.xml b/res/values/plurals.xml
new file mode 100644
index 0000000..be184b5
--- /dev/null
+++ b/res/values/plurals.xml
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+ <!-- Used to indicate the number of artists -->
+ <plurals name="Nartists">
+
+ <!-- Only one artist -->
+ <item quantity="one">1 artist</item>
+ <!-- More than one artist -->
+ <item quantity="other"><xliff:g id="count">%d</xliff:g> artists</item>
+ </plurals>
+
+ <!-- Used to indicate the number of albums for an artist -->
+ <plurals name="Nalbums">
+
+ <!-- Only one album -->
+ <item quantity="one">1 album</item>
+ <!-- More than one album -->
+ <item quantity="other"><xliff:g id="count">%d</xliff:g> albums</item>
+ </plurals>
+
+ <!-- Used to indicate the number of songs for an album -->
+ <plurals name="Nsongs">
+
+ <!-- Only one song -->
+ <item quantity="one">1 song</item>
+ <item quantity="other"><xliff:g id="count">%d</xliff:g> songs</item>
+ </plurals>
+
+ <!-- Used to indicate the number of genres -->
+ <plurals name="Ngenres">
+
+ <!-- Only one genre -->
+ <item quantity="one">1 genre</item>
+ <!-- More than one genre -->
+ <item quantity="other"><xliff:g id="count">%d</xliff:g> genres</item>
+ </plurals>
+
+ <!-- Toasts after adding song(s) to playlists -->
+ <plurals name="NNNtrackstoplaylist">
+
+ <!-- message shown when one song was added -->
+ <item quantity="one">1 song added to playlist.</item>
+ <!-- message shown when zero or more than one song was added -->
+ <item quantity="other"><xliff:g id="number" example="27">%d</xliff:g> songs added to playlist.</item>
+ </plurals>
+
+ <!-- Toasts after adding song(s) to queue -->
+ <plurals name="NNNtrackstoqueue">
+
+ <!-- message shown when one song was added -->
+ <item quantity="one">1 song added to the queue.</item>
+ <!-- message shown when zero or more than one song was added -->
+ <item quantity="other"><xliff:g id="number" example="27">%d</xliff:g> songs added to the queue.</item>
+ </plurals>
+
+ <!-- Toasts after adding song(s) to the favorites list -->
+ <plurals name="NNNtrackstofavorites">
+
+ <!-- message shown when one song was added -->
+ <item quantity="one">1 song added to Favorites.</item>
+ <!-- message shown when zero or more than one song was added -->
+ <item quantity="other"><xliff:g id="number" example="27">%d</xliff:g> songs added to Favorites.</item>
+ </plurals>
+
+ <!-- Toast confirming that song(s) was/were deleted. -->
+ <plurals name="NNNtracksdeleted">
+
+ <!-- delete confirmation message for 1 song -->
+ <item quantity="one">1 song was deleted.</item>
+ <!-- delete confirmation message for 0 or more than 1 songs -->
+ <item quantity="other"><xliff:g id="songs_to_delete">%d</xliff:g> songs were deleted.</item>
+ </plurals>
+
+</resources> \ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 525556d..5e32581 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1,127 +1,190 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-
- <!-- App name -->
- <string name="app_name">Apollo</string>
-
- <!-- Content descriptions for the Bottom Action Bar -->
- <string name="cd_favorite">Make this a favorite song</string>
- <string name="cd_search">Search through your music</string>
- <string name="cd_overflow">View more options</string>
- <string name="cd_bottom_action_bar_album_art">Album art for this song</string>
- <string name="cd_repeat">Repeat one or all</string>
- <string name="cd_previous">Skip backwards</string>
- <string name="cd_play">Play and pause</string>
- <string name="cd_next">Skip forwards</string>
- <string name="cd_shuffle">Shuffle tracks</string>
-
- <!-- AudioPlayer title -->
- <string name="nowplaying">Now Playing</string>
-
- <!-- Used to indicate the number of albums for an artist -->
- <plurals name="Nalbums">
-
- <!-- Number of albums is one -->
- <item quantity="one">1 album</item>
- <!-- Number of albums is more than one -->
- <item quantity="other"><xliff:g id="count">%d</xliff:g> albums</item>
- </plurals>
-
- <!-- Used to indicate the number of songs for an album -->
-
- <plurals name="Nsongs">
-
- <!-- Number of songs is one -->
- <item quantity="one">1 song</item>
- <item quantity="other"><xliff:g id="count">%d</xliff:g> songs</item>
- </plurals>
-
- <!-- Toasts after adding song(s) to playlists -->
- <plurals name="NNNtrackstoplaylist">
-
- <!-- message shown when one song was added -->
- <item quantity="one">1 song added to playlist</item>
- <!-- message shown when zero or more than one song was added -->
- <item quantity="other"><xliff:g id="number" example="27">%d</xliff:g> songs added to playlis.</item>
- </plurals>
-
- <!-- Headers -->
- <string name="album_header">ALBUM LIST</string>
- <string name="track_header">TRACK LIST</string>
-
- <!-- Options MenuItems -->
- <string name="settings">Settings</string>
- <string name="shuffle_all">Shuffle all</string>
- <string name="share">Share</string>
- <string name="play_store">Play Store</string>
-
- <!-- Set track as ringtone -->
- <string name="set_as_ringtone">\"<xliff:g id="name" example="Alarm Bell">%s</xliff:g>\" set as ringtone</string>
-
- <!-- Do not translate. Duration format for duration < 1 hour -->
- <string name="durationformatshort" translatable="false"><xliff:g id="format">%2$d:%5$02d</xliff:g></string>
- <!-- Do not translate. Duration format for duration >= 1 hour -->
- <string name="durationformatlong" translatable="false"><xliff:g id="format">%1$d:%3$02d:%5$02d</xliff:g></string>
-
- <!-- Transient popup message shown after renaming a playlist -->
- <string name="rename_playlist">Rename playlist</string>
-
- <!-- Shuffle and repeat messages -->
- <string name="repeat_one">Repeat one</string>
- <string name="repeat_all">Repeat all</string>
- <string name="repeat_off">Repeat off</string>
- <string name="shuffle_off">Shuffle off</string>
- <string name="shuffle_on">Shuffle on</string>
-
- <!-- Share Intent -->
- <string name="now_listening_to">Now listening to:</string>
- <string name="by">by</string>
- <string name="share_track_using">Share track using</string>
-
- <!-- ContextMenu items -->
- <string name="play_all">Play all</string>
- <string name="add_to_playlist">Add to playlist</string>
- <string name="use_as_ringtone">Use as ringtone</string>
- <string name="delete_playlist">Delete playlist</string>
- <string name="search">Search</string>
- <string name="remove">Remove from playlist</string>
-
- <!-- App Widgets -->
- <string name="apollo_1x1">Apollo (1x1)</string>
- <string name="apollo_4x1">Apollo (4x1)</string>
- <string name="apollo_4x2">Apollo (4x2)</string>
-
- <!-- Unknown genre name -->
- <string name="unknown">Unknown</string>
-
- <!-- Settings -->
- <string name="about">About Apollo</string>
- <string name="eqalizer">Equalizer</string>
- <string name="header_interface">Interface</string>
- <string name="themes">Themes</string>
- <string name="apollo_themes">Select theme for Apollo</string>
- <string name="select_theme">Select your theme</string>
- <string name="version">Apollo Version</string>
-
- <!-- Settings keys -->
- <string name="key_themes">themepreview</string>
- <string name="key_themes_package">themePackageName</string>
- <string name="key_themes_preferences">themePrefences</string>
- <string name="key_build_version">build_version</string>
-
- <!-- Theme layout Buttons -->
- <string name="apply_theme">Apply theme</string>
- <string name="get_more_themes">Get more themes</string>
-
- <!-- Playlists menu -->
- <string name="favorite">Favorites</string>
- <string name="queue">Queue</string>
- <string name="new_playlist">New</string>
- <string name="new_playlist_name_template">Playlist <xliff:g id="number">%d</xliff:g></string>
- <string name="save">Save</string>
- <string name="overwrite">Overwrite</string>
-
- <!-- Something went wrong -->
- <string name="error">Error</string>
-
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+ <string name="app_name">Apollo</string>
+
+ <!-- Page titles -->
+ <string name="page_recent">Recent</string>
+ <string name="page_artists">Artists</string>
+ <string name="page_albums">Albums</string>
+ <string name="page_songs">Songs</string>
+ <string name="page_playlists">Playlists</string>
+ <string name="page_genres">Genres</string>
+
+ <!-- Option menu items -->
+ <string name="menu_settings">Settings</string>
+ <string name="menu_search">Search</string>
+ <string name="menu_shuffle">Shuffle all</string>
+ <string name="menu_play_all">Play all</string>
+ <string name="menu_sort_by">Sort by</string>
+ <string name="menu_shop">Shop for themes</string>
+ <string name="menu_clear_recents">Clear recent</string>
+ <string name="menu_clear_queue">Clear queue</string>
+ <string name="menu_save_queue">Save queue</string>
+ <string name="menu_clear_favorites">Clear Favorites</string>
+ <string name="menu_share">Share</string>
+ <string name="menu_save_lyrics">Save lyrics</string>
+ <string name="menu_remove_lyrics">Delete lyrics</string>
+ <string name="menu_download_lyrics">Fetch lyrics</string>
+ <string name="menu_add_to_homescreen">Place on Home screen</string>
+ <string name="menu_equalizer">Equalizer</string>
+ <string name="menu_simple">Simple</string>
+ <string name="menu_detailed">Detailed</string>
+ <string name="menu_grid">Grid</string>
+ <string name="menu_view_as">View as</string>
+
+ <!-- Playlist items -->
+ <string name="add_to_playlist">Add to playlist</string>
+ <string name="add_to_favorites">Add to Favorites</string>
+ <string name="add_to_queue">Add to queue</string>
+ <string name="add_to_quickplay">Pin to Quickplay</string>
+ <string name="remove_from_favorites">Remove from Favorites</string>
+ <string name="remove_from_playlist">Remove from playlist</string>
+ <string name="remove_from_recent">Remove from recent</string>
+ <string name="remove_from_queue">Remove from queue</string>
+ <string name="remove_from_quickplay">Remove from Quickplay</string>
+
+ <!-- Sort orders -->
+ <string name="sort_order_entry_az">A-Z</string>
+ <string name="sort_order_entry_za">Z-A</string>
+ <string name="sort_order_entry_artist">Artist</string>
+ <string name="sort_order_entry_album">Album</string>
+ <string name="sort_order_entry_year">Year</string>
+ <string name="sort_order_entry_duration">Duration</string>
+ <string name="sort_order_entry_date_added">Date added</string>
+ <string name="sort_order_entry_track_list">Track list</string>
+ <string name="sort_order_entry_number_of_songs">Number of songs</string>
+ <string name="sort_order_entry_number_of_albums">Number of albums</string>
+
+ <!-- Default playlist names -->
+ <string name="playlist_favorites">Favorites</string>
+ <string name="playlist_last_added">Last added</string>
+
+ <!-- AlertDialog items -->
+ <string name="sort_order_title">Sort by</string>
+ <string name="new_playlist">New playlist</string>
+ <string name="save">Save</string>
+ <string name="cancel">Cancel</string>
+ <string name="overwrite">Overwrite</string>
+ <string name="new_playlist_name_template">Playlist <xliff:g id="number">%d</xliff:g></string>
+ <string name="create_playlist_prompt" msgid="942607395076646686">"Playlist name"</string>
+ <string name="cannot_be_undone">This cannot be undone</string>
+ <string name="delete_warning">This will permanently delete the cached image entries</string>
+ <string name="new_photo">Choose photo from Gallery</string>
+ <string name="google_search">Google search</string>
+ <string name="use_default">Use default photo</string>
+ <string name="old_photo">Use old photo</string>
+
+ <!-- Context menu items -->
+ <string name="context_menu_play_selection">Play</string>
+ <string name="context_menu_play_next">Play next</string>
+ <string name="context_menu_more_by_artist">More by artist</string>
+ <string name="context_menu_rename_playlist">Rename</string>
+ <string name="context_menu_delete">Delete</string>
+ <string name="context_menu_fetch_album_art">Fetch album art</string>
+ <string name="context_menu_fetch_artist_image">Fetch artist image</string>
+ <string name="context_menu_open_in_play_store">Open in Play Store</string>
+ <string name="context_menu_remove_from_recent">Remove from recent</string>
+ <string name="context_menu_remove_from_queue">Remove from queue</string>
+ <string name="context_menu_play_next">Play next</string>
+ <string name="context_menu_use_as_ringtone">Use as ringtone</string>
+
+ <!-- Content descriptions -->
+ <string name="accessibility_play">Play</string>
+ <string name="accessibility_pause">Pause</string>
+ <string name="accessibility_next">Next</string>
+ <string name="accessibility_prev">Previous</string>
+ <string name="accessibility_shuffle">Shuffle</string>
+ <string name="accessibility_shuffle_all">Shuffle all</string>
+ <string name="accessibility_repeat">Repeat</string>
+ <string name="accessibility_repeat_all">Repeat all</string>
+ <string name="accessibility_repeat_one">Repeat one</string>
+ <string name="accessibility_add_to_favorites">Add to favorites</string>
+ <string name="accessibility_remove_from_favorites">Remove from favorites</string>
+
+ <!-- Toast messages -->
+ <string name="removed_from_favorites">removed from Favorites</string>
+ <string name="removed_from_recent">removed from recent</string>
+ <string name="removed_from_playlist">removed from playlist</string>
+ <string name="added_to_favorites">added to Favorites</string>
+ <string name="pinned_to_home_screen">pinned to your Home screen</string>
+ <string name="could_not_be_pinned_to_home_screen">could not be pinned to your Home screen</string>
+ <string name="set_as_ringtone">\"<xliff:g id="name" example="Alarm Bell">%s</xliff:g>\" set as ringtone</string>
+ <string name="playlist_renamed">Playlist renamed</string>
+ <string name="theme_set">set as the theme</string>
+ <string name="lyrics_saved">lyrics saved</string>
+ <string name="lyrics_deleted">lyrics deleted</string>
+
+ <!-- Settings -->
+ <string name="settings_ui_category">Interface</string>
+ <string name="settings_storage_category">Storage</string>
+ <string name="settings_data_category">Data</string>
+ <string name="settings_about_category">About</string>
+ <string name="settings_author_title">Author</string>
+ <string name="settings_about_apollo">About Apollo</string>
+ <string name="settings_special_thanks">Special thanks</string>
+ <string name="settings_cyanogenmod_title">CyanogenMod</string>
+ <string name="settings_self_title">Andrew Neal</string>
+ <string name="settings_lopez_title">A.J. Lopez</string>
+ <string name="settings_lopez_summary">Icon and Play Store banner design</string>
+ <string name="settings_color_scheme_title">Choose Apollo\'s default color scheme</string>
+ <string name="settings_color_scheme_summary">Changes the accent color in Apollo</string>
+ <string name="settings_theme_chooser_title">Theme chooser</string>
+ <string name="settings_delete_cache_title">Delete cache</string>
+ <string name="settings_delete_cache_summary">Remove all cached images</string>
+ <string name="settings_download_only_on_wifi_title">Download via Wi-Fi only</string>
+ <string name="settings_download_only_on_wifi_summary">To reduce carrier charges, don\'t download over mobile networks</string>
+ <string name="settings_download_missing_artwork_title">Download missing album art</string>
+ <string name="settings_download_artist_images_title">Download missing artist images</string>
+ <string name="settings_open_source_licenses">Open source licenses</string>
+ <string name="settings_use_lockscreen_controls">Use lockscreen controls</string>
+ <string name="settings_version_title">Version number</string>
+
+ <!-- Share Intent -->
+ <string name="now_listening_to">#NowPlaying</string>
+ <string name="by">by</string>
+ <string name="share_track_using">Share track using:</string>
+ <string name="hash_apollo">#Apollo</string>
+
+ <!-- ColorPicker -->
+ <string name="color_picker_title">Color scheme</string>
+ <string name="hex">#</string>
+ <string name="current_color">Current</string>
+ <string name="new_color">New</string>
+
+ <!-- App widget -->
+ <string name="app_widget_small">Apollo: 4x1</string>
+ <string name="app_widget_large">Apollo: 4x2</string>
+ <string name="app_widget_large_alt">Apollo: 4x2 (Alternate)</string>
+ <string name="app_widget_recent">Apollo: Recently listened</string>
+ <string name="app_widget_text_separator">-</string>
+
+ <!-- What keywords to use when shopping for Apollo themes -->
+ <string name="apollo_themes_shop_key">Apollo Themes</string>
+
+ <!-- Empty list / error messages -->
+ <string name="no_effects_for_you">The equalizer could not be opened.</string>
+ <string name="empty_music">To copy music from your computer to your device, use a USB cable.</string>
+ <string name="empty_last_added">Songs you\'ve added over the last month will be shown here.</string>
+ <string name="empty_search">No search results found</string>
+ <string name="empty_favorite">Songs you mark as favorites will be shown here.</string>
+ <string name="empty_recent">Albums you\'ve listened to will show up here. Try playing some music.</string>
+ <string name="no_lyrics">Lyrics for \"<xliff:g id="name">%s</xliff:g>\" could not be found</string>
+ <string name="try_fetch_lyrics">To fetch lyrics for \"<xliff:g id="name">%s</xliff:g>\" use \"Fetch lyrics\" in the menu.</string>
+
</resources> \ No newline at end of file
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 6ac2f46..b9fd264 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -1,186 +1,129 @@
<?xml version="1.0" encoding="utf-8"?>
-<resources>
-
- <!-- Custom tabs -->
- <style name="Tabs">
- <item name="android:layout_width">wrap_content</item>
- <item name="android:layout_height">@dimen/bottom_action_bar_height</item>
- <item name="android:background">@drawable/tab</item>
- <item name="android:gravity">center</item>
- <item name="android:paddingBottom">@dimen/tab_padding_top_bottom</item>
- <item name="android:paddingLeft">@dimen/tab_padding_left_right</item>
- <item name="android:paddingRight">@dimen/tab_padding_left_right</item>
- <item name="android:paddingTop">@dimen/tab_padding_top_bottom</item>
- <item name="android:textColor">@color/tab_text_color</item>
- <item name="android:textSize">@dimen/text_size_micro</item>
- <item name="android:textStyle">bold</item>
- <item name="android:focusable">true</item>
- <item name="android:focusableInTouchMode">false</item>
- <item name="android:selectAllOnFocus">false</item>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <!-- Main Holo -->
+ <style name="Apollo.Theme.Dark" parent="Theme.Sherlock">
+ <item name="android:windowContentOverlay">@null</item>
+ <item name="windowContentOverlay">@null</item>
</style>
- <!-- ImageButton in the bottom bar -->
- <style name="BottomActionBarItem">
- <item name="android:layout_width">@dimen/bottom_action_bar_item_width</item>
- <item name="android:layout_height">match_parent</item>
- <item name="android:paddingLeft">@dimen/bottom_action_bar_item_padding_left_right</item>
- <item name="android:paddingRight">@dimen/bottom_action_bar_item_padding_left_right</item>
- <item name="android:background">@drawable/holo_selector</item>
- <item name="android:gravity">center|right</item>
+ <!-- Main Holo light -->
+ <style name="Apollo.Theme.Light" parent="Theme.Sherlock.Light">
+ <item name="android:windowContentOverlay">@null</item>
+ <item name="windowContentOverlay">@null</item>
</style>
- <!-- TextView in the bottom bar -->
- <style name="BottomActionBarText">
- <item name="android:layout_width">wrap_content</item>
- <item name="android:layout_height">wrap_content</item>
- <item name="android:ellipsize">end</item>
- <item name="android:gravity">top|left|center</item>
- <item name="android:singleLine">true</item>
- <item name="android:textSize">@dimen/text_size_extra_micro</item>
- <item name="android:textAllCaps">true</item>
+ <!-- Shortcut Activity theme -->
+ <style name="Theme.Transparent" parent="Theme.Sherlock.NoActionBar">
+ <item name="android:windowIsTranslucent">true</item>
+ <item name="android:windowBackground">@color/transparent</item>
+ <item name="android:windowContentOverlay">@null</item>
+ <item name="android:windowNoTitle">true</item>
+ <item name="android:windowIsFloating">true</item>
+ <item name="android:backgroundDimEnabled">false</item>
</style>
- <!-- List separator with a blue underline -->
- <style name="SeparatorTextViewStyle">
+ <!-- Notification bar event text -->
+ <style name="NotificationText">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>
- <item name="android:minHeight">@dimen/list_separator_min_height</item>
- <item name="android:background">@drawable/list_section_divider_holo_custom</item>
- <item name="android:textAppearance">?android:attr/textAppearanceSmall</item>
- <item name="android:textStyle">bold</item>
- <item name="android:textColor">@color/holo_blue_dark</item>
- <item name="android:gravity">center_vertical</item>
- <item name="android:paddingRight">@dimen/list_separator_padding_left_right</item>
- <item name="android:visibility">gone</item>
<item name="android:ellipsize">end</item>
+ <item name="android:fadingEdge">horizontal</item>
<item name="android:singleLine">true</item>
- <item name="android:textAllCaps">true</item>
- </style>
- <!-- Notification bar button -->
- <style name="StatusBarButton">
- <item name="android:layout_width">@dimen/status_bar_button_width_height</item>
- <item name="android:layout_height">@dimen/status_bar_button_width_height</item>
- <item name="android:layout_gravity">center|right</item>
- <item name="android:background">?android:listChoiceBackgroundIndicator</item>
</style>
- <!-- Notification bar text -->
- <style name="StatusBarText">
- <item name="android:layout_width">wrap_content</item>
- <item name="android:layout_height">wrap_content</item>
- <item name="android:layout_gravity">left</item>
- <item name="android:ellipsize">marquee</item>
- <item name="android:scrollHorizontally">true</item>
- <item name="android:singleLine">true</item>
+ <!-- Notification bar actions -->
+ <style name="NotificationAction">
+ <item name="android:layout_width">@dimen/notification_action_width</item>
+ <item name="android:layout_height">@dimen/notification_action_height</item>
+ <item name="android:gravity">center|right</item>
+ <item name="android:scaleType">fitCenter</item>
+ <item name="android:padding">@dimen/notification_action_padding</item>
+ <item name="android:background">?android:selectableItemBackground</item>
</style>
- <!-- Half and half layout -->
- <style name="HalfText">
- <item name="android:layout_width">match_parent</item>
- <item name="android:layout_height">wrap_content</item>
- <item name="android:background">@color/transparent_black</item>
- <item name="android:ellipsize">end</item>
- <item name="android:gravity">center</item>
- <item name="android:padding">@dimen/half_and_half_text_padding</item>
- <item name="android:singleLine">true</item>
- <item name="android:textColor">@color/white</item>
- <item name="android:textSize">@dimen/text_size_small</item>
+ <style name="NotificationAction.Previous" parent="@style/NotificationAction">
+ <item name="android:src">@drawable/btn_playback_previous</item>
+ <item name="android:visibility">gone</item>
+ <item name="android:contentDescription">@string/accessibility_prev</item>
</style>
- <!-- ContextMenu header text -->
- <style name="HeaderText">
- <item name="android:layout_width">match_parent</item>
- <item name="android:layout_height">wrap_content</item>
- <item name="android:ellipsize">end</item>
- <item name="android:gravity">center|left</item>
- <item name="android:paddingTop">@dimen/header_text_padding</item>
- <item name="android:paddingLeft">@dimen/header_text_padding_left</item>
- <item name="android:paddingBottom">@dimen/header_text_padding</item>
- <item name="android:paddingRight">@dimen/header_text_padding</item>
- <item name="android:singleLine">true</item>
- <item name="android:textColor">@color/white</item>
- <item name="android:textSize">@dimen/text_size_large</item>
+ <style name="NotificationAction.Play" parent="@style/NotificationAction">
+ <item name="android:src">@drawable/btn_playback_play</item>
+ <item name="android:contentDescription">@string/accessibility_play</item>
</style>
- <!-- TextView in shown over the images in the GridView -->
- <style name="GridViewTextItem">
- <item name="android:layout_width">wrap_content</item>
- <item name="android:layout_height">wrap_content</item>
- <item name="android:singleLine">true</item>
- <item name="android:ellipsize">end</item>
- <item name="android:shadowColor">@color/white</item>
- <item name="android:shadowRadius">1</item>
- <item name="android:textColor">@color/white</item>
- <item name="android:textSize">@dimen/text_size_medium</item>
+ <style name="NotificationAction.Next" parent="@style/NotificationAction">
+ <item name="android:src">@drawable/btn_playback_next</item>
+ <item name="android:contentDescription">@string/accessibility_next</item>
</style>
- <!-- Now playing indicator -->
- <style name="PeakMeter">
- <item name="android:layout_width">wrap_content</item>
- <item name="android:layout_height">wrap_content</item>
- <item name="android:layout_alignParentRight">true</item>
+ <style name="NotificationAction.Collapse" parent="@style/NotificationAction">
+ <item name="android:src">@drawable/btn_notification_collapse</item>
</style>
- <!-- TextView in the audio player -->
- <style name="AudioPlayerText">
+ <!-- Bottom Action Bar TextViews -->
+ <style name="BottomActionBarText">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
- <item name="android:ellipsize">marquee</item>
+ <item name="android:ellipsize">end</item>
+ <item name="android:paddingLeft">5dp</item>
+ <item name="android:gravity">top|left|center</item>
<item name="android:singleLine">true</item>
- <item name="android:focusable">true</item>
- <item name="android:focusableInTouchMode">true</item>
- <item name="android:lineSpacingMultiplier">1.2</item>
- <item name="android:scrollHorizontally">true</item>
+ <item name="android:textAllCaps">true</item>
+ <item name="android:textSize">@dimen/text_size_extra_micro</item>
</style>
- <!-- ImageButton in the audio player controls -->
- <style name="AudioPlayerButton">
- <item name="android:layout_width">0dp</item>
- <item name="android:layout_height">match_parent</item>
- <item name="android:layout_weight">1</item>
- <item name="android:background">@drawable/holo_selector</item>
+ <style name="BottomActionBarLineOne" parent="@style/BottomActionBarText">
+ <item name="android:textStyle">bold</item>
</style>
- <!-- QuickQueue -->
- <style name="Theme.QuickQueue" parent="@android:style/Theme.Holo.Light">
- <item name="android:windowBackground">@color/transparent</item>
- <item name="android:colorBackgroundCacheHint">@null</item>
- <item name="android:windowFrame">@null</item>
- <item name="android:windowContentOverlay">@null</item>
- <item name="android:windowAnimationStyle">@null</item>
- <item name="android:windowIsFloating">false</item>
- <item name="android:backgroundDimEnabled">true</item>
- <item name="android:windowIsTranslucent">true</item>
- <item name="android:windowNoTitle">true</item>
- </style>
+ <style name="BottomActionBarLineTwo" parent="@style/BottomActionBarText"></style>
- <!-- App Widget 4x2 -->
- <style name="FourByTwoMediaButton">
+ <!-- Bottom Action Bar Image Buttons -->
+ <style name="BottomActionBarItem">
+ <item name="android:layout_weight">1</item>
<item name="android:layout_width">0dp</item>
<item name="android:layout_height">match_parent</item>
- <item name="android:layout_weight">1</item>
- <item name="android:background">@drawable/holo_selector</item>
- <item name="android:scaleType">center</item>
+ <item name="android:gravity">center|right</item>
+ <item name="android:paddingLeft">@dimen/bottom_action_bar_item_padding_left</item>
+ <item name="android:paddingRight">@dimen/bottom_action_bar_item_padding_right</item>
+ <item name="android:scaleType">centerInside</item>
+ </style>
+
+ <style name="BottomActionBarItem.Previous" parent="@style/BottomActionBarItem">
+ <item name="android:contentDescription">@string/accessibility_prev</item>
</style>
- <!-- Overflow Holo theme -->
- <style name="Apollo.Holo" parent="@android:style/Theme.Holo.Light">
- <item name="android:actionOverflowButtonStyle">@style/OverFlowHolo</item>
+ <style name="BottomActionBarItem.Next" parent="@style/BottomActionBarItem">
+ <item name="android:contentDescription">@string/accessibility_next</item>
</style>
- <!-- Overflow Holo.Light theme -->
- <style name="Apollo.Holo.Light" parent="@android:style/Theme.Holo.Light">
- <item name="android:actionOverflowButtonStyle">@style/OverFlowHolo.Light</item>
+ <style name="BottomActionBarItem.Play" parent="@style/BottomActionBarItem">
+ <item name="android:contentDescription">@string/accessibility_play</item>
</style>
- <!-- Overflow Holo.Dark -->
- <style name="OverFlowHolo" parent="@android:style/Widget.Holo.ActionButton.Overflow">
- <item name="android:src">@drawable/apollo_holo_dark_overflow</item>
+ <style name="BottomActionBarItem.Shuffle" parent="@style/BottomActionBarItem">
+ <item name="android:contentDescription">@string/accessibility_shuffle</item>
</style>
- <!-- Overflow Holo.Dark -->
- <style name="OverFlowHolo.Light" parent="@android:style/Widget.Holo.ActionButton.Overflow">
- <item name="android:src">@drawable/apollo_holo_light_overflow</item>
+ <style name="BottomActionBarItem.Repeat" parent="@style/BottomActionBarItem">
+ <item name="android:contentDescription">@string/accessibility_repeat</item>
</style>
</resources> \ No newline at end of file
diff --git a/res/values/themeconfig.xml b/res/values/themeconfig.xml
new file mode 100644
index 0000000..bb5fb21
--- /dev/null
+++ b/res/values/themeconfig.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<resources>
+
+ <!-- action bar color -->
+ <color name="action_bar">@color/action_bar_color</color>
+
+ <!-- The action bar title color -->
+ <color name="action_bar_title">@color/white</color>
+
+ <!-- The action bar sub title color -->
+ <color name="action_bar_subtitle">@color/transparent_white</color>
+
+ <!-- Lyrics color -->
+ <color name="lyrics">@color/white</color>
+
+ <!-- Adpater lines -->
+ <color name="line_one">@color/white</color>
+ <color name="line_two">@color/transparent_white</color>
+ <color name="line_three">@color/transparent_white</color>
+
+ <!-- Now playing -->
+ <color name="audio_player_current_time">@color/white</color>
+ <color name="audio_player_total_time">@color/white</color>
+ <color name="audio_player_line_one">@color/white</color>
+ <color name="audio_player_line_two">@color/transparent_white</color>
+ <color name="audio_player_pager_container">@color/action_bar_color</color>
+
+ <!-- Bottom action bar -->
+ <color name="bottom_action_bar">@color/action_bar_color</color>
+ <color name="bab_line_one">@color/white</color>
+ <color name="bab_line_two">@color/transparent_white</color>
+
+ <!-- Action bar items -->
+ <color name="favorite_normal">@color/transparent_white</color>
+ <color name="search_action">@color/transparent_white</color>
+ <color name="shop_action">@color/transparent_white</color>
+ <color name="pinn_to_action">@color/transparent_white</color>
+
+</resources> \ No newline at end of file
diff --git a/res/xml-v14/app_widget_recents.xml b/res/xml-v14/app_widget_recents.xml
new file mode 100644
index 0000000..700b6d2
--- /dev/null
+++ b/res/xml-v14/app_widget_recents.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
+ android:initialLayout="@layout/app_widget_recents"
+ android:minHeight="@dimen/app_widget_scrollable_min_height"
+ android:minResizeHeight="@dimen/app_widget_scrollable_min_resize_height"
+ android:minResizeWidth="@dimen/app_widget_large_min_width"
+ android:minWidth="@dimen/app_widget_large_min_width"
+ android:previewImage="@drawable/app_widget_recents"
+ android:resizeMode="vertical|horizontal"
+ android:updatePeriodMillis="0"
+ android:widgetCategory="keyguard|home_screen" />
diff --git a/res/xml-v14/settings.xml b/res/xml-v14/settings.xml
new file mode 100644
index 0000000..2ad14a8
--- /dev/null
+++ b/res/xml-v14/settings.xml
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <!-- UI catetgory -->
+ <PreferenceCategory android:title="@string/settings_ui_category" >
+
+ <!-- Color scheme -->
+ <Preference
+ android:key="color_scheme"
+ android:summary="@string/settings_color_scheme_summary"
+ android:title="@string/settings_color_scheme_title" />
+ <!-- Theme chooser -->
+ <Preference
+ android:key="theme_chooser"
+ android:title="@string/settings_theme_chooser_title" />
+ <!-- Enable lockscreen controls -->
+ <CheckBoxPreference
+ android:defaultValue="true"
+ android:key="lockscreen_controls"
+ android:title="@string/settings_use_lockscreen_controls" />
+ </PreferenceCategory>
+ <!-- Data catetory -->
+ <PreferenceCategory android:title="@string/settings_data_category" >
+
+ <!-- Only on Wi-Fi -->
+ <CheckBoxPreference
+ android:defaultValue="true"
+ android:key="only_on_wifi"
+ android:summary="@string/settings_download_only_on_wifi_summary"
+ android:title="@string/settings_download_only_on_wifi_title" />
+ <!-- Missing artwork -->
+ <CheckBoxPreference
+ android:defaultValue="true"
+ android:key="album_images"
+ android:title="@string/settings_download_missing_artwork_title" />
+ <!-- Missing artist images -->
+ <CheckBoxPreference
+ android:defaultValue="true"
+ android:key="artist_images"
+ android:title="@string/settings_download_artist_images_title" />
+ </PreferenceCategory>
+ <!-- Storage catetory -->
+ <PreferenceCategory android:title="@string/settings_storage_category" >
+
+ <!-- Delete cache -->
+ <Preference
+ android:key="delete_cache"
+ android:summary="@string/settings_delete_cache_summary"
+ android:title="@string/settings_delete_cache_title" />
+ </PreferenceCategory>
+ <!-- About -->
+ <PreferenceCategory android:title="@string/settings_about_category" >
+ <PreferenceScreen android:title="@string/settings_about_apollo" >
+ <PreferenceCategory android:title="@string/settings_about_category" >
+ <Preference
+ android:summary="@string/settings_self_title"
+ android:title="@string/settings_author_title" />
+ <Preference
+ android:key="version"
+ android:title="@string/settings_version_title" />
+ <Preference
+ android:key="open_source"
+ android:title="@string/settings_open_source_licenses" />
+ </PreferenceCategory>
+ <PreferenceCategory android:title="@string/settings_special_thanks" >
+ <Preference
+ android:summary="@string/settings_lopez_summary"
+ android:title="@string/settings_lopez_title" />
+ <Preference android:title="@string/settings_cyanogenmod_title" />
+ </PreferenceCategory>
+ </PreferenceScreen>
+ </PreferenceCategory>
+
+</PreferenceScreen> \ No newline at end of file
diff --git a/res/xml/app_widget_large.xml b/res/xml/app_widget_large.xml
new file mode 100644
index 0000000..4061634
--- /dev/null
+++ b/res/xml/app_widget_large.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
+ android:initialLayout="@layout/app_widget_large"
+ android:minHeight="@dimen/app_widget_large_min_height"
+ android:minWidth="@dimen/app_widget_large_min_width"
+ android:previewImage="@drawable/app_widget_large"
+ android:updatePeriodMillis="0"
+ android:widgetCategory="keyguard|home_screen" />
diff --git a/res/xml/app_widget_large_alternate.xml b/res/xml/app_widget_large_alternate.xml
new file mode 100644
index 0000000..5b80b2c
--- /dev/null
+++ b/res/xml/app_widget_large_alternate.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
+ android:initialLayout="@layout/app_widget_large_alternate"
+ android:minHeight="@dimen/app_widget_large_min_height"
+ android:minWidth="@dimen/app_widget_large_min_width"
+ android:previewImage="@drawable/app_widget_large_alternate"
+ android:updatePeriodMillis="0"
+ android:widgetCategory="keyguard|home_screen" />
diff --git a/res/xml/app_widget_small.xml b/res/xml/app_widget_small.xml
new file mode 100644
index 0000000..4bcee5e
--- /dev/null
+++ b/res/xml/app_widget_small.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
+ android:initialLayout="@layout/app_widget_small"
+ android:minHeight="@dimen/app_widget_small_min_height"
+ android:minWidth="@dimen/app_widget_small_min_width"
+ android:previewImage="@drawable/app_widget_small"
+ android:updatePeriodMillis="0"
+ android:widgetCategory="keyguard|home_screen" />
diff --git a/res/xml/appwidget1x1_info.xml b/res/xml/appwidget1x1_info.xml
deleted file mode 100644
index 680c1ef..0000000
--- a/res/xml/appwidget1x1_info.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
- android:initialLayout="@layout/onebyone_app_widget"
- android:minHeight="40dp"
- android:minWidth="40dp"
- android:updatePeriodMillis="0" /> \ No newline at end of file
diff --git a/res/xml/appwidget4x1_info.xml b/res/xml/appwidget4x1_info.xml
deleted file mode 100644
index aad2bb0..0000000
--- a/res/xml/appwidget4x1_info.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
- android:initialLayout="@layout/fourbyone_app_widget"
- android:minHeight="40dp"
- android:minWidth="260dp"
- android:updatePeriodMillis="0" />
diff --git a/res/xml/appwidget4x2_info.xml b/res/xml/appwidget4x2_info.xml
deleted file mode 100644
index 0605762..0000000
--- a/res/xml/appwidget4x2_info.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
- android:initialLayout="@layout/fourbytwo_app_widget"
- android:minHeight="110dp"
- android:minWidth="250dp"
- android:updatePeriodMillis="0" />
diff --git a/res/xml/searchable.xml b/res/xml/searchable.xml
index c4f8174..ce451fb 100644
--- a/res/xml/searchable.xml
+++ b/res/xml/searchable.xml
@@ -1,22 +1,25 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright (C) 2009 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.
--->
-
-<searchable xmlns:android="http://schemas.android.com/apk/res/android"
- android:includeInGlobalSearch="true"
- android:label="@string/search"
- android:searchSuggestIntentAction="android.intent.action.VIEW"
- android:voiceSearchMode="showVoiceSearchButton|launchRecognizer" />
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
+<searchable xmlns:android="http://schemas.android.com/apk/res/android"
+ android:imeOptions="actionSearch"
+ android:label="@string/menu_search"
+ android:searchSuggestIntentAction="android.intent.action.SEARCH"
+ android:searchSuggestSelection=" ? "
+ android:searchSuggestThreshold="2"
+ android:voiceSearchMode="showVoiceSearchButton|launchRecognizer" >
+
+</searchable> \ No newline at end of file
diff --git a/res/xml/settings.xml b/res/xml/settings.xml
index fbd15fc..e2850a5 100644
--- a/res/xml/settings.xml
+++ b/res/xml/settings.xml
@@ -1,28 +1,84 @@
<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Andrew Neal
+
+ 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.
+-->
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" >
- <PreferenceCategory android:title="@string/header_interface" >
- <PreferenceScreen
- android:icon="@drawable/apollo_settings_themes"
- android:key="@string/key_themes_preferences"
- android:title="@string/themes" >
- <ListPreference
- android:key="@string/key_themes_package"
- android:summary="@string/apollo_themes"
- android:title="@string/select_theme" />
+ <!-- UI catetgory -->
+ <PreferenceCategory android:title="@string/settings_ui_category" >
- <com.andrew.apollo.preferences.ThemePreview
- android:key="@string/key_themes"
- android:layout="@layout/theme_preview" />
- </PreferenceScreen>
+ <!-- Color scheme -->
+ <Preference
+ android:key="color_scheme"
+ android:summary="@string/settings_color_scheme_summary"
+ android:title="@string/settings_color_scheme_title" />
+ <!-- Theme chooser -->
+ <Preference
+ android:key="theme_chooser"
+ android:title="@string/settings_theme_chooser_title" />
+ </PreferenceCategory>
+ <!-- Data catetory -->
+ <PreferenceCategory android:title="@string/settings_data_category" >
+
+ <!-- Only on Wi-Fi -->
+ <CheckBoxPreference
+ android:defaultValue="true"
+ android:key="only_on_wifi"
+ android:summary="@string/settings_download_only_on_wifi_summary"
+ android:title="@string/settings_download_only_on_wifi_title" />
+ <!-- Missing artwork -->
+ <CheckBoxPreference
+ android:defaultValue="true"
+ android:key="album_images"
+ android:title="@string/settings_download_missing_artwork_title" />
+ <!-- Missing artist images -->
+ <CheckBoxPreference
+ android:defaultValue="true"
+ android:key="artist_images"
+ android:title="@string/settings_download_artist_images_title" />
</PreferenceCategory>
- <PreferenceCategory android:title="@string/about" >
+ <!-- Storage catetory -->
+ <PreferenceCategory android:title="@string/settings_storage_category" >
+
+ <!-- Delete cache -->
<Preference
- style="?android:preferenceInformationStyle"
- android:enabled="false"
- android:key="@string/key_build_version"
- android:summary="1.0"
- android:title="@string/version" />
+ android:key="delete_cache"
+ android:summary="@string/settings_delete_cache_summary"
+ android:title="@string/settings_delete_cache_title" />
+ </PreferenceCategory>
+ <!-- About -->
+ <PreferenceCategory android:title="@string/settings_about_category" >
+ <PreferenceScreen android:title="@string/settings_about_apollo" >
+ <PreferenceCategory android:title="@string/settings_about_category" >
+ <Preference
+ android:summary="@string/settings_self_title"
+ android:title="@string/settings_author_title" />
+ <Preference
+ android:key="version"
+ android:title="@string/settings_version_title" />
+ <Preference
+ android:key="open_source"
+ android:title="@string/settings_open_source_licenses" />
+ </PreferenceCategory>
+ <PreferenceCategory android:title="@string/settings_special_thanks" >
+ <Preference
+ android:summary="@string/settings_lopez_summary"
+ android:title="@string/settings_lopez_title" />
+ <Preference android:title="@string/settings_cyanogenmod_title" />
+ </PreferenceCategory>
+ </PreferenceScreen>
</PreferenceCategory>
</PreferenceScreen> \ No newline at end of file
diff --git a/src/com/andrew/apollo/ApolloApplication.java b/src/com/andrew/apollo/ApolloApplication.java
new file mode 100644
index 0000000..677915e
--- /dev/null
+++ b/src/com/andrew/apollo/ApolloApplication.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.app.Application;
+import android.os.StrictMode;
+
+import com.andrew.apollo.cache.ImageCache;
+import com.andrew.apollo.utils.ApolloUtils;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Used to turn off logging for jaudiotagger and free up memory when
+ * {@code #onLowMemory()} is called on pre-ICS devices. On post-ICS memory is
+ * released within {@link ImageCache}.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+@SuppressLint("NewApi")
+public class ApolloApplication extends Application {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate() {
+ // Enable strict mode logging
+ enableStrictMode();
+ // Turn off logging for jaudiotagger.
+ Logger.getLogger("org.jaudiotagger").setLevel(Level.OFF);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLowMemory() {
+ ImageCache.getInstance(this).evictAll();
+ super.onLowMemory();
+ }
+
+ @TargetApi(11)
+ private void enableStrictMode() {
+ if (ApolloUtils.hasGingerbread() && BuildConfig.DEBUG) {
+ final StrictMode.ThreadPolicy.Builder threadPolicyBuilder = new StrictMode.ThreadPolicy.Builder()
+ .detectAll().penaltyLog();
+ final StrictMode.VmPolicy.Builder vmPolicyBuilder = new StrictMode.VmPolicy.Builder()
+ .detectAll().penaltyLog();
+
+ if (ApolloUtils.hasHoneycomb()) {
+ threadPolicyBuilder.penaltyFlashScreen();
+ }
+ StrictMode.setThreadPolicy(threadPolicyBuilder.build());
+ StrictMode.setVmPolicy(vmPolicyBuilder.build());
+ }
+ }
+}
diff --git a/src/com/andrew/apollo/AudioPlayerFragment.java b/src/com/andrew/apollo/AudioPlayerFragment.java
deleted file mode 100644
index d61fb68..0000000
--- a/src/com/andrew/apollo/AudioPlayerFragment.java
+++ /dev/null
@@ -1,638 +0,0 @@
-/**
- *
- */
-
-package com.andrew.apollo;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.Message;
-import android.os.RemoteException;
-import android.os.SystemClock;
-import android.provider.BaseColumns;
-import android.provider.MediaStore.Audio;
-import android.support.v4.app.Fragment;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.view.ViewGroup;
-import android.widget.ImageButton;
-import android.widget.ImageView;
-import android.widget.SeekBar;
-import android.widget.SeekBar.OnSeekBarChangeListener;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import com.andrew.apollo.activities.TracksBrowser;
-import com.andrew.apollo.service.ApolloService;
-import com.andrew.apollo.tasks.GetCachedImages;
-import com.andrew.apollo.tasks.LastfmGetAlbumImages;
-import com.andrew.apollo.ui.widgets.RepeatingImageButton;
-import com.andrew.apollo.utils.ApolloUtils;
-import com.andrew.apollo.utils.MusicUtils;
-import com.andrew.apollo.utils.ThemeUtils;
-
-/**
- * @author Andrew Neal
- */
-public class AudioPlayerFragment extends Fragment implements Constants {
-
- // Track, album, and artist name
- private TextView mTrackName, mAlbumArtistName;
-
- // Total and current time
- private TextView mTotalTime, mCurrentTime;
-
- // Album art
- private ImageView mAlbumArt;
-
- // Controls
- private ImageButton mRepeat, mPlay, mShuffle;
-
- private RepeatingImageButton mPrev, mNext;
-
- // Progress
- private SeekBar mProgress;
-
- // Where we are in the track
- private long mDuration, mLastSeekEventTime, mPosOverride = -1, mStartSeekPos = 0;
-
- private boolean mFromTouch, paused = false;
-
- // Handler
- private static final int REFRESH = 1, UPDATEINFO = 2;
-
- // Notify if repeat or shuffle changes
- private Toast mToast;
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
- View root = inflater.inflate(R.layout.audio_player, container, false);
-
- mTrackName = (TextView)root.findViewById(R.id.audio_player_track);
- mTrackName.setOnClickListener(new OnClickListener() {
-
- @Override
- public void onClick(View v) {
- tracksBrowser();
- }
- });
- mAlbumArtistName = (TextView)root.findViewById(R.id.audio_player_album_artist);
- mAlbumArtistName.setOnClickListener(new OnClickListener() {
-
- @Override
- public void onClick(View v) {
- tracksBrowserArtist();
- }
- });
-
- mTotalTime = (TextView)root.findViewById(R.id.audio_player_total_time);
- mCurrentTime = (TextView)root.findViewById(R.id.audio_player_current_time);
-
- mAlbumArt = (ImageView)root.findViewById(R.id.audio_player_album_art);
-
- mRepeat = (ImageButton)root.findViewById(R.id.audio_player_repeat);
- mPrev = (RepeatingImageButton)root.findViewById(R.id.audio_player_prev);
- mPlay = (ImageButton)root.findViewById(R.id.audio_player_play);
- mNext = (RepeatingImageButton)root.findViewById(R.id.audio_player_next);
- mShuffle = (ImageButton)root.findViewById(R.id.audio_player_shuffle);
-
- mRepeat.setOnClickListener(new OnClickListener() {
-
- @Override
- public void onClick(View v) {
- cycleRepeat();
- }
- });
-
- mPrev.setRepeatListener(mRewListener, 260);
- mPrev.setOnClickListener(new OnClickListener() {
-
- @Override
- public void onClick(View v) {
- if (MusicUtils.mService == null)
- return;
- try {
- if (MusicUtils.mService.position() < 2000) {
- MusicUtils.mService.prev();
- } else {
- MusicUtils.mService.seek(0);
- MusicUtils.mService.play();
- }
- } catch (RemoteException ex) {
- ex.printStackTrace();
- }
- }
- });
-
- mPlay.setOnClickListener(new OnClickListener() {
-
- @Override
- public void onClick(View v) {
- doPauseResume();
- }
- });
-
- mNext.setRepeatListener(mFfwdListener, 260);
- mNext.setOnClickListener(new OnClickListener() {
-
- @Override
- public void onClick(View v) {
- if (MusicUtils.mService == null)
- return;
- try {
- MusicUtils.mService.next();
- } catch (RemoteException ex) {
- ex.printStackTrace();
- }
- }
- });
-
- mShuffle.setOnClickListener(new OnClickListener() {
-
- @Override
- public void onClick(View v) {
- toggleShuffle();
- }
- });
-
- mProgress = (SeekBar)root.findViewById(android.R.id.progress);
- if (mProgress instanceof SeekBar) {
- SeekBar seeker = mProgress;
- seeker.setOnSeekBarChangeListener(mSeekListener);
- }
- mProgress.setMax(1000);
-
- // Theme chooser
- ThemeUtils.setImageButton(getActivity(), mPrev, "apollo_previous");
- ThemeUtils.setImageButton(getActivity(), mNext, "apollo_next");
- ThemeUtils.setProgessDrawable(getActivity(), mProgress, "apollo_seekbar_background");
- return root;
- }
-
- /**
- * Update everything as the meta or playstate changes
- */
- private final BroadcastReceiver mStatusListener = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- if (intent.getAction().equals(ApolloService.META_CHANGED))
- mHandler.sendMessage(mHandler.obtainMessage(UPDATEINFO));
- setPauseButtonImage();
- setShuffleButtonImage();
- setRepeatButtonImage();
- }
- };
-
- @Override
- public void onStart() {
- super.onStart();
- IntentFilter f = new IntentFilter();
- f.addAction(ApolloService.PLAYSTATE_CHANGED);
- f.addAction(ApolloService.META_CHANGED);
- getActivity().registerReceiver(mStatusListener, new IntentFilter(f));
-
- long next = refreshNow();
- queueNextRefresh(next);
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- paused = true;
- mHandler.removeMessages(REFRESH);
- getActivity().unregisterReceiver(mStatusListener);
- }
-
- /**
- * Cycle repeat states
- */
- private void cycleRepeat() {
- if (MusicUtils.mService == null) {
- return;
- }
- try {
- int mode = MusicUtils.mService.getRepeatMode();
- if (mode == ApolloService.REPEAT_NONE) {
- MusicUtils.mService.setRepeatMode(ApolloService.REPEAT_ALL);
- ApolloUtils.showToast(R.string.repeat_all, mToast, getActivity());
- } else if (mode == ApolloService.REPEAT_ALL) {
- MusicUtils.mService.setRepeatMode(ApolloService.REPEAT_CURRENT);
- if (MusicUtils.mService.getShuffleMode() != ApolloService.SHUFFLE_NONE) {
- MusicUtils.mService.setShuffleMode(ApolloService.SHUFFLE_NONE);
- setShuffleButtonImage();
- }
- ApolloUtils.showToast(R.string.repeat_one, mToast, getActivity());
- } else {
- MusicUtils.mService.setRepeatMode(ApolloService.REPEAT_NONE);
- ApolloUtils.showToast(R.string.repeat_off, mToast, getActivity());
- }
- setRepeatButtonImage();
- } catch (RemoteException ex) {
- ex.printStackTrace();
- }
-
- }
-
- /**
- * Scan backwards
- */
- private final RepeatingImageButton.RepeatListener mRewListener = new RepeatingImageButton.RepeatListener() {
- @Override
- public void onRepeat(View v, long howlong, int repcnt) {
- scanBackward(repcnt, howlong);
- }
- };
-
- /**
- * Play and pause music
- */
- private void doPauseResume() {
- try {
- if (MusicUtils.mService != null) {
- if (MusicUtils.mService.isPlaying()) {
- MusicUtils.mService.pause();
- } else {
- MusicUtils.mService.play();
- }
- }
- refreshNow();
- setPauseButtonImage();
- } catch (RemoteException ex) {
- ex.printStackTrace();
- }
- }
-
- /**
- * Scan forwards
- */
- private final RepeatingImageButton.RepeatListener mFfwdListener = new RepeatingImageButton.RepeatListener() {
- @Override
- public void onRepeat(View v, long howlong, int repcnt) {
- scanForward(repcnt, howlong);
- }
- };
-
- /**
- * Set the shuffle mode
- */
- private void toggleShuffle() {
- if (MusicUtils.mService == null) {
- return;
- }
- try {
- int shuffle = MusicUtils.mService.getShuffleMode();
- if (shuffle == ApolloService.SHUFFLE_NONE) {
- MusicUtils.mService.setShuffleMode(ApolloService.SHUFFLE_NORMAL);
- if (MusicUtils.mService.getRepeatMode() == ApolloService.REPEAT_CURRENT) {
- MusicUtils.mService.setRepeatMode(ApolloService.REPEAT_ALL);
- setRepeatButtonImage();
- }
- ApolloUtils.showToast(R.string.shuffle_on, mToast, getActivity());
- } else if (shuffle == ApolloService.SHUFFLE_NORMAL
- || shuffle == ApolloService.SHUFFLE_AUTO) {
- MusicUtils.mService.setShuffleMode(ApolloService.SHUFFLE_NONE);
- ApolloUtils.showToast(R.string.shuffle_off, mToast, getActivity());
- }
- setShuffleButtonImage();
- } catch (RemoteException ex) {
- ex.printStackTrace();
- }
- }
-
- private void scanBackward(int repcnt, long delta) {
- if (MusicUtils.mService == null)
- return;
- try {
- if (repcnt == 0) {
- mStartSeekPos = MusicUtils.mService.position();
- mLastSeekEventTime = 0;
- } else {
- if (delta < 5000) {
- // seek at 10x speed for the first 5 seconds
- delta = delta * 10;
- } else {
- // seek at 40x after that
- delta = 50000 + (delta - 5000) * 40;
- }
- long newpos = mStartSeekPos - delta;
- if (newpos < 0) {
- // move to previous track
- MusicUtils.mService.prev();
- long duration = MusicUtils.mService.duration();
- mStartSeekPos += duration;
- newpos += duration;
- }
- if (((delta - mLastSeekEventTime) > 250) || repcnt < 0) {
- MusicUtils.mService.seek(newpos);
- mLastSeekEventTime = delta;
- }
- if (repcnt >= 0) {
- mPosOverride = newpos;
- } else {
- mPosOverride = -1;
- }
- refreshNow();
- }
- } catch (RemoteException ex) {
- ex.printStackTrace();
- }
- }
-
- private void scanForward(int repcnt, long delta) {
- if (MusicUtils.mService == null)
- return;
- try {
- if (repcnt == 0) {
- mStartSeekPos = MusicUtils.mService.position();
- mLastSeekEventTime = 0;
- } else {
- if (delta < 5000) {
- // seek at 10x speed for the first 5 seconds
- delta = delta * 10;
- } else {
- // seek at 40x after that
- delta = 50000 + (delta - 5000) * 40;
- }
- long newpos = mStartSeekPos + delta;
- long duration = MusicUtils.mService.duration();
- if (newpos >= duration) {
- // move to next track
- MusicUtils.mService.next();
- mStartSeekPos -= duration; // is OK to go negative
- newpos -= duration;
- }
- if (((delta - mLastSeekEventTime) > 250) || repcnt < 0) {
- MusicUtils.mService.seek(newpos);
- mLastSeekEventTime = delta;
- }
- if (repcnt >= 0) {
- mPosOverride = newpos;
- } else {
- mPosOverride = -1;
- }
- refreshNow();
- }
- } catch (RemoteException ex) {
- ex.printStackTrace();
- }
- }
-
- /**
- * Set the repeat images
- */
- private void setRepeatButtonImage() {
- if (MusicUtils.mService == null)
- return;
- try {
- switch (MusicUtils.mService.getRepeatMode()) {
- case ApolloService.REPEAT_ALL:
- mRepeat.setImageResource(R.drawable.apollo_holo_light_repeat_all);
- break;
- case ApolloService.REPEAT_CURRENT:
- mRepeat.setImageResource(R.drawable.apollo_holo_light_repeat_one);
- break;
- default:
- mRepeat.setImageResource(R.drawable.apollo_holo_light_repeat_normal);
- // Theme chooser
- ThemeUtils.setImageButton(getActivity(), mRepeat, "apollo_repeat_normal");
- break;
- }
- } catch (RemoteException ex) {
- ex.printStackTrace();
- }
- }
-
- /**
- * Set the shuffle images
- */
- private void setShuffleButtonImage() {
- if (MusicUtils.mService == null)
- return;
- try {
- switch (MusicUtils.mService.getShuffleMode()) {
- case ApolloService.SHUFFLE_NONE:
- mShuffle.setImageResource(R.drawable.apollo_holo_light_shuffle_normal);
- // Theme chooser
- ThemeUtils.setImageButton(getActivity(), mShuffle, "apollo_shuffle_normal");
- break;
- case ApolloService.SHUFFLE_AUTO:
- mShuffle.setImageResource(R.drawable.apollo_holo_light_shuffle_on);
- break;
- default:
- mShuffle.setImageResource(R.drawable.apollo_holo_light_shuffle_on);
- break;
- }
- } catch (RemoteException ex) {
- ex.printStackTrace();
- }
- }
-
- /**
- * Set the play and pause image
- */
- private void setPauseButtonImage() {
- try {
- if (MusicUtils.mService != null && MusicUtils.mService.isPlaying()) {
- mPlay.setImageResource(R.drawable.apollo_holo_light_pause);
- // Theme chooser
- ThemeUtils.setImageButton(getActivity(), mPlay, "apollo_pause");
- } else {
- mPlay.setImageResource(R.drawable.apollo_holo_light_play);
- // Theme chooser
- ThemeUtils.setImageButton(getActivity(), mPlay, "apollo_play");
- }
- } catch (RemoteException ex) {
- ex.printStackTrace();
- }
- }
-
- /**
- * @param delay
- */
- private void queueNextRefresh(long delay) {
- if (!paused) {
- Message msg = mHandler.obtainMessage(REFRESH);
- mHandler.removeMessages(REFRESH);
- mHandler.sendMessageDelayed(msg, delay);
- }
- }
-
- /**
- * We need to refresh the time via a Handler
- */
- private final Handler mHandler = new Handler() {
- @Override
- public void handleMessage(Message msg) {
- switch (msg.what) {
- case REFRESH:
- long next = refreshNow();
- queueNextRefresh(next);
- break;
- case UPDATEINFO:
- updateMusicInfo();
- break;
- default:
- break;
- }
- }
- };
-
- /**
- * Drag to a specfic duration
- */
- private final OnSeekBarChangeListener mSeekListener = new OnSeekBarChangeListener() {
- @Override
- public void onStartTrackingTouch(SeekBar bar) {
- mLastSeekEventTime = 0;
- mFromTouch = true;
- }
-
- @Override
- public void onProgressChanged(SeekBar bar, int progress, boolean fromuser) {
- if (!fromuser || (MusicUtils.mService == null))
- return;
- long now = SystemClock.elapsedRealtime();
- if ((now - mLastSeekEventTime) > 250) {
- mLastSeekEventTime = now;
- mPosOverride = mDuration * progress / 1000;
- try {
- MusicUtils.mService.seek(mPosOverride);
- } catch (RemoteException ex) {
- ex.printStackTrace();
- }
-
- if (!mFromTouch) {
- refreshNow();
- mPosOverride = -1;
- }
- }
- }
-
- @Override
- public void onStopTrackingTouch(SeekBar bar) {
- mPosOverride = -1;
- mFromTouch = false;
- }
- };
-
- /**
- * @return current time
- */
- private long refreshNow() {
- if (MusicUtils.mService == null)
- return 500;
- try {
- long pos = mPosOverride < 0 ? MusicUtils.mService.position() : mPosOverride;
- long remaining = 1000 - (pos % 1000);
- if ((pos >= 0) && (mDuration > 0)) {
- mCurrentTime.setText(MusicUtils.makeTimeString(getActivity(), pos / 1000));
-
- if (MusicUtils.mService.isPlaying()) {
- mCurrentTime.setVisibility(View.VISIBLE);
- mCurrentTime.setTextColor(getResources().getColor(R.color.transparent_black));
- // Theme chooser
- ThemeUtils.setTextColor(getActivity(), mCurrentTime, "audio_player_text_color");
- } else {
- // blink the counter
- int col = mCurrentTime.getCurrentTextColor();
- mCurrentTime.setTextColor(col == getResources().getColor(
- R.color.transparent_black) ? getResources().getColor(
- R.color.holo_blue_dark) : getResources().getColor(
- R.color.transparent_black));
- remaining = 500;
- // Theme chooser
- ThemeUtils.setTextColor(getActivity(), mCurrentTime, "audio_player_text_color");
- }
-
- mProgress.setProgress((int)(1000 * pos / mDuration));
- } else {
- mCurrentTime.setText("--:--");
- mProgress.setProgress(1000);
- }
- return remaining;
- } catch (RemoteException ex) {
- ex.printStackTrace();
- }
- return 500;
- }
-
- /**
- * Update what's playing
- */
- private void updateMusicInfo() {
- if (MusicUtils.mService == null) {
- return;
- }
-
- String artistName = MusicUtils.getArtistName();
- String albumName = MusicUtils.getAlbumName();
- String trackName = MusicUtils.getTrackName();
- mTrackName.setText(trackName);
- mAlbumArtistName.setText(albumName + " - " + artistName);
- mDuration = MusicUtils.getDuration();
- mTotalTime.setText(MusicUtils.makeTimeString(getActivity(), mDuration / 1000));
-
- if (ApolloUtils.getImageURL(albumName, ALBUM_IMAGE, getActivity()) == null)
- new LastfmGetAlbumImages(getActivity(), mAlbumArt, 1).executeOnExecutor(
- AsyncTask.THREAD_POOL_EXECUTOR, artistName, albumName);
-
- new GetCachedImages(getActivity(), 1, mAlbumArt).executeOnExecutor(
- AsyncTask.THREAD_POOL_EXECUTOR, albumName);
-
- // Theme chooser
- ThemeUtils.setTextColor(getActivity(), mTrackName, "audio_player_text_color");
- ThemeUtils.setTextColor(getActivity(), mAlbumArtistName, "audio_player_text_color");
- ThemeUtils.setTextColor(getActivity(), mTotalTime, "audio_player_text_color");
-
- }
-
- /**
- * Takes you into the @TracksBrowser to view all of the tracks on the
- * current album
- */
- private void tracksBrowser() {
-
- String artistName = MusicUtils.getArtistName();
- String albumName = MusicUtils.getAlbumName();
- long id = MusicUtils.getCurrentAlbumId();
-
- Bundle bundle = new Bundle();
- bundle.putString(MIME_TYPE, Audio.Albums.CONTENT_TYPE);
- bundle.putString(ARTIST_KEY, artistName);
- bundle.putString(ALBUM_KEY, albumName);
- bundle.putLong(BaseColumns._ID, id);
-
- Intent intent = new Intent(Intent.ACTION_VIEW);
- intent.setClass(getActivity(), TracksBrowser.class);
- intent.putExtras(bundle);
- getActivity().startActivity(intent);
- }
-
- /**
- * Takes you into the @TracksBrowser to view all of the tracks and albums by
- * the current artist
- */
- private void tracksBrowserArtist() {
-
- String artistName = MusicUtils.getArtistName();
- long id = MusicUtils.getCurrentArtistId();
-
- Bundle bundle = new Bundle();
- bundle.putString(MIME_TYPE, Audio.Artists.CONTENT_TYPE);
- bundle.putString(ARTIST_KEY, artistName);
- bundle.putLong(BaseColumns._ID, id);
-
- ApolloUtils.setArtistId(artistName, id, ARTIST_ID, getActivity());
-
- Intent intent = new Intent(Intent.ACTION_VIEW);
- intent.setClass(getActivity(), TracksBrowser.class);
- intent.putExtras(bundle);
- getActivity().startActivity(intent);
- }
-}
diff --git a/src/com/andrew/apollo/BottomActionBarControlsFragment.java b/src/com/andrew/apollo/BottomActionBarControlsFragment.java
deleted file mode 100644
index ef2723b..0000000
--- a/src/com/andrew/apollo/BottomActionBarControlsFragment.java
+++ /dev/null
@@ -1,283 +0,0 @@
-/**
- *
- */
-
-package com.andrew.apollo;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.os.Bundle;
-import android.os.RemoteException;
-import android.support.v4.app.Fragment;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.view.ViewGroup;
-import android.widget.ImageButton;
-import android.widget.ImageView;
-import android.widget.Toast;
-
-import com.andrew.apollo.service.ApolloService;
-import com.andrew.apollo.utils.ApolloUtils;
-import com.andrew.apollo.utils.MusicUtils;
-import com.andrew.apollo.utils.ThemeUtils;
-
-/**
- * @author Andrew Neal
- */
-public class BottomActionBarControlsFragment extends Fragment {
-
- private ImageButton mRepeat, mPrev, mPlay, mNext, mShuffle;
-
- private ImageView mDivider;
-
- // Notify if repeat or shuffle changes
- private Toast mToast;
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
- View root = inflater.inflate(R.layout.bottom_action_bar_controls, container, false);
-
- mRepeat = (ImageButton)root.findViewById(R.id.bottom_action_bar_repeat);
- mRepeat.setOnClickListener(new OnClickListener() {
-
- @Override
- public void onClick(View v) {
- cycleRepeat();
- }
- });
-
- mPrev = (ImageButton)root.findViewById(R.id.bottom_action_bar_previous);
- mPrev.setOnClickListener(new OnClickListener() {
-
- @Override
- public void onClick(View v) {
- if (MusicUtils.mService == null)
- return;
- try {
- if (MusicUtils.mService.position() < 2000) {
- MusicUtils.mService.prev();
- } else {
- MusicUtils.mService.seek(0);
- MusicUtils.mService.play();
- }
- } catch (RemoteException ex) {
- ex.printStackTrace();
- }
- }
- });
-
- mPlay = (ImageButton)root.findViewById(R.id.bottom_action_bar_play);
- mPlay.setOnClickListener(new OnClickListener() {
-
- @Override
- public void onClick(View v) {
- doPauseResume();
- }
- });
-
- mNext = (ImageButton)root.findViewById(R.id.bottom_action_bar_next);
- mNext.setOnClickListener(new OnClickListener() {
-
- @Override
- public void onClick(View v) {
- if (MusicUtils.mService == null)
- return;
- try {
- MusicUtils.mService.next();
- } catch (RemoteException ex) {
- ex.printStackTrace();
- }
- }
- });
-
- mShuffle = (ImageButton)root.findViewById(R.id.bottom_action_bar_shuffle);
- mShuffle.setOnClickListener(new OnClickListener() {
-
- @Override
- public void onClick(View v) {
- toggleShuffle();
- }
- });
-
- mDivider = (ImageView)root.findViewById(R.id.bottom_action_bar_control_divider);
- // Theme chooser
- ThemeUtils.setImageButton(getActivity(), mPrev, "apollo_previous");
- ThemeUtils.setImageButton(getActivity(), mNext, "apollo_next");
- ThemeUtils.setBackgroundColor(getActivity(), mDivider, "bottom_action_bar_info_divider");
- return root;
- }
-
- /**
- * Update everything as the meta or playstate changes
- */
- private final BroadcastReceiver mStatusListener = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- setPauseButtonImage();
- setShuffleButtonImage();
- setRepeatButtonImage();
- }
- };
-
- @Override
- public void onStart() {
- super.onStart();
- IntentFilter f = new IntentFilter();
- f.addAction(ApolloService.PLAYSTATE_CHANGED);
- getActivity().registerReceiver(mStatusListener, new IntentFilter(f));
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- getActivity().unregisterReceiver(mStatusListener);
- }
-
- /**
- * Cycle repeat states
- */
- private void cycleRepeat() {
- if (MusicUtils.mService == null) {
- return;
- }
- try {
- int mode = MusicUtils.mService.getRepeatMode();
- if (mode == ApolloService.REPEAT_NONE) {
- MusicUtils.mService.setRepeatMode(ApolloService.REPEAT_ALL);
- ApolloUtils.showToast(R.string.repeat_all, mToast, getActivity());
- } else if (mode == ApolloService.REPEAT_ALL) {
- MusicUtils.mService.setRepeatMode(ApolloService.REPEAT_CURRENT);
- if (MusicUtils.mService.getShuffleMode() != ApolloService.SHUFFLE_NONE) {
- MusicUtils.mService.setShuffleMode(ApolloService.SHUFFLE_NONE);
- setShuffleButtonImage();
- }
- ApolloUtils.showToast(R.string.repeat_one, mToast, getActivity());
- } else {
- MusicUtils.mService.setRepeatMode(ApolloService.REPEAT_NONE);
- ApolloUtils.showToast(R.string.repeat_off, mToast, getActivity());
- }
- setRepeatButtonImage();
- } catch (RemoteException ex) {
- ex.printStackTrace();
- }
-
- }
-
- /**
- * Play and pause music
- */
- private void doPauseResume() {
- try {
- if (MusicUtils.mService != null) {
- if (MusicUtils.mService.isPlaying()) {
- MusicUtils.mService.pause();
- } else {
- MusicUtils.mService.play();
- }
- }
- setPauseButtonImage();
- } catch (RemoteException ex) {
- ex.printStackTrace();
- }
- }
-
- /**
- * Set the shuffle mode
- */
- private void toggleShuffle() {
- if (MusicUtils.mService == null) {
- return;
- }
- try {
- int shuffle = MusicUtils.mService.getShuffleMode();
- if (shuffle == ApolloService.SHUFFLE_NONE) {
- MusicUtils.mService.setShuffleMode(ApolloService.SHUFFLE_NORMAL);
- if (MusicUtils.mService.getRepeatMode() == ApolloService.REPEAT_CURRENT) {
- MusicUtils.mService.setRepeatMode(ApolloService.REPEAT_ALL);
- setRepeatButtonImage();
- }
- ApolloUtils.showToast(R.string.shuffle_on, mToast, getActivity());
- } else if (shuffle == ApolloService.SHUFFLE_NORMAL
- || shuffle == ApolloService.SHUFFLE_AUTO) {
- MusicUtils.mService.setShuffleMode(ApolloService.SHUFFLE_NONE);
- ApolloUtils.showToast(R.string.shuffle_off, mToast, getActivity());
- }
- setShuffleButtonImage();
- } catch (RemoteException ex) {
- ex.printStackTrace();
- }
- }
-
- /**
- * Set the repeat images
- */
- private void setRepeatButtonImage() {
- if (MusicUtils.mService == null)
- return;
- try {
- switch (MusicUtils.mService.getRepeatMode()) {
- case ApolloService.REPEAT_ALL:
- mRepeat.setImageResource(R.drawable.apollo_holo_light_repeat_all);
- break;
- case ApolloService.REPEAT_CURRENT:
- mRepeat.setImageResource(R.drawable.apollo_holo_light_repeat_one);
- break;
- default:
- mRepeat.setImageResource(R.drawable.apollo_holo_light_repeat_normal);
- // Theme chooser
- ThemeUtils.setImageButton(getActivity(), mRepeat, "apollo_repeat_normal");
- break;
- }
- } catch (RemoteException ex) {
- ex.printStackTrace();
- }
- }
-
- /**
- * Set the shuffle images
- */
- private void setShuffleButtonImage() {
- if (MusicUtils.mService == null)
- return;
- try {
- switch (MusicUtils.mService.getShuffleMode()) {
- case ApolloService.SHUFFLE_NONE:
- mShuffle.setImageResource(R.drawable.apollo_holo_light_shuffle_normal);
- // Theme chooser
- ThemeUtils.setImageButton(getActivity(), mShuffle, "apollo_shuffle_normal");
- break;
- case ApolloService.SHUFFLE_AUTO:
- mShuffle.setImageResource(R.drawable.apollo_holo_light_shuffle_on);
- break;
- default:
- mShuffle.setImageResource(R.drawable.apollo_holo_light_shuffle_on);
- break;
- }
- } catch (RemoteException ex) {
- ex.printStackTrace();
- }
- }
-
- /**
- * Set the play and pause image
- */
- private void setPauseButtonImage() {
- try {
- if (MusicUtils.mService != null && MusicUtils.mService.isPlaying()) {
- mPlay.setImageResource(R.drawable.apollo_holo_light_pause);
- // Theme chooser
- ThemeUtils.setImageButton(getActivity(), mPlay, "apollo_pause");
- } else {
- mPlay.setImageResource(R.drawable.apollo_holo_light_play);
- // Theme chooser
- ThemeUtils.setImageButton(getActivity(), mPlay, "apollo_play");
- }
- } catch (RemoteException ex) {
- ex.printStackTrace();
- }
- }
-
-}
diff --git a/src/com/andrew/apollo/BottomActionBarFragment.java b/src/com/andrew/apollo/BottomActionBarFragment.java
deleted file mode 100644
index a1f7136..0000000
--- a/src/com/andrew/apollo/BottomActionBarFragment.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/**
- *
- */
-
-package com.andrew.apollo;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.os.Bundle;
-import android.support.v4.app.Fragment;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import com.andrew.apollo.service.ApolloService;
-import com.andrew.apollo.ui.widgets.BottomActionBar;
-
-/**
- * @author Andrew Neal
- */
-public class BottomActionBarFragment extends Fragment {
-
- private BottomActionBar mBottomActionBar;
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
- View root = inflater.inflate(R.layout.bottom_action_bar, container, false);
- mBottomActionBar = new BottomActionBar(getActivity());
- return root;
- }
-
- /**
- * Update the list as needed
- */
- private final BroadcastReceiver mMediaStatusReceiver = new BroadcastReceiver() {
-
- @Override
- public void onReceive(Context context, Intent intent) {
- if (mBottomActionBar != null) {
- mBottomActionBar.updateBottomActionBar(getActivity());
- }
- }
- };
-
- @Override
- public void onStart() {
- super.onStart();
- IntentFilter filter = new IntentFilter();
- filter.addAction(ApolloService.META_CHANGED);
- getActivity().registerReceiver(mMediaStatusReceiver, filter);
- }
-
- @Override
- public void onStop() {
- getActivity().unregisterReceiver(mMediaStatusReceiver);
- super.onStop();
- }
-}
diff --git a/src/com/andrew/apollo/Config.java b/src/com/andrew/apollo/Config.java
new file mode 100644
index 0000000..944a35b
--- /dev/null
+++ b/src/com/andrew/apollo/Config.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo;
+
+/**
+ * App-wide constants.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public final class Config {
+
+ /* This class is never initiated. */
+ public Config() {
+ }
+
+ /**
+ * My personal Last.fm API key, please use your own.
+ */
+ public static final String LASTFM_API_KEY = "0bec3f7ec1f914d7c960c12a916c8fb3";
+
+ /**
+ * Used to distinguish album art from artist images
+ */
+ public static final String ALBUM_ART_SUFFIX = "album";
+
+ /**
+ * The ID of an artist, album, genre, or playlist passed to the profile
+ * activity
+ */
+ public static final String ID = "id";
+
+ /**
+ * The name of an artist, album, genre, or playlist passed to the profile
+ * activity
+ */
+ public static final String NAME = "name";
+
+ /**
+ * The name of an artist passed to the profile activity
+ */
+ public static final String ARTIST_NAME = "artist_name";
+
+ /**
+ * The year an album was released passed to the profile activity
+ */
+ public static final String ALBUM_YEAR = "album_year";
+
+ /**
+ * The MIME type passed to a the profile activity
+ */
+ public static final String MIME_TYPE = "mime_type";
+
+ /**
+ * Play from search intent
+ */
+ public static final String PLAY_FROM_SEARCH = "android.media.action.MEDIA_PLAY_FROM_SEARCH";
+}
diff --git a/src/com/andrew/apollo/Constants.java b/src/com/andrew/apollo/Constants.java
deleted file mode 100644
index 9514080..0000000
--- a/src/com/andrew/apollo/Constants.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/**
- *
- */
-
-package com.andrew.apollo;
-
-/**
- * @author Andrew Neal
- */
-public interface Constants {
-
- // Last.fm API
- public String LASTFM_API_KEY = "0bec3f7ec1f914d7c960c12a916c8fb3";
-
- // Tab titles
- public String[] mTitles = {
- "RECENT", "ARTISTS", "ALBUMS", "SONGS", "PLAYLISTS", "GENRES"
- };
-
- // SharedPreferences
- public String APOLLO = "Apollo", APOLLO_PREFERENCES = "apollopreferences",
- ARTIST_IMAGE = "artistimage", ARTIST_IMAGE_ORIGINAL = "artistimageoriginal",
- ALBUM_IMAGE = "albumimage", ARTIST_KEY = "artist", ALBUM_KEY = "album",
- GENRE_KEY = "genres", ARTIST_ID = "artistid", NUMWEEKS = "numweeks",
- PLAYLIST_NAME_FAVORITES = "Favorites", PLAYLIST_NAME = "playlist",
- THEME_PACKAGE_NAME = "themePackageName", THEME_DESCRIPTION = "themeDescription",
- THEME_PREVIEW = "themepreview", THEME_TITLE = "themeTitle";
-
- // Bundle & Intent type
- public String MIME_TYPE = "mimetype", INTENT_ACTION = "action", DATA_SCHEME = "file";
-
- // Storage Volume
- public String EXTERNAL = "external";
-
- // Playlists
- public final static long PLAYLIST_UNKNOWN = -1, PLAYLIST_ALL_SONGS = -2, PLAYLIST_QUEUE = -3,
- PLAYLIST_NEW = -4, PLAYLIST_FAVORITES = -5, PLAYLIST_RECENTLY_ADDED = -6;
-
- // Genres
- public final static String[] GENRES_DB = {
- "Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge", "Hip-Hop",
- "Jazz", "Metal", "New Age", "Oldies", "Other", "Pop", "R&B", "Rap", "Reggae", "Rock",
- "Techno", "Industrial", "Alternative", "Ska", "Death Metal", "Pranks", "Soundtrack",
- "Euro-Techno", "Ambient", "Trip-Hop", "Vocal", "Jazz+Funk", "Fusion", "Trance",
- "Classical", "Instrumental", "Acid", "House", "Game", "Sound Clip", "Gospel", "Noise",
- "AlternRock", "Bass", "Soul", "Punk", "Space", "Meditative", "Instrumental Pop",
- "Instrumental Rock", "Ethnic", "Gothic", "Darkwave", "Techno-Industrial", "Electronic",
- "Pop-Folk", "Eurodance", "Dream", "Southern Rock", "Comedy", "Cult", "Gangsta",
- "Top 40", "Christian Rap", "Pop/Funk", "Jungle", "Native American", "Cabaret",
- "New Wave", "Psychedelic", "Rave", "Showtunes", "Trailer", "Lo-Fi", "Tribal",
- "Acid Punk", "Acid Jazz", "Polka", "Retro", "Musical", "Rock & Roll", "Hard Rock",
- "Folk", "Folk-Rock", "National Folk", "Swing", "Fast Fusion", "Bebob", "Latin",
- "Revival", "Celtic", "Bluegrass", "Avantgarde", "Gothic Rock", "Progressive Rock",
- "Psychedelic Rock", "Symphonic Rock", "Slow Rock", "Big Band", "Chorus",
- "Easy Listening", "Acoustic", "Humour", "Speech", "Chanson", "Opera", "Chamber Music",
- "Sonata", "Symphony", "Booty Bass", "Primus", "Porn Groove", "Satire", "Slow Jam",
- "Club", "Tango", "Samba", "Folklore", "Ballad", "Power Ballad", "Rhythmic Soul",
- "Freestyle", "Duet", "Punk Rock", "Drum Solo", "A capella", "Euro-House", "Dance Hall",
- "Goa", "Drum & Bass", "Club-House", "Hardcore", "Terror", "Indie", "Britpop",
- "Negerpunk", "Polsk Punk", "Beat", "Christian Gangsta Rap", "Heavy Metal",
- "Black Metal", "Crossover", "Contemporary Christian", "Christian Rock ", "Merengue",
- "Salsa", "Thrash Metal", "Anime", "JPop", "Synthpop"
- };
-
- // Theme item type
- public final static int THEME_ITEM_BACKGROUND = 0, THEME_ITEM_FOREGROUND = 1;
-
- public final static String INTENT_ADD_TO_PLAYLIST = "com.andrew.apollo.ADD_TO_PLAYLIST",
- INTENT_PLAYLIST_LIST = "playlistlist",
- INTENT_CREATE_PLAYLIST = "com.andrew.apollo.CREATE_PLAYLIST",
- INTENT_RENAME_PLAYLIST = "com.andrew.apollo.RENAME_PLAYLIST",
- INTENT_KEY_RENAME = "rename", INTENT_KEY_DEFAULT_NAME = "default_name";
-
-}
diff --git a/src/com/andrew/apollo/IApolloService.aidl b/src/com/andrew/apollo/IApolloService.aidl
index b44edef..89624e3 100644
--- a/src/com/andrew/apollo/IApolloService.aidl
+++ b/src/com/andrew/apollo/IApolloService.aidl
@@ -6,37 +6,37 @@ interface IApolloService
{
void openFile(String path);
void open(in long [] list, int position);
- int getQueuePosition();
- boolean isPlaying();
void stop();
void pause();
void play();
void prev();
void next();
+ void enqueue(in long [] list, int action);
+ void setQueuePosition(int index);
+ void setShuffleMode(int shufflemode);
+ void setRepeatMode(int repeatmode);
+ void moveQueueItem(int from, int to);
+ void toggleFavorite();
+ void refresh();
+ boolean isFavorite();
+ boolean isPlaying();
+ long [] getQueue();
long duration();
long position();
long seek(long pos);
- String getTrackName();
- String getAlbumName();
+ long getAudioId();
+ long getArtistId();
long getAlbumId();
String getArtistName();
- long getArtistId();
- void enqueue(in long [] list, int action);
- long [] getQueue();
- void setQueuePosition(int index);
+ String getTrackName();
+ String getAlbumName();
String getPath();
- long getAudioId();
- void setShuffleMode(int shufflemode);
+ int getQueuePosition();
int getShuffleMode();
int removeTracks(int first, int last);
- int removeTrack(long id);
- void setRepeatMode(int repeatmode);
+ int removeTrack(long id);
int getRepeatMode();
int getMediaMountedCount();
int getAudioSessionId();
- void addToFavorites(long id);
- void removeFromFavorites(long id);
- boolean isFavorite(long id);
- void toggleFavorite();
}
diff --git a/src/com/andrew/apollo/service/MediaButtonIntentReceiver.java b/src/com/andrew/apollo/MediaButtonIntentReceiver.java
index a95214c..080ab93 100644
--- a/src/com/andrew/apollo/service/MediaButtonIntentReceiver.java
+++ b/src/com/andrew/apollo/MediaButtonIntentReceiver.java
@@ -1,20 +1,15 @@
/*
- * 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.
+ * 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.
*/
-package com.andrew.apollo.service;
+package com.andrew.apollo;
import android.content.BroadcastReceiver;
import android.content.Context;
@@ -24,14 +19,20 @@ import android.os.Handler;
import android.os.Message;
import android.view.KeyEvent;
-import com.andrew.apollo.activities.AudioPlayerHolder;
+import com.andrew.apollo.ui.activities.HomeActivity;
+/**
+ * Used to control headset playback. Single press: pause/resume. Double press:
+ * next track Long press: voice search.
+ */
public class MediaButtonIntentReceiver extends BroadcastReceiver {
private static final int MSG_LONGPRESS_TIMEOUT = 1;
private static final int LONG_PRESS_DELAY = 1000;
+ private static final int DOUBLE_CLICK = 800;
+
private static long mLastClickTime = 0;
private static boolean mDown = false;
@@ -39,15 +40,18 @@ public class MediaButtonIntentReceiver extends BroadcastReceiver {
private static boolean mLaunched = false;
private static Handler mHandler = new Handler() {
+
+ /**
+ * {@inheritDoc}
+ */
@Override
- public void handleMessage(Message msg) {
+ public void handleMessage(final Message msg) {
switch (msg.what) {
case MSG_LONGPRESS_TIMEOUT:
if (!mLaunched) {
- Context context = (Context)msg.obj;
- Intent i = new Intent();
- i.putExtra("autoshuffle", "true");
- i.setClass(context, AudioPlayerHolder.class);
+ final Context context = (Context)msg.obj;
+ final Intent i = new Intent();
+ i.setClass(context, HomeActivity.class);
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
context.startActivity(i);
mLaunched = true;
@@ -57,57 +61,53 @@ public class MediaButtonIntentReceiver extends BroadcastReceiver {
}
};
+ /**
+ * {@inheritDoc}
+ */
@Override
- public void onReceive(Context context, Intent intent) {
- String intentAction = intent.getAction();
+ public void onReceive(final Context context, final Intent intent) {
+ final String intentAction = intent.getAction();
if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intentAction)) {
- Intent i = new Intent(context, ApolloService.class);
- i.setAction(ApolloService.SERVICECMD);
- i.putExtra(ApolloService.CMDNAME, ApolloService.CMDPAUSE);
+ final Intent i = new Intent(context, MusicPlaybackService.class);
+ i.setAction(MusicPlaybackService.SERVICECMD);
+ i.putExtra(MusicPlaybackService.CMDNAME, MusicPlaybackService.CMDPAUSE);
context.startService(i);
} else if (Intent.ACTION_MEDIA_BUTTON.equals(intentAction)) {
- KeyEvent event = (KeyEvent)intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
-
+ final KeyEvent event = (KeyEvent)intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
if (event == null) {
return;
}
- int keycode = event.getKeyCode();
- int action = event.getAction();
- long eventtime = event.getEventTime();
- int buttonId = intent.getIntExtra(ApolloService.CMDNOTIF, 0);
-
- // single quick press: pause/resume.
- // double press: next track
- // long press: start auto-shuffle mode.
+ final int keycode = event.getKeyCode();
+ final int action = event.getAction();
+ final long eventtime = event.getEventTime();
String command = null;
switch (keycode) {
case KeyEvent.KEYCODE_MEDIA_STOP:
- command = ApolloService.CMDSTOP;
+ command = MusicPlaybackService.CMDSTOP;
break;
case KeyEvent.KEYCODE_HEADSETHOOK:
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
- command = ApolloService.CMDTOGGLEPAUSE;
+ command = MusicPlaybackService.CMDTOGGLEPAUSE;
break;
case KeyEvent.KEYCODE_MEDIA_NEXT:
- command = ApolloService.CMDNEXT;
+ command = MusicPlaybackService.CMDNEXT;
break;
case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
- command = ApolloService.CMDPREVIOUS;
+ command = MusicPlaybackService.CMDPREVIOUS;
break;
case KeyEvent.KEYCODE_MEDIA_PAUSE:
- command = ApolloService.CMDPAUSE;
+ command = MusicPlaybackService.CMDPAUSE;
break;
case KeyEvent.KEYCODE_MEDIA_PLAY:
- command = ApolloService.CMDPLAY;
+ command = MusicPlaybackService.CMDPLAY;
break;
}
-
if (command != null) {
if (action == KeyEvent.ACTION_DOWN) {
- if (mDown && (buttonId == 0)) {
- if ((ApolloService.CMDTOGGLEPAUSE.equals(command) || ApolloService.CMDPLAY
+ if (mDown) {
+ if ((MusicPlaybackService.CMDTOGGLEPAUSE.equals(command) || MusicPlaybackService.CMDPLAY
.equals(command))
&& mLastClickTime != 0
&& eventtime - mLastClickTime > LONG_PRESS_DELAY) {
@@ -115,7 +115,7 @@ public class MediaButtonIntentReceiver extends BroadcastReceiver {
context));
}
} else if (event.getRepeatCount() == 0) {
- // only consider the first event in a sequence, not the
+ // Only consider the first event in a sequence, not the
// repeat events,
// so that we don't trigger in cases where the first
// event went to
@@ -126,24 +126,20 @@ public class MediaButtonIntentReceiver extends BroadcastReceiver {
// The service may or may not be running, but we need to
// send it
// a command.
- Intent i = new Intent(context, ApolloService.class);
- i.setAction(ApolloService.SERVICECMD);
- i.putExtra(ApolloService.CMDNOTIF, buttonId);
+ final Intent i = new Intent(context, MusicPlaybackService.class);
+ i.setAction(MusicPlaybackService.SERVICECMD);
if (keycode == KeyEvent.KEYCODE_HEADSETHOOK
- && eventtime - mLastClickTime < 300) {
- i.putExtra(ApolloService.CMDNAME, ApolloService.CMDNEXT);
+ && eventtime - mLastClickTime < DOUBLE_CLICK) {
+ i.putExtra(MusicPlaybackService.CMDNAME, MusicPlaybackService.CMDNEXT);
context.startService(i);
mLastClickTime = 0;
} else {
- i.putExtra(ApolloService.CMDNAME, command);
+ i.putExtra(MusicPlaybackService.CMDNAME, command);
context.startService(i);
mLastClickTime = eventtime;
}
-
mLaunched = false;
- if (buttonId == 0) {
- mDown = true;
- }
+ mDown = true;
}
} else {
mHandler.removeMessages(MSG_LONGPRESS_TIMEOUT);
diff --git a/src/com/andrew/apollo/MusicPlaybackService.java b/src/com/andrew/apollo/MusicPlaybackService.java
new file mode 100644
index 0000000..3877202
--- /dev/null
+++ b/src/com/andrew/apollo/MusicPlaybackService.java
@@ -0,0 +1,2981 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo;
+
+import android.annotation.SuppressLint;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.appwidget.AppWidgetManager;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.media.AudioManager;
+import android.media.AudioManager.OnAudioFocusChangeListener;
+import android.media.MediaMetadataRetriever;
+import android.media.MediaPlayer;
+import android.media.MediaPlayer.OnCompletionListener;
+import android.media.RemoteControlClient;
+import android.media.audiofx.AudioEffect;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Audio.AudioColumns;
+
+import com.andrew.apollo.appwidgets.AppWidgetLarge;
+import com.andrew.apollo.appwidgets.AppWidgetLargeAlternate;
+import com.andrew.apollo.appwidgets.AppWidgetSmall;
+import com.andrew.apollo.appwidgets.RecentWidgetProvider;
+import com.andrew.apollo.cache.ImageCache;
+import com.andrew.apollo.cache.ImageFetcher;
+import com.andrew.apollo.provider.FavoritesStore;
+import com.andrew.apollo.provider.RecentStore;
+import com.andrew.apollo.utils.ApolloUtils;
+import com.andrew.apollo.utils.Lists;
+import com.andrew.apollo.utils.MusicUtils;
+import com.andrew.apollo.utils.PreferenceUtils;
+import com.andrew.apollo.utils.SharedPreferencesCompat;
+
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.util.LinkedList;
+import java.util.Random;
+import java.util.TreeSet;
+
+/**
+ * A backbround {@link Service} used to keep music playing between activities
+ * and when the user moves Apollo into the background.
+ */
+@SuppressLint("NewApi")
+public class MusicPlaybackService extends Service {
+
+ /**
+ * Indicates that the music has paused or resumed
+ */
+ public static final String PLAYSTATE_CHANGED = "com.andrew.apollo.playstatechanged";
+
+ /**
+ * Indicates the meta data has changed in some way, like a track change
+ */
+ public static final String META_CHANGED = "com.andrew.apollo.metachanged";
+
+ /**
+ * Indicates the queue has been updated
+ */
+ public static final String QUEUE_CHANGED = "com.andrew.apollo.queuechanged";
+
+ /**
+ * Indicates the repeat mode chaned
+ */
+ public static final String REPEATMODE_CHANGED = "com.andrew.apollo.repeatmodechanged";
+
+ /**
+ * Indicates the shuffle mode chaned
+ */
+ public static final String SHUFFLEMODE_CHANGED = "com.andrew.apollo.shufflemodechanged";
+
+ /**
+ * Called to indicate a general service commmand. Used in
+ * {@link MediaButtonIntentReceiver}
+ */
+ public static final String SERVICECMD = "com.andrew.apollo.musicservicecommand";
+
+ /**
+ * Called to go toggle between pausing and playing the music
+ */
+ public static final String TOGGLEPAUSE_ACTION = "com.andrew.apollo.togglepause";
+
+ /**
+ * Called to go to pause the playback
+ */
+ public static final String PAUSE_ACTION = "com.andrew.apollo.pause";
+
+ /**
+ * Called to go to stop the playback
+ */
+ public static final String STOP_ACTION = "com.andrew.apollo.stop";
+
+ /**
+ * Called to go to the previous track
+ */
+ public static final String PREVIOUS_ACTION = "com.andrew.apollo.previous";
+
+ /**
+ * Called to go to the next track
+ */
+ public static final String NEXT_ACTION = "com.andrew.apollo.next";
+
+ /**
+ * Called to change the repeat mode
+ */
+ public static final String REPEAT_ACTION = "com.andrew.apollo.repeat";
+
+ /**
+ * Called to change the shuffle mode
+ */
+ public static final String SHUFFLE_ACTION = "com.andrew.apollo.shuffle";
+
+ /**
+ * Called to kill the notification while Apollo is in the foreground
+ */
+ public static final String KILL_FOREGROUND = "com.andrew.apollo.killforeground";
+
+ /**
+ * Used to easily notify a list that it should refresh. i.e. A playlist
+ * changes
+ */
+ public static final String REFRESH = "com.andrew.apollo.refresh";
+
+ /**
+ * Called to build the notification while Apollo is in the background
+ */
+ public static final String START_BACKGROUND = "com.andrew.apollo.startbackground";
+
+ /**
+ * Called to update the remote control client
+ */
+ public static final String UPDATE_LOCKSCREEN = "com.andrew.apollo.updatelockscreen";
+
+ public static final String CMDNAME = "command";
+
+ public static final String CMDTOGGLEPAUSE = "togglepause";
+
+ public static final String CMDSTOP = "stop";
+
+ public static final String CMDPAUSE = "pause";
+
+ public static final String CMDPLAY = "play";
+
+ public static final String CMDPREVIOUS = "previous";
+
+ public static final String CMDNEXT = "next";
+
+ public static final String CMDNOTIF = "buttonId";
+
+ private static final int IDCOLIDX = 0;
+
+ /**
+ * Moves a list to the front of the queue
+ */
+ public static final int NOW = 1;
+
+ /**
+ * Moves a list to the next position in the queue
+ */
+ public static final int NEXT = 2;
+
+ /**
+ * Moves a list to the last position in the queue
+ */
+ public static final int LAST = 3;
+
+ /**
+ * Shuffles no songs, turns shuffling off
+ */
+ public static final int SHUFFLE_NONE = 0;
+
+ /**
+ * Shuffles all songs
+ */
+ public static final int SHUFFLE_NORMAL = 1;
+
+ /**
+ * Party shuffle
+ */
+ public static final int SHUFFLE_AUTO = 2;
+
+ /**
+ * Turns repeat off
+ */
+ public static final int REPEAT_NONE = 0;
+
+ /**
+ * Repeats the current track in a list
+ */
+ public static final int REPEAT_CURRENT = 1;
+
+ /**
+ * Repeats all the tracks in a list
+ */
+ public static final int REPEAT_ALL = 2;
+
+ /**
+ * Indicates when the track ends
+ */
+ private static final int TRACK_ENDED = 1;
+
+ /**
+ * Indicates that the current track was changed the next track
+ */
+ private static final int TRACK_WENT_TO_NEXT = 2;
+
+ /**
+ * Indicates when the release the wake lock
+ */
+ private static final int RELEASE_WAKELOCK = 3;
+
+ /**
+ * Indicates the player died
+ */
+ private static final int SERVER_DIED = 4;
+
+ /**
+ * Indicates some sort of focus change, maybe a phone call
+ */
+ private static final int FOCUSCHANGE = 5;
+
+ /**
+ * Indicates to fade the volume down
+ */
+ private static final int FADEDOWN = 6;
+
+ /**
+ * Indicates to fade the volume back up
+ */
+ private static final int FADEUP = 7;
+
+ /**
+ * Idle time before stopping the foreground notfication (1 minute)
+ */
+ private static final int IDLE_DELAY = 60000;
+
+ /**
+ * The max size allowed for the track history
+ */
+ private static final int MAX_HISTORY_SIZE = 100;
+
+ /**
+ * The columns used to retrieve any info from the current track
+ */
+ private static final String[] PROJECTION = new String[] {
+ "audio._id AS _id", MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.ALBUM,
+ MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.DATA,
+ MediaStore.Audio.Media.MIME_TYPE, MediaStore.Audio.Media.ALBUM_ID,
+ MediaStore.Audio.Media.ARTIST_ID
+ };
+
+ /**
+ * Keeps a mapping of the track history
+ */
+ private static final LinkedList<Integer> mHistory = Lists.newLinkedList();
+
+ /**
+ * Used to shuffle the tracks
+ */
+ private static final Shuffler mShuffler = new Shuffler();
+
+ /**
+ * Used to save the queue as reverse hexadecimal numbers, which we can
+ * generate faster than normal decimal or // hexadecimal numbers, which in
+ * turn allows us to save the playlist // more often without worrying too
+ * much about performance
+ */
+ private static final char HEX_DIGITS[] = new char[] {
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
+ };
+
+ /**
+ * Service stub
+ */
+ private final IBinder mBinder = new ServiceStub(this);
+
+ /**
+ * 4x1 widget
+ */
+ private final AppWidgetSmall mAppWidgetSmall = AppWidgetSmall.getInstance();
+
+ /**
+ * 4x2 widget
+ */
+ private final AppWidgetLarge mAppWidgetLarge = AppWidgetLarge.getInstance();
+
+ /**
+ * 4x2 alternate widget
+ */
+ private final AppWidgetLargeAlternate mAppWidgetLargeAlternate = AppWidgetLargeAlternate
+ .getInstance();
+
+ /**
+ * Recently listened widget
+ */
+ private final RecentWidgetProvider mRecentWidgetProvider = RecentWidgetProvider.getInstance();
+
+ /**
+ * The media player
+ */
+ private MultiPlayer mPlayer;
+
+ /**
+ * The path of the current file to play
+ */
+ private String mFileToPlay;
+
+ /**
+ * Keeps the service running when the screen is off
+ */
+ private WakeLock mWakeLock;
+
+ /**
+ * The cursor used to retrieve info on the current track and run the
+ * necessary queries to play audio files
+ */
+ private Cursor mCursor;
+
+ /**
+ * Monitors the audio state
+ */
+ private AudioManager mAudioManager;
+
+ /**
+ * Settings used to save and retrieve the queue and history
+ */
+ private SharedPreferences mPreferences;
+
+ /**
+ * Used to know when the service is active
+ */
+ private boolean mServiceInUse = false;
+
+ /**
+ * Used to know if something should be playing or not
+ */
+ private boolean mIsSupposedToBePlaying = false;
+
+ /**
+ * Used to indicate if the queue can be saved
+ */
+ private boolean mQueueIsSaveable = true;
+
+ /**
+ * Used to track what type of audio focus loss caused the playback to pause
+ */
+ private boolean mPausedByTransientLossOfFocus = false;
+
+ /**
+ * Returns true if the Apollo is sent to the background, false otherwise
+ */
+ public boolean mBuildNotification = false;
+
+ /**
+ * Lock screen controls ICS+
+ */
+ private RemoteControlClientCompat mRemoteControlClientCompat;
+
+ /**
+ * Enables the remote control client
+ */
+ private boolean mEnableLockscreenControls;
+
+ private ComponentName mMediaButtonReceiverComponent;
+
+ // We use this to distinguish between different cards when saving/restoring
+ // playlists
+ private int mCardId;
+
+ private int mPlayListLen = 0;
+
+ private int mPlayPos = -1;
+
+ private int mNextPlayPos = -1;
+
+ private int mOpenFailedCounter = 0;
+
+ private int mMediaMountedCount = 0;
+
+ private int mShuffleMode = SHUFFLE_NONE;
+
+ private int mRepeatMode = REPEAT_NONE;
+
+ private int mServiceStartId = -1;
+
+ private long[] mPlayList = null;
+
+ private long[] mAutoShuffleList = null;
+
+ private MusicPlayerHandler mPlayerHandler;
+
+ private DelayedHandler mDelayedStopHandler;
+
+ private BroadcastReceiver mUnmountReceiver = null;
+
+ /**
+ * Image cache
+ */
+ private ImageFetcher mImageFetcher;
+
+ /**
+ * Used to build the notification
+ */
+ private NotificationHelper mNotificationHelper;
+
+ /**
+ * Recently listened database
+ */
+ private RecentStore mRecentsCache;
+
+ /**
+ * Favorites database
+ */
+ private FavoritesStore mFavoritesCache;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public IBinder onBind(final Intent intent) {
+ mDelayedStopHandler.removeCallbacksAndMessages(null);
+ mServiceInUse = true;
+ return mBinder;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onUnbind(final Intent intent) {
+ mServiceInUse = false;
+ saveQueue(true);
+
+ if (mIsSupposedToBePlaying || mPausedByTransientLossOfFocus) {
+ // Something is currently playing, or will be playing once
+ // an in-progress action requesting audio focus ends, so don't stop
+ // the service now.
+ return true;
+
+ // If there is a playlist but playback is paused, then wait a while
+ // before stopping the service, so that pause/resume isn't slow.
+ // Also delay stopping the service if we're transitioning between
+ // tracks.
+ } else if (mPlayListLen > 0 || mPlayerHandler.hasMessages(TRACK_ENDED)) {
+ final Message msg = mDelayedStopHandler.obtainMessage();
+ mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY);
+ return true;
+ }
+ stopSelf(mServiceStartId);
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onRebind(final Intent intent) {
+ mDelayedStopHandler.removeCallbacksAndMessages(null);
+ mServiceInUse = true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ // Initialize the favorites and recents databases
+ mRecentsCache = RecentStore.getInstance(this);
+ mFavoritesCache = FavoritesStore.getInstance(this);
+
+ // Initialze the notification helper
+ mNotificationHelper = new NotificationHelper(this);
+
+ // Initialize the image fetcher
+ mImageFetcher = ImageFetcher.getInstance(this);
+ // Initialize the image cache
+ mImageFetcher.setImageCache(ImageCache.getInstance(this));
+
+ // Start up the thread running the service. Note that we create a
+ // separate thread because the service normally runs in the process's
+ // main thread, which we don't want to block. We also make it
+ // background priority so CPU-intensive work will not disrupt the UI.
+ final HandlerThread thread = new HandlerThread("MusicPlayerHandler",
+ android.os.Process.THREAD_PRIORITY_BACKGROUND);
+ thread.start();
+
+ // Initialize the handlers
+ mPlayerHandler = new MusicPlayerHandler(this, thread.getLooper());
+ mDelayedStopHandler = new DelayedHandler(this);
+
+ // Initialze the audio manager and register any headset controls for
+ // playback
+ mAudioManager = (AudioManager)getSystemService(Context.AUDIO_SERVICE);
+ mMediaButtonReceiverComponent = new ComponentName(getPackageName(),
+ MediaButtonIntentReceiver.class.getName());
+ mAudioManager.registerMediaButtonEventReceiver(mMediaButtonReceiverComponent);
+
+ // Use the remote control APIs (if available and the user allows it) to
+ // set the playback state
+ mEnableLockscreenControls = PreferenceUtils.getInstace(this).enableLockscreenControls();
+ setUpRemoteControlClient();
+
+ // Initialize the preferences
+ mPreferences = getSharedPreferences("Service", 0);
+ mCardId = getCardId();
+
+ registerExternalStorageListener();
+
+ // Initialze the media player
+ mPlayer = new MultiPlayer(this);
+ mPlayer.setHandler(mPlayerHandler);
+
+ // Initialze the intent filter and each action
+ final IntentFilter filter = new IntentFilter();
+ filter.addAction(SERVICECMD);
+ filter.addAction(TOGGLEPAUSE_ACTION);
+ filter.addAction(PAUSE_ACTION);
+ filter.addAction(STOP_ACTION);
+ filter.addAction(NEXT_ACTION);
+ filter.addAction(PREVIOUS_ACTION);
+ filter.addAction(REPEAT_ACTION);
+ filter.addAction(SHUFFLE_ACTION);
+ filter.addAction(KILL_FOREGROUND);
+ filter.addAction(START_BACKGROUND);
+ filter.addAction(UPDATE_LOCKSCREEN);
+ // Attach the broadcast listener
+ registerReceiver(mIntentReceiver, filter);
+
+ // Initialize the wake lock
+ final PowerManager powerManager = (PowerManager)getSystemService(Context.POWER_SERVICE);
+ mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, getClass().getName());
+ mWakeLock.setReferenceCounted(false);
+
+ // Bring the queue back
+ reloadQueue();
+ notifyChange(QUEUE_CHANGED);
+ notifyChange(META_CHANGED);
+
+ // Listen for the idle state
+ final Message message = mDelayedStopHandler.obtainMessage();
+ mDelayedStopHandler.sendMessageDelayed(message, IDLE_DELAY);
+ }
+
+ /**
+ * Initializes the remote control client
+ */
+ private void setUpRemoteControlClient() {
+ if (mEnableLockscreenControls) {
+ if (mRemoteControlClientCompat == null) {
+ final Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
+ mediaButtonIntent.setComponent(mMediaButtonReceiverComponent);
+ mRemoteControlClientCompat = new RemoteControlClientCompat(
+ PendingIntent.getBroadcast(getApplicationContext(), 0, mediaButtonIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT));
+ RemoteControlHelper.registerRemoteControlClient(mAudioManager,
+ mRemoteControlClientCompat);
+ }
+ // Flags for the media transport control that this client supports.
+ final int flags = RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS
+ | RemoteControlClient.FLAG_KEY_MEDIA_NEXT
+ | RemoteControlClient.FLAG_KEY_MEDIA_PLAY
+ | RemoteControlClient.FLAG_KEY_MEDIA_PAUSE
+ | RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE
+ | RemoteControlClient.FLAG_KEY_MEDIA_STOP;
+ mRemoteControlClientCompat.setTransportControlFlags(flags);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ // Remove any sound effects
+ if (ApolloUtils.hasGingerbread()) {
+ final Intent audioEffectsIntent = new Intent(
+ AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION);
+ audioEffectsIntent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, getAudioSessionId());
+ audioEffectsIntent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName());
+ sendBroadcast(audioEffectsIntent);
+ }
+
+ // Release the player
+ mPlayer.release();
+ mPlayer = null;
+
+ // Remove the audio focus listener and lock screen controls
+ mAudioManager.abandonAudioFocus(mAudioFocusListener);
+ RemoteControlHelper
+ .unregisterRemoteControlClient(mAudioManager, mRemoteControlClientCompat);
+
+ // Remove any callbacks from the handlers
+ mDelayedStopHandler.removeCallbacksAndMessages(null);
+ mPlayerHandler.removeCallbacksAndMessages(null);
+
+ // Close the cursor
+ if (mCursor != null) {
+ mCursor.close();
+ mCursor = null;
+ }
+
+ // Unregister the mount listener
+ unregisterReceiver(mIntentReceiver);
+ if (mUnmountReceiver != null) {
+ unregisterReceiver(mUnmountReceiver);
+ mUnmountReceiver = null;
+ }
+
+ // Release the wake lock
+ mWakeLock.release();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int onStartCommand(final Intent intent, final int flags, final int startId) {
+ mServiceStartId = startId;
+ mDelayedStopHandler.removeCallbacksAndMessages(null);
+ if (intent != null) {
+ final String action = intent.getAction();
+ final String command = intent.getStringExtra("command");
+ if (CMDNEXT.equals(command) || NEXT_ACTION.equals(action)) {
+ gotoNext(true);
+ } else if (CMDPREVIOUS.equals(command) || PREVIOUS_ACTION.equals(action)) {
+ if (position() < 2000) {
+ prev();
+ } else {
+ seek(0);
+ play();
+ }
+ } else if (CMDTOGGLEPAUSE.equals(command) || TOGGLEPAUSE_ACTION.equals(action)) {
+ if (mIsSupposedToBePlaying) {
+ pause();
+ mPausedByTransientLossOfFocus = false;
+ } else {
+ play();
+ }
+ } else if (CMDPAUSE.equals(command) || PAUSE_ACTION.equals(action)) {
+ pause();
+ mPausedByTransientLossOfFocus = false;
+ } else if (CMDPLAY.equals(command)) {
+ play();
+ } else if (CMDSTOP.equals(command) || STOP_ACTION.equals(action)) {
+ pause();
+ mPausedByTransientLossOfFocus = false;
+ seek(0);
+ killNotification();
+ mBuildNotification = false;
+ } else if (REPEAT_ACTION.equals(action)) {
+ cycleRepeat();
+ } else if (SHUFFLE_ACTION.equals(action)) {
+ cycleShuffle();
+ } else if (KILL_FOREGROUND.equals(action)) {
+ mBuildNotification = false;
+ killNotification();
+ } else if (START_BACKGROUND.equals(action)) {
+ mBuildNotification = true;
+ buildNotification();
+ } else if (UPDATE_LOCKSCREEN.equals(action)) {
+ mEnableLockscreenControls = intent.getBooleanExtra(UPDATE_LOCKSCREEN, true);
+ if (mEnableLockscreenControls) {
+ setUpRemoteControlClient();
+ // Update the controls according to the current playback
+ notifyChange(PLAYSTATE_CHANGED);
+ notifyChange(META_CHANGED);
+ } else {
+ // Remove then unregister the conrols
+ mRemoteControlClientCompat
+ .setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED);
+ RemoteControlHelper.unregisterRemoteControlClient(mAudioManager,
+ mRemoteControlClientCompat);
+ }
+ }
+ }
+
+ // Make sure the service will shut down on its own if it was
+ // just started but not bound to and nothing is playing
+ mDelayedStopHandler.removeCallbacksAndMessages(null);
+ final Message msg = mDelayedStopHandler.obtainMessage();
+ mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY);
+ return START_STICKY;
+ }
+
+ /**
+ * Builds the notification for Apollo
+ */
+ public void buildNotification() {
+ if (mBuildNotification || ApolloUtils.isApplicationSentToBackground(this)) {
+ try {
+ mNotificationHelper.buildNotification(getAlbumName(), getArtistName(),
+ getTrackName(), getAlbumId(), getAlbumArt());
+ } catch (final IllegalStateException parcelBitmap) {
+ parcelBitmap.printStackTrace();
+ }
+ }
+ }
+
+ /**
+ * Removes the foreground notification
+ */
+ public void killNotification() {
+ stopForeground(true);
+ }
+
+ /**
+ * @return A card ID used to save and restore playlists, i.e., the queue.
+ */
+ private int getCardId() {
+ final ContentResolver resolver = getContentResolver();
+ Cursor cursor = resolver.query(Uri.parse("content://media/external/fs_id"), null, null,
+ null, null);
+ int mCardId = -1;
+ if (cursor != null && cursor.moveToFirst()) {
+ mCardId = cursor.getInt(0);
+ cursor.close();
+ cursor = null;
+ }
+ return mCardId;
+ }
+
+ /**
+ * Called when we receive a ACTION_MEDIA_EJECT notification.
+ *
+ * @param storagePath The path to mount point for the removed media
+ */
+ public void closeExternalStorageFiles(final String storagePath) {
+ stop(true);
+ notifyChange(QUEUE_CHANGED);
+ notifyChange(META_CHANGED);
+ }
+
+ /**
+ * Registers an intent to listen for ACTION_MEDIA_EJECT notifications. The
+ * intent will call closeExternalStorageFiles() if the external media is
+ * going to be ejected, so applications can clean up any files they have
+ * open.
+ */
+ public void registerExternalStorageListener() {
+ if (mUnmountReceiver == null) {
+ mUnmountReceiver = new BroadcastReceiver() {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onReceive(final Context context, final Intent intent) {
+ final String action = intent.getAction();
+ if (action.equals(Intent.ACTION_MEDIA_EJECT)) {
+ saveQueue(true);
+ mQueueIsSaveable = false;
+ closeExternalStorageFiles(intent.getData().getPath());
+ } else if (action.equals(Intent.ACTION_MEDIA_MOUNTED)) {
+ mMediaMountedCount++;
+ mCardId = getCardId();
+ reloadQueue();
+ mQueueIsSaveable = true;
+ notifyChange(QUEUE_CHANGED);
+ notifyChange(META_CHANGED);
+ }
+ }
+ };
+ final IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_MEDIA_EJECT);
+ filter.addAction(Intent.ACTION_MEDIA_MOUNTED);
+ filter.addDataScheme("file");
+ registerReceiver(mUnmountReceiver, filter);
+ }
+ }
+
+ /**
+ * Changes the notification buttons to a paused state and beging the
+ * countdown to calling {@code #stopForeground(true)}
+ */
+ private void gotoIdleState() {
+ mDelayedStopHandler.removeCallbacksAndMessages(null);
+ final Message msg = mDelayedStopHandler.obtainMessage();
+ mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY);
+ if (mBuildNotification) {
+ mNotificationHelper.goToIdleState(mIsSupposedToBePlaying);
+ }
+ mDelayedStopHandler.postDelayed(new Runnable() {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void run() {
+ killNotification();
+ }
+ }, IDLE_DELAY);
+ }
+
+ /**
+ * Stops playback
+ *
+ * @param remove_status_icon True to go to the idle state, false otherwise
+ */
+ private void stop(final boolean remove_status_icon) {
+ if (mPlayer.isInitialized()) {
+ mPlayer.stop();
+ }
+ mFileToPlay = null;
+ if (mCursor != null) {
+ mCursor.close();
+ mCursor = null;
+ }
+ if (remove_status_icon) {
+ gotoIdleState();
+ } else {
+ stopForeground(false);
+ }
+ if (remove_status_icon) {
+ mIsSupposedToBePlaying = false;
+ }
+ }
+
+ /**
+ * Removes the range of tracks specified from the play list. If a file
+ * within the range is the file currently being played, playback will move
+ * to the next file after the range.
+ *
+ * @param first The first file to be removed
+ * @param last The last file to be removed
+ * @return the number of tracks deleted
+ */
+ private int removeTracksInternal(int first, int last) {
+ synchronized (this) {
+ if (last < first) {
+ return 0;
+ } else if (first < 0) {
+ first = 0;
+ } else if (last >= mPlayListLen) {
+ last = mPlayListLen - 1;
+ }
+
+ boolean gotonext = false;
+ if (first <= mPlayPos && mPlayPos <= last) {
+ mPlayPos = first;
+ gotonext = true;
+ } else if (mPlayPos > last) {
+ mPlayPos -= last - first + 1;
+ }
+ final int num = mPlayListLen - last - 1;
+ for (int i = 0; i < num; i++) {
+ mPlayList[first + i] = mPlayList[last + 1 + i];
+ }
+ mPlayListLen -= last - first + 1;
+
+ if (gotonext) {
+ if (mPlayListLen == 0) {
+ stop(true);
+ mPlayPos = -1;
+ if (mCursor != null) {
+ mCursor.close();
+ mCursor = null;
+ }
+ } else {
+ if (mPlayPos >= mPlayListLen) {
+ mPlayPos = 0;
+ }
+ final boolean wasPlaying = isPlaying();
+ stop(false);
+ openCurrentAndNext();
+ if (wasPlaying) {
+ play();
+ }
+ }
+ notifyChange(META_CHANGED);
+ }
+ return last - first + 1;
+ }
+ }
+
+ /**
+ * Adds a list to the playlist
+ *
+ * @param list The list to add
+ * @param position The position to place the tracks
+ */
+ private void addToPlayList(final long[] list, int position) {
+ final int addlen = list.length;
+ if (position < 0) {
+ mPlayListLen = 0;
+ position = 0;
+ }
+ ensurePlayListCapacity(mPlayListLen + addlen);
+ if (position > mPlayListLen) {
+ position = mPlayListLen;
+ }
+
+ final int tailsize = mPlayListLen - position;
+ for (int i = tailsize; i > 0; i--) {
+ mPlayList[position + i] = mPlayList[position + i - addlen];
+ }
+
+ for (int i = 0; i < addlen; i++) {
+ mPlayList[position + i] = list[i];
+ }
+ mPlayListLen += addlen;
+ if (mPlayListLen == 0) {
+ mCursor.close();
+ mCursor = null;
+ notifyChange(META_CHANGED);
+ }
+ }
+
+ /**
+ * @param lid The list ID
+ * @return The cursor used for a specific ID
+ */
+ private Cursor getCursorForId(final long lid) {
+ final String id = String.valueOf(lid);
+ final Cursor c = getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+ PROJECTION, "_id=" + id, null, null);
+ if (c != null) {
+ c.moveToFirst();
+ }
+ return c;
+ }
+
+ /**
+ * Called to open a new file as the current track and prepare the next for
+ * playback
+ */
+ private void openCurrentAndNext() {
+ openCurrentAndMaybeNext(true);
+ }
+
+ /**
+ * Called to open a new file as the current track and prepare the next for
+ * playback
+ *
+ * @param openNext True to prepare the next track for playback, false
+ * otherwise.
+ */
+ private void openCurrentAndMaybeNext(final boolean openNext) {
+ synchronized (this) {
+ if (mCursor != null) {
+ mCursor.close();
+ mCursor = null;
+ }
+
+ if (mPlayListLen == 0) {
+ return;
+ }
+ stop(false);
+
+ mCursor = getCursorForId(mPlayList[mPlayPos]);
+ while (true) {
+ if (mCursor != null
+ && mCursor.getCount() != 0
+ && openFile(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + "/"
+ + mCursor.getLong(IDCOLIDX))) {
+ break;
+ }
+ // if we get here then opening the file failed. We can close the
+ // cursor now, because
+ // we're either going to create a new one next, or stop trying
+ if (mCursor != null) {
+ mCursor.close();
+ mCursor = null;
+ }
+ if (mOpenFailedCounter++ < 10 && mPlayListLen > 1) {
+ final int pos = getNextPosition(false);
+ if (pos < 0) {
+ gotoIdleState();
+ if (mIsSupposedToBePlaying) {
+ mIsSupposedToBePlaying = false;
+ notifyChange(PLAYSTATE_CHANGED);
+ }
+ return;
+ }
+ mPlayPos = pos;
+ stop(false);
+ mPlayPos = pos;
+ mCursor = getCursorForId(mPlayList[mPlayPos]);
+ } else {
+ mOpenFailedCounter = 0;
+ gotoIdleState();
+ if (mIsSupposedToBePlaying) {
+ mIsSupposedToBePlaying = false;
+ notifyChange(PLAYSTATE_CHANGED);
+ }
+ return;
+ }
+ }
+ if (openNext) {
+ setNextTrack();
+ }
+ }
+ }
+
+ /**
+ * @param force True to force the player onto the track next, false
+ * otherwise.
+ * @return The next position to play.
+ */
+ private int getNextPosition(final boolean force) {
+ if (!force && mRepeatMode == REPEAT_CURRENT) {
+ if (mPlayPos < 0) {
+ return 0;
+ }
+ return mPlayPos;
+ } else if (mShuffleMode == SHUFFLE_NORMAL) {
+ if (mPlayPos >= 0) {
+ mHistory.add(mPlayPos);
+ }
+ if (mHistory.size() > MAX_HISTORY_SIZE) {
+ mHistory.remove(0);
+ }
+ final int numTracks = mPlayListLen;
+ final int[] tracks = new int[numTracks];
+ for (int i = 0; i < numTracks; i++) {
+ tracks[i] = i;
+ }
+
+ final int numHistory = mHistory.size();
+ int numUnplayed = numTracks;
+ for (int i = 0; i < numHistory; i++) {
+ final int idx = mHistory.get(i).intValue();
+ if (idx < numTracks && tracks[idx] >= 0) {
+ numUnplayed--;
+ tracks[idx] = -1;
+ }
+ }
+ if (numUnplayed <= 0) {
+ if (mRepeatMode == REPEAT_ALL || force) {
+ numUnplayed = numTracks;
+ for (int i = 0; i < numTracks; i++) {
+ tracks[i] = i;
+ }
+ } else {
+ return -1;
+ }
+ }
+ int skip = 0;
+ if (mShuffleMode == SHUFFLE_NORMAL || mShuffleMode == SHUFFLE_AUTO) {
+ skip = mShuffler.nextInt(numUnplayed);
+ }
+ int cnt = -1;
+ while (true) {
+ while (tracks[++cnt] < 0) {
+ ;
+ }
+ skip--;
+ if (skip < 0) {
+ break;
+ }
+ }
+ return cnt;
+ } else if (mShuffleMode == SHUFFLE_AUTO) {
+ doAutoShuffleUpdate();
+ return mPlayPos + 1;
+ } else {
+ if (mPlayPos >= mPlayListLen - 1) {
+ if (mRepeatMode == REPEAT_NONE && !force) {
+ return -1;
+ } else if (mRepeatMode == REPEAT_ALL || force) {
+ return 0;
+ }
+ return -1;
+ } else {
+ return mPlayPos + 1;
+ }
+ }
+ }
+
+ /**
+ * Sets the track track to be played
+ */
+ private void setNextTrack() {
+ mNextPlayPos = getNextPosition(false);
+ if (mNextPlayPos >= 0 && mPlayList != null) {
+ final long id = mPlayList[mNextPlayPos];
+ mPlayer.setNextDataSource(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + "/" + id);
+ }
+ }
+
+ /**
+ * Creates a shuffled playlist used for party mode
+ */
+ private boolean makeAutoShuffleList() {
+ Cursor cursor = null;
+ try {
+ cursor = getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+ new String[] {
+ MediaStore.Audio.Media._ID
+ }, MediaStore.Audio.Media.IS_MUSIC + "=1", null, null);
+ if (cursor == null || cursor.getCount() == 0) {
+ return false;
+ }
+ final int len = cursor.getCount();
+ final long[] list = new long[len];
+ for (int i = 0; i < len; i++) {
+ cursor.moveToNext();
+ list[i] = cursor.getLong(0);
+ }
+ mAutoShuffleList = list;
+ return true;
+ } catch (final RuntimeException e) {
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ cursor = null;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Creates the party shuffle playlist
+ */
+ private void doAutoShuffleUpdate() {
+ boolean notify = false;
+ if (mPlayPos > 10) {
+ removeTracks(0, mPlayPos - 9);
+ notify = true;
+ }
+ final int toAdd = 7 - (mPlayListLen - (mPlayPos < 0 ? -1 : mPlayPos));
+ for (int i = 0; i < toAdd; i++) {
+ int lookback = mHistory.size();
+ int idx = -1;
+ while (true) {
+ idx = mShuffler.nextInt(mAutoShuffleList.length);
+ if (!wasRecentlyUsed(idx, lookback)) {
+ break;
+ }
+ lookback /= 2;
+ }
+ mHistory.add(idx);
+ if (mHistory.size() > MAX_HISTORY_SIZE) {
+ mHistory.remove(0);
+ }
+ ensurePlayListCapacity(mPlayListLen + 1);
+ mPlayList[mPlayListLen++] = mAutoShuffleList[idx];
+ notify = true;
+ }
+ if (notify) {
+ notifyChange(QUEUE_CHANGED);
+ }
+ }
+
+ /**/
+ private boolean wasRecentlyUsed(final int idx, int lookbacksize) {
+ if (lookbacksize == 0) {
+ return false;
+ }
+ final int histsize = mHistory.size();
+ if (histsize < lookbacksize) {
+ lookbacksize = histsize;
+ }
+ final int maxidx = histsize - 1;
+ for (int i = 0; i < lookbacksize; i++) {
+ final long entry = mHistory.get(maxidx - i);
+ if (entry == idx) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Makes sure the playlist has enough space to hold all of the songs
+ *
+ * @param size The size of the playlist
+ */
+ private void ensurePlayListCapacity(final int size) {
+ if (mPlayList == null || size > mPlayList.length) {
+ // reallocate at 2x requested size so we don't
+ // need to grow and copy the array for every
+ // insert
+ final long[] newlist = new long[size * 2];
+ final int len = mPlayList != null ? mPlayList.length : mPlayListLen;
+ for (int i = 0; i < len; i++) {
+ newlist[i] = mPlayList[i];
+ }
+ mPlayList = newlist;
+ }
+ // FIXME: shrink the array when the needed size is much smaller
+ // than the allocated size
+ }
+
+ /**
+ * Notify the change-receivers that something has changed.
+ */
+ private void notifyChange(final String what) {
+ final Intent intent = new Intent(what);
+ sendStickyBroadcast(intent);
+
+ // Update the lockscreen controls
+ updateRemoteControlClient(what);
+
+ if (what.equals(META_CHANGED)) {
+ // Increase the play count for favorite songs.
+ if (mFavoritesCache.getSongId(getAudioId()) != null) {
+ mFavoritesCache.addSongId(getAudioId(), getTrackName(), getAlbumName(),
+ getArtistName());
+ }
+ // Add the track to the recently played list.
+ mRecentsCache.addAlbumId(getAlbumId(), getAlbumName(), getArtistName(),
+ MusicUtils.getSongCountForAlbum(this, getAlbumName()),
+ MusicUtils.getReleaseDateForAlbum(this, getAlbumName()));
+ } else if (what.equals(QUEUE_CHANGED)) {
+ saveQueue(true);
+ } else {
+ saveQueue(false);
+ }
+
+ // Update the app-widgets
+ mAppWidgetSmall.notifyChange(this, what);
+ mAppWidgetLarge.notifyChange(this, what);
+ mAppWidgetLargeAlternate.notifyChange(this, what);
+ if (ApolloUtils.hasHoneycomb()) {
+ mRecentWidgetProvider.notifyChange(this, what);
+ }
+ }
+
+ /**
+ * Updates the lockscreen controls, if enabled.
+ *
+ * @param what The broadcast
+ */
+ private void updateRemoteControlClient(final String what) {
+ if (mEnableLockscreenControls && mRemoteControlClientCompat != null) {
+ if (what.equals(PLAYSTATE_CHANGED)) {
+ // If the playstate change notify the lock screen
+ // controls
+ mRemoteControlClientCompat
+ .setPlaybackState(mIsSupposedToBePlaying ? RemoteControlClient.PLAYSTATE_PLAYING
+ : RemoteControlClient.PLAYSTATE_PAUSED);
+ } else if (what.equals(META_CHANGED)) {
+ // Update the ockscreen controls
+ mRemoteControlClientCompat
+ .editMetadata(true)
+ .putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, getArtistName())
+ .putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, getAlbumName())
+ .putString(MediaMetadataRetriever.METADATA_KEY_TITLE, getTrackName())
+ .putLong(MediaMetadataRetriever.METADATA_KEY_DURATION, duration())
+ .putBitmap(
+ RemoteControlClientCompat.MetadataEditorCompat.METADATA_KEY_ARTWORK,
+ getAlbumArt()).apply();
+ }
+ }
+ }
+
+ /**
+ * Saves the queue
+ *
+ * @param full True if the queue is full
+ */
+ private void saveQueue(final boolean full) {
+ if (!mQueueIsSaveable) {
+ return;
+ }
+
+ final SharedPreferences.Editor editor = mPreferences.edit();
+ if (full) {
+ final StringBuilder q = new StringBuilder();
+ int len = mPlayListLen;
+ for (int i = 0; i < len; i++) {
+ long n = mPlayList[i];
+ if (n < 0) {
+ continue;
+ } else if (n == 0) {
+ q.append("0;");
+ } else {
+ while (n != 0) {
+ final int digit = (int)(n & 0xf);
+ n >>>= 4;
+ q.append(HEX_DIGITS[digit]);
+ }
+ q.append(";");
+ }
+ }
+ editor.putString("queue", q.toString());
+ editor.putInt("cardid", mCardId);
+ if (mShuffleMode != SHUFFLE_NONE) {
+ len = mHistory.size();
+ q.setLength(0);
+ for (int i = 0; i < len; i++) {
+ int n = mHistory.get(i);
+ if (n == 0) {
+ q.append("0;");
+ } else {
+ while (n != 0) {
+ final int digit = n & 0xf;
+ n >>>= 4;
+ q.append(HEX_DIGITS[digit]);
+ }
+ q.append(";");
+ }
+ }
+ editor.putString("history", q.toString());
+ }
+ }
+ editor.putInt("curpos", mPlayPos);
+ if (mPlayer.isInitialized()) {
+ editor.putLong("seekpos", mPlayer.position());
+ }
+ editor.putInt("repeatmode", mRepeatMode);
+ editor.putInt("shufflemode", mShuffleMode);
+ SharedPreferencesCompat.apply(editor);
+ }
+
+ /**
+ * Reloads the queue as the user left it the last time they stopped using
+ * Apollo
+ */
+ private void reloadQueue() {
+ String q = null;
+ int id = mCardId;
+ if (mPreferences.contains("cardid")) {
+ id = mPreferences.getInt("cardid", ~mCardId);
+ }
+ if (id == mCardId) {
+ q = mPreferences.getString("queue", "");
+ }
+ int qlen = q != null ? q.length() : 0;
+ if (qlen > 1) {
+ int plen = 0;
+ int n = 0;
+ int shift = 0;
+ for (int i = 0; i < qlen; i++) {
+ final char c = q.charAt(i);
+ if (c == ';') {
+ ensurePlayListCapacity(plen + 1);
+ mPlayList[plen] = n;
+ plen++;
+ n = 0;
+ shift = 0;
+ } else {
+ if (c >= '0' && c <= '9') {
+ n += c - '0' << shift;
+ } else if (c >= 'a' && c <= 'f') {
+ n += 10 + c - 'a' << shift;
+ } else {
+ plen = 0;
+ break;
+ }
+ shift += 4;
+ }
+ }
+ mPlayListLen = plen;
+ final int pos = mPreferences.getInt("curpos", 0);
+ if (pos < 0 || pos >= mPlayListLen) {
+ mPlayListLen = 0;
+ return;
+ }
+ mPlayPos = pos;
+ Cursor mCursor = getContentResolver().query(
+ MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, new String[] {
+ "_id"
+ }, "_id=" + mPlayList[mPlayPos], null, null);
+ if (mCursor == null || mCursor.getCount() == 0) {
+ SystemClock.sleep(3000);
+ mCursor = getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+ PROJECTION, "_id=" + mPlayList[mPlayPos], null, null);
+ }
+ if (mCursor != null) {
+ mCursor.close();
+ mCursor = null;
+ }
+ mOpenFailedCounter = 20;
+ openCurrentAndNext();
+ if (!mPlayer.isInitialized()) {
+ mPlayListLen = 0;
+ return;
+ }
+
+ final long seekpos = mPreferences.getLong("seekpos", 0);
+ seek(seekpos >= 0 && seekpos < duration() ? seekpos : 0);
+
+ int repmode = mPreferences.getInt("repeatmode", REPEAT_NONE);
+ if (repmode != REPEAT_ALL && repmode != REPEAT_CURRENT) {
+ repmode = REPEAT_NONE;
+ }
+ mRepeatMode = repmode;
+
+ int shufmode = mPreferences.getInt("shufflemode", SHUFFLE_NONE);
+ if (shufmode != SHUFFLE_AUTO && shufmode != SHUFFLE_NORMAL) {
+ shufmode = SHUFFLE_NONE;
+ }
+ if (shufmode != SHUFFLE_NONE) {
+ q = mPreferences.getString("history", "");
+ qlen = q != null ? q.length() : 0;
+ if (qlen > 1) {
+ plen = 0;
+ n = 0;
+ shift = 0;
+ mHistory.clear();
+ for (int i = 0; i < qlen; i++) {
+ final char c = q.charAt(i);
+ if (c == ';') {
+ if (n >= mPlayListLen) {
+ mHistory.clear();
+ break;
+ }
+ mHistory.add(n);
+ n = 0;
+ shift = 0;
+ } else {
+ if (c >= '0' && c <= '9') {
+ n += c - '0' << shift;
+ } else if (c >= 'a' && c <= 'f') {
+ n += 10 + c - 'a' << shift;
+ } else {
+ mHistory.clear();
+ break;
+ }
+ shift += 4;
+ }
+ }
+ }
+ }
+ if (shufmode == SHUFFLE_AUTO) {
+ if (!makeAutoShuffleList()) {
+ shufmode = SHUFFLE_NONE;
+ }
+ }
+ mShuffleMode = shufmode;
+ }
+ }
+
+ /**
+ * Opens a file and prepares it for playback
+ *
+ * @param path The path of the file to open
+ */
+ public boolean openFile(final String path) {
+ synchronized (this) {
+ if (path == null) {
+ return false;
+ }
+
+ // If mCursor is null, try to associate path with a database cursor
+ if (mCursor == null) {
+ final ContentResolver resolver = getContentResolver();
+ Uri uri;
+ String where;
+ String selectionArgs[];
+ if (path.startsWith("content://media/")) {
+ uri = Uri.parse(path);
+ where = null;
+ selectionArgs = null;
+ } else {
+ uri = MediaStore.Audio.Media.getContentUriForPath(path);
+ where = MediaStore.Audio.Media.DATA + "=?";
+ selectionArgs = new String[] {
+ path
+ };
+ }
+ try {
+ mCursor = resolver.query(uri, PROJECTION, where, selectionArgs, null);
+ if (mCursor != null) {
+ if (mCursor.getCount() == 0) {
+ mCursor.close();
+ mCursor = null;
+ } else {
+ mCursor.moveToNext();
+ ensurePlayListCapacity(1);
+ mPlayListLen = 1;
+ mPlayList[0] = mCursor.getLong(IDCOLIDX);
+ mPlayPos = 0;
+ }
+ }
+ } catch (final UnsupportedOperationException ex) {
+ }
+ }
+ mFileToPlay = path;
+ mPlayer.setDataSource(mFileToPlay);
+ if (mPlayer.isInitialized()) {
+ mOpenFailedCounter = 0;
+ return true;
+ }
+ stop(true);
+ return false;
+ }
+ }
+
+ /**
+ * Returns the audio session ID
+ *
+ * @return The current media player audio session ID
+ */
+ public int getAudioSessionId() {
+ synchronized (this) {
+ return mPlayer.getAudioSessionId();
+ }
+ }
+
+ /**
+ * Sets the audio session ID.
+ *
+ * @param sessionId: the audio session ID.
+ */
+ public void setAudioSessionId(final int sessionId) {
+ synchronized (this) {
+ mPlayer.setAudioSessionId(sessionId);
+ }
+ }
+
+ /**
+ * Indicates if the media storeage device has been mounted or not
+ *
+ * @return 1 if Intent.ACTION_MEDIA_MOUNTED is called, 0 otherwise
+ */
+ public int getMediaMountedCount() {
+ return mMediaMountedCount;
+ }
+
+ /**
+ * Returns the shuffle mode
+ *
+ * @return The current shuffle mode (all, party, none)
+ */
+ public int getShuffleMode() {
+ return mShuffleMode;
+ }
+
+ /**
+ * Returns the repeat mode
+ *
+ * @return The current repeat mode (all, one, none)
+ */
+ public int getRepeatMode() {
+ return mRepeatMode;
+ }
+
+ /**
+ * Removes all instances of the track with the given ID from the playlist.
+ *
+ * @param id The id to be removed
+ * @return how many instances of the track were removed
+ */
+ public int removeTrack(final long id) {
+ int numremoved = 0;
+ synchronized (this) {
+ for (int i = 0; i < mPlayListLen; i++) {
+ if (mPlayList[i] == id) {
+ numremoved += removeTracksInternal(i, i);
+ i--;
+ }
+ }
+ }
+ if (numremoved > 0) {
+ notifyChange(QUEUE_CHANGED);
+ }
+ return numremoved;
+ }
+
+ /**
+ * Removes the range of tracks specified from the play list. If a file
+ * within the range is the file currently being played, playback will move
+ * to the next file after the range.
+ *
+ * @param first The first file to be removed
+ * @param last The last file to be removed
+ * @return the number of tracks deleted
+ */
+ public int removeTracks(final int first, final int last) {
+ final int numremoved = removeTracksInternal(first, last);
+ if (numremoved > 0) {
+ notifyChange(QUEUE_CHANGED);
+ }
+ return numremoved;
+ }
+
+ /**
+ * Returns the position in the queue
+ *
+ * @return the current position in the queue
+ */
+ public int getQueuePosition() {
+ synchronized (this) {
+ return mPlayPos;
+ }
+ }
+
+ /**
+ * Returns the path to current song
+ *
+ * @return The path to the current song
+ */
+ public String getPath() {
+ synchronized (this) {
+ if (mCursor == null) {
+ return null;
+ }
+ return mCursor.getString(mCursor.getColumnIndexOrThrow(AudioColumns.DATA));
+ }
+ }
+
+ /**
+ * Returns the album name
+ *
+ * @return The current song album Name
+ */
+ public String getAlbumName() {
+ synchronized (this) {
+ if (mCursor == null) {
+ return null;
+ }
+ return mCursor.getString(mCursor.getColumnIndexOrThrow(AudioColumns.ALBUM));
+ }
+ }
+
+ /**
+ * Returns the song name
+ *
+ * @return The current song name
+ */
+ public String getTrackName() {
+ synchronized (this) {
+ if (mCursor == null) {
+ return null;
+ }
+ return mCursor.getString(mCursor.getColumnIndexOrThrow(AudioColumns.TITLE));
+ }
+ }
+
+ /**
+ * Returns the artist name
+ *
+ * @return The current song artist name
+ */
+ public String getArtistName() {
+ synchronized (this) {
+ if (mCursor == null) {
+ return null;
+ }
+ return mCursor.getString(mCursor.getColumnIndexOrThrow(AudioColumns.ARTIST));
+ }
+ }
+
+ /**
+ * Returns the album ID
+ *
+ * @return The current song album ID
+ */
+ public long getAlbumId() {
+ synchronized (this) {
+ if (mCursor == null) {
+ return -1;
+ }
+ return mCursor.getLong(mCursor.getColumnIndexOrThrow(AudioColumns.ALBUM_ID));
+ }
+ }
+
+ /**
+ * Returns the artist ID
+ *
+ * @return The current song artist ID
+ */
+ public long getArtistId() {
+ synchronized (this) {
+ if (mCursor == null) {
+ return -1;
+ }
+ return mCursor.getLong(mCursor.getColumnIndexOrThrow(AudioColumns.ARTIST_ID));
+ }
+ }
+
+ /**
+ * Returns the current audio ID
+ *
+ * @return The current track ID
+ */
+ public long getAudioId() {
+ synchronized (this) {
+ if (mPlayPos >= 0 && mPlayer.isInitialized()) {
+ return mPlayList[mPlayPos];
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Seeks the current track to a specific time
+ *
+ * @param position The time to seek to
+ * @return The time to play the track at
+ */
+ public long seek(long position) {
+ if (mPlayer.isInitialized()) {
+ if (position < 0) {
+ position = 0;
+ } else if (position > mPlayer.duration()) {
+ position = mPlayer.duration();
+ }
+ return mPlayer.seek(position);
+ }
+ return -1;
+ }
+
+ /**
+ * Returns the current position in time of the currenttrack
+ *
+ * @return The current playback position in miliseconds
+ */
+ public long position() {
+ if (mPlayer.isInitialized()) {
+ return mPlayer.position();
+ }
+ return -1;
+ }
+
+ /**
+ * Returns the full duration of the current track
+ *
+ * @return The duration of the current track in miliseconds
+ */
+ public long duration() {
+ if (mPlayer.isInitialized()) {
+ return mPlayer.duration();
+ }
+ return -1;
+ }
+
+ /**
+ * Returns the queue
+ *
+ * @return The queue as a long[]
+ */
+ public long[] getQueue() {
+ synchronized (this) {
+ final int len = mPlayListLen;
+ final long[] list = new long[len];
+ for (int i = 0; i < len; i++) {
+ list[i] = mPlayList[i];
+ }
+ return list;
+ }
+ }
+
+ /**
+ * @return True if music is playing, false otherwise
+ */
+ public boolean isPlaying() {
+ return mIsSupposedToBePlaying;
+ }
+
+ /**
+ * True if the current track is a "favorite", false otherwise
+ */
+ public boolean isFavorite() {
+ if (mFavoritesCache != null) {
+ synchronized (this) {
+ final Long id = mFavoritesCache.getSongId(getAudioId());
+ return id != null ? true : false;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Opens a list for playback
+ *
+ * @param list The list of tracks to open
+ * @param position The position to start playback at
+ */
+ public void open(final long[] list, final int position) {
+ synchronized (this) {
+ if (mShuffleMode == SHUFFLE_AUTO) {
+ mShuffleMode = SHUFFLE_NORMAL;
+ }
+ final long oldId = getAudioId();
+ final int listlength = list.length;
+ boolean newlist = true;
+ if (mPlayListLen == listlength) {
+ newlist = false;
+ for (int i = 0; i < listlength; i++) {
+ if (list[i] != mPlayList[i]) {
+ newlist = true;
+ break;
+ }
+ }
+ }
+ if (newlist) {
+ addToPlayList(list, -1);
+ notifyChange(QUEUE_CHANGED);
+ }
+ if (position >= 0) {
+ mPlayPos = position;
+ } else {
+ mPlayPos = mShuffler.nextInt(mPlayListLen);
+ }
+ mHistory.clear();
+ openCurrentAndNext();
+ if (oldId != getAudioId()) {
+ notifyChange(META_CHANGED);
+ }
+ }
+ }
+
+ /**
+ * Stops playback.
+ */
+ public void stop() {
+ stop(true);
+ }
+
+ /**
+ * Resumes or starts playback.
+ */
+ public void play() {
+ mAudioManager.requestAudioFocus(mAudioFocusListener, AudioManager.STREAM_MUSIC,
+ AudioManager.AUDIOFOCUS_GAIN);
+ mAudioManager.registerMediaButtonEventReceiver(new ComponentName(getPackageName(),
+ MediaButtonIntentReceiver.class.getName()));
+
+ if (mPlayer.isInitialized()) {
+ final long duration = mPlayer.duration();
+ if (mRepeatMode != REPEAT_CURRENT && duration > 2000
+ && mPlayer.position() >= duration - 2000) {
+ gotoNext(true);
+ }
+
+ mPlayer.start();
+ mPlayerHandler.removeMessages(FADEDOWN);
+ mPlayerHandler.sendEmptyMessage(FADEUP);
+
+ // Update the notification
+ buildNotification();
+ if (!mIsSupposedToBePlaying) {
+ mIsSupposedToBePlaying = true;
+ notifyChange(PLAYSTATE_CHANGED);
+ }
+
+ } else if (mPlayListLen <= 0) {
+ setShuffleMode(SHUFFLE_AUTO);
+ }
+ }
+
+ /**
+ * Temporarily pauses playback.
+ */
+ public void pause() {
+ synchronized (this) {
+ mPlayerHandler.removeMessages(FADEUP);
+ if (mIsSupposedToBePlaying) {
+ mPlayer.pause();
+ gotoIdleState();
+ mIsSupposedToBePlaying = false;
+ notifyChange(PLAYSTATE_CHANGED);
+ }
+ }
+ }
+
+ /**
+ * Changes from the current track to the next track
+ */
+ public void gotoNext(final boolean force) {
+ synchronized (this) {
+ if (mPlayListLen <= 0) {
+ return;
+ }
+ final int pos = getNextPosition(force);
+ if (pos < 0) {
+ gotoIdleState();
+ if (mIsSupposedToBePlaying) {
+ mIsSupposedToBePlaying = false;
+ notifyChange(PLAYSTATE_CHANGED);
+ }
+ return;
+ }
+ mPlayPos = pos;
+ stop(false);
+ mPlayPos = pos;
+ openCurrentAndNext();
+ play();
+ notifyChange(META_CHANGED);
+ }
+ }
+
+ /**
+ * Changes from the current track to the previous played track
+ */
+ public void prev() {
+ synchronized (this) {
+ if (mShuffleMode == SHUFFLE_NORMAL) {
+ // Go to previously-played track and remove it from the history
+ final int histsize = mHistory.size();
+ if (histsize == 0) {
+ return;
+ }
+ final Integer pos = mHistory.remove(histsize - 1);
+ mPlayPos = pos.intValue();
+ } else {
+ if (mPlayPos > 0) {
+ mPlayPos--;
+ } else {
+ mPlayPos = mPlayListLen - 1;
+ }
+ }
+ stop(false);
+ openCurrent();
+ play();
+ notifyChange(META_CHANGED);
+ }
+ }
+
+ /**
+ * We don't want to open the current and next track when the user is using
+ * the {@code #prev()} method because they won't be able to travel back to
+ * the previously listened track if they're shuffling.
+ */
+ private void openCurrent() {
+ openCurrentAndMaybeNext(false);
+ }
+
+ /**
+ * Toggles the current song as a favorite.
+ */
+ public void toggleFavorite() {
+ if (mFavoritesCache != null) {
+ synchronized (this) {
+ mFavoritesCache.toggleSong(getAudioId(), getTrackName(), getAlbumName(),
+ getArtistName());
+ }
+ }
+ }
+
+ /**
+ * Moves an item in the queue from one position to another
+ *
+ * @param from The position the item is currently at
+ * @param to The position the item is being moved to
+ */
+ public void moveQueueItem(int index1, int index2) {
+ synchronized (this) {
+ if (index1 >= mPlayListLen) {
+ index1 = mPlayListLen - 1;
+ }
+ if (index2 >= mPlayListLen) {
+ index2 = mPlayListLen - 1;
+ }
+ if (index1 < index2) {
+ final long tmp = mPlayList[index1];
+ for (int i = index1; i < index2; i++) {
+ mPlayList[i] = mPlayList[i + 1];
+ }
+ mPlayList[index2] = tmp;
+ if (mPlayPos == index1) {
+ mPlayPos = index2;
+ } else if (mPlayPos >= index1 && mPlayPos <= index2) {
+ mPlayPos--;
+ }
+ } else if (index2 < index1) {
+ final long tmp = mPlayList[index1];
+ for (int i = index1; i > index2; i--) {
+ mPlayList[i] = mPlayList[i - 1];
+ }
+ mPlayList[index2] = tmp;
+ if (mPlayPos == index1) {
+ mPlayPos = index2;
+ } else if (mPlayPos >= index2 && mPlayPos <= index1) {
+ mPlayPos++;
+ }
+ }
+ notifyChange(QUEUE_CHANGED);
+ }
+ }
+
+ /**
+ * Sets the repeat mode
+ *
+ * @param repeatmode The repeat mode to use
+ */
+ public void setRepeatMode(final int repeatmode) {
+ synchronized (this) {
+ mRepeatMode = repeatmode;
+ setNextTrack();
+ saveQueue(false);
+ notifyChange(REPEATMODE_CHANGED);
+ }
+ }
+
+ /**
+ * Sets the shuffle mode
+ *
+ * @param shufflemode The shuffle mode to use
+ */
+ public void setShuffleMode(final int shufflemode) {
+ synchronized (this) {
+ if (mShuffleMode == shufflemode && mPlayListLen > 0) {
+ return;
+ }
+ mShuffleMode = shufflemode;
+ if (mShuffleMode == SHUFFLE_AUTO) {
+ if (makeAutoShuffleList()) {
+ mPlayListLen = 0;
+ doAutoShuffleUpdate();
+ mPlayPos = 0;
+ openCurrentAndNext();
+ play();
+ notifyChange(META_CHANGED);
+ return;
+ } else {
+ mShuffleMode = SHUFFLE_NONE;
+ }
+ }
+ saveQueue(false);
+ notifyChange(SHUFFLEMODE_CHANGED);
+ }
+ }
+
+ /**
+ * Sets the position of a track in the queue
+ *
+ * @param index The position to place the track
+ */
+ public void setQueuePosition(final int index) {
+ synchronized (this) {
+ stop(false);
+ mPlayPos = index;
+ openCurrentAndNext();
+ play();
+ notifyChange(META_CHANGED);
+ if (mShuffleMode == SHUFFLE_AUTO) {
+ doAutoShuffleUpdate();
+ }
+ }
+ }
+
+ /**
+ * Queues a new list for playback
+ *
+ * @param list The list to queue
+ * @param action The action to take
+ */
+ public void enqueue(final long[] list, final int action) {
+ synchronized (this) {
+ if (action == NEXT && mPlayPos + 1 < mPlayListLen) {
+ addToPlayList(list, mPlayPos + 1);
+ notifyChange(QUEUE_CHANGED);
+ } else {
+ addToPlayList(list, Integer.MAX_VALUE);
+ notifyChange(QUEUE_CHANGED);
+ if (action == NOW) {
+ mPlayPos = mPlayListLen - list.length;
+ openCurrentAndNext();
+ play();
+ notifyChange(META_CHANGED);
+ return;
+ }
+ }
+ if (mPlayPos < 0) {
+ mPlayPos = 0;
+ openCurrentAndNext();
+ play();
+ notifyChange(META_CHANGED);
+ }
+ }
+ }
+
+ /**
+ * Cycles through the different repeat modes
+ */
+ private void cycleRepeat() {
+ if (mRepeatMode == REPEAT_NONE) {
+ setRepeatMode(REPEAT_ALL);
+ } else if (mRepeatMode == REPEAT_ALL) {
+ setRepeatMode(REPEAT_CURRENT);
+ if (mShuffleMode != SHUFFLE_NONE) {
+ setShuffleMode(SHUFFLE_NONE);
+ }
+ } else {
+ setRepeatMode(REPEAT_NONE);
+ }
+ }
+
+ /**
+ * Cycles through the different shuffle modes
+ */
+ private void cycleShuffle() {
+ if (mShuffleMode == SHUFFLE_NONE) {
+ setShuffleMode(SHUFFLE_NORMAL);
+ if (mRepeatMode == REPEAT_CURRENT) {
+ setRepeatMode(REPEAT_ALL);
+ }
+ } else if (mShuffleMode == SHUFFLE_NORMAL || mShuffleMode == SHUFFLE_AUTO) {
+ setShuffleMode(SHUFFLE_NONE);
+ }
+ }
+
+ /**
+ * @return The album art for the current album.
+ */
+ public Bitmap getAlbumArt() {
+ // Return the cached artwork
+ final Bitmap bitmap = mImageFetcher.getArtwork(getAlbumName(),
+ String.valueOf(getAlbumId()), getArtistName());
+ return bitmap;
+ }
+
+ /**
+ * Called when one of the lists should refresh or requery.
+ */
+ public void refresh() {
+ notifyChange(REFRESH);
+ }
+
+ private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onReceive(final Context context, final Intent intent) {
+ final String action = intent.getAction();
+ final String command = intent.getStringExtra("command");
+ if (CMDNEXT.equals(command) || NEXT_ACTION.equals(action)) {
+ gotoNext(true);
+ } else if (CMDPREVIOUS.equals(command) || PREVIOUS_ACTION.equals(action)) {
+ if (position() < 2000) {
+ prev();
+ } else {
+ seek(0);
+ play();
+ }
+ } else if (CMDTOGGLEPAUSE.equals(command) || TOGGLEPAUSE_ACTION.equals(action)) {
+ if (mIsSupposedToBePlaying) {
+ pause();
+ mPausedByTransientLossOfFocus = false;
+ } else {
+ play();
+ }
+ } else if (CMDPAUSE.equals(command) || PAUSE_ACTION.equals(action)) {
+ pause();
+ mPausedByTransientLossOfFocus = false;
+ } else if (CMDPLAY.equals(command)) {
+ play();
+ } else if (CMDSTOP.equals(command) || STOP_ACTION.equals(action)) {
+ pause();
+ mPausedByTransientLossOfFocus = false;
+ seek(0);
+ killNotification();
+ mBuildNotification = false;
+ } else if (REPEAT_ACTION.equals(action)) {
+ cycleRepeat();
+ } else if (SHUFFLE_ACTION.equals(action)) {
+ cycleShuffle();
+ } else if (KILL_FOREGROUND.equals(action)) {
+ mBuildNotification = false;
+ killNotification();
+ } else if (START_BACKGROUND.equals(action)) {
+ mBuildNotification = true;
+ buildNotification();
+ } else if (UPDATE_LOCKSCREEN.equals(action)) {
+ mEnableLockscreenControls = intent.getBooleanExtra(UPDATE_LOCKSCREEN, true);
+ if (mEnableLockscreenControls) {
+ setUpRemoteControlClient();
+ // Update the controls according to the current playback
+ notifyChange(PLAYSTATE_CHANGED);
+ notifyChange(META_CHANGED);
+ } else {
+ // Remove then unregister the conrols
+ mRemoteControlClientCompat
+ .setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED);
+ RemoteControlHelper.unregisterRemoteControlClient(mAudioManager,
+ mRemoteControlClientCompat);
+ }
+ } else if (AppWidgetSmall.CMDAPPWIDGETUPDATE.equals(command)) {
+ final int[] small = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
+ mAppWidgetSmall.performUpdate(MusicPlaybackService.this, small);
+ } else if (AppWidgetLarge.CMDAPPWIDGETUPDATE.equals(command)) {
+ final int[] large = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
+ mAppWidgetLarge.performUpdate(MusicPlaybackService.this, large);
+ } else if (AppWidgetLargeAlternate.CMDAPPWIDGETUPDATE.equals(command)) {
+ final int[] largeAlt = intent
+ .getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
+ mAppWidgetLargeAlternate.performUpdate(MusicPlaybackService.this, largeAlt);
+ } else if (RecentWidgetProvider.CMDAPPWIDGETUPDATE.equals(command)) {
+ final int[] recent = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
+ mRecentWidgetProvider.performUpdate(MusicPlaybackService.this, recent);
+ }
+ }
+ };
+
+ private final OnAudioFocusChangeListener mAudioFocusListener = new OnAudioFocusChangeListener() {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onAudioFocusChange(final int focusChange) {
+ mPlayerHandler.obtainMessage(FOCUSCHANGE, focusChange, 0).sendToTarget();
+ }
+ };
+
+ private static final class DelayedHandler extends Handler {
+
+ private final WeakReference<MusicPlaybackService> mService;
+
+ /**
+ * Constructor of <code>DelayedHandler</code>
+ *
+ * @param service The service to use.
+ */
+ public DelayedHandler(final MusicPlaybackService service) {
+ mService = new WeakReference<MusicPlaybackService>(service);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void handleMessage(final Message msg) {
+ if (mService.get().isPlaying() || mService.get().mPausedByTransientLossOfFocus
+ || mService.get().mServiceInUse
+ || mService.get().mPlayerHandler.hasMessages(TRACK_ENDED)) {
+ return;
+ }
+ mService.get().saveQueue(true);
+ mService.get().stopSelf(mService.get().mServiceStartId);
+ }
+ }
+
+ private static final class MusicPlayerHandler extends Handler {
+
+ private final WeakReference<MusicPlaybackService> mService;
+
+ private float mCurrentVolume = 1.0f;
+
+ /**
+ * Constructor of <code>MusicPlayerHandler</code>
+ *
+ * @param service The service to use.
+ * @param looper The thread to run on.
+ */
+ public MusicPlayerHandler(final MusicPlaybackService service, final Looper looper) {
+ super(looper);
+ mService = new WeakReference<MusicPlaybackService>(service);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void handleMessage(final Message msg) {
+ switch (msg.what) {
+ case FADEDOWN:
+ mCurrentVolume -= .05f;
+ if (mCurrentVolume > .2f) {
+ sendEmptyMessageDelayed(FADEDOWN, 10);
+ } else {
+ mCurrentVolume = .2f;
+ }
+ mService.get().mPlayer.setVolume(mCurrentVolume);
+ break;
+ case FADEUP:
+ mCurrentVolume += .01f;
+ if (mCurrentVolume < 1.0f) {
+ sendEmptyMessageDelayed(FADEUP, 10);
+ } else {
+ mCurrentVolume = 1.0f;
+ }
+ mService.get().mPlayer.setVolume(mCurrentVolume);
+ break;
+ case SERVER_DIED:
+ if (mService.get().mIsSupposedToBePlaying) {
+ mService.get().gotoNext(true);
+ } else {
+ mService.get().openCurrentAndNext();
+ }
+ break;
+ case TRACK_WENT_TO_NEXT:
+ mService.get().mPlayPos = mService.get().mNextPlayPos;
+ if (mService.get().mCursor != null) {
+ mService.get().mCursor.close();
+ mService.get().mCursor = null;
+ }
+ mService.get().mCursor = mService.get().getCursorForId(
+ mService.get().mPlayList[mService.get().mPlayPos]);
+ mService.get().notifyChange(META_CHANGED);
+ mService.get().buildNotification();
+ mService.get().setNextTrack();
+ break;
+ case TRACK_ENDED:
+ if (mService.get().mRepeatMode == REPEAT_CURRENT) {
+ mService.get().seek(0);
+ mService.get().play();
+ } else {
+ mService.get().gotoNext(false);
+ }
+ break;
+ case RELEASE_WAKELOCK:
+ mService.get().mWakeLock.release();
+ break;
+ case FOCUSCHANGE:
+ switch (msg.arg1) {
+ case AudioManager.AUDIOFOCUS_LOSS:
+ if (mService.get().isPlaying()) {
+ mService.get().mPausedByTransientLossOfFocus = false;
+ }
+ mService.get().pause();
+ break;
+ case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
+ removeMessages(FADEUP);
+ sendEmptyMessage(FADEDOWN);
+ break;
+ case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
+ if (mService.get().isPlaying()) {
+ mService.get().mPausedByTransientLossOfFocus = true;
+ }
+ mService.get().pause();
+ break;
+ case AudioManager.AUDIOFOCUS_GAIN:
+ if (!mService.get().isPlaying()
+ && mService.get().mPausedByTransientLossOfFocus) {
+ mService.get().mPausedByTransientLossOfFocus = false;
+ mCurrentVolume = 0f;
+ mService.get().mPlayer.setVolume(mCurrentVolume);
+ mService.get().play();
+ } else {
+ removeMessages(FADEDOWN);
+ sendEmptyMessage(FADEUP);
+ }
+ break;
+ default:
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ private static final class Shuffler {
+
+ private final LinkedList<Integer> mHistoryOfNumbers = new LinkedList<Integer>();
+
+ private final TreeSet<Integer> mPreviousNumbers = new TreeSet<Integer>();
+
+ private final Random mRandom = new Random();
+
+ private int mPrevious;
+
+ /**
+ * Constructor of <code>Shuffler</code>
+ */
+ public Shuffler() {
+ super();
+ }
+
+ /**
+ * @param interval The length the queue
+ * @return The position of the next track to play
+ */
+ public int nextInt(final int interval) {
+ int next;
+ do {
+ next = mRandom.nextInt(interval);
+ } while (next == mPrevious && interval > 1
+ && !mPreviousNumbers.contains(Integer.valueOf(next)));
+ mPrevious = next;
+ mHistoryOfNumbers.add(mPrevious);
+ mPreviousNumbers.add(mPrevious);
+ cleanUpHistory();
+ return next;
+ }
+
+ /**
+ * Removes old tracks and cleans up the history preparing for new tracks
+ * to be added to the mapping
+ */
+ private void cleanUpHistory() {
+ if (!mHistoryOfNumbers.isEmpty() && mHistoryOfNumbers.size() >= MAX_HISTORY_SIZE) {
+ for (int i = 0; i < Math.max(1, MAX_HISTORY_SIZE / 2); i++) {
+ mPreviousNumbers.remove(mHistoryOfNumbers.removeFirst());
+ }
+ }
+ }
+ };
+
+ private static final class MultiPlayer implements MediaPlayer.OnErrorListener,
+ MediaPlayer.OnCompletionListener {
+
+ private final WeakReference<MusicPlaybackService> mService;
+
+ private CompatMediaPlayer mCurrentMediaPlayer = new CompatMediaPlayer();
+
+ private CompatMediaPlayer mNextMediaPlayer;
+
+ private Handler mHandler;
+
+ private boolean mIsInitialized = false;
+
+ /**
+ * Constructor of <code>MultiPlayer</code>
+ */
+ public MultiPlayer(final MusicPlaybackService service) {
+ mService = new WeakReference<MusicPlaybackService>(service);
+ mCurrentMediaPlayer.setWakeMode(mService.get(), PowerManager.PARTIAL_WAKE_LOCK);
+ }
+
+ /**
+ * @param path The path of the file, or the http/rtsp URL of the stream
+ * you want to play
+ */
+ public void setDataSource(final String path) {
+ mIsInitialized = setDataSourceImpl(mCurrentMediaPlayer, path);
+ if (mIsInitialized) {
+ setNextDataSource(null);
+ }
+ }
+
+ /**
+ * @param player The {@link MediaPlayer} to use
+ * @param path The path of the file, or the http/rtsp URL of the stream
+ * you want to play
+ * @return True if the <code>player</code> has been prepared and is
+ * ready to play, false otherwise
+ */
+ private boolean setDataSourceImpl(final MediaPlayer player, final String path) {
+ try {
+ player.reset();
+ player.setOnPreparedListener(null);
+ if (path.startsWith("content://")) {
+ player.setDataSource(mService.get(), Uri.parse(path));
+ } else {
+ player.setDataSource(path);
+ }
+ player.setAudioStreamType(AudioManager.STREAM_MUSIC);
+ player.prepare();
+ } catch (final IOException todo) {
+ // TODO: notify the user why the file couldn't be opened
+ return false;
+ } catch (final IllegalArgumentException todo) {
+ // TODO: notify the user why the file couldn't be opened
+ return false;
+ }
+ player.setOnCompletionListener(this);
+ player.setOnErrorListener(this);
+ final Intent mIntent = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION);
+ mIntent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, getAudioSessionId());
+ mIntent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, mService.get().getPackageName());
+ mService.get().sendBroadcast(mIntent);
+ return true;
+ }
+
+ /**
+ * Set the MediaPlayer to start when this MediaPlayer finishes playback.
+ *
+ * @param path The path of the file, or the http/rtsp URL of the stream
+ * you want to play
+ */
+ public void setNextDataSource(final String path) {
+ mCurrentMediaPlayer.setNextMediaPlayer(null);
+ if (mNextMediaPlayer != null) {
+ mNextMediaPlayer.release();
+ mNextMediaPlayer = null;
+ }
+ if (path == null) {
+ return;
+ }
+ mNextMediaPlayer = new CompatMediaPlayer();
+ mNextMediaPlayer.setWakeMode(mService.get(), PowerManager.PARTIAL_WAKE_LOCK);
+ mNextMediaPlayer.setAudioSessionId(getAudioSessionId());
+ if (setDataSourceImpl(mNextMediaPlayer, path)) {
+ mCurrentMediaPlayer.setNextMediaPlayer(mNextMediaPlayer);
+ } else {
+ if (mNextMediaPlayer != null) {
+ mNextMediaPlayer.release();
+ mNextMediaPlayer = null;
+ }
+ }
+ }
+
+ /**
+ * Sets the handler
+ *
+ * @param handler The handler to use
+ */
+ public void setHandler(final Handler handler) {
+ mHandler = handler;
+ }
+
+ /**
+ * @return True if the player is ready to go, false otherwise
+ */
+ public boolean isInitialized() {
+ return mIsInitialized;
+ }
+
+ /**
+ * Starts or resumes playback.
+ */
+ public void start() {
+ mCurrentMediaPlayer.start();
+ }
+
+ /**
+ * Resets the MediaPlayer to its uninitialized state.
+ */
+ public void stop() {
+ mCurrentMediaPlayer.reset();
+ mIsInitialized = false;
+ }
+
+ /**
+ * Releases resources associated with this MediaPlayer object.
+ */
+ public void release() {
+ stop();
+ mCurrentMediaPlayer.release();
+ }
+
+ /**
+ * Pauses playback. Call start() to resume.
+ */
+ public void pause() {
+ mCurrentMediaPlayer.pause();
+ }
+
+ /**
+ * Gets the duration of the file.
+ *
+ * @return The duration in milliseconds
+ */
+ public long duration() {
+ return mCurrentMediaPlayer.getDuration();
+ }
+
+ /**
+ * Gets the current playback position.
+ *
+ * @return The current position in milliseconds
+ */
+ public long position() {
+ return mCurrentMediaPlayer.getCurrentPosition();
+ }
+
+ /**
+ * Gets the current playback position.
+ *
+ * @param whereto The offset in milliseconds from the start to seek to
+ * @return The offset in milliseconds from the start to seek to
+ */
+ public long seek(final long whereto) {
+ mCurrentMediaPlayer.seekTo((int)whereto);
+ return whereto;
+ }
+
+ /**
+ * Sets the volume on this player.
+ *
+ * @param vol Left and right volume scalar
+ */
+ public void setVolume(final float vol) {
+ mCurrentMediaPlayer.setVolume(vol, vol);
+ }
+
+ /**
+ * Sets the audio session ID.
+ *
+ * @param sessionId The audio session ID
+ */
+ public void setAudioSessionId(final int sessionId) {
+ mCurrentMediaPlayer.setAudioSessionId(sessionId);
+ }
+
+ /**
+ * Returns the audio session ID.
+ *
+ * @return The current audio session ID.
+ */
+ public int getAudioSessionId() {
+ return mCurrentMediaPlayer.getAudioSessionId();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onError(final MediaPlayer mp, final int what, final int extra) {
+ switch (what) {
+ case MediaPlayer.MEDIA_ERROR_SERVER_DIED:
+ mIsInitialized = false;
+ mCurrentMediaPlayer.release();
+ mCurrentMediaPlayer = new CompatMediaPlayer();
+ mCurrentMediaPlayer.setWakeMode(mService.get(), PowerManager.PARTIAL_WAKE_LOCK);
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(SERVER_DIED), 2000);
+ return true;
+ default:
+ break;
+ }
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCompletion(final MediaPlayer mp) {
+ if (mp == mCurrentMediaPlayer && mNextMediaPlayer != null) {
+ mCurrentMediaPlayer.release();
+ mCurrentMediaPlayer = mNextMediaPlayer;
+ mNextMediaPlayer = null;
+ mHandler.sendEmptyMessage(TRACK_WENT_TO_NEXT);
+ } else {
+ mService.get().mWakeLock.acquire(30000);
+ mHandler.sendEmptyMessage(TRACK_ENDED);
+ mHandler.sendEmptyMessage(RELEASE_WAKELOCK);
+ }
+ }
+ }
+
+ private static final class CompatMediaPlayer extends MediaPlayer implements
+ OnCompletionListener {
+
+ private boolean mCompatMode = true;
+
+ private MediaPlayer mNextPlayer;
+
+ private OnCompletionListener mCompletion;
+
+ /**
+ * Constructor of <code>CompatMediaPlayer</code>
+ */
+ public CompatMediaPlayer() {
+ try {
+ MediaPlayer.class.getMethod("setNextMediaPlayer", MediaPlayer.class);
+ mCompatMode = false;
+ } catch (final NoSuchMethodException e) {
+ mCompatMode = true;
+ super.setOnCompletionListener(this);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setNextMediaPlayer(final MediaPlayer next) {
+ if (mCompatMode) {
+ mNextPlayer = next;
+ } else {
+ super.setNextMediaPlayer(next);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setOnCompletionListener(final OnCompletionListener listener) {
+ if (mCompatMode) {
+ mCompletion = listener;
+ } else {
+ super.setOnCompletionListener(listener);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCompletion(final MediaPlayer mp) {
+ if (mNextPlayer != null) {
+ // SystemClock.sleep(25);
+ mNextPlayer.start();
+ }
+ mCompletion.onCompletion(this);
+ }
+ }
+
+ private static final class ServiceStub extends IApolloService.Stub {
+
+ private final WeakReference<MusicPlaybackService> mService;
+
+ private ServiceStub(final MusicPlaybackService service) {
+ mService = new WeakReference<MusicPlaybackService>(service);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void openFile(final String path) throws RemoteException {
+ mService.get().openFile(path);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void open(final long[] list, final int position) throws RemoteException {
+ mService.get().open(list, position);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void stop() throws RemoteException {
+ mService.get().stop();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void pause() throws RemoteException {
+ mService.get().pause();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void play() throws RemoteException {
+ mService.get().play();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void prev() throws RemoteException {
+ mService.get().prev();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void next() throws RemoteException {
+ mService.get().gotoNext(true);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void enqueue(final long[] list, final int action) throws RemoteException {
+ mService.get().enqueue(list, action);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setQueuePosition(final int index) throws RemoteException {
+ mService.get().setQueuePosition(index);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setShuffleMode(final int shufflemode) throws RemoteException {
+ mService.get().setShuffleMode(shufflemode);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setRepeatMode(final int repeatmode) throws RemoteException {
+ mService.get().setRepeatMode(repeatmode);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void moveQueueItem(final int from, final int to) throws RemoteException {
+ mService.get().moveQueueItem(from, to);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void toggleFavorite() throws RemoteException {
+ mService.get().toggleFavorite();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void refresh() throws RemoteException {
+ mService.get().refresh();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean isFavorite() throws RemoteException {
+ return mService.get().isFavorite();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean isPlaying() throws RemoteException {
+ return mService.get().isPlaying();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public long[] getQueue() throws RemoteException {
+ return mService.get().getQueue();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public long duration() throws RemoteException {
+ return mService.get().duration();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public long position() throws RemoteException {
+ return mService.get().position();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public long seek(final long position) throws RemoteException {
+ return mService.get().seek(position);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public long getAudioId() throws RemoteException {
+ return mService.get().getAudioId();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public long getArtistId() throws RemoteException {
+ return mService.get().getArtistId();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public long getAlbumId() throws RemoteException {
+ return mService.get().getAlbumId();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getArtistName() throws RemoteException {
+ return mService.get().getArtistName();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getTrackName() throws RemoteException {
+ return mService.get().getTrackName();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getAlbumName() throws RemoteException {
+ return mService.get().getAlbumName();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getPath() throws RemoteException {
+ return mService.get().getPath();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getQueuePosition() throws RemoteException {
+ return mService.get().getQueuePosition();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getShuffleMode() throws RemoteException {
+ return mService.get().getShuffleMode();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getRepeatMode() throws RemoteException {
+ return mService.get().getRepeatMode();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int removeTracks(final int first, final int last) throws RemoteException {
+ return mService.get().removeTracks(first, last);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int removeTrack(final long id) throws RemoteException {
+ return mService.get().removeTrack(id);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getMediaMountedCount() throws RemoteException {
+ return mService.get().getMediaMountedCount();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getAudioSessionId() throws RemoteException {
+ return mService.get().getAudioSessionId();
+ }
+
+ }
+
+}
diff --git a/src/com/andrew/apollo/MusicStateListener.java b/src/com/andrew/apollo/MusicStateListener.java
new file mode 100644
index 0000000..53d6baa
--- /dev/null
+++ b/src/com/andrew/apollo/MusicStateListener.java
@@ -0,0 +1,19 @@
+
+package com.andrew.apollo;
+
+/**
+ * Listens for playback changes to send the the fragments bound to this activity
+ */
+public interface MusicStateListener {
+
+ /**
+ * Called when {@link MusicPlaybackService#REFRESH} is invoked
+ */
+ public void restartLoader();
+
+ /**
+ * Called when {@link MusicPlaybackService#META_CHANGED} is invoked
+ */
+ public void onMetaChanged();
+
+}
diff --git a/src/com/andrew/apollo/NotificationHelper.java b/src/com/andrew/apollo/NotificationHelper.java
new file mode 100644
index 0000000..cdcc584
--- /dev/null
+++ b/src/com/andrew/apollo/NotificationHelper.java
@@ -0,0 +1,282 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo;
+
+import android.annotation.SuppressLint;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.support.v4.app.NotificationCompat;
+import android.util.Log;
+import android.widget.RemoteViews;
+
+import com.andrew.apollo.utils.ApolloUtils;
+
+/**
+ * Builds the notification for Apollo's service. Jelly Bean and higher uses the
+ * expanded notification by default.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+@SuppressLint("NewApi")
+public class NotificationHelper {
+
+ /**
+ * Notification ID
+ */
+ private static final int APOLLO_MUSIC_SERVICE = 1;
+
+ /**
+ * NotificationManager
+ */
+ private final NotificationManager mNotificationManager;
+
+ /**
+ * Context
+ */
+ private final MusicPlaybackService mService;
+
+ /**
+ * Custom notification layout
+ */
+ private RemoteViews mNotificationTemplate;
+
+ /**
+ * The Notification
+ */
+ private Notification mNotification = null;
+
+ /**
+ * API 16+ bigContentView
+ */
+ private RemoteViews mExpandedView;
+
+ /**
+ * Constructor of <code>NotificationHelper</code>
+ *
+ * @param service The {@link Context} to use
+ */
+ public NotificationHelper(final MusicPlaybackService service) {
+ mService = service;
+ mNotificationManager = (NotificationManager)service
+ .getSystemService(Context.NOTIFICATION_SERVICE);
+ }
+
+ /**
+ * Call this to build the {@link Notification}.
+ */
+ public void buildNotification(final String albumName, final String artistName,
+ final String trackName, final Long albumId, final Bitmap albumArt) {
+
+ // Default notfication layout
+ mNotificationTemplate = new RemoteViews(mService.getPackageName(),
+ R.layout.notification_template_base);
+
+ // Set up the content view
+ initCollapsedLayout(trackName, artistName, albumArt);
+
+ if (ApolloUtils.hasHoneycomb()) {
+ // Notification Builder
+ mNotification = new NotificationCompat.Builder(mService)
+ .setSmallIcon(R.drawable.stat_notify_music)
+ .setContentIntent(getPendingIntent())
+ .setPriority(Notification.PRIORITY_DEFAULT).setContent(mNotificationTemplate)
+ .build();
+ // Control playback from the notification
+ initPlaybackActions();
+ if (ApolloUtils.hasJellyBean()) {
+ // Expanded notifiction style
+ mExpandedView = new RemoteViews(mService.getPackageName(),
+ R.layout.notification_template_expanded_base);
+ mNotification.bigContentView = mExpandedView;
+ // Control playback from the notification
+ initExpandedPlaybackActions();
+ // Set up the expanded content view
+ initExpandedLayout(trackName, albumName, artistName, albumArt);
+ }
+ mService.startForeground(APOLLO_MUSIC_SERVICE, mNotification);
+ } else {
+ // FIXME: I do not understand why this happens, but the
+ // NotificationCompat
+ // API does not work on Gingerbread. Specifically, {@code
+ // #mBuilder.setContent()} won't apply the custom RV in Gingerbread.
+ // So,
+ // until this is fixed I'll just use the old way.
+ mNotification = new Notification();
+ mNotification.contentView = mNotificationTemplate;
+ mNotification.flags |= Notification.FLAG_ONGOING_EVENT;
+ mNotification.icon = R.drawable.stat_notify_music;
+ mNotification.contentIntent = getPendingIntent();
+ mService.startForeground(APOLLO_MUSIC_SERVICE, mNotification);
+ }
+ }
+
+ /**
+ * Changes the playback controls in and out of a paused state
+ *
+ * @param isPlaying True if music is playing, false otherwise
+ */
+ public void goToIdleState(final boolean isPlaying) {
+ if (mNotification == null || mNotificationManager == null) {
+ return;
+ }
+ if (ApolloUtils.hasHoneycomb() && mNotificationTemplate != null) {
+ mNotificationTemplate.setImageViewResource(R.id.notification_base_play,
+ isPlaying ? R.drawable.btn_playback_play : R.drawable.btn_playback_pause);
+ }
+
+ if (ApolloUtils.hasJellyBean() && mExpandedView != null && ApolloUtils.hasJellyBean()) {
+ mExpandedView.setImageViewResource(R.id.notification_expanded_base_play,
+ isPlaying ? R.drawable.btn_playback_play : R.drawable.btn_playback_pause);
+ }
+ try {
+ mNotificationManager.notify(APOLLO_MUSIC_SERVICE, mNotification);
+ } catch (final IllegalStateException e) {
+ Log.e("NotificationHelper", "goToIdleState - " + e);
+ // FIXME Every so often an ISE is throw reading
+ // "can't parcel recycled Bitmap". Figure out and understand why
+ // this is happening, then prevent it.
+ }
+ }
+
+ /**
+ * Open to the now playing screen
+ */
+ private PendingIntent getPendingIntent() {
+ return PendingIntent.getActivity(mService, 0, new Intent("com.andrew.apollo.AUDIO_PLAYER")
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), 0);
+ }
+
+ /**
+ * Lets the buttons in the remote view control playback in the expanded
+ * layout
+ */
+ private void initExpandedPlaybackActions() {
+ // Play and pause
+ mExpandedView.setOnClickPendingIntent(R.id.notification_expanded_base_play,
+ retreivePlaybackActions(1));
+
+ // Skip tracks
+ mExpandedView.setOnClickPendingIntent(R.id.notification_expanded_base_next,
+ retreivePlaybackActions(2));
+
+ // Previous tracks
+ mExpandedView.setOnClickPendingIntent(R.id.notification_expanded_base_previous,
+ retreivePlaybackActions(3));
+
+ // Stop and collapse the notification
+ mExpandedView.setOnClickPendingIntent(R.id.notification_expanded_base_collapse,
+ retreivePlaybackActions(4));
+
+ // Update the play button image
+ mExpandedView.setImageViewResource(R.id.notification_expanded_base_play,
+ R.drawable.btn_playback_pause);
+ }
+
+ /**
+ * Lets the buttons in the remote view control playback in the normal layout
+ */
+ private void initPlaybackActions() {
+ // Play and pause
+ mNotificationTemplate.setOnClickPendingIntent(R.id.notification_base_play,
+ retreivePlaybackActions(1));
+
+ // Skip tracks
+ mNotificationTemplate.setOnClickPendingIntent(R.id.notification_base_next,
+ retreivePlaybackActions(2));
+
+ // Previous tracks
+ mNotificationTemplate.setOnClickPendingIntent(R.id.notification_base_previous,
+ retreivePlaybackActions(3));
+
+ // Stop and collapse the notification
+ mNotificationTemplate.setOnClickPendingIntent(R.id.notification_base_collapse,
+ retreivePlaybackActions(4));
+
+ // Update the play button image
+ mNotificationTemplate.setImageViewResource(R.id.notification_base_play,
+ R.drawable.btn_playback_pause);
+ }
+
+ /**
+ * @param which Which {@link PendingIntent} to return
+ * @return A {@link PendingIntent} ready to control playback
+ */
+ private final PendingIntent retreivePlaybackActions(final int which) {
+ Intent action;
+ PendingIntent pendingIntent;
+ final ComponentName serviceName = new ComponentName(mService, MusicPlaybackService.class);
+ switch (which) {
+ case 1:
+ // Play and pause
+ action = new Intent(MusicPlaybackService.TOGGLEPAUSE_ACTION);
+ action.setComponent(serviceName);
+ pendingIntent = PendingIntent.getService(mService, 1, action, 0);
+ return pendingIntent;
+ case 2:
+ // Skip tracks
+ action = new Intent(MusicPlaybackService.NEXT_ACTION);
+ action.setComponent(serviceName);
+ pendingIntent = PendingIntent.getService(mService, 2, action, 0);
+ return pendingIntent;
+ case 3:
+ // Previous tracks
+ action = new Intent(MusicPlaybackService.PREVIOUS_ACTION);
+ action.setComponent(serviceName);
+ pendingIntent = PendingIntent.getService(mService, 3, action, 0);
+ return pendingIntent;
+ case 4:
+ // Stop and collapse the notification
+ action = new Intent(MusicPlaybackService.STOP_ACTION);
+ action.setComponent(serviceName);
+ pendingIntent = PendingIntent.getService(mService, 4, action, 0);
+ return pendingIntent;
+ default:
+ break;
+ }
+ return null;
+ }
+
+ /**
+ * Sets the track name, artist name, and album art in the normal layout
+ */
+ private void initCollapsedLayout(final String trackName, final String artistName,
+ final Bitmap albumArt) {
+ // Track name (line one)
+ mNotificationTemplate.setTextViewText(R.id.notification_base_line_one, trackName);
+ // Artist name (line two)
+ mNotificationTemplate.setTextViewText(R.id.notification_base_line_two, artistName);
+ // Album art
+ mNotificationTemplate.setImageViewBitmap(R.id.notification_base_image, albumArt);
+ }
+
+ /**
+ * Sets the track name, album name, artist name, and album art in the
+ * expanded layout
+ */
+ private void initExpandedLayout(final String trackName, final String artistName,
+ final String albumName, final Bitmap albumArt) {
+ // Track name (line one)
+ mExpandedView.setTextViewText(R.id.notification_expanded_base_line_one, trackName);
+ // Album name (line two)
+ mExpandedView.setTextViewText(R.id.notification_expanded_base_line_two, albumName);
+ // Artist name (line three)
+ mExpandedView.setTextViewText(R.id.notification_expanded_base_line_three, artistName);
+ // Album art
+ mExpandedView.setImageViewBitmap(R.id.notification_expanded_base_image, albumArt);
+ }
+
+}
diff --git a/src/com/andrew/apollo/NowPlayingCursor.java b/src/com/andrew/apollo/NowPlayingCursor.java
deleted file mode 100644
index d62190e..0000000
--- a/src/com/andrew/apollo/NowPlayingCursor.java
+++ /dev/null
@@ -1,194 +0,0 @@
-
-package com.andrew.apollo;
-
-import java.util.Arrays;
-
-import android.content.Context;
-import android.database.AbstractCursor;
-import android.database.Cursor;
-import android.os.RemoteException;
-import android.provider.BaseColumns;
-import android.provider.MediaStore.Audio;
-
-import com.andrew.apollo.utils.MusicUtils;
-
-public class NowPlayingCursor extends AbstractCursor {
-
- public NowPlayingCursor(IApolloService service, String[] projection, Context c) {
- mProjection = projection;
- mService = service;
- makeNowPlayingCursor();
- context = c;
- }
-
- private void makeNowPlayingCursor() {
- mCurrentPlaylistCursor = null;
- try {
- mNowPlaying = mService.getQueue();
- } catch (RemoteException ex) {
- mNowPlaying = new long[0];
- }
- mSize = mNowPlaying.length;
- if (mSize == 0) {
- return;
- }
-
- StringBuilder where = new StringBuilder();
- where.append(BaseColumns._ID + " IN (");
- for (int i = 0; i < mSize; i++) {
- where.append(mNowPlaying[i]);
- if (i < mSize - 1) {
- where.append(",");
- }
- }
- where.append(")");
-
- mCurrentPlaylistCursor = MusicUtils.query(context, Audio.Media.EXTERNAL_CONTENT_URI,
- mProjection, where.toString(), null, BaseColumns._ID);
-
- if (mCurrentPlaylistCursor == null) {
- mSize = 0;
- return;
- }
-
- int size = mCurrentPlaylistCursor.getCount();
- mCursorIdxs = new long[size];
- mCurrentPlaylistCursor.moveToFirst();
- int colidx = mCurrentPlaylistCursor.getColumnIndexOrThrow(BaseColumns._ID);
- for (int i = 0; i < size; i++) {
- mCursorIdxs[i] = mCurrentPlaylistCursor.getLong(colidx);
- mCurrentPlaylistCursor.moveToNext();
- }
- mCurrentPlaylistCursor.moveToFirst();
- try {
- int removed = 0;
- for (int i = mNowPlaying.length - 1; i >= 0; i--) {
- long trackid = mNowPlaying[i];
- int crsridx = Arrays.binarySearch(mCursorIdxs, trackid);
- if (crsridx < 0) {
- removed += mService.removeTrack(trackid);
- }
- }
- if (removed > 0) {
- mNowPlaying = mService.getQueue();
- mSize = mNowPlaying.length;
- if (mSize == 0) {
- mCursorIdxs = null;
- return;
- }
- }
- } catch (RemoteException ex) {
- mNowPlaying = new long[0];
- }
- }
-
- @Override
- public int getCount() {
- return mSize;
- }
-
- @Override
- public boolean onMove(int oldPosition, int newPosition) {
- if (oldPosition == newPosition)
- return true;
-
- if (mNowPlaying == null || mCursorIdxs == null || newPosition >= mNowPlaying.length) {
- return false;
- }
-
- // The cursor doesn't have any duplicates in it, and is not ordered
- // in queue-order, so we need to figure out where in the cursor we
- // should be.
-
- long newid = mNowPlaying[newPosition];
- int crsridx = Arrays.binarySearch(mCursorIdxs, newid);
- mCurrentPlaylistCursor.moveToPosition(crsridx);
- return true;
- }
-
- @Override
- public String getString(int column) {
- try {
- return mCurrentPlaylistCursor.getString(column);
- } catch (Exception ex) {
- onChange(true);
- return "";
- }
- }
-
- @Override
- public short getShort(int column) {
- return mCurrentPlaylistCursor.getShort(column);
- }
-
- @Override
- public int getInt(int column) {
- try {
- return mCurrentPlaylistCursor.getInt(column);
- } catch (Exception ex) {
- onChange(true);
- return 0;
- }
- }
-
- @Override
- public long getLong(int column) {
- try {
- return mCurrentPlaylistCursor.getLong(column);
- } catch (Exception ex) {
- onChange(true);
- return 0;
- }
- }
-
- @Override
- public float getFloat(int column) {
- return mCurrentPlaylistCursor.getFloat(column);
- }
-
- @Override
- public double getDouble(int column) {
- return mCurrentPlaylistCursor.getDouble(column);
- }
-
- @Override
- public int getType(int column) {
- return mCurrentPlaylistCursor.getType(column);
- }
-
- @Override
- public boolean isNull(int column) {
- return mCurrentPlaylistCursor.isNull(column);
- }
-
- @Override
- public String[] getColumnNames() {
- return mProjection;
- }
-
- @Override
- public void deactivate() {
- if (mCurrentPlaylistCursor != null)
- mCurrentPlaylistCursor.deactivate();
- }
-
- @Override
- public boolean requery() {
- makeNowPlayingCursor();
- return true;
- }
-
- private final String[] mProjection;
-
- private Cursor mCurrentPlaylistCursor;
-
- private int mSize;
-
- private long[] mNowPlaying;
-
- private long[] mCursorIdxs;
-
- private final Context context;
-
- private final IApolloService mService;
-}
diff --git a/src/com/andrew/apollo/RemoteControlClientCompat.java b/src/com/andrew/apollo/RemoteControlClientCompat.java
new file mode 100644
index 0000000..8f3f30a
--- /dev/null
+++ b/src/com/andrew/apollo/RemoteControlClientCompat.java
@@ -0,0 +1,397 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo;
+
+import android.app.PendingIntent;
+import android.graphics.Bitmap;
+import android.os.Looper;
+import android.util.Log;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+
+/**
+ * RemoteControlClient enables exposing information meant to be consumed by
+ * remote controls capable of displaying metadata, artwork and media transport
+ * control buttons. A remote control client object is associated with a media
+ * button event receiver. This event receiver must have been previously
+ * registered with
+ * {@link android.media.AudioManager#registerMediaButtonEventReceiver(android.content.ComponentName)}
+ * before the RemoteControlClient can be registered through
+ * {@link android.media.AudioManager#registerRemoteControlClient(android.media.RemoteControlClient)}
+ * .
+ */
+@SuppressWarnings({
+ "rawtypes", "unchecked"
+})
+public class RemoteControlClientCompat {
+
+ private static final String TAG = "RemoteControlCompat";
+
+ private static Class sRemoteControlClientClass;
+
+ // RCC short for RemoteControlClient
+ private static Method sRCCEditMetadataMethod;
+
+ private static Method sRCCSetPlayStateMethod;
+
+ private static Method sRCCSetTransportControlFlags;
+
+ private static boolean sHasRemoteControlAPIs = false;
+
+ static {
+ try {
+ final ClassLoader classLoader = RemoteControlClientCompat.class.getClassLoader();
+ sRemoteControlClientClass = getActualRemoteControlClientClass(classLoader);
+ // dynamically populate the playstate and flag values in case they
+ // change
+ // in future versions.
+ for (final Field field : RemoteControlClientCompat.class.getFields()) {
+ try {
+ final Field realField = sRemoteControlClientClass.getField(field.getName());
+ final Object realValue = realField.get(null);
+ field.set(null, realValue);
+ } catch (final NoSuchFieldException e) {
+ Log.w(TAG, "Could not get real field: " + field.getName());
+ } catch (final IllegalArgumentException e) {
+ Log.w(TAG,
+ "Error trying to pull field value for: " + field.getName() + " "
+ + e.getMessage());
+ } catch (final IllegalAccessException e) {
+ Log.w(TAG,
+ "Error trying to pull field value for: " + field.getName() + " "
+ + e.getMessage());
+ }
+ }
+
+ // get the required public methods on RemoteControlClient
+ sRCCEditMetadataMethod = sRemoteControlClientClass.getMethod("editMetadata",
+ boolean.class);
+ sRCCSetPlayStateMethod = sRemoteControlClientClass.getMethod("setPlaybackState",
+ int.class);
+ sRCCSetTransportControlFlags = sRemoteControlClientClass.getMethod(
+ "setTransportControlFlags", int.class);
+
+ sHasRemoteControlAPIs = true;
+ } catch (final ClassNotFoundException e) {
+ // Silently fail when running on an OS before ICS.
+ } catch (final NoSuchMethodException e) {
+ // Silently fail when running on an OS before ICS.
+ } catch (final IllegalArgumentException e) {
+ // Silently fail when running on an OS before ICS.
+ } catch (final SecurityException e) {
+ // Silently fail when running on an OS before ICS.
+ }
+ }
+
+ public static Class getActualRemoteControlClientClass(final ClassLoader classLoader)
+ throws ClassNotFoundException {
+ return classLoader.loadClass("android.media.RemoteControlClient");
+ }
+
+ private Object mActualRemoteControlClient;
+
+ public RemoteControlClientCompat(final PendingIntent pendingIntent) {
+ if (!sHasRemoteControlAPIs) {
+ return;
+ }
+ try {
+ mActualRemoteControlClient = sRemoteControlClientClass.getConstructor(
+ PendingIntent.class).newInstance(pendingIntent);
+ } catch (final Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public RemoteControlClientCompat(final PendingIntent pendingIntent, final Looper looper) {
+ if (!sHasRemoteControlAPIs) {
+ return;
+ }
+
+ try {
+ mActualRemoteControlClient = sRemoteControlClientClass.getConstructor(
+ PendingIntent.class, Looper.class).newInstance(pendingIntent, looper);
+ } catch (final Exception e) {
+ Log.e(TAG, "Error creating new instance of " + sRemoteControlClientClass.getName(), e);
+ }
+ }
+
+ /**
+ * Class used to modify metadata in a
+ * {@link android.media.RemoteControlClient} object. Use
+ * {@link android.media.RemoteControlClient#editMetadata(boolean)} to create
+ * an instance of an editor, on which you set the metadata for the
+ * RemoteControlClient instance. Once all the information has been set, use
+ * {@link #apply()} to make it the new metadata that should be displayed for
+ * the associated client. Once the metadata has been "applied", you cannot
+ * reuse this instance of the MetadataEditor.
+ */
+ public class MetadataEditorCompat {
+
+ private Method mPutStringMethod;
+
+ private Method mPutBitmapMethod;
+
+ private Method mPutLongMethod;
+
+ private Method mClearMethod;
+
+ private Method mApplyMethod;
+
+ private final Object mActualMetadataEditor;
+
+ /**
+ * The metadata key for the content artwork / album art.
+ */
+ public final static int METADATA_KEY_ARTWORK = 100;
+
+ private MetadataEditorCompat(final Object actualMetadataEditor) {
+ if (sHasRemoteControlAPIs && actualMetadataEditor == null) {
+ throw new IllegalArgumentException("Remote Control API's exist, "
+ + "should not be given a null MetadataEditor");
+ }
+ if (sHasRemoteControlAPIs) {
+ final Class metadataEditorClass = actualMetadataEditor.getClass();
+
+ try {
+ mPutStringMethod = metadataEditorClass.getMethod("putString", int.class,
+ String.class);
+ mPutBitmapMethod = metadataEditorClass.getMethod("putBitmap", int.class,
+ Bitmap.class);
+ mPutLongMethod = metadataEditorClass
+ .getMethod("putLong", int.class, long.class);
+ mClearMethod = metadataEditorClass.getMethod("clear", new Class[] {});
+ mApplyMethod = metadataEditorClass.getMethod("apply", new Class[] {});
+ } catch (final Exception e) {
+ throw new RuntimeException(e.getMessage(), e);
+ }
+ }
+ mActualMetadataEditor = actualMetadataEditor;
+ }
+
+ /**
+ * Adds textual information to be displayed. Note that none of the
+ * information added after {@link #apply()} has been called, will be
+ * displayed.
+ *
+ * @param key The identifier of a the metadata field to set. Valid
+ * values are
+ * {@link android.media.MediaMetadataRetriever#METADATA_KEY_ALBUM}
+ * ,
+ * {@link android.media.MediaMetadataRetriever#METADATA_KEY_ALBUMARTIST}
+ * ,
+ * {@link android.media.MediaMetadataRetriever#METADATA_KEY_TITLE}
+ * ,
+ * {@link android.media.MediaMetadataRetriever#METADATA_KEY_ARTIST}
+ * ,
+ * {@link android.media.MediaMetadataRetriever#METADATA_KEY_AUTHOR}
+ * ,
+ * {@link android.media.MediaMetadataRetriever#METADATA_KEY_COMPILATION}
+ * ,
+ * {@link android.media.MediaMetadataRetriever#METADATA_KEY_COMPOSER}
+ * ,
+ * {@link android.media.MediaMetadataRetriever#METADATA_KEY_DATE}
+ * ,
+ * {@link android.media.MediaMetadataRetriever#METADATA_KEY_GENRE}
+ * ,
+ * {@link android.media.MediaMetadataRetriever#METADATA_KEY_TITLE}
+ * ,
+ * {@link android.media.MediaMetadataRetriever#METADATA_KEY_WRITER}
+ * .
+ * @param value The text for the given key, or {@code null} to signify
+ * there is no valid information for the field.
+ * @return Returns a reference to the same MetadataEditor object, so you
+ * can chain put calls together.
+ */
+ public MetadataEditorCompat putString(final int key, final String value) {
+ if (sHasRemoteControlAPIs) {
+ try {
+ mPutStringMethod.invoke(mActualMetadataEditor, key, value);
+ } catch (final Exception e) {
+ throw new RuntimeException(e.getMessage(), e);
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Sets the album / artwork picture to be displayed on the remote
+ * control.
+ *
+ * @param key the identifier of the bitmap to set. The only valid value
+ * is {@link #METADATA_KEY_ARTWORK}
+ * @param bitmap The bitmap for the artwork, or null if there isn't any.
+ * @return Returns a reference to the same MetadataEditor object, so you
+ * can chain put calls together.
+ * @throws IllegalArgumentException
+ * @see android.graphics.Bitmap
+ */
+ public MetadataEditorCompat putBitmap(final int key, final Bitmap bitmap) {
+ if (sHasRemoteControlAPIs) {
+ try {
+ mPutBitmapMethod.invoke(mActualMetadataEditor, key, bitmap);
+ } catch (final Exception e) {
+ throw new RuntimeException(e.getMessage(), e);
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Adds numerical information to be displayed. Note that none of the
+ * information added after {@link #apply()} has been called, will be
+ * displayed.
+ *
+ * @param key the identifier of a the metadata field to set. Valid
+ * values are
+ * {@link android.media.MediaMetadataRetriever#METADATA_KEY_CD_TRACK_NUMBER}
+ * ,
+ * {@link android.media.MediaMetadataRetriever#METADATA_KEY_DISC_NUMBER}
+ * ,
+ * {@link android.media.MediaMetadataRetriever#METADATA_KEY_DURATION}
+ * (with a value expressed in milliseconds),
+ * {@link android.media.MediaMetadataRetriever#METADATA_KEY_YEAR}
+ * .
+ * @param value The long value for the given key
+ * @return Returns a reference to the same MetadataEditor object, so you
+ * can chain put calls together.
+ * @throws IllegalArgumentException
+ */
+ public MetadataEditorCompat putLong(final int key, final long value) {
+ if (sHasRemoteControlAPIs) {
+ try {
+ mPutLongMethod.invoke(mActualMetadataEditor, key, value);
+ } catch (final Exception e) {
+ throw new RuntimeException(e.getMessage(), e);
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Clears all the metadata that has been set since the MetadataEditor
+ * instance was created with
+ * {@link android.media.RemoteControlClient#editMetadata(boolean)}.
+ */
+ public void clear() {
+ if (sHasRemoteControlAPIs) {
+ try {
+ mClearMethod.invoke(mActualMetadataEditor, (Object[])null);
+ } catch (final Exception e) {
+ throw new RuntimeException(e.getMessage(), e);
+ }
+ }
+ }
+
+ /**
+ * Associates all the metadata that has been set since the
+ * MetadataEditor instance was created with
+ * {@link android.media.RemoteControlClient#editMetadata(boolean)}, or
+ * since {@link #clear()} was called, with the RemoteControlClient. Once
+ * "applied", this MetadataEditor cannot be reused to edit the
+ * RemoteControlClient's metadata.
+ */
+ public void apply() {
+ if (sHasRemoteControlAPIs) {
+ try {
+ mApplyMethod.invoke(mActualMetadataEditor, (Object[])null);
+ } catch (final Exception e) {
+ throw new RuntimeException(e.getMessage(), e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Creates a {@link android.media.RemoteControlClient.MetadataEditor}.
+ *
+ * @param startEmpty Set to false if you want the MetadataEditor to contain
+ * the metadata that was previously applied to the
+ * RemoteControlClient, or true if it is to be created empty.
+ * @return a new MetadataEditor instance.
+ */
+ public MetadataEditorCompat editMetadata(final boolean startEmpty) {
+ Object metadataEditor;
+ if (sHasRemoteControlAPIs) {
+ try {
+ metadataEditor = sRCCEditMetadataMethod.invoke(mActualRemoteControlClient,
+ startEmpty);
+ } catch (final Exception e) {
+ throw new RuntimeException(e);
+ }
+ } else {
+ metadataEditor = null;
+ }
+ return new MetadataEditorCompat(metadataEditor);
+ }
+
+ /**
+ * Sets the current playback state.
+ *
+ * @param state The current playback state, one of the following values:
+ * {@link android.media.RemoteControlClient#PLAYSTATE_STOPPED},
+ * {@link android.media.RemoteControlClient#PLAYSTATE_PAUSED},
+ * {@link android.media.RemoteControlClient#PLAYSTATE_PLAYING},
+ * {@link android.media.RemoteControlClient#PLAYSTATE_FAST_FORWARDING}
+ * ,
+ * {@link android.media.RemoteControlClient#PLAYSTATE_REWINDING},
+ * {@link android.media.RemoteControlClient#PLAYSTATE_SKIPPING_FORWARDS}
+ * ,
+ * {@link android.media.RemoteControlClient#PLAYSTATE_SKIPPING_BACKWARDS}
+ * ,
+ * {@link android.media.RemoteControlClient#PLAYSTATE_BUFFERING},
+ * {@link android.media.RemoteControlClient#PLAYSTATE_ERROR}.
+ */
+ public void setPlaybackState(final int state) {
+ if (sHasRemoteControlAPIs) {
+ try {
+ sRCCSetPlayStateMethod.invoke(mActualRemoteControlClient, state);
+ } catch (final Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ /**
+ * Sets the flags for the media transport control buttons that this client
+ * supports.
+ *
+ * @param transportControlFlags A combination of the following flags:
+ * {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_PREVIOUS}
+ * ,
+ * {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_REWIND}
+ * ,
+ * {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_PLAY},
+ * {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_PLAY_PAUSE}
+ * ,
+ * {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_PAUSE}
+ * ,
+ * {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_STOP},
+ * {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_FAST_FORWARD}
+ * ,
+ * {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_NEXT}
+ */
+ public void setTransportControlFlags(final int transportControlFlags) {
+ if (sHasRemoteControlAPIs) {
+ try {
+ sRCCSetTransportControlFlags.invoke(mActualRemoteControlClient,
+ transportControlFlags);
+ } catch (final Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ public final Object getActualRemoteControlClientObject() {
+ return mActualRemoteControlClient;
+ }
+}
diff --git a/src/com/andrew/apollo/RemoteControlHelper.java b/src/com/andrew/apollo/RemoteControlHelper.java
new file mode 100644
index 0000000..cca56f6
--- /dev/null
+++ b/src/com/andrew/apollo/RemoteControlHelper.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2012 Android Open Source Project Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.andrew.apollo;
+
+import android.media.AudioManager;
+import android.util.Log;
+
+import java.lang.reflect.Method;
+
+/**
+ * Contains methods to handle registering/unregistering remote control clients.
+ * These methods only run on ICS devices. On previous devices, all methods are
+ * no-ops.
+ */
+@SuppressWarnings({
+ "rawtypes"
+})
+public class RemoteControlHelper {
+ private static final String TAG = "RemoteControlHelper";
+
+ private static boolean sHasRemoteControlAPIs = false;
+
+ private static Method sRegisterRemoteControlClientMethod;
+
+ private static Method sUnregisterRemoteControlClientMethod;
+
+ static {
+ try {
+ final ClassLoader classLoader = RemoteControlHelper.class.getClassLoader();
+ final Class sRemoteControlClientClass = RemoteControlClientCompat
+ .getActualRemoteControlClientClass(classLoader);
+ sRegisterRemoteControlClientMethod = AudioManager.class.getMethod(
+ "registerRemoteControlClient", new Class[] {
+ sRemoteControlClientClass
+ });
+ sUnregisterRemoteControlClientMethod = AudioManager.class.getMethod(
+ "unregisterRemoteControlClient", new Class[] {
+ sRemoteControlClientClass
+ });
+ sHasRemoteControlAPIs = true;
+ } catch (final ClassNotFoundException e) {
+ // Silently fail when running on an OS before ICS.
+ } catch (final NoSuchMethodException e) {
+ // Silently fail when running on an OS before ICS.
+ } catch (final IllegalArgumentException e) {
+ // Silently fail when running on an OS before ICS.
+ } catch (final SecurityException e) {
+ // Silently fail when running on an OS before ICS.
+ }
+ }
+
+ public static void registerRemoteControlClient(final AudioManager audioManager,
+ final RemoteControlClientCompat remoteControlClient) {
+ if (!sHasRemoteControlAPIs) {
+ return;
+ }
+
+ try {
+ sRegisterRemoteControlClientMethod.invoke(audioManager,
+ remoteControlClient.getActualRemoteControlClientObject());
+ } catch (final Exception e) {
+ Log.e(TAG, e.getMessage(), e);
+ }
+ }
+
+ public static void unregisterRemoteControlClient(final AudioManager audioManager,
+ final RemoteControlClientCompat remoteControlClient) {
+ if (!sHasRemoteControlAPIs) {
+ return;
+ }
+
+ try {
+ sUnregisterRemoteControlClientMethod.invoke(audioManager,
+ remoteControlClient.getActualRemoteControlClientObject());
+ } catch (final Exception e) {
+ Log.e(TAG, e.getMessage(), e);
+ }
+ }
+}
diff --git a/src/com/andrew/apollo/activities/AudioPlayerHolder.java b/src/com/andrew/apollo/activities/AudioPlayerHolder.java
deleted file mode 100644
index a22c4c1..0000000
--- a/src/com/andrew/apollo/activities/AudioPlayerHolder.java
+++ /dev/null
@@ -1,291 +0,0 @@
-/**
- *
- */
-
-package com.andrew.apollo.activities;
-
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.ServiceConnection;
-import android.content.pm.ActivityInfo;
-import android.content.res.Resources;
-import android.media.AudioManager;
-import android.media.audiofx.AudioEffect;
-import android.os.Bundle;
-import android.os.IBinder;
-import android.os.RemoteException;
-import android.provider.BaseColumns;
-import android.provider.MediaStore.Audio;
-import android.support.v4.app.FragmentActivity;
-import android.support.v4.view.ViewPager;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.widget.FrameLayout;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import com.andrew.apollo.AudioPlayerFragment;
-import com.andrew.apollo.Constants;
-import com.andrew.apollo.IApolloService;
-import com.andrew.apollo.R;
-import com.andrew.apollo.adapters.PagerAdapter;
-import com.andrew.apollo.list.fragments.TracksFragment;
-import com.andrew.apollo.preferences.SettingsHolder;
-import com.andrew.apollo.service.ApolloService;
-import com.andrew.apollo.service.ServiceToken;
-import com.andrew.apollo.utils.ApolloUtils;
-import com.andrew.apollo.utils.MusicUtils;
-import com.andrew.apollo.utils.ThemeUtils;
-
-/**
- * @author Andrew Neal
- * @Note This is the "holder" for the @TracksFragment(queue) and @AudioPlayerFragment
- */
-public class AudioPlayerHolder extends FragmentActivity implements ServiceConnection, Constants {
-
- private ServiceToken mToken;
-
- // Options
- private static final int FAVORITE = 0;
-
- private static final int SEARCH = 1;
-
- private static final int EFFECTS_PANEL = 0;
-
- @Override
- protected void onCreate(Bundle icicle) {
- // For the theme chooser and overflow MenuItem
- if (ThemeUtils.overflowLight(this)) {
- setTheme(R.style.Apollo_Holo);
- } else {
- setTheme(R.style.Apollo_Holo_Light);
- }
- // Landscape mode on phone isn't ready
- if (!ApolloUtils.isTablet(this))
- setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
-
- // Control Media volume
- setVolumeControlStream(AudioManager.STREAM_MUSIC);
-
- // Layout
- setContentView(R.layout.audio_player_browser);
-
- // Set up the colorstrip
- initColorstrip();
-
- // Set up the ActionBar
- initActionBar();
-
- // Important!
- initPager();
- super.onCreate(icicle);
- }
-
- @Override
- public void onServiceConnected(ComponentName name, IBinder obj) {
- MusicUtils.mService = IApolloService.Stub.asInterface(obj);
- }
-
- @Override
- public void onServiceDisconnected(ComponentName name) {
- MusicUtils.mService = null;
- }
-
- /**
- * Update the MenuItems in the ActionBar
- */
- private final BroadcastReceiver mMediaStatusReceiver = new BroadcastReceiver() {
-
- @Override
- public void onReceive(Context context, Intent intent) {
- invalidateOptionsMenu();
- }
-
- };
-
- @Override
- protected void onStart() {
- // Bind to Service
- mToken = MusicUtils.bindToService(this, this);
-
- IntentFilter filter = new IntentFilter();
- filter.addAction(ApolloService.META_CHANGED);
- filter.addAction(ApolloService.PLAYSTATE_CHANGED);
-
- registerReceiver(mMediaStatusReceiver, filter);
- super.onStart();
- }
-
- @Override
- protected void onStop() {
- // Unbind
- if (MusicUtils.mService != null) {
- MusicUtils.unbindFromService(mToken);
- mToken = null;
- }
-
- unregisterReceiver(mMediaStatusReceiver);
- super.onStop();
- }
-
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- menu.add(0, FAVORITE, 0, R.string.cd_favorite).setShowAsAction(
- MenuItem.SHOW_AS_ACTION_IF_ROOM);
- menu.add(0, SEARCH, 0, R.string.cd_search).setIcon(R.drawable.apollo_holo_light_search)
- .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
-
- MenuInflater inflater = getMenuInflater();
- inflater.inflate(R.menu.overflow_now_playing, menu);
- return super.onCreateOptionsMenu(menu);
- }
-
- @Override
- public boolean onPrepareOptionsMenu(Menu menu) {
- MenuItem favorite = menu.findItem(FAVORITE);
- MenuItem search = menu.findItem(SEARCH);
- if (MusicUtils.mService != null && MusicUtils.getCurrentAudioId() != -1) {
- if (MusicUtils.isFavorite(this, MusicUtils.getCurrentAudioId())) {
- favorite.setIcon(R.drawable.apollo_holo_light_favorite_selected);
- } else {
- favorite.setIcon(R.drawable.apollo_holo_light_favorite_normal);
- // Theme chooser
- ThemeUtils.setActionBarItem(this, favorite, "apollo_favorite_normal");
- }
- }
- // Theme chooser
- ThemeUtils.setActionBarItem(this, search, "apollo_search");
- return super.onPrepareOptionsMenu(menu);
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
-
- switch (item.getItemId()) {
- case android.R.id.home: {
- Intent intent = new Intent();
- intent.setClass(this, MusicLibrary.class);
- intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
- startActivity(intent);
- finish();
- break;
- }
- case FAVORITE: {
- MusicUtils.toggleFavorite();
- invalidateOptionsMenu();
- break;
- }
- case SEARCH: {
- onSearchRequested();
- break;
- }
- case R.id.add_to_playlist: {
- Intent intent = new Intent(INTENT_ADD_TO_PLAYLIST);
- long[] list = new long[1];
- list[0] = MusicUtils.getCurrentAudioId();
- intent.putExtra(INTENT_PLAYLIST_LIST, list);
- startActivity(intent);
- break;
- }
- case R.id.eq: {
- Intent i = new Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL);
- i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, MusicUtils.getCurrentAudioId());
- startActivityForResult(i, EFFECTS_PANEL);
- break;
- }
- case R.id.play_store: {
- ApolloUtils.shopFor(this, MusicUtils.getArtistName());
- break;
- }
- case R.id.share: {
- shareCurrentTrack();
- break;
- }
- case R.id.settings: {
- startActivity(new Intent(this, SettingsHolder.class));
- break;
- }
- default:
- break;
- }
- return super.onOptionsItemSelected(item);
- }
-
- private void initActionBar() {
- ApolloUtils.showUpTitleOnly(getActionBar());
-
- // The ActionBar Title and UP ids are hidden.
- int titleId = Resources.getSystem().getIdentifier("action_bar_title", "id", "android");
- int upId = Resources.getSystem().getIdentifier("up", "id", "android");
-
- TextView actionBarTitle = (TextView)findViewById(titleId);
- ImageView actionBarUp = (ImageView)findViewById(upId);
-
- // Theme chooser
- ThemeUtils.setActionBarBackground(this, getActionBar(), "action_bar_background");
- ThemeUtils.setTextColor(this, actionBarTitle, "action_bar_title_color");
- ThemeUtils.initThemeChooser(this, actionBarUp, "action_bar_up", THEME_ITEM_BACKGROUND);
- }
-
- /**
- * @return Share intent
- * @throws RemoteException
- */
- private String shareCurrentTrack() {
- if (MusicUtils.getTrackName() == null || MusicUtils.getArtistName() == null) {
-
- }
-
- Intent shareIntent = new Intent();
- String currentTrackMessage = getResources().getString(R.string.now_listening_to) + " "
- + MusicUtils.getTrackName() + " " + getResources().getString(R.string.by) + " "
- + MusicUtils.getArtistName();
-
- shareIntent.setAction(Intent.ACTION_SEND);
- shareIntent.setType("text/plain");
- shareIntent.putExtra(Intent.EXTRA_TEXT, currentTrackMessage);
-
- startActivity(Intent.createChooser(shareIntent,
- getResources().getString(R.string.share_track_using)));
- return currentTrackMessage;
- }
-
- /**
- * Initiate ViewPager and PagerAdapter
- */
- public void initPager() {
- // Initiate PagerAdapter
- PagerAdapter mPagerAdapter = new PagerAdapter(getSupportFragmentManager());
- Bundle bundle = new Bundle();
- bundle.putString(MIME_TYPE, Audio.Playlists.CONTENT_TYPE);
- bundle.putLong(BaseColumns._ID, PLAYLIST_QUEUE);
- mPagerAdapter.addFragment(new TracksFragment(bundle));
- // Artists
- mPagerAdapter.addFragment(new AudioPlayerFragment());
-
- // Initiate ViewPager
- ViewPager mViewPager = (ViewPager)findViewById(R.id.viewPager);
- mViewPager.setPageMargin(getResources().getInteger(R.integer.viewpager_margin_width));
- mViewPager.setPageMarginDrawable(R.drawable.viewpager_margin);
- mViewPager.setOffscreenPageLimit(mPagerAdapter.getCount());
- mViewPager.setAdapter(mPagerAdapter);
- mViewPager.setCurrentItem(1);
-
- // Theme chooser
- ThemeUtils.initThemeChooser(this, mViewPager, "viewpager", THEME_ITEM_BACKGROUND);
- ThemeUtils.setMarginDrawable(this, mViewPager, "viewpager_margin");
- }
-
- /**
- * For the theme chooser
- */
- private void initColorstrip() {
- FrameLayout mColorstrip = (FrameLayout)findViewById(R.id.colorstrip);
- mColorstrip.setBackgroundColor(getResources().getColor(R.color.holo_blue_dark));
- ThemeUtils.setBackgroundColor(this, mColorstrip, "colorstrip");
- }
-}
diff --git a/src/com/andrew/apollo/activities/MusicLibrary.java b/src/com/andrew/apollo/activities/MusicLibrary.java
deleted file mode 100644
index 42f8e7c..0000000
--- a/src/com/andrew/apollo/activities/MusicLibrary.java
+++ /dev/null
@@ -1,165 +0,0 @@
-/**
- *
- */
-
-package com.andrew.apollo.activities;
-
-import android.content.ComponentName;
-import android.content.IntentFilter;
-import android.content.ServiceConnection;
-import android.content.pm.ActivityInfo;
-import android.media.AudioManager;
-import android.os.Bundle;
-import android.os.IBinder;
-import android.provider.BaseColumns;
-import android.provider.MediaStore.Audio;
-import android.support.v4.app.FragmentActivity;
-import android.support.v4.view.ViewPager;
-import android.view.Window;
-
-import com.andrew.apollo.BottomActionBarControlsFragment;
-import com.andrew.apollo.BottomActionBarFragment;
-import com.andrew.apollo.Constants;
-import com.andrew.apollo.IApolloService;
-import com.andrew.apollo.R;
-import com.andrew.apollo.adapters.PagerAdapter;
-import com.andrew.apollo.adapters.ScrollingTabsAdapter;
-import com.andrew.apollo.grid.fragments.AlbumsFragment;
-import com.andrew.apollo.grid.fragments.ArtistsFragment;
-import com.andrew.apollo.list.fragments.GenresFragment;
-import com.andrew.apollo.list.fragments.PlaylistsFragment;
-import com.andrew.apollo.list.fragments.RecentlyAddedFragment;
-import com.andrew.apollo.list.fragments.TracksFragment;
-import com.andrew.apollo.service.ApolloService;
-import com.andrew.apollo.service.ServiceToken;
-import com.andrew.apollo.ui.widgets.ScrollableTabView;
-import com.andrew.apollo.utils.ApolloUtils;
-import com.andrew.apollo.utils.MusicUtils;
-import com.andrew.apollo.utils.ThemeUtils;
-
-/**
- * @author Andrew Neal
- * @Note This is the "holder" for all of the tabs
- */
-public class MusicLibrary extends FragmentActivity implements ServiceConnection, Constants {
-
- private ServiceToken mToken;
-
- @Override
- protected void onCreate(Bundle icicle) {
- // Landscape mode on phone isn't ready
- if (!ApolloUtils.isTablet(this))
- setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
-
- // Scan for music
- requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
-
- // Control Media volume
- setVolumeControlStream(AudioManager.STREAM_MUSIC);
-
- // Layout
- setContentView(R.layout.library_browser);
-
- // Hide the ActionBar
- getActionBar().hide();
-
- // Important!
- initPager();
-
- // Update the BottomActionBar
- initBottomActionBar();
- super.onCreate(icicle);
- }
-
- @Override
- public void onServiceConnected(ComponentName name, IBinder obj) {
- MusicUtils.mService = IApolloService.Stub.asInterface(obj);
- }
-
- @Override
- public void onServiceDisconnected(ComponentName name) {
- MusicUtils.mService = null;
- }
-
- @Override
- protected void onStart() {
- // Bind to Service
- mToken = MusicUtils.bindToService(this, this);
-
- IntentFilter filter = new IntentFilter();
- filter.addAction(ApolloService.META_CHANGED);
- super.onStart();
- }
-
- @Override
- protected void onStop() {
- // Unbind
- if (MusicUtils.mService != null)
- MusicUtils.unbindFromService(mToken);
- super.onStop();
- }
-
- /**
- * Initiate ViewPager and PagerAdapter
- */
- public void initPager() {
- // Initiate PagerAdapter
- PagerAdapter mPagerAdapter = new PagerAdapter(getSupportFragmentManager());
-
- Bundle bundle = new Bundle();
- bundle.putString(MIME_TYPE, Audio.Playlists.CONTENT_TYPE);
- bundle.putLong(BaseColumns._ID, PLAYLIST_RECENTLY_ADDED);
- // Recently added tracks
- mPagerAdapter.addFragment(new RecentlyAddedFragment(bundle));
- // Artists
- mPagerAdapter.addFragment(new ArtistsFragment());
- // Albums
- mPagerAdapter.addFragment(new AlbumsFragment());
- // // Tracks
- mPagerAdapter.addFragment(new TracksFragment());
- // // Playlists
- mPagerAdapter.addFragment(new PlaylistsFragment());
- // // Genres
- mPagerAdapter.addFragment(new GenresFragment());
-
- // Initiate ViewPager
- ViewPager mViewPager = (ViewPager)findViewById(R.id.viewPager);
- mViewPager.setPageMargin(getResources().getInteger(R.integer.viewpager_margin_width));
- mViewPager.setPageMarginDrawable(R.drawable.viewpager_margin);
- mViewPager.setOffscreenPageLimit(mPagerAdapter.getCount());
- mViewPager.setAdapter(mPagerAdapter);
- mViewPager.setCurrentItem(1);
-
- // Tabs
- initScrollableTabs(mViewPager);
-
- // Theme chooser
- ThemeUtils.initThemeChooser(this, mViewPager, "viewpager", THEME_ITEM_BACKGROUND);
- ThemeUtils.setMarginDrawable(this, mViewPager, "viewpager_margin");
- }
-
- /**
- * Initiate the tabs
- */
- public void initScrollableTabs(ViewPager mViewPager) {
- ScrollableTabView mScrollingTabs = (ScrollableTabView)findViewById(R.id.scrollingTabs);
- ScrollingTabsAdapter mScrollingTabsAdapter = new ScrollingTabsAdapter(this);
- mScrollingTabs.setAdapter(mScrollingTabsAdapter);
- mScrollingTabs.setViewPager(mViewPager);
-
- // Theme chooser
- ThemeUtils.initThemeChooser(this, mScrollingTabs, "scrollable_tab_background",
- THEME_ITEM_BACKGROUND);
- }
-
- /**
- * Initiate the BottomActionBar
- */
- private void initBottomActionBar() {
- PagerAdapter pagerAdatper = new PagerAdapter(getSupportFragmentManager());
- pagerAdatper.addFragment(new BottomActionBarFragment());
- pagerAdatper.addFragment(new BottomActionBarControlsFragment());
- ViewPager viewPager = (ViewPager)findViewById(R.id.bottomActionBarPager);
- viewPager.setAdapter(pagerAdatper);
- }
-}
diff --git a/src/com/andrew/apollo/activities/QueryBrowserActivity.java b/src/com/andrew/apollo/activities/QueryBrowserActivity.java
deleted file mode 100644
index 3ec7281..0000000
--- a/src/com/andrew/apollo/activities/QueryBrowserActivity.java
+++ /dev/null
@@ -1,448 +0,0 @@
-/*
- * 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.
- */
-
-package com.andrew.apollo.activities;
-
-import android.app.ListActivity;
-import android.app.SearchManager;
-import android.content.AsyncQueryHandler;
-import android.content.ComponentName;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.ServiceConnection;
-import android.database.Cursor;
-import android.database.DatabaseUtils;
-import android.graphics.Color;
-import android.media.AudioManager;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.IBinder;
-import android.provider.BaseColumns;
-import android.provider.MediaStore;
-import android.provider.MediaStore.Audio;
-import android.text.TextUtils;
-import android.util.Log;
-import android.view.KeyEvent;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.FrameLayout;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.ListView;
-import android.widget.SimpleCursorAdapter;
-import android.widget.TextView;
-
-import com.andrew.apollo.Constants;
-import com.andrew.apollo.R;
-import com.andrew.apollo.service.ServiceToken;
-import com.andrew.apollo.utils.ApolloUtils;
-import com.andrew.apollo.utils.MusicUtils;
-
-public class QueryBrowserActivity extends ListActivity implements Constants, ServiceConnection {
- private QueryListAdapter mAdapter;
-
- private boolean mAdapterSent;
-
- private String mFilterString = "";
-
- private ServiceToken mToken;
-
- public QueryBrowserActivity() {
- }
-
- /** Called when the activity is first created. */
- @SuppressWarnings("deprecation")
- @Override
- public void onCreate(Bundle icicle) {
- super.onCreate(icicle);
- setVolumeControlStream(AudioManager.STREAM_MUSIC);
- mAdapter = (QueryListAdapter)getLastNonConfigurationInstance();
- mToken = MusicUtils.bindToService(this, this);
- // defer the real work until we're bound to the service
- }
-
- @Override
- public void onServiceConnected(ComponentName name, IBinder service) {
- if (mAdapter != null) {
- getQueryCursor(mAdapter.getQueryHandler(), null);
- }
-
- Intent intent = getIntent();
- String action = intent != null ? intent.getAction() : null;
-
- if (Intent.ACTION_VIEW.equals(action)) {
- // this is something we got from the search bar
- Uri uri = intent.getData();
- String path = uri.toString();
- if (path.startsWith("content://media/external/audio/media/")) {
- // This is a specific file
- String id = uri.getLastPathSegment();
- long[] list = new long[] {
- Long.valueOf(id)
- };
- MusicUtils.playAll(this, list, 0);
- finish();
- return;
- } else if (path.startsWith("content://media/external/audio/albums/")) {
- // This is an album, show the songs on it
- Intent i = new Intent(Intent.ACTION_VIEW);
- i.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/track");
- i.putExtra("album", uri.getLastPathSegment());
- startActivity(i);
- finish();
- return;
- } else if (path.startsWith("content://media/external/audio/artists/")) {
- intent = new Intent(Intent.ACTION_VIEW);
-
- Bundle bundle = new Bundle();
- bundle.putString(MIME_TYPE, Audio.Artists.CONTENT_TYPE);
- bundle.putString(ARTIST_KEY, uri.getLastPathSegment());
- bundle.putLong(BaseColumns._ID,
- ApolloUtils.getArtistId(uri.getLastPathSegment(), ARTIST_ID, this));
-
- intent.setClass(this, TracksBrowser.class);
- intent.putExtras(bundle);
- startActivity(intent);
- return;
- }
- }
-
- mFilterString = intent.getStringExtra(SearchManager.QUERY);
- if (MediaStore.INTENT_ACTION_MEDIA_SEARCH.equals(action)) {
- String focus = intent.getStringExtra(MediaStore.EXTRA_MEDIA_FOCUS);
- String artist = intent.getStringExtra(MediaStore.EXTRA_MEDIA_ARTIST);
- String album = intent.getStringExtra(MediaStore.EXTRA_MEDIA_ALBUM);
- String title = intent.getStringExtra(MediaStore.EXTRA_MEDIA_TITLE);
- if (focus != null) {
- if (focus.startsWith("audio/") && title != null) {
- mFilterString = title;
- } else if (focus.equals(Audio.Albums.ENTRY_CONTENT_TYPE)) {
- if (album != null) {
- mFilterString = album;
- if (artist != null) {
- mFilterString = mFilterString + " " + artist;
- }
- }
- } else if (focus.equals(Audio.Artists.ENTRY_CONTENT_TYPE)) {
- if (artist != null) {
- mFilterString = artist;
- }
- }
- }
- }
-
- setContentView(R.layout.listview);
- mTrackList = getListView();
- mTrackList.setTextFilterEnabled(true);
- if (mAdapter == null) {
- mAdapter = new QueryListAdapter(getApplication(), this, R.layout.listview_items, null, // cursor
- new String[] {}, new int[] {}, 0);
- setListAdapter(mAdapter);
- if (TextUtils.isEmpty(mFilterString)) {
- getQueryCursor(mAdapter.getQueryHandler(), null);
- } else {
- mTrackList.setFilterText(mFilterString);
- mFilterString = null;
- }
- } else {
- mAdapter.setActivity(this);
- setListAdapter(mAdapter);
- mQueryCursor = mAdapter.getCursor();
- if (mQueryCursor != null) {
- init(mQueryCursor);
- } else {
- getQueryCursor(mAdapter.getQueryHandler(), mFilterString);
- }
- }
-
- LinearLayout emptyness = (LinearLayout)findViewById(R.id.empty_view);
- emptyness.setVisibility(View.GONE);
- }
-
- @Override
- public void onServiceDisconnected(ComponentName name) {
-
- }
-
- @Override
- public Object onRetainNonConfigurationInstance() {
- mAdapterSent = true;
- return mAdapter;
- }
-
- @Override
- public void onDestroy() {
- MusicUtils.unbindFromService(mToken);
- // If we have an adapter and didn't send it off to another activity yet,
- // we should
- // close its cursor, which we do by assigning a null cursor to it. Doing
- // this
- // instead of closing the cursor directly keeps the framework from
- // accessing
- // the closed cursor later.
- if (!mAdapterSent && mAdapter != null) {
- mAdapter.changeCursor(null);
- }
- // Because we pass the adapter to the next activity, we need to make
- // sure it doesn't keep a reference to this activity. We can do this
- // by clearing its DatasetObservers, which setListAdapter(null) does.
- try {
- setListAdapter(null);
- } catch (NullPointerException e) {
-
- }
- mAdapter = null;
- super.onDestroy();
- }
-
- public void init(Cursor c) {
-
- if (mAdapter == null) {
- return;
- }
- mAdapter.changeCursor(c);
-
- if (mQueryCursor == null) {
- setListAdapter(null);
- return;
- }
- }
-
- @Override
- protected void onListItemClick(ListView l, View v, int position, long id) {
- // Dialog doesn't allow us to wait for a result, so we need to store
- // the info we need for when the dialog posts its result
- mQueryCursor.moveToPosition(position);
- if (mQueryCursor.isBeforeFirst() || mQueryCursor.isAfterLast()) {
- return;
- }
- String selectedType = mQueryCursor.getString(mQueryCursor
- .getColumnIndexOrThrow(Audio.Media.MIME_TYPE));
-
- if ("artist".equals(selectedType)) {
- Intent intent = new Intent(Intent.ACTION_VIEW);
-
- TextView tv1 = (TextView)v.findViewById(R.id.listview_item_line_one);
- String artistName = tv1.getText().toString();
-
- Bundle bundle = new Bundle();
- bundle.putString(MIME_TYPE, Audio.Artists.CONTENT_TYPE);
- bundle.putString(ARTIST_KEY, artistName);
- bundle.putLong(BaseColumns._ID, id);
-
- intent.setClass(this, TracksBrowser.class);
- intent.putExtras(bundle);
- startActivity(intent);
- finish();
- } else if ("album".equals(selectedType)) {
- TextView tv1 = (TextView)v.findViewById(R.id.listview_item_line_one);
- TextView tv2 = (TextView)v.findViewById(R.id.listview_item_line_two);
-
- String artistName = tv2.getText().toString();
- String albumName = tv1.getText().toString();
-
- Bundle bundle = new Bundle();
- bundle.putString(MIME_TYPE, Audio.Albums.CONTENT_TYPE);
- bundle.putString(ARTIST_KEY, artistName);
- bundle.putString(ALBUM_KEY, albumName);
- bundle.putLong(BaseColumns._ID, id);
-
- Intent intent = new Intent(Intent.ACTION_VIEW);
- intent.setClass(this, TracksBrowser.class);
- intent.putExtras(bundle);
- startActivity(intent);
- finish();
- } else if (position >= 0 && id >= 0) {
- long[] list = new long[] {
- id
- };
- MusicUtils.playAll(this, list, 0);
- } else {
- Log.e("QueryBrowser", "invalid position/id: " + position + "/" + id);
- }
- }
-
- private Cursor getQueryCursor(AsyncQueryHandler async, String filter) {
- if (filter == null) {
- filter = "";
- }
- String[] ccols = new String[] {
- BaseColumns._ID, Audio.Media.MIME_TYPE, Audio.Artists.ARTIST, Audio.Albums.ALBUM,
- Audio.Media.TITLE, "data1", "data2"
- };
-
- Uri search = Uri.parse("content://media/external/audio/search/fancy/" + Uri.encode(filter));
-
- Cursor ret = null;
- if (async != null) {
- async.startQuery(0, null, search, ccols, null, null, null);
- } else {
- ret = MusicUtils.query(this, search, ccols, null, null, null);
- }
- return ret;
- }
-
- static class QueryListAdapter extends SimpleCursorAdapter {
- private QueryBrowserActivity mActivity = null;
-
- private final AsyncQueryHandler mQueryHandler;
-
- private String mConstraint = null;
-
- private boolean mConstraintIsValid = false;
-
- class QueryHandler extends AsyncQueryHandler {
- QueryHandler(ContentResolver res) {
- super(res);
- }
-
- @Override
- protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
- mActivity.init(cursor);
- }
- }
-
- QueryListAdapter(Context context, QueryBrowserActivity currentactivity, int layout,
- Cursor cursor, String[] from, int[] to, int flags) {
- super(context, layout, cursor, from, to, flags);
- mActivity = currentactivity;
- mQueryHandler = new QueryHandler(context.getContentResolver());
- }
-
- public void setActivity(QueryBrowserActivity newactivity) {
- mActivity = newactivity;
- }
-
- public AsyncQueryHandler getQueryHandler() {
- return mQueryHandler;
- }
-
- @Override
- public void bindView(View view, Context context, Cursor cursor) {
-
- TextView tv1 = (TextView)view.findViewById(R.id.listview_item_line_one);
- tv1.setTextColor(Color.BLACK);
- TextView tv2 = (TextView)view.findViewById(R.id.listview_item_line_two);
- tv2.setTextColor(Color.BLACK);
- ImageView iv = (ImageView)view.findViewById(R.id.listview_item_image);
- iv.setVisibility(View.GONE);
- FrameLayout fl = (FrameLayout)view.findViewById(R.id.track_list_context_frame);
- fl.setVisibility(View.GONE);
- ViewGroup.LayoutParams p = iv.getLayoutParams();
- if (p == null) {
- // seen this happen, not sure why
- DatabaseUtils.dumpCursor(cursor);
- return;
- }
- p.width = ViewGroup.LayoutParams.WRAP_CONTENT;
- p.height = ViewGroup.LayoutParams.WRAP_CONTENT;
-
- String mimetype = cursor.getString(cursor.getColumnIndexOrThrow(Audio.Media.MIME_TYPE));
-
- if (mimetype == null) {
- mimetype = "audio/";
- }
- if (mimetype.equals("artist")) {
- String name = cursor.getString(cursor.getColumnIndexOrThrow(Audio.Artists.ARTIST));
- String displayname = name;
- boolean isunknown = false;
- if (name == null || name.equals(MediaStore.UNKNOWN_STRING)) {
- displayname = context.getString(R.string.unknown);
- isunknown = true;
- }
- tv1.setText(displayname);
-
- int numalbums = cursor.getInt(cursor.getColumnIndexOrThrow("data1"));
- int numsongs = cursor.getInt(cursor.getColumnIndexOrThrow("data2"));
-
- String songs_albums = MusicUtils.makeAlbumsLabel(context, numalbums, numsongs,
- isunknown);
-
- tv2.setText(songs_albums);
-
- } else if (mimetype.equals("album")) {
- String name = cursor.getString(cursor.getColumnIndexOrThrow(Audio.Albums.ALBUM));
- String displayname = name;
- if (name == null || name.equals(MediaStore.UNKNOWN_STRING)) {
- displayname = context.getString(R.string.unknown);
- }
- tv1.setText(displayname);
-
- name = cursor.getString(cursor.getColumnIndexOrThrow(Audio.Artists.ARTIST));
- displayname = name;
- if (name == null || name.equals(MediaStore.UNKNOWN_STRING)) {
- displayname = context.getString(R.string.unknown);
- }
- tv2.setText(displayname);
-
- } else if (mimetype.startsWith("audio/") || mimetype.equals("application/ogg")
- || mimetype.equals("application/x-ogg")) {
- String name = cursor.getString(cursor.getColumnIndexOrThrow(Audio.Media.TITLE));
- tv1.setText(name);
-
- String displayname = cursor.getString(cursor
- .getColumnIndexOrThrow(Audio.Artists.ARTIST));
- if (displayname == null || displayname.equals(MediaStore.UNKNOWN_STRING)) {
- displayname = context.getString(R.string.unknown);
- }
- name = cursor.getString(cursor.getColumnIndexOrThrow(Audio.Albums.ALBUM));
- if (name == null || name.equals(MediaStore.UNKNOWN_STRING)) {
- name = context.getString(R.string.unknown);
- }
- tv2.setText(displayname + " - " + name);
- }
- }
-
- @Override
- public void changeCursor(Cursor cursor) {
- if (mActivity.isFinishing() && cursor != null) {
- cursor.close();
- cursor = null;
- }
- if (cursor != mActivity.mQueryCursor) {
- mActivity.mQueryCursor = cursor;
- super.changeCursor(cursor);
- }
- }
-
- @Override
- public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
- String s = constraint.toString();
- if (mConstraintIsValid
- && ((s == null && mConstraint == null) || (s != null && s.equals(mConstraint)))) {
- return getCursor();
- }
- Cursor c = mActivity.getQueryCursor(null, s);
- mConstraint = s;
- mConstraintIsValid = true;
- return c;
- }
- }
-
- private ListView mTrackList;
-
- private Cursor mQueryCursor;
-
- @Override
- public boolean onKeyDown(int keyCode, KeyEvent event) {
- if (keyCode == KeyEvent.KEYCODE_BACK) {
- finish();
- return true;
- }
- return super.onKeyDown(keyCode, event);
- }
-}
diff --git a/src/com/andrew/apollo/activities/QuickQueue.java b/src/com/andrew/apollo/activities/QuickQueue.java
deleted file mode 100644
index 136123a..0000000
--- a/src/com/andrew/apollo/activities/QuickQueue.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/**
- *
- */
-
-package com.andrew.apollo.activities;
-
-import android.media.AudioManager;
-import android.os.Bundle;
-import android.provider.BaseColumns;
-import android.provider.MediaStore.Audio;
-import android.support.v4.app.FragmentActivity;
-
-import com.andrew.apollo.Constants;
-import com.andrew.apollo.grid.fragments.QuickQueueFragment;
-
-/**
- * @author Andrew Neal
- */
-public class QuickQueue extends FragmentActivity implements Constants {
-
- @Override
- protected void onCreate(Bundle icicle) {
- // This needs to be called first
- super.onCreate(icicle);
-
- // Control Media volume
- setVolumeControlStream(AudioManager.STREAM_MUSIC);
-
- Bundle bundle = new Bundle();
- bundle.putString(MIME_TYPE, Audio.Playlists.CONTENT_TYPE);
- bundle.putLong(BaseColumns._ID, PLAYLIST_QUEUE);
- getSupportFragmentManager().beginTransaction()
- .replace(android.R.id.content, new QuickQueueFragment(bundle)).commit();
- }
-}
diff --git a/src/com/andrew/apollo/activities/TracksBrowser.java b/src/com/andrew/apollo/activities/TracksBrowser.java
deleted file mode 100644
index 513e5c3..0000000
--- a/src/com/andrew/apollo/activities/TracksBrowser.java
+++ /dev/null
@@ -1,444 +0,0 @@
-/**
- *
- */
-
-package com.andrew.apollo.activities;
-
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.ServiceConnection;
-import android.content.pm.ActivityInfo;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.drawable.AnimationDrawable;
-import android.media.AudioManager;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.os.IBinder;
-import android.os.SystemClock;
-import android.provider.BaseColumns;
-import android.provider.MediaStore.Audio;
-import android.support.v4.app.FragmentActivity;
-import android.support.v4.view.ViewPager;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.widget.FrameLayout;
-import android.widget.ImageView;
-import android.widget.RelativeLayout;
-import android.widget.TextView;
-
-import com.andrew.apollo.BottomActionBarControlsFragment;
-import com.andrew.apollo.BottomActionBarFragment;
-import com.andrew.apollo.Constants;
-import com.andrew.apollo.IApolloService;
-import com.andrew.apollo.R;
-import com.andrew.apollo.adapters.PagerAdapter;
-import com.andrew.apollo.list.fragments.ArtistAlbumsFragment;
-import com.andrew.apollo.list.fragments.TracksFragment;
-import com.andrew.apollo.service.ApolloService;
-import com.andrew.apollo.service.ServiceToken;
-import com.andrew.apollo.tasks.GetCachedImages;
-import com.andrew.apollo.tasks.LastfmGetAlbumImages;
-import com.andrew.apollo.tasks.LastfmGetArtistImagesOriginal;
-import com.andrew.apollo.utils.ApolloUtils;
-import com.andrew.apollo.utils.MusicUtils;
-import com.andrew.apollo.utils.ThemeUtils;
-
-/**
- * @author Andrew Neal
- * @Note This displays specific track or album listings
- */
-public class TracksBrowser extends FragmentActivity implements Constants, ServiceConnection {
-
- // Bundle
- private Bundle bundle;
-
- private Intent intent;
-
- private String mimeType;
-
- private ServiceToken mToken;
-
- private final long[] mHits = new long[3];
-
- @Override
- protected void onCreate(Bundle icicle) {
- // Landscape mode on phone isn't ready
- if (!ApolloUtils.isTablet(this))
- setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
-
- // Control Media volume
- setVolumeControlStream(AudioManager.STREAM_MUSIC);
-
- // Layout
- setContentView(R.layout.track_browser);
-
- // Important!
- whatBundle(icicle);
-
- // Update the colorstrip color
- initColorstrip();
-
- // Update the ActionBar
- initActionBar();
-
- // Update the half_and_half layout
- initUpperHalf();
-
- // Important!
- initPager();
-
- // Update the BottomActionBar
- initBottomActionBar();
- super.onCreate(icicle);
- }
-
- @Override
- public void onSaveInstanceState(Bundle outcicle) {
- outcicle.putAll(bundle);
- super.onSaveInstanceState(outcicle);
- }
-
- @Override
- public void onServiceConnected(ComponentName name, IBinder obj) {
- MusicUtils.mService = IApolloService.Stub.asInterface(obj);
- }
-
- @Override
- public void onServiceDisconnected(ComponentName name) {
- MusicUtils.mService = null;
- }
-
- /**
- * Update next BottomActionBar as needed
- */
- private final BroadcastReceiver mMediaStatusReceiver = new BroadcastReceiver() {
-
- @Override
- public void onReceive(Context context, Intent intent) {
- if (ApolloUtils.isArtist(mimeType) || ApolloUtils.isAlbum(mimeType))
- setArtistImage();
- }
-
- };
-
- @Override
- protected void onStart() {
- // Bind to Service
- mToken = MusicUtils.bindToService(this, this);
-
- IntentFilter filter = new IntentFilter();
- filter.addAction(ApolloService.META_CHANGED);
- registerReceiver(mMediaStatusReceiver, filter);
-
- setTitle();
- super.onStart();
- }
-
- @Override
- protected void onStop() {
- // Unbind
- if (MusicUtils.mService != null)
- MusicUtils.unbindFromService(mToken);
-
- unregisterReceiver(mMediaStatusReceiver);
- super.onStop();
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case android.R.id.home:
- // Artist ID
- long id = ApolloUtils.getArtistId(getArtist(), ARTIST_ID, this);
- if (ApolloUtils.isAlbum(mimeType) && id != 0)
- tracksBrowser(id);
- super.onBackPressed();
- break;
- default:
- break;
- }
- return super.onOptionsItemSelected(item);
- }
-
- /**
- * @param icicle
- * @return what Bundle we're dealing with
- */
- public void whatBundle(Bundle icicle) {
- intent = getIntent();
- bundle = icicle != null ? icicle : intent.getExtras();
- if (bundle == null) {
- bundle = new Bundle();
- }
- if (bundle.getString(INTENT_ACTION) == null) {
- bundle.putString(INTENT_ACTION, intent.getAction());
- }
- if (bundle.getString(MIME_TYPE) == null) {
- bundle.putString(MIME_TYPE, intent.getType());
- }
- mimeType = bundle.getString(MIME_TYPE);
- }
-
- /**
- * For the theme chooser
- */
- private void initColorstrip() {
- FrameLayout mColorstrip = (FrameLayout)findViewById(R.id.colorstrip);
- mColorstrip.setBackgroundColor(getResources().getColor(R.color.holo_blue_dark));
- ThemeUtils.setBackgroundColor(this, mColorstrip, "colorstrip");
- }
-
- /**
- * Set the ActionBar title
- */
- private void initActionBar() {
- ApolloUtils.showUpTitleOnly(getActionBar());
-
- // The ActionBar Title and UP ids are hidden.
- int titleId = Resources.getSystem().getIdentifier("action_bar_title", "id", "android");
- int upId = Resources.getSystem().getIdentifier("up", "id", "android");
-
- TextView actionBarTitle = (TextView)findViewById(titleId);
- ImageView actionBarUp = (ImageView)findViewById(upId);
-
- // Theme chooser
- ThemeUtils.setActionBarBackground(this, getActionBar(), "action_bar_background");
- ThemeUtils.setTextColor(this, actionBarTitle, "action_bar_title_color");
- ThemeUtils.initThemeChooser(this, actionBarUp, "action_bar_up", THEME_ITEM_BACKGROUND);
-
- }
-
- /**
- * Sets up the @half_and_half.xml layout
- */
- private void initUpperHalf() {
-
- if (ApolloUtils.isArtist(mimeType)) {
- // Get next artist image
- } else if (ApolloUtils.isAlbum(mimeType)) {
- // Album image
- setAlbumImage();
-
- // Artist name
- TextView mArtistName = (TextView)findViewById(R.id.half_artist_image_text);
- mArtistName.setVisibility(View.VISIBLE);
- mArtistName.setText(getArtist());
- mArtistName.setBackgroundColor(getResources().getColor(R.color.transparent_black));
-
- // Album name
- TextView mAlbumName = (TextView)findViewById(R.id.half_album_image_text);
- mAlbumName.setText(getAlbum());
- mAlbumName.setBackgroundColor(getResources().getColor(R.color.transparent_black));
-
- // Album half container
- RelativeLayout mSecondHalfContainer = (RelativeLayout)findViewById(R.id.album_half_container);
- // Show the second half while viewing an album
- mSecondHalfContainer.setVisibility(View.VISIBLE);
- } else {
- // Set the logo
- setPromoImage();
- }
- }
-
- /**
- * Initiate ViewPager and PagerAdapter
- */
- private void initPager() {
- // Initiate PagerAdapter
- PagerAdapter mPagerAdapter = new PagerAdapter(getSupportFragmentManager());
- if (ApolloUtils.isArtist(mimeType))
- // Show all albums for an artist
- mPagerAdapter.addFragment(new ArtistAlbumsFragment(bundle));
- // Show the tracks for an artist or album
- mPagerAdapter.addFragment(new TracksFragment(bundle));
-
- // Set up ViewPager
- ViewPager mViewPager = (ViewPager)findViewById(R.id.viewPager);
- mViewPager.setPageMargin(getResources().getInteger(R.integer.viewpager_margin_width));
- mViewPager.setPageMarginDrawable(R.drawable.viewpager_margin);
- mViewPager.setOffscreenPageLimit(mPagerAdapter.getCount());
- mViewPager.setAdapter(mPagerAdapter);
-
- // Theme chooser
- ThemeUtils.initThemeChooser(this, mViewPager, "viewpager", THEME_ITEM_BACKGROUND);
- ThemeUtils.setMarginDrawable(this, mViewPager, "viewpager_margin");
- }
-
- /**
- * Initiate the BottomActionBar
- */
- private void initBottomActionBar() {
- PagerAdapter pagerAdatper = new PagerAdapter(getSupportFragmentManager());
- pagerAdatper.addFragment(new BottomActionBarFragment());
- pagerAdatper.addFragment(new BottomActionBarControlsFragment());
- ViewPager viewPager = (ViewPager)findViewById(R.id.bottomActionBarPager);
- viewPager.setAdapter(pagerAdatper);
- }
-
- /**
- * @return artist name from Bundle
- */
- public String getArtist() {
- if (bundle.getString(ARTIST_KEY) != null)
- return bundle.getString(ARTIST_KEY);
- return getResources().getString(R.string.app_name);
- }
-
- /**
- * @return album name from Bundle
- */
- public String getAlbum() {
- if (bundle.getString(ALBUM_KEY) != null)
- return bundle.getString(ALBUM_KEY);
- return getResources().getString(R.string.app_name);
- }
-
- /**
- * @return genre name from Bundle
- */
- public String getGenre() {
- if (bundle.getString(GENRE_KEY) != null)
- return bundle.getString(GENRE_KEY);
- return getResources().getString(R.string.app_name);
- }
-
- /**
- * @return playlist name from Bundle
- */
- public String getPlaylist() {
- if (bundle.getString(PLAYLIST_NAME) != null)
- return bundle.getString(PLAYLIST_NAME);
- return getResources().getString(R.string.app_name);
- }
-
- /**
- * Set the header when viewing a genre
- */
- public void setPromoImage() {
-
- // Artist image & Genre image
- ImageView mFirstHalfImage = (ImageView)findViewById(R.id.half_artist_image);
-
- Bitmap header = BitmapFactory.decodeResource(getResources(), R.drawable.promo);
- ApolloUtils.runnableBackground(mFirstHalfImage, header);
- }
-
- /**
- * Cache and set artist image
- */
- public void setArtistImage() {
-
- // Artist image & Genre image
- final ImageView mFirstHalfImage = (ImageView)findViewById(R.id.half_artist_image);
-
- mFirstHalfImage.post(new Runnable() {
- @Override
- public void run() {
- // Only download images we don't already have
- if (ApolloUtils.getImageURL(getArtist(), ARTIST_IMAGE_ORIGINAL, TracksBrowser.this) == null)
- new LastfmGetArtistImagesOriginal(TracksBrowser.this, mFirstHalfImage)
- .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, getArtist());
- // Get and set cached image
- new GetCachedImages(TracksBrowser.this, 0, mFirstHalfImage).executeOnExecutor(
- AsyncTask.THREAD_POOL_EXECUTOR, getArtist());
- }
- });
-
- mFirstHalfImage.setOnClickListener(new OnClickListener() {
-
- @Override
- public void onClick(View v) {
- System.arraycopy(mHits, 1, mHits, 0, mHits.length - 1);
- mHits[mHits.length - 1] = SystemClock.uptimeMillis();
- if (mHits[0] >= (SystemClock.uptimeMillis() - 250)) {
- AnimationDrawable meow = ApolloUtils.getNyanCat(TracksBrowser.this);
- mFirstHalfImage.setImageDrawable(meow);
- meow.start();
- }
- }
- });
- }
-
- /**
- * Cache and set album image
- */
- public void setAlbumImage() {
-
- // Album image
- final ImageView mSecondHalfImage = (ImageView)findViewById(R.id.half_album_image);
-
- mSecondHalfImage.post(new Runnable() {
- @Override
- public void run() {
- // Only download images we don't already have
- if (ApolloUtils.getImageURL(getAlbum(), ALBUM_IMAGE, TracksBrowser.this) == null)
- new LastfmGetAlbumImages(TracksBrowser.this, mSecondHalfImage, 1)
- .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, getArtist(),
- getAlbum());
- // Get and set cached image
- new GetCachedImages(TracksBrowser.this, 1, mSecondHalfImage).executeOnExecutor(
- AsyncTask.THREAD_POOL_EXECUTOR, getAlbum());
- }
- });
- }
-
- /**
- * Return here from viewing the tracks for an album and view all albums and
- * tracks for the same artist
- */
- private void tracksBrowser(long id) {
-
- bundle.putString(MIME_TYPE, Audio.Artists.CONTENT_TYPE);
- bundle.putString(ARTIST_KEY, getArtist());
- bundle.putLong(BaseColumns._ID, id);
-
- Intent intent = new Intent(Intent.ACTION_VIEW);
- intent.setClass(this, TracksBrowser.class);
- intent.putExtras(bundle);
- startActivity(intent);
- }
-
- /**
- * Set the correct title
- */
- private void setTitle() {
- String name;
- long id;
- if (Audio.Playlists.CONTENT_TYPE.equals(mimeType)) {
- id = bundle.getLong(BaseColumns._ID);
- switch ((int)id) {
- case (int)PLAYLIST_QUEUE:
- setTitle(R.string.nowplaying);
- return;
- case (int)PLAYLIST_FAVORITES:
- setTitle(R.string.favorite);
- return;
- default:
- if (id < 0) {
- setTitle(R.string.app_name);
- return;
- }
- }
- name = MusicUtils.getPlaylistName(this, id);
- } else if (Audio.Artists.CONTENT_TYPE.equals(mimeType)) {
- id = bundle.getLong(BaseColumns._ID);
- name = MusicUtils.getArtistName(this, id, true);
- } else if (Audio.Albums.CONTENT_TYPE.equals(mimeType)) {
- id = bundle.getLong(BaseColumns._ID);
- name = MusicUtils.getAlbumName(this, id, true);
- } else if (Audio.Genres.CONTENT_TYPE.equals(mimeType)) {
- id = bundle.getLong(BaseColumns._ID);
- name = MusicUtils.parseGenreName(this, MusicUtils.getGenreName(this, id, true));
- } else {
- setTitle(R.string.app_name);
- return;
- }
- setTitle(name);
- }
-}
diff --git a/src/com/andrew/apollo/adapters/AlbumAdapter.java b/src/com/andrew/apollo/adapters/AlbumAdapter.java
index 69ee784..07c8aef 100644
--- a/src/com/andrew/apollo/adapters/AlbumAdapter.java
+++ b/src/com/andrew/apollo/adapters/AlbumAdapter.java
@@ -1,105 +1,245 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.adapters;
-import java.lang.ref.WeakReference;
-
import android.content.Context;
-import android.database.Cursor;
-import android.graphics.drawable.AnimationDrawable;
-import android.os.AsyncTask;
-import android.os.RemoteException;
-import android.support.v4.widget.SimpleCursorAdapter;
+import android.view.LayoutInflater;
import android.view.View;
+import android.view.View.OnClickListener;
import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
-import com.andrew.apollo.Constants;
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.andrew.apollo.Config;
import com.andrew.apollo.R;
-import com.andrew.apollo.grid.fragments.AlbumsFragment;
-import com.andrew.apollo.tasks.LastfmGetAlbumImages;
-import com.andrew.apollo.tasks.ViewHolderTask;
+import com.andrew.apollo.cache.ImageFetcher;
+import com.andrew.apollo.model.Album;
+import com.andrew.apollo.ui.MusicHolder;
+import com.andrew.apollo.ui.MusicHolder.DataHolder;
import com.andrew.apollo.utils.ApolloUtils;
import com.andrew.apollo.utils.MusicUtils;
-import com.andrew.apollo.views.ViewHolderGrid;
-import com.androidquery.AQuery;
/**
- * @author Andrew Neal
+ * This {@link ArrayAdapter} is used to display all of the albums on a user's
+ * device for {@link RecentsFragment} and {@link AlbumsFragment}.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
*/
-public class AlbumAdapter extends SimpleCursorAdapter implements Constants {
+public class AlbumAdapter extends ArrayAdapter<Album> {
+
+ /**
+ * Number of views (ImageView and TextView)
+ */
+ private static final int VIEW_TYPE_COUNT = 2;
+
+ /**
+ * The resource Id of the layout to inflate
+ */
+ private final int mLayoutId;
+
+ /**
+ * Image cache and image fetcher
+ */
+ private final ImageFetcher mImageFetcher;
+
+ /**
+ * Semi-transparent overlay
+ */
+ private final int mOverlay;
+
+ /**
+ * Determines if the grid or list should be the default style
+ */
+ private boolean mLoadExtraData = false;
+
+ /**
+ * Sets the album art on click listener to start playing them album when
+ * touched.
+ */
+ private boolean mTouchPlay = false;
+
+ /**
+ * Used to cache the album info
+ */
+ private DataHolder[] mData;
+
+ /**
+ * Constructor of <code>AlbumAdapter</code>
+ *
+ * @param context The {@link Context} to use.
+ * @param layoutId The resource Id of the view to inflate.
+ * @param style Determines which layout to use and therefore which items to
+ * load.
+ */
+ public AlbumAdapter(final Context context, final int layoutId) {
+ super(context, 0);
+ // Get the layout Id
+ mLayoutId = layoutId;
+ // Initialize the cache & image fetcher
+ mImageFetcher = ApolloUtils.getImageFetcher((SherlockFragmentActivity)context);
+ // Cache the transparent overlay
+ mOverlay = context.getResources().getColor(R.color.list_item_background);
+ }
- private AnimationDrawable mPeakOneAnimation, mPeakTwoAnimation;
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View getView(final int position, View convertView, final ViewGroup parent) {
+ // Recycle ViewHolder's items
+ MusicHolder holder;
+ if (convertView == null) {
+ convertView = LayoutInflater.from(getContext()).inflate(mLayoutId, parent, false);
+ holder = new MusicHolder(convertView);
+ convertView.setTag(holder);
+ } else {
+ holder = (MusicHolder)convertView.getTag();
+ }
- private WeakReference<ViewHolderGrid> holderReference;
+ // Retrieve the data holder
+ final DataHolder dataHolder = mData[position];
+
+ // Set each album name (line one)
+ holder.mLineOne.get().setText(dataHolder.mLineOne);
+ // Set the artist name (line two)
+ holder.mLineTwo.get().setText(dataHolder.mLineTwo);
+ // Asynchronously load the album images into the adapter
+ mImageFetcher.loadAlbumImage(dataHolder.mLineTwo, dataHolder.mLineOne, dataHolder.mItemId,
+ holder.mImage.get());
+ // List view only items
+ if (mLoadExtraData) {
+ // Make sure the background layer gets set
+ holder.mOverlay.get().setBackgroundColor(mOverlay);
+ // Set the number of songs (line three)
+ holder.mLineThree.get().setText(dataHolder.mLineThree);
+ // Asynchronously load the artist image on the background view
+ mImageFetcher.loadArtistImage(dataHolder.mLineTwo, holder.mBackground.get());
+ }
+ if (mTouchPlay) {
+ // Play the album when the artwork is touched
+ playAlbum(holder.mImage.get(), position);
+ }
+ return convertView;
+ }
- public AlbumAdapter(Context context, int layout, Cursor c, String[] from, int[] to, int flags) {
- super(context, layout, c, from, to, flags);
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean hasStableIds() {
+ return true;
}
+ /**
+ * {@inheritDoc}
+ */
@Override
- public View getView(final int position, View convertView, ViewGroup parent) {
- final View view = super.getView(position, convertView, parent);
- // ViewHolderGrid
- final ViewHolderGrid viewholder;
+ public int getViewTypeCount() {
+ return VIEW_TYPE_COUNT;
+ }
- if (view != null) {
+ /**
+ * Method used to cache the data used to populate the list or grid. The idea
+ * is to cache everything before {@code #getView(int, View, ViewGroup)} is
+ * called.
+ */
+ public void buildCache() {
+ mData = new DataHolder[getCount()];
+ for (int i = 0; i < getCount(); i++) {
+ // Build the album
+ final Album album = getItem(i);
+
+ // Build the data holder
+ mData[i] = new DataHolder();
+ // Album Id
+ mData[i].mItemId = album.mAlbumId;
+ // Album names (line one)
+ mData[i].mLineOne = album.mAlbumName;
+ // Album artist names (line two)
+ mData[i].mLineTwo = album.mArtistName;
+ // Number of songs for each album (line three)
+ mData[i].mLineThree = album.mSongNumber;
+ }
+ }
- viewholder = new ViewHolderGrid(view);
- holderReference = new WeakReference<ViewHolderGrid>(viewholder);
- view.setTag(holderReference.get());
+ /**
+ * Starts playing an album if the user touches the artwork in the list.
+ *
+ * @param album The {@link ImageView} holding the album
+ * @param position The position of the album to play.
+ */
+ private void playAlbum(final ImageView album, final int position) {
+ // Prevent accidental touches
+ if (!mImageFetcher.isScrolling()) {
+ album.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(final View v) {
+ final String id = getItem(position).mAlbumId;
+ final long[] list = MusicUtils.getSongListForAlbum(getContext(), id);
+ MusicUtils.playAll(getContext(), list, 0, false);
+ }
+ });
+ }
+ }
- } else {
- viewholder = (ViewHolderGrid)convertView.getTag();
+ /**
+ * Method that unloads and clears the items in the adapter
+ */
+ public void unload() {
+ clear();
+ mData = null;
+ }
+
+ /**
+ * @param pause True to temporarily pause the disk cache, false otherwise.
+ */
+ public void setPauseDiskCache(final boolean pause) {
+ if (mImageFetcher != null) {
+ mImageFetcher.setPauseDiskCache(pause);
}
+ }
- // AQuery
- final AQuery aq = new AQuery(view);
+ /**
+ * @param album The key used to find the cached album to remove
+ */
+ public void removeFromCache(final Album album) {
+ if (mImageFetcher != null) {
+ mImageFetcher.removeFromCache(album.mAlbumName + Config.ALBUM_ART_SUFFIX);
+ }
+ }
- // Album name
- String albumName = mCursor.getString(AlbumsFragment.mAlbumNameIndex);
- holderReference.get().mViewHolderLineOne.setText(albumName);
+ /**
+ * Flushes the disk cache.
+ */
+ public void flush() {
+ mImageFetcher.flush();
+ }
- // Artist name
- String artistName = mCursor.getString(AlbumsFragment.mArtistNameIndex);
- holderReference.get().mViewHolderLineTwo.setText(artistName);
+ /**
+ * @param extra True to load line three and the background image, false
+ * otherwise.
+ */
+ public void setLoadExtraData(final boolean extra) {
+ mLoadExtraData = extra;
+ setTouchPlay(true);
+ }
- // Match positions
- holderReference.get().position = position;
- if (aq.shouldDelay(position, view, parent, "")) {
- holderReference.get().mViewHolderImage.setImageDrawable(null);
- } else {
- // Check for missing album images and cache them
- if (ApolloUtils.getImageURL(albumName, ALBUM_IMAGE, mContext) == null) {
- new LastfmGetAlbumImages(mContext, null, 0).executeOnExecutor(
- AsyncTask.THREAD_POOL_EXECUTOR, artistName, albumName);
- } else {
- new ViewHolderTask(null, holderReference.get(), position, mContext, 1, 1,
- holderReference.get().mViewHolderImage).executeOnExecutor(
- AsyncTask.THREAD_POOL_EXECUTOR, albumName);
- }
- }
- // Now playing indicator
- long currentalbumid = MusicUtils.getCurrentAlbumId();
- long albumid = mCursor.getLong(AlbumsFragment.mAlbumIdIndex);
- if (currentalbumid == albumid) {
- holderReference.get().mPeakOne.setImageResource(R.anim.peak_meter_1);
- holderReference.get().mPeakTwo.setImageResource(R.anim.peak_meter_2);
- mPeakOneAnimation = (AnimationDrawable)holderReference.get().mPeakOne.getDrawable();
- mPeakTwoAnimation = (AnimationDrawable)holderReference.get().mPeakTwo.getDrawable();
- try {
- if (MusicUtils.mService.isPlaying()) {
- mPeakOneAnimation.start();
- mPeakTwoAnimation.start();
- } else {
- mPeakOneAnimation.stop();
- mPeakTwoAnimation.stop();
- }
- } catch (RemoteException e) {
- e.printStackTrace();
- }
- } else {
- holderReference.get().mPeakOne.setImageResource(0);
- holderReference.get().mPeakTwo.setImageResource(0);
- }
- return view;
+ /**
+ * @param play True to play the album when the artwork is touched, false
+ * otherwise.
+ */
+ public void setTouchPlay(final boolean play) {
+ mTouchPlay = play;
}
}
diff --git a/src/com/andrew/apollo/adapters/ArtistAdapter.java b/src/com/andrew/apollo/adapters/ArtistAdapter.java
index eac0ef6..83b20c7 100644
--- a/src/com/andrew/apollo/adapters/ArtistAdapter.java
+++ b/src/com/andrew/apollo/adapters/ArtistAdapter.java
@@ -1,107 +1,227 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.adapters;
-import java.lang.ref.WeakReference;
-
import android.content.Context;
-import android.database.Cursor;
-import android.graphics.drawable.AnimationDrawable;
-import android.os.AsyncTask;
-import android.os.RemoteException;
-import android.provider.MediaStore;
-import android.support.v4.widget.SimpleCursorAdapter;
+import android.view.LayoutInflater;
import android.view.View;
+import android.view.View.OnClickListener;
import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
-import com.andrew.apollo.Constants;
+import com.actionbarsherlock.app.SherlockFragmentActivity;
import com.andrew.apollo.R;
-import com.andrew.apollo.grid.fragments.ArtistsFragment;
-import com.andrew.apollo.tasks.LastfmGetArtistImages;
-import com.andrew.apollo.tasks.ViewHolderTask;
+import com.andrew.apollo.cache.ImageFetcher;
+import com.andrew.apollo.model.Artist;
+import com.andrew.apollo.ui.MusicHolder;
+import com.andrew.apollo.ui.MusicHolder.DataHolder;
import com.andrew.apollo.utils.ApolloUtils;
import com.andrew.apollo.utils.MusicUtils;
-import com.andrew.apollo.views.ViewHolderGrid;
-import com.androidquery.AQuery;
/**
- * @author Andrew Neal
+ * This {@link ArrayAdapter} is used to display all of the artists on a user's
+ * device for {@link ArtistFragment}.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
*/
-public class ArtistAdapter extends SimpleCursorAdapter implements Constants {
-
- private AnimationDrawable mPeakOneAnimation, mPeakTwoAnimation;
+/**
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class ArtistAdapter extends ArrayAdapter<Artist> {
+
+ /**
+ * Number of views (ImageView and TextView)
+ */
+ private static final int VIEW_TYPE_COUNT = 2;
+
+ /**
+ * The resource Id of the layout to inflate
+ */
+ private final int mLayoutId;
+
+ /**
+ * Image cache and image fetcher
+ */
+ private final ImageFetcher mImageFetcher;
+
+ /**
+ * Semi-transparent overlay
+ */
+ private final int mOverlay;
+
+ /**
+ * Used to cache the artist info
+ */
+ private DataHolder[] mData;
+
+ /**
+ * Loads line three and the background image if the user decides to.
+ */
+ private boolean mLoadExtraData = false;
+
+ /**
+ * Constructor of <code>ArtistAdapter</code>
+ *
+ * @param context The {@link Context} to use.
+ * @param layoutId The resource Id of the view to inflate.
+ */
+ public ArtistAdapter(final Context context, final int layoutId) {
+ super(context, 0);
+ // Get the layout Id
+ mLayoutId = layoutId;
+ // Initialize the cache & image fetcher
+ mImageFetcher = ApolloUtils.getImageFetcher((SherlockFragmentActivity)context);
+ // Cache the transparent overlay
+ mOverlay = context.getResources().getColor(R.color.list_item_background);
+ }
- private WeakReference<ViewHolderGrid> holderReference;
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View getView(final int position, View convertView, final ViewGroup parent) {
+ // Recycle ViewHolder's items
+ MusicHolder holder;
+ if (convertView == null) {
+ convertView = LayoutInflater.from(getContext()).inflate(mLayoutId, parent, false);
+ holder = new MusicHolder(convertView);
+ convertView.setTag(holder);
+ } else {
+ holder = (MusicHolder)convertView.getTag();
+ }
- public ArtistAdapter(Context context, int layout, Cursor c, String[] from, int[] to, int flags) {
- super(context, layout, c, from, to, flags);
+ // Retrieve the data holder
+ final DataHolder dataHolder = mData[position];
+
+ // Set each artist name (line one)
+ holder.mLineOne.get().setText(dataHolder.mLineOne);
+ // Set the number of albums (line two)
+ holder.mLineTwo.get().setText(dataHolder.mLineTwo);
+ // Asynchronously load the artist image into the adapter
+ mImageFetcher.loadArtistImage(dataHolder.mLineOne, holder.mImage.get());
+ if (mLoadExtraData) {
+ // Make sure the background layer gets set
+ holder.mOverlay.get().setBackgroundColor(mOverlay);
+ // Set the number of songs (line three)
+ holder.mLineThree.get().setText(dataHolder.mLineThree);
+ // Set the background image
+ mImageFetcher.loadArtistImage(dataHolder.mLineOne, holder.mBackground.get());
+ // Play the artist when the artwork is touched
+ playArtist(holder.mImage.get(), position);
+ }
+ return convertView;
}
+ /**
+ * {@inheritDoc}
+ */
@Override
- public View getView(final int position, View convertView, ViewGroup parent) {
- final View view = super.getView(position, convertView, parent);
- // ViewHolderGrid
- final ViewHolderGrid viewholder;
-
- if (view != null) {
+ public boolean hasStableIds() {
+ return true;
+ }
- viewholder = new ViewHolderGrid(view);
- holderReference = new WeakReference<ViewHolderGrid>(viewholder);
- view.setTag(holderReference.get());
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getViewTypeCount() {
+ return VIEW_TYPE_COUNT;
+ }
- } else {
- viewholder = (ViewHolderGrid)convertView.getTag();
+ /**
+ * Method used to cache the data used to populate the list or grid. The idea
+ * is to cache everything before {@code #getView(int, View, ViewGroup)} is
+ * called.
+ */
+ public void buildCache() {
+ mData = new DataHolder[getCount()];
+ for (int i = 0; i < getCount(); i++) {
+ // Build the artist
+ final Artist artist = getItem(i);
+
+ // Build the data holder
+ mData[i] = new DataHolder();
+ // Artist Id
+ mData[i].mItemId = artist.mArtistId;
+ // Artist names (line one)
+ mData[i].mLineOne = artist.mArtistName;
+ // Number of albums (line two)
+ mData[i].mLineTwo = artist.mAlbumNumber;
+ // Number of songs (line three)
+ mData[i].mLineThree = artist.mSongNumber;
}
+ }
- // AQuery
- final AQuery aq = new AQuery(view);
-
- // Artist Name
- String artistName = mCursor.getString(ArtistsFragment.mArtistNameIndex);
- holderReference.get().mViewHolderLineOne.setText(artistName);
+ /**
+ * Starts playing an artist if the user touches the artist image in the
+ * list.
+ *
+ * @param artist The {@link ImageView} holding the aritst image
+ * @param position The position of the artist to play.
+ */
+ private void playArtist(final ImageView artist, final int position) {
+ // Prevent accidental touches
+ if (!mImageFetcher.isScrolling()) {
+ artist.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(final View v) {
+ final String id = getItem(position).mArtistId;
+ final long[] list = MusicUtils.getSongListForArtist(getContext(), id);
+ MusicUtils.playAll(getContext(), list, 0, false);
+ }
+ });
+ }
+ }
- // Number of albums
- int albums_plural = mCursor.getInt(ArtistsFragment.mArtistNumAlbumsIndex);
- boolean unknown = artistName == null || artistName.equals(MediaStore.UNKNOWN_STRING);
- String numAlbums = MusicUtils.makeAlbumsLabel(mContext, albums_plural, 0, unknown);
- holderReference.get().mViewHolderLineTwo.setText(numAlbums);
+ /**
+ * Method that unloads and clears the items in the adapter
+ */
+ public void unload() {
+ clear();
+ mData = null;
+ }
- holderReference.get().position = position;
- if (aq.shouldDelay(position, view, parent, "")) {
- holderReference.get().mViewHolderImage.setImageDrawable(null);
- } else {
- // Check for missing artist images and cache them
- if (ApolloUtils.getImageURL(artistName, ARTIST_IMAGE, mContext) == null) {
- new LastfmGetArtistImages(mContext).executeOnExecutor(
- AsyncTask.THREAD_POOL_EXECUTOR, artistName);
- } else {
- new ViewHolderTask(null, holderReference.get(), position, mContext, 0, 1,
- holderReference.get().mViewHolderImage).executeOnExecutor(
- AsyncTask.THREAD_POOL_EXECUTOR, artistName);
- }
+ /**
+ * @param pause True to temporarily pause the disk cache, false otherwise.
+ */
+ public void setPauseDiskCache(final boolean pause) {
+ if (mImageFetcher != null) {
+ mImageFetcher.setPauseDiskCache(pause);
}
- // Now playing indicator
- long currentartistid = MusicUtils.getCurrentArtistId();
- long artistid = mCursor.getLong(ArtistsFragment.mArtistIdIndex);
- if (currentartistid == artistid) {
- holderReference.get().mPeakOne.setImageResource(R.anim.peak_meter_1);
- holderReference.get().mPeakTwo.setImageResource(R.anim.peak_meter_2);
- mPeakOneAnimation = (AnimationDrawable)holderReference.get().mPeakOne.getDrawable();
- mPeakTwoAnimation = (AnimationDrawable)holderReference.get().mPeakTwo.getDrawable();
- try {
- if (MusicUtils.mService.isPlaying()) {
- mPeakOneAnimation.start();
- mPeakTwoAnimation.start();
- } else {
- mPeakOneAnimation.stop();
- mPeakTwoAnimation.stop();
- }
- } catch (RemoteException e) {
- e.printStackTrace();
- }
- } else {
- holderReference.get().mPeakOne.setImageResource(0);
- holderReference.get().mPeakTwo.setImageResource(0);
+ }
+
+ /**
+ * @param artist The key used to find the cached artist to remove
+ */
+ public void removeFromCache(final Artist artist) {
+ if (mImageFetcher != null) {
+ mImageFetcher.removeFromCache(artist.mArtistName);
}
- return view;
+ }
+
+ /**
+ * Flushes the disk cache.
+ */
+ public void flush() {
+ mImageFetcher.flush();
+ }
+
+ /**
+ * @param extra True to load line three and the background image, false
+ * otherwise.
+ */
+ public void setLoadExtraData(final boolean extra) {
+ mLoadExtraData = extra;
}
}
diff --git a/src/com/andrew/apollo/adapters/ArtistAlbumAdapter.java b/src/com/andrew/apollo/adapters/ArtistAlbumAdapter.java
index b75e517..50059bd 100644
--- a/src/com/andrew/apollo/adapters/ArtistAlbumAdapter.java
+++ b/src/com/andrew/apollo/adapters/ArtistAlbumAdapter.java
@@ -1,123 +1,248 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.adapters;
-import java.lang.ref.WeakReference;
-
import android.content.Context;
-import android.database.Cursor;
-import android.graphics.drawable.AnimationDrawable;
-import android.os.AsyncTask;
-import android.os.RemoteException;
-import android.support.v4.widget.SimpleCursorAdapter;
+import android.view.LayoutInflater;
import android.view.View;
+import android.view.View.OnClickListener;
import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
-import com.andrew.apollo.Constants;
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.andrew.apollo.Config;
import com.andrew.apollo.R;
-import com.andrew.apollo.list.fragments.ArtistAlbumsFragment;
-import com.andrew.apollo.tasks.LastfmGetAlbumImages;
-import com.andrew.apollo.tasks.ViewHolderTask;
+import com.andrew.apollo.cache.ImageFetcher;
+import com.andrew.apollo.model.Album;
+import com.andrew.apollo.ui.MusicHolder;
+import com.andrew.apollo.ui.fragments.profile.ArtistAlbumFragment;
import com.andrew.apollo.utils.ApolloUtils;
+import com.andrew.apollo.utils.Lists;
import com.andrew.apollo.utils.MusicUtils;
-import com.andrew.apollo.views.ViewHolderList;
-import com.androidquery.AQuery;
+
+import java.util.List;
/**
- * @author Andrew Neal
+ * This {@link ArrayAdapter} is used to display the albums for a particular
+ * artist for {@link ArtistAlbumFragment} .
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
*/
-public class ArtistAlbumAdapter extends SimpleCursorAdapter implements Constants {
+public class ArtistAlbumAdapter extends ArrayAdapter<Album> {
- private AnimationDrawable mPeakOneAnimation, mPeakTwoAnimation;
+ /**
+ * The header view
+ */
+ private static final int ITEM_VIEW_TYPE_HEADER = 0;
- private WeakReference<ViewHolderList> holderReference;
+ /**
+ * * The data in the list.
+ */
+ private static final int ITEM_VIEW_TYPE_MUSIC = 1;
- public ArtistAlbumAdapter(Context context, int layout, Cursor c, String[] from, int[] to,
- int flags) {
- super(context, layout, c, from, to, flags);
- }
+ /**
+ * Number of views (ImageView, TextView, header)
+ */
+ private static final int VIEW_TYPE_COUNT = 3;
/**
- * Used to quickly our the ContextMenu
+ * LayoutInflater
*/
- private final View.OnClickListener showContextMenu = new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- v.showContextMenu();
- }
- };
+ private final LayoutInflater mInflater;
- @Override
- public View getView(int position, View convertView, ViewGroup parent) {
- final View view = super.getView(position, convertView, parent);
- // ViewHolderList
- ViewHolderList viewholder;
+ /**
+ * Fake header
+ */
+ private final View mHeader;
+
+ /**
+ * The resource Id of the layout to inflate
+ */
+ private final int mLayoutId;
+
+ /**
+ * Image cache and image fetcher
+ */
+ private final ImageFetcher mImageFetcher;
- if (view != null) {
+ /**
+ * Used to set the size of the data in the adapter
+ */
+ private List<Album> mCount = Lists.newArrayList();
+
+ /**
+ * Constructor of <code>ArtistAlbumAdapter</code>
+ *
+ * @param context The {@link Context} to use
+ * @param layoutId The resource Id of the view to inflate.
+ */
+ public ArtistAlbumAdapter(final Context context, final int layoutId) {
+ super(context, 0);
+ // Used to create the custom layout
+ mInflater = LayoutInflater.from(context);
+ // Cache the header
+ mHeader = mInflater.inflate(R.layout.faux_carousel, null);
+ // Get the layout Id
+ mLayoutId = layoutId;
+ // Initialize the cache & image fetcher
+ mImageFetcher = ApolloUtils.getImageFetcher((SherlockFragmentActivity)context);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View getView(final int position, View convertView, final ViewGroup parent) {
- viewholder = new ViewHolderList(view);
- holderReference = new WeakReference<ViewHolderList>(viewholder);
- view.setTag(holderReference.get());
+ // Return a faux header at position 0
+ if (position == 0) {
+ return mHeader;
+ }
+ // Recycle MusicHolder's items
+ MusicHolder holder;
+ if (convertView == null) {
+ convertView = LayoutInflater.from(getContext()).inflate(mLayoutId, parent, false);
+ holder = new MusicHolder(convertView);
+ // Remove the background layer
+ holder.mOverlay.get().setBackgroundColor(0);
+ convertView.setTag(holder);
} else {
- viewholder = (ViewHolderList)convertView.getTag();
+ holder = (MusicHolder)convertView.getTag();
}
- // AQuery
- AQuery aq = new AQuery(view);
+ // Retrieve the album
+ final Album album = getItem(position - 1);
+ final String albumName = album.mAlbumName;
+
+ // Set each album name (line one)
+ holder.mLineOne.get().setText(albumName);
+ // Set the number of songs (line two)
+ holder.mLineTwo.get().setText(album.mSongNumber);
+ // Set the album year (line three)
+ holder.mLineThree.get().setText(album.mYear);
+ // Asynchronously load the album images into the adapter
+ mImageFetcher.loadAlbumImage(album.mArtistName, albumName, album.mAlbumId,
+ holder.mImage.get());
+ // Play the album when the artwork is touched
+ playAlbum(holder.mImage.get(), position);
+ return convertView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
- // Album name
- String albumName = mCursor.getString(ArtistAlbumsFragment.mAlbumNameIndex);
- holderReference.get().mViewHolderLineOne.setText(albumName);
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getCount() {
+ final int size = mCount.size();
+ return size == 0 ? 0 : size + 1;
+ }
- // Artist name
- String artistName = mCursor.getString(ArtistAlbumsFragment.mArtistNameIndex);
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public long getItemId(final int position) {
+ if (position == 0) {
+ return -1;
+ }
+ return position - 1;
+ }
- // Number of songs
- int songs_plural = mCursor.getInt(ArtistAlbumsFragment.mSongCountIndex);
- String numSongs = MusicUtils.makeAlbumsLabel(mContext, 0, songs_plural, true);
- holderReference.get().mViewHolderLineTwo.setText(numSongs);
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getViewTypeCount() {
+ return VIEW_TYPE_COUNT;
+ }
- // Match positions
- holderReference.get().position = position;
- if (aq.shouldDelay(position, view, parent, "")) {
- holderReference.get().mViewHolderImage.setImageDrawable(null);
- } else {
- // Check for missing album images and cache them
- if (ApolloUtils.getImageURL(albumName, ALBUM_IMAGE, mContext) == null) {
- new LastfmGetAlbumImages(mContext, null, 0).executeOnExecutor(
- AsyncTask.THREAD_POOL_EXECUTOR, artistName, albumName);
- } else {
- new ViewHolderTask(holderReference.get(), null, position, mContext, 1, 0,
- holderReference.get().mViewHolderImage).executeOnExecutor(
- AsyncTask.THREAD_POOL_EXECUTOR, albumName);
- }
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getItemViewType(final int position) {
+ if (position == 0) {
+ return ITEM_VIEW_TYPE_HEADER;
}
+ return ITEM_VIEW_TYPE_MUSIC;
+ }
- holderReference.get().mQuickContext.setOnClickListener(showContextMenu);
-
- // Now playing indicator
- long currentalbumid = MusicUtils.getCurrentAlbumId();
- long albumid = mCursor.getLong(ArtistAlbumsFragment.mAlbumIdIndex);
- if (currentalbumid == albumid) {
- holderReference.get().mPeakOne.setImageResource(R.anim.peak_meter_1);
- holderReference.get().mPeakTwo.setImageResource(R.anim.peak_meter_2);
- mPeakOneAnimation = (AnimationDrawable)holderReference.get().mPeakOne.getDrawable();
- mPeakTwoAnimation = (AnimationDrawable)holderReference.get().mPeakTwo.getDrawable();
- try {
- if (MusicUtils.mService.isPlaying()) {
- mPeakOneAnimation.start();
- mPeakTwoAnimation.start();
- } else {
- mPeakOneAnimation.stop();
- mPeakTwoAnimation.stop();
+ /**
+ * Starts playing an album if the user touches the artwork in the list.
+ *
+ * @param album The {@link ImageView} holding the album
+ * @param position The position of the album to play.
+ */
+ private void playAlbum(final ImageView album, final int position) {
+ // Prevent accidental touches
+ if (!mImageFetcher.isScrolling()) {
+ album.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(final View v) {
+ final String id = getItem(position - 1).mAlbumId;
+ final long[] list = MusicUtils.getSongListForAlbum(getContext(), id);
+ MusicUtils.playAll(getContext(), list, 0, false);
}
- } catch (RemoteException e) {
- e.printStackTrace();
- }
- } else {
- holderReference.get().mPeakOne.setImageResource(0);
- holderReference.get().mPeakTwo.setImageResource(0);
+ });
+ }
+ }
+
+ /**
+ * Method that unloads and clears the items in the adapter
+ */
+ public void unload() {
+ clear();
+ }
+
+ /**
+ * @param pause True to temporarily pause the disk cache, false otherwise.
+ */
+ public void setPauseDiskCache(final boolean pause) {
+ if (mImageFetcher != null) {
+ mImageFetcher.setPauseDiskCache(pause);
+ }
+ }
+
+ /**
+ * @param album The key used to find the cached album to remove
+ */
+ public void removeFromCache(final Album album) {
+ if (mImageFetcher != null) {
+ mImageFetcher.removeFromCache(album.mAlbumName + Config.ALBUM_ART_SUFFIX);
}
- return view;
+ }
+
+ /**
+ * @param data The {@link List} used to return the count for the adapter.
+ */
+ public void setCount(final List<Album> data) {
+ mCount = data;
+ }
+
+ /**
+ * Flushes the disk cache.
+ */
+ public void flush() {
+ mImageFetcher.flush();
}
}
diff --git a/src/com/andrew/apollo/adapters/GenreAdapter.java b/src/com/andrew/apollo/adapters/GenreAdapter.java
index ce64a28..137c772 100644
--- a/src/com/andrew/apollo/adapters/GenreAdapter.java
+++ b/src/com/andrew/apollo/adapters/GenreAdapter.java
@@ -1,73 +1,135 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.adapters;
-import java.lang.ref.WeakReference;
-
import android.content.Context;
-import android.database.Cursor;
-import android.support.v4.widget.SimpleCursorAdapter;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
-import com.andrew.apollo.Constants;
import com.andrew.apollo.R;
-import com.andrew.apollo.list.fragments.GenresFragment;
-import com.andrew.apollo.utils.MusicUtils;
-import com.andrew.apollo.views.ViewHolderList;
+import com.andrew.apollo.model.Genre;
+import com.andrew.apollo.ui.MusicHolder;
+import com.andrew.apollo.ui.MusicHolder.DataHolder;
+import com.andrew.apollo.ui.fragments.GenreFragment;
/**
- * @author Andrew Neal
+ * This {@link ArrayAdapter} is used to display all of the genres on a user's
+ * device for {@link GenreFragment} .
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
*/
-public class GenreAdapter extends SimpleCursorAdapter implements Constants {
+public class GenreAdapter extends ArrayAdapter<Genre> {
- private WeakReference<ViewHolderList> holderReference;
+ /**
+ * Number of views (TextView)
+ */
+ private static final int VIEW_TYPE_COUNT = 1;
- private final int left;
+ /**
+ * The resource Id of the layout to inflate
+ */
+ private final int mLayoutId;
- public GenreAdapter(Context context, int layout, Cursor c, String[] from, int[] to, int flags) {
- super(context, layout, c, from, to, flags);
- // Helps center the text in the Genres tab
- left = mContext.getResources().getDimensionPixelSize(
- R.dimen.listview_items_padding_left_top);
- }
+ /**
+ * Used to cache the genre info
+ */
+ private DataHolder[] mData;
/**
- * Used to quickly our the ContextMenu
+ * Constructor of <code>GenreAdapter</code>
+ *
+ * @param context The {@link Context} to use.
+ * @param layoutId The resource Id of the view to inflate.
*/
- private final View.OnClickListener showContextMenu = new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- v.showContextMenu();
- }
- };
+ public GenreAdapter(final Context context, final int layoutId) {
+ super(context, 0);
+ // Get the layout Id
+ mLayoutId = layoutId;
+ }
+ /**
+ * {@inheritDoc}
+ */
@Override
- public View getView(final int position, View convertView, ViewGroup parent) {
- final View view = super.getView(position, convertView, parent);
- // ViewHolderList
- final ViewHolderList viewholder;
-
- if (view != null) {
-
- viewholder = new ViewHolderList(view);
- holderReference = new WeakReference<ViewHolderList>(viewholder);
- view.setTag(holderReference.get());
-
+ public View getView(final int position, View convertView, final ViewGroup parent) {
+ // Recycle ViewHolder's items
+ MusicHolder holder;
+ if (convertView == null) {
+ convertView = LayoutInflater.from(getContext()).inflate(mLayoutId, parent, false);
+ holder = new MusicHolder(convertView);
+ // Hide the second and third lines of text
+ holder.mLineTwo.get().setVisibility(View.GONE);
+ holder.mLineThree.get().setVisibility(View.GONE);
+ // Make line one slightly larger
+ holder.mLineOne.get().setTextSize(TypedValue.COMPLEX_UNIT_PX,
+ getContext().getResources().getDimension(R.dimen.text_size_large));
+ convertView.setTag(holder);
} else {
- viewholder = (ViewHolderList)convertView.getTag();
+ holder = (MusicHolder)convertView.getTag();
}
- // Genre name
- String genreName = mCursor.getString(GenresFragment.mGenreNameIndex);
- holderReference.get().mViewHolderLineOne.setText(MusicUtils.parseGenreName(mContext,
- genreName));
+ // Retrieve the data holder
+ final DataHolder dataHolder = mData[position];
+
+ // Set each genre name (line one)
+ holder.mLineOne.get().setText(dataHolder.mLineOne);
+ return convertView;
+ }
- holderReference.get().mViewHolderLineOne.setPadding(left, 40, 0, 0);
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
- holderReference.get().mViewHolderImage.setVisibility(View.GONE);
- holderReference.get().mViewHolderLineTwo.setVisibility(View.GONE);
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getViewTypeCount() {
+ return VIEW_TYPE_COUNT;
+ }
- holderReference.get().mQuickContext.setOnClickListener(showContextMenu);
- return view;
+ /**
+ * Method used to cache the data used to populate the list or grid. The idea
+ * is to cache everything before {@code #getView(int, View, ViewGroup)} is
+ * called.
+ */
+ public void buildCache() {
+ mData = new DataHolder[getCount()];
+ for (int i = 0; i < getCount(); i++) {
+ // Build the artist
+ final Genre genre = getItem(i);
+
+ // Build the data holder
+ mData[i] = new DataHolder();
+ // Genre Id
+ mData[i].mItemId = genre.mGenreId;
+ // Genre names (line one)
+ mData[i].mLineOne = genre.mGenreName;
+ }
+ }
+
+ /**
+ * Method that unloads and clears the items in the adapter
+ */
+ public void unload() {
+ clear();
+ mData = null;
}
+
}
diff --git a/src/com/andrew/apollo/adapters/PagerAdapter.java b/src/com/andrew/apollo/adapters/PagerAdapter.java
index 263a875..7ddce67 100644
--- a/src/com/andrew/apollo/adapters/PagerAdapter.java
+++ b/src/com/andrew/apollo/adapters/PagerAdapter.java
@@ -1,39 +1,224 @@
-/**
- *
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.adapters;
-import java.util.ArrayList;
-
+import android.os.Bundle;
import android.support.v4.app.Fragment;
-import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
+import android.util.SparseArray;
+import android.view.ViewGroup;
+
+import com.actionbarsherlock.app.SherlockFragment;
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.andrew.apollo.R;
+import com.andrew.apollo.ui.fragments.AlbumFragment;
+import com.andrew.apollo.ui.fragments.ArtistFragment;
+import com.andrew.apollo.ui.fragments.GenreFragment;
+import com.andrew.apollo.ui.fragments.PlaylistFragment;
+import com.andrew.apollo.ui.fragments.RecentFragment;
+import com.andrew.apollo.ui.fragments.SongFragment;
+import com.andrew.apollo.utils.Lists;
+
+import java.lang.ref.WeakReference;
+import java.util.List;
+import java.util.Locale;
/**
- * @author Andrew Neal
+ * A {@link FragmentPagerAdapter} class for swiping between playlists, recent,
+ * artists, albums, songs, and genre {@link SherlockFragment}s on phones.<br/>
*/
public class PagerAdapter extends FragmentPagerAdapter {
- private final ArrayList<Fragment> mFragments = new ArrayList<Fragment>();
+ private final SparseArray<WeakReference<Fragment>> mFragmentArray = new SparseArray<WeakReference<Fragment>>();
- public PagerAdapter(FragmentManager manager) {
- super(manager);
+ private final List<Holder> mHolderList = Lists.newArrayList();
+
+ private final SherlockFragmentActivity mFragmentActivity;
+
+ private int mCurrentPage;
+
+ /**
+ * Constructor of <code>PagerAdatper<code>
+ *
+ * @param fragmentActivity The {@link SherlockFragmentActivity} of the
+ * {@link SherlockFragment}.
+ */
+ public PagerAdapter(final SherlockFragmentActivity fragmentActivity) {
+ super(fragmentActivity.getSupportFragmentManager());
+ mFragmentActivity = fragmentActivity;
}
- public void addFragment(Fragment fragment) {
- mFragments.add(fragment);
+ /**
+ * Method that adds a new fragment class to the viewer (the fragment is
+ * internally instantiate)
+ *
+ * @param className The full qualified name of fragment class.
+ * @param params The instantiate params.
+ */
+ @SuppressWarnings("synthetic-access")
+ public void add(final Class<? extends Fragment> className, final Bundle params) {
+ final Holder mHolder = new Holder();
+ mHolder.mClassName = className.getName();
+ mHolder.mParams = params;
+
+ final int mPosition = mHolderList.size();
+ mHolderList.add(mPosition, mHolder);
notifyDataSetChanged();
}
+ /**
+ * Method that returns the {@link SherlockFragment} in the argument
+ * position.
+ *
+ * @param position The position of the fragment to return.
+ * @return Fragment The {@link SherlockFragment} in the argument position.
+ */
+ public Fragment getFragment(final int position) {
+ final WeakReference<Fragment> mWeakFragment = mFragmentArray.get(position);
+ if (mWeakFragment != null && mWeakFragment.get() != null) {
+ return mWeakFragment.get();
+ }
+ return getItem(position);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Object instantiateItem(final ViewGroup container, final int position) {
+ final Fragment mFragment = (Fragment)super.instantiateItem(container, position);
+ final WeakReference<Fragment> mWeakFragment = mFragmentArray.get(position);
+ if (mWeakFragment != null) {
+ mWeakFragment.clear();
+ }
+ mFragmentArray.put(position, new WeakReference<Fragment>(mFragment));
+ return mFragment;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Fragment getItem(final int position) {
+ final Holder mCurrentHolder = mHolderList.get(position);
+ final Fragment mFragment = Fragment.instantiate(mFragmentActivity,
+ mCurrentHolder.mClassName, mCurrentHolder.mParams);
+ return mFragment;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void destroyItem(final ViewGroup container, final int position, final Object object) {
+ super.destroyItem(container, position, object);
+ final WeakReference<Fragment> mWeakFragment = mFragmentArray.get(position);
+ if (mWeakFragment != null) {
+ mWeakFragment.clear();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
@Override
public int getCount() {
- return mFragments.size();
+ return mHolderList.size();
}
+ /**
+ * {@inheritDoc}
+ */
@Override
- public Fragment getItem(int position) {
- return mFragments.get(position);
+ public CharSequence getPageTitle(final int position) {
+ return mFragmentActivity.getResources().getStringArray(R.array.page_titles)[position]
+ .toUpperCase(Locale.getDefault());
+ }
+
+ /**
+ * Method that returns the current page position.
+ *
+ * @return int The current page.
+ */
+ public int getCurrentPage() {
+ return mCurrentPage;
+ }
+
+ /**
+ * Method that sets the current page position.
+ *
+ * @param currentPage The current page.
+ */
+ protected void setCurrentPage(final int currentPage) {
+ mCurrentPage = currentPage;
}
+ /**
+ * An enumeration of all the main fragments supported.
+ */
+ public enum MusicFragments {
+ /**
+ * The playlist fragment
+ */
+ PLAYLIST(PlaylistFragment.class),
+ /**
+ * The recent fragment
+ */
+ RECENT(RecentFragment.class),
+ /**
+ * The artist fragment
+ */
+ ARTIST(ArtistFragment.class),
+ /**
+ * The album fragment
+ */
+ ALBUM(AlbumFragment.class),
+ /**
+ * The song fragment
+ */
+ SONG(SongFragment.class),
+ /**
+ * The genre fragment
+ */
+ GENRE(GenreFragment.class);
+
+ private Class<? extends Fragment> mFragmentClass;
+
+ /**
+ * Constructor of <code>MusicFragments</code>
+ *
+ * @param fragmentClass The fragment class
+ */
+ private MusicFragments(final Class<? extends Fragment> fragmentClass) {
+ mFragmentClass = fragmentClass;
+ }
+
+ /**
+ * Method that returns the fragment class.
+ *
+ * @return Class<? extends Fragment> The fragment class.
+ */
+ public Class<? extends Fragment> getFragmentClass() {
+ return mFragmentClass;
+ }
+
+ }
+
+ /**
+ * A private class with information about fragment initialization
+ */
+ private final static class Holder {
+ String mClassName;
+
+ Bundle mParams;
+ }
}
diff --git a/src/com/andrew/apollo/adapters/PlaylistAdapter.java b/src/com/andrew/apollo/adapters/PlaylistAdapter.java
index bddddda..bbd168a 100644
--- a/src/com/andrew/apollo/adapters/PlaylistAdapter.java
+++ b/src/com/andrew/apollo/adapters/PlaylistAdapter.java
@@ -1,70 +1,135 @@
-/**
- *
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.adapters;
-import java.lang.ref.WeakReference;
-
import android.content.Context;
-import android.database.Cursor;
-import android.support.v4.widget.SimpleCursorAdapter;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
import com.andrew.apollo.R;
-import com.andrew.apollo.list.fragments.PlaylistsFragment;
-import com.andrew.apollo.views.ViewHolderList;
+import com.andrew.apollo.model.Playlist;
+import com.andrew.apollo.ui.MusicHolder;
+import com.andrew.apollo.ui.MusicHolder.DataHolder;
+import com.andrew.apollo.ui.fragments.PlaylistFragment;
/**
- * @author Andrew Neal
+ * This {@link ArrayAdapter} is used to display all of the playlists on a user's
+ * device for {@link PlaylistFragment}.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
*/
-public class PlaylistAdapter extends SimpleCursorAdapter {
-
- private WeakReference<ViewHolderList> holderReference;
-
- public PlaylistAdapter(Context context, int layout, Cursor c, String[] from, int[] to, int flags) {
- super(context, layout, c, from, to, flags);
- }
+public class PlaylistAdapter extends ArrayAdapter<Playlist> {
/**
- * Used to quickly our the ContextMenu
+ * Number of views (TextView)
*/
- private final View.OnClickListener showContextMenu = new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- v.showContextMenu();
- }
- };
+ private static final int VIEW_TYPE_COUNT = 1;
- @Override
- public View getView(final int position, View convertView, ViewGroup parent) {
- final View view = super.getView(position, convertView, parent);
- // ViewHolderList
- final ViewHolderList viewholder;
+ /**
+ * The resource Id of the layout to inflate
+ */
+ private final int mLayoutId;
- if (view != null) {
+ /**
+ * Used to cache the playlist info
+ */
+ private DataHolder[] mData;
- viewholder = new ViewHolderList(view);
- holderReference = new WeakReference<ViewHolderList>(viewholder);
- view.setTag(holderReference.get());
+ /**
+ * Constructor of <code>PlaylistAdapter</code>
+ *
+ * @param context The {@link Context} to use.
+ * @param layoutId The resource Id of the view to inflate.
+ */
+ public PlaylistAdapter(final Context context, final int layoutId) {
+ super(context, 0);
+ // Get the layout Id
+ mLayoutId = layoutId;
+ }
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View getView(final int position, View convertView, final ViewGroup parent) {
+ // Recycle ViewHolder's items
+ MusicHolder holder;
+ if (convertView == null) {
+ convertView = LayoutInflater.from(getContext()).inflate(mLayoutId, parent, false);
+ holder = new MusicHolder(convertView);
+ // Hide the second and third lines of text
+ holder.mLineTwo.get().setVisibility(View.GONE);
+ holder.mLineThree.get().setVisibility(View.GONE);
+ // Make line one slightly larger
+ holder.mLineOne.get().setTextSize(TypedValue.COMPLEX_UNIT_PX,
+ getContext().getResources().getDimension(R.dimen.text_size_large));
+ convertView.setTag(holder);
} else {
- viewholder = (ViewHolderList)convertView.getTag();
+ holder = (MusicHolder)convertView.getTag();
}
- String playlist_name = mCursor.getString(PlaylistsFragment.mPlaylistNameIndex);
- holderReference.get().mViewHolderLineOne.setText(playlist_name);
+ // Retrieve the data holder
+ final DataHolder dataHolder = mData[position];
+
+ // Set each playlist name (line one)
+ holder.mLineOne.get().setText(dataHolder.mLineOne);
+ return convertView;
+ }
- // Helps center the text in the Playlist tab
- int left = mContext.getResources().getDimensionPixelSize(
- R.dimen.listview_items_padding_left_top);
- holderReference.get().mViewHolderLineOne.setPadding(left, 40, 0, 0);
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getViewTypeCount() {
+ return VIEW_TYPE_COUNT;
+ }
- holderReference.get().mViewHolderImage.setVisibility(View.GONE);
+ /**
+ * Method used to cache the data used to populate the list or grid. The idea
+ * is to cache everything before {@code #getView(int, View, ViewGroup)} is
+ * called.
+ */
+ public void buildCache() {
+ mData = new DataHolder[getCount()];
+ for (int i = 0; i < getCount(); i++) {
+ // Build the artist
+ final Playlist playlist = getItem(i);
+
+ // Build the data holder
+ mData[i] = new DataHolder();
+ // Playlist Id
+ mData[i].mItemId = playlist.mPlaylistId;
+ // Playlist names (line one)
+ mData[i].mLineOne = playlist.mPlaylistName;
+ }
+ }
- holderReference.get().mQuickContext.setOnClickListener(showContextMenu);
- return view;
+ /**
+ * Method that unloads and clears the items in the adapter
+ */
+ public void unload() {
+ clear();
+ mData = null;
}
}
diff --git a/src/com/andrew/apollo/adapters/ProfileSongAdapter.java b/src/com/andrew/apollo/adapters/ProfileSongAdapter.java
new file mode 100644
index 0000000..3e2856b
--- /dev/null
+++ b/src/com/andrew/apollo/adapters/ProfileSongAdapter.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.adapters;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+
+import com.andrew.apollo.R;
+import com.andrew.apollo.model.Song;
+import com.andrew.apollo.ui.MusicHolder;
+import com.andrew.apollo.ui.fragments.profile.AlbumSongFragment;
+import com.andrew.apollo.ui.fragments.profile.ArtistSongFragment;
+import com.andrew.apollo.ui.fragments.profile.FavoriteFragment;
+import com.andrew.apollo.ui.fragments.profile.GenreSongFragment;
+import com.andrew.apollo.ui.fragments.profile.LastAddedFragment;
+import com.andrew.apollo.ui.fragments.profile.PlaylistSongFragment;
+import com.andrew.apollo.utils.Lists;
+
+import java.util.List;
+
+/**
+ * This {@link ArrayAdapter} is used to display the songs for a particular
+ * artist, album, playlist, or genre for {@link ArtistSongFragment},
+ * {@link AlbumSongFragment},{@link PlaylistSongFragment},
+ * {@link GenreSongFragment},{@link FavoriteFragment},{@link LastAddedFragment}.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class ProfileSongAdapter extends ArrayAdapter<Song> {
+
+ /**
+ * The header view
+ */
+ private static final int ITEM_VIEW_TYPE_HEADER = 0;
+
+ /**
+ * * The data in the list.
+ */
+ private static final int ITEM_VIEW_TYPE_MUSIC = 1;
+
+ /**
+ * Number of views (ImageView, TextView, header)
+ */
+ private static final int VIEW_TYPE_COUNT = 3;
+
+ /**
+ * LayoutInflater
+ */
+ private final LayoutInflater mInflater;
+
+ /**
+ * Fake header
+ */
+ private final View mHeader;
+
+ /**
+ * The resource Id of the layout to inflate
+ */
+ private final int mLayoutId;
+
+ /**
+ * In {@link AlbumSongFragment} that duration is shown on line two, this
+ * makes that happen while showing the album name on all others
+ */
+ private final boolean mShowDuration;
+
+ /**
+ * Used to set the size of the data in the adapter
+ */
+ private List<Song> mCount = Lists.newArrayList();
+
+ /**
+ * Constructor of <code>ProfileSongAdapter</code>
+ *
+ * @param context The {@link Context} to use
+ * @param layoutId The resource Id of the view to inflate.
+ * @param yesToDuration True to show the duration of a track on line two,
+ * false otherwise
+ */
+ public ProfileSongAdapter(final Context context, final int layoutId, final boolean yesToDuration) {
+ super(context, 0);
+ // Used to create the custom layout
+ mInflater = LayoutInflater.from(context);
+ // Cache the header
+ mHeader = mInflater.inflate(R.layout.faux_carousel, null);
+ // Get the layout Id
+ mLayoutId = layoutId;
+ // Know what to put in line two
+ mShowDuration = yesToDuration;
+ }
+
+ /**
+ * Constructor of <code>ProfileSongAdapter</code>
+ *
+ * @param context The {@link Context} to use
+ * @param layoutId The resource Id of the view to inflate.
+ */
+ public ProfileSongAdapter(final Context context, final int layoutId) {
+ super(context, 0);
+ // Used to create the custom layout
+ mInflater = LayoutInflater.from(context);
+ // Cache the header
+ mHeader = mInflater.inflate(R.layout.faux_carousel, null);
+ // Get the layout Id
+ mLayoutId = layoutId;
+ // Know what to put in line two
+ mShowDuration = false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View getView(final int position, View convertView, final ViewGroup parent) {
+
+ // Return a faux header at position 0
+ if (position == 0) {
+ return mHeader;
+ }
+
+ // Recycle MusicHolder's items
+ MusicHolder holder;
+ if (convertView == null) {
+ convertView = LayoutInflater.from(getContext()).inflate(mLayoutId, parent, false);
+ holder = new MusicHolder(convertView);
+ // Hide the third line of text
+ holder.mLineThree.get().setVisibility(View.GONE);
+ convertView.setTag(holder);
+ } else {
+ holder = (MusicHolder)convertView.getTag();
+ }
+
+ // Retrieve the album
+ final Song song = getItem(position - 1);
+
+ // Set each track name (line one)
+ holder.mLineOne.get().setText(song.mSongName);
+ // Set the duration or album name (line two)
+ holder.mLineTwo.get().setText(mShowDuration ? song.mDuration : song.mAlbumName);
+ return convertView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getCount() {
+ final int size = mCount.size();
+ return size == 0 ? 0 : size + 1;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public long getItemId(final int position) {
+ if (position == 0) {
+ return -1;
+ }
+ return position - 1;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getViewTypeCount() {
+ return VIEW_TYPE_COUNT;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getItemViewType(final int position) {
+ if (position == 0) {
+ return ITEM_VIEW_TYPE_HEADER;
+ }
+ return ITEM_VIEW_TYPE_MUSIC;
+ }
+
+ /**
+ * Method that unloads and clears the items in the adapter
+ */
+ public void unload() {
+ clear();
+ }
+
+ /**
+ * @param data The {@link List} used to return the count for the adapter.
+ */
+ public void setCount(final List<Song> data) {
+ mCount = data;
+ }
+
+}
diff --git a/src/com/andrew/apollo/adapters/QuickQueueAdapter.java b/src/com/andrew/apollo/adapters/QuickQueueAdapter.java
deleted file mode 100644
index 416815e..0000000
--- a/src/com/andrew/apollo/adapters/QuickQueueAdapter.java
+++ /dev/null
@@ -1,95 +0,0 @@
-
-package com.andrew.apollo.adapters;
-
-import java.lang.ref.WeakReference;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.os.AsyncTask;
-import android.support.v4.widget.SimpleCursorAdapter;
-import android.view.View;
-import android.view.ViewGroup;
-
-import com.andrew.apollo.Constants;
-import com.andrew.apollo.grid.fragments.QuickQueueFragment;
-import com.andrew.apollo.tasks.LastfmGetAlbumImages;
-import com.andrew.apollo.tasks.LastfmGetArtistImages;
-import com.andrew.apollo.tasks.ViewHolderQueueTask;
-import com.andrew.apollo.utils.ApolloUtils;
-import com.andrew.apollo.views.ViewHolderQueue;
-import com.androidquery.AQuery;
-
-/**
- * @author Andrew Neal
- */
-public class QuickQueueAdapter extends SimpleCursorAdapter implements Constants {
-
- private WeakReference<ViewHolderQueue> holderReference;
-
- public QuickQueueAdapter(Context context, int layout, Cursor c, String[] from, int[] to,
- int flags) {
- super(context, layout, c, from, to, flags);
- }
-
- @Override
- public View getView(final int position, View convertView, ViewGroup parent) {
- final View view = super.getView(position, convertView, parent);
- // ViewHolderQueue
- final ViewHolderQueue viewholder;
-
- if (view != null) {
-
- viewholder = new ViewHolderQueue(view);
- holderReference = new WeakReference<ViewHolderQueue>(viewholder);
- view.setTag(holderReference.get());
-
- } else {
- viewholder = (ViewHolderQueue)convertView.getTag();
- }
-
- // AQuery
- final AQuery aq = new AQuery(view);
-
- // Artist Name
- String artistName = mCursor.getString(QuickQueueFragment.mArtistIndex);
-
- // Album name
- String albumName = mCursor.getString(QuickQueueFragment.mAlbumIndex);
-
- // Track name
- String trackName = mCursor.getString(QuickQueueFragment.mTitleIndex);
- holderReference.get().mTrackName.setText(trackName);
-
- holderReference.get().position = position;
- // Artist Image
- if (aq.shouldDelay(position, view, parent, "")) {
- holderReference.get().mArtistImage.setImageDrawable(null);
- } else {
- // Check for missing artist images and cache them
- if (ApolloUtils.getImageURL(artistName, ARTIST_IMAGE, mContext) == null) {
- new LastfmGetArtistImages(mContext).executeOnExecutor(
- AsyncTask.THREAD_POOL_EXECUTOR, artistName);
- } else {
- new ViewHolderQueueTask(holderReference.get(), position, mContext, 0, 0,
- holderReference.get().mArtistImage).executeOnExecutor(
- AsyncTask.THREAD_POOL_EXECUTOR, artistName);
- }
- }
-
- // Album Image
- if (aq.shouldDelay(position, view, parent, "")) {
- holderReference.get().mAlbumArt.setImageDrawable(null);
- } else {
- // Check for missing album images and cache them
- if (ApolloUtils.getImageURL(albumName, ALBUM_IMAGE, mContext) == null) {
- new LastfmGetAlbumImages(mContext, null, 0).executeOnExecutor(
- AsyncTask.THREAD_POOL_EXECUTOR, artistName, albumName);
- } else {
- new ViewHolderQueueTask(holderReference.get(), position, mContext, 1, 1,
- holderReference.get().mAlbumArt).executeOnExecutor(
- AsyncTask.THREAD_POOL_EXECUTOR, albumName);
- }
- }
- return view;
- }
-}
diff --git a/src/com/andrew/apollo/adapters/RecentlyAddedAdapter.java b/src/com/andrew/apollo/adapters/RecentlyAddedAdapter.java
deleted file mode 100644
index 665fa53..0000000
--- a/src/com/andrew/apollo/adapters/RecentlyAddedAdapter.java
+++ /dev/null
@@ -1,111 +0,0 @@
-
-package com.andrew.apollo.adapters;
-
-import java.lang.ref.WeakReference;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.graphics.drawable.AnimationDrawable;
-import android.os.AsyncTask;
-import android.os.RemoteException;
-import android.support.v4.widget.SimpleCursorAdapter;
-import android.view.View;
-import android.view.ViewGroup;
-
-import com.andrew.apollo.Constants;
-import com.andrew.apollo.R;
-import com.andrew.apollo.list.fragments.RecentlyAddedFragment;
-import com.andrew.apollo.tasks.LastfmGetAlbumImages;
-import com.andrew.apollo.tasks.ViewHolderTask;
-import com.andrew.apollo.utils.ApolloUtils;
-import com.andrew.apollo.utils.MusicUtils;
-import com.andrew.apollo.views.ViewHolderList;
-import com.androidquery.AQuery;
-
-/**
- * @author Andrew Neal
- */
-public class RecentlyAddedAdapter extends SimpleCursorAdapter implements Constants {
-
- private AnimationDrawable mPeakOneAnimation, mPeakTwoAnimation;
-
- private WeakReference<ViewHolderList> holderReference;
-
- public RecentlyAddedAdapter(Context context, int layout, Cursor c, String[] from, int[] to,
- int flags) {
- super(context, layout, c, from, to, flags);
- }
-
- @Override
- public View getView(int position, View convertView, ViewGroup parent) {
- final View view = super.getView(position, convertView, parent);
- // ViewHolderList
- ViewHolderList viewholder;
-
- if (view != null) {
-
- viewholder = new ViewHolderList(view);
- holderReference = new WeakReference<ViewHolderList>(viewholder);
- view.setTag(holderReference.get());
-
- } else {
- viewholder = (ViewHolderList)convertView.getTag();
- }
- // AQuery
- final AQuery aq = new AQuery(convertView);
-
- // Track name
- String trackName = mCursor.getString(RecentlyAddedFragment.mTitleIndex);
- holderReference.get().mViewHolderLineOne.setText(trackName);
-
- // Artist name
- String artistName = mCursor.getString(RecentlyAddedFragment.mArtistIndex);
- holderReference.get().mViewHolderLineTwo.setText(artistName);
-
- // Album name
- String albumName = mCursor.getString(RecentlyAddedFragment.mAlbumIndex);
-
- // Match positions
- holderReference.get().position = position;
- if (aq.shouldDelay(position, view, parent, "")) {
- holderReference.get().mViewHolderImage.setImageDrawable(null);
- } else {
- // Check for missing artwork and cache then cache it
- if (ApolloUtils.getImageURL(albumName, ALBUM_IMAGE, mContext) == null) {
- new LastfmGetAlbumImages(mContext, null, 0).executeOnExecutor(
- AsyncTask.THREAD_POOL_EXECUTOR, artistName, albumName);
- } else {
- new ViewHolderTask(holderReference.get(), null, position, mContext, 1, 0,
- holderReference.get().mViewHolderImage).executeOnExecutor(
- AsyncTask.THREAD_POOL_EXECUTOR, albumName);
- }
- }
-
- holderReference.get().mQuickContext.setVisibility(View.GONE);
-
- // Now playing indicator
- long currentaudioid = MusicUtils.getCurrentAudioId();
- long audioid = mCursor.getLong(RecentlyAddedFragment.mMediaIdIndex);
- if (currentaudioid == audioid) {
- holderReference.get().mPeakOne.setImageResource(R.anim.peak_meter_1);
- holderReference.get().mPeakTwo.setImageResource(R.anim.peak_meter_2);
- mPeakOneAnimation = (AnimationDrawable)holderReference.get().mPeakOne.getDrawable();
- mPeakTwoAnimation = (AnimationDrawable)holderReference.get().mPeakTwo.getDrawable();
- try {
- if (MusicUtils.mService.isPlaying()) {
- mPeakOneAnimation.start();
- mPeakTwoAnimation.start();
- } else {
- mPeakOneAnimation.stop();
- mPeakTwoAnimation.stop();
- }
- } catch (RemoteException e) {
- e.printStackTrace();
- }
- } else {
- holderReference.get().mPeakOne.setImageResource(0);
- holderReference.get().mPeakTwo.setImageResource(0);
- }
- return view;
- }
-}
diff --git a/src/com/andrew/apollo/adapters/ScrollingTabsAdapter.java b/src/com/andrew/apollo/adapters/ScrollingTabsAdapter.java
deleted file mode 100644
index 8ed4239..0000000
--- a/src/com/andrew/apollo/adapters/ScrollingTabsAdapter.java
+++ /dev/null
@@ -1,34 +0,0 @@
-
-package com.andrew.apollo.adapters;
-
-import android.support.v4.app.FragmentActivity;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.Button;
-
-import com.andrew.apollo.Constants;
-import com.andrew.apollo.R;
-import com.andrew.apollo.utils.ThemeUtils;
-
-public class ScrollingTabsAdapter implements TabAdapter, Constants {
-
- private final FragmentActivity activity;
-
- public ScrollingTabsAdapter(FragmentActivity act) {
- activity = act;
- }
-
- @Override
- public View getView(int position) {
-
- LayoutInflater inflater = activity.getLayoutInflater();
- final Button tab = (Button)inflater.inflate(R.layout.tabs, null);
-
- if (position < mTitles.length)
- tab.setText(mTitles[position]);
-
- // Theme chooser
- ThemeUtils.setTextColor(activity, tab, "tab_text_color");
- return tab;
- }
-}
diff --git a/src/com/andrew/apollo/adapters/SongAdapter.java b/src/com/andrew/apollo/adapters/SongAdapter.java
new file mode 100644
index 0000000..c1f58e0
--- /dev/null
+++ b/src/com/andrew/apollo/adapters/SongAdapter.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.adapters;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+
+import com.andrew.apollo.model.Song;
+import com.andrew.apollo.ui.MusicHolder;
+import com.andrew.apollo.ui.MusicHolder.DataHolder;
+import com.andrew.apollo.ui.fragments.QueueFragment;
+import com.andrew.apollo.ui.fragments.SongFragment;
+
+/**
+ * This {@link ArrayAdapter} is used to display all of the songs on a user's
+ * device for {@link SongFragment}. It is also used to show the queue in
+ * {@link QueueFragment}.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class SongAdapter extends ArrayAdapter<Song> {
+
+ /**
+ * Number of views (TextView)
+ */
+ private static final int VIEW_TYPE_COUNT = 1;
+
+ /**
+ * The resource Id of the layout to inflate
+ */
+ private final int mLayoutId;
+
+ /**
+ * Used to cache the song info
+ */
+ private DataHolder[] mData;
+
+ /**
+ * Constructor of <code>SongAdapter</code>
+ *
+ * @param context The {@link Context} to use.
+ * @param layoutId The resource Id of the view to inflate.
+ */
+ public SongAdapter(final Context context, final int layoutId) {
+ super(context, 0);
+ // Get the layout Id
+ mLayoutId = layoutId;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View getView(final int position, View convertView, final ViewGroup parent) {
+ // Recycle ViewHolder's items
+ MusicHolder holder;
+ if (convertView == null) {
+ convertView = LayoutInflater.from(getContext()).inflate(mLayoutId, parent, false);
+ holder = new MusicHolder(convertView);
+ // Hide the third line of text
+ holder.mLineThree.get().setVisibility(View.GONE);
+ convertView.setTag(holder);
+ } else {
+ holder = (MusicHolder)convertView.getTag();
+ }
+
+ // Retrieve the data holder
+ final DataHolder dataHolder = mData[position];
+
+ // Set each song name (line one)
+ holder.mLineOne.get().setText(dataHolder.mLineOne);
+ // Set the album name (line two)
+ holder.mLineTwo.get().setText(dataHolder.mLineTwo);
+ return convertView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getViewTypeCount() {
+ return VIEW_TYPE_COUNT;
+ }
+
+ /**
+ * Method used to cache the data used to populate the list or grid. The idea
+ * is to cache everything before {@code #getView(int, View, ViewGroup)} is
+ * called.
+ */
+ public void buildCache() {
+ mData = new DataHolder[getCount()];
+ for (int i = 0; i < getCount(); i++) {
+ // Build the song
+ final Song song = getItem(i);
+
+ // Build the data holder
+ mData[i] = new DataHolder();
+ // Song Id
+ mData[i].mItemId = song.mSongId;
+ // Song names (line one)
+ mData[i].mLineOne = song.mSongName;
+ // Album names (line two)
+ mData[i].mLineTwo = song.mAlbumName;
+ }
+ }
+
+ /**
+ * Method that unloads and clears the items in the adapter
+ */
+ public void unload() {
+ clear();
+ mData = null;
+ }
+
+}
diff --git a/src/com/andrew/apollo/adapters/TabAdapter.java b/src/com/andrew/apollo/adapters/TabAdapter.java
deleted file mode 100644
index e24e6f4..0000000
--- a/src/com/andrew/apollo/adapters/TabAdapter.java
+++ /dev/null
@@ -1,8 +0,0 @@
-
-package com.andrew.apollo.adapters;
-
-import android.view.View;
-
-public interface TabAdapter {
- public View getView(int position);
-}
diff --git a/src/com/andrew/apollo/adapters/TrackAdapter.java b/src/com/andrew/apollo/adapters/TrackAdapter.java
deleted file mode 100644
index f465c8e..0000000
--- a/src/com/andrew/apollo/adapters/TrackAdapter.java
+++ /dev/null
@@ -1,97 +0,0 @@
-
-package com.andrew.apollo.adapters;
-
-import java.lang.ref.WeakReference;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.graphics.drawable.AnimationDrawable;
-import android.os.RemoteException;
-import android.support.v4.widget.SimpleCursorAdapter;
-import android.view.View;
-import android.view.ViewGroup;
-
-import com.andrew.apollo.Constants;
-import com.andrew.apollo.R;
-import com.andrew.apollo.list.fragments.TracksFragment;
-import com.andrew.apollo.utils.MusicUtils;
-import com.andrew.apollo.views.ViewHolderList;
-
-/**
- * @author Andrew Neal
- */
-public class TrackAdapter extends SimpleCursorAdapter implements Constants {
-
- private AnimationDrawable mPeakOneAnimation, mPeakTwoAnimation;
-
- private WeakReference<ViewHolderList> holderReference;
-
- public TrackAdapter(Context context, int layout, Cursor c, String[] from, int[] to, int flags) {
- super(context, layout, c, from, to, flags);
- }
-
- /**
- * Used to quickly our the ContextMenu
- */
- private final View.OnClickListener showContextMenu = new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- v.showContextMenu();
- }
- };
-
- @Override
- public View getView(int position, View convertView, ViewGroup parent) {
- final View view = super.getView(position, convertView, parent);
- // ViewHolderList
- ViewHolderList viewholder;
-
- if (view != null) {
-
- viewholder = new ViewHolderList(view);
- holderReference = new WeakReference<ViewHolderList>(viewholder);
- view.setTag(holderReference.get());
-
- } else {
- viewholder = (ViewHolderList)convertView.getTag();
- }
-
- // Track name
- String trackName = mCursor.getString(TracksFragment.mTitleIndex);
- viewholder.mViewHolderLineOne.setText(trackName);
-
- // Artist name
- String artistName = mCursor.getString(TracksFragment.mArtistIndex);
- holderReference.get().mViewHolderLineTwo.setText(artistName);
-
- // Hide the album art
- holderReference.get().mViewHolderImage.setVisibility(View.GONE);
-
- holderReference.get().mQuickContext.setOnClickListener(showContextMenu);
-
- // Now playing indicator
- long currentaudioid = MusicUtils.getCurrentAudioId();
- long audioid = mCursor.getLong(TracksFragment.mMediaIdIndex);
- if (currentaudioid == audioid) {
- holderReference.get().mPeakOne.setImageResource(R.anim.peak_meter_1);
- holderReference.get().mPeakTwo.setImageResource(R.anim.peak_meter_2);
- mPeakOneAnimation = (AnimationDrawable)holderReference.get().mPeakOne.getDrawable();
- mPeakTwoAnimation = (AnimationDrawable)holderReference.get().mPeakTwo.getDrawable();
- try {
- if (MusicUtils.mService.isPlaying()) {
- mPeakOneAnimation.start();
- mPeakTwoAnimation.start();
- } else {
- mPeakOneAnimation.stop();
- mPeakTwoAnimation.stop();
- }
- } catch (RemoteException e) {
- e.printStackTrace();
- }
- } else {
- holderReference.get().mPeakOne.setImageResource(0);
- holderReference.get().mPeakTwo.setImageResource(0);
- }
- return view;
- }
-}
diff --git a/src/com/andrew/apollo/app/widgets/AppWidget11.java b/src/com/andrew/apollo/app/widgets/AppWidget11.java
deleted file mode 100644
index 6fdb63c..0000000
--- a/src/com/andrew/apollo/app/widgets/AppWidget11.java
+++ /dev/null
@@ -1,145 +0,0 @@
-
-package com.andrew.apollo.app.widgets;
-
-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.graphics.Bitmap;
-import android.view.View;
-import android.widget.RemoteViews;
-
-import com.andrew.apollo.Constants;
-import com.andrew.apollo.R;
-import com.andrew.apollo.service.ApolloService;
-import com.andrew.apollo.utils.ApolloUtils;
-import com.androidquery.AQuery;
-
-/**
- * Simple widget to show currently playing album art along with play/pause and
- * next track buttons.
- */
-public class AppWidget11 extends AppWidgetProvider implements Constants {
- static final String TAG = "MusicAppWidgetProvider1x1";
-
- public static final String CMDAPPWIDGETUPDATE = "appwidgetupdate1x1";
-
- private static AppWidget11 sInstance;
-
- public static synchronized AppWidget11 getInstance() {
- if (sInstance == null) {
- sInstance = new AppWidget11();
- }
- return sInstance;
- }
-
- @Override
- public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
- defaultAppWidget(context, appWidgetIds);
-
- // Send broadcast intent to any running ApolloService so it can
- // wrap around with an immediate update.
- Intent updateIntent = new Intent(ApolloService.SERVICECMD);
- updateIntent.putExtra(ApolloService.CMDNAME, AppWidget11.CMDAPPWIDGETUPDATE);
- updateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds);
- updateIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
- context.sendBroadcast(updateIntent);
- }
-
- /**
- * Initialize given widgets to default state, where we launch Music on
- * default click and hide actions if service not running.
- */
- private void defaultAppWidget(Context context, int[] appWidgetIds) {
- final RemoteViews views = new RemoteViews(context.getPackageName(),
- R.layout.onebyone_app_widget);
-
- views.setImageViewResource(R.id.one_by_one_albumart, View.GONE);
-
- linkButtons(context, views, false /* not playing */);
- 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 gm = AppWidgetManager.getInstance(context);
- if (appWidgetIds != null) {
- gm.updateAppWidget(appWidgetIds, views);
- } else {
- gm.updateAppWidget(new ComponentName(context, this.getClass()), views);
- }
- }
-
- /**
- * Check against {@link AppWidgetManager} if there are any instances of this
- * widget.
- */
- private boolean hasInstances(Context context) {
- AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
- int[] appWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(context, this
- .getClass()));
- return (appWidgetIds.length > 0);
- }
-
- /**
- * Handle a change notification coming over from {@link ApolloService}
- */
- public void notifyChange(ApolloService service, String what) {
- if (hasInstances(service)) {
- if (ApolloService.META_CHANGED.equals(what)
- || ApolloService.PLAYSTATE_CHANGED.equals(what)) {
- performUpdate(service, null);
- }
- }
- }
-
- /**
- * Update all active widget instances by pushing changes
- */
- public void performUpdate(ApolloService service, int[] appWidgetIds) {
- final RemoteViews views = new RemoteViews(service.getPackageName(),
- R.layout.onebyone_app_widget);
-
- // Set album art
- AQuery aq = new AQuery(service);
- Bitmap bitmap = aq.getCachedImage(ApolloUtils.getImageURL(service.getAlbumName(),
- ALBUM_IMAGE, service));
- if (bitmap != null) {
- views.setViewVisibility(R.id.one_by_one_albumart, View.VISIBLE);
- views.setImageViewBitmap(R.id.one_by_one_albumart, bitmap);
- } else {
- views.setViewVisibility(R.id.one_by_one_albumart, View.INVISIBLE);
- }
- // Set correct drawable for pause state
- final boolean playing = service.isPlaying();
-
- // Link actions buttons to intents
- linkButtons(service, views, playing);
-
- pushUpdate(service, appWidgetIds, views);
- }
-
- /**
- * Link up various button actions using {@link PendingIntents}.
- *
- * @param playerActive True if player is active in background, which means
- * widget click will launch {@link MediaPlaybackActivity},
- * otherwise we launch {@link MusicBrowserActivity}.
- */
- private void linkButtons(Context context, RemoteViews views, boolean playerActive) {
- // Connect up various buttons and touch events
- Intent intent;
- PendingIntent pendingIntent;
-
- final ComponentName serviceName = new ComponentName(context, ApolloService.class);
-
- intent = new Intent(ApolloService.TOGGLEPAUSE_ACTION);
- intent.setComponent(serviceName);
- pendingIntent = PendingIntent.getService(context, 0, intent, 0);
- views.setOnClickPendingIntent(R.id.one_by_one_albumart, pendingIntent);
-
- }
-}
diff --git a/src/com/andrew/apollo/app/widgets/AppWidget41.java b/src/com/andrew/apollo/app/widgets/AppWidget41.java
deleted file mode 100644
index be6b3bb..0000000
--- a/src/com/andrew/apollo/app/widgets/AppWidget41.java
+++ /dev/null
@@ -1,196 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.andrew.apollo.app.widgets;
-
-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.graphics.Bitmap;
-import android.view.View;
-import android.widget.RemoteViews;
-
-import com.andrew.apollo.Constants;
-import com.andrew.apollo.R;
-import com.andrew.apollo.activities.AudioPlayerHolder;
-import com.andrew.apollo.activities.MusicLibrary;
-import com.andrew.apollo.service.ApolloService;
-import com.andrew.apollo.utils.ApolloUtils;
-import com.androidquery.AQuery;
-
-/**
- * Simple widget to show currently playing album art along with play/pause and
- * next track buttons.
- */
-
-public class AppWidget41 extends AppWidgetProvider implements Constants {
-
- public static final String CMDAPPWIDGETUPDATE = "appwidgetupdate4x1";
-
- private static AppWidget41 sInstance;
-
- public static synchronized AppWidget41 getInstance() {
- if (sInstance == null) {
- sInstance = new AppWidget41();
- }
- return sInstance;
- }
-
- @Override
- public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
- defaultAppWidget(context, appWidgetIds);
-
- // Send broadcast intent to any running ApolloService so it can
- // wrap around with an immediate update.
- Intent updateIntent = new Intent(ApolloService.SERVICECMD);
- updateIntent.putExtra(ApolloService.CMDNAME, AppWidget41.CMDAPPWIDGETUPDATE);
- updateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds);
- updateIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
- context.sendBroadcast(updateIntent);
- }
-
- /**
- * Initialize given widgets to default state, where we launch Music on
- * default click and hide actions if service not running.
- */
- private void defaultAppWidget(Context context, int[] appWidgetIds) {
- final RemoteViews views = new RemoteViews(context.getPackageName(),
- R.layout.fourbyone_app_widget);
-
- linkButtons(context, views, false /* not playing */);
- 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 gm = AppWidgetManager.getInstance(context);
- if (appWidgetIds != null) {
- gm.updateAppWidget(appWidgetIds, views);
- } else {
- gm.updateAppWidget(new ComponentName(context, this.getClass()), views);
- }
- }
-
- /**
- * Check against {@link AppWidgetManager} if there are any instances of this
- * widget.
- */
- private boolean hasInstances(Context context) {
- AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
- int[] appWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(context, this
- .getClass()));
- return (appWidgetIds.length > 0);
- }
-
- /**
- * Handle a change notification coming over from {@link ApolloService}
- */
- public void notifyChange(ApolloService service, String what) {
- if (hasInstances(service)) {
- if (ApolloService.META_CHANGED.equals(what)
- || ApolloService.PLAYSTATE_CHANGED.equals(what)) {
- performUpdate(service, null);
- }
- }
- }
-
- /**
- * Update all active widget instances by pushing changes
- */
- public void performUpdate(ApolloService service, int[] appWidgetIds) {
- final RemoteViews views = new RemoteViews(service.getPackageName(),
- R.layout.fourbyone_app_widget);
-
- CharSequence titleName = service.getTrackName();
- CharSequence artistName = service.getArtistName();
-
- views.setTextViewText(R.id.four_by_one_title, titleName);
- views.setTextViewText(R.id.four_by_one_artist, artistName);
- // Set album art
- AQuery aq = new AQuery(service);
- Bitmap bitmap = aq.getCachedImage(ApolloUtils.getImageURL(service.getAlbumName(),
- ALBUM_IMAGE, service));
- if (bitmap != null) {
- views.setViewVisibility(R.id.four_by_one_albumart, View.VISIBLE);
- views.setViewVisibility(R.id.four_by_one_control_prev, View.GONE);
- views.setImageViewBitmap(R.id.four_by_one_albumart, bitmap);
- } else {
- views.setViewVisibility(R.id.four_by_one_control_prev, View.VISIBLE);
- views.setViewVisibility(R.id.four_by_one_albumart, View.GONE);
- }
-
- // Set correct drawable for pause state
- final boolean playing = service.isPlaying();
- if (playing) {
- views.setImageViewResource(R.id.four_by_one_control_play,
- R.drawable.apollo_holo_light_pause);
- } else {
- views.setImageViewResource(R.id.four_by_one_control_play,
- R.drawable.apollo_holo_light_play);
- }
-
- // Link actions buttons to intents
- linkButtons(service, views, playing);
-
- pushUpdate(service, appWidgetIds, views);
- }
-
- /**
- * Link up various button actions using {@link PendingIntents}.
- *
- * @param playerActive True if player is active in background, which means
- * widget click will launch {@link MediaPlaybackActivity},
- * otherwise we launch {@link MusicBrowserActivity}.
- */
- private void linkButtons(Context context, RemoteViews views, boolean playerActive) {
- // Connect up various buttons and touch events
- Intent intent;
- PendingIntent pendingIntent;
-
- final ComponentName serviceName = new ComponentName(context, ApolloService.class);
-
- if (playerActive) {
- intent = new Intent(context, AudioPlayerHolder.class);
- pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
- views.setOnClickPendingIntent(R.id.four_by_one_album_appwidget, pendingIntent);
- views.setOnClickPendingIntent(R.id.four_by_one_albumart, pendingIntent);
- } else {
- intent = new Intent(context, MusicLibrary.class);
- pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
- views.setOnClickPendingIntent(R.id.four_by_one_album_appwidget, pendingIntent);
- views.setOnClickPendingIntent(R.id.four_by_one_albumart, pendingIntent);
- }
-
- intent = new Intent(ApolloService.TOGGLEPAUSE_ACTION);
- intent.setComponent(serviceName);
- pendingIntent = PendingIntent.getService(context, 0, intent, 0);
- views.setOnClickPendingIntent(R.id.four_by_one_control_play, pendingIntent);
-
- intent = new Intent(ApolloService.NEXT_ACTION);
- intent.setComponent(serviceName);
- pendingIntent = PendingIntent.getService(context, 0, intent, 0);
- views.setOnClickPendingIntent(R.id.four_by_one_control_next, pendingIntent);
-
- intent = new Intent(ApolloService.PREVIOUS_ACTION);
- intent.setComponent(serviceName);
- pendingIntent = PendingIntent.getService(context, 0, intent, 0);
- views.setOnClickPendingIntent(R.id.four_by_one_control_prev, pendingIntent);
- }
-}
diff --git a/src/com/andrew/apollo/app/widgets/AppWidget42.java b/src/com/andrew/apollo/app/widgets/AppWidget42.java
deleted file mode 100644
index cda0719..0000000
--- a/src/com/andrew/apollo/app/widgets/AppWidget42.java
+++ /dev/null
@@ -1,240 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.andrew.apollo.app.widgets;
-
-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.graphics.Bitmap;
-import android.view.View;
-import android.widget.RemoteViews;
-
-import com.andrew.apollo.Constants;
-import com.andrew.apollo.R;
-import com.andrew.apollo.activities.AudioPlayerHolder;
-import com.andrew.apollo.activities.MusicLibrary;
-import com.andrew.apollo.service.ApolloService;
-import com.andrew.apollo.utils.ApolloUtils;
-import com.androidquery.AQuery;
-
-/**
- * Simple widget to show currently playing album art along with play/pause and
- * next track buttons.
- */
-public class AppWidget42 extends AppWidgetProvider implements Constants {
-
- public static final String CMDAPPWIDGETUPDATE = "appwidgetupdate4x2";
-
- private static AppWidget42 sInstance;
-
- public static synchronized AppWidget42 getInstance() {
- if (sInstance == null) {
- sInstance = new AppWidget42();
- }
- return sInstance;
- }
-
- @Override
- public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
- defaultAppWidget(context, appWidgetIds);
-
- // Send broadcast intent to any running ApolloService so it can
- // wrap around with an immediate update.
- Intent updateIntent = new Intent(ApolloService.SERVICECMD);
- updateIntent.putExtra(ApolloService.CMDNAME, AppWidget42.CMDAPPWIDGETUPDATE);
- updateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds);
- updateIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
- context.sendBroadcast(updateIntent);
- }
-
- /**
- * Initialize given widgets to default state, where we launch Music on
- * default click and hide actions if service not running.
- */
- private void defaultAppWidget(Context context, int[] appWidgetIds) {
- final RemoteViews views = new RemoteViews(context.getPackageName(),
- R.layout.fourbytwo_app_widget);
-
- linkButtons(context, views, false /* not playing */);
- 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 gm = AppWidgetManager.getInstance(context);
- if (appWidgetIds != null) {
- gm.updateAppWidget(appWidgetIds, views);
- } else {
- gm.updateAppWidget(new ComponentName(context, this.getClass()), views);
- }
- }
-
- /**
- * Check against {@link AppWidgetManager} if there are any instances of this
- * widget.
- */
- private boolean hasInstances(Context context) {
- AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
- int[] appWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(context, this
- .getClass()));
- return (appWidgetIds.length > 0);
- }
-
- /**
- * Handle a change notification coming over from {@link ApolloService}
- */
- public void notifyChange(ApolloService service, String what) {
- if (hasInstances(service)) {
- if (ApolloService.META_CHANGED.equals(what)
- || ApolloService.PLAYSTATE_CHANGED.equals(what)
- || ApolloService.REPEATMODE_CHANGED.equals(what)
- || ApolloService.SHUFFLEMODE_CHANGED.equals(what)) {
- performUpdate(service, null);
- }
- }
- }
-
- /**
- * Update all active widget instances by pushing changes
- */
- public void performUpdate(ApolloService service, int[] appWidgetIds) {
- final RemoteViews views = new RemoteViews(service.getPackageName(),
- R.layout.fourbytwo_app_widget);
-
- CharSequence artistName = service.getArtistName();
- CharSequence albumName = service.getAlbumName();
- CharSequence trackName = service.getTrackName();
- views.setTextViewText(R.id.four_by_two_artistname, artistName);
- views.setTextViewText(R.id.four_by_two_albumname, albumName);
- views.setTextViewText(R.id.four_by_two_trackname, trackName);
-
- // Set album art
- AQuery aq = new AQuery(service);
- Bitmap bitmap = aq.getCachedImage(ApolloUtils.getImageURL(service.getAlbumName(),
- ALBUM_IMAGE, service));
- if (bitmap != null) {
- views.setViewVisibility(R.id.four_by_two_albumart, View.VISIBLE);
- views.setImageViewBitmap(R.id.four_by_two_albumart, bitmap);
- } else {
- views.setViewVisibility(R.id.four_by_two_albumart, View.GONE);
- }
-
- // Set correct drawable for pause state
- final boolean playing = service.isPlaying();
- if (playing) {
- views.setImageViewResource(R.id.four_by_two_control_play,
- R.drawable.apollo_holo_light_pause);
- } else {
- views.setImageViewResource(R.id.four_by_two_control_play,
- R.drawable.apollo_holo_light_play);
- }
-
- // Set correct drawable for repeat state
- switch (service.getRepeatMode()) {
- case ApolloService.REPEAT_ALL:
- views.setImageViewResource(R.id.four_by_two_control_repeat,
- R.drawable.apollo_holo_light_repeat_all);
- break;
- case ApolloService.REPEAT_CURRENT:
- views.setImageViewResource(R.id.four_by_two_control_repeat,
- R.drawable.apollo_holo_light_repeat_one);
- break;
- default:
- views.setImageViewResource(R.id.four_by_two_control_repeat,
- R.drawable.apollo_holo_light_repeat_normal);
- break;
- }
-
- // Set correct drawable for shuffle state
- switch (service.getShuffleMode()) {
- case ApolloService.SHUFFLE_NONE:
- views.setImageViewResource(R.id.four_by_two_control_shuffle,
- R.drawable.apollo_holo_light_shuffle_normal);
- break;
- case ApolloService.SHUFFLE_AUTO:
- views.setImageViewResource(R.id.four_by_two_control_shuffle,
- R.drawable.apollo_holo_light_shuffle_on);
- break;
- default:
- views.setImageViewResource(R.id.four_by_two_control_shuffle,
- R.drawable.apollo_holo_light_shuffle_on);
- break;
- }
- // Link actions buttons to intents
- linkButtons(service, views, playing);
-
- pushUpdate(service, appWidgetIds, views);
-
- }
-
- /**
- * Link up various button actions using {@link PendingIntents}.
- *
- * @param playerActive True if player is active in background, which means
- * widget click will launch {@link MediaPlaybackActivity},
- * otherwise we launch {@link MusicBrowserActivity}.
- */
- private void linkButtons(Context context, RemoteViews views, boolean playerActive) {
-
- // Connect up various buttons and touch events
- Intent intent;
- PendingIntent pendingIntent;
-
- final ComponentName serviceName = new ComponentName(context, ApolloService.class);
-
- if (playerActive) {
- intent = new Intent(context, AudioPlayerHolder.class);
- pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
- views.setOnClickPendingIntent(R.id.four_by_two_albumart, pendingIntent);
- views.setOnClickPendingIntent(R.id.four_by_two_info, pendingIntent);
- } else {
- intent = new Intent(context, MusicLibrary.class);
- pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
- views.setOnClickPendingIntent(R.id.four_by_two_albumart, pendingIntent);
- views.setOnClickPendingIntent(R.id.four_by_two_info, pendingIntent);
- }
-
- intent = new Intent(ApolloService.TOGGLEPAUSE_ACTION);
- intent.setComponent(serviceName);
- pendingIntent = PendingIntent.getService(context, 0, intent, 0);
- views.setOnClickPendingIntent(R.id.four_by_two_control_play, pendingIntent);
-
- intent = new Intent(ApolloService.NEXT_ACTION);
- intent.setComponent(serviceName);
- pendingIntent = PendingIntent.getService(context, 0, intent, 0);
- views.setOnClickPendingIntent(R.id.four_by_two_control_next, pendingIntent);
-
- intent = new Intent(ApolloService.PREVIOUS_ACTION);
- intent.setComponent(serviceName);
- pendingIntent = PendingIntent.getService(context, 0, intent, 0);
- views.setOnClickPendingIntent(R.id.four_by_two_control_prev, pendingIntent);
-
- intent = new Intent(ApolloService.CYCLEREPEAT_ACTION);
- intent.setComponent(serviceName);
- pendingIntent = PendingIntent.getService(context, 0, intent, 0);
- views.setOnClickPendingIntent(R.id.four_by_two_control_repeat, pendingIntent);
-
- intent = new Intent(ApolloService.TOGGLESHUFFLE_ACTION);
- intent.setComponent(serviceName);
- pendingIntent = PendingIntent.getService(context, 0, intent, 0);
- views.setOnClickPendingIntent(R.id.four_by_two_control_shuffle, pendingIntent);
- }
-}
diff --git a/src/com/andrew/apollo/appwidgets/AppWidgetLarge.java b/src/com/andrew/apollo/appwidgets/AppWidgetLarge.java
new file mode 100644
index 0000000..753caa5
--- /dev/null
+++ b/src/com/andrew/apollo/appwidgets/AppWidgetLarge.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.appwidgets;
+
+import android.annotation.SuppressLint;
+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.graphics.Bitmap;
+import android.widget.RemoteViews;
+
+import com.andrew.apollo.MusicPlaybackService;
+import com.andrew.apollo.R;
+import com.andrew.apollo.ui.activities.AudioPlayerActivity;
+import com.andrew.apollo.ui.activities.HomeActivity;
+import com.andrew.apollo.utils.ApolloUtils;
+
+/**
+ * 4x2 App-Widget
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+@SuppressLint("NewApi")
+public class AppWidgetLarge extends AppWidgetProvider {
+
+ public static final String CMDAPPWIDGETUPDATE = "app_widget_large_update";
+
+ private static AppWidgetLarge mInstance;
+
+ public static synchronized AppWidgetLarge getInstance() {
+ if (mInstance == null) {
+ mInstance = new AppWidgetLarge();
+ }
+ return mInstance;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onUpdate(final Context context, final AppWidgetManager appWidgetManager,
+ final int[] appWidgetIds) {
+ defaultAppWidget(context, appWidgetIds);
+ final Intent updateIntent = new Intent(MusicPlaybackService.SERVICECMD);
+ updateIntent.putExtra(MusicPlaybackService.CMDNAME, AppWidgetLarge.CMDAPPWIDGETUPDATE);
+ updateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds);
+ updateIntent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
+ context.sendBroadcast(updateIntent);
+ }
+
+ /**
+ * Initialize given widgets to default state, where we launch Music on
+ * default click and hide actions if service not running.
+ */
+ private void defaultAppWidget(final Context context, final int[] appWidgetIds) {
+ final RemoteViews appWidgetViews = new RemoteViews(context.getPackageName(),
+ R.layout.app_widget_large);
+ linkButtons(context, appWidgetViews, false);
+ pushUpdate(context, appWidgetIds, appWidgetViews);
+ }
+
+ private void pushUpdate(final Context context, final int[] appWidgetIds, final RemoteViews views) {
+ final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
+ if (appWidgetIds != null) {
+ appWidgetManager.updateAppWidget(appWidgetIds, views);
+ } else {
+ appWidgetManager.updateAppWidget(new ComponentName(context, getClass()), views);
+ }
+ }
+
+ /**
+ * Check against {@link AppWidgetManager} if there are any instances of this
+ * widget.
+ */
+ private boolean hasInstances(final Context context) {
+ final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
+ final int[] mAppWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(context,
+ getClass()));
+ return mAppWidgetIds.length > 0;
+ }
+
+ /**
+ * Handle a change notification coming over from
+ * {@link MusicPlaybackService}
+ */
+ public void notifyChange(final MusicPlaybackService service, final String what) {
+ if (hasInstances(service)) {
+ if (MusicPlaybackService.META_CHANGED.equals(what)
+ || MusicPlaybackService.PLAYSTATE_CHANGED.equals(what)) {
+ performUpdate(service, null);
+ }
+ }
+ }
+
+ /**
+ * Update all active widget instances by pushing changes
+ */
+ public void performUpdate(final MusicPlaybackService service, final int[] appWidgetIds) {
+ final RemoteViews appWidgetView = new RemoteViews(service.getPackageName(),
+ R.layout.app_widget_large);
+
+ final CharSequence trackName = service.getTrackName();
+ final CharSequence artistName = service.getArtistName();
+ final CharSequence albumName = service.getAlbumName();
+ final Bitmap bitmap = service.getAlbumArt();
+
+ // Set the titles and artwork
+ appWidgetView.setTextViewText(R.id.app_widget_large_line_one, trackName);
+ appWidgetView.setTextViewText(R.id.app_widget_large_line_two, artistName);
+ appWidgetView.setTextViewText(R.id.app_widget_large_line_three, albumName);
+ appWidgetView.setImageViewBitmap(R.id.app_widget_large_image, bitmap);
+
+ // Set correct drawable for pause state
+ final boolean isPlaying = service.isPlaying();
+ if (isPlaying) {
+ appWidgetView.setImageViewResource(R.id.app_widget_large_play,
+ R.drawable.btn_playback_pause);
+ if (ApolloUtils.hasJellyBean()) {
+ appWidgetView.setContentDescription(R.id.app_widget_large_play,
+ service.getString(R.string.accessibility_pause));
+ }
+ } else {
+ appWidgetView.setImageViewResource(R.id.app_widget_large_play,
+ R.drawable.btn_playback_play);
+ if (ApolloUtils.hasJellyBean()) {
+ appWidgetView.setContentDescription(R.id.app_widget_large_play,
+ service.getString(R.string.accessibility_play));
+ }
+ }
+
+ // Link actions buttons to intents
+ linkButtons(service, appWidgetView, isPlaying);
+
+ // Update the app-widget
+ pushUpdate(service, appWidgetIds, appWidgetView);
+
+ // Build the notification
+ if (ApolloUtils.isApplicationSentToBackground(service)) {
+ service.mBuildNotification = true;
+ }
+ }
+
+ /**
+ * Link up various button actions using {@link PendingIntents}.
+ *
+ * @param playerActive True if player is active in background, which means
+ * widget click will launch {@link AudioPlayerActivity},
+ * otherwise we launch {@link MusicBrowserActivity}.
+ */
+ private void linkButtons(final Context context, final RemoteViews views,
+ final boolean playerActive) {
+ Intent action;
+ PendingIntent pendingIntent;
+
+ final ComponentName serviceName = new ComponentName(context, MusicPlaybackService.class);
+
+ // Now playing
+ if (playerActive) {
+ action = new Intent(context, AudioPlayerActivity.class);
+ pendingIntent = PendingIntent.getActivity(context, 0, action, 0);
+ views.setOnClickPendingIntent(R.id.app_widget_large_info_container, pendingIntent);
+ views.setOnClickPendingIntent(R.id.app_widget_large_image, pendingIntent);
+ } else {
+ // Home
+ action = new Intent(context, HomeActivity.class);
+ pendingIntent = PendingIntent.getActivity(context, 0, action, 0);
+ views.setOnClickPendingIntent(R.id.app_widget_large_info_container, pendingIntent);
+ views.setOnClickPendingIntent(R.id.app_widget_large_image, pendingIntent);
+ }
+
+ // Previous track
+ action = new Intent(MusicPlaybackService.PREVIOUS_ACTION);
+ action.setComponent(serviceName);
+ pendingIntent = PendingIntent.getService(context, 0, action, 0);
+ views.setOnClickPendingIntent(R.id.app_widget_large_previous, pendingIntent);
+
+ // Play and pause
+ action = new Intent(MusicPlaybackService.TOGGLEPAUSE_ACTION);
+ action.setComponent(serviceName);
+ pendingIntent = PendingIntent.getService(context, 0, action, 0);
+ views.setOnClickPendingIntent(R.id.app_widget_large_play, pendingIntent);
+
+ // Next track
+ action = new Intent(MusicPlaybackService.NEXT_ACTION);
+ action.setComponent(serviceName);
+ pendingIntent = PendingIntent.getService(context, 0, action, 0);
+ views.setOnClickPendingIntent(R.id.app_widget_large_next, pendingIntent);
+ }
+
+}
diff --git a/src/com/andrew/apollo/appwidgets/AppWidgetLargeAlternate.java b/src/com/andrew/apollo/appwidgets/AppWidgetLargeAlternate.java
new file mode 100644
index 0000000..b504fc5
--- /dev/null
+++ b/src/com/andrew/apollo/appwidgets/AppWidgetLargeAlternate.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.appwidgets;
+
+import android.annotation.SuppressLint;
+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.graphics.Bitmap;
+import android.widget.RemoteViews;
+
+import com.andrew.apollo.MusicPlaybackService;
+import com.andrew.apollo.R;
+import com.andrew.apollo.ui.activities.AudioPlayerActivity;
+import com.andrew.apollo.ui.activities.HomeActivity;
+import com.andrew.apollo.utils.ApolloUtils;
+
+/**
+ * 4x2 App-Widget
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+@SuppressLint("NewApi")
+public class AppWidgetLargeAlternate extends AppWidgetProvider {
+
+ public static final String CMDAPPWIDGETUPDATE = "app_widget_large_alternate_update";
+
+ private static AppWidgetLargeAlternate mInstance;
+
+ public static synchronized AppWidgetLargeAlternate getInstance() {
+ if (mInstance == null) {
+ mInstance = new AppWidgetLargeAlternate();
+ }
+ return mInstance;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onUpdate(final Context context, final AppWidgetManager appWidgetManager,
+ final int[] appWidgetIds) {
+ defaultAppWidget(context, appWidgetIds);
+ final Intent updateIntent = new Intent(MusicPlaybackService.SERVICECMD);
+ updateIntent.putExtra(MusicPlaybackService.CMDNAME,
+ AppWidgetLargeAlternate.CMDAPPWIDGETUPDATE);
+ updateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds);
+ updateIntent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
+ context.sendBroadcast(updateIntent);
+ }
+
+ /**
+ * Initialize given widgets to default state, where we launch Music on
+ * default click and hide actions if service not running.
+ */
+ private void defaultAppWidget(final Context context, final int[] appWidgetIds) {
+ final RemoteViews appWidgetViews = new RemoteViews(context.getPackageName(),
+ R.layout.app_widget_large_alternate);
+ linkButtons(context, appWidgetViews, false);
+ pushUpdate(context, appWidgetIds, appWidgetViews);
+ }
+
+ private void pushUpdate(final Context context, final int[] appWidgetIds, final RemoteViews views) {
+ final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
+ if (appWidgetIds != null) {
+ appWidgetManager.updateAppWidget(appWidgetIds, views);
+ } else {
+ appWidgetManager.updateAppWidget(new ComponentName(context, getClass()), views);
+ }
+ }
+
+ /**
+ * Check against {@link AppWidgetManager} if there are any instances of this
+ * widget.
+ */
+ private boolean hasInstances(final Context context) {
+ final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
+ final int[] mAppWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(context,
+ getClass()));
+ return mAppWidgetIds.length > 0;
+ }
+
+ /**
+ * Handle a change notification coming over from
+ * {@link MusicPlaybackService}
+ */
+ public void notifyChange(final MusicPlaybackService service, final String what) {
+ if (hasInstances(service)) {
+ if (MusicPlaybackService.META_CHANGED.equals(what)
+ || MusicPlaybackService.PLAYSTATE_CHANGED.equals(what)
+ || MusicPlaybackService.REPEATMODE_CHANGED.equals(what)
+ || MusicPlaybackService.SHUFFLEMODE_CHANGED.equals(what)) {
+ performUpdate(service, null);
+ }
+ }
+ }
+
+ /**
+ * Update all active widget instances by pushing changes
+ */
+ public void performUpdate(final MusicPlaybackService service, final int[] appWidgetIds) {
+ final RemoteViews appWidgetView = new RemoteViews(service.getPackageName(),
+ R.layout.app_widget_large_alternate);
+
+ final CharSequence trackName = service.getTrackName();
+ final CharSequence artistName = service.getArtistName();
+ final CharSequence albumName = service.getAlbumName();
+ final Bitmap bitmap = service.getAlbumArt();
+
+ // Set the titles and artwork
+ appWidgetView.setTextViewText(R.id.app_widget_large_alternate_line_one, trackName);
+ appWidgetView.setTextViewText(R.id.app_widget_large_alternate_line_two, artistName);
+ appWidgetView.setTextViewText(R.id.app_widget_large_alternate_line_three, albumName);
+ appWidgetView.setImageViewBitmap(R.id.app_widget_large_alternate_image, bitmap);
+
+ // Set correct drawable for pause state
+ final boolean isPlaying = service.isPlaying();
+ if (isPlaying) {
+ appWidgetView.setImageViewResource(R.id.app_widget_large_alternate_play,
+ R.drawable.btn_playback_pause);
+ if (ApolloUtils.hasJellyBean()) {
+ appWidgetView.setContentDescription(R.id.app_widget_large_alternate_play,
+ service.getString(R.string.accessibility_pause));
+ }
+ } else {
+ appWidgetView.setImageViewResource(R.id.app_widget_large_alternate_play,
+ R.drawable.btn_playback_play);
+ if (ApolloUtils.hasJellyBean()) {
+ appWidgetView.setContentDescription(R.id.app_widget_large_alternate_play,
+ service.getString(R.string.accessibility_play));
+ }
+ }
+
+ // Set the correct drawable for the repeat state
+ switch (service.getRepeatMode()) {
+ case MusicPlaybackService.REPEAT_ALL:
+ appWidgetView.setImageViewResource(R.id.app_widget_large_alternate_repeat,
+ R.drawable.btn_playback_repeat_all);
+ break;
+ case MusicPlaybackService.REPEAT_CURRENT:
+ appWidgetView.setImageViewResource(R.id.app_widget_large_alternate_repeat,
+ R.drawable.btn_playback_repeat_one);
+ break;
+ default:
+ appWidgetView.setImageViewResource(R.id.app_widget_large_alternate_repeat,
+ R.drawable.btn_playback_repeat);
+ break;
+ }
+
+ // Set the correct drawable for the shuffle state
+ switch (service.getShuffleMode()) {
+ case MusicPlaybackService.SHUFFLE_NONE:
+ appWidgetView.setImageViewResource(R.id.app_widget_large_alternate_shuffle,
+ R.drawable.btn_playback_shuffle);
+ break;
+ case MusicPlaybackService.SHUFFLE_AUTO:
+ appWidgetView.setImageViewResource(R.id.app_widget_large_alternate_shuffle,
+ R.drawable.btn_playback_shuffle_all);
+ break;
+ default:
+ appWidgetView.setImageViewResource(R.id.app_widget_large_alternate_shuffle,
+ R.drawable.btn_playback_shuffle_all);
+ break;
+ }
+
+ // Link actions buttons to intents
+ linkButtons(service, appWidgetView, isPlaying);
+
+ // Update the app-widget
+ pushUpdate(service, appWidgetIds, appWidgetView);
+
+ // Build the notification
+ if (ApolloUtils.isApplicationSentToBackground(service)) {
+ service.mBuildNotification = true;
+ }
+ }
+
+ /**
+ * Link up various button actions using {@link PendingIntents}.
+ *
+ * @param playerActive True if player is active in background, which means
+ * widget click will launch {@link AudioPlayerActivity},
+ * otherwise we launch {@link MusicBrowserActivity}.
+ */
+ private void linkButtons(final Context context, final RemoteViews views,
+ final boolean playerActive) {
+ Intent action;
+ PendingIntent pendingIntent;
+
+ final ComponentName serviceName = new ComponentName(context, MusicPlaybackService.class);
+
+ // Now playing
+ if (playerActive) {
+ action = new Intent(context, AudioPlayerActivity.class);
+ pendingIntent = PendingIntent.getActivity(context, 0, action, 0);
+ views.setOnClickPendingIntent(R.id.app_widget_large_alternate_info_container,
+ pendingIntent);
+ views.setOnClickPendingIntent(R.id.app_widget_large_alternate_image, pendingIntent);
+ } else {
+ // Home
+ action = new Intent(context, HomeActivity.class);
+ pendingIntent = PendingIntent.getActivity(context, 0, action, 0);
+ views.setOnClickPendingIntent(R.id.app_widget_large_alternate_info_container,
+ pendingIntent);
+ views.setOnClickPendingIntent(R.id.app_widget_large_alternate_image, pendingIntent);
+ }
+ // Shuffle modes
+ action = new Intent(MusicPlaybackService.SHUFFLE_ACTION);
+ action.setComponent(serviceName);
+ pendingIntent = PendingIntent.getService(context, 0, action, 0);
+ views.setOnClickPendingIntent(R.id.app_widget_large_alternate_shuffle, pendingIntent);
+
+ // Previous track
+ action = new Intent(MusicPlaybackService.PREVIOUS_ACTION);
+ action.setComponent(serviceName);
+ pendingIntent = PendingIntent.getService(context, 0, action, 0);
+ views.setOnClickPendingIntent(R.id.app_widget_large_alternate_previous, pendingIntent);
+
+ // Play and pause
+ action = new Intent(MusicPlaybackService.TOGGLEPAUSE_ACTION);
+ action.setComponent(serviceName);
+ pendingIntent = PendingIntent.getService(context, 0, action, 0);
+ views.setOnClickPendingIntent(R.id.app_widget_large_alternate_play, pendingIntent);
+
+ // Next track
+ action = new Intent(MusicPlaybackService.NEXT_ACTION);
+ action.setComponent(serviceName);
+ pendingIntent = PendingIntent.getService(context, 0, action, 0);
+ views.setOnClickPendingIntent(R.id.app_widget_large_alternate_next, pendingIntent);
+
+ // Repeat modes
+ action = new Intent(MusicPlaybackService.REPEAT_ACTION);
+ action.setComponent(serviceName);
+ pendingIntent = PendingIntent.getService(context, 0, action, 0);
+ views.setOnClickPendingIntent(R.id.app_widget_large_alternate_repeat, pendingIntent);
+ }
+
+}
diff --git a/src/com/andrew/apollo/appwidgets/AppWidgetSmall.java b/src/com/andrew/apollo/appwidgets/AppWidgetSmall.java
new file mode 100644
index 0000000..b57f510
--- /dev/null
+++ b/src/com/andrew/apollo/appwidgets/AppWidgetSmall.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.appwidgets;
+
+import android.annotation.SuppressLint;
+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.graphics.Bitmap;
+import android.widget.RemoteViews;
+
+import com.andrew.apollo.MusicPlaybackService;
+import com.andrew.apollo.R;
+import com.andrew.apollo.ui.activities.AudioPlayerActivity;
+import com.andrew.apollo.ui.activities.HomeActivity;
+import com.andrew.apollo.utils.ApolloUtils;
+
+/**
+ * 4x1 App-Widget
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+@SuppressLint("NewApi")
+public class AppWidgetSmall extends AppWidgetProvider {
+
+ public static final String CMDAPPWIDGETUPDATE = "app_widget_small_update";
+
+ private static AppWidgetSmall mInstance;
+
+ public static synchronized AppWidgetSmall getInstance() {
+ if (mInstance == null) {
+ mInstance = new AppWidgetSmall();
+ }
+ return mInstance;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onUpdate(final Context context, final AppWidgetManager appWidgetManager,
+ final int[] appWidgetIds) {
+ defaultAppWidget(context, appWidgetIds);
+ final Intent updateIntent = new Intent(MusicPlaybackService.SERVICECMD);
+ updateIntent.putExtra(MusicPlaybackService.CMDNAME, AppWidgetSmall.CMDAPPWIDGETUPDATE);
+ updateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds);
+ updateIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
+ context.sendBroadcast(updateIntent);
+ }
+
+ /**
+ * Initialize given widgets to default state, where we launch Music on
+ * default click and hide actions if service not running.
+ */
+ private void defaultAppWidget(final Context context, final int[] appWidgetIds) {
+ final RemoteViews appWidgetViews = new RemoteViews(context.getPackageName(),
+ R.layout.app_widget_small);
+ linkButtons(context, appWidgetViews, false);
+ pushUpdate(context, appWidgetIds, appWidgetViews);
+ }
+
+ private void pushUpdate(final Context context, final int[] appWidgetIds, final RemoteViews views) {
+ final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
+ if (appWidgetIds != null) {
+ appWidgetManager.updateAppWidget(appWidgetIds, views);
+ } else {
+ appWidgetManager.updateAppWidget(new ComponentName(context, getClass()), views);
+ }
+ }
+
+ /**
+ * Check against {@link AppWidgetManager} if there are any instances of this
+ * widget.
+ */
+ private boolean hasInstances(final Context context) {
+ final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
+ final int[] mAppWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(context,
+ getClass()));
+ return mAppWidgetIds.length > 0;
+ }
+
+ /**
+ * Handle a change notification coming over from
+ * {@link MusicPlaybackService}
+ */
+ public void notifyChange(final MusicPlaybackService service, final String what) {
+ if (hasInstances(service)) {
+ if (MusicPlaybackService.META_CHANGED.equals(what)
+ || MusicPlaybackService.PLAYSTATE_CHANGED.equals(what)) {
+ performUpdate(service, null);
+ }
+ }
+ }
+
+ /**
+ * Update all active widget instances by pushing changes
+ */
+ public void performUpdate(final MusicPlaybackService service, final int[] appWidgetIds) {
+ final RemoteViews appWidgetView = new RemoteViews(service.getPackageName(),
+ R.layout.app_widget_small);
+
+ final CharSequence trackName = service.getTrackName();
+ final CharSequence artistName = service.getArtistName();
+ final Bitmap bitmap = service.getAlbumArt();
+
+ // Set the titles and artwork
+ appWidgetView.setTextViewText(R.id.app_widget_small_line_one, trackName);
+ appWidgetView.setTextViewText(R.id.app_widget_small_line_two, artistName);
+ appWidgetView.setImageViewBitmap(R.id.app_widget_small_image, bitmap);
+
+ // Set correct drawable for pause state
+ final boolean isPlaying = service.isPlaying();
+ if (isPlaying) {
+ appWidgetView.setImageViewResource(R.id.app_widget_small_play,
+ R.drawable.btn_playback_pause);
+ if (ApolloUtils.hasJellyBean()) {
+ appWidgetView.setContentDescription(R.id.app_widget_small_play,
+ service.getString(R.string.accessibility_pause));
+ }
+ } else {
+ appWidgetView.setImageViewResource(R.id.app_widget_small_play,
+ R.drawable.btn_playback_play);
+ if (ApolloUtils.hasJellyBean()) {
+ appWidgetView.setContentDescription(R.id.app_widget_small_play,
+ service.getString(R.string.accessibility_play));
+ }
+ }
+
+ // Link actions buttons to intents
+ linkButtons(service, appWidgetView, isPlaying);
+
+ // Update the app-widget
+ pushUpdate(service, appWidgetIds, appWidgetView);
+
+ // Build the notification
+ if (ApolloUtils.isApplicationSentToBackground(service)) {
+ service.mBuildNotification = true;
+ }
+ }
+
+ /**
+ * Link up various button actions using {@link PendingIntents}.
+ *
+ * @param playerActive True if player is active in background, which means
+ * widget click will launch {@link AudioPlayerActivity},
+ * otherwise we launch {@link MusicBrowserActivity}.
+ */
+ private void linkButtons(final Context context, final RemoteViews views,
+ final boolean playerActive) {
+ Intent action;
+ PendingIntent pendingIntent;
+
+ final ComponentName serviceName = new ComponentName(context, MusicPlaybackService.class);
+
+ // Now playing
+ if (playerActive) {
+ action = new Intent(context, AudioPlayerActivity.class);
+ pendingIntent = PendingIntent.getActivity(context, 0, action, 0);
+ views.setOnClickPendingIntent(R.id.app_widget_small_info_container, pendingIntent);
+ views.setOnClickPendingIntent(R.id.app_widget_small_image, pendingIntent);
+ } else {
+ // Home
+ action = new Intent(context, HomeActivity.class);
+ pendingIntent = PendingIntent.getActivity(context, 0, action, 0);
+ views.setOnClickPendingIntent(R.id.app_widget_small_info_container, pendingIntent);
+ views.setOnClickPendingIntent(R.id.app_widget_small_image, pendingIntent);
+ }
+
+ // Previous track
+ action = new Intent(MusicPlaybackService.PREVIOUS_ACTION);
+ action.setComponent(serviceName);
+ pendingIntent = PendingIntent.getService(context, 0, action, 0);
+ views.setOnClickPendingIntent(R.id.app_widget_small_previous, pendingIntent);
+
+ // Play and pause
+ action = new Intent(MusicPlaybackService.TOGGLEPAUSE_ACTION);
+ action.setComponent(serviceName);
+ pendingIntent = PendingIntent.getService(context, 0, action, 0);
+ views.setOnClickPendingIntent(R.id.app_widget_small_play, pendingIntent);
+
+ // Next track
+ action = new Intent(MusicPlaybackService.NEXT_ACTION);
+ action.setComponent(serviceName);
+ pendingIntent = PendingIntent.getService(context, 0, action, 0);
+ views.setOnClickPendingIntent(R.id.app_widget_small_next, pendingIntent);
+ }
+
+}
diff --git a/src/com/andrew/apollo/appwidgets/RecentWidgetProvider.java b/src/com/andrew/apollo/appwidgets/RecentWidgetProvider.java
new file mode 100644
index 0000000..ce82262
--- /dev/null
+++ b/src/com/andrew/apollo/appwidgets/RecentWidgetProvider.java
@@ -0,0 +1,300 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.appwidgets;
+
+import android.annotation.TargetApi;
+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.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.provider.MediaStore;
+import android.widget.RemoteViews;
+
+import com.andrew.apollo.Config;
+import com.andrew.apollo.MusicPlaybackService;
+import com.andrew.apollo.R;
+import com.andrew.apollo.ui.activities.AudioPlayerActivity;
+import com.andrew.apollo.ui.activities.HomeActivity;
+import com.andrew.apollo.ui.activities.ProfileActivity;
+import com.andrew.apollo.ui.activities.ShortcutActivity;
+import com.andrew.apollo.utils.ApolloUtils;
+import com.andrew.apollo.utils.MusicUtils;
+
+/**
+ * App-Widget used to display a list of recently listened albums.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+@TargetApi(11)
+public class RecentWidgetProvider extends AppWidgetProvider {
+
+ public static final String SET_ACTION = "set_action";
+
+ public static final String OPEN_PROFILE = "open_profile";
+
+ public static final String PLAY_ALBUM = "play_album";
+
+ public static final String CMDAPPWIDGETUPDATE = "app_widget_recents_update";
+
+ public static final String CLICK_ACTION = "com.andrew.apollo.recents.appwidget.action.CLICK";
+
+ public static final String REFRESH_ACTION = "com.andrew.apollo.recents.appwidget.action.REFRESH";
+
+ private static Handler sWorkerQueue;
+
+ private static RecentWidgetProvider mInstance;
+
+ private RemoteViews mViews;
+
+ /**
+ * Constructor of <code>RecentWidgetProvider</code>
+ */
+ public RecentWidgetProvider() {
+ // Start the worker thread
+ final HandlerThread workerThread = new HandlerThread("RecentWidgetProviderWorker",
+ android.os.Process.THREAD_PRIORITY_BACKGROUND);
+ workerThread.start();
+ sWorkerQueue = new Handler(workerThread.getLooper());
+ }
+
+ /**
+ * @return A singelton of {@link RecentWidgetProvider}
+ */
+ public static synchronized RecentWidgetProvider getInstance() {
+ if (mInstance == null) {
+ mInstance = new RecentWidgetProvider();
+ }
+ return mInstance;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onUpdate(final Context context, final AppWidgetManager appWidgetManager,
+ final int[] appWidgetIds) {
+ for (final int appWidgetId : appWidgetIds) {
+ // Create the remote views
+ mViews = new RemoteViews(context.getPackageName(), R.layout.app_widget_recents);
+
+ // Link actions buttons to intents
+ linkButtons(context, mViews, false);
+
+ final Intent intent = new Intent(context, RecentWidgetService.class);
+ intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
+ intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
+ compatSetRemoteAdapter(mViews, appWidgetId, intent);
+
+ final Intent updateIntent = new Intent(MusicPlaybackService.SERVICECMD);
+ updateIntent.putExtra(MusicPlaybackService.CMDNAME,
+ RecentWidgetProvider.CMDAPPWIDGETUPDATE);
+ updateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds);
+ updateIntent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
+ context.sendBroadcast(updateIntent);
+
+ final Intent onClickIntent = new Intent(context, RecentWidgetProvider.class);
+ onClickIntent.setAction(RecentWidgetProvider.CLICK_ACTION);
+ onClickIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
+ onClickIntent.setData(Uri.parse(onClickIntent.toUri(Intent.URI_INTENT_SCHEME)));
+ final PendingIntent onClickPendingIntent = PendingIntent.getBroadcast(context, 0,
+ onClickIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+ mViews.setPendingIntentTemplate(R.id.app_widget_recents_list, onClickPendingIntent);
+
+ // Update the widget
+ appWidgetManager.updateAppWidget(appWidgetId, mViews);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onReceive(final Context context, final Intent intent) {
+ final String action = intent.getAction();
+
+ if (CLICK_ACTION.equals(action)) {
+ final long albumId = intent.getLongExtra(Config.ID, -1);
+
+ if (intent.getStringExtra(SET_ACTION).equals(PLAY_ALBUM)) {
+ // Play the selected album
+ if (albumId != -1) {
+ final Intent shortcutIntent = new Intent(context, ShortcutActivity.class);
+ shortcutIntent.setAction(Intent.ACTION_VIEW);
+ shortcutIntent.putExtra(Config.ID, albumId);
+ shortcutIntent.putExtra(Config.MIME_TYPE, MediaStore.Audio.Albums.CONTENT_TYPE);
+ shortcutIntent.putExtra(ShortcutActivity.OPEN_AUDIO_PLAYER, false);
+ shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(shortcutIntent);
+ }
+ } else if (intent.getStringExtra(SET_ACTION).equals(OPEN_PROFILE)) {
+ final String albumName = intent.getStringExtra(Config.NAME);
+ // Transfer the album name and MIME type
+ final Bundle bundle = new Bundle();
+ bundle.putString(Config.MIME_TYPE, MediaStore.Audio.Albums.CONTENT_TYPE);
+ bundle.putString(Config.NAME, albumName);
+ bundle.putString(Config.ARTIST_NAME, intent.getStringExtra(Config.ARTIST_NAME));
+ bundle.putString(Config.ALBUM_YEAR,
+ MusicUtils.getReleaseDateForAlbum(context, albumName));
+ bundle.putLong(Config.ID, albumId);
+
+ // Open the album profile
+ final Intent profileIntent = new Intent(context, ProfileActivity.class);
+ profileIntent.putExtras(bundle);
+ profileIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ profileIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ if (albumId != -1) {
+ context.startActivity(profileIntent);
+ }
+ }
+
+ }
+ super.onReceive(context, intent);
+ }
+
+ @SuppressWarnings("deprecation")
+ @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
+ private void compatSetRemoteAdapter(final RemoteViews rv, final int appWidgetId,
+ final Intent intent) {
+ if (ApolloUtils.hasICS()) {
+ rv.setRemoteAdapter(R.id.app_widget_recents_list, intent);
+ } else {
+ rv.setRemoteAdapter(appWidgetId, R.id.app_widget_recents_list, intent);
+ }
+ }
+
+ /**
+ * Check against {@link AppWidgetManager} if there are any instances of this
+ * widget.
+ */
+ private boolean hasInstances(final Context context) {
+ final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
+ final int[] appWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(context, this
+ .getClass()));
+ return appWidgetIds.length > 0;
+ }
+
+ private void pushUpdate(final Context context, final int[] appWidgetIds, final RemoteViews views) {
+ final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
+ if (appWidgetIds != null) {
+ appWidgetManager.updateAppWidget(appWidgetIds, views);
+ } else {
+ appWidgetManager.updateAppWidget(new ComponentName(context, this.getClass()), views);
+ }
+ }
+
+ /**
+ * Handle a change notification coming over from
+ * {@link MusicPlaybackService}
+ */
+ public void notifyChange(final MusicPlaybackService service, final String what) {
+ if (hasInstances(service)) {
+ if (MusicPlaybackService.PLAYSTATE_CHANGED.equals(what)) {
+ performUpdate(service, null);
+ } else if (MusicPlaybackService.META_CHANGED.equals(what)) {
+ synchronized (service) {
+ sWorkerQueue.post(new Runnable() {
+ @Override
+ public void run() {
+ final AppWidgetManager appWidgetManager = AppWidgetManager
+ .getInstance(service);
+ final ComponentName componentName = new ComponentName(service,
+ RecentWidgetProvider.class);
+ appWidgetManager.notifyAppWidgetViewDataChanged(
+ appWidgetManager.getAppWidgetIds(componentName),
+ R.id.app_widget_recents_list);
+ }
+ });
+ }
+ }
+ }
+ }
+
+ /**
+ * Update all active widget instances by pushing changes
+ */
+ public void performUpdate(final MusicPlaybackService service, final int[] appWidgetIds) {
+ mViews = new RemoteViews(service.getPackageName(), R.layout.app_widget_recents);
+
+ /* Set correct drawable for pause state */
+ final boolean isPlaying = service.isPlaying();
+ if (isPlaying) {
+ mViews.setImageViewResource(R.id.app_widget_recents_play, R.drawable.btn_playback_pause);
+ } else {
+ mViews.setImageViewResource(R.id.app_widget_recents_play, R.drawable.btn_playback_play);
+ }
+
+ // Link actions buttons to intents
+ linkButtons(service, mViews, isPlaying);
+
+ // Update the app-widget
+ pushUpdate(service, appWidgetIds, mViews);
+
+ // Build the notification
+ if (ApolloUtils.isApplicationSentToBackground(service)) {
+ service.mBuildNotification = true;
+ }
+ }
+
+ /**
+ * Link up various button actions using {@link PendingIntents}.
+ *
+ * @param playerActive True if player is active in background, which means
+ * widget click will launch {@link AudioPlayerActivity},
+ * otherwise we launch {@link MusicBrowserActivity}.
+ */
+ private void linkButtons(final Context context, final RemoteViews views,
+ final boolean playerActive) {
+ Intent action;
+ PendingIntent pendingIntent;
+
+ final ComponentName serviceName = new ComponentName(context, MusicPlaybackService.class);
+
+ // Now playing
+ if (playerActive) {
+ action = new Intent(context, AudioPlayerActivity.class);
+ pendingIntent = PendingIntent.getActivity(context, 0, action, 0);
+ views.setOnClickPendingIntent(R.id.app_widget_recents_action_bar, pendingIntent);
+ } else {
+ // Home
+ action = new Intent(context, HomeActivity.class);
+ pendingIntent = PendingIntent.getActivity(context, 0, action, 0);
+ views.setOnClickPendingIntent(R.id.app_widget_recents_action_bar, pendingIntent);
+ }
+
+ // Previous track
+ action = new Intent(MusicPlaybackService.PREVIOUS_ACTION);
+ action.setComponent(serviceName);
+ pendingIntent = PendingIntent.getService(context, 0, action, 0);
+ views.setOnClickPendingIntent(R.id.app_widget_recents_previous, pendingIntent);
+
+ // Play and pause
+ action = new Intent(MusicPlaybackService.TOGGLEPAUSE_ACTION);
+ action.setComponent(serviceName);
+ pendingIntent = PendingIntent.getService(context, 0, action, 0);
+ views.setOnClickPendingIntent(R.id.app_widget_recents_play, pendingIntent);
+
+ // Next track
+ action = new Intent(MusicPlaybackService.NEXT_ACTION);
+ action.setComponent(serviceName);
+ pendingIntent = PendingIntent.getService(context, 0, action, 0);
+ views.setOnClickPendingIntent(R.id.app_widget_recents_next, pendingIntent);
+ }
+
+}
diff --git a/src/com/andrew/apollo/appwidgets/RecentWidgetService.java b/src/com/andrew/apollo/appwidgets/RecentWidgetService.java
new file mode 100644
index 0000000..6f515ec
--- /dev/null
+++ b/src/com/andrew/apollo/appwidgets/RecentWidgetService.java
@@ -0,0 +1,245 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.appwidgets;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.widget.RemoteViews;
+import android.widget.RemoteViewsService;
+
+import com.andrew.apollo.Config;
+import com.andrew.apollo.R;
+import com.andrew.apollo.cache.ImageCache;
+import com.andrew.apollo.cache.ImageFetcher;
+import com.andrew.apollo.provider.RecentStore;
+import com.andrew.apollo.provider.RecentStore.RecentStoreColumns;
+
+/**
+ * This class is used to build the recently listened list for the
+ * {@link RecentWidgetProvicer}.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+@TargetApi(11)
+public class RecentWidgetService extends RemoteViewsService {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public RemoteViewsFactory onGetViewFactory(final Intent intent) {
+ return new WidgetRemoteViewsFactory(getApplicationContext());
+ }
+
+ /**
+ * This is the factory that will provide data to the collection widget.
+ */
+ private static final class WidgetRemoteViewsFactory implements
+ RemoteViewsService.RemoteViewsFactory {
+ /**
+ * Number of views (ImageView and TextView)
+ */
+ private static final int VIEW_TYPE_COUNT = 2;
+
+ /**
+ * The context to use
+ */
+ private final Context mContext;
+
+ /**
+ * Image cache
+ */
+ private final ImageFetcher mFetcher;
+
+ /**
+ * Recents db
+ */
+ private final RecentStore mRecentsStore;
+
+ /**
+ * Cursor to use
+ */
+ private Cursor mCursor;
+
+ /**
+ * Remove views
+ */
+ private RemoteViews mViews;
+
+ /**
+ * Constructor of <code>WidgetRemoteViewsFactory</code>
+ *
+ * @param context The {@link Context} to use.
+ */
+ public WidgetRemoteViewsFactory(final Context context) {
+ // Get the context
+ mContext = context;
+ // Initialze the image cache
+ mFetcher = ImageFetcher.getInstance(context);
+ mFetcher.setImageCache(ImageCache.getInstance(context));
+ // Initialze the recents store
+ mRecentsStore = RecentStore.getInstance(context);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getCount() {
+ // Check for errors
+ if (mCursor == null || mCursor.isClosed() || mCursor.getCount() <= 0) {
+ return 0;
+ }
+ return mCursor.getCount();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public long getItemId(final int position) {
+ return position;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public RemoteViews getViewAt(final int position) {
+ mCursor.moveToPosition(position);
+
+ // Create the remote views
+ mViews = new RemoteViews(mContext.getPackageName(), R.layout.app_widget_recents_items);
+
+ // Copy the album id
+ final String id = mCursor.getString(mCursor
+ .getColumnIndexOrThrow(RecentStoreColumns.ID));
+
+ // Copy the album name
+ final String albumName = mCursor.getString(mCursor
+ .getColumnIndexOrThrow(RecentStoreColumns.ALBUMNAME));
+
+ // Copy the artist name
+ final String artist = mCursor.getString(mCursor
+ .getColumnIndexOrThrow(RecentStoreColumns.ARTISTNAME));
+
+ // Set the album names
+ mViews.setTextViewText(R.id.app_widget_recents_line_one, albumName);
+ // Set the artist names
+ mViews.setTextViewText(R.id.app_widget_recents_line_two, artist);
+ // Set the album art
+ Bitmap bitmap = mFetcher.getCachedBitmap(albumName);
+
+ if (bitmap == null) {
+ bitmap = mFetcher.getCachedArtwork(albumName);
+ }
+ if (bitmap != null) {
+ mViews.setImageViewBitmap(R.id.app_widget_recents_base_image, bitmap);
+ } else {
+ mViews.setImageViewResource(R.id.app_widget_recents_base_image,
+ R.drawable.default_artwork);
+ }
+
+ // Open the profile of the touched album
+ final Intent profileIntent = new Intent();
+ final Bundle profileExtras = new Bundle();
+ profileExtras.putLong(Config.ID, Long.valueOf(id));
+ profileExtras.putString(Config.NAME, albumName);
+ profileExtras.putString(Config.ARTIST_NAME, artist);
+ profileExtras.putString(RecentWidgetProvider.SET_ACTION,
+ RecentWidgetProvider.OPEN_PROFILE);
+ profileIntent.putExtras(profileExtras);
+ mViews.setOnClickFillInIntent(R.id.app_widget_recents_items, profileIntent);
+
+ // Play the album when the artwork is touched
+ final Intent playAlbum = new Intent();
+ final Bundle playAlbumExtras = new Bundle();
+ playAlbumExtras.putLong(Config.ID, Long.valueOf(id));
+ playAlbumExtras.putString(RecentWidgetProvider.SET_ACTION,
+ RecentWidgetProvider.PLAY_ALBUM);
+ playAlbum.putExtras(playAlbumExtras);
+ mViews.setOnClickFillInIntent(R.id.app_widget_recents_base_image, playAlbum);
+ return mViews;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getViewTypeCount() {
+ return VIEW_TYPE_COUNT;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onDataSetChanged() {
+ if (mCursor != null && !mCursor.isClosed()) {
+ mCursor.close();
+ mCursor = null;
+ }
+ mCursor = mRecentsStore.getReadableDatabase().query(
+ RecentStoreColumns.NAME,
+ new String[] {
+ RecentStoreColumns.ID + " as id", RecentStoreColumns.ID,
+ RecentStoreColumns.ALBUMNAME, RecentStoreColumns.ARTISTNAME,
+ RecentStoreColumns.ALBUMSONGCOUNT, RecentStoreColumns.ALBUMYEAR,
+ RecentStoreColumns.TIMEPLAYED
+ }, null, null, null, null, RecentStoreColumns.TIMEPLAYED + " DESC");
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onDestroy() {
+ closeCursor();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public RemoteViews getLoadingView() {
+ // Nothing to do
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate() {
+ // Nothing to do
+ }
+
+ private void closeCursor() {
+ if (mCursor != null && !mCursor.isClosed()) {
+ mCursor.close();
+ mCursor = null;
+ }
+ }
+ }
+}
diff --git a/src/com/andrew/apollo/cache/DiskLruCache.java b/src/com/andrew/apollo/cache/DiskLruCache.java
new file mode 100644
index 0000000..320c359
--- /dev/null
+++ b/src/com/andrew/apollo/cache/DiskLruCache.java
@@ -0,0 +1,969 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.andrew.apollo.cache;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedWriter;
+import java.io.Closeable;
+import java.io.EOFException;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.FileWriter;
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.lang.reflect.Array;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ ****************************************************************************** Taken from the JB source code, can be found in:
+ * libcore/luni/src/main/java/libcore/io/DiskLruCache.java or direct link:
+ * https:
+ * //android.googlesource.com/platform/libcore/+/android-4.1.1_r1/luni/src/
+ * main/java/libcore/io/DiskLruCache.java A cache that uses a bounded amount of
+ * space on a filesystem. Each cache entry has a string key and a fixed number
+ * of values. Values are byte sequences, accessible as streams or files. Each
+ * value must be between {@code 0} and {@code Integer.MAX_VALUE} bytes in
+ * length.
+ * <p>
+ * The cache stores its data in a directory on the filesystem. This directory
+ * must be exclusive to the cache; the cache may delete or overwrite files from
+ * its directory. It is an error for multiple processes to use the same cache
+ * directory at the same time.
+ * <p>
+ * This cache limits the number of bytes that it will store on the filesystem.
+ * When the number of stored bytes exceeds the limit, the cache will remove
+ * entries in the background until the limit is satisfied. The limit is not
+ * strict: the cache may temporarily exceed it while waiting for files to be
+ * deleted. The limit does not include filesystem overhead or the cache journal
+ * so space-sensitive applications should set a conservative limit.
+ * <p>
+ * Clients call {@link #edit} to create or update the values of an entry. An
+ * entry may have only one editor at one time; if a value is not available to be
+ * edited then {@link #edit} will return null.
+ * <ul>
+ * <li>When an entry is being <strong>created</strong> it is necessary to supply
+ * a full set of values; the empty value should be used as a placeholder if
+ * necessary.
+ * <li>When an entry is being <strong>edited</strong>, it is not necessary to
+ * supply data for every value; values default to their previous value.
+ * </ul>
+ * Every {@link #edit} call must be matched by a call to {@link Editor#commit}
+ * or {@link Editor#abort}. Committing is atomic: a read observes the full set
+ * of values as they were before or after the commit, but never a mix of values.
+ * <p>
+ * Clients call {@link #get} to read a snapshot of an entry. The read will
+ * observe the value at the time that {@link #get} was called. Updates and
+ * removals after the call do not impact ongoing reads.
+ * <p>
+ * This class is tolerant of some I/O errors. If files are missing from the
+ * filesystem, the corresponding entries will be dropped from the cache. If an
+ * error occurs while writing a cache value, the edit will fail silently.
+ * Callers should handle other problems by catching {@code IOException} and
+ * responding appropriately.
+ */
+public final class DiskLruCache implements Closeable {
+ static final String JOURNAL_FILE = "journal";
+
+ static final String JOURNAL_FILE_TMP = "journal.tmp";
+
+ static final String MAGIC = "libcore.io.DiskLruCache";
+
+ static final String VERSION_1 = "1";
+
+ static final long ANY_SEQUENCE_NUMBER = -1;
+
+ private static final String CLEAN = "CLEAN";
+
+ private static final String DIRTY = "DIRTY";
+
+ private static final String REMOVE = "REMOVE";
+
+ private static final String READ = "READ";
+
+ private static final Charset UTF_8 = Charset.forName("UTF-8");
+
+ private static final int IO_BUFFER_SIZE = 8 * 1024;
+
+ /*
+ * This cache uses a journal file named "journal". A typical journal file
+ * looks like this: libcore.io.DiskLruCache 1 100 2 CLEAN
+ * 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054 DIRTY
+ * 335c4c6028171cfddfbaae1a9c313c52 CLEAN 335c4c6028171cfddfbaae1a9c313c52
+ * 3934 2342 REMOVE 335c4c6028171cfddfbaae1a9c313c52 DIRTY
+ * 1ab96a171faeeee38496d8b330771a7a CLEAN 1ab96a171faeeee38496d8b330771a7a
+ * 1600 234 READ 335c4c6028171cfddfbaae1a9c313c52 READ
+ * 3400330d1dfc7f3f7f4b8d4d803dfcf6 The first five lines of the journal form
+ * its header. They are the constant string "libcore.io.DiskLruCache", the
+ * disk cache's version, the application's version, the value count, and a
+ * blank line. Each of the subsequent lines in the file is a record of the
+ * state of a cache entry. Each line contains space-separated values: a
+ * state, a key, and optional state-specific values. o DIRTY lines track
+ * that an entry is actively being created or updated. Every successful
+ * DIRTY action should be followed by a CLEAN or REMOVE action. DIRTY lines
+ * without a matching CLEAN or REMOVE indicate that temporary files may need
+ * to be deleted. o CLEAN lines track a cache entry that has been
+ * successfully published and may be read. A publish line is followed by the
+ * lengths of each of its values. o READ lines track accesses for LRU. o
+ * REMOVE lines track entries that have been deleted. The journal file is
+ * appended to as cache operations occur. The journal may occasionally be
+ * compacted by dropping redundant lines. A temporary file named
+ * "journal.tmp" will be used during compaction; that file should be deleted
+ * if it exists when the cache is opened.
+ */
+
+ private final File directory;
+
+ private final File journalFile;
+
+ private final File journalFileTmp;
+
+ private final int appVersion;
+
+ private final long maxSize;
+
+ private final int valueCount;
+
+ private long size = 0;
+
+ private Writer journalWriter;
+
+ private final LinkedHashMap<String, Entry> lruEntries = new LinkedHashMap<String, Entry>(0,
+ 0.75f, true);
+
+ private int redundantOpCount;
+
+ /**
+ * To differentiate between old and current snapshots, each entry is given a
+ * sequence number each time an edit is committed. A snapshot is stale if
+ * its sequence number is not equal to its entry's sequence number.
+ */
+ private long nextSequenceNumber = 0;
+
+ /* From java.util.Arrays */
+ @SuppressWarnings("unchecked")
+ private static <T> T[] copyOfRange(final T[] original, final int start, final int end) {
+ final int originalLength = original.length; // For exception priority
+ // compatibility.
+ if (start > end) {
+ throw new IllegalArgumentException();
+ }
+ if (start < 0 || start > originalLength) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+ final int resultLength = end - start;
+ final int copyLength = Math.min(resultLength, originalLength - start);
+ final T[] result = (T[])Array.newInstance(original.getClass().getComponentType(),
+ resultLength);
+ System.arraycopy(original, start, result, 0, copyLength);
+ return result;
+ }
+
+ /**
+ * Returns the remainder of 'reader' as a string, closing it when done.
+ */
+ public static String readFully(final Reader reader) throws IOException {
+ try {
+ final StringWriter writer = new StringWriter();
+ final char[] buffer = new char[1024];
+ int count;
+ while ((count = reader.read(buffer)) != -1) {
+ writer.write(buffer, 0, count);
+ }
+ return writer.toString();
+ } finally {
+ reader.close();
+ }
+ }
+
+ /**
+ * Returns the ASCII characters up to but not including the next "\r\n", or
+ * "\n".
+ *
+ * @throws java.io.EOFException if the stream is exhausted before the next
+ * newline character.
+ */
+ public static String readAsciiLine(final InputStream in) throws IOException {
+ // TODO: support UTF-8 here instead
+
+ final StringBuilder result = new StringBuilder(80);
+ while (true) {
+ final int c = in.read();
+ if (c == -1) {
+ throw new EOFException();
+ } else if (c == '\n') {
+ break;
+ }
+
+ result.append((char)c);
+ }
+ final int length = result.length();
+ if (length > 0 && result.charAt(length - 1) == '\r') {
+ result.setLength(length - 1);
+ }
+ return result.toString();
+ }
+
+ /**
+ * Closes 'closeable', ignoring any checked exceptions. Does nothing if
+ * 'closeable' is null.
+ */
+ public static void closeQuietly(final Closeable closeable) {
+ if (closeable != null) {
+ try {
+ closeable.close();
+ } catch (final RuntimeException rethrown) {
+ throw rethrown;
+ } catch (final Exception ignored) {
+ }
+ }
+ }
+
+ /**
+ * Recursively delete everything in {@code dir}.
+ */
+ // TODO: this should specify paths as Strings rather than as Files
+ public static void deleteContents(final File dir) throws IOException {
+ final File[] files = dir.listFiles();
+ if (files == null) {
+ throw new IllegalArgumentException("not a directory: " + dir);
+ }
+ for (final File file : files) {
+ if (file.isDirectory()) {
+ deleteContents(file);
+ }
+ if (!file.delete()) {
+ throw new IOException("failed to delete file: " + file);
+ }
+ }
+ }
+
+ /** This cache uses a single background thread to evict entries. */
+ private final ExecutorService executorService = new ThreadPoolExecutor(0, 1, 60L,
+ TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
+
+ private final Callable<Void> cleanupCallable = new Callable<Void>() {
+ @Override
+ public Void call() throws Exception {
+ synchronized (DiskLruCache.this) {
+ if (journalWriter == null) {
+ return null; // closed
+ }
+ trimToSize();
+ if (journalRebuildRequired()) {
+ rebuildJournal();
+ redundantOpCount = 0;
+ }
+ }
+ return null;
+ }
+ };
+
+ private DiskLruCache(final File directory, final int appVersion, final int valueCount,
+ final long maxSize) {
+ this.directory = directory;
+ this.appVersion = appVersion;
+ journalFile = new File(directory, JOURNAL_FILE);
+ journalFileTmp = new File(directory, JOURNAL_FILE_TMP);
+ this.valueCount = valueCount;
+ this.maxSize = maxSize;
+ }
+
+ /**
+ * Opens the cache in {@code directory}, creating a cache if none exists
+ * there.
+ *
+ * @param directory a writable directory
+ * @param appVersion
+ * @param valueCount the number of values per cache entry. Must be positive.
+ * @param maxSize the maximum number of bytes this cache should use to store
+ * @throws IOException if reading or writing the cache directory fails
+ */
+ public static DiskLruCache open(final File directory, final int appVersion,
+ final int valueCount, final long maxSize) throws IOException {
+ if (maxSize <= 0) {
+ throw new IllegalArgumentException("maxSize <= 0");
+ }
+ if (valueCount <= 0) {
+ throw new IllegalArgumentException("valueCount <= 0");
+ }
+
+ // prefer to pick up where we left off
+ DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
+ if (cache.journalFile.exists()) {
+ try {
+ cache.readJournal();
+ cache.processJournal();
+ cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true),
+ IO_BUFFER_SIZE);
+ return cache;
+ } catch (final IOException journalIsCorrupt) {
+ // System.logW("DiskLruCache " + directory + " is corrupt: "
+ // + journalIsCorrupt.getMessage() + ", removing");
+ cache.delete();
+ }
+ }
+
+ // create a new empty cache
+ directory.mkdirs();
+ cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
+ cache.rebuildJournal();
+ return cache;
+ }
+
+ private void readJournal() throws IOException {
+ final InputStream in = new BufferedInputStream(new FileInputStream(journalFile),
+ IO_BUFFER_SIZE);
+ try {
+ final String magic = readAsciiLine(in);
+ final String version = readAsciiLine(in);
+ final String appVersionString = readAsciiLine(in);
+ final String valueCountString = readAsciiLine(in);
+ final String blank = readAsciiLine(in);
+ if (!MAGIC.equals(magic) || !VERSION_1.equals(version)
+ || !Integer.toString(appVersion).equals(appVersionString)
+ || !Integer.toString(valueCount).equals(valueCountString) || !"".equals(blank)) {
+ throw new IOException("unexpected journal header: [" + magic + ", " + version
+ + ", " + valueCountString + ", " + blank + "]");
+ }
+
+ while (true) {
+ try {
+ readJournalLine(readAsciiLine(in));
+ } catch (final EOFException endOfJournal) {
+ break;
+ }
+ }
+ } finally {
+ closeQuietly(in);
+ }
+ }
+
+ private void readJournalLine(final String line) throws IOException {
+ final String[] parts = line.split(" ");
+ if (parts.length < 2) {
+ throw new IOException("unexpected journal line: " + line);
+ }
+
+ final String key = parts[1];
+ if (parts[0].equals(REMOVE) && parts.length == 2) {
+ lruEntries.remove(key);
+ return;
+ }
+
+ Entry entry = lruEntries.get(key);
+ if (entry == null) {
+ entry = new Entry(key);
+ lruEntries.put(key, entry);
+ }
+
+ if (parts[0].equals(CLEAN) && parts.length == 2 + valueCount) {
+ entry.readable = true;
+ entry.currentEditor = null;
+ entry.setLengths(copyOfRange(parts, 2, parts.length));
+ } else if (parts[0].equals(DIRTY) && parts.length == 2) {
+ entry.currentEditor = new Editor(entry);
+ } else if (parts[0].equals(READ) && parts.length == 2) {
+ // this work was already done by calling lruEntries.get()
+ } else {
+ throw new IOException("unexpected journal line: " + line);
+ }
+ }
+
+ /**
+ * Computes the initial size and collects garbage as a part of opening the
+ * cache. Dirty entries are assumed to be inconsistent and will be deleted.
+ */
+ private void processJournal() throws IOException {
+ deleteIfExists(journalFileTmp);
+ for (final Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext();) {
+ final Entry entry = i.next();
+ if (entry.currentEditor == null) {
+ for (int t = 0; t < valueCount; t++) {
+ size += entry.lengths[t];
+ }
+ } else {
+ entry.currentEditor = null;
+ for (int t = 0; t < valueCount; t++) {
+ deleteIfExists(entry.getCleanFile(t));
+ deleteIfExists(entry.getDirtyFile(t));
+ }
+ i.remove();
+ }
+ }
+ }
+
+ /**
+ * Creates a new journal that omits redundant information. This replaces the
+ * current journal if it exists.
+ */
+ private synchronized void rebuildJournal() throws IOException {
+ if (journalWriter != null) {
+ journalWriter.close();
+ }
+
+ final Writer writer = new BufferedWriter(new FileWriter(journalFileTmp), IO_BUFFER_SIZE);
+ writer.write(MAGIC);
+ writer.write("\n");
+ writer.write(VERSION_1);
+ writer.write("\n");
+ writer.write(Integer.toString(appVersion));
+ writer.write("\n");
+ writer.write(Integer.toString(valueCount));
+ writer.write("\n");
+ writer.write("\n");
+
+ for (final Entry entry : lruEntries.values()) {
+ if (entry.currentEditor != null) {
+ writer.write(DIRTY + ' ' + entry.key + '\n');
+ } else {
+ writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
+ }
+ }
+
+ writer.close();
+ journalFileTmp.renameTo(journalFile);
+ journalWriter = new BufferedWriter(new FileWriter(journalFile, true), IO_BUFFER_SIZE);
+ }
+
+ private static void deleteIfExists(final File file) throws IOException {
+ // try {
+ // Libcore.os.remove(file.getPath());
+ // } catch (ErrnoException errnoException) {
+ // if (errnoException.errno != OsConstants.ENOENT) {
+ // throw errnoException.rethrowAsIOException();
+ // }
+ // }
+ if (file.exists() && !file.delete()) {
+ throw new IOException();
+ }
+ }
+
+ /**
+ * Returns a snapshot of the entry named {@code key}, or null if it doesn't
+ * exist is not currently readable. If a value is returned, it is moved to
+ * the head of the LRU queue.
+ */
+ public synchronized Snapshot get(final String key) throws IOException {
+ checkNotClosed();
+ validateKey(key);
+ final Entry entry = lruEntries.get(key);
+ if (entry == null) {
+ return null;
+ }
+
+ if (!entry.readable) {
+ return null;
+ }
+
+ /*
+ * Open all streams eagerly to guarantee that we see a single published
+ * snapshot. If we opened streams lazily then the streams could come
+ * from different edits.
+ */
+ final InputStream[] ins = new InputStream[valueCount];
+ try {
+ for (int i = 0; i < valueCount; i++) {
+ ins[i] = new FileInputStream(entry.getCleanFile(i));
+ }
+ } catch (final FileNotFoundException e) {
+ // a file must have been deleted manually!
+ return null;
+ }
+
+ redundantOpCount++;
+ journalWriter.append(READ + ' ' + key + '\n');
+ if (journalRebuildRequired()) {
+ executorService.submit(cleanupCallable);
+ }
+
+ return new Snapshot(key, entry.sequenceNumber, ins);
+ }
+
+ /**
+ * Returns an editor for the entry named {@code key}, or null if another
+ * edit is in progress.
+ */
+ public Editor edit(final String key) throws IOException {
+ return edit(key, ANY_SEQUENCE_NUMBER);
+ }
+
+ private synchronized Editor edit(final String key, final long expectedSequenceNumber)
+ throws IOException {
+ checkNotClosed();
+ validateKey(key);
+ Entry entry = lruEntries.get(key);
+ if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER
+ && (entry == null || entry.sequenceNumber != expectedSequenceNumber)) {
+ return null; // snapshot is stale
+ }
+ if (entry == null) {
+ entry = new Entry(key);
+ lruEntries.put(key, entry);
+ } else if (entry.currentEditor != null) {
+ return null; // another edit is in progress
+ }
+
+ final Editor editor = new Editor(entry);
+ entry.currentEditor = editor;
+
+ // flush the journal before creating files to prevent file leaks
+ journalWriter.write(DIRTY + ' ' + key + '\n');
+ journalWriter.flush();
+ return editor;
+ }
+
+ /**
+ * Returns the directory where this cache stores its data.
+ */
+ public File getDirectory() {
+ return directory;
+ }
+
+ /**
+ * Returns the maximum number of bytes that this cache should use to store
+ * its data.
+ */
+ public long maxSize() {
+ return maxSize;
+ }
+
+ /**
+ * Returns the number of bytes currently being used to store the values in
+ * this cache. This may be greater than the max size if a background
+ * deletion is pending.
+ */
+ public synchronized long size() {
+ return size;
+ }
+
+ private synchronized void completeEdit(final Editor editor, final boolean success)
+ throws IOException {
+ final Entry entry = editor.entry;
+ if (entry.currentEditor != editor) {
+ throw new IllegalStateException();
+ }
+
+ // if this edit is creating the entry for the first time, every index
+ // must have a value
+ if (success && !entry.readable) {
+ for (int i = 0; i < valueCount; i++) {
+ if (!entry.getDirtyFile(i).exists()) {
+ editor.abort();
+ throw new IllegalStateException("edit didn't create file " + i);
+ }
+ }
+ }
+
+ for (int i = 0; i < valueCount; i++) {
+ final File dirty = entry.getDirtyFile(i);
+ if (success) {
+ if (dirty.exists()) {
+ final File clean = entry.getCleanFile(i);
+ dirty.renameTo(clean);
+ final long oldLength = entry.lengths[i];
+ final long newLength = clean.length();
+ entry.lengths[i] = newLength;
+ size = size - oldLength + newLength;
+ }
+ } else {
+ deleteIfExists(dirty);
+ }
+ }
+
+ redundantOpCount++;
+ entry.currentEditor = null;
+ if (entry.readable | success) {
+ entry.readable = true;
+ journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
+ if (success) {
+ entry.sequenceNumber = nextSequenceNumber++;
+ }
+ } else {
+ lruEntries.remove(entry.key);
+ journalWriter.write(REMOVE + ' ' + entry.key + '\n');
+ }
+
+ if (size > maxSize || journalRebuildRequired()) {
+ executorService.submit(cleanupCallable);
+ }
+ }
+
+ /**
+ * We only rebuild the journal when it will halve the size of the journal
+ * and eliminate at least 2000 ops.
+ */
+ private boolean journalRebuildRequired() {
+ final int REDUNDANT_OP_COMPACT_THRESHOLD = 2000;
+ return redundantOpCount >= REDUNDANT_OP_COMPACT_THRESHOLD
+ && redundantOpCount >= lruEntries.size();
+ }
+
+ /**
+ * Drops the entry for {@code key} if it exists and can be removed. Entries
+ * actively being edited cannot be removed.
+ *
+ * @return true if an entry was removed.
+ */
+ public synchronized boolean remove(final String key) throws IOException {
+ checkNotClosed();
+ validateKey(key);
+ final Entry entry = lruEntries.get(key);
+ if (entry == null || entry.currentEditor != null) {
+ return false;
+ }
+
+ for (int i = 0; i < valueCount; i++) {
+ final File file = entry.getCleanFile(i);
+ if (!file.delete()) {
+ throw new IOException("failed to delete " + file);
+ }
+ size -= entry.lengths[i];
+ entry.lengths[i] = 0;
+ }
+
+ redundantOpCount++;
+ journalWriter.append(REMOVE + ' ' + key + '\n');
+ lruEntries.remove(key);
+
+ if (journalRebuildRequired()) {
+ executorService.submit(cleanupCallable);
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns true if this cache has been closed.
+ */
+ public boolean isClosed() {
+ return journalWriter == null;
+ }
+
+ private void checkNotClosed() {
+ if (journalWriter == null) {
+ throw new IllegalStateException("cache is closed");
+ }
+ }
+
+ /**
+ * Force buffered operations to the filesystem.
+ */
+ public synchronized void flush() throws IOException {
+ checkNotClosed();
+ trimToSize();
+ journalWriter.flush();
+ }
+
+ /**
+ * Closes this cache. Stored values will remain on the filesystem.
+ */
+ @Override
+ public synchronized void close() throws IOException {
+ if (journalWriter == null) {
+ return; // already closed
+ }
+ for (final Entry entry : new ArrayList<Entry>(lruEntries.values())) {
+ if (entry.currentEditor != null) {
+ entry.currentEditor.abort();
+ }
+ }
+ trimToSize();
+ journalWriter.close();
+ journalWriter = null;
+ }
+
+ private void trimToSize() throws IOException {
+ while (size > maxSize) {
+ // Map.Entry<String, Entry> toEvict = lruEntries.eldest();
+ final Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
+ remove(toEvict.getKey());
+ }
+ }
+
+ /**
+ * Closes the cache and deletes all of its stored values. This will delete
+ * all files in the cache directory including files that weren't created by
+ * the cache.
+ */
+ public void delete() throws IOException {
+ close();
+ deleteContents(directory);
+ }
+
+ private void validateKey(final String key) {
+ if (key.contains(" ") || key.contains("\n") || key.contains("\r")) {
+ throw new IllegalArgumentException("keys must not contain spaces or newlines: \"" + key
+ + "\"");
+ }
+ }
+
+ private static String inputStreamToString(final InputStream in) throws IOException {
+ return readFully(new InputStreamReader(in, UTF_8));
+ }
+
+ /**
+ * A snapshot of the values for an entry.
+ */
+ public final class Snapshot implements Closeable {
+ private final String key;
+
+ private final long sequenceNumber;
+
+ private final InputStream[] ins;
+
+ private Snapshot(final String key, final long sequenceNumber, final InputStream[] ins) {
+ this.key = key;
+ this.sequenceNumber = sequenceNumber;
+ this.ins = ins;
+ }
+
+ /**
+ * Returns an editor for this snapshot's entry, or null if either the
+ * entry has changed since this snapshot was created or if another edit
+ * is in progress.
+ */
+ public Editor edit() throws IOException {
+ return DiskLruCache.this.edit(key, sequenceNumber);
+ }
+
+ /**
+ * Returns the unbuffered stream with the value for {@code index}.
+ */
+ public InputStream getInputStream(final int index) {
+ return ins[index];
+ }
+
+ /**
+ * Returns the string value for {@code index}.
+ */
+ public String getString(final int index) throws IOException {
+ return inputStreamToString(getInputStream(index));
+ }
+
+ @Override
+ public void close() {
+ for (final InputStream in : ins) {
+ closeQuietly(in);
+ }
+ }
+ }
+
+ /**
+ * Edits the values for an entry.
+ */
+ public final class Editor {
+ private final Entry entry;
+
+ private boolean hasErrors;
+
+ private Editor(final Entry entry) {
+ this.entry = entry;
+ }
+
+ /**
+ * Returns an unbuffered input stream to read the last committed value,
+ * or null if no value has been committed.
+ */
+ public InputStream newInputStream(final int index) throws IOException {
+ synchronized (DiskLruCache.this) {
+ if (entry.currentEditor != this) {
+ throw new IllegalStateException();
+ }
+ if (!entry.readable) {
+ return null;
+ }
+ return new FileInputStream(entry.getCleanFile(index));
+ }
+ }
+
+ /**
+ * Returns the last committed value as a string, or null if no value has
+ * been committed.
+ */
+ public String getString(final int index) throws IOException {
+ final InputStream in = newInputStream(index);
+ return in != null ? inputStreamToString(in) : null;
+ }
+
+ /**
+ * Returns a new unbuffered output stream to write the value at
+ * {@code index}. If the underlying output stream encounters errors when
+ * writing to the filesystem, this edit will be aborted when
+ * {@link #commit} is called. The returned output stream does not throw
+ * IOExceptions.
+ */
+ public OutputStream newOutputStream(final int index) throws IOException {
+ synchronized (DiskLruCache.this) {
+ if (entry.currentEditor != this) {
+ throw new IllegalStateException();
+ }
+ return new FaultHidingOutputStream(new FileOutputStream(entry.getDirtyFile(index)));
+ }
+ }
+
+ /**
+ * Sets the value at {@code index} to {@code value}.
+ */
+ public void set(final int index, final String value) throws IOException {
+ Writer writer = null;
+ try {
+ writer = new OutputStreamWriter(newOutputStream(index), UTF_8);
+ writer.write(value);
+ } finally {
+ closeQuietly(writer);
+ }
+ }
+
+ /**
+ * Commits this edit so it is visible to readers. This releases the edit
+ * lock so another edit may be started on the same key.
+ */
+ public void commit() throws IOException {
+ if (hasErrors) {
+ completeEdit(this, false);
+ remove(entry.key); // the previous entry is stale
+ } else {
+ completeEdit(this, true);
+ }
+ }
+
+ /**
+ * Aborts this edit. This releases the edit lock so another edit may be
+ * started on the same key.
+ */
+ public void abort() throws IOException {
+ completeEdit(this, false);
+ }
+
+ private class FaultHidingOutputStream extends FilterOutputStream {
+ private FaultHidingOutputStream(final OutputStream out) {
+ super(out);
+ }
+
+ @Override
+ public void write(final int oneByte) {
+ try {
+ out.write(oneByte);
+ } catch (final IOException e) {
+ hasErrors = true;
+ }
+ }
+
+ @Override
+ public void write(final byte[] buffer, final int offset, final int length) {
+ try {
+ out.write(buffer, offset, length);
+ } catch (final IOException e) {
+ hasErrors = true;
+ }
+ }
+
+ @Override
+ public void close() {
+ try {
+ out.close();
+ } catch (final IOException e) {
+ hasErrors = true;
+ }
+ }
+
+ @Override
+ public void flush() {
+ try {
+ out.flush();
+ } catch (final IOException e) {
+ hasErrors = true;
+ }
+ }
+ }
+ }
+
+ private final class Entry {
+ private final String key;
+
+ /** Lengths of this entry's files. */
+ private final long[] lengths;
+
+ /** True if this entry has ever been published */
+ private boolean readable;
+
+ /** The ongoing edit or null if this entry is not being edited. */
+ private Editor currentEditor;
+
+ /**
+ * The sequence number of the most recently committed edit to this
+ * entry.
+ */
+ private long sequenceNumber;
+
+ private Entry(final String key) {
+ this.key = key;
+ lengths = new long[valueCount];
+ }
+
+ public String getLengths() throws IOException {
+ final StringBuilder result = new StringBuilder();
+ for (final long size : lengths) {
+ result.append(' ').append(size);
+ }
+ return result.toString();
+ }
+
+ /**
+ * Set lengths using decimal numbers like "10123".
+ */
+ private void setLengths(final String[] strings) throws IOException {
+ if (strings.length != valueCount) {
+ throw invalidLengths(strings);
+ }
+
+ try {
+ for (int i = 0; i < strings.length; i++) {
+ lengths[i] = Long.parseLong(strings[i]);
+ }
+ } catch (final NumberFormatException e) {
+ throw invalidLengths(strings);
+ }
+ }
+
+ private IOException invalidLengths(final String[] strings) throws IOException {
+ throw new IOException("unexpected journal line: " + Arrays.toString(strings));
+ }
+
+ public File getCleanFile(final int i) {
+ return new File(directory, key + "." + i);
+ }
+
+ public File getDirtyFile(final int i) {
+ return new File(directory, key + "." + i + ".tmp");
+ }
+ }
+}
diff --git a/src/com/andrew/apollo/cache/ImageCache.java b/src/com/andrew/apollo/cache/ImageCache.java
new file mode 100644
index 0000000..84e9040
--- /dev/null
+++ b/src/com/andrew/apollo/cache/ImageCache.java
@@ -0,0 +1,790 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.cache;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.app.ActivityManager;
+import android.content.ComponentCallbacks2;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.CompressFormat;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.ParcelFileDescriptor;
+import android.os.StatFs;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.FragmentManager;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.actionbarsherlock.app.SherlockFragment;
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.andrew.apollo.utils.ApolloUtils;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * This class holds the memory and disk bitmap caches.
+ */
+public final class ImageCache {
+
+ private static final String TAG = ImageCache.class.getSimpleName();
+
+ /**
+ * The {@link Uri} used to retrieve album art
+ */
+ private static final Uri mArtworkUri;
+
+ /**
+ * Default memory cache size as a percent of device memory class
+ */
+ private static final float MEM_CACHE_DIVIDER = 0.25f;
+
+ /**
+ * Default disk cache size 10MB
+ */
+ private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10;
+
+ /**
+ * Compression settings when writing images to disk cache
+ */
+ private static final CompressFormat COMPRESS_FORMAT = CompressFormat.JPEG;
+
+ /**
+ * Disk cache index to read from
+ */
+ private static final int DISK_CACHE_INDEX = 0;
+
+ /**
+ * Image compression quality
+ */
+ private static final int COMPRESS_QUALITY = 98;
+
+ /**
+ * LRU cache
+ */
+ private MemoryCache mLruCache;
+
+ /**
+ * Disk LRU cache
+ */
+ private DiskLruCache mDiskCache;
+
+ private static ImageCache sInstance;
+
+ /**
+ * Used to temporarily pause the disk cache while scrolling
+ */
+ public boolean mPauseDiskAccess = false;
+
+ static {
+ mArtworkUri = Uri.parse("content://media/external/audio/albumart");
+ }
+
+ /**
+ * Constructor of <code>ImageCache</code>
+ *
+ * @param context The {@link Context} to use
+ */
+ public ImageCache(final Context context) {
+ init(context);
+ }
+
+ /**
+ * Used to create a singleton of {@link ImageCache}
+ *
+ * @param context The {@link Context} to use
+ * @return A new instance of this class.
+ */
+ public final static ImageCache getInstance(final Context context) {
+ if (sInstance == null) {
+ sInstance = new ImageCache(context.getApplicationContext());
+ }
+ return sInstance;
+ }
+
+ /**
+ * Initialize the cache, providing all parameters.
+ *
+ * @param context The {@link Context} to use
+ * @param cacheParams The cache parameters to initialize the cache
+ */
+ private void init(final Context context) {
+ ApolloUtils.execute(false, new AsyncTask<Void, Void, Void>() {
+
+ @Override
+ protected Void doInBackground(final Void... unused) {
+ // Initialize the disk cahe in a background thread
+ initDiskCache(context);
+ return null;
+ }
+ }, (Void[])null);
+ // Set up the memory cache
+ initLruCache(context);
+ }
+
+ /**
+ * Initializes the disk cache. Note that this includes disk access so this
+ * should not be executed on the main/UI thread. By default an ImageCache
+ * does not initialize the disk cache when it is created, instead you should
+ * call initDiskCache() to initialize it on a background thread.
+ *
+ * @param context The {@link Context} to use
+ */
+ public void initDiskCache(final Context context) {
+ // Set up disk cache
+ if (mDiskCache == null || mDiskCache.isClosed()) {
+ File diskCacheDir = getDiskCacheDir(context, TAG);
+ if (diskCacheDir != null) {
+ if (!diskCacheDir.exists()) {
+ diskCacheDir.mkdirs();
+ }
+ if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
+ try {
+ mDiskCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
+ } catch (final IOException e) {
+ diskCacheDir = null;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Sets up the Lru cache
+ *
+ * @param context The {@link Context} to use
+ */
+ @SuppressLint("NewApi")
+ public void initLruCache(final Context context) {
+ final ActivityManager activityManager = (ActivityManager)context
+ .getSystemService(Context.ACTIVITY_SERVICE);
+ final int lruCacheSize = Math.round(MEM_CACHE_DIVIDER * activityManager.getMemoryClass()
+ * 1024 * 1024);
+ mLruCache = new MemoryCache(lruCacheSize);
+
+ // Release some memory as needed
+ if (ApolloUtils.hasICS()) {
+ context.registerComponentCallbacks(new ComponentCallbacks2() {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onTrimMemory(final int level) {
+ if (level >= TRIM_MEMORY_MODERATE) {
+ evictAll();
+ } else if (level >= TRIM_MEMORY_BACKGROUND) {
+ mLruCache.trimToSize(mLruCache.size() / 2);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLowMemory() {
+ // Nothing to do
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onConfigurationChanged(final Configuration newConfig) {
+ // Nothing to do
+ }
+ });
+ }
+ }
+
+ /**
+ * Find and return an existing ImageCache stored in a {@link RetainFragment}
+ * , if not found a new one is created using the supplied params and saved
+ * to a {@link RetainFragment}
+ *
+ * @param activity The calling {@link FragmentActivity}
+ * @return An existing retained ImageCache object or a new one if one did
+ * not exist
+ */
+ public static final ImageCache findOrCreateCache(final SherlockFragmentActivity activity) {
+
+ // Search for, or create an instance of the non-UI RetainFragment
+ final RetainFragment retainFragment = findOrCreateRetainFragment(activity
+ .getSupportFragmentManager());
+
+ // See if we already have an ImageCache stored in RetainFragment
+ ImageCache cache = (ImageCache)retainFragment.getObject();
+
+ // No existing ImageCache, create one and store it in RetainFragment
+ if (cache == null) {
+ cache = getInstance(activity);
+ retainFragment.setObject(cache);
+ }
+ return cache;
+ }
+
+ /**
+ * Locate an existing instance of this {@link Fragment} or if not found,
+ * create and add it using {@link FragmentManager}
+ *
+ * @param fm The {@link FragmentManager} to use
+ * @return The existing instance of the {@link Fragment} or the new instance
+ * if just created
+ */
+ public static final RetainFragment findOrCreateRetainFragment(final FragmentManager fm) {
+ // Check to see if we have retained the worker fragment
+ RetainFragment retainFragment = (RetainFragment)fm.findFragmentByTag(TAG);
+
+ // If not retained, we need to create and add it
+ if (retainFragment == null) {
+ retainFragment = new RetainFragment();
+ fm.beginTransaction().add(retainFragment, TAG).commit();
+ }
+ return retainFragment;
+ }
+
+ /**
+ * Adds a new image to the memory and disk caches
+ *
+ * @param data The key used to store the image
+ * @param bitmap The {@link Bitmap} to cache
+ */
+ public void addBitmapToCache(final String data, final Bitmap bitmap) {
+ if (data == null || bitmap == null) {
+ return;
+ }
+
+ // Add to memory cache
+ addBitmapToMemCache(data, bitmap);
+
+ // Add to disk cache
+ if (mDiskCache != null) {
+ final String key = hashKeyForDisk(data);
+ OutputStream out = null;
+ try {
+ final DiskLruCache.Snapshot snapshot = mDiskCache.get(key);
+ if (snapshot == null) {
+ final DiskLruCache.Editor editor = mDiskCache.edit(key);
+ if (editor != null) {
+ out = editor.newOutputStream(DISK_CACHE_INDEX);
+ bitmap.compress(COMPRESS_FORMAT, COMPRESS_QUALITY, out);
+ editor.commit();
+ out.close();
+ flush();
+ }
+ } else {
+ snapshot.getInputStream(DISK_CACHE_INDEX).close();
+ }
+ } catch (final IOException e) {
+ Log.e(TAG, "addBitmapToCache - " + e);
+ } finally {
+ try {
+ if (out != null) {
+ out.close();
+ out = null;
+ }
+ } catch (final IOException e) {
+ Log.e(TAG, "addBitmapToCache - " + e);
+ } catch (final IllegalStateException e) {
+ Log.e(TAG, "addBitmapToCache - " + e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Called to add a new image to the memory cache
+ *
+ * @param data The key identifier
+ * @param bitmap The {@link Bitmap} to cache
+ */
+ public void addBitmapToMemCache(final String data, final Bitmap bitmap) {
+ if (data == null || bitmap == null) {
+ return;
+ }
+ // Add to memory cache
+ if (getBitmapFromMemCache(data) == null) {
+ mLruCache.put(data, bitmap);
+ }
+ }
+
+ /**
+ * Fetches a cached image from the memory cache
+ *
+ * @param data Unique identifier for which item to get
+ * @return The {@link Bitmap} if found in cache, null otherwise
+ */
+ public final Bitmap getBitmapFromMemCache(final String data) {
+ if (data == null) {
+ return null;
+ }
+ if (mLruCache != null) {
+ final Bitmap lruBitmap = mLruCache.get(data);
+ if (lruBitmap != null) {
+ return lruBitmap;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Fetches a cached image from the disk cache
+ *
+ * @param data Unique identifier for which item to get
+ * @return The {@link Bitmap} if found in cache, null otherwise
+ */
+ public final Bitmap getBitmapFromDiskCache(final String data) {
+ if (data == null) {
+ return null;
+ }
+
+ // Check in the memory cache here to avoid going to the disk cache less
+ // often
+ if (getBitmapFromMemCache(data) != null) {
+ return getBitmapFromMemCache(data);
+ }
+
+ while (mPauseDiskAccess) {
+ // Pause for moment
+ }
+ final String key = hashKeyForDisk(data);
+ if (mDiskCache != null) {
+ InputStream inputStream = null;
+ try {
+ final DiskLruCache.Snapshot snapshot = mDiskCache.get(key);
+ if (snapshot != null) {
+ inputStream = snapshot.getInputStream(DISK_CACHE_INDEX);
+ if (inputStream != null) {
+ final Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
+ if (bitmap != null) {
+ return bitmap;
+ }
+ }
+ }
+ } catch (final IOException e) {
+ Log.e(TAG, "getBitmapFromDiskCache - " + e);
+ } finally {
+ try {
+ if (inputStream != null) {
+ inputStream.close();
+ }
+ } catch (final IOException e) {
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Tries to return a cached image from memory cache before fetching from the
+ * disk cache
+ *
+ * @param data Unique identifier for which item to get
+ * @return The {@link Bitmap} if found in cache, null otherwise
+ */
+ public final Bitmap getCachedBitmap(final String data) {
+ if (data == null) {
+ return null;
+ }
+ Bitmap cachedImage = getBitmapFromMemCache(data);
+ if (cachedImage == null) {
+ cachedImage = getBitmapFromDiskCache(data);
+ }
+ if (cachedImage != null) {
+ addBitmapToMemCache(data, cachedImage);
+ return cachedImage;
+ }
+ return null;
+ }
+
+ /**
+ * Tries to return the album art from memory cache and disk cache, before
+ * calling {@code #getArtworkFromFile(Context, String)} again
+ *
+ * @param context The {@link Context} to use
+ * @param data The name of the album art
+ * @param id The ID of the album to find artwork for
+ * @return The artwork for an album
+ */
+ public final Bitmap getCachedArtwork(final Context context, final String data, final String id) {
+ if (context == null || data == null) {
+ return null;
+ }
+ Bitmap cachedImage = getCachedBitmap(data);
+ if (cachedImage == null && id != null) {
+ cachedImage = getArtworkFromFile(context, id);
+ }
+ if (cachedImage != null) {
+ addBitmapToMemCache(data, cachedImage);
+ return cachedImage;
+ }
+ return null;
+ }
+
+ /**
+ * Used to fetch the artwork for an album locally from the user's device
+ *
+ * @param context The {@link Context} to use
+ * @param albumID The ID of the album to find artwork for
+ * @return The artwork for an album
+ */
+ public final Bitmap getArtworkFromFile(final Context context, final String albumId) {
+ if (TextUtils.isEmpty(albumId)) {
+ return null;
+ }
+ Bitmap artwork = null;
+ while (mPauseDiskAccess) {
+ // Pause for a moment
+ }
+ try {
+ final Uri uri = ContentUris.withAppendedId(mArtworkUri, Long.valueOf(albumId));
+ final ParcelFileDescriptor parcelFileDescriptor = context.getContentResolver()
+ .openFileDescriptor(uri, "r");
+ if (parcelFileDescriptor != null) {
+ final FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
+ artwork = BitmapFactory.decodeFileDescriptor(fileDescriptor);
+ }
+ } catch (final IllegalStateException e) {
+ // Log.e(TAG, "IllegalStateExcetpion - getArtworkFromFile - ", e);
+ } catch (final FileNotFoundException e) {
+ // Log.e(TAG, "FileNotFoundException - getArtworkFromFile - ", e);
+ } catch (final OutOfMemoryError evict) {
+ // Log.e(TAG, "OutOfMemoryError - getArtworkFromFile - ", evict);
+ evictAll();
+ }
+ return artwork;
+ }
+
+ /**
+ * flush() is called to synchronize up other methods that are accessing the
+ * cache first
+ */
+ public void flush() {
+ ApolloUtils.execute(false, new AsyncTask<Void, Void, Void>() {
+
+ @Override
+ protected Void doInBackground(final Void... unused) {
+ if (mDiskCache != null) {
+ try {
+ if (!mDiskCache.isClosed()) {
+ mDiskCache.flush();
+ }
+ } catch (final IOException e) {
+ Log.e(TAG, "flush - " + e);
+ }
+ }
+ return null;
+ }
+ }, (Void[])null);
+ }
+
+ /**
+ * Clears the disk and memory caches
+ */
+ public void clearCaches() {
+ ApolloUtils.execute(false, new AsyncTask<Void, Void, Void>() {
+
+ @Override
+ protected Void doInBackground(final Void... unused) {
+ // Clear the disk cache
+ try {
+ if (mDiskCache != null) {
+ mDiskCache.delete();
+ mDiskCache = null;
+ }
+ } catch (final IOException e) {
+ Log.e(TAG, "clearCaches - " + e);
+ }
+ // Clear the memory cache
+ evictAll();
+ return null;
+ }
+ }, (Void[])null);
+ }
+
+ /**
+ * Closes the disk cache associated with this ImageCache object. Note that
+ * this includes disk access so this should not be executed on the main/UI
+ * thread.
+ */
+ public void close() {
+ ApolloUtils.execute(false, new AsyncTask<Void, Void, Void>() {
+
+ @Override
+ protected Void doInBackground(final Void... unused) {
+ if (mDiskCache != null) {
+ try {
+ if (!mDiskCache.isClosed()) {
+ mDiskCache.close();
+ mDiskCache = null;
+ }
+ } catch (final IOException e) {
+ Log.e(TAG, "close - " + e);
+ }
+ }
+ return null;
+ }
+ }, (Void[])null);
+ }
+
+ /**
+ * Evicts all of the items from the memory cache and lets the system know
+ * now would be a good time to garbage collect
+ */
+ public void evictAll() {
+ if (mLruCache != null) {
+ mLruCache.evictAll();
+ }
+ System.gc();
+ }
+
+ /**
+ * @param key The key used to identify which cache entries to delete.
+ */
+ public void removeFromCache(final String key) {
+ if (key == null) {
+ return;
+ }
+ // Remove the Lru entry
+ if (mLruCache != null) {
+ mLruCache.remove(key);
+ }
+
+ try {
+ // Remove the disk entry
+ if (mDiskCache != null) {
+ mDiskCache.remove(hashKeyForDisk(key));
+ }
+ } catch (final IOException e) {
+ Log.e(TAG, "remove - " + e);
+ }
+ flush();
+ }
+
+ /**
+ * Used to temporarily pause the disk cache while the user is scrolling to
+ * improve scrolling.
+ *
+ * @param pause True to temporarily pause the disk cache, false otherwise.
+ */
+ public void setPauseDiskCache(final boolean pause) {
+ mPauseDiskAccess = pause;
+ }
+
+ /**
+ * @return True if the user is scrolling, false otherwise.
+ */
+ public boolean isScrolling() {
+ return mPauseDiskAccess;
+ }
+
+ /**
+ * Get a usable cache directory (external if available, internal otherwise)
+ *
+ * @param context The {@link Context} to use
+ * @param uniqueName A unique directory name to append to the cache
+ * directory
+ * @return The cache directory
+ */
+ public static final File getDiskCacheDir(final Context context, final String uniqueName) {
+ final String cachePath = Environment.MEDIA_MOUNTED.equals(Environment
+ .getExternalStorageState()) || !isExternalStorageRemovable() ? getExternalCacheDir(
+ context).getPath() : context.getCacheDir().getPath();
+
+ return new File(cachePath + File.separator + uniqueName);
+ }
+
+ /**
+ * Check if external storage is built-in or removable
+ *
+ * @return True if external storage is removable (like an SD card), false
+ * otherwise
+ */
+ @TargetApi(Build.VERSION_CODES.GINGERBREAD)
+ public static final boolean isExternalStorageRemovable() {
+ if (ApolloUtils.hasGingerbread()) {
+ return Environment.isExternalStorageRemovable();
+ }
+ return true;
+ }
+
+ /**
+ * Get the external app cache directory
+ *
+ * @param context The {@link Context} to use
+ * @return The external cache directory
+ */
+ public static final File getExternalCacheDir(final Context context) {
+ if (ApolloUtils.hasFroyo()) {
+ final File mCacheDir = context.getExternalCacheDir();
+ if (mCacheDir != null) {
+ return mCacheDir;
+ }
+ }
+
+ /* Before Froyo we need to construct the external cache dir ourselves */
+ final String mCacheDir = "/Android/data/" + context.getPackageName() + "/cache/";
+ return new File(Environment.getExternalStorageDirectory().getPath() + mCacheDir);
+ }
+
+ /**
+ * Check how much usable space is available at a given path.
+ *
+ * @param path The path to check
+ * @return The space available in bytes
+ */
+ @TargetApi(Build.VERSION_CODES.GINGERBREAD)
+ public static final long getUsableSpace(final File path) {
+ if (ApolloUtils.hasGingerbread()) {
+ return path.getUsableSpace();
+ }
+ final StatFs stats = new StatFs(path.getPath());
+ return (long)stats.getBlockSize() * (long)stats.getAvailableBlocks();
+ }
+
+ /**
+ * A hashing method that changes a string (like a URL) into a hash suitable
+ * for using as a disk filename.
+ *
+ * @param key The key used to store the file
+ */
+ public static final String hashKeyForDisk(final String key) {
+ String cacheKey;
+ try {
+ final MessageDigest digest = MessageDigest.getInstance("MD5");
+ digest.update(key.getBytes());
+ cacheKey = bytesToHexString(digest.digest());
+ } catch (final NoSuchAlgorithmException e) {
+ cacheKey = String.valueOf(key.hashCode());
+ }
+ return cacheKey;
+ }
+
+ /**
+ * http://stackoverflow.com/questions/332079
+ *
+ * @param bytes The bytes to convert.
+ * @return A {@link String} converted from the bytes of a hashable key used
+ * to store a filename on the disk, to hex digits.
+ */
+ private static final String bytesToHexString(final byte[] bytes) {
+ final StringBuilder builder = new StringBuilder();
+ for (final byte b : bytes) {
+ final String hex = Integer.toHexString(0xFF & b);
+ if (hex.length() == 1) {
+ builder.append('0');
+ }
+ builder.append(hex);
+ }
+ return builder.toString();
+ }
+
+ /**
+ * A simple non-UI Fragment that stores a single Object and is retained over
+ * configuration changes. In this sample it will be used to retain an
+ * {@link ImageCache} object.
+ */
+ public static final class RetainFragment extends SherlockFragment {
+
+ /**
+ * The object to be stored
+ */
+ private Object mObject;
+
+ /**
+ * Empty constructor as per the {@link Fragment} documentation
+ */
+ public RetainFragment() {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Make sure this Fragment is retained over a configuration change
+ setRetainInstance(true);
+ }
+
+ /**
+ * Store a single object in this {@link Fragment}
+ *
+ * @param object The object to store
+ */
+ public void setObject(final Object object) {
+ mObject = object;
+ }
+
+ /**
+ * Get the stored object
+ *
+ * @return The stored object
+ */
+ public Object getObject() {
+ return mObject;
+ }
+ }
+
+ /**
+ * Used to cache images via {@link LruCache}.
+ */
+ public static final class MemoryCache extends LruCache<String, Bitmap> {
+
+ /**
+ * Constructor of <code>MemoryCache</code>
+ *
+ * @param maxSize The allowed size of the {@link LruCache}
+ */
+ public MemoryCache(final int maxSize) {
+ super(maxSize);
+ }
+
+ /**
+ * Get the size in bytes of a bitmap.
+ */
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
+ public static final int getBitmapSize(final Bitmap bitmap) {
+ if (ApolloUtils.hasHoneycombMR1()) {
+ return bitmap.getByteCount();
+ }
+ /* Pre HC-MR1 */
+ return bitmap.getRowBytes() * bitmap.getHeight();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected int sizeOf(final String paramString, final Bitmap paramBitmap) {
+ return getBitmapSize(paramBitmap);
+ }
+
+ }
+
+}
diff --git a/src/com/andrew/apollo/cache/ImageFetcher.java b/src/com/andrew/apollo/cache/ImageFetcher.java
new file mode 100644
index 0000000..b440897
--- /dev/null
+++ b/src/com/andrew/apollo/cache/ImageFetcher.java
@@ -0,0 +1,410 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.cache;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.text.TextUtils;
+import android.widget.ImageView;
+
+import com.andrew.apollo.Config;
+import com.andrew.apollo.MusicPlaybackService;
+import com.andrew.apollo.lastfm.Album;
+import com.andrew.apollo.lastfm.Artist;
+import com.andrew.apollo.lastfm.Image;
+import com.andrew.apollo.lastfm.ImageSize;
+import com.andrew.apollo.lastfm.PaginatedResult;
+import com.andrew.apollo.utils.ApolloUtils;
+import com.andrew.apollo.utils.MusicUtils;
+import com.andrew.apollo.utils.PreferenceUtils;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.Iterator;
+
+/**
+ * A subclass of {@link ImageWorker} that fetches images from a URL.
+ */
+public class ImageFetcher extends ImageWorker {
+
+ public static final int IO_BUFFER_SIZE_BYTES = 1024;
+
+ private static final int DEFAULT_MAX_IMAGE_HEIGHT = 1024;
+
+ private static final int DEFAULT_MAX_IMAGE_WIDTH = 1024;
+
+ private static final String DEFAULT_HTTP_CACHE_DIR = "http"; //$NON-NLS-1$
+
+ private static ImageFetcher sInstance = null;
+
+ /**
+ * Creates a new instance of {@link ImageFetcher}.
+ *
+ * @param context The {@link Context} to use.
+ */
+ public ImageFetcher(final Context context) {
+ super(context);
+ }
+
+ /**
+ * Used to create a singleton of the image fetcher
+ *
+ * @param context The {@link Context} to use
+ * @return A new instance of this class.
+ */
+ public static final ImageFetcher getInstance(final Context context) {
+ if (sInstance == null) {
+ sInstance = new ImageFetcher(context.getApplicationContext());
+ }
+ return sInstance;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected Bitmap processBitmap(final String url) {
+ if (url == null) {
+ return null;
+ }
+ final File file = downloadBitmapToFile(mContext, url, DEFAULT_HTTP_CACHE_DIR);
+ if (file != null) {
+ // Return a sampled down version
+ final Bitmap bitmap = decodeSampledBitmapFromFile(file.toString());
+ file.delete();
+ if (bitmap != null) {
+ return bitmap;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected String processImageUrl(final String artistName, final String albumName,
+ final ImageType imageType) {
+ switch (imageType) {
+ case ARTIST:
+ if (!TextUtils.isEmpty(artistName)) {
+ if (PreferenceUtils.getInstace(mContext).downloadMissingArtistImages()) {
+ final PaginatedResult<Image> paginatedResult = Artist.getImages(mContext,
+ artistName);
+ if (paginatedResult != null) {
+ final Iterator<Image> iterator = paginatedResult.pageResults.iterator();
+ while (iterator.hasNext()) {
+ final Image temp = iterator.next();
+ final String url = temp.getImageURL(ImageSize.EXTRALARGE);
+ if (url != null) {
+ return url;
+ }
+ }
+ }
+ }
+ }
+ break;
+ case ALBUM:
+ if (!TextUtils.isEmpty(artistName) && !TextUtils.isEmpty(albumName)) {
+ if (PreferenceUtils.getInstace(mContext).downloadMissingArtwork()) {
+ final Artist correction = Artist.getCorrection(mContext, artistName);
+ if (correction != null) {
+ final Album album = Album.getInfo(mContext, correction.getName(),
+ albumName);
+ if (album != null) {
+ final String url = album.getImageURL(ImageSize.LARGE);
+ if (url != null) {
+ return url;
+ }
+ }
+ }
+ }
+ }
+ break;
+ default:
+ break;
+ }
+ return null;
+ }
+
+ /**
+ * Used to fetch album images.
+ */
+ public void loadAlbumImage(final String artistName, final String albumName, final String index,
+ final ImageView imageView) {
+ loadImage(albumName + Config.ALBUM_ART_SUFFIX, artistName, albumName, index, imageView,
+ ImageType.ALBUM);
+ }
+
+ /**
+ * Used to fetch the current artwork.
+ */
+ public void loadCurrentArtwork(final ImageView imageView) {
+ loadImage(MusicUtils.getAlbumName() + Config.ALBUM_ART_SUFFIX, MusicUtils.getArtistName(),
+ MusicUtils.getAlbumName(), String.valueOf(MusicUtils.getCurrentAlbumId()),
+ imageView, ImageType.ALBUM);
+ }
+
+ /**
+ * Used to fetch artist images.
+ */
+ public void loadArtistImage(final String key, final ImageView imageView) {
+ loadImage(key, key, null, null, imageView, ImageType.ARTIST);
+ }
+
+ /**
+ * Used to fetch the current artist image.
+ */
+ public void loadCurrentArtistImage(final ImageView imageView) {
+ loadImage(MusicUtils.getArtistName(), MusicUtils.getArtistName(), null, null, imageView,
+ ImageType.ARTIST);
+ }
+
+ /**
+ * @param pause True to temporarily pause the disk cache, false otherwise.
+ */
+ public void setPauseDiskCache(final boolean pause) {
+ if (mImageCache != null) {
+ mImageCache.setPauseDiskCache(pause);
+ }
+ }
+
+ /**
+ * Clears the disk and memory caches
+ */
+ public void clearCaches() {
+ if (mImageCache != null) {
+ mImageCache.clearCaches();
+ }
+ }
+
+ /**
+ * @param key The key used to find the image to remove
+ */
+ public void removeFromCache(final String key) {
+ if (mImageCache != null) {
+ mImageCache.removeFromCache(key);
+ }
+ }
+
+ /**
+ * @param key The key used to find the image to return
+ */
+ public Bitmap getCachedBitmap(final String key) {
+ if (mImageCache != null) {
+ return mImageCache.getCachedBitmap(key);
+ }
+ return getDefaultArtwork();
+ }
+
+ /**
+ * @param key The key used to find the album art to return
+ */
+ public Bitmap getCachedArtwork(final String key) {
+ if (mImageCache != null) {
+ return mImageCache.getCachedArtwork(mContext, key + Config.ALBUM_ART_SUFFIX,
+ String.valueOf(MusicUtils.getIdForAlbum(mContext, key)));
+ }
+ return getDefaultArtwork();
+ }
+
+ /**
+ * Finds cached or downloads album art. Used in {@link MusicPlaybackService}
+ * to set the current album art in the notification and lock screen
+ *
+ * @param albumName The name of the current album
+ * @param albumId The ID of the current album
+ * @param artistName The album artist in case we should have to download
+ * missing artwork
+ * @return The album art as an {@link Bitmap}
+ */
+ public Bitmap getArtwork(final String albumName, final String albumId, final String artistName) {
+ // Check the disk cache
+ Bitmap artwork = null;
+
+ if (artwork == null && albumName != null && mImageCache != null) {
+ artwork = mImageCache.getBitmapFromDiskCache(albumName + Config.ALBUM_ART_SUFFIX);
+ }
+ if (artwork == null && albumId != null && mImageCache != null) {
+ // Check for local artwork
+ artwork = mImageCache.getArtworkFromFile(mContext, albumId);
+ }
+ // if (artwork == null && artistName != null && albumName != null) {
+ // // Download missing artwork
+ // artwork = processBitmap(processImageUrl(artistName, albumName,
+ // ImageType.ALBUM));
+ // }
+ if (artwork != null) {
+ return artwork;
+ }
+ return getDefaultArtwork();
+ }
+
+ /**
+ * Download a {@link Bitmap} from a URL, write it to a disk and return the
+ * File pointer. This implementation uses a simple disk cache.
+ *
+ * @param context The context to use
+ * @param urlString The URL to fetch
+ * @return A {@link File} pointing to the fetched bitmap
+ */
+ public static final File downloadBitmapToFile(final Context context, final String urlString,
+ final String uniqueName) {
+ final File cacheDir = ImageCache.getDiskCacheDir(context, uniqueName);
+
+ if (!cacheDir.exists()) {
+ cacheDir.mkdir();
+ }
+
+ disableConnectionReuseIfNecessary();
+ HttpURLConnection urlConnection = null;
+ BufferedOutputStream out = null;
+
+ try {
+ final File tempFile = File.createTempFile("bitmap", null, cacheDir); //$NON-NLS-1$
+
+ final URL url = new URL(urlString);
+ urlConnection = (HttpURLConnection)url.openConnection();
+ if (urlConnection.getResponseCode() != HttpURLConnection.HTTP_OK) {
+ return null;
+ }
+ final InputStream in = new BufferedInputStream(urlConnection.getInputStream(),
+ IO_BUFFER_SIZE_BYTES);
+ out = new BufferedOutputStream(new FileOutputStream(tempFile), IO_BUFFER_SIZE_BYTES);
+
+ int oneByte;
+ while ((oneByte = in.read()) != -1) {
+ out.write(oneByte);
+ }
+ return tempFile;
+ } catch (final IOException ignored) {
+ } finally {
+ if (urlConnection != null) {
+ urlConnection.disconnect();
+ }
+ if (out != null) {
+ try {
+ out.close();
+ } catch (final IOException ignored) {
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Decode and sample down a {@link Bitmap} from a file to the requested
+ * width and height.
+ *
+ * @param filename The full path of the file to decode
+ * @param reqWidth The requested width of the resulting bitmap
+ * @param reqHeight The requested height of the resulting bitmap
+ * @return A {@link Bitmap} sampled down from the original with the same
+ * aspect ratio and dimensions that are equal to or greater than the
+ * requested width and height
+ */
+ public static Bitmap decodeSampledBitmapFromFile(final String filename) {
+
+ // First decode with inJustDecodeBounds=true to check dimensions
+ final BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(filename, options);
+
+ // Calculate inSampleSize
+ options.inSampleSize = calculateInSampleSize(options, DEFAULT_MAX_IMAGE_WIDTH,
+ DEFAULT_MAX_IMAGE_HEIGHT);
+
+ // Decode bitmap with inSampleSize set
+ options.inJustDecodeBounds = false;
+ return BitmapFactory.decodeFile(filename, options);
+ }
+
+ /**
+ * Calculate an inSampleSize for use in a
+ * {@link android.graphics.BitmapFactory.Options} object when decoding
+ * bitmaps using the decode* methods from {@link BitmapFactory}. This
+ * implementation calculates the closest inSampleSize that will result in
+ * the final decoded bitmap having a width and height equal to or larger
+ * than the requested width and height. This implementation does not ensure
+ * a power of 2 is returned for inSampleSize which can be faster when
+ * decoding but results in a larger bitmap which isn't as useful for caching
+ * purposes.
+ *
+ * @param options An options object with out* params already populated (run
+ * through a decode* method with inJustDecodeBounds==true
+ * @param reqWidth The requested width of the resulting bitmap
+ * @param reqHeight The requested height of the resulting bitmap
+ * @return The value to be used for inSampleSize
+ */
+ public static final int calculateInSampleSize(final BitmapFactory.Options options,
+ final int reqWidth, final int reqHeight) {
+ /* Raw height and width of image */
+ final int height = options.outHeight;
+ final int width = options.outWidth;
+ int inSampleSize = 1;
+
+ if (height > reqHeight || width > reqWidth) {
+ if (width > height) {
+ inSampleSize = Math.round((float)height / (float)reqHeight);
+ } else {
+ inSampleSize = Math.round((float)width / (float)reqWidth);
+ }
+
+ // This offers some additional logic in case the image has a strange
+ // aspect ratio. For example, a panorama may have a much larger
+ // width than height. In these cases the total pixels might still
+ // end up being too large to fit comfortably in memory, so we should
+ // be more aggressive with sample down the image (=larger
+ // inSampleSize).
+
+ final float totalPixels = width * height;
+
+ /* More than 2x the requested pixels we'll sample down further */
+ final float totalReqPixelsCap = reqWidth * reqHeight * 2;
+
+ while (totalPixels / (inSampleSize * inSampleSize) > totalReqPixelsCap) {
+ inSampleSize++;
+ }
+ }
+ return inSampleSize;
+ }
+
+ /**
+ * Workaround for bug pre-Froyo, see here for more info:
+ * http://android-developers.blogspot.com/2011/09/androids-http-clients.html
+ */
+ public static void disableConnectionReuseIfNecessary() {
+ /* HTTP connection reuse which was buggy pre-froyo */
+ if (hasHttpConnectionBug()) {
+ System.setProperty("http.keepAlive", "false"); //$NON-NLS-1$
+ }
+ }
+
+ /**
+ * Check if OS version has a http URLConnection bug. See here for more
+ * information:
+ * http://android-developers.blogspot.com/2011/09/androids-http-clients.html
+ *
+ * @return true if this OS version is affected, false otherwise
+ */
+ public static final boolean hasHttpConnectionBug() {
+ return !ApolloUtils.hasFroyo();
+ }
+
+}
diff --git a/src/com/andrew/apollo/cache/ImageWorker.java b/src/com/andrew/apollo/cache/ImageWorker.java
new file mode 100644
index 0000000..1ac403b
--- /dev/null
+++ b/src/com/andrew/apollo/cache/ImageWorker.java
@@ -0,0 +1,454 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.cache;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.TransitionDrawable;
+import android.os.AsyncTask;
+import android.widget.ImageView;
+
+import com.andrew.apollo.R;
+import com.andrew.apollo.utils.ApolloUtils;
+import com.andrew.apollo.utils.ThemeUtils;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * This class wraps up completing some arbitrary long running work when loading
+ * a {@link Bitmap} to an {@link ImageView}. It handles things like using a
+ * memory and disk cache, running the work in a background thread and setting a
+ * placeholder image.
+ */
+public abstract class ImageWorker {
+
+ /**
+ * Default transition drawable fade time
+ */
+ private static final int FADE_IN_TIME = 200;
+
+ /**
+ * Default artwork
+ */
+ private final BitmapDrawable mDefaultArtwork;
+
+ /**
+ * The resources to use
+ */
+ private final Resources mResources;
+
+ /**
+ * First layer of the transition drawable
+ */
+ private final ColorDrawable mCurrentDrawable;
+
+ /**
+ * Layer drawable used to cross fade the result from the worker
+ */
+ private final Drawable[] mArrayDrawable;
+
+ /**
+ * Default album art
+ */
+ private final Bitmap mDefault;
+
+ /**
+ * The Context to use
+ */
+ protected Context mContext;
+
+ /**
+ * Disk and memory caches
+ */
+ protected ImageCache mImageCache;
+
+ /**
+ * Constructor of <code>ImageWorker</code>
+ *
+ * @param context The {@link Context} to use
+ */
+ protected ImageWorker(final Context context) {
+ mContext = context.getApplicationContext();
+ mResources = mContext.getResources();
+ // Create the default artwork
+ final ThemeUtils theme = new ThemeUtils(context);
+ mDefault = ((BitmapDrawable)theme.getDrawable("default_artwork")).getBitmap();
+ mDefaultArtwork = new BitmapDrawable(mResources, mDefault);
+ // No filter and no dither makes things much quicker
+ mDefaultArtwork.setFilterBitmap(false);
+ mDefaultArtwork.setDither(false);
+ // Create the transparent layer for the transition drawable
+ mCurrentDrawable = new ColorDrawable(mResources.getColor(R.color.transparent));
+ // A transparent image (layer 0) and the new result (layer 1)
+ mArrayDrawable = new Drawable[2];
+ mArrayDrawable[0] = mCurrentDrawable;
+ // XXX The second layer is set in the worker task.
+ }
+
+ /**
+ * Set the {@link ImageCache} object to use with this ImageWorker.
+ *
+ * @param cacheCallback new {@link ImageCache} object.
+ */
+ public void setImageCache(final ImageCache cacheCallback) {
+ mImageCache = cacheCallback;
+ }
+
+ /**
+ * @return True if the user is scrolling, false otherwise
+ */
+ public boolean isScrolling() {
+ if (mImageCache != null) {
+ return mImageCache.isScrolling();
+ }
+ return true;
+ }
+
+ /**
+ * Closes the disk cache associated with this ImageCache object. Note that
+ * this includes disk access so this should not be executed on the main/UI
+ * thread.
+ */
+ public void close() {
+ if (mImageCache != null) {
+ mImageCache.close();
+ }
+ }
+
+ /**
+ * flush() is called to synchronize up other methods that are accessing the
+ * cache first
+ */
+ public void flush() {
+ if (mImageCache != null) {
+ mImageCache.flush();
+ }
+ }
+
+ /**
+ * Adds a new image to the memory and disk caches
+ *
+ * @param data The key used to store the image
+ * @param bitmap The {@link Bitmap} to cache
+ */
+ public void addBitmapToCache(final String key, final Bitmap bitmap) {
+ if (mImageCache != null) {
+ mImageCache.addBitmapToCache(key, bitmap);
+ }
+ }
+
+ /**
+ * @return The deafult artwork
+ */
+ public Bitmap getDefaultArtwork() {
+ return mDefault;
+ }
+
+ /**
+ * The actual {@link AsyncTask} that will process the image.
+ */
+ private final class BitmapWorkerTask extends AsyncTask<String, Void, TransitionDrawable> {
+
+ /**
+ * The {@link ImageView} used to set the result
+ */
+ private final WeakReference<ImageView> mImageReference;
+
+ /**
+ * Type of URL to download
+ */
+ private final ImageType mImageType;
+
+ /**
+ * The key used to store cached entries
+ */
+ private String mKey;
+
+ /**
+ * Artist name param
+ */
+ private String mArtistName;
+
+ /**
+ * Album name parm
+ */
+ private String mAlbumName;
+
+ /**
+ * The album ID used to find the corresponding artwork
+ */
+ private String mAlbumId;
+
+ /**
+ * The URL of an image to download
+ */
+ private String mUrl;
+
+ /**
+ * Constructor of <code>BitmapWorkerTask</code>
+ *
+ * @param imageView The {@link ImageView} to use.
+ * @param imageType The type of image URL to fetch for.
+ */
+ @SuppressWarnings("deprecation")
+ public BitmapWorkerTask(final ImageView imageView, final ImageType imageType) {
+ imageView.setBackgroundDrawable(mDefaultArtwork);
+ mImageReference = new WeakReference<ImageView>(imageView);
+ mImageType = imageType;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected TransitionDrawable doInBackground(final String... params) {
+ // Define the key
+ mKey = params[0];
+
+ // The result
+ Bitmap bitmap = null;
+
+ // Wait here if work is paused and the task is not cancelled. This
+ // shouldn't even occur because this isn't executing while the user
+ // is scrolling, but just in case.
+ while (isScrolling() && !isCancelled()) {
+ cancel(true);
+ }
+
+ // First, check the disk cache for the image
+ if (mKey != null && mImageCache != null && !isCancelled()
+ && getAttachedImageView() != null) {
+ bitmap = mImageCache.getCachedBitmap(mKey);
+ }
+
+ // Define the album id now
+ mAlbumId = params[3];
+
+ // Second, if we're fetching artwork, check the device for the image
+ if (bitmap == null && mImageType.equals(ImageType.ALBUM) && mAlbumId != null
+ && mKey != null && !isCancelled() && getAttachedImageView() != null
+ && mImageCache != null) {
+ bitmap = mImageCache.getCachedArtwork(mContext, mKey, mAlbumId);
+ }
+
+ // Third, by now we need to download the image
+ if (bitmap == null && ApolloUtils.isOnline(mContext) && !isCancelled()
+ && getAttachedImageView() != null) {
+ // Now define what the artist name, album name, and url are.
+ mArtistName = params[1];
+ mAlbumName = params[2];
+ mUrl = processImageUrl(mArtistName, mAlbumName, mImageType);
+ if (mUrl != null) {
+ bitmap = processBitmap(mUrl);
+ }
+ }
+
+ // Fourth, add the new image to the cache
+ if (bitmap != null && mKey != null && mImageCache != null) {
+ addBitmapToCache(mKey, bitmap);
+ }
+
+ // Add the second layer to the transiation drawable
+ if (bitmap != null) {
+ final BitmapDrawable layerTwo = new BitmapDrawable(mResources, bitmap);
+ layerTwo.setFilterBitmap(false);
+ layerTwo.setDither(false);
+ mArrayDrawable[1] = layerTwo;
+
+ // Finally, return the image
+ final TransitionDrawable result = new TransitionDrawable(mArrayDrawable);
+ result.setCrossFadeEnabled(true);
+ result.startTransition(FADE_IN_TIME);
+ return result;
+ }
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onPostExecute(TransitionDrawable result) {
+ if (isCancelled()) {
+ result = null;
+ }
+ final ImageView imageView = getAttachedImageView();
+ if (result != null && imageView != null) {
+ imageView.setImageDrawable(result);
+ }
+ }
+
+ /**
+ * @return The {@link ImageView} associated with this task as long as
+ * the ImageView's task still points to this task as well.
+ * Returns null otherwise.
+ */
+ private final ImageView getAttachedImageView() {
+ final ImageView imageView = mImageReference.get();
+ final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
+ if (this == bitmapWorkerTask) {
+ return imageView;
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Calls {@code cancel()} in the worker task
+ *
+ * @param imageView the {@link ImageView} to use
+ */
+ public static final void cancelWork(final ImageView imageView) {
+ final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
+ if (bitmapWorkerTask != null) {
+ bitmapWorkerTask.cancel(true);
+ }
+ }
+
+ /**
+ * Returns true if the current work has been canceled or if there was no
+ * work in progress on this image view. Returns false if the work in
+ * progress deals with the same data. The work is not stopped in that case.
+ */
+ public static final boolean executePotentialWork(final Object data, final ImageView imageView) {
+ final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
+ if (bitmapWorkerTask != null) {
+ final Object bitmapData = bitmapWorkerTask.mKey;
+ if (bitmapData == null || !bitmapData.equals(data)) {
+ bitmapWorkerTask.cancel(true);
+ } else {
+ // The same work is already in progress
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Used to determine if the current image drawable has an instance of
+ * {@link BitmapWorkerTask}
+ *
+ * @param imageView Any {@link ImageView}.
+ * @return Retrieve the currently active work task (if any) associated with
+ * this {@link ImageView}. null if there is no such task.
+ */
+ private static final BitmapWorkerTask getBitmapWorkerTask(final ImageView imageView) {
+ if (imageView != null) {
+ final Drawable drawable = imageView.getDrawable();
+ if (drawable instanceof AsyncDrawable) {
+ final AsyncDrawable asyncDrawable = (AsyncDrawable)drawable;
+ return asyncDrawable.getBitmapWorkerTask();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * A custom {@link BitmapDrawable} that will be attached to the
+ * {@link ImageView} while the work is in progress. Contains a reference to
+ * the actual worker task, so that it can be stopped if a new binding is
+ * required, and makes sure that only the last started worker process can
+ * bind its result, independently of the finish order.
+ */
+ private static final class AsyncDrawable extends ColorDrawable {
+
+ private final WeakReference<BitmapWorkerTask> mBitmapWorkerTaskReference;
+
+ /**
+ * Constructor of <code>AsyncDrawable</code>
+ */
+ public AsyncDrawable(final Resources res, final Bitmap bitmap,
+ final BitmapWorkerTask mBitmapWorkerTask) {
+ super(Color.TRANSPARENT);
+ mBitmapWorkerTaskReference = new WeakReference<BitmapWorkerTask>(mBitmapWorkerTask);
+ }
+
+ /**
+ * @return The {@link BitmapWorkerTask} associated with this drawable
+ */
+ public BitmapWorkerTask getBitmapWorkerTask() {
+ return mBitmapWorkerTaskReference.get();
+ }
+ }
+
+ /**
+ * Called to fetch the artist or ablum art.
+ *
+ * @param key The unique identifier for the image.
+ * @param artistName The artist name for the Last.fm API.
+ * @param albumName The album name for the Last.fm API.
+ * @param albumId The album art index, to check for missing artwork.
+ * @param imageView The {@link ImageView} used to set the cached
+ * {@link Bitmap}.
+ * @param imageType The type of image URL to fetch for.
+ */
+ protected void loadImage(final String key, final String artistName, final String albumName,
+ final String albumId, final ImageView imageView, final ImageType imageType) {
+ if (key == null || mImageCache == null || imageView == null) {
+ return;
+ }
+ // First, check the memory for the image
+ final Bitmap lruBitmap = mImageCache.getBitmapFromMemCache(key);
+ if (lruBitmap != null && imageView != null) {
+ // Bitmap found in memory cache
+ imageView.setImageBitmap(lruBitmap);
+ } else if (executePotentialWork(key, imageView) && imageView != null && !isScrolling()) {
+ // Otherwise run the worker task
+ final BitmapWorkerTask bitmapWorkerTask = new BitmapWorkerTask(imageView, imageType);
+ final AsyncDrawable asyncDrawable = new AsyncDrawable(mResources, mDefault,
+ bitmapWorkerTask);
+ imageView.setImageDrawable(asyncDrawable);
+ // Don't execute the BitmapWorkerTask while scrolling
+ if (isScrolling()) {
+ cancelWork(imageView);
+ } else {
+ ApolloUtils.execute(false, bitmapWorkerTask, key, artistName, albumName, albumId);
+ }
+ }
+ }
+
+ /**
+ * Subclasses should override this to define any processing or work that
+ * must happen to produce the final {@link Bitmap}. This will be executed in
+ * a background thread and be long running.
+ *
+ * @param key The key to identify which image to process, as provided by
+ * {@link ImageWorker#loadImage(mKey, ImageView)}
+ * @return The processed {@link Bitmap}.
+ */
+ protected abstract Bitmap processBitmap(String key);
+
+ /**
+ * Subclasses should override this to define any processing or work that
+ * must happen to produce the URL needed to fetch the final {@link Bitmap}.
+ *
+ * @param artistName The artist name param used in the Last.fm API.
+ * @param albumName The album name param used in the Last.fm API.
+ * @param imageType The type of image URL to fetch for.
+ * @return The image URL for an artist image or album image.
+ */
+ protected abstract String processImageUrl(String artistName, String albumName,
+ ImageType imageType);
+
+ /**
+ * Used to define what type of image URL to fetch for, artist or album.
+ */
+ public enum ImageType {
+ ARTIST, ALBUM;
+ }
+
+}
diff --git a/src/com/andrew/apollo/cache/LruCache.java b/src/com/andrew/apollo/cache/LruCache.java
new file mode 100644
index 0000000..3b59a41
--- /dev/null
+++ b/src/com/andrew/apollo/cache/LruCache.java
@@ -0,0 +1,333 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.andrew.apollo.cache;
+
+// NOTE: upstream of this class is android.util.LruCache, changes below
+// expose trimToSize() to be called externally.
+
+import android.annotation.SuppressLint;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Static library version of {@link android.util.LruCache}. Used to write apps
+ * that run on API levels prior to 12. When running on API level 12 or above,
+ * this implementation is still used; it does not try to switch to the
+ * framework's implementation. See the framework SDK documentation for a class
+ * overview.
+ */
+public class LruCache<K, V> {
+
+ private final LinkedHashMap<K, V> map;
+
+ private final int maxSize;
+
+ /** Size of this cache in units. Not necessarily the number of elements. */
+ private int size;
+
+ private int putCount;
+
+ private int createCount;
+
+ private int evictionCount;
+
+ private int hitCount;
+
+ private int missCount;
+
+ /**
+ * @param maxSize for caches that do not override {@link #sizeOf}, this is
+ * the maximum number of entries in the cache. For all other
+ * caches, this is the maximum sum of the sizes of the entries in
+ * this cache.
+ */
+ public LruCache(final int maxSize) {
+ if (maxSize <= 0) {
+ throw new IllegalArgumentException("maxSize <= 0");
+ }
+ this.maxSize = maxSize;
+ this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
+ }
+
+ /**
+ * Returns the value for {@code key} if it exists in the cache or can be
+ * created by {@code #create}. If a value was returned, it is moved to the
+ * head of the queue. This returns null if a value is not cached and cannot
+ * be created.
+ */
+ public final V get(final K key) {
+ if (key == null) {
+ throw new NullPointerException("key == null");
+ }
+
+ V mapValue;
+ synchronized (this) {
+ mapValue = map.get(key);
+ if (mapValue != null) {
+ this.hitCount++;
+ return mapValue;
+ }
+ this.missCount++;
+ }
+
+ /*
+ * Attempt to create a value. This may take a long time, and the map may
+ * be different when create() returns. If a conflicting value was added
+ * to the map while create() was working, we leave that value in the map
+ * and release the created value.
+ */
+
+ final V createdValue = create(key);
+ if (createdValue == null) {
+ return null;
+ }
+
+ synchronized (this) {
+ this.createCount++;
+ mapValue = map.put(key, createdValue);
+
+ if (mapValue != null) {
+ /* There was a conflict so undo that last put */
+ this.map.put(key, mapValue);
+ } else {
+ this.size += safeSizeOf(key, createdValue);
+ }
+ }
+
+ if (mapValue != null) {
+ entryRemoved(false, key, createdValue, mapValue);
+ return mapValue;
+ } else {
+ trimToSize(maxSize);
+ return createdValue;
+ }
+ }
+
+ /**
+ * Caches {@code value} for {@code key}. The value is moved to the head of
+ * the queue.
+ *
+ * @return the previous value mapped by {@code key}.
+ */
+ public final V put(final K key, final V value) {
+ if (key == null || value == null) {
+ throw new NullPointerException("key == null || value == null");
+ }
+
+ V previous;
+ synchronized (this) {
+ this.putCount++;
+ this.size += safeSizeOf(key, value);
+ previous = this.map.put(key, value);
+ if (previous != null) {
+ this.size -= safeSizeOf(key, previous);
+ }
+ }
+
+ if (previous != null) {
+ entryRemoved(false, key, previous, value);
+ }
+
+ trimToSize(maxSize);
+ return previous;
+ }
+
+ /**
+ * @param maxSize the maximum size of the cache before returning. May be -1
+ * to evict even 0-sized elements.
+ */
+ public void trimToSize(final int maxSize) {
+ while (true) {
+ K key;
+ V value;
+ synchronized (this) {
+ if (this.size < 0 || this.map.isEmpty() && size != 0) {
+ throw new IllegalStateException(getClass().getName()
+ + ".sizeOf() is reporting inconsistent results!");
+ }
+
+ if (this.size <= maxSize || this.map.isEmpty()) {
+ break;
+ }
+
+ final Map.Entry<K, V> toEvict = this.map.entrySet().iterator().next();
+ key = toEvict.getKey();
+ value = toEvict.getValue();
+ this.map.remove(key);
+ this.size -= safeSizeOf(key, value);
+ this.evictionCount++;
+ }
+
+ entryRemoved(true, key, value, null);
+ }
+ }
+
+ /**
+ * Removes the entry for {@code key} if it exists.
+ *
+ * @return the previous value mapped by {@code key}.
+ */
+ public final V remove(final K key) {
+ if (key == null) {
+ throw new NullPointerException("key == null");
+ }
+
+ V previous;
+ synchronized (this) {
+ previous = this.map.remove(key);
+ if (previous != null) {
+ this.size -= safeSizeOf(key, previous);
+ }
+ }
+
+ if (previous != null) {
+ entryRemoved(false, key, previous, null);
+ }
+
+ return previous;
+ }
+
+ /**
+ * Called for entries that have been evicted or removed. This method is
+ * invoked when a value is evicted to make space, removed by a call to
+ * {@link #remove}, or replaced by a call to {@link #put}. The default
+ * implementation does nothing.
+ * <p>
+ * The method is called without synchronization: other threads may access
+ * the cache while this method is executing.
+ *
+ * @param evicted true if the entry is being removed to make space, false if
+ * the removal was caused by a {@link #put} or {@link #remove}.
+ * @param newValue the new value for {@code key}, if it exists. If non-null,
+ * this removal was caused by a {@link #put}. Otherwise it was
+ * caused by an eviction or a {@link #remove}.
+ */
+ protected void entryRemoved(final boolean evicted, final K key, final V oldValue,
+ final V newValue) {
+ }
+
+ /**
+ * Called after a cache miss to compute a value for the corresponding key.
+ * Returns the computed value or null if no value can be computed. The
+ * default implementation returns null.
+ * <p>
+ * The method is called without synchronization: other threads may access
+ * the cache while this method is executing.
+ * <p>
+ * If a value for {@code key} exists in the cache when this method returns,
+ * the created value will be released with {@link #entryRemoved} and
+ * discarded. This can occur when multiple threads request the same key at
+ * the same time (causing multiple values to be created), or when one thread
+ * calls {@link #put} while another is creating a value for the same key.
+ */
+ protected V create(final K key) {
+ return null;
+ }
+
+ private int safeSizeOf(final K key, final V value) {
+ final int result = sizeOf(key, value);
+ if (result < 0) {
+ throw new IllegalStateException("Negative size: " + key + "=" + value);
+ }
+ return result;
+ }
+
+ /**
+ * Returns the size of the entry for {@code key} and {@code value} in
+ * user-defined units. The default implementation returns 1 so that size is
+ * the number of entries and max size is the maximum number of entries.
+ * <p>
+ * An entry's size must not change while it is in the cache.
+ */
+ protected int sizeOf(final K key, final V value) {
+ return 1;
+ }
+
+ /**
+ * Clear the cache, calling {@link #entryRemoved} on each removed entry.
+ */
+ public final void evictAll() {
+ trimToSize(-1); // -1 will evict 0-sized elements
+ }
+
+ /**
+ * For caches that do not override {@link #sizeOf}, this returns the number
+ * of entries in the cache. For all other caches, this returns the sum of
+ * the sizes of the entries in this cache.
+ */
+ public synchronized final int size() {
+ return this.size;
+ }
+
+ /**
+ * For caches that do not override {@link #sizeOf}, this returns the maximum
+ * number of entries in the cache. For all other caches, this returns the
+ * maximum sum of the sizes of the entries in this cache.
+ */
+ public synchronized final int maxSize() {
+ return this.maxSize;
+ }
+
+ /**
+ * Returns the number of times {@link #get} returned a value.
+ */
+ public synchronized final int hitCount() {
+ return this.hitCount;
+ }
+
+ /**
+ * Returns the number of times {@link #get} returned null or required a new
+ * value to be created.
+ */
+ public synchronized final int missCount() {
+ return this.missCount;
+ }
+
+ /**
+ * Returns the number of times {@link #create(Object)} returned a value.
+ */
+ public synchronized final int createCount() {
+ return this.createCount;
+ }
+
+ /**
+ * Returns the number of times {@link #put} was called.
+ */
+ public synchronized final int putCount() {
+ return this.putCount;
+ }
+
+ /**
+ * Returns the number of values that have been evicted.
+ */
+ public synchronized final int evictionCount() {
+ return this.evictionCount;
+ }
+
+ /**
+ * Returns a copy of the current contents of the cache, ordered from least
+ * recently accessed to most recently accessed.
+ */
+ public synchronized final Map<K, V> snapshot() {
+ return new LinkedHashMap<K, V>(this.map);
+ }
+
+ @SuppressLint("DefaultLocale")
+ @Override
+ public synchronized final String toString() {
+ final int accesses = this.hitCount + this.missCount;
+ final int hitPercent = accesses != 0 ? 100 * this.hitCount / accesses : 0;
+ return String.format("LruCache[maxSize=%d,hits=%d,misses=%d,hitRate=%d%%]", this.maxSize,
+ this.hitCount, this.missCount, hitPercent);
+ }
+}
diff --git a/src/com/andrew/apollo/dragdrop/DragSortController.java b/src/com/andrew/apollo/dragdrop/DragSortController.java
new file mode 100644
index 0000000..1aa5e8a
--- /dev/null
+++ b/src/com/andrew/apollo/dragdrop/DragSortController.java
@@ -0,0 +1,442 @@
+
+package com.andrew.apollo.dragdrop;
+
+import android.graphics.Point;
+import android.view.GestureDetector;
+import android.view.HapticFeedbackConstants;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.widget.AdapterView;
+
+/**
+ * Class that starts and stops item drags on a {@link DragSortListView} based on
+ * touch gestures. This class also inherits from {@link SimpleFloatViewManager},
+ * which provides basic float View creation. An instance of this class is meant
+ * to be passed to the methods {@link DragSortListView#setTouchListener()} and
+ * {@link DragSortListView#setFloatViewManager()} of your
+ * {@link DragSortListView} instance.
+ */
+public class DragSortController extends SimpleFloatViewManager implements View.OnTouchListener,
+ GestureDetector.OnGestureListener {
+
+ public final static int ON_DOWN = 0;
+
+ public final static int ON_DRAG = 1;
+
+ public final static int ON_LONG_PRESS = 2;
+
+ public final static int FLING_RIGHT_REMOVE = 0;
+
+ public final static int FLING_LEFT_REMOVE = 1;
+
+ public final static int SLIDE_RIGHT_REMOVE = 2;
+
+ public final static int SLIDE_LEFT_REMOVE = 3;
+
+ public final static int MISS = -1;
+
+ private final GestureDetector mDetector;
+
+ private final GestureDetector mFlingRemoveDetector;
+
+ private final int mTouchSlop;
+
+ private final int[] mTempLoc = new int[2];
+
+ private final float mFlingSpeed = 500f;
+
+ private final DragSortListView mDslv;
+
+ private boolean mSortEnabled = true;
+
+ private boolean mRemoveEnabled = false;
+
+ private boolean mDragging = false;
+
+ private int mDragInitMode = ON_DOWN;
+
+ private int mRemoveMode;
+
+ private int mHitPos = MISS;
+
+ private int mItemX;
+
+ private int mItemY;
+
+ private int mCurrX;
+
+ private int mCurrY;
+
+ private int mDragHandleId;
+
+ private float mOrigFloatAlpha = 1.0f;
+
+ /**
+ * Calls {@link #DragSortController(DragSortListView, int)} with a 0 drag
+ * handle id, FLING_RIGHT_REMOVE remove mode, and ON_DOWN drag init. By
+ * default, sorting is enabled, and removal is disabled.
+ *
+ * @param dslv The DSLV instance
+ */
+ public DragSortController(DragSortListView dslv) {
+ this(dslv, 0, ON_DOWN, FLING_RIGHT_REMOVE);
+ }
+
+ /**
+ * By default, sorting is enabled, and removal is disabled.
+ *
+ * @param dslv The DSLV instance
+ * @param dragHandleId The resource id of the View that represents the drag
+ * handle in a list item.
+ */
+ public DragSortController(DragSortListView dslv, int dragHandleId, int dragInitMode,
+ int removeMode) {
+ super(dslv);
+ mDslv = dslv;
+ mDetector = new GestureDetector(dslv.getContext(), this);
+ mFlingRemoveDetector = new GestureDetector(dslv.getContext(), mFlingRemoveListener);
+ mFlingRemoveDetector.setIsLongpressEnabled(false);
+ mTouchSlop = ViewConfiguration.get(dslv.getContext()).getScaledTouchSlop();
+ mDragHandleId = dragHandleId;
+ setRemoveMode(removeMode);
+ setDragInitMode(dragInitMode);
+ mOrigFloatAlpha = dslv.getFloatAlpha();
+ }
+
+ /**
+ * @return The current drag init mode.
+ */
+ public int getDragInitMode() {
+ return mDragInitMode;
+ }
+
+ /**
+ * Set how a drag is initiated. Needs to be one of {@link ON_DOWN},
+ * {@link ON_DRAG}, or {@link ON_LONG_PRESS}.
+ *
+ * @param mode The drag init mode.
+ */
+ public void setDragInitMode(int mode) {
+ mDragInitMode = mode;
+ }
+
+ /**
+ * Enable/Disable list item sorting. Disabling is useful if only item
+ * removal is desired. Prevents drags in the vertical direction.
+ *
+ * @param enabled Set <code>true</code> to enable list item sorting.
+ */
+ public void setSortEnabled(boolean enabled) {
+ mSortEnabled = enabled;
+ }
+
+ /**
+ * @return True if sort is enabled, false otherwise.
+ */
+ public boolean isSortEnabled() {
+ return mSortEnabled;
+ }
+
+ /**
+ * One of {@link FLING_RIGHT_REMOVE}, {@link FLING_LEFT_REMOVE},
+ * {@link SLIDE_RIGHT_REMOVE}, or {@link SLIDE_LEFT_REMOVE}.
+ */
+ public void setRemoveMode(int mode) {
+ mRemoveMode = mode;
+ }
+
+ /**
+ * @return The current remove mode.
+ */
+ public int getRemoveMode() {
+ return mRemoveMode;
+ }
+
+ /**
+ * Enable/Disable item removal without affecting remove mode.
+ */
+ public void setRemoveEnabled(boolean enabled) {
+ mRemoveEnabled = enabled;
+ }
+
+ /**
+ * @return True if remove is enabled, false otherwise.
+ */
+ public boolean isRemoveEnabled() {
+ return mRemoveEnabled;
+ }
+
+ /**
+ * Set the resource id for the View that represents the drag handle in a
+ * list item.
+ *
+ * @param id An android resource id.
+ */
+ public void setDragHandleId(int id) {
+ mDragHandleId = id;
+ }
+
+ /**
+ * Sets flags to restrict certain motions of the floating View based on
+ * DragSortController settings (such as remove mode). Starts the drag on the
+ * DragSortListView.
+ *
+ * @param position The list item position (includes headers).
+ * @param deltaX Touch x-coord minus left edge of floating View.
+ * @param deltaY Touch y-coord minus top edge of floating View.
+ * @return True if drag started, false otherwise.
+ */
+ public boolean startDrag(int position, int deltaX, int deltaY) {
+
+ int mDragFlags = 0;
+ if (mSortEnabled) {
+ mDragFlags |= DragSortListView.DRAG_POS_Y | DragSortListView.DRAG_NEG_Y;
+ }
+
+ if (mRemoveEnabled) {
+ if (mRemoveMode == FLING_RIGHT_REMOVE) {
+ mDragFlags |= DragSortListView.DRAG_POS_X;
+ } else if (mRemoveMode == FLING_LEFT_REMOVE) {
+ mDragFlags |= DragSortListView.DRAG_NEG_X;
+ }
+ }
+
+ mDragging = mDslv.startDrag(position - mDslv.getHeaderViewsCount(), mDragFlags, deltaX,
+ deltaY);
+ return mDragging;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onTouch(View v, MotionEvent ev) {
+ mDetector.onTouchEvent(ev);
+ if (mRemoveEnabled && mDragging
+ && (mRemoveMode == FLING_RIGHT_REMOVE || mRemoveMode == FLING_LEFT_REMOVE)) {
+ mFlingRemoveDetector.onTouchEvent(ev);
+ }
+
+ final int mAction = ev.getAction() & MotionEvent.ACTION_MASK;
+
+ switch (mAction) {
+ case MotionEvent.ACTION_DOWN:
+ mCurrX = (int)ev.getX();
+ mCurrY = (int)ev.getY();
+ break;
+ case MotionEvent.ACTION_UP:
+ if (mRemoveEnabled) {
+ final int x = (int)ev.getX();
+ int thirdW = mDslv.getWidth() / 3;
+ int twoThirdW = mDslv.getWidth() - thirdW;
+ if ((mRemoveMode == SLIDE_RIGHT_REMOVE && x > twoThirdW)
+ || (mRemoveMode == SLIDE_LEFT_REMOVE && x < thirdW)) {
+ mDslv.stopDrag(true);
+ }
+ }
+ case MotionEvent.ACTION_CANCEL:
+ mDragging = false;
+ break;
+ }
+ return false;
+ }
+
+ /**
+ * Overrides to provide fading when slide removal is enabled.
+ */
+ @Override
+ public void onDragFloatView(View floatView, Point position, Point touch) {
+
+ if (mRemoveEnabled) {
+ int x = touch.x;
+
+ if (mRemoveMode == SLIDE_RIGHT_REMOVE) {
+ int width = mDslv.getWidth();
+ int thirdWidth = width / 3;
+
+ float alpha;
+ if (x < thirdWidth) {
+ alpha = 1.0f;
+ } else if (x < width - thirdWidth) {
+ alpha = ((float)(width - thirdWidth - x)) / ((float)thirdWidth);
+ } else {
+ alpha = 0.0f;
+ }
+ mDslv.setFloatAlpha(mOrigFloatAlpha * alpha);
+ } else if (mRemoveMode == SLIDE_LEFT_REMOVE) {
+ int width = mDslv.getWidth();
+ int thirdWidth = width / 3;
+
+ float alpha;
+ if (x < thirdWidth) {
+ alpha = 0.0f;
+ } else if (x < width - thirdWidth) {
+ alpha = ((float)(x - thirdWidth)) / ((float)thirdWidth);
+ } else {
+ alpha = 1.0f;
+ }
+ mDslv.setFloatAlpha(mOrigFloatAlpha * alpha);
+ }
+ }
+ }
+
+ /**
+ * Get the position to start dragging based on the ACTION_DOWN MotionEvent.
+ * This function simply calls {@link #dragHandleHitPosition(MotionEvent)}.
+ * Override to change drag handle behavior; this function is called
+ * internally when an ACTION_DOWN event is detected.
+ *
+ * @param ev The ACTION_DOWN MotionEvent.
+ * @return The list position to drag if a drag-init gesture is detected;
+ * MISS if unsuccessful.
+ */
+ public int startDragPosition(MotionEvent ev) {
+ return dragHandleHitPosition(ev);
+ }
+
+ /**
+ * Checks for the touch of an item's drag handle (specified by
+ * {@link #setDragHandleId(int)}), and returns that item's position if a
+ * drag handle touch was detected.
+ *
+ * @param ev The ACTION_DOWN MotionEvent.
+ * @return The list position of the item whose drag handle was touched; MISS
+ * if unsuccessful.
+ */
+ public int dragHandleHitPosition(MotionEvent ev) {
+ final int x = (int)ev.getX();
+ final int y = (int)ev.getY();
+
+ int touchPos = mDslv.pointToPosition(x, y);
+
+ final int numHeaders = mDslv.getHeaderViewsCount();
+ final int numFooters = mDslv.getFooterViewsCount();
+ final int count = mDslv.getCount();
+
+ if (touchPos != AdapterView.INVALID_POSITION && touchPos >= numHeaders
+ && touchPos < (count - numFooters)) {
+ final View item = mDslv.getChildAt(touchPos - mDslv.getFirstVisiblePosition());
+ final int rawX = (int)ev.getRawX();
+ final int rawY = (int)ev.getRawY();
+
+ View dragBox = item.findViewById(mDragHandleId);
+ if (dragBox != null) {
+ dragBox.getLocationOnScreen(mTempLoc);
+
+ if (rawX > mTempLoc[0] && rawY > mTempLoc[1]
+ && rawX < mTempLoc[0] + dragBox.getWidth()
+ && rawY < mTempLoc[1] + dragBox.getHeight()) {
+
+ mItemX = item.getLeft();
+ mItemY = item.getTop();
+
+ return touchPos;
+ }
+ }
+ }
+ return MISS;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onDown(MotionEvent ev) {
+ mHitPos = startDragPosition(ev);
+
+ if (mHitPos != MISS && mDragInitMode == ON_DOWN) {
+ startDrag(mHitPos, (int)ev.getX() - mItemX, (int)ev.getY() - mItemY);
+ }
+
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+ if (mHitPos != MISS && mDragInitMode == ON_DRAG && !mDragging) {
+ final int x1 = (int)e1.getX();
+ final int y1 = (int)e1.getY();
+ final int x2 = (int)e2.getX();
+ final int y2 = (int)e2.getY();
+
+ boolean start = false;
+ if (mRemoveEnabled && mSortEnabled) {
+ start = true;
+ } else if (mRemoveEnabled) {
+ start = Math.abs(x2 - x1) > mTouchSlop;
+ } else if (mSortEnabled) {
+ start = Math.abs(y2 - y1) > mTouchSlop;
+ }
+
+ if (start) {
+ startDrag(mHitPos, x2 - mItemX, y2 - mItemY);
+ }
+ }
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLongPress(MotionEvent e) {
+ if (mHitPos != MISS && mDragInitMode == ON_LONG_PRESS) {
+ mDslv.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+ startDrag(mHitPos, mCurrX - mItemX, mCurrY - mItemY);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public final boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onSingleTapUp(MotionEvent ev) {
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onShowPress(MotionEvent ev) {
+ }
+
+ private final GestureDetector.OnGestureListener mFlingRemoveListener = new GestureDetector.SimpleOnGestureListener() {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public final boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
+ float velocityY) {
+ if (mRemoveEnabled) {
+ switch (mRemoveMode) {
+ case FLING_RIGHT_REMOVE:
+ if (velocityX > mFlingSpeed) {
+ mDslv.stopDrag(true);
+ }
+ break;
+ case FLING_LEFT_REMOVE:
+ if (velocityX < -mFlingSpeed) {
+ mDslv.stopDrag(true);
+ }
+ break;
+ }
+ }
+ return false;
+ }
+ };
+
+}
diff --git a/src/com/andrew/apollo/dragdrop/DragSortListView.java b/src/com/andrew/apollo/dragdrop/DragSortListView.java
new file mode 100644
index 0000000..0331a97
--- /dev/null
+++ b/src/com/andrew/apollo/dragdrop/DragSortListView.java
@@ -0,0 +1,2115 @@
+/*
+ * DragSortListView. A subclass of the Android ListView component that enables
+ * drag and drop re-ordering of list items. Copyright 2012 Carl Bauer 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 com.andrew.apollo.dragdrop;
+
+import android.content.Context;
+import android.database.DataSetObserver;
+import android.graphics.Canvas;
+import android.graphics.Point;
+import android.graphics.drawable.Drawable;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.BaseAdapter;
+import android.widget.HeaderViewListAdapter;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.RelativeLayout;
+
+import com.andrew.apollo.R;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+
+/**
+ * ListView subclass that mediates drag and drop resorting of items.
+ *
+ * @author heycosmo
+ */
+public class DragSortListView extends ListView {
+
+ /**
+ * The View that floats above the ListView and represents the dragged item.
+ */
+ private View mFloatView;
+
+ /**
+ * A proposed float View location based on touch location and given deltaX
+ * and deltaY.
+ */
+ private final Point mFloatLoc = new Point();
+
+ /**
+ * The middle (in the y-direction) of the floating View.
+ */
+ private int mFloatViewMid;
+
+ /**
+ * Left edge of floating View.
+ */
+ private int mFloatViewLeft;
+
+ /**
+ * Top edge of floating View.
+ */
+ private int mFloatViewTop;
+
+ /**
+ * Watch the Adapter for data changes. Cancel a drag if coincident with a
+ * change.
+ */
+ private final DataSetObserver mObserver;
+
+ /**
+ * Transparency for the floating View (XML attribute).
+ */
+ private final float mFloatAlpha = 1.0f;
+
+ private float mCurrFloatAlpha = 1.0f;
+
+ /**
+ * While drag-sorting, the current position of the floating View. If
+ * dropped, the dragged item will land in this position.
+ */
+ private int mFloatPos;
+
+ /**
+ * The amount to scroll during the next layout pass. Used only for
+ * drag-scrolling, not standard ListView scrolling.
+ */
+ private int mScrollY = 0;
+
+ /**
+ * The first expanded ListView position that helps represent the drop slot
+ * tracking the floating View.
+ */
+ private int mFirstExpPos;
+
+ /**
+ * The second expanded ListView position that helps represent the drop slot
+ * tracking the floating View. This can equal mFirstExpPos if there is no
+ * slide shuffle occurring; otherwise it is equal to mFirstExpPos + 1.
+ */
+ private int mSecondExpPos;
+
+ /**
+ * Flag set if slide shuffling is enabled.
+ */
+ private boolean mAnimate = false;
+
+ /**
+ * The user dragged from this position.
+ */
+ private int mSrcPos;
+
+ /**
+ * Offset (in x) within the dragged item at which the user picked it up (or
+ * first touched down with the digitalis).
+ */
+ private int mDragDeltaX;
+
+ /**
+ * Offset (in y) within the dragged item at which the user picked it up (or
+ * first touched down with the digitalis).
+ */
+ private int mDragDeltaY;
+
+ /**
+ * A listener that receives callbacks whenever the floating View hovers over
+ * a new position.
+ */
+ private DragListener mDragListener;
+
+ /**
+ * A listener that receives a callback when the floating View is dropped.
+ */
+ private DropListener mDropListener;
+
+ /**
+ * A listener that receives a callback when the floating View (or more
+ * precisely the originally dragged item) is removed by one of the provided
+ * gestures.
+ */
+ private RemoveListener mRemoveListener;
+
+ /**
+ * Enable/Disable item dragging
+ */
+ private boolean mDragEnabled = true;
+
+ /**
+ * Drag state enum.
+ */
+ private final static int IDLE = 0;
+
+ private final static int STOPPED = 1;
+
+ private final static int DRAGGING = 2;
+
+ private int mDragState = IDLE;
+
+ /**
+ * Height in pixels to which the originally dragged item is collapsed during
+ * a drag-sort. Currently, this value must be greater than zero.
+ */
+ private int mItemHeightCollapsed = 1;
+
+ /**
+ * Height of the floating View. Stored for the purpose of providing the
+ * tracking drop slot.
+ */
+ private int mFloatViewHeight;
+
+ /**
+ * Convenience member. See above.
+ */
+ private int mFloatViewHeightHalf;
+
+ /**
+ * Save the given width spec for use in measuring children
+ */
+ private int mWidthMeasureSpec = 0;
+
+ /**
+ * Sample Views ultimately used for calculating the height of ListView items
+ * that are off-screen.
+ */
+ private View[] mSampleViewTypes = new View[1];
+
+ /**
+ * Drag-scroll encapsulator!
+ */
+ private final DragScroller mDragScroller;
+
+ /**
+ * Determines the start of the upward drag-scroll region at the top of the
+ * ListView. Specified by a fraction of the ListView height, thus screen
+ * resolution agnostic.
+ */
+ private float mDragUpScrollStartFrac = 1.0f / 3.0f;
+
+ /**
+ * Determines the start of the downward drag-scroll region at the bottom of
+ * the ListView. Specified by a fraction of the ListView height, thus screen
+ * resolution agnostic.
+ */
+ private float mDragDownScrollStartFrac = 1.0f / 3.0f;
+
+ /**
+ * The following are calculated from the above fracs.
+ */
+ private int mUpScrollStartY;
+
+ private int mDownScrollStartY;
+
+ private float mDownScrollStartYF;
+
+ private float mUpScrollStartYF;
+
+ /**
+ * Calculated from above above and current ListView height.
+ */
+ private float mDragUpScrollHeight;
+
+ /**
+ * Calculated from above above and current ListView height.
+ */
+ private float mDragDownScrollHeight;
+
+ /**
+ * Maximum drag-scroll speed in pixels per ms. Only used with default linear
+ * drag-scroll profile.
+ */
+ private float mMaxScrollSpeed = 0.3f;
+
+ /**
+ * Defines the scroll speed during a drag-scroll. User can provide their
+ * own; this default is a simple linear profile where scroll speed increases
+ * linearly as the floating View nears the top/bottom of the ListView.
+ */
+ private DragScrollProfile mScrollProfile = new DragScrollProfile() {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public float getSpeed(final float w, final long t) {
+ return mMaxScrollSpeed * w;
+ }
+ };
+
+ /**
+ * Current touch x.
+ */
+ private int mX;
+
+ /**
+ * Current touch y.
+ */
+ private int mY;
+
+ /**
+ * Last touch y.
+ */
+ private int mLastY;
+
+ /**
+ * Drag flag bit. Floating View can move in the positive x direction.
+ */
+ public final static int DRAG_POS_X = 0x1;
+
+ /**
+ * Drag flag bit. Floating View can move in the negative x direction.
+ */
+ public final static int DRAG_NEG_X = 0x2;
+
+ /**
+ * Drag flag bit. Floating View can move in the positive y direction. This
+ * is subtle. What this actually means is that, if enabled, the floating
+ * View can be dragged below its starting position. Remove in favor of
+ * upper-bounding item position?
+ */
+ public final static int DRAG_POS_Y = 0x4;
+
+ /**
+ * Drag flag bit. Floating View can move in the negative y direction. This
+ * is subtle. What this actually means is that the floating View can be
+ * dragged above its starting position. Remove in favor of lower-bounding
+ * item position?
+ */
+ public final static int DRAG_NEG_Y = 0x8;
+
+ /**
+ * Flags that determine limits on the motion of the floating View. See flags
+ * above.
+ */
+ private int mDragFlags = 0;
+
+ /**
+ * Last call to an on*TouchEvent was a call to onInterceptTouchEvent.
+ */
+ private boolean mLastCallWasIntercept = false;
+
+ /**
+ * A touch event is in progress.
+ */
+ private boolean mInTouchEvent = false;
+
+ /**
+ * Let the user customize the floating View.
+ */
+ private FloatViewManager mFloatViewManager = null;
+
+ /**
+ * Given to ListView to cancel its action when a drag-sort begins.
+ */
+ private final MotionEvent mCancelEvent;
+
+ /**
+ * Enum telling where to cancel the ListView action when a drag-sort begins
+ */
+ private static final int NO_CANCEL = 0;
+
+ private static final int ON_TOUCH_EVENT = 1;
+
+ private static final int ON_INTERCEPT_TOUCH_EVENT = 2;
+
+ /**
+ * Where to cancel the ListView action when a drag-sort begins
+ */
+ private int mCancelMethod = NO_CANCEL;
+
+ /**
+ * Determines when a slide shuffle animation starts. That is, defines how
+ * close to the edge of the drop slot the floating View must be to initiate
+ * the slide.
+ */
+ private float mSlideRegionFrac = 0.25f;
+
+ /**
+ * Number between 0 and 1 indicating the relative location of a sliding item
+ * (only used if drag-sort animations are turned on). Nearly 1 means the
+ * item is at the top of the slide region (nearly full blank item is
+ * directly below).
+ */
+ private float mSlideFrac = 0.0f;
+
+ /**
+ * Wraps the user-provided ListAdapter. This is used to wrap each item View
+ * given by the user inside another View (currenly a RelativeLayout) which
+ * expands and collapses to simulate the item shuffling.
+ */
+ private AdapterWrapper mAdapterWrapper;
+
+ /**
+ * Turn on custom debugger.
+ */
+ private final boolean mTrackDragSort = false;
+
+ /**
+ * Debugging class.
+ */
+ private DragSortTracker mDragSortTracker;
+
+ /**
+ * Needed for adjusting item heights from within layoutChildren
+ */
+ private boolean mBlockLayoutRequests = false;
+
+ private final DragSortController mController;
+
+ /**
+ * @param context The {@link Context} to use
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ */
+ public DragSortListView(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ mItemHeightCollapsed = 1;
+
+ mCurrFloatAlpha = mFloatAlpha;
+
+ mSlideRegionFrac = 0.75f;
+
+ mAnimate = mSlideRegionFrac > 0.0f;
+
+ setDragScrollStart(mDragUpScrollStartFrac);
+
+ mController = new DragSortController(this, R.id.edit_track_list_item_handle,
+ DragSortController.ON_DOWN, DragSortController.FLING_RIGHT_REMOVE);
+ mController.setRemoveEnabled(true);
+ mController.setSortEnabled(true);
+ /* Transparent holo light blue */
+ mController
+ .setBackgroundColor(getResources().getColor(R.color.holo_blue_light_transparent));
+
+ mFloatViewManager = mController;
+ setOnTouchListener(mController);
+
+ mDragScroller = new DragScroller();
+ setOnScrollListener(mDragScroller);
+
+ mCancelEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0f, 0f, 0, 0f,
+ 0f, 0, 0);
+
+ mObserver = new DataSetObserver() {
+ private void cancel() {
+ if (mDragState == DRAGGING) {
+ stopDrag(false);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onChanged() {
+ cancel();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onInvalidated() {
+ cancel();
+ }
+ };
+ }
+
+ /**
+ * Usually called from a FloatViewManager. The float alpha will be reset to
+ * the xml-defined value every time a drag is stopped.
+ */
+ public void setFloatAlpha(final float alpha) {
+ mCurrFloatAlpha = alpha;
+ }
+
+ public float getFloatAlpha() {
+ return mCurrFloatAlpha;
+ }
+
+ /**
+ * Set maximum drag scroll speed in positions/second. Only applies if using
+ * default ScrollSpeedProfile.
+ *
+ * @param max Maximum scroll speed.
+ */
+ public void setMaxScrollSpeed(final float max) {
+ mMaxScrollSpeed = max;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setAdapter(final ListAdapter adapter) {
+ mAdapterWrapper = new AdapterWrapper(adapter);
+ adapter.registerDataSetObserver(mObserver);
+ super.setAdapter(mAdapterWrapper);
+ }
+
+ /**
+ * As opposed to {@link ListView#getAdapter()}, which returns a heavily
+ * wrapped ListAdapter (DragSortListView wraps the input ListAdapter {\emph
+ * and} ListView wraps the wrapped one).
+ *
+ * @return The ListAdapter set as the argument of {@link setAdapter()}
+ */
+ public ListAdapter getInputAdapter() {
+ if (mAdapterWrapper == null) {
+ return null;
+ } else {
+ return mAdapterWrapper.getAdapter();
+ }
+ }
+
+ private class AdapterWrapper extends HeaderViewListAdapter {
+ private final ListAdapter mAdapter;
+
+ public AdapterWrapper(final ListAdapter adapter) {
+ super(null, null, adapter);
+ mAdapter = adapter;
+ }
+
+ public ListAdapter getAdapter() {
+ return mAdapter;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View getView(final int position, final View convertView, final ViewGroup parent) {
+
+ RelativeLayout v;
+ View child;
+ if (convertView != null) {
+
+ v = (RelativeLayout)convertView;
+ final View oldChild = v.getChildAt(0);
+ try {
+ child = mAdapter.getView(position, oldChild, v);
+ if (child != oldChild) {
+ v.removeViewAt(0);
+ v.addView(child);
+ }
+ } catch (final Exception nullz) {
+
+ }
+ } else {
+ final AbsListView.LayoutParams params = new AbsListView.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+ v = new RelativeLayout(getContext());
+ v.setLayoutParams(params);
+ try {
+ child = mAdapter.getView(position, null, v);
+ v.addView(child);
+ } catch (final Exception todo) {
+
+ }
+ }
+ adjustItem(position + getHeaderViewsCount(), v, true);
+ return v;
+ }
+ }
+
+ private void drawDivider(final int expPosition, final Canvas canvas) {
+
+ final Drawable divider = getDivider();
+ final int dividerHeight = getDividerHeight();
+
+ if (divider != null && dividerHeight != 0) {
+ final ViewGroup expItem = (ViewGroup)getChildAt(expPosition - getFirstVisiblePosition());
+ if (expItem != null) {
+ final int l = getPaddingLeft();
+ final int r = getWidth() - getPaddingRight();
+ final int t;
+ final int b;
+
+ final int childHeight = expItem.getChildAt(0).getHeight();
+
+ if (expPosition > mSrcPos) {
+ t = expItem.getTop() + childHeight;
+ b = t + dividerHeight;
+ } else {
+ b = expItem.getBottom() - childHeight;
+ t = b - dividerHeight;
+ }
+
+ divider.setBounds(l, t, r, b);
+ divider.draw(canvas);
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void dispatchDraw(final Canvas canvas) {
+ super.dispatchDraw(canvas);
+
+ if (mFloatView != null) {
+ if (mFirstExpPos != mSrcPos) {
+ drawDivider(mFirstExpPos, canvas);
+ }
+ if (mSecondExpPos != mFirstExpPos && mSecondExpPos != mSrcPos) {
+ drawDivider(mSecondExpPos, canvas);
+ }
+
+ final int w = mFloatView.getWidth();
+ final int h = mFloatView.getHeight();
+ final int alpha = (int)(255f * mCurrFloatAlpha);
+
+ canvas.save();
+ canvas.translate(mFloatViewLeft, mFloatViewTop);
+ canvas.clipRect(0, 0, w, h);
+
+ canvas.saveLayerAlpha(0, 0, w, h, alpha, Canvas.ALL_SAVE_FLAG);
+ mFloatView.draw(canvas);
+ canvas.restore();
+ canvas.restore();
+ }
+ }
+
+ private class ItemHeights {
+ int item;
+
+ int child;
+ }
+
+ private void measureItemAndGetHeights(final int position, final View item,
+ final ItemHeights heights) {
+ ViewGroup.LayoutParams lp = item.getLayoutParams();
+
+ final boolean isHeadFoot = position < getHeaderViewsCount()
+ || position >= getCount() - getFooterViewsCount();
+
+ int height = lp == null ? 0 : lp.height;
+ if (height > 0) {
+ heights.item = height;
+
+ // get height of child, measure if we have to
+ if (isHeadFoot) {
+ heights.child = heights.item;
+ } else if (position == mSrcPos) {
+ heights.child = 0;
+ } else {
+ final View child = ((ViewGroup)item).getChildAt(0);
+ lp = child.getLayoutParams();
+ height = lp == null ? 0 : lp.height;
+ if (height > 0) {
+ heights.child = height;
+ } else {
+ final int hspec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ final int wspec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
+ getListPaddingLeft() + getListPaddingRight(), lp.width);
+ child.measure(wspec, hspec);
+ heights.child = child.getMeasuredHeight();
+ }
+ }
+ } else {
+ final int hspec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ final int wspec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec, getListPaddingLeft()
+ + getListPaddingRight(), lp == null ? ViewGroup.LayoutParams.MATCH_PARENT
+ : lp.width);
+ item.measure(wspec, hspec);
+
+ heights.item = item.getMeasuredHeight();
+ if (isHeadFoot) {
+ heights.child = heights.item;
+ } else if (position == mSrcPos) {
+ heights.child = 0;
+ } else {
+ heights.child = ((ViewGroup)item).getChildAt(0).getMeasuredHeight();
+ }
+ }
+ }
+
+ /**
+ * Get the height of the given wrapped item and its child.
+ *
+ * @param position Position from which item was obtained.
+ * @param item List item (usually obtained from
+ * {@link ListView#getChildAt()}).
+ * @param heights Object to fill with heights of item.
+ */
+ private void getItemHeights(final int position, final View item, final ItemHeights heights) {
+ final boolean isHeadFoot = position < getHeaderViewsCount()
+ || position >= getCount() - getFooterViewsCount();
+
+ heights.item = item.getHeight();
+
+ if (isHeadFoot) {
+ heights.child = heights.item;
+ } else if (position == mSrcPos) {
+ heights.child = 0;
+ } else {
+ heights.child = ((ViewGroup)item).getChildAt(0).getHeight();
+ }
+ }
+
+ /**
+ * This function works for arbitrary positions (could be off-screen). If
+ * requested position is off-screen, this function calls
+ * <code>getView</code> to get height information.
+ *
+ * @param position ListView position.
+ * @param heights Object to fill with heights of item at
+ * <code>position</code>.
+ */
+ private void getItemHeights(final int position, final ItemHeights heights) {
+
+ final int first = getFirstVisiblePosition();
+ final int last = getLastVisiblePosition();
+
+ if (position >= first && position <= last) {
+ getItemHeights(position, getChildAt(position - first), heights);
+ } else {
+ // Log.d("mobeta", "getView for height");
+
+ final ListAdapter adapter = getAdapter();
+ final int type = adapter.getItemViewType(position);
+
+ // There might be a better place for checking for the following
+ final int typeCount = adapter.getViewTypeCount();
+ if (typeCount != mSampleViewTypes.length) {
+ mSampleViewTypes = new View[typeCount];
+ }
+
+ View v;
+ if (type >= 0) {
+ if (mSampleViewTypes[type] == null) {
+ v = adapter.getView(position, null, this);
+ mSampleViewTypes[type] = v;
+ } else {
+ v = adapter.getView(position, mSampleViewTypes[type], this);
+ }
+ } else {
+ // type is HEADER_OR_FOOTER or IGNORE
+ v = adapter.getView(position, null, this);
+ }
+
+ measureItemAndGetHeights(position, v, heights);
+ }
+
+ }
+
+ private int getShuffleEdge(final int position, final int top) {
+ return getShuffleEdge(position, top, null);
+ }
+
+ /**
+ * Get the shuffle edge for item at position when top of item is at y-coord
+ * top
+ *
+ * @param position
+ * @param top
+ * @param height Height of item at position. If -1, this function calculates
+ * this height.
+ * @return Shuffle line between position-1 and position (for the given view
+ * of the list; that is, for when top of item at position has
+ * y-coord of given `top`). If floating View (treated as horizontal
+ * line) is dropped immediately above this line, it lands in
+ * position-1. If dropped immediately below this line, it lands in
+ * position.
+ */
+ private int getShuffleEdge(final int position, final int top, ItemHeights heights) {
+
+ final int numHeaders = getHeaderViewsCount();
+ final int numFooters = getFooterViewsCount();
+
+ // shuffle edges are defined between items that can be
+ // dragged; there are N-1 of them if there are N draggable
+ // items.
+
+ if (position <= numHeaders || position >= getCount() - numFooters) {
+ return top;
+ }
+
+ final int divHeight = getDividerHeight();
+
+ int edge;
+
+ final int maxBlankHeight = mFloatViewHeight - mItemHeightCollapsed;
+
+ if (heights == null) {
+ heights = new ItemHeights();
+ getItemHeights(position, heights);
+ }
+
+ // first calculate top of item given that floating View is
+ // centered over src position
+ int otop = top;
+ if (mSecondExpPos <= mSrcPos) {
+ // items are expanded on and/or above the source position
+
+ if (position == mSecondExpPos && mFirstExpPos != mSecondExpPos) {
+ if (position == mSrcPos) {
+ otop = top + heights.item - mFloatViewHeight;
+ } else {
+ final int blankHeight = heights.item - heights.child;
+ otop = top + blankHeight - maxBlankHeight;
+ }
+ } else if (position > mSecondExpPos && position <= mSrcPos) {
+ otop = top - maxBlankHeight;
+ }
+
+ } else {
+ // items are expanded on and/or below the source position
+
+ if (position > mSrcPos && position <= mFirstExpPos) {
+ otop = top + maxBlankHeight;
+ } else if (position == mSecondExpPos && mFirstExpPos != mSecondExpPos) {
+ final int blankHeight = heights.item - heights.child;
+ otop = top + blankHeight;
+ }
+ }
+
+ // otop is set
+ if (position <= mSrcPos) {
+ final ItemHeights tmpHeights = new ItemHeights();
+ getItemHeights(position - 1, tmpHeights);
+ edge = otop + (mFloatViewHeight - divHeight - tmpHeights.child) / 2;
+ } else {
+ edge = otop + (heights.child - divHeight - mFloatViewHeight) / 2;
+ }
+
+ return edge;
+ }
+
+ private boolean updatePositions() {
+
+ final int first = getFirstVisiblePosition();
+ int startPos = mFirstExpPos;
+ View startView = getChildAt(startPos - first);
+
+ if (startView == null) {
+ startPos = first + getChildCount() / 2;
+ startView = getChildAt(startPos - first);
+ }
+ final int startTop = startView.getTop() + mScrollY;
+
+ final ItemHeights itemHeights = new ItemHeights();
+ getItemHeights(startPos, startView, itemHeights);
+
+ int edge = getShuffleEdge(startPos, startTop, itemHeights);
+ int lastEdge = edge;
+
+ final int divHeight = getDividerHeight();
+
+ // Log.d("mobeta", "float mid="+mFloatViewMid);
+
+ int itemPos = startPos;
+ int itemTop = startTop;
+ if (mFloatViewMid < edge) {
+ // scanning up for float position
+ // Log.d("mobeta", " edge="+edge);
+ while (itemPos >= 0) {
+ itemPos--;
+ getItemHeights(itemPos, itemHeights);
+
+ // if (itemPos <= 0)
+ if (itemPos == 0) {
+ edge = itemTop - divHeight - itemHeights.item;
+ // itemPos = 0;
+ break;
+ }
+
+ itemTop -= itemHeights.item + divHeight;
+ edge = getShuffleEdge(itemPos, itemTop, itemHeights);
+ // Log.d("mobeta", " edge="+edge);
+
+ if (mFloatViewMid >= edge) {
+ break;
+ }
+
+ lastEdge = edge;
+ }
+ } else {
+ // scanning down for float position
+ // Log.d("mobeta", " edge="+edge);
+ final int count = getCount();
+ while (itemPos < count) {
+ if (itemPos == count - 1) {
+ edge = itemTop + divHeight + itemHeights.item;
+ break;
+ }
+
+ itemTop += divHeight + itemHeights.item;
+ getItemHeights(itemPos + 1, itemHeights);
+ edge = getShuffleEdge(itemPos + 1, itemTop, itemHeights);
+ // Log.d("mobeta", " edge="+edge);
+
+ // test for hit
+ if (mFloatViewMid < edge) {
+ break;
+ }
+
+ lastEdge = edge;
+ itemPos++;
+ }
+ }
+
+ final int numHeaders = getHeaderViewsCount();
+ final int numFooters = getFooterViewsCount();
+
+ boolean updated = false;
+
+ final int oldFirstExpPos = mFirstExpPos;
+ final int oldSecondExpPos = mSecondExpPos;
+ final float oldSlideFrac = mSlideFrac;
+
+ if (mAnimate) {
+ final int edgeToEdge = Math.abs(edge - lastEdge);
+
+ int edgeTop, edgeBottom;
+ if (mFloatViewMid < edge) {
+ edgeBottom = edge;
+ edgeTop = lastEdge;
+ } else {
+ edgeTop = edge;
+ edgeBottom = lastEdge;
+ }
+ // Log.d("mobeta", "edgeTop="+edgeTop+" edgeBot="+edgeBottom);
+
+ final int slideRgnHeight = (int)(0.5f * mSlideRegionFrac * edgeToEdge);
+ final float slideRgnHeightF = slideRgnHeight;
+ final int slideEdgeTop = edgeTop + slideRgnHeight;
+ final int slideEdgeBottom = edgeBottom - slideRgnHeight;
+
+ // Three regions
+ if (mFloatViewMid < slideEdgeTop) {
+ mFirstExpPos = itemPos - 1;
+ mSecondExpPos = itemPos;
+ mSlideFrac = 0.5f * (slideEdgeTop - mFloatViewMid) / slideRgnHeightF;
+ // Log.d("mobeta",
+ // "firstExp="+mFirstExpPos+" secExp="+mSecondExpPos+" slideFrac="+mSlideFrac);
+ } else if (mFloatViewMid < slideEdgeBottom) {
+ mFirstExpPos = itemPos;
+ mSecondExpPos = itemPos;
+ } else {
+ mFirstExpPos = itemPos;
+ mSecondExpPos = itemPos + 1;
+ mSlideFrac = 0.5f * (1.0f + (edgeBottom - mFloatViewMid) / slideRgnHeightF);
+ // Log.d("mobeta",
+ // "firstExp="+mFirstExpPos+" secExp="+mSecondExpPos+" slideFrac="+mSlideFrac);
+ }
+
+ } else {
+ mFirstExpPos = itemPos;
+ mSecondExpPos = itemPos;
+ }
+
+ // correct for headers and footers
+ if (mFirstExpPos < numHeaders) {
+ itemPos = numHeaders;
+ mFirstExpPos = itemPos;
+ mSecondExpPos = itemPos;
+ } else if (mSecondExpPos >= getCount() - numFooters) {
+ itemPos = getCount() - numFooters - 1;
+ mFirstExpPos = itemPos;
+ mSecondExpPos = itemPos;
+ }
+
+ if (mFirstExpPos != oldFirstExpPos || mSecondExpPos != oldSecondExpPos
+ || mSlideFrac != oldSlideFrac) {
+ updated = true;
+ }
+
+ if (itemPos != mFloatPos) {
+ if (mDragListener != null) {
+ mDragListener.drag(mFloatPos - numHeaders, itemPos - numHeaders);
+ }
+
+ mFloatPos = itemPos;
+ updated = true;
+ }
+
+ return updated;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onDraw(final Canvas canvas) {
+ super.onDraw(canvas);
+
+ if (mTrackDragSort) {
+ mDragSortTracker.appendState();
+ }
+ }
+
+ /**
+ * Stop a drag in progress. Pass <code>true</code> if you would like to
+ * remove the dragged item from the list.
+ *
+ * @param remove Remove the dragged item from the list. Calls a registered
+ * DropListener, if one exists.
+ * @return True if the stop was successful.
+ */
+ public boolean stopDrag(final boolean remove) {
+ if (mFloatView != null) {
+ mDragState = STOPPED;
+
+ // stop the drag
+ dropFloatView(remove);
+
+ return true;
+ } else {
+ // stop failed
+ return false;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onTouchEvent(final MotionEvent ev) {
+
+ if (!mDragEnabled) {
+ return super.onTouchEvent(ev);
+ }
+
+ boolean more = false;
+
+ final boolean lastCallWasIntercept = mLastCallWasIntercept;
+ mLastCallWasIntercept = false;
+
+ if (!lastCallWasIntercept) {
+ saveTouchCoords(ev);
+ }
+
+ if (mFloatView != null) {
+ onDragTouchEvent(ev);
+ more = true; // give us more!
+ } else {
+ // what if float view is null b/c we dropped in middle
+ // of drag touch event?
+
+ if (mDragState != STOPPED) {
+ if (super.onTouchEvent(ev)) {
+ more = true;
+ }
+ }
+
+ final int action = ev.getAction() & MotionEvent.ACTION_MASK;
+ switch (action) {
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ doActionUpOrCancel();
+ break;
+ default:
+ if (more) {
+ mCancelMethod = ON_TOUCH_EVENT;
+ }
+ }
+ }
+
+ return more;
+
+ }
+
+ private void doActionUpOrCancel() {
+ mCancelMethod = NO_CANCEL;
+ mInTouchEvent = false;
+ mDragState = IDLE;
+ mCurrFloatAlpha = mFloatAlpha;
+ }
+
+ private void saveTouchCoords(final MotionEvent ev) {
+ final int action = ev.getAction() & MotionEvent.ACTION_MASK;
+ if (action != MotionEvent.ACTION_DOWN) {
+ mLastY = mY;
+ }
+ mX = (int)ev.getX();
+ mY = (int)ev.getY();
+ if (action == MotionEvent.ACTION_DOWN) {
+ mLastY = mY;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onInterceptTouchEvent(final MotionEvent ev) {
+ if (!mDragEnabled) {
+ return super.onInterceptTouchEvent(ev);
+ }
+
+ saveTouchCoords(ev);
+ mLastCallWasIntercept = true;
+
+ boolean intercept = false;
+
+ final int action = ev.getAction() & MotionEvent.ACTION_MASK;
+
+ if (action == MotionEvent.ACTION_DOWN) {
+ mInTouchEvent = true;
+ }
+
+ // the following deals with calls to super.onInterceptTouchEvent
+ if (mFloatView != null) {
+ // super's touch event canceled in startDrag
+ intercept = true;
+ } else {
+ if (super.onInterceptTouchEvent(ev)) {
+ intercept = true;
+ }
+
+ switch (action) {
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ doActionUpOrCancel();
+ break;
+ default:
+ if (intercept) {
+ mCancelMethod = ON_TOUCH_EVENT;
+ } else {
+ mCancelMethod = ON_INTERCEPT_TOUCH_EVENT;
+ }
+ }
+ }
+
+ // check for startDragging
+
+ if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
+ mInTouchEvent = false;
+ }
+
+ return intercept;
+ }
+
+ /**
+ * Set the width of each drag scroll region by specifying a fraction of the
+ * ListView height.
+ *
+ * @param heightFraction Fraction of ListView height. Capped at 0.5f.
+ */
+ public void setDragScrollStart(final float heightFraction) {
+ setDragScrollStarts(heightFraction, heightFraction);
+ }
+
+ /**
+ * Set the width of each drag scroll region by specifying a fraction of the
+ * ListView height.
+ *
+ * @param upperFrac Fraction of ListView height for up-scroll bound. Capped
+ * at 0.5f.
+ * @param lowerFrac Fraction of ListView height for down-scroll bound.
+ * Capped at 0.5f.
+ */
+ public void setDragScrollStarts(final float upperFrac, final float lowerFrac) {
+ if (lowerFrac > 0.5f) {
+ mDragDownScrollStartFrac = 0.5f;
+ } else {
+ mDragDownScrollStartFrac = lowerFrac;
+ }
+
+ if (upperFrac > 0.5f) {
+ mDragUpScrollStartFrac = 0.5f;
+ } else {
+ mDragUpScrollStartFrac = upperFrac;
+ }
+
+ if (getHeight() != 0) {
+ updateScrollStarts();
+ }
+ }
+
+ private void continueDrag(final int x, final int y) {
+
+ // Log.d("mobeta", "move");
+ dragView(x, y);
+
+ // if (mTrackDragSort) {
+ // mDragSortTracker.appendState();
+ // }
+
+ requestLayout();
+
+ final int minY = Math.min(y, mFloatViewMid + mFloatViewHeightHalf);
+ final int maxY = Math.max(y, mFloatViewMid - mFloatViewHeightHalf);
+
+ // get the current scroll direction
+ final int currentScrollDir = mDragScroller.getScrollDir();
+
+ if (minY > mLastY && minY > mDownScrollStartY && currentScrollDir != DragScroller.DOWN) {
+ // dragged down, it is below the down scroll start and it is not
+ // scrolling up
+
+ if (currentScrollDir != DragScroller.STOP) {
+ // moved directly from up scroll to down scroll
+ mDragScroller.stopScrolling(true);
+ }
+
+ // start scrolling down
+ mDragScroller.startScrolling(DragScroller.DOWN);
+ } else if (maxY < mLastY && maxY < mUpScrollStartY && currentScrollDir != DragScroller.UP) {
+ // dragged up, it is above the up scroll start and it is not
+ // scrolling up
+
+ if (currentScrollDir != DragScroller.STOP) {
+ // moved directly from down scroll to up scroll
+ mDragScroller.stopScrolling(true);
+ }
+
+ // start scrolling up
+ mDragScroller.startScrolling(DragScroller.UP);
+ } else if (maxY >= mUpScrollStartY && minY <= mDownScrollStartY
+ && mDragScroller.isScrolling()) {
+ // not in the upper nor in the lower drag-scroll regions but it is
+ // still scrolling
+
+ mDragScroller.stopScrolling(true);
+ }
+ }
+
+ private void updateScrollStarts() {
+ final int padTop = getPaddingTop();
+ final int listHeight = getHeight() - padTop - getPaddingBottom();
+ final float heightF = listHeight;
+
+ mUpScrollStartYF = padTop + mDragUpScrollStartFrac * heightF;
+ mDownScrollStartYF = padTop + (1.0f - mDragDownScrollStartFrac) * heightF;
+
+ mUpScrollStartY = (int)mUpScrollStartYF;
+ mDownScrollStartY = (int)mDownScrollStartYF;
+
+ mDragUpScrollHeight = mUpScrollStartYF - padTop;
+ mDragDownScrollHeight = padTop + listHeight - mDownScrollStartYF;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onSizeChanged(final int w, final int h, final int oldw, final int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ updateScrollStarts();
+ }
+
+ private void dropFloatView(final boolean removeSrcItem) {
+
+ mDragScroller.stopScrolling(true);
+
+ if (removeSrcItem) {
+ if (mRemoveListener != null) {
+ mRemoveListener.remove(mSrcPos - getHeaderViewsCount());
+ }
+ } else {
+ if (mDropListener != null && mFloatPos >= 0 && mFloatPos < getCount()) {
+ final int numHeaders = getHeaderViewsCount();
+ mDropListener.drop(mSrcPos - numHeaders, mFloatPos - numHeaders);
+ }
+
+ // adjustAllItems();
+
+ final int firstPos = getFirstVisiblePosition();
+ if (mSrcPos < firstPos) {
+ // collapsed src item is off screen;
+ // adjust the scroll after item heights have been fixed
+ final View v = getChildAt(0);
+ int top = 0;
+ if (v != null) {
+ top = v.getTop();
+ }
+ // Log.d("mobeta", "top="+top+" fvh="+mFloatViewHeight);
+ setSelectionFromTop(firstPos - 1, top - getPaddingTop());
+ }
+ }
+
+ mSrcPos = -1;
+ mFirstExpPos = -1;
+ mSecondExpPos = -1;
+ mFloatPos = -1;
+
+ removeFloatView();
+
+ if (mTrackDragSort) {
+ mDragSortTracker.stopTracking();
+ }
+ }
+
+ private void adjustAllItems() {
+ final int first = getFirstVisiblePosition();
+ final int last = getLastVisiblePosition();
+
+ final int begin = Math.max(0, getHeaderViewsCount() - first);
+ final int end = Math.min(last - first, getCount() - 1 - getFooterViewsCount() - first);
+
+ for (int i = begin; i <= end; ++i) {
+ final View v = getChildAt(i);
+ if (v != null) {
+ adjustItem(first + i, v, false);
+ }
+ }
+ }
+
+ private void adjustItem(final int position, final View v, final boolean needsMeasure) {
+
+ final ViewGroup.LayoutParams lp = v.getLayoutParams();
+ final int oldHeight = lp.height;
+ int height = oldHeight;
+
+ getDividerHeight();
+
+ final boolean isSliding = mAnimate && mFirstExpPos != mSecondExpPos;
+ final int maxNonSrcBlankHeight = mFloatViewHeight - mItemHeightCollapsed;
+ final int slideHeight = (int)(mSlideFrac * maxNonSrcBlankHeight);
+
+ if (position == mSrcPos) {
+ if (mSrcPos == mFirstExpPos) {
+ if (isSliding) {
+ height = slideHeight + mItemHeightCollapsed;
+ } else {
+ height = mFloatViewHeight;
+ }
+ } else if (mSrcPos == mSecondExpPos) {
+ // if gets here, we know an item is sliding
+ height = mFloatViewHeight - slideHeight;
+ } else {
+ height = mItemHeightCollapsed;
+ }
+ } else if (position == mFirstExpPos || position == mSecondExpPos) {
+ // position is not src
+
+ final ItemHeights itemHeights = new ItemHeights();
+ if (needsMeasure) {
+ measureItemAndGetHeights(position, v, itemHeights);
+ } else {
+ getItemHeights(position, v, itemHeights);
+ }
+
+ if (position == mFirstExpPos) {
+ if (isSliding) {
+ height = itemHeights.child + slideHeight;
+ } else {
+ height = itemHeights.child + maxNonSrcBlankHeight;
+ }
+ } else { // position=mSecondExpPos
+ // we know an item is sliding (b/c 2ndPos != 1stPos)
+ height = itemHeights.child + maxNonSrcBlankHeight - slideHeight;
+ }
+ } else {
+ height = ViewGroup.LayoutParams.WRAP_CONTENT;
+ }
+
+ if (height != oldHeight) {
+ lp.height = height;
+
+ v.setLayoutParams(lp);
+ }
+
+ // Adjust item gravity
+
+ if (position == mFirstExpPos || position == mSecondExpPos) {
+ if (position < mSrcPos) {
+ ((RelativeLayout)v).setGravity(Gravity.BOTTOM);
+ } else if (position > mSrcPos) {
+ ((RelativeLayout)v).setGravity(Gravity.TOP);
+ }
+ }
+
+ // Finally adjust item visibility
+
+ final int oldVis = v.getVisibility();
+ int vis = View.VISIBLE;
+
+ if (position == mSrcPos && mFloatView != null) {
+ vis = View.INVISIBLE;
+ }
+
+ if (vis != oldVis) {
+ v.setVisibility(vis);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void requestLayout() {
+ if (!mBlockLayoutRequests) {
+ super.requestLayout();
+ }
+ }
+
+ private void doDragScroll(final int oldFirstExpPos, final int oldSecondExpPos) {
+ if (mScrollY == 0) {
+ return;
+ }
+
+ final int padTop = getPaddingTop();
+ final int listHeight = getHeight() - padTop - getPaddingBottom();
+ final int first = getFirstVisiblePosition();
+ final int last = getLastVisiblePosition();
+
+ int movePos;
+
+ if (mScrollY >= 0) {
+ mScrollY = Math.min(listHeight, mScrollY);
+ movePos = first;
+ } else {
+ mScrollY = Math.max(-listHeight, mScrollY);
+ movePos = last;
+ }
+
+ final View moveItem = getChildAt(movePos - first);
+ int top = moveItem.getTop() + mScrollY;
+
+ if (movePos == 0 && top > padTop) {
+ top = padTop;
+ }
+
+ final ItemHeights itemHeightsBefore = new ItemHeights();
+ getItemHeights(movePos, moveItem, itemHeightsBefore);
+ final int moveHeightBefore = itemHeightsBefore.item;
+ final int moveBlankBefore = moveHeightBefore - itemHeightsBefore.child;
+
+ final ItemHeights itemHeightsAfter = new ItemHeights();
+ measureItemAndGetHeights(movePos, moveItem, itemHeightsAfter);
+ final int moveHeightAfter = itemHeightsAfter.item;
+ final int moveBlankAfter = moveHeightAfter - itemHeightsAfter.child;
+
+ if (movePos <= oldFirstExpPos) {
+ if (movePos > mFirstExpPos) {
+ top += mFloatViewHeight - moveBlankAfter;
+ }
+ } else if (movePos == oldSecondExpPos) {
+ if (movePos <= mFirstExpPos) {
+ top += moveBlankBefore - mFloatViewHeight;
+ } else if (movePos == mSecondExpPos) {
+ top += moveHeightBefore - moveHeightAfter;
+ } else {
+ top += moveBlankBefore;
+ }
+ } else {
+ if (movePos <= mFirstExpPos) {
+ top -= mFloatViewHeight;
+ } else if (movePos == mSecondExpPos) {
+ top -= moveBlankAfter;
+ }
+ }
+
+ setSelectionFromTop(movePos, top - padTop);
+
+ mScrollY = 0;
+ }
+
+ private void measureFloatView() {
+ if (mFloatView != null) {
+ ViewGroup.LayoutParams lp = mFloatView.getLayoutParams();
+ if (lp == null) {
+ lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ }
+ final int wspec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec, getListPaddingLeft()
+ + getListPaddingRight(), lp.width);
+ int hspec;
+ if (lp.height > 0) {
+ hspec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
+ } else {
+ hspec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ }
+ mFloatView.measure(wspec, hspec);
+ mFloatViewHeight = mFloatView.getMeasuredHeight();
+ mFloatViewHeightHalf = mFloatViewHeight / 2;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ if (mFloatView != null) {
+ if (mFloatView.isLayoutRequested()) {
+ measureFloatView();
+ }
+ }
+ mWidthMeasureSpec = widthMeasureSpec;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void layoutChildren() {
+
+ if (mFloatView != null) {
+ mFloatView.layout(0, 0, mFloatView.getMeasuredWidth(), mFloatView.getMeasuredHeight());
+
+ // Log.d("mobeta", "layout children");
+ final int oldFirstExpPos = mFirstExpPos;
+ final int oldSecondExpPos = mSecondExpPos;
+
+ mBlockLayoutRequests = true;
+
+ if (updatePositions()) {
+ adjustAllItems();
+ }
+
+ if (mScrollY != 0) {
+ doDragScroll(oldFirstExpPos, oldSecondExpPos);
+ }
+
+ mBlockLayoutRequests = false;
+ }
+
+ super.layoutChildren();
+ }
+
+ protected boolean onDragTouchEvent(final MotionEvent ev) {
+ switch (ev.getAction() & MotionEvent.ACTION_MASK) {
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ stopDrag(false);
+ doActionUpOrCancel();
+ break;
+ case MotionEvent.ACTION_MOVE:
+ continueDrag((int)ev.getX(), (int)ev.getY());
+ break;
+ }
+
+ return true;
+ }
+
+ /**
+ * Start a drag of item at <code>position</code> using the registered
+ * FloatViewManager. Calls through to
+ * {@link #startDrag(int,View,int,int,int)} after obtaining the floating
+ * View from the FloatViewManager.
+ *
+ * @param position Item to drag.
+ * @param dragFlags Flags that restrict some movements of the floating View.
+ * For example, set <code>dragFlags |=
+ * ~{@link #DRAG_NEG_X}</code> to allow dragging the floating View in all
+ * directions except off the screen to the left.
+ * @param deltaX Offset in x of the touch coordinate from the left edge of
+ * the floating View (i.e. touch-x minus float View left).
+ * @param deltaY Offset in y of the touch coordinate from the top edge of
+ * the floating View (i.e. touch-y minus float View top).
+ * @return True if the drag was started, false otherwise. This
+ * <code>startDrag</code> will fail if we are not currently in a
+ * touch event, there is no registered FloatViewManager, or the
+ * FloatViewManager returns a null View.
+ */
+ public boolean startDrag(final int position, final int dragFlags, final int deltaX,
+ final int deltaY) {
+ if (!mInTouchEvent || mFloatViewManager == null) {
+ return false;
+ }
+
+ final View v = mFloatViewManager.onCreateFloatView(position);
+
+ if (v == null) {
+ return false;
+ } else {
+ return startDrag(position, v, dragFlags, deltaX, deltaY);
+ }
+
+ }
+
+ /**
+ * Start a drag of item at <code>position</code> without using a
+ * FloatViewManager.
+ *
+ * @param position Item to drag.
+ * @param floatView Floating View.
+ * @param dragFlags Flags that restrict some movements of the floating View.
+ * For example, set <code>dragFlags |=
+ * ~{@link #DRAG_NEG_X}</code> to allow dragging the floating View in all
+ * directions except off the screen to the left.
+ * @param deltaX Offset in x of the touch coordinate from the left edge of
+ * the floating View (i.e. touch-x minus float View left).
+ * @param deltaY Offset in y of the touch coordinate from the top edge of
+ * the floating View (i.e. touch-y minus float View top).
+ * @return True if the drag was started, false otherwise. This
+ * <code>startDrag</code> will fail if we are not currently in a
+ * touch event, <code>floatView</code> is null, or there is a drag
+ * in progress.
+ */
+ public boolean startDrag(final int position, final View floatView, final int dragFlags,
+ final int deltaX, final int deltaY) {
+ if (!mInTouchEvent || mFloatView != null || floatView == null) {
+ return false;
+ }
+
+ if (getParent() != null) {
+ getParent().requestDisallowInterceptTouchEvent(true);
+ }
+
+ final int pos = position + getHeaderViewsCount();
+ mFirstExpPos = pos;
+ mSecondExpPos = pos;
+ mSrcPos = pos;
+ mFloatPos = pos;
+
+ // mDragState = dragType;
+ mDragState = DRAGGING;
+ mDragFlags = 0;
+ mDragFlags |= dragFlags;
+
+ mFloatView = floatView;
+ measureFloatView(); // sets mFloatViewHeight
+
+ mDragDeltaX = deltaX;
+ mDragDeltaY = deltaY;
+ updateFloatView(mX - mDragDeltaX, mY - mDragDeltaY);
+
+ // set src item invisible
+ final View srcItem = getChildAt(mSrcPos - getFirstVisiblePosition());
+ if (srcItem != null) {
+ srcItem.setVisibility(View.INVISIBLE);
+ }
+
+ if (mTrackDragSort) {
+ mDragSortTracker.startTracking();
+ }
+
+ // once float view is created, events are no longer passed
+ // to ListView
+ switch (mCancelMethod) {
+ case ON_TOUCH_EVENT:
+ super.onTouchEvent(mCancelEvent);
+ break;
+ case ON_INTERCEPT_TOUCH_EVENT:
+ super.onInterceptTouchEvent(mCancelEvent);
+ break;
+ }
+
+ requestLayout();
+
+ return true;
+ }
+
+ /**
+ * Sets float View location based on suggested values and constraints set in
+ * mDragFlags.
+ */
+ private void updateFloatView(final int floatX, final int floatY) {
+
+ // restrict x motion
+ final int padLeft = getPaddingLeft();
+ if ((mDragFlags & DRAG_POS_X) == 0 && floatX > padLeft) {
+ mFloatViewLeft = padLeft;
+ } else if ((mDragFlags & DRAG_NEG_X) == 0 && floatX < padLeft) {
+ mFloatViewLeft = padLeft;
+ } else {
+ mFloatViewLeft = floatX;
+ }
+
+ // keep floating view from going past bottom of last header view
+ final int numHeaders = getHeaderViewsCount();
+ final int numFooters = getFooterViewsCount();
+ final int firstPos = getFirstVisiblePosition();
+ final int lastPos = getLastVisiblePosition();
+
+ // Log.d("mobeta",
+ // "nHead="+numHeaders+" nFoot="+numFooters+" first="+firstPos+" last="+lastPos);
+ int topLimit = getPaddingTop();
+ if (firstPos < numHeaders) {
+ topLimit = getChildAt(numHeaders - firstPos - 1).getBottom();
+ }
+ if ((mDragFlags & DRAG_NEG_Y) == 0) {
+ if (firstPos <= mSrcPos) {
+ topLimit = Math.max(getChildAt(mSrcPos - firstPos).getTop(), topLimit);
+ }
+ }
+ // bottom limit is top of first footer View or
+ // bottom of last item in list
+ int bottomLimit = getHeight() - getPaddingBottom();
+ if (lastPos >= getCount() - numFooters - 1) {
+ bottomLimit = getChildAt(getCount() - numFooters - 1 - firstPos).getBottom();
+ }
+ if ((mDragFlags & DRAG_POS_Y) == 0) {
+ if (lastPos >= mSrcPos) {
+ bottomLimit = Math.min(getChildAt(mSrcPos - firstPos).getBottom(), bottomLimit);
+ }
+ }
+
+ // Log.d("mobeta", "dragView top=" + (y - mDragDeltaY));
+ // Log.d("mobeta", "limit=" + limit);
+ // Log.d("mobeta", "mDragDeltaY=" + mDragDeltaY);
+
+ if (floatY < topLimit) {
+ mFloatViewTop = topLimit;
+ } else if (floatY + mFloatViewHeight > bottomLimit) {
+ mFloatViewTop = bottomLimit - mFloatViewHeight;
+ } else {
+ mFloatViewTop = floatY;
+ }
+
+ // get y-midpoint of floating view (constrained to ListView bounds)
+ mFloatViewMid = mFloatViewTop + mFloatViewHeightHalf;
+ }
+
+ private void dragView(final int x, final int y) {
+ // Log.d("mobeta", "float view pure x=" + x + " y=" + y);
+
+ // proposed position
+ mFloatLoc.x = x - mDragDeltaX;
+ mFloatLoc.y = y - mDragDeltaY;
+
+ final Point touch = new Point(x, y);
+
+ // let manager adjust proposed position first
+ if (mFloatViewManager != null) {
+ mFloatViewManager.onDragFloatView(mFloatView, mFloatLoc, touch);
+ }
+
+ // then we override if manager gives an unsatisfactory
+ // position (e.g. over a header/footer view). Also,
+ // dragFlags override manager adjustments.
+ updateFloatView(mFloatLoc.x, mFloatLoc.y);
+ }
+
+ private void removeFloatView() {
+ if (mFloatView != null) {
+ mFloatView.setVisibility(GONE);
+ if (mFloatViewManager != null) {
+ mFloatViewManager.onDestroyFloatView(mFloatView);
+ }
+ mFloatView = null;
+ }
+ }
+
+ /**
+ * Interface for customization of the floating View appearance and dragging
+ * behavior. Implement your own and pass it to {@link #setFloatViewManager}.
+ * If your own is not passed, the default {@link SimpleFloatViewManager}
+ * implementation is used.
+ */
+ public interface FloatViewManager {
+ /**
+ * Return the floating View for item at <code>position</code>.
+ * DragSortListView will measure and layout this View for you, so feel
+ * free to just inflate it. You can help DSLV by setting some
+ * {@link ViewGroup.LayoutParams} on this View; otherwise it will set
+ * some for you (with a width of FILL_PARENT and a height of
+ * WRAP_CONTENT).
+ *
+ * @param position Position of item to drag (NOTE: <code>position</code>
+ * excludes header Views; thus, if you want to call
+ * {@link ListView#getChildAt(int)}, you will need to add
+ * {@link ListView#getHeaderViewsCount()} to the index).
+ * @return The View you wish to display as the floating View.
+ */
+ public View onCreateFloatView(int position);
+
+ /**
+ * Called whenever the floating View is dragged. Float View properties
+ * can be changed here. Also, the upcoming location of the float View
+ * can be altered by setting <code>location.x</code> and
+ * <code>location.y</code>.
+ *
+ * @param floatView The floating View.
+ * @param location The location (top-left; relative to DSLV top-left) at
+ * which the float View would like to appear, given the
+ * current touch location and the offset provided in
+ * {@link DragSortListView#startDrag}.
+ * @param touch The current touch location (relative to DSLV top-left).
+ */
+ public void onDragFloatView(View floatView, Point location, Point touch);
+
+ /**
+ * Called when the float View is dropped; lets you perform any necessary
+ * cleanup. The internal DSLV floating View reference is set to null
+ * immediately after this is called.
+ *
+ * @param floatView The floating View passed to
+ * {@link #onCreateFloatView(int)}.
+ */
+ public void onDestroyFloatView(View floatView);
+ }
+
+ public void setFloatViewManager(final FloatViewManager manager) {
+ mFloatViewManager = manager;
+ }
+
+ public void setDragListener(final DragListener l) {
+ mDragListener = l;
+ }
+
+ /**
+ * Allows for easy toggling between a DragSortListView and a regular old
+ * ListView. If enabled, items are draggable, where the drag init mode
+ * determines how items are lifted (see {@link setDragInitMode(int)}). If
+ * disabled, items cannot be dragged.
+ *
+ * @param enabled Set <code>true</code> to enable list item dragging
+ */
+ public void setDragEnabled(final boolean enabled) {
+ mDragEnabled = enabled;
+ }
+
+ public boolean isDragEnabled() {
+ return mDragEnabled;
+ }
+
+ /**
+ * This better reorder your ListAdapter! DragSortListView does not do this
+ * for you; doesn't make sense to. Make sure
+ * {@link BaseAdapter#notifyDataSetChanged()} or something like it is called
+ * in your implementation.
+ *
+ * @param l
+ */
+ public void setDropListener(final DropListener l) {
+ mDropListener = l;
+ }
+
+ /**
+ * Probably a no-brainer, but make sure that your remove listener calls
+ * {@link BaseAdapter#notifyDataSetChanged()} or something like it. When an
+ * item removal occurs, DragSortListView relies on a redraw of all the items
+ * to recover invisible views and such. Strictly speaking, if you remove
+ * something, your dataset has changed...
+ *
+ * @param l
+ */
+ public void setRemoveListener(final RemoveListener l) {
+ if (mController != null && l == null) {
+ mController.setRemoveEnabled(false);
+ }
+ mRemoveListener = l;
+ }
+
+ public interface DragListener {
+ public void drag(int from, int to);
+ }
+
+ /**
+ * Your implementation of this has to reorder your ListAdapter! Make sure to
+ * call {@link BaseAdapter#notifyDataSetChanged()} or something like it in
+ * your implementation.
+ *
+ * @author heycosmo
+ */
+ public interface DropListener {
+ public void drop(int from, int to);
+ }
+
+ /**
+ * Make sure to call {@link BaseAdapter#notifyDataSetChanged()} or something
+ * like it in your implementation.
+ *
+ * @author heycosmo
+ */
+ public interface RemoveListener {
+ public void remove(int which);
+ }
+
+ public interface DragSortListener extends DropListener, DragListener, RemoveListener {
+ }
+
+ public void setDragSortListener(final DragSortListener l) {
+ setDropListener(l);
+ setDragListener(l);
+ setRemoveListener(l);
+ }
+
+ /**
+ * Completely custom scroll speed profile. Default increases linearly with
+ * position and is constant in time. Create your own by implementing
+ * {@link DragSortListView.DragScrollProfile}.
+ *
+ * @param ssp
+ */
+ public void setDragScrollProfile(final DragScrollProfile ssp) {
+ if (ssp != null) {
+ mScrollProfile = ssp;
+ }
+ }
+
+ /**
+ * Interface for controlling scroll speed as a function of touch position
+ * and time. Use
+ * {@link DragSortListView#setDragScrollProfile(DragScrollProfile)} to set
+ * custom profile.
+ *
+ * @author heycosmo
+ */
+ public interface DragScrollProfile {
+ /**
+ * Return a scroll speed in pixels/millisecond. Always return a positive
+ * number.
+ *
+ * @param w Normalized position in scroll region (i.e. w \in [0,1]).
+ * Small w typically means slow scrolling.
+ * @param t Time (in milliseconds) since start of scroll (handy if you
+ * want scroll acceleration).
+ * @return Scroll speed at position w and time t in pixels/ms.
+ */
+ float getSpeed(float w, long t);
+ }
+
+ private class DragScroller implements Runnable, AbsListView.OnScrollListener {
+
+ private boolean mAbort;
+
+ private long mPrevTime;
+
+ private int dy;
+
+ private float dt;
+
+ private long tStart;
+
+ private int scrollDir;
+
+ public final static int STOP = -1;
+
+ public final static int UP = 0;
+
+ public final static int DOWN = 1;
+
+ private float mScrollSpeed; // pixels per ms
+
+ private boolean mScrolling = false;
+
+ public boolean isScrolling() {
+ return mScrolling;
+ }
+
+ public int getScrollDir() {
+ return mScrolling ? scrollDir : STOP;
+ }
+
+ public DragScroller() {
+ }
+
+ public void startScrolling(final int dir) {
+ if (!mScrolling) {
+ // Debug.startMethodTracing("dslv-scroll");
+ mAbort = false;
+ mScrolling = true;
+ tStart = SystemClock.uptimeMillis();
+ mPrevTime = tStart;
+ scrollDir = dir;
+ post(this);
+ }
+ }
+
+ public void stopScrolling(final boolean now) {
+ if (now) {
+ removeCallbacks(this);
+ mScrolling = false;
+ } else {
+ mAbort = true;
+ }
+
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void run() {
+ if (mAbort) {
+ mScrolling = false;
+ return;
+ }
+
+ final int first = getFirstVisiblePosition();
+ final int last = getLastVisiblePosition();
+ final int count = getCount();
+ final int padTop = getPaddingTop();
+ final int listHeight = getHeight() - padTop - getPaddingBottom();
+
+ final int minY = Math.min(mY, mFloatViewMid + mFloatViewHeightHalf);
+ final int maxY = Math.max(mY, mFloatViewMid - mFloatViewHeightHalf);
+
+ if (scrollDir == UP) {
+ final View v = getChildAt(0);
+ if (v == null) {
+ mScrolling = false;
+ return;
+ } else {
+ if (first == 0 && v.getTop() == padTop) {
+ mScrolling = false;
+ return;
+ }
+ }
+ mScrollSpeed = mScrollProfile.getSpeed((mUpScrollStartYF - maxY)
+ / mDragUpScrollHeight, mPrevTime);
+ } else {
+ final View v = getChildAt(last - first);
+ if (v == null) {
+ mScrolling = false;
+ return;
+ } else {
+ if (last == count - 1 && v.getBottom() <= listHeight + padTop) {
+ mScrolling = false;
+ return;
+ }
+ }
+ mScrollSpeed = -mScrollProfile.getSpeed((minY - mDownScrollStartYF)
+ / mDragDownScrollHeight, mPrevTime);
+ }
+
+ dt = SystemClock.uptimeMillis() - mPrevTime;
+ // dy is change in View position of a list item; i.e. positive dy
+ // means user is scrolling up (list item moves down the screen,
+ // remember
+ // y=0 is at top of View).
+ dy = Math.round(mScrollSpeed * dt);
+ mScrollY += dy;
+
+ requestLayout();
+
+ mPrevTime += dt;
+
+ post(this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onScroll(final AbsListView view, final int firstVisibleItem,
+ final int visibleItemCount, final int totalItemCount) {
+ if (mScrolling && visibleItemCount != 0) {
+ dragView(mX, mY);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onScrollStateChanged(final AbsListView view, final int scrollState) {
+ }
+
+ }
+
+ private class DragSortTracker {
+ StringBuilder mBuilder = new StringBuilder();
+
+ File mFile;
+
+ private int mNumInBuffer = 0;
+
+ private int mNumFlushes = 0;
+
+ private boolean mTracking = false;
+
+ public void startTracking() {
+ mBuilder.append("<DSLVStates>\n");
+ mNumFlushes = 0;
+ mTracking = true;
+ }
+
+ public void appendState() {
+ if (!mTracking) {
+ return;
+ }
+
+ mBuilder.append("<DSLVState>\n");
+ final int children = getChildCount();
+ final int first = getFirstVisiblePosition();
+ final ItemHeights itemHeights = new ItemHeights();
+ mBuilder.append(" <Positions>");
+ for (int i = 0; i < children; ++i) {
+ mBuilder.append(first + i).append(",");
+ }
+ mBuilder.append("</Positions>\n");
+
+ mBuilder.append(" <Tops>");
+ for (int i = 0; i < children; ++i) {
+ mBuilder.append(getChildAt(i).getTop()).append(",");
+ }
+ mBuilder.append("</Tops>\n");
+ mBuilder.append(" <Bottoms>");
+ for (int i = 0; i < children; ++i) {
+ mBuilder.append(getChildAt(i).getBottom()).append(",");
+ }
+ mBuilder.append("</Bottoms>\n");
+
+ mBuilder.append(" <FirstExpPos>").append(mFirstExpPos).append("</FirstExpPos>\n");
+ getItemHeights(mFirstExpPos, itemHeights);
+ mBuilder.append(" <FirstExpBlankHeight>")
+ .append(itemHeights.item - itemHeights.child)
+ .append("</FirstExpBlankHeight>\n");
+ mBuilder.append(" <SecondExpPos>").append(mSecondExpPos).append("</SecondExpPos>\n");
+ getItemHeights(mSecondExpPos, itemHeights);
+ mBuilder.append(" <SecondExpBlankHeight>")
+ .append(itemHeights.item - itemHeights.child)
+ .append("</SecondExpBlankHeight>\n");
+ mBuilder.append(" <SrcPos>").append(mSrcPos).append("</SrcPos>\n");
+ mBuilder.append(" <SrcHeight>").append(mFloatViewHeight + getDividerHeight())
+ .append("</SrcHeight>\n");
+ mBuilder.append(" <ViewHeight>").append(getHeight()).append("</ViewHeight>\n");
+ mBuilder.append(" <LastY>").append(mLastY).append("</LastY>\n");
+ mBuilder.append(" <FloatY>").append(mFloatViewMid).append("</FloatY>\n");
+ mBuilder.append(" <ShuffleEdges>");
+ for (int i = 0; i < children; ++i) {
+ mBuilder.append(getShuffleEdge(first + i, getChildAt(i).getTop())).append(",");
+ }
+ mBuilder.append("</ShuffleEdges>\n");
+
+ mBuilder.append("</DSLVState>\n");
+ mNumInBuffer++;
+
+ if (mNumInBuffer > 1000) {
+ flush();
+ mNumInBuffer = 0;
+ }
+ }
+
+ public void flush() {
+ if (!mTracking) {
+ return;
+ }
+
+ // save to file on sdcard
+ try {
+ boolean append = true;
+ if (mNumFlushes == 0) {
+ append = false;
+ }
+ final FileWriter writer = new FileWriter(mFile, append);
+
+ writer.write(mBuilder.toString());
+ mBuilder.delete(0, mBuilder.length());
+
+ writer.flush();
+ writer.close();
+
+ mNumFlushes++;
+ } catch (final IOException e) {
+ // do nothing
+ }
+ }
+
+ public void stopTracking() {
+ if (mTracking) {
+ mBuilder.append("</DSLVStates>\n");
+ flush();
+ mTracking = false;
+ }
+ }
+
+ }
+
+}
diff --git a/src/com/andrew/apollo/dragdrop/SimpleFloatViewManager.java b/src/com/andrew/apollo/dragdrop/SimpleFloatViewManager.java
new file mode 100644
index 0000000..5869a63
--- /dev/null
+++ b/src/com/andrew/apollo/dragdrop/SimpleFloatViewManager.java
@@ -0,0 +1,77 @@
+
+package com.andrew.apollo.dragdrop;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.Point;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.ListView;
+
+/**
+ * Simple implementation of the FloatViewManager class. Uses list items as they
+ * appear in the ListView to create the floating View.
+ */
+public class SimpleFloatViewManager implements DragSortListView.FloatViewManager {
+
+ private final ListView mListView;
+
+ private Bitmap mFloatBitmap;
+
+ private int mFloatBGColor = Color.BLACK;
+
+ public SimpleFloatViewManager(ListView lv) {
+ mListView = lv;
+ }
+
+ public void setBackgroundColor(int color) {
+ mFloatBGColor = color;
+ }
+
+ /**
+ * This simple implementation creates a Bitmap copy of the list item
+ * currently shown at ListView <code>position</code>.
+ */
+ @Override
+ public View onCreateFloatView(int position) {
+ View v = mListView.getChildAt(position + mListView.getHeaderViewsCount()
+ - mListView.getFirstVisiblePosition());
+
+ if (v == null) {
+ return null;
+ }
+
+ v.setPressed(false);
+
+ v.setDrawingCacheEnabled(true);
+ mFloatBitmap = Bitmap.createBitmap(v.getDrawingCache());
+ v.setDrawingCacheEnabled(false);
+
+ ImageView iv = new ImageView(mListView.getContext());
+ iv.setBackgroundColor(mFloatBGColor);
+ iv.setPadding(0, 0, 0, 0);
+ iv.setImageBitmap(mFloatBitmap);
+
+ return iv;
+ }
+
+ /**
+ * Removes the Bitmap from the ImageView created in onCreateFloatView() and
+ * tells the system to recycle it.
+ */
+ @Override
+ public void onDestroyFloatView(View floatView) {
+ ((ImageView)floatView).setImageDrawable(null);
+
+ mFloatBitmap.recycle();
+ mFloatBitmap = null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onDragFloatView(View floatView, Point position, Point touch) {
+ /* Nothing to do */
+ }
+}
diff --git a/src/com/andrew/apollo/format/Capitalize.java b/src/com/andrew/apollo/format/Capitalize.java
new file mode 100644
index 0000000..959c151
--- /dev/null
+++ b/src/com/andrew/apollo/format/Capitalize.java
@@ -0,0 +1,60 @@
+
+package com.andrew.apollo.format;
+
+import android.text.TextUtils;
+
+public class Capitalize {
+
+ /* This class is never initiated */
+ public Capitalize() {
+ }
+
+ public static final String capitalize(String str) {
+ return capitalize(str, null);
+ }
+
+ /**
+ * Capitalizes the first character in a string
+ *
+ * @param str The string to capitalize
+ * @param delimiters The delimiters
+ * @return A captitalized string
+ */
+ public static final String capitalize(String str, char... delimiters) {
+ final int delimLen = delimiters == null ? -1 : delimiters.length;
+ if (TextUtils.isEmpty(str) || delimLen == 0) {
+ return str;
+ }
+ final char[] buffer = str.toCharArray();
+ boolean capitalizeNext = true;
+ for (int i = 0; i < buffer.length; i++) {
+ char ch = buffer[i];
+ if (isDelimiter(ch, delimiters)) {
+ capitalizeNext = true;
+ } else if (capitalizeNext) {
+ buffer[i] = Character.toTitleCase(ch);
+ capitalizeNext = false;
+ }
+ }
+ return new String(buffer);
+ }
+
+ /**
+ * Is the character a delimiter.
+ *
+ * @param ch the character to check
+ * @param delimiters the delimiters
+ * @return true if it is a delimiter
+ */
+ private static final boolean isDelimiter(char ch, char[] delimiters) {
+ if (delimiters == null) {
+ return Character.isWhitespace(ch);
+ }
+ for (char delimiter : delimiters) {
+ if (ch == delimiter) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/com/andrew/apollo/format/PrefixHighlighter.java b/src/com/andrew/apollo/format/PrefixHighlighter.java
new file mode 100644
index 0000000..b58d45a
--- /dev/null
+++ b/src/com/andrew/apollo/format/PrefixHighlighter.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.andrew.apollo.format;
+
+import android.content.Context;
+import android.text.SpannableString;
+import android.text.TextUtils;
+import android.text.style.ForegroundColorSpan;
+import android.widget.TextView;
+
+import com.andrew.apollo.utils.PreferenceUtils;
+
+/**
+ * Highlights the text in a text field.
+ */
+public class PrefixHighlighter {
+
+ /* Color used when highlighting the prefixes */
+ private final int mPrefixHighlightColor;
+
+ private ForegroundColorSpan mPrefixColorSpan;
+
+ /**
+ * @param prefixHighlightColor The color used to highlight the prefixes.
+ */
+ public PrefixHighlighter(final Context context) {
+ mPrefixHighlightColor = PreferenceUtils.getInstace(context).getDefaultThemeColor(context);
+ }
+
+ /**
+ * Sets the text on the given {@link TextView}, highlighting the word that
+ * matches the given prefix.
+ *
+ * @param view The {@link TextView} on which to set the text
+ * @param text The string to use as the text
+ * @param prefix The prefix to look for
+ */
+ public void setText(final TextView view, final String text, final char[] prefix) {
+ if (view == null || TextUtils.isEmpty(text) || prefix == null || prefix.length == 0) {
+ return;
+ }
+ view.setText(apply(text, prefix));
+ }
+
+ /**
+ * Returns a {@link CharSequence} which highlights the given prefix if found
+ * in the given text.
+ *
+ * @param text the text to which to apply the highlight
+ * @param prefix the prefix to look for
+ */
+ public CharSequence apply(final CharSequence text, final char[] prefix) {
+ final int mIndex = indexOfWordPrefix(text, prefix);
+ if (mIndex != -1) {
+ if (mPrefixColorSpan == null) {
+ mPrefixColorSpan = new ForegroundColorSpan(mPrefixHighlightColor);
+ }
+ final SpannableString mResult = new SpannableString(text);
+ mResult.setSpan(mPrefixColorSpan, mIndex, mIndex + prefix.length, 0);
+ return mResult;
+ } else {
+ return text;
+ }
+ }
+
+ /**
+ * Finds the index of the first word that starts with the given prefix. If
+ * not found, returns -1.
+ *
+ * @param text the text in which to search for the prefix
+ * @param prefix the text to find, in upper case letters
+ */
+ private int indexOfWordPrefix(final CharSequence text, final char[] prefix) {
+ if (TextUtils.isEmpty(text) || prefix == null) {
+ return -1;
+ }
+
+ final int mTextLength = text.length();
+ final int mPrefixLength = prefix.length;
+
+ if (mPrefixLength == 0 || mTextLength < mPrefixLength) {
+ return -1;
+ }
+
+ int i = 0;
+ while (i < mTextLength) {
+ /* Skip non-word characters */
+ while (i < mTextLength && !Character.isLetterOrDigit(text.charAt(i))) {
+ i++;
+ }
+
+ if (i + mPrefixLength > mTextLength) {
+ return -1;
+ }
+
+ /* Compare the prefixes */
+ int j;
+ for (j = 0; j < mPrefixLength; j++) {
+ if (Character.toUpperCase(text.charAt(i + j)) != prefix[j]) {
+ break;
+ }
+ }
+ if (j == mPrefixLength) {
+ return i;
+ }
+
+ /* Skip this word */
+ while (i < mTextLength && Character.isLetterOrDigit(text.charAt(i))) {
+ i++;
+ }
+ }
+ return -1;
+ }
+
+}
diff --git a/src/com/andrew/apollo/grid/fragments/AlbumsFragment.java b/src/com/andrew/apollo/grid/fragments/AlbumsFragment.java
deleted file mode 100644
index eb2bd19..0000000
--- a/src/com/andrew/apollo/grid/fragments/AlbumsFragment.java
+++ /dev/null
@@ -1,263 +0,0 @@
-/**
- *
- */
-
-package com.andrew.apollo.grid.fragments;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.provider.BaseColumns;
-import android.provider.MediaStore.Audio;
-import android.provider.MediaStore.Audio.AlbumColumns;
-import android.support.v4.app.Fragment;
-import android.support.v4.app.LoaderManager.LoaderCallbacks;
-import android.support.v4.content.CursorLoader;
-import android.support.v4.content.Loader;
-import android.view.ContextMenu;
-import android.view.ContextMenu.ContextMenuInfo;
-import android.view.LayoutInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.AdapterView;
-import android.widget.AdapterView.OnItemClickListener;
-import android.widget.GridView;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import com.andrew.apollo.Constants;
-import com.andrew.apollo.R;
-import com.andrew.apollo.activities.TracksBrowser;
-import com.andrew.apollo.adapters.AlbumAdapter;
-import com.andrew.apollo.service.ApolloService;
-import com.andrew.apollo.tasks.GetCachedImages;
-import com.andrew.apollo.tasks.LastfmGetAlbumImages;
-import com.andrew.apollo.utils.ApolloUtils;
-import com.andrew.apollo.utils.MusicUtils;
-
-/**
- * @author Andrew Neal
- */
-public class AlbumsFragment extends Fragment implements LoaderCallbacks<Cursor>, Constants,
- OnItemClickListener {
-
- // Adapter
- private AlbumAdapter mAlbumAdapter;
-
- // GridView
- private GridView mGridView;
-
- // Cursor
- private Cursor mCursor;
-
- // Options
- private final int PLAY_SELECTION = 3;
-
- private final int ADD_TO_PLAYLIST = 4;
-
- private final int SEARCH = 5;
-
- // Album ID
- private String mCurrentAlbumId;
-
- // Audio columns
- public static int mAlbumIdIndex, mAlbumNameIndex, mArtistNameIndex;
-
- // Bundle
- public AlbumsFragment() {
- }
-
- public AlbumsFragment(Bundle args) {
- setArguments(args);
- }
-
- @Override
- public void onActivityCreated(Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
- // AlbumAdapter
- mAlbumAdapter = new AlbumAdapter(getActivity(), R.layout.gridview_items, null,
- new String[] {}, new int[] {}, 0);
- mGridView.setOnCreateContextMenuListener(this);
- mGridView.setOnItemClickListener(this);
- mGridView.setTextFilterEnabled(true);
- mGridView.setAdapter(mAlbumAdapter);
-
- // Important!
- getLoaderManager().initLoader(0, null, this);
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
- View root = inflater.inflate(R.layout.gridview, container, false);
- mGridView = (GridView)root.findViewById(R.id.gridview);
- return root;
- }
-
- @Override
- public Loader<Cursor> onCreateLoader(int id, Bundle args) {
- String[] projection = {
- BaseColumns._ID, AlbumColumns.ALBUM, AlbumColumns.ARTIST, AlbumColumns.ALBUM_ART
- };
- Uri uri = Audio.Albums.EXTERNAL_CONTENT_URI;
- String sortOrder = Audio.Albums.DEFAULT_SORT_ORDER;
- return new CursorLoader(getActivity(), uri, projection, null, null, sortOrder);
- }
-
- @Override
- public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
- // Check for database errors
- if (data == null) {
- return;
- }
-
- mAlbumIdIndex = data.getColumnIndexOrThrow(BaseColumns._ID);
- mAlbumNameIndex = data.getColumnIndexOrThrow(AlbumColumns.ALBUM);
- mArtistNameIndex = data.getColumnIndexOrThrow(AlbumColumns.ARTIST);
- mAlbumAdapter.changeCursor(data);
- mCursor = data;
- }
-
- @Override
- public void onLoaderReset(Loader<Cursor> loader) {
- if (mAlbumAdapter != null)
- mAlbumAdapter.changeCursor(null);
- }
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- outState.putAll(getArguments() != null ? getArguments() : new Bundle());
- super.onSaveInstanceState(outState);
- }
-
- @Override
- public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
- tracksBrowser(id);
- }
-
- /**
- * Update the list as needed
- */
- private final BroadcastReceiver mMediaStatusReceiver = new BroadcastReceiver() {
-
- @Override
- public void onReceive(Context context, Intent intent) {
- if (mGridView != null) {
- mAlbumAdapter.notifyDataSetChanged();
- }
- }
-
- };
-
- @Override
- public void onStart() {
- super.onStart();
- IntentFilter filter = new IntentFilter();
- filter.addAction(ApolloService.META_CHANGED);
- filter.addAction(ApolloService.PLAYSTATE_CHANGED);
- getActivity().registerReceiver(mMediaStatusReceiver, filter);
- }
-
- @Override
- public void onStop() {
- getActivity().unregisterReceiver(mMediaStatusReceiver);
- super.onStop();
- }
-
- /**
- * @param index
- * @param id
- */
- private void tracksBrowser(long id) {
-
- String artistName = mCursor.getString(mArtistNameIndex);
- String albumName = mCursor.getString(mAlbumNameIndex);
-
- Bundle bundle = new Bundle();
- bundle.putString(MIME_TYPE, Audio.Albums.CONTENT_TYPE);
- bundle.putString(ARTIST_KEY, artistName);
- bundle.putString(ALBUM_KEY, albumName);
- bundle.putLong(BaseColumns._ID, id);
-
- Intent intent = new Intent(Intent.ACTION_VIEW);
- intent.setClass(getActivity(), TracksBrowser.class);
- intent.putExtras(bundle);
- getActivity().startActivity(intent);
- }
-
- @Override
- public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
- menu.add(0, PLAY_SELECTION, 0, getResources().getString(R.string.play_all));
- menu.add(0, ADD_TO_PLAYLIST, 0, getResources().getString(R.string.add_to_playlist));
- menu.add(0, SEARCH, 0, getResources().getString(R.string.search));
-
- mCurrentAlbumId = mCursor.getString(mCursor.getColumnIndexOrThrow(BaseColumns._ID));
-
- menu.setHeaderView(setHeaderLayout());
- super.onCreateContextMenu(menu, v, menuInfo);
- }
-
- @Override
- public boolean onContextItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case PLAY_SELECTION: {
- long[] list = MusicUtils.getSongListForAlbum(getActivity(),
- Long.parseLong(mCurrentAlbumId));
- MusicUtils.playAll(getActivity(), list, 0);
- break;
- }
- case ADD_TO_PLAYLIST: {
- Intent intent = new Intent(INTENT_ADD_TO_PLAYLIST);
- long[] list = MusicUtils.getSongListForAlbum(getActivity(),
- Long.parseLong(mCurrentAlbumId));
- intent.putExtra(INTENT_PLAYLIST_LIST, list);
- getActivity().startActivity(intent);
- break;
- }
- case SEARCH: {
- MusicUtils.doSearch(getActivity(), mCursor, mAlbumNameIndex);
- break;
- }
- default:
- break;
- }
- return super.onContextItemSelected(item);
- }
-
- /**
- * @return A custom ContextMenu header
- */
- public View setHeaderLayout() {
- // Get album name
- String albumName = mCursor.getString(mAlbumNameIndex);
- // Get artist name
- String artistName = mCursor.getString(mArtistNameIndex);
-
- // Inflate the header View
- LayoutInflater inflater = getActivity().getLayoutInflater();
- View header = inflater.inflate(R.layout.context_menu_header, null, false);
-
- // Artist image
- ImageView headerImage = (ImageView)header.findViewById(R.id.header_image);
-
- // Only download images we don't already have
- if (ApolloUtils.getImageURL(albumName, ALBUM_IMAGE, getActivity()) == null)
- new LastfmGetAlbumImages(getActivity(), null, 0).executeOnExecutor(
- AsyncTask.THREAD_POOL_EXECUTOR, artistName, albumName);
-
- // Get and set cached image
- new GetCachedImages(getActivity(), 1, headerImage).executeOnExecutor(
- AsyncTask.THREAD_POOL_EXECUTOR, albumName);
-
- // Set artist name
- TextView headerText = (TextView)header.findViewById(R.id.header_text);
- headerText.setText(albumName);
- headerText.setBackgroundColor(getResources().getColor(R.color.transparent_black));
- return header;
- }
-}
diff --git a/src/com/andrew/apollo/grid/fragments/ArtistsFragment.java b/src/com/andrew/apollo/grid/fragments/ArtistsFragment.java
deleted file mode 100644
index 5b18e49..0000000
--- a/src/com/andrew/apollo/grid/fragments/ArtistsFragment.java
+++ /dev/null
@@ -1,272 +0,0 @@
-/**
- *
- */
-
-package com.andrew.apollo.grid.fragments;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.provider.BaseColumns;
-import android.provider.MediaStore.Audio;
-import android.provider.MediaStore.Audio.ArtistColumns;
-import android.support.v4.app.Fragment;
-import android.support.v4.app.LoaderManager.LoaderCallbacks;
-import android.support.v4.content.CursorLoader;
-import android.support.v4.content.Loader;
-import android.view.ContextMenu;
-import android.view.ContextMenu.ContextMenuInfo;
-import android.view.LayoutInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.AdapterView;
-import android.widget.AdapterView.OnItemClickListener;
-import android.widget.GridView;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import com.andrew.apollo.Constants;
-import com.andrew.apollo.R;
-import com.andrew.apollo.activities.TracksBrowser;
-import com.andrew.apollo.adapters.ArtistAdapter;
-import com.andrew.apollo.service.ApolloService;
-import com.andrew.apollo.tasks.GetCachedImages;
-import com.andrew.apollo.tasks.LastfmGetArtistImagesOriginal;
-import com.andrew.apollo.utils.ApolloUtils;
-import com.andrew.apollo.utils.MusicUtils;
-
-/**
- * @author Andrew Neal
- * @Note This is the first tab
- */
-public class ArtistsFragment extends Fragment implements LoaderCallbacks<Cursor>,
- OnItemClickListener, Constants {
-
- // Adapter
- private ArtistAdapter mArtistAdapter;
-
- // GridView
- private GridView mGridView;
-
- // Cursor
- private Cursor mCursor;
-
- // Options
- private final int PLAY_SELECTION = 0;
-
- private final int ADD_TO_PLAYLIST = 1;
-
- private final int SEARCH = 2;
-
- // Artist ID
- private String mCurrentArtistId;
-
- // Album ID
- private String mCurrentAlbumId;
-
- // Audio columns
- public static int mArtistIdIndex, mArtistNameIndex, mArtistNumAlbumsIndex;
-
- public ArtistsFragment() {
- }
-
- public ArtistsFragment(Bundle bundle) {
- setArguments(bundle);
- }
-
- @Override
- public void onActivityCreated(Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
- // ArtistAdapter
- mArtistAdapter = new ArtistAdapter(getActivity(), R.layout.gridview_items, null,
- new String[] {}, new int[] {}, 0);
- mGridView.setOnCreateContextMenuListener(this);
- mGridView.setOnItemClickListener(this);
- mGridView.setAdapter(mArtistAdapter);
- mGridView.setTextFilterEnabled(true);
-
- // Important!
- getLoaderManager().initLoader(0, null, this);
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
- View root = inflater.inflate(R.layout.gridview, container, false);
- mGridView = ((GridView)root.findViewById(R.id.gridview));
- return root;
- }
-
- @Override
- public Loader<Cursor> onCreateLoader(int id, Bundle args) {
- String[] projection = {
- BaseColumns._ID, ArtistColumns.ARTIST, ArtistColumns.NUMBER_OF_ALBUMS
- };
- Uri uri = Audio.Artists.EXTERNAL_CONTENT_URI;
- String sortOrder = Audio.Artists.DEFAULT_SORT_ORDER;
- return new CursorLoader(getActivity(), uri, projection, null, null, sortOrder);
- }
-
- @Override
- public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
- // Check for database errors
- if (data == null) {
- return;
- }
-
- mArtistIdIndex = data.getColumnIndexOrThrow(BaseColumns._ID);
- mArtistNameIndex = data.getColumnIndexOrThrow(ArtistColumns.ARTIST);
- mArtistNumAlbumsIndex = data.getColumnIndexOrThrow(ArtistColumns.NUMBER_OF_ALBUMS);
- mArtistAdapter.changeCursor(data);
- mCursor = data;
- }
-
- @Override
- public void onLoaderReset(Loader<Cursor> loader) {
- if (mArtistAdapter != null)
- mArtistAdapter.changeCursor(null);
- }
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- outState.putAll(getArguments() != null ? getArguments() : new Bundle());
- super.onSaveInstanceState(outState);
- }
-
- @Override
- public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
- tracksBrowser(id);
- }
-
- /**
- * @param id
- */
- private void tracksBrowser(long id) {
-
- String artistName = mCursor.getString(mArtistNameIndex);
-
- Bundle bundle = new Bundle();
- bundle.putString(MIME_TYPE, Audio.Artists.CONTENT_TYPE);
- bundle.putString(ARTIST_KEY, artistName);
- bundle.putLong(BaseColumns._ID, id);
-
- ApolloUtils.setArtistId(artistName, id, ARTIST_ID, getActivity());
-
- Intent intent = new Intent(Intent.ACTION_VIEW);
- intent.setClass(getActivity(), TracksBrowser.class);
- intent.putExtras(bundle);
- getActivity().startActivity(intent);
- }
-
- @Override
- public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
- menu.add(0, PLAY_SELECTION, 0, getResources().getString(R.string.play_all));
- menu.add(0, ADD_TO_PLAYLIST, 0, getResources().getString(R.string.add_to_playlist));
- menu.add(0, SEARCH, 0, getResources().getString(R.string.search));
-
- mCurrentArtistId = mCursor.getString(mArtistIdIndex);
- mCurrentAlbumId = mCursor.getString(mCursor.getColumnIndexOrThrow(BaseColumns._ID));
-
- menu.setHeaderView(setHeaderLayout());
- super.onCreateContextMenu(menu, v, menuInfo);
- }
-
- @Override
- public boolean onContextItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case PLAY_SELECTION: {
- long[] list = mCurrentArtistId != null ? MusicUtils.getSongListForArtist(
- getActivity(), Long.parseLong(mCurrentArtistId)) : MusicUtils
- .getSongListForAlbum(getActivity(), Long.parseLong(mCurrentAlbumId));
- MusicUtils.playAll(getActivity(), list, 0);
- break;
- }
- case ADD_TO_PLAYLIST: {
- Intent intent = new Intent(INTENT_ADD_TO_PLAYLIST);
- long[] list = mCurrentArtistId != null ? MusicUtils.getSongListForArtist(
- getActivity(), Long.parseLong(mCurrentArtistId)) : MusicUtils
- .getSongListForAlbum(getActivity(), Long.parseLong(mCurrentAlbumId));
- intent.putExtra(INTENT_PLAYLIST_LIST, list);
- getActivity().startActivity(intent);
- break;
- }
- case SEARCH: {
- MusicUtils.doSearch(getActivity(), mCursor, mArtistNameIndex);
- break;
- }
- default:
- break;
- }
- return super.onContextItemSelected(item);
- }
-
- /**
- * Update the list as needed
- */
- private final BroadcastReceiver mMediaStatusReceiver = new BroadcastReceiver() {
-
- @Override
- public void onReceive(Context context, Intent intent) {
- if (mGridView != null) {
- mArtistAdapter.notifyDataSetChanged();
- }
- }
-
- };
-
- @Override
- public void onStart() {
- super.onStart();
- IntentFilter filter = new IntentFilter();
- filter.addAction(ApolloService.META_CHANGED);
- filter.addAction(ApolloService.PLAYSTATE_CHANGED);
- getActivity().registerReceiver(mMediaStatusReceiver, filter);
- }
-
- @Override
- public void onStop() {
- getActivity().unregisterReceiver(mMediaStatusReceiver);
- super.onStop();
- }
-
- /**
- * @return A custom ContextMenu header
- */
- public View setHeaderLayout() {
- // Get artist name
- final String artistName = mCursor.getString(mArtistNameIndex);
-
- // Inflate the header View
- LayoutInflater inflater = getActivity().getLayoutInflater();
- View header = inflater.inflate(R.layout.context_menu_header, null, false);
-
- // Artist image
- final ImageView mHanderImage = (ImageView)header.findViewById(R.id.header_image);
-
- mHanderImage.post(new Runnable() {
-
- @Override
- public void run() {
- // Only download images we don't already have
- if (ApolloUtils.getImageURL(artistName, ARTIST_IMAGE_ORIGINAL, getActivity()) == null)
- new LastfmGetArtistImagesOriginal(getActivity(), mHanderImage)
- .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, artistName);
-
- // Get and set cached image
- new GetCachedImages(getActivity(), 0, mHanderImage).executeOnExecutor(
- AsyncTask.THREAD_POOL_EXECUTOR, artistName);
- }
- });
-
- // Set artist name
- TextView headerText = (TextView)header.findViewById(R.id.header_text);
- headerText.setText(artistName);
- headerText.setBackgroundColor(getResources().getColor(R.color.transparent_black));
- return header;
- }
-}
diff --git a/src/com/andrew/apollo/grid/fragments/QuickQueueFragment.java b/src/com/andrew/apollo/grid/fragments/QuickQueueFragment.java
deleted file mode 100644
index 4e2c254..0000000
--- a/src/com/andrew/apollo/grid/fragments/QuickQueueFragment.java
+++ /dev/null
@@ -1,270 +0,0 @@
-/**
- *
- */
-
-package com.andrew.apollo.grid.fragments;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.Bundle;
-import android.provider.BaseColumns;
-import android.provider.MediaStore.Audio;
-import android.provider.MediaStore.Audio.AudioColumns;
-import android.provider.MediaStore.MediaColumns;
-import android.support.v4.app.Fragment;
-import android.support.v4.app.LoaderManager.LoaderCallbacks;
-import android.support.v4.content.CursorLoader;
-import android.support.v4.content.Loader;
-import android.view.ContextMenu;
-import android.view.ContextMenu.ContextMenuInfo;
-import android.view.LayoutInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.AdapterView;
-import android.widget.AdapterView.AdapterContextMenuInfo;
-import android.widget.AdapterView.OnItemClickListener;
-import android.widget.GridView;
-import android.widget.LinearLayout;
-
-import com.andrew.apollo.Constants;
-import com.andrew.apollo.NowPlayingCursor;
-import com.andrew.apollo.R;
-import com.andrew.apollo.adapters.QuickQueueAdapter;
-import com.andrew.apollo.service.ApolloService;
-import com.andrew.apollo.utils.MusicUtils;
-
-/**
- * @author Andrew Neal
- */
-public class QuickQueueFragment extends Fragment implements LoaderCallbacks<Cursor>,
- OnItemClickListener, Constants {
-
- // Adapter
- private QuickQueueAdapter mQuickQueueAdapter;
-
- // GridView
- private GridView mGridView;
-
- // Cursor
- private Cursor mCursor;
-
- // Selected position
- private int mSelectedPosition;
-
- // Options
- private final int PLAY_SELECTION = 0;
-
- private final int REMOVE = 1;
-
- // Audio columns
- public static int mTitleIndex, mAlbumIndex, mArtistIndex, mMediaIdIndex;
-
- // Bundle
- public QuickQueueFragment() {
- }
-
- public QuickQueueFragment(Bundle args) {
- setArguments(args);
- }
-
- @Override
- public void onActivityCreated(Bundle savedInstanceState) {
- // Adapter
- mQuickQueueAdapter = new QuickQueueAdapter(getActivity(), R.layout.quick_queue_items, null,
- new String[] {}, new int[] {}, 0);
- mGridView.setOnCreateContextMenuListener(this);
- mGridView.setOnItemClickListener(this);
- mGridView.setAdapter(mQuickQueueAdapter);
-
- // Important!
- getLoaderManager().initLoader(0, null, this);
- super.onActivityCreated(savedInstanceState);
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
- View root = inflater.inflate(R.layout.quick_queue, container, false);
- mGridView = (GridView)root.findViewById(R.id.gridview);
- mGridView.setNumColumns(1);
-
- LinearLayout mQueueHolder = (LinearLayout)root.findViewById(R.id.quick_queue_holder);
- mQueueHolder.setBackgroundColor(getResources().getColor(R.color.transparent_black));
- return root;
- }
-
- @Override
- public Loader<Cursor> onCreateLoader(int id, Bundle args) {
- String[] projection = new String[] {
- BaseColumns._ID, MediaColumns.TITLE, AudioColumns.ALBUM, AudioColumns.ARTIST,
- };
- StringBuilder selection = new StringBuilder();
- Uri uri = Audio.Media.EXTERNAL_CONTENT_URI;
- String sortOrder = Audio.Media.DEFAULT_SORT_ORDER;
- uri = Audio.Media.EXTERNAL_CONTENT_URI;
- long[] mNowPlaying = MusicUtils.getQueue();
- if (mNowPlaying.length == 0)
- return null;
- selection = new StringBuilder();
- selection.append(BaseColumns._ID + " IN (");
- if (mNowPlaying == null || mNowPlaying.length <= 0)
- return null;
- for (long queue_id : mNowPlaying) {
- selection.append(queue_id + ",");
- }
- selection.deleteCharAt(selection.length() - 1);
- selection.append(")");
-
- return new CursorLoader(getActivity(), uri, projection, selection.toString(), null,
- sortOrder);
- }
-
- @Override
- public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
- // Check for database errors
- if (data == null) {
- return;
- }
-
- mMediaIdIndex = data.getColumnIndexOrThrow(BaseColumns._ID);
- mTitleIndex = data.getColumnIndexOrThrow(MediaColumns.TITLE);
- mArtistIndex = data.getColumnIndexOrThrow(AudioColumns.ARTIST);
- mAlbumIndex = data.getColumnIndexOrThrow(AudioColumns.ALBUM);
- mQuickQueueAdapter.changeCursor(data);
- mCursor = data;
- }
-
- @Override
- public void onLoaderReset(Loader<Cursor> loader) {
- if (mQuickQueueAdapter != null)
- mQuickQueueAdapter.changeCursor(null);
- }
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- outState.putAll(getArguments() != null ? getArguments() : new Bundle());
- super.onSaveInstanceState(outState);
- }
-
- @Override
- public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
- menu.add(0, PLAY_SELECTION, 0, getResources().getString(R.string.play_all));
- menu.add(0, REMOVE, 0, getResources().getString(R.string.remove));
-
- AdapterContextMenuInfo mi = (AdapterContextMenuInfo)menuInfo;
- mSelectedPosition = mi.position;
- mCursor.moveToPosition(mSelectedPosition);
-
- String title = mCursor.getString(mTitleIndex);
- menu.setHeaderTitle(title);
- super.onCreateContextMenu(menu, v, menuInfo);
- }
-
- @Override
- public boolean onContextItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case PLAY_SELECTION:
- int position = mSelectedPosition;
- MusicUtils.playAll(getActivity(), mCursor, position);
- getActivity().finish();
- break;
- case REMOVE:
- removePlaylistItem(mSelectedPosition);
- break;
- default:
- break;
- }
- return super.onContextItemSelected(item);
- }
-
- @Override
- public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
- if (mCursor instanceof NowPlayingCursor) {
- if (MusicUtils.mService != null) {
- MusicUtils.setQueuePosition(position);
- }
- }
- MusicUtils.playAll(getActivity(), mCursor, position);
- getActivity().finish();
- }
-
- /**
- * @param which
- */
- private void removePlaylistItem(int which) {
- mCursor.moveToPosition(which);
- long id = mCursor.getLong(mMediaIdIndex);
- MusicUtils.removeTrack(id);
- reloadQueueCursor();
- mGridView.invalidateViews();
- }
-
- /**
- * Reload the queue after we remove a track
- */
- private void reloadQueueCursor() {
- String[] projection = new String[] {
- BaseColumns._ID, MediaColumns.TITLE, AudioColumns.ALBUM, AudioColumns.ARTIST,
- };
- StringBuilder selection = new StringBuilder();
- Uri uri = Audio.Media.EXTERNAL_CONTENT_URI;
- String sortOrder = Audio.Media.DEFAULT_SORT_ORDER;
- uri = Audio.Media.EXTERNAL_CONTENT_URI;
- long[] mNowPlaying = MusicUtils.getQueue();
- if (mNowPlaying.length == 0)
- return;
- selection = new StringBuilder();
- selection.append(BaseColumns._ID + " IN (");
- if (mNowPlaying == null || mNowPlaying.length <= 0)
- return;
- for (long queue_id : mNowPlaying) {
- selection.append(queue_id + ",");
- }
- selection.deleteCharAt(selection.length() - 1);
- selection.append(")");
-
- mCursor = MusicUtils.query(getActivity(), uri, projection, selection.toString(), null,
- sortOrder);
- mQuickQueueAdapter.changeCursor(mCursor);
- }
-
- /**
- * Update the list as needed
- */
- private final BroadcastReceiver mMediaStatusReceiver = new BroadcastReceiver() {
-
- @Override
- public void onReceive(Context context, Intent intent) {
- if (mGridView != null) {
- mQuickQueueAdapter.notifyDataSetChanged();
- // Scroll to the currently playing track in the queue
- mGridView.postDelayed(new Runnable() {
- @Override
- public void run() {
- mGridView.setSelection(MusicUtils.getQueuePosition());
- }
- }, 100);
- }
- }
-
- };
-
- @Override
- public void onStart() {
- super.onStart();
- IntentFilter filter = new IntentFilter();
- filter.addAction(ApolloService.META_CHANGED);
- filter.addAction(ApolloService.QUEUE_CHANGED);
- getActivity().registerReceiver(mMediaStatusReceiver, filter);
- }
-
- @Override
- public void onStop() {
- getActivity().unregisterReceiver(mMediaStatusReceiver);
- super.onStop();
- }
-}
diff --git a/src/com/andrew/apollo/lastfm/Album.java b/src/com/andrew/apollo/lastfm/Album.java
new file mode 100644
index 0000000..2a10c0e
--- /dev/null
+++ b/src/com/andrew/apollo/lastfm/Album.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (c) 2012, the Last.fm Java Project and Committers All rights
+ * reserved. Redistribution and use of this software in source and binary forms,
+ * with or without modification, are permitted provided that the following
+ * conditions are met: - Redistributions of source code must retain the above
+ * copyright notice, this list of conditions and the following disclaimer. -
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution. THIS SOFTWARE IS
+ * PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.andrew.apollo.lastfm;
+
+import android.content.Context;
+
+import com.andrew.apollo.Config;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Wrapper class for Album related API calls and Album Bean.
+ *
+ * @author Janni Kovacs
+ */
+public class Album extends MusicEntry {
+
+ protected final static ItemFactory<Album> FACTORY = new AlbumFactory();
+
+ private String artist;
+
+ /**
+ * @param name
+ * @param url
+ * @param artist
+ */
+ private Album(final String name, final String url, final String artist) {
+ super(name, url);
+ this.artist = artist;
+ }
+
+ /**
+ * Get the metadata for an album on Last.fm using the album name or a
+ * musicbrainz id. See playlist.fetch on how to get the album playlist.
+ *
+ * @param artist Artist's name
+ * @param albumOrMbid Album name or MBID
+ * @return Album metadata
+ */
+ public final static Album getInfo(final Context context, final String artist,
+ final String albumOrMbid) {
+ return getInfo(context, artist, albumOrMbid, null, Config.LASTFM_API_KEY);
+ }
+
+ /**
+ * Get the metadata for an album on Last.fm using the album name or a
+ * musicbrainz id. See playlist.fetch on how to get the album playlist.
+ *
+ * @param artist Artist's name
+ * @param albumOrMbid Album name or MBID
+ * @param username The username for the context of the request. If supplied,
+ * the user's playcount for this album is included in the
+ * response.
+ * @param apiKey The API key
+ * @return Album metadata
+ */
+ public final static Album getInfo(final Context context, final String artist,
+ final String albumOrMbid, final String username, final String apiKey) {
+ final Map<String, String> params = new HashMap<String, String>();
+ params.put("artist", artist);
+ params.put("album", albumOrMbid);
+ MapUtilities.nullSafePut(params, "username", username);
+ final Result result = Caller.getInstance(context).call("album.getInfo", apiKey, params);
+ return ResponseBuilder.buildItem(result, Album.class);
+ }
+
+ private final static class AlbumFactory implements ItemFactory<Album> {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Album createItemFromElement(final DomElement element) {
+ if (element == null) {
+ return null;
+ }
+ final Album album = new Album(null, null, null);
+ MusicEntry.loadStandardInfo(album, element);
+ if (element.hasChild("artist")) {
+ album.artist = element.getChild("artist").getChildText("name");
+ if (album.artist == null) {
+ album.artist = element.getChildText("artist");
+ }
+ }
+ return album;
+ }
+ }
+}
diff --git a/src/com/andrew/apollo/lastfm/Artist.java b/src/com/andrew/apollo/lastfm/Artist.java
new file mode 100644
index 0000000..b049bfd
--- /dev/null
+++ b/src/com/andrew/apollo/lastfm/Artist.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (c) 2012, the Last.fm Java Project and Committers All rights
+ * reserved. Redistribution and use of this software in source and binary forms,
+ * with or without modification, are permitted provided that the following
+ * conditions are met: - Redistributions of source code must retain the above
+ * copyright notice, this list of conditions and the following disclaimer. -
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution. THIS SOFTWARE IS
+ * PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.andrew.apollo.lastfm;
+
+import android.content.Context;
+
+import com.andrew.apollo.Config;
+
+import java.util.Locale;
+import java.util.Map;
+import java.util.WeakHashMap;
+
+/**
+ * Bean that contains artist information.<br/>
+ * This class contains static methods that executes API methods relating to
+ * artists.<br/>
+ * Method names are equivalent to the last.fm API method names.
+ *
+ * @author Janni Kovacs
+ */
+public class Artist extends MusicEntry {
+
+ protected final static ItemFactory<Artist> FACTORY = new ArtistFactory();
+
+ protected Artist(final String name, final String url) {
+ super(name, url);
+ }
+
+ /**
+ * Retrieves detailed artist info for the given artist or mbid entry.
+ *
+ * @param artistOrMbid Name of the artist or an mbid
+ * @param apiKey The API key
+ * @return detailed artist info
+ */
+ public final static Artist getInfo(final Context context, final String artistOrMbid,
+ final String apiKey) {
+ return getInfo(context, artistOrMbid, Locale.getDefault(), apiKey);
+ }
+
+ /**
+ * Retrieves detailed artist info for the given artist or mbid entry.
+ *
+ * @param artistOrMbid Name of the artist or an mbid
+ * @param locale The language to fetch info in, or <code>null</code>
+ * @param username The username for the context of the request, or
+ * <code>null</code>. If supplied, the user's playcount for this
+ * artist is included in the response
+ * @param apiKey The API key
+ * @return detailed artist info
+ */
+ public final static Artist getInfo(final Context context, final String artistOrMbid,
+ final Locale locale, final String apiKey) {
+ final Map<String, String> mParams = new WeakHashMap<String, String>();
+ mParams.put("artist", artistOrMbid);
+ if (locale != null && locale.getLanguage().length() != 0) {
+ mParams.put("lang", locale.getLanguage());
+ }
+ final Result mResult = Caller.getInstance(context).call("artist.getInfo", apiKey, mParams);
+ return ResponseBuilder.buildItem(mResult, Artist.class);
+ }
+
+ /**
+ * Use the last.fm corrections data to check whether the supplied artist has
+ * a correction to a canonical artist. This method returns a new
+ * {@link Artist} object containing the corrected data, or <code>null</code>
+ * if the supplied Artist was not found.
+ *
+ * @param artist The artist name to correct
+ * @return a new {@link Artist}, or <code>null</code>
+ */
+ public final static Artist getCorrection(final Context context, final String artist) {
+ Result result = null;
+ try {
+ result = Caller.getInstance(context).call("artist.getCorrection",
+ Config.LASTFM_API_KEY, "artist", artist);
+ if (!result.isSuccessful()) {
+ return null;
+ }
+ final DomElement correctionElement = result.getContentElement().getChild("correction");
+ if (correctionElement == null) {
+ return new Artist(artist, null);
+ }
+ final DomElement artistElem = correctionElement.getChild("artist");
+ return FACTORY.createItemFromElement(artistElem);
+ } catch (final Exception ignored) {
+ return null;
+ }
+ }
+
+ /**
+ * Get {@link Image}s for this artist in a variety of sizes.
+ *
+ * @param artistOrMbid The artist name in question
+ * @return a list of {@link Image}s
+ */
+ public final static PaginatedResult<Image> getImages(final Context context,
+ final String artistOrMbid) {
+ return getImages(context, artistOrMbid, -1, -1, Config.LASTFM_API_KEY);
+ }
+
+ /**
+ * Get {@link Image}s for this artist in a variety of sizes.
+ *
+ * @param artistOrMbid The artist name in question
+ * @param page Which page of limit amount to display
+ * @param limit How many to return. Defaults and maxes out at 50
+ * @param apiKey A Last.fm API key
+ * @return a list of {@link Image}s
+ */
+ public final static PaginatedResult<Image> getImages(final Context context,
+ final String artistOrMbid, final int page, final int limit, final String apiKey) {
+ final Map<String, String> params = new WeakHashMap<String, String>();
+ params.put("artist", artistOrMbid);
+ MapUtilities.nullSafePut(params, "page", page);
+ MapUtilities.nullSafePut(params, "limit", limit);
+ Result result = null;
+ try {
+ result = Caller.getInstance(context).call("artist.getImages", apiKey, params);
+ } catch (final Exception ignored) {
+ return null;
+ }
+ return ResponseBuilder.buildPaginatedResult(result, Image.class);
+ }
+
+ private final static class ArtistFactory implements ItemFactory<Artist> {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Artist createItemFromElement(final DomElement element) {
+ if (element == null) {
+ return null;
+ }
+ final Artist artist = new Artist(null, null);
+ MusicEntry.loadStandardInfo(artist, element);
+ return artist;
+ }
+ }
+}
diff --git a/src/com/andrew/apollo/lastfm/Caller.java b/src/com/andrew/apollo/lastfm/Caller.java
new file mode 100644
index 0000000..b88674b
--- /dev/null
+++ b/src/com/andrew/apollo/lastfm/Caller.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright (c) 2012, the Last.fm Java Project and Committers All rights
+ * reserved. Redistribution and use of this software in source and binary forms,
+ * with or without modification, are permitted provided that the following
+ * conditions are met: - Redistributions of source code must retain the above
+ * copyright notice, this list of conditions and the following disclaimer. -
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution. THIS SOFTWARE IS
+ * PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.andrew.apollo.lastfm;
+
+import static com.andrew.apollo.lastfm.StringUtilities.encode;
+import static com.andrew.apollo.lastfm.StringUtilities.map;
+
+import android.content.Context;
+
+import com.andrew.apollo.lastfm.Result.Status;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.net.HttpURLConnection;
+import java.net.Proxy;
+import java.net.URL;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.WeakHashMap;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+/**
+ * The <code>Caller</code> class handles the low-level communication between the
+ * client and last.fm.<br/>
+ * Direct usage of this class should be unnecessary since all method calls are
+ * available via the methods in the <code>Artist</code>, <code>Album</code>,
+ * <code>User</code>, etc. classes. If specialized calls which are not covered
+ * by the Java API are necessary this class may be used directly.<br/>
+ * Supports the setting of a custom {@link Proxy} and a custom
+ * <code>User-Agent</code> HTTP header.
+ *
+ * @author Janni Kovacs
+ */
+public class Caller {
+
+ private final static String PARAM_API_KEY = "api_key";
+
+ private final static String DEFAULT_API_ROOT = "http://ws.audioscrobbler.com/2.0/";
+
+ private static Caller mInstance = null;
+
+ private final String apiRootUrl = DEFAULT_API_ROOT;
+
+ private final String userAgent = "Apollo";
+
+ private Result lastResult;
+
+ /**
+ * @param context The {@link Context} to use
+ */
+ private Caller(final Context context) {
+ }
+
+ /**
+ * @param context The {@link Context} to use
+ * @return A new instance of this class
+ */
+ public final static synchronized Caller getInstance(final Context context) {
+ if (mInstance == null) {
+ mInstance = new Caller(context.getApplicationContext());
+ }
+ return mInstance;
+ }
+
+ /**
+ * @param method
+ * @param apiKey
+ * @param params
+ * @return
+ * @throws CallException
+ */
+ public Result call(final String method, final String apiKey, final String... params) {
+ return call(method, apiKey, map(params));
+ }
+
+ /**
+ * Performs the web-service call. If the <code>session</code> parameter is
+ * <code>non-null</code> then an authenticated call is made. If it's
+ * <code>null</code> then an unauthenticated call is made.<br/>
+ * The <code>apiKey</code> parameter is always required, even when a valid
+ * session is passed to this method.
+ *
+ * @param method The method to call
+ * @param apiKey A Last.fm API key
+ * @param params Parameters
+ * @param session A Session instance or <code>null</code>
+ * @return the result of the operation
+ */
+ public Result call(final String method, final String apiKey, Map<String, String> params) {
+ params = new WeakHashMap<String, String>(params);
+ InputStream inputStream = null;
+
+ // no entry in cache, load from web
+ if (inputStream == null) {
+ // fill parameter map with apiKey and session info
+ params.put(PARAM_API_KEY, apiKey);
+ try {
+ final HttpURLConnection urlConnection = openPostConnection(method, params);
+ inputStream = getInputStreamFromConnection(urlConnection);
+
+ if (inputStream == null) {
+ lastResult = Result.createHttpErrorResult(urlConnection.getResponseCode(),
+ urlConnection.getResponseMessage());
+ return lastResult;
+ }
+ } catch (final IOException ignored) {
+ }
+ }
+
+ try {
+ final Result result = createResultFromInputStream(inputStream);
+ lastResult = result;
+ return result;
+ } catch (final IOException ignored) {
+ } catch (final SAXException ignored) {
+ }
+ return null;
+ }
+
+ /**
+ * Creates a new {@link HttpURLConnection}, sets the proxy, if available,
+ * and sets the User-Agent property.
+ *
+ * @param url URL to connect to
+ * @return a new connection.
+ * @throws IOException if an I/O exception occurs.
+ */
+ public HttpURLConnection openConnection(final String url) throws IOException {
+ final URL u = new URL(url);
+ HttpURLConnection urlConnection;
+ urlConnection = (HttpURLConnection)u.openConnection();
+ urlConnection.setRequestProperty("User-Agent", userAgent);
+ urlConnection.setUseCaches(true);
+ return urlConnection;
+ }
+
+ /**
+ * @param method
+ * @param params
+ * @return
+ * @throws IOException
+ */
+ private HttpURLConnection openPostConnection(final String method,
+ final Map<String, String> params) throws IOException {
+ final HttpURLConnection urlConnection = openConnection(apiRootUrl);
+ urlConnection.setRequestMethod("POST");
+ urlConnection.setDoOutput(true);
+ urlConnection.setUseCaches(true);
+ final OutputStream outputStream = urlConnection.getOutputStream();
+ final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream));
+ final String post = buildPostBody(method, params);
+ writer.write(post);
+ writer.close();
+ return urlConnection;
+ }
+
+ /**
+ * @param connection
+ * @return
+ * @throws IOException
+ */
+ private InputStream getInputStreamFromConnection(final HttpURLConnection connection)
+ throws IOException {
+ final int responseCode = connection.getResponseCode();
+
+ if (responseCode == HttpURLConnection.HTTP_FORBIDDEN
+ || responseCode == HttpURLConnection.HTTP_BAD_REQUEST) {
+ return connection.getErrorStream();
+ } else if (responseCode == HttpURLConnection.HTTP_OK) {
+ return connection.getInputStream();
+ }
+
+ return null;
+ }
+
+ /**
+ * @param inputStream
+ * @return
+ * @throws SAXException
+ * @throws IOException
+ */
+ private Result createResultFromInputStream(final InputStream inputStream) throws SAXException,
+ IOException {
+ final Document document = newDocumentBuilder().parse(
+ new InputSource(new InputStreamReader(inputStream, "UTF-8")));
+ final Element root = document.getDocumentElement(); // lfm element
+ final String statusString = root.getAttribute("status");
+ final Status status = "ok".equals(statusString) ? Status.OK : Status.FAILED;
+ if (status == Status.FAILED) {
+ final Element errorElement = (Element)root.getElementsByTagName("error").item(0);
+ final int errorCode = Integer.parseInt(errorElement.getAttribute("code"));
+ final String message = errorElement.getTextContent();
+ return Result.createRestErrorResult(errorCode, message);
+ } else {
+ return Result.createOkResult(document);
+ }
+ }
+
+ /**
+ * @return
+ */
+ private DocumentBuilder newDocumentBuilder() {
+ try {
+ final DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
+ return builderFactory.newDocumentBuilder();
+ } catch (final ParserConfigurationException e) {
+ // better never happens
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * @param method
+ * @param params
+ * @param strings
+ * @return
+ */
+ private String buildPostBody(final String method, final Map<String, String> params,
+ final String... strings) {
+ final StringBuilder builder = new StringBuilder(100);
+ builder.append("method=");
+ builder.append(method);
+ builder.append('&');
+ for (final Iterator<Entry<String, String>> it = params.entrySet().iterator(); it.hasNext();) {
+ final Entry<String, String> entry = it.next();
+ builder.append(entry.getKey());
+ builder.append('=');
+ builder.append(encode(entry.getValue()));
+ if (it.hasNext() || strings.length > 0) {
+ builder.append('&');
+ }
+ }
+ int count = 0;
+ for (final String string : strings) {
+ builder.append(count % 2 == 0 ? string : encode(string));
+ count++;
+ if (count != strings.length) {
+ if (count % 2 == 0) {
+ builder.append('&');
+ } else {
+ builder.append('=');
+ }
+ }
+ }
+ return builder.toString();
+ }
+}
diff --git a/src/com/andrew/apollo/lastfm/DomElement.java b/src/com/andrew/apollo/lastfm/DomElement.java
new file mode 100644
index 0000000..d22187c
--- /dev/null
+++ b/src/com/andrew/apollo/lastfm/DomElement.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (c) 2012, the Last.fm Java Project and Committers All rights
+ * reserved. Redistribution and use of this software in source and binary forms,
+ * with or without modification, are permitted provided that the following
+ * conditions are met: - Redistributions of source code must retain the above
+ * copyright notice, this list of conditions and the following disclaimer. -
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution. THIS SOFTWARE IS
+ * PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.andrew.apollo.lastfm;
+
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * <code>DomElement</code> wraps around an {@link Element} and provides
+ * convenience methods.
+ *
+ * @author Janni Kovacs
+ */
+public class DomElement {
+ private final Element e;
+
+ /**
+ * Creates a new wrapper around the given {@link Element}.
+ *
+ * @param elem An w3c Element
+ */
+ public DomElement(final Element elem) {
+ e = elem;
+ }
+
+ /**
+ * @return the original Element
+ */
+ public Element getElement() {
+ return e;
+ }
+
+ /**
+ * Tests if this element has an attribute with the specified name.
+ *
+ * @param name Name of the attribute.
+ * @return <code>true</code> if this element has an attribute with the
+ * specified name.
+ */
+ public boolean hasAttribute(final String name) {
+ return e.hasAttribute(name);
+ }
+
+ /**
+ * Returns the attribute value to a given attribute name or
+ * <code>null</code> if the attribute doesn't exist.
+ *
+ * @param name The attribute's name
+ * @return Attribute value or <code>null</code>
+ */
+ public String getAttribute(final String name) {
+ return e.hasAttribute(name) ? e.getAttribute(name) : null;
+ }
+
+ /**
+ * @return the text content of the element
+ */
+ public String getText() {
+ // XXX e.getTextContent() doesn't exsist under Android (Lukasz
+ // Wisniewski)
+ // / getTextContent() is now available in at least Android 2.2 if not
+ // earlier, so we'll keep using that
+ // return e.hasChildNodes() ? e.getFirstChild().getNodeValue() : null;
+ return e.getTextContent();
+ }
+
+ /**
+ * Checks if this element has a child element with the given name.
+ *
+ * @param name The child's name
+ * @return <code>true</code> if this element has a child element with the
+ * given name
+ */
+ public boolean hasChild(final String name) {
+ final NodeList list = e.getElementsByTagName(name);
+ for (int i = 0, j = list.getLength(); i < j; i++) {
+ final Node item = list.item(i);
+ if (item.getParentNode() == e) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns the child element with the given name or <code>null</code> if it
+ * doesn't exist.
+ *
+ * @param name The child's name
+ * @return the child element or <code>null</code>
+ */
+ public DomElement getChild(final String name) {
+ final NodeList list = e.getElementsByTagName(name);
+ if (list.getLength() == 0) {
+ return null;
+ }
+ for (int i = 0, j = list.getLength(); i < j; i++) {
+ final Node item = list.item(i);
+ if (item.getParentNode() == e) {
+ return new DomElement((Element)item);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the text content of a child node with the given name. If no such
+ * child exists or the child does not have text content, <code>null</code>
+ * is returned.
+ *
+ * @param name The child's name
+ * @return the child's text content or <code>null</code>
+ */
+ public String getChildText(final String name) {
+ final DomElement child = getChild(name);
+ return child != null ? child.getText() : null;
+ }
+
+ /**
+ * @return all children of this element
+ */
+ public List<DomElement> getChildren() {
+ return getChildren("*");
+ }
+
+ /**
+ * Returns all children of this element with the given tag name.
+ *
+ * @param name The children's tag name
+ * @return all matching children
+ */
+ public List<DomElement> getChildren(final String name) {
+ final List<DomElement> l = new ArrayList<DomElement>();
+ final NodeList list = e.getElementsByTagName(name);
+ for (int i = 0; i < list.getLength(); i++) {
+ final Node node = list.item(i);
+ if (node.getParentNode() == e) {
+ l.add(new DomElement((Element)node));
+ }
+ }
+ return l;
+ }
+
+ /**
+ * Returns this element's tag name.
+ *
+ * @return the tag name
+ */
+ public String getTagName() {
+ return e.getTagName();
+ }
+}
diff --git a/src/com/andrew/apollo/lastfm/Image.java b/src/com/andrew/apollo/lastfm/Image.java
new file mode 100644
index 0000000..b21ea43
--- /dev/null
+++ b/src/com/andrew/apollo/lastfm/Image.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2012, the Last.fm Java Project and Committers All rights
+ * reserved. Redistribution and use of this software in source and binary forms,
+ * with or without modification, are permitted provided that the following
+ * conditions are met: - Redistributions of source code must retain the above
+ * copyright notice, this list of conditions and the following disclaimer. -
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution. THIS SOFTWARE IS
+ * PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.andrew.apollo.lastfm;
+
+import java.util.Locale;
+
+/**
+ * An <code>Image</code> contains metadata and URLs for an artist's image.
+ * Metadata contains title, votes, format and other. Images are available in
+ * various sizes, see {@link ImageSize} for all sizes.
+ *
+ * @author Janni Kovacs
+ * @see ImageSize
+ * @see Artist#getImages(String, String)
+ */
+public class Image extends ImageHolder {
+
+ final static ItemFactory<Image> FACTORY = new ImageFactory();
+
+ private String url;
+
+ private Image() {
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ private static class ImageFactory implements ItemFactory<Image> {
+ @Override
+ public Image createItemFromElement(final DomElement element) {
+ final Image i = new Image();
+ i.url = element.getChildText("url");
+ final DomElement sizes = element.getChild("sizes");
+ for (final DomElement image : sizes.getChildren("size")) {
+ // code copied from ImageHolder.loadImages
+ final String attribute = image.getAttribute("name");
+ ImageSize size = null;
+ if (attribute == null) {
+ size = ImageSize.LARGESQUARE;
+ } else {
+ try {
+ size = ImageSize.valueOf(attribute.toUpperCase(Locale.ENGLISH));
+ } catch (final IllegalArgumentException e) {
+ // if they suddenly again introduce a new image size
+ }
+ }
+ if (size != null) {
+ i.imageUrls.put(size, image.getText());
+ }
+ }
+ return i;
+ }
+ }
+}
diff --git a/src/com/andrew/apollo/lastfm/ImageHolder.java b/src/com/andrew/apollo/lastfm/ImageHolder.java
new file mode 100644
index 0000000..d623e25
--- /dev/null
+++ b/src/com/andrew/apollo/lastfm/ImageHolder.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (c) 2012, the Last.fm Java Project and Committers All rights
+ * reserved. Redistribution and use of this software in source and binary forms,
+ * with or without modification, are permitted provided that the following
+ * conditions are met: - Redistributions of source code must retain the above
+ * copyright notice, this list of conditions and the following disclaimer. -
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution. THIS SOFTWARE IS
+ * PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.andrew.apollo.lastfm;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Abstract superclass for all items that may contain images (such as
+ * {@link Artist}s, {@link Album}s or {@link Track}s).
+ *
+ * @author Janni Kovacs
+ */
+public abstract class ImageHolder {
+
+ protected Map<ImageSize, String> imageUrls = new HashMap<ImageSize, String>();
+
+ /**
+ * Returns a Set of all {@link ImageSize}s available.
+ *
+ * @return all sizes
+ */
+ public Set<ImageSize> availableSizes() {
+ return imageUrls.keySet();
+ }
+
+ /**
+ * Returns the URL of the image in the specified size, or <code>null</code>
+ * if not available.
+ *
+ * @param size The preferred size
+ * @return an image URL
+ */
+ public String getImageURL(final ImageSize size) {
+ return imageUrls.get(size);
+ }
+
+ /**
+ * @param holder
+ * @param element
+ */
+ protected static void loadImages(final ImageHolder holder, final DomElement element) {
+ final Collection<DomElement> images = element.getChildren("image");
+ for (final DomElement image : images) {
+ final String attribute = image.getAttribute("size");
+ ImageSize size = null;
+ if (attribute == null) {
+ size = ImageSize.LARGESQUARE;
+ } else {
+ try {
+ size = ImageSize.valueOf(attribute.toUpperCase(Locale.ENGLISH));
+ } catch (final IllegalArgumentException e) {
+ // if they suddenly again introduce a new image size
+ }
+ }
+ if (size != null) {
+ holder.imageUrls.put(size, image.getText());
+ }
+ }
+ }
+}
diff --git a/src/com/andrew/apollo/lastfm/ImageSize.java b/src/com/andrew/apollo/lastfm/ImageSize.java
new file mode 100644
index 0000000..5b2c6aa
--- /dev/null
+++ b/src/com/andrew/apollo/lastfm/ImageSize.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2012, the Last.fm Java Project and Committers All rights
+ * reserved. Redistribution and use of this software in source and binary forms,
+ * with or without modification, are permitted provided that the following
+ * conditions are met: - Redistributions of source code must retain the above
+ * copyright notice, this list of conditions and the following disclaimer. -
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution. THIS SOFTWARE IS
+ * PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.andrew.apollo.lastfm;
+
+/**
+ * @author Janni Kovacs
+ */
+public enum ImageSize {
+
+ LARGE, LARGESQUARE, ORIGINAL, EXTRALARGE
+
+}
diff --git a/src/com/andrew/apollo/lastfm/ItemFactory.java b/src/com/andrew/apollo/lastfm/ItemFactory.java
new file mode 100644
index 0000000..61dfc6a
--- /dev/null
+++ b/src/com/andrew/apollo/lastfm/ItemFactory.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2012, the Last.fm Java Project and Committers All rights
+ * reserved. Redistribution and use of this software in source and binary forms,
+ * with or without modification, are permitted provided that the following
+ * conditions are met: - Redistributions of source code must retain the above
+ * copyright notice, this list of conditions and the following disclaimer. -
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution. THIS SOFTWARE IS
+ * PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.andrew.apollo.lastfm;
+
+/**
+ * An <code>ItemFactory</code> can be used to instantiate a value object - such
+ * as Artist, Album, Track, Tag - from an XML element. Use the
+ * {@link ItemFactoryBuilder} to obtain item factories for a specific type.
+ *
+ * @author Janni Kovacs
+ * @see com.andrew.apollo.lastfm.api.ItemFactoryBuilder
+ * @see ResponseBuilder
+ */
+interface ItemFactory<T> {
+
+ /**
+ * Create a new instance of the type <code>T</code>, based on the passed
+ * {@link DomElement}.
+ *
+ * @param element the XML element
+ * @return a new object
+ */
+ public T createItemFromElement(DomElement element);
+
+}
diff --git a/src/com/andrew/apollo/lastfm/ItemFactoryBuilder.java b/src/com/andrew/apollo/lastfm/ItemFactoryBuilder.java
new file mode 100644
index 0000000..a5df2e2
--- /dev/null
+++ b/src/com/andrew/apollo/lastfm/ItemFactoryBuilder.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2012, the Last.fm Java Project and Committers All rights
+ * reserved. Redistribution and use of this software in source and binary forms,
+ * with or without modification, are permitted provided that the following
+ * conditions are met: - Redistributions of source code must retain the above
+ * copyright notice, this list of conditions and the following disclaimer. -
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution. THIS SOFTWARE IS
+ * PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.andrew.apollo.lastfm;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * The <code>ItemFactoryBuilder</code> can be used to obtain {@link ItemFactory
+ * ItemFactories} for a specific type.
+ *
+ * @author Janni Kovacs
+ * @see ItemFactory
+ */
+final class ItemFactoryBuilder {
+
+ private final static ItemFactoryBuilder INSTANCE = new ItemFactoryBuilder();
+
+ @SuppressWarnings("rawtypes")
+ private final Map<Class, ItemFactory> factories = new HashMap<Class, ItemFactory>();
+
+ private ItemFactoryBuilder() {
+ // register default factories
+ addItemFactory(Album.class, Album.FACTORY);
+ addItemFactory(Artist.class, Artist.FACTORY);
+ addItemFactory(Image.class, Image.FACTORY);
+ }
+
+ /**
+ * Retrieve the instance of the <code>ItemFactoryBuilder</code>.
+ *
+ * @return the instance
+ */
+ public static ItemFactoryBuilder getFactoryBuilder() {
+ return INSTANCE;
+ }
+
+ /**
+ * @param <T>
+ * @param itemClass
+ * @param factory
+ */
+ public <T> void addItemFactory(final Class<T> itemClass, final ItemFactory<T> factory) {
+ factories.put(itemClass, factory);
+ }
+
+ /**
+ * Retrieves an {@link ItemFactory} for the given type, or <code>null</code>
+ * if no such factory was registered.
+ *
+ * @param itemClass the type's Class object
+ * @return the <code>ItemFactory</code> or <code>null</code>
+ */
+ @SuppressWarnings("unchecked")
+ public <T> ItemFactory<T> getItemFactory(final Class<T> itemClass) {
+ return factories.get(itemClass);
+ }
+}
diff --git a/src/com/andrew/apollo/lastfm/MapUtilities.java b/src/com/andrew/apollo/lastfm/MapUtilities.java
new file mode 100644
index 0000000..04185ee
--- /dev/null
+++ b/src/com/andrew/apollo/lastfm/MapUtilities.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2012, the Last.fm Java Project and Committers All rights
+ * reserved. Redistribution and use of this software in source and binary forms,
+ * with or without modification, are permitted provided that the following
+ * conditions are met: - Redistributions of source code must retain the above
+ * copyright notice, this list of conditions and the following disclaimer. -
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution. THIS SOFTWARE IS
+ * PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.andrew.apollo.lastfm;
+
+import java.util.Map;
+
+/**
+ * Utility class to perform various operations on Maps.
+ *
+ * @author Adrian Woodhead
+ */
+public final class MapUtilities {
+
+ private MapUtilities() {
+ }
+
+ /**
+ * Puts the passed key and value into the map only if the value is not null.
+ *
+ * @param map Map to add key and value to.
+ * @param key Map key.
+ * @param value Map value, if null will not be added to map.
+ */
+ public static void nullSafePut(final Map<String, String> map, final String key,
+ final String value) {
+ if (value != null) {
+ map.put(key, value);
+ }
+ }
+
+ /**
+ * Puts the passed key and value into the map only if the value is not null.
+ *
+ * @param map Map to add key and value to.
+ * @param key Map key.
+ * @param value Map value, if null will not be added to map.
+ */
+ public static void nullSafePut(final Map<String, String> map, final String key,
+ final Integer value) {
+ if (value != null) {
+ map.put(key, value.toString());
+ }
+ }
+
+ /**
+ * Puts the passed key and value into the map only if the value is not -1.
+ *
+ * @param map Map to add key and value to.
+ * @param key Map key.
+ * @param value Map value, if -1 will not be added to map.
+ */
+ public static void nullSafePut(final Map<String, String> map, final String key, final int value) {
+ if (value != -1) {
+ map.put(key, Integer.toString(value));
+ }
+ }
+
+ /**
+ * Puts the passed key and value into the map only if the value is not -1.
+ *
+ * @param map Map to add key and value to.
+ * @param key Map key.
+ * @param value Map value, if -1 will not be added to map.
+ */
+ public static void nullSafePut(final Map<String, String> map, final String key,
+ final double value) {
+ if (value != -1) {
+ map.put(key, Double.toString(value));
+ }
+ }
+}
diff --git a/src/com/andrew/apollo/lastfm/MusicEntry.java b/src/com/andrew/apollo/lastfm/MusicEntry.java
new file mode 100644
index 0000000..df75fe1
--- /dev/null
+++ b/src/com/andrew/apollo/lastfm/MusicEntry.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (c) 2012, the Last.fm Java Project and Committers All rights
+ * reserved. Redistribution and use of this software in source and binary forms,
+ * with or without modification, are permitted provided that the following
+ * conditions are met: - Redistributions of source code must retain the above
+ * copyright notice, this list of conditions and the following disclaimer. -
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution. THIS SOFTWARE IS
+ * PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.andrew.apollo.lastfm;
+
+/**
+ * <code>MusicEntry</code> is the abstract superclass for {@link Track},
+ * {@link Artist} and {@link Album}. It encapsulates data and provides methods
+ * used in all subclasses, for example: name, playcount, images and more.
+ *
+ * @author Janni Kovacs
+ */
+public abstract class MusicEntry extends ImageHolder {
+
+ protected String name;
+
+ protected String url;
+
+ private String wikiSummary;
+
+ protected MusicEntry(final String name, final String url) {
+ this.name = name;
+ this.url = url;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public String getWikiSummary() {
+ return wikiSummary;
+ }
+
+ @Override
+ public String toString() {
+ return this.getClass().getSimpleName() + "[" + "name='" + name + '\'' + ", url='" + url
+ + '\'' + ']';
+ }
+
+ /**
+ * Loads all generic information from an XML <code>DomElement</code> into
+ * the given <code>MusicEntry</code> instance, i.e. the following tags:<br/>
+ * <ul>
+ * <li>playcount/plays</li>
+ * <li>listeners</li>
+ * <li>streamable</li>
+ * <li>name</li>
+ * <li>url</li>
+ * <li>mbid</li>
+ * <li>image</li>
+ * <li>tags</li>
+ * </ul>
+ *
+ * @param entry An entry
+ * @param element XML source element
+ */
+ protected static void loadStandardInfo(final MusicEntry entry, final DomElement element) {
+ // copy
+ entry.name = element.getChildText("name");
+ entry.url = element.getChildText("url");
+ // wiki
+ DomElement wiki = element.getChild("bio");
+ if (wiki == null) {
+ wiki = element.getChild("wiki");
+ }
+ if (wiki != null) {
+ entry.wikiSummary = wiki.getChildText("summary");
+ }
+ // images
+ ImageHolder.loadImages(entry, element);
+ }
+}
diff --git a/src/com/andrew/apollo/lastfm/PaginatedResult.java b/src/com/andrew/apollo/lastfm/PaginatedResult.java
new file mode 100644
index 0000000..1a48c21
--- /dev/null
+++ b/src/com/andrew/apollo/lastfm/PaginatedResult.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2012, the Last.fm Java Project and Committers All rights
+ * reserved. Redistribution and use of this software in source and binary forms,
+ * with or without modification, are permitted provided that the following
+ * conditions are met: - Redistributions of source code must retain the above
+ * copyright notice, this list of conditions and the following disclaimer. -
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution. THIS SOFTWARE IS
+ * PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.andrew.apollo.lastfm;
+
+import java.util.Collection;
+import java.util.Iterator;
+
+/**
+ * A <code>PaginatedResult</code> is returned by methods which result set might
+ * be so large that it needs to be paginated. Each <code>PaginatedResult</code>
+ * contains the total number of result pages, the current page and a
+ * <code>Collection</code> of entries for the current page.
+ *
+ * @author Janni Kovacs
+ */
+public class PaginatedResult<T> implements Iterable<T> {
+
+ private final int page;
+
+ private final int totalPages;
+
+ public final Collection<T> pageResults;
+
+ /**
+ * @param page
+ * @param totalPages
+ * @param pageResults
+ */
+ PaginatedResult(final int page, final int totalPages, final Collection<T> pageResults) {
+ this.page = page;
+ this.totalPages = totalPages;
+ this.pageResults = pageResults;
+ }
+
+ /**
+ * Returns the page number of this result.
+ *
+ * @return page number
+ */
+ public int getPage() {
+ return page;
+ }
+
+ /**
+ * Returns the total number of pages available.
+ *
+ * @return total pages
+ */
+ public int getTotalPages() {
+ return totalPages;
+ }
+
+ /**
+ * Returns <code>true</code> if this Result contains no elements, which is
+ * the case for service calls that would have returned a
+ * <code>PaginatedResult</code> but fail.
+ *
+ * @return <code>true</code> if this result contains no elements
+ */
+ public boolean isEmpty() {
+ return pageResults == null || pageResults.isEmpty();
+ }
+
+ @Override
+ public Iterator<T> iterator() {
+ return pageResults.iterator();
+ }
+}
diff --git a/src/com/andrew/apollo/lastfm/ResponseBuilder.java b/src/com/andrew/apollo/lastfm/ResponseBuilder.java
new file mode 100644
index 0000000..b3670ec
--- /dev/null
+++ b/src/com/andrew/apollo/lastfm/ResponseBuilder.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright (c) 2012, the Last.fm Java Project and Committers All rights
+ * reserved. Redistribution and use of this software in source and binary forms,
+ * with or without modification, are permitted provided that the following
+ * conditions are met: - Redistributions of source code must retain the above
+ * copyright notice, this list of conditions and the following disclaimer. -
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution. THIS SOFTWARE IS
+ * PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.andrew.apollo.lastfm;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+
+/**
+ * This utility class can be used to generically generate Result objects
+ * (usually Lists or {@link PaginatedResult}s) from an XML response using
+ * {@link ItemFactory ItemFactories}.
+ *
+ * @author Janni Kovacs
+ */
+public final class ResponseBuilder {
+
+ private ResponseBuilder() {
+ }
+
+ /**
+ * @param <T>
+ * @param itemClass
+ * @return
+ */
+ private static <T> ItemFactory<T> getItemFactory(final Class<T> itemClass) {
+ return ItemFactoryBuilder.getFactoryBuilder().getItemFactory(itemClass);
+ }
+
+ /**
+ * @param <T>
+ * @param result
+ * @param itemClass
+ * @return
+ */
+ public static <T> Collection<T> buildCollection(final Result result, final Class<T> itemClass) {
+ return buildCollection(result, getItemFactory(itemClass));
+ }
+
+ /**
+ * @param <T>
+ * @param result
+ * @param factory
+ * @return
+ */
+ public static <T> Collection<T> buildCollection(final Result result,
+ final ItemFactory<T> factory) {
+ if (!result.isSuccessful()) {
+ return Collections.emptyList();
+ }
+ return buildCollection(result.getContentElement(), factory);
+ }
+
+ /**
+ * @param <T>
+ * @param element
+ * @param itemClass
+ * @return
+ */
+ public static <T> Collection<T> buildCollection(final DomElement element,
+ final Class<T> itemClass) {
+ return buildCollection(element, getItemFactory(itemClass));
+ }
+
+ /**
+ * @param <T>
+ * @param element
+ * @param factory
+ * @return
+ */
+ public static <T> Collection<T> buildCollection(final DomElement element,
+ final ItemFactory<T> factory) {
+ if (element == null) {
+ return Collections.emptyList();
+ }
+ final Collection<DomElement> children = element.getChildren();
+ final Collection<T> items = new ArrayList<T>(children.size());
+ for (final DomElement child : children) {
+ items.add(factory.createItemFromElement(child));
+ }
+ return items;
+ }
+
+ /**
+ * @param <T>
+ * @param result
+ * @param itemClass
+ * @return
+ */
+ public static <T> PaginatedResult<T> buildPaginatedResult(final Result result,
+ final Class<T> itemClass) {
+ return buildPaginatedResult(result, getItemFactory(itemClass));
+ }
+
+ /**
+ * @param <T>
+ * @param result
+ * @param factory
+ * @return
+ */
+ public static <T> PaginatedResult<T> buildPaginatedResult(final Result result,
+ final ItemFactory<T> factory) {
+ if (result != null) {
+ if (!result.isSuccessful()) {
+ return new PaginatedResult<T>(0, 0, Collections.<T> emptyList());
+ }
+
+ final DomElement contentElement = result.getContentElement();
+ return buildPaginatedResult(contentElement, contentElement, factory);
+ }
+ return null;
+ }
+
+ /**
+ * @param <T>
+ * @param contentElement
+ * @param childElement
+ * @param itemClass
+ * @return
+ */
+ public static <T> PaginatedResult<T> buildPaginatedResult(final DomElement contentElement,
+ final DomElement childElement, final Class<T> itemClass) {
+ return buildPaginatedResult(contentElement, childElement, getItemFactory(itemClass));
+ }
+
+ /**
+ * @param <T>
+ * @param contentElement
+ * @param childElement
+ * @param factory
+ * @return
+ */
+ public static <T> PaginatedResult<T> buildPaginatedResult(final DomElement contentElement,
+ final DomElement childElement, final ItemFactory<T> factory) {
+ final Collection<T> items = buildCollection(childElement, factory);
+
+ String totalPagesAttribute = contentElement.getAttribute("totalPages");
+ if (totalPagesAttribute == null) {
+ totalPagesAttribute = contentElement.getAttribute("totalpages");
+ }
+
+ final int page = Integer.parseInt(contentElement.getAttribute("page"));
+ final int totalPages = Integer.parseInt(totalPagesAttribute);
+
+ return new PaginatedResult<T>(page, totalPages, items);
+ }
+
+ /**
+ * @param <T>
+ * @param result
+ * @param itemClass
+ * @return
+ */
+ public static <T> T buildItem(final Result result, final Class<T> itemClass) {
+ return buildItem(result, getItemFactory(itemClass));
+ }
+
+ /**
+ * @param <T>
+ * @param result
+ * @param factory
+ * @return
+ */
+ public static <T> T buildItem(final Result result, final ItemFactory<T> factory) {
+ if (!result.isSuccessful()) {
+ return null;
+ }
+ return buildItem(result.getContentElement(), factory);
+ }
+
+ /**
+ * @param <T>
+ * @param element
+ * @param itemClass
+ * @return
+ */
+ public static <T> T buildItem(final DomElement element, final Class<T> itemClass) {
+ return buildItem(element, getItemFactory(itemClass));
+ }
+
+ /**
+ * @param <T>
+ * @param element
+ * @param factory
+ * @return
+ */
+ private static <T> T buildItem(final DomElement element, final ItemFactory<T> factory) {
+ return factory.createItemFromElement(element);
+ }
+}
diff --git a/src/com/andrew/apollo/lastfm/Result.java b/src/com/andrew/apollo/lastfm/Result.java
new file mode 100644
index 0000000..4086020
--- /dev/null
+++ b/src/com/andrew/apollo/lastfm/Result.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (c) 2012, the Last.fm Java Project and Committers All rights
+ * reserved. Redistribution and use of this software in source and binary forms,
+ * with or without modification, are permitted provided that the following
+ * conditions are met: - Redistributions of source code must retain the above
+ * copyright notice, this list of conditions and the following disclaimer. -
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution. THIS SOFTWARE IS
+ * PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.andrew.apollo.lastfm;
+
+import org.w3c.dom.Document;
+
+/**
+ * The <code>Result</code> class contains the response sent by the server, i.e.
+ * the status (either ok or failed), an error code and message if failed and the
+ * xml response sent by the server.
+ *
+ * @author Janni Kovacs
+ */
+public class Result {
+
+ public enum Status {
+ OK, FAILED
+ }
+
+ protected Status status;
+
+ protected String errorMessage = null;
+
+ protected int errorCode = -1;
+
+ protected int httpErrorCode = -1;
+
+ protected Document resultDocument;
+
+ /**
+ * @param resultDocument
+ */
+ protected Result(final Document resultDocument) {
+ status = Status.OK;
+ this.resultDocument = resultDocument;
+ }
+
+ /**
+ * @param errorMessage
+ */
+ protected Result(final String errorMessage) {
+ status = Status.FAILED;
+ this.errorMessage = errorMessage;
+ }
+
+ /**
+ * @param resultDocument
+ * @return
+ */
+ static Result createOkResult(final Document resultDocument) {
+ return new Result(resultDocument);
+ }
+
+ /**
+ * @param httpErrorCode
+ * @param errorMessage
+ * @return
+ */
+ static Result createHttpErrorResult(final int httpErrorCode, final String errorMessage) {
+ final Result r = new Result(errorMessage);
+ r.httpErrorCode = httpErrorCode;
+ return r;
+ }
+
+ /**
+ * @param errorCode
+ * @param errorMessage
+ * @return
+ */
+ static Result createRestErrorResult(final int errorCode, final String errorMessage) {
+ final Result r = new Result(errorMessage);
+ r.errorCode = errorCode;
+ return r;
+ }
+
+ /**
+ * Returns if the operation was successful. Same as
+ * <code>getStatus() == Status.OK</code>.
+ *
+ * @return <code>true</code> if the operation was successful
+ */
+ public boolean isSuccessful() {
+ return status == Status.OK;
+ }
+
+ public int getErrorCode() {
+ return errorCode;
+ }
+
+ public int getHttpErrorCode() {
+ return httpErrorCode;
+ }
+
+ public Status getStatus() {
+ return status;
+ }
+
+ public Document getResultDocument() {
+ return resultDocument;
+ }
+
+ public String getErrorMessage() {
+ return errorMessage;
+ }
+
+ public DomElement getContentElement() {
+ if (!isSuccessful()) {
+ return null;
+ }
+ return new DomElement(resultDocument.getDocumentElement()).getChild("*");
+ }
+
+ @Override
+ public String toString() {
+ return "Result[isSuccessful=" + isSuccessful() + ", errorCode=" + errorCode
+ + ", httpErrorCode=" + httpErrorCode + ", errorMessage=" + errorMessage
+ + ", status=" + status + "]";
+ }
+}
diff --git a/src/com/andrew/apollo/lastfm/StringUtilities.java b/src/com/andrew/apollo/lastfm/StringUtilities.java
new file mode 100644
index 0000000..648d9c9
--- /dev/null
+++ b/src/com/andrew/apollo/lastfm/StringUtilities.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (c) 2012, the Last.fm Java Project and Committers All rights
+ * reserved. Redistribution and use of this software in source and binary forms,
+ * with or without modification, are permitted provided that the following
+ * conditions are met: - Redistributions of source code must retain the above
+ * copyright notice, this list of conditions and the following disclaimer. -
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution. THIS SOFTWARE IS
+ * PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.andrew.apollo.lastfm;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+/**
+ * Utilitiy class with methods to calculate an md5 hash and to encode URLs.
+ *
+ * @author Janni Kovacs
+ */
+public final class StringUtilities {
+
+ private static MessageDigest mDigest;
+
+ private final static Pattern MD5_PATTERN = Pattern.compile("[a-fA-F0-9]{32}");
+
+ static {
+ try {
+ mDigest = MessageDigest.getInstance("MD5");
+ } catch (final NoSuchAlgorithmException ignored) {
+ }
+ }
+
+ /**
+ * Returns a 32 chararacter hexadecimal representation of an MD5 hash of the
+ * given String.
+ *
+ * @param s the String to hash
+ * @return the md5 hash
+ */
+ public final static String md5(final String s) {
+ try {
+ final byte[] mBytes = mDigest.digest(s.getBytes("UTF-8"));
+ final StringBuilder mBuilder = new StringBuilder(32);
+ for (final byte aByte : mBytes) {
+ final String mHex = Integer.toHexString(aByte & 0xFF);
+ if (mHex.length() == 1) {
+ mBuilder.append('0');
+ }
+ mBuilder.append(mHex);
+ }
+ return mBuilder.toString();
+ } catch (final UnsupportedEncodingException ignored) {
+ }
+ return null;
+ }
+
+ /**
+ * URL Encodes the given String <code>s</code> using the UTF-8 character
+ * encoding.
+ *
+ * @param s a String
+ * @return url encoded string
+ */
+ public static String encode(final String s) {
+ if (s == null) {
+ return null;
+ }
+ try {
+ return URLEncoder.encode(s, "UTF-8");
+ } catch (final UnsupportedEncodingException ignored) {
+ }
+ return null;
+ }
+
+ /**
+ * Decodes an URL encoded String <code>s</code> using the UTF-8 character
+ * encoding.
+ *
+ * @param s an encoded String
+ * @return the decoded String
+ */
+ public static String decode(final String s) {
+ if (s == null) {
+ return null;
+ }
+ try {
+ return URLDecoder.decode(s, "UTF-8");
+ } catch (final UnsupportedEncodingException ignored) {
+ }
+ return null;
+ }
+
+ /**
+ * Creates a Map out of an array with Strings.
+ *
+ * @param strings input strings, key-value alternating
+ * @return a parameter map
+ */
+ public static Map<String, String> map(final String... strings) {
+ if (strings.length % 2 != 0) {
+ throw new IllegalArgumentException("strings.length % 2 != 0");
+ }
+ final Map<String, String> sMap = new HashMap<String, String>();
+ for (int i = 0; i < strings.length; i += 2) {
+ sMap.put(strings[i], strings[i + 1]);
+ }
+ return sMap;
+ }
+
+ /**
+ * Strips all characters from a String, that might be invalid to be used in
+ * file names. By default <tt>: / \ < > | ? " *</tt> are all replaced by
+ * <tt>-</tt>. Note that this is no guarantee that the returned name will be
+ * definately valid.
+ *
+ * @param s the String to clean up
+ * @return the cleaned up String
+ */
+ public static String cleanUp(final String s) {
+ return s.replaceAll("[*:/\\\\?|<>\"]", "-");
+ }
+
+ /**
+ * Tests if the given string <i>might</i> already be a 32-char md5 string.
+ *
+ * @param s String to test
+ * @return <code>true</code> if the given String might be a md5 string
+ */
+ public static boolean isMD5(final String s) {
+ return s.length() == 32 && MD5_PATTERN.matcher(s).matches();
+ }
+
+ /**
+ * Converts a Last.fm boolean result string to a boolean.
+ *
+ * @param resultString A Last.fm boolean result string.
+ * @return <code>true</code> if the given String represents a true,
+ * <code>false</code> otherwise.
+ */
+ public static boolean convertToBoolean(final String resultString) {
+ return "1".equals(resultString);
+ }
+
+ /**
+ * Converts from a boolean to a Last.fm boolean result string.
+ *
+ * @param value A boolean value.
+ * @return A string representing a Last.fm boolean.
+ */
+ public static String convertFromBoolean(final boolean value) {
+ if (value) {
+ return "1";
+ } else {
+ return "0";
+ }
+ }
+
+}
diff --git a/src/com/andrew/apollo/lastfm/api/Album.java b/src/com/andrew/apollo/lastfm/api/Album.java
deleted file mode 100644
index e5dab7a..0000000
--- a/src/com/andrew/apollo/lastfm/api/Album.java
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * Copyright (c) 2012, the Last.fm Java Project and Committers
- * All rights reserved.
- *
- * Redistribution and use of this software in source and binary forms, with or without modification, are
- * permitted provided that the following conditions are met:
- *
- * - Redistributions of source code must retain the above
- * copyright notice, this list of conditions and the
- * following disclaimer.
- *
- * - Redistributions in binary form must reproduce the above
- * copyright notice, this list of conditions and the
- * following disclaimer in the documentation and/or other
- * materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
- * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
- * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
- * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
- * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package com.andrew.apollo.lastfm.api;
-
-import java.util.HashMap;
-import java.util.Map;
-
-import com.andrew.apollo.utils.DomElement;
-import com.andrew.apollo.utils.MapUtilities;
-import com.andrew.apollo.utils.StringUtilities;
-
-
-/**
- * Wrapper class for Album related API calls and Album Bean.
- *
- * @author Janni Kovacs
- */
-public class Album extends MusicEntry {
-
- static final ItemFactory<Album> FACTORY = new AlbumFactory();
-
- private String artist;
-
- private Album(String name, String url, String artist) {
- super(name, url);
- this.artist = artist;
- }
-
- private Album(String name, String url, String mbid, int playcount, int listeners,
- boolean streamable, String artist) {
- super(name, url, mbid, playcount, listeners, streamable);
- this.artist = artist;
- }
-
- public String getArtist() {
- return artist;
- }
-
- /**
- * Get the metadata for an album on Last.fm using the album name or a
- * musicbrainz id. See playlist.fetch on how to get the album playlist.
- *
- * @param artist Artist's name
- * @param albumOrMbid Album name or MBID
- * @param apiKey The API key
- * @return Album metadata
- */
- public static Album getInfo(String artist, String albumOrMbid, String apiKey) {
- return getInfo(artist, albumOrMbid, null, apiKey);
- }
-
- /**
- * Get the metadata for an album on Last.fm using the album name or a
- * musicbrainz id. See playlist.fetch on how to get the album playlist.
- *
- * @param artist Artist's name
- * @param albumOrMbid Album name or MBID
- * @param username The username for the context of the request. If supplied,
- * the user's playcount for this album is included in the
- * response.
- * @param apiKey The API key
- * @return Album metadata
- */
- public static Album getInfo(String artist, String albumOrMbid, String username, String apiKey) {
- Map<String, String> params = new HashMap<String, String>();
- if (StringUtilities.isMbid(albumOrMbid)) {
- params.put("mbid", albumOrMbid);
- } else {
- params.put("artist", artist);
- params.put("album", albumOrMbid);
- }
- MapUtilities.nullSafePut(params, "username", username);
- Result result = Caller.getInstance().call("album.getInfo", apiKey, params);
- return ResponseBuilder.buildItem(result, Album.class);
- }
-
- private static class AlbumFactory implements ItemFactory<Album> {
- @Override
- public Album createItemFromElement(DomElement element) {
- Album album = new Album(null, null, null);
- MusicEntry.loadStandardInfo(album, element);
- if (element.hasChild("artist")) {
- album.artist = element.getChild("artist").getChildText("name");
- if (album.artist == null)
- album.artist = element.getChildText("artist");
- }
- return album;
- }
- }
-}
diff --git a/src/com/andrew/apollo/lastfm/api/Artist.java b/src/com/andrew/apollo/lastfm/api/Artist.java
deleted file mode 100644
index 7738cf1..0000000
--- a/src/com/andrew/apollo/lastfm/api/Artist.java
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * Copyright (c) 2012, the Last.fm Java Project and Committers
- * All rights reserved.
- *
- * Redistribution and use of this software in source and binary forms, with or without modification, are
- * permitted provided that the following conditions are met:
- *
- * - Redistributions of source code must retain the above
- * copyright notice, this list of conditions and the
- * following disclaimer.
- *
- * - Redistributions in binary form must reproduce the above
- * copyright notice, this list of conditions and the
- * following disclaimer in the documentation and/or other
- * materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
- * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
- * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
- * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
- * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package com.andrew.apollo.lastfm.api;
-
-import java.util.HashMap;
-import java.util.Map;
-
-import com.andrew.apollo.utils.DomElement;
-import com.andrew.apollo.utils.MapUtilities;
-import com.andrew.apollo.utils.StringUtilities;
-
-/**
- * Bean that contains artist information.<br/>
- * This class contains static methods that executes API methods relating to
- * artists.<br/>
- * Method names are equivalent to the last.fm API method names.
- *
- * @author Janni Kovacs
- */
-public class Artist extends MusicEntry {
-
- static final ItemFactory<Artist> FACTORY = new ArtistFactory();
-
- protected Artist(String name, String url) {
- super(name, url);
- }
-
- protected Artist(String name, String url, String mbid, int playcount, int listeners,
- boolean streamable) {
- super(name, url, mbid, playcount, listeners, streamable);
- }
-
- /**
- * Get {@link Image}s for this artist in a variety of sizes.
- *
- * @param artistOrMbid The artist name in question
- * @param apiKey A Last.fm API key
- * @return a list of {@link Image}s
- */
- public static PaginatedResult<Image> getImages(String artistOrMbid, String apiKey) {
- return getImages(artistOrMbid, -1, -1, apiKey);
- }
-
- /**
- * Get {@link Image}s for this artist in a variety of sizes.
- *
- * @param artistOrMbid The artist name in question
- * @param page Which page of limit amount to display
- * @param limit How many to return. Defaults and maxes out at 50
- * @param apiKey A Last.fm API key
- * @return a list of {@link Image}s
- */
- public static PaginatedResult<Image> getImages(String artistOrMbid, int page, int limit,
- String apiKey) {
- Map<String, String> params = new HashMap<String, String>();
- if (StringUtilities.isMbid(artistOrMbid)) {
- params.put("mbid", artistOrMbid);
- } else {
- params.put("artist", artistOrMbid);
- }
- MapUtilities.nullSafePut(params, "page", page);
- MapUtilities.nullSafePut(params, "limit", limit);
- Result result = Caller.getInstance().call("artist.getImages", apiKey, params);
- return ResponseBuilder.buildPaginatedResult(result, Image.class);
- }
-
- private static class ArtistFactory implements ItemFactory<Artist> {
- @Override
- public Artist createItemFromElement(DomElement element) {
- Artist artist = new Artist(null, null);
- MusicEntry.loadStandardInfo(artist, element);
- return artist;
- }
- }
-}
diff --git a/src/com/andrew/apollo/lastfm/api/CallException.java b/src/com/andrew/apollo/lastfm/api/CallException.java
deleted file mode 100644
index eada28a..0000000
--- a/src/com/andrew/apollo/lastfm/api/CallException.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright (c) 2012, the Last.fm Java Project and Committers
- * All rights reserved.
- *
- * Redistribution and use of this software in source and binary forms, with or without modification, are
- * permitted provided that the following conditions are met:
- *
- * - Redistributions of source code must retain the above
- * copyright notice, this list of conditions and the
- * following disclaimer.
- *
- * - Redistributions in binary form must reproduce the above
- * copyright notice, this list of conditions and the
- * following disclaimer in the documentation and/or other
- * materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
- * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
- * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
- * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
- * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package com.andrew.apollo.lastfm.api;
-
-/**
- * @author Janni Kovacs
- */
-public class CallException extends RuntimeException {
-
- /**
- *
- */
- private static final long serialVersionUID = 1L;
-
- public CallException() {
- }
-
- public CallException(Throwable cause) {
- super(cause);
- }
-
- public CallException(String message) {
- super(message);
- }
-
- public CallException(String message, Throwable cause) {
- super(message, cause);
- }
-}
diff --git a/src/com/andrew/apollo/lastfm/api/Caller.java b/src/com/andrew/apollo/lastfm/api/Caller.java
deleted file mode 100644
index 50796f4..0000000
--- a/src/com/andrew/apollo/lastfm/api/Caller.java
+++ /dev/null
@@ -1,262 +0,0 @@
-/*
- * Copyright (c) 2012, the Last.fm Java Project and Committers
- * All rights reserved.
- *
- * Redistribution and use of this software in source and binary forms, with or without modification, are
- * permitted provided that the following conditions are met:
- *
- * - Redistributions of source code must retain the above
- * copyright notice, this list of conditions and the
- * following disclaimer.
- *
- * - Redistributions in binary form must reproduce the above
- * copyright notice, this list of conditions and the
- * following disclaimer in the documentation and/or other
- * materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
- * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
- * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
- * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
- * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package com.andrew.apollo.lastfm.api;
-
-import static com.andrew.apollo.utils.StringUtilities.encode;
-import static com.andrew.apollo.utils.StringUtilities.map;
-
-import java.io.BufferedWriter;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.net.HttpURLConnection;
-import java.net.Proxy;
-import java.net.URL;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.Map.Entry;
-
-import javax.xml.parsers.DocumentBuilder;
-import javax.xml.parsers.DocumentBuilderFactory;
-import javax.xml.parsers.ParserConfigurationException;
-
-import org.w3c.dom.Document;
-import org.w3c.dom.Element;
-import org.xml.sax.InputSource;
-import org.xml.sax.SAXException;
-
-import com.andrew.apollo.lastfm.api.Result.Status;
-
-/**
- * The <code>Caller</code> class handles the low-level communication between the
- * client and last.fm.<br/>
- * Direct usage of this class should be unnecessary since all method calls are
- * available via the methods in the <code>Artist</code>, <code>Album</code>,
- * <code>User</code>, etc. classes. If specialized calls which are not covered
- * by the Java API are necessary this class may be used directly.<br/>
- * Supports the setting of a custom {@link Proxy} and a custom
- * <code>User-Agent</code> HTTP header.
- *
- * @author Janni Kovacs
- */
-public class Caller {
-
- private static final String PARAM_API_KEY = "api_key";
-
- private static final String DEFAULT_API_ROOT = "http://ws.audioscrobbler.com/2.0/";
-
- private static final Caller instance = new Caller();
-
- private final String apiRootUrl = DEFAULT_API_ROOT;
-
- private final String userAgent = "Apollo";
-
- private Result lastResult;
-
- private Caller() {
- }
-
- /**
- * Returns the single instance of the <code>Caller</code> class.
- *
- * @return a <code>Caller</code>
- */
- public static Caller getInstance() {
- return instance;
- }
-
- public Result call(String method, String apiKey, String... params) throws CallException {
- return call(method, apiKey, map(params));
- }
-
- public Result call(String method, String apiKey, Map<String, String> params)
- throws CallException {
- return call(method, apiKey, params, null);
- }
-
- public Result call(String method, Session session, String... params) {
- return call(method, session.getApiKey(), map(params), session);
- }
-
- public Result call(String method, Session session, Map<String, String> params) {
- return call(method, session.getApiKey(), params, session);
- }
-
- /**
- * Performs the web-service call. If the <code>session</code> parameter is
- * <code>non-null</code> then an authenticated call is made. If it's
- * <code>null</code> then an unauthenticated call is made.<br/>
- * The <code>apiKey</code> parameter is always required, even when a valid
- * session is passed to this method.
- *
- * @param method The method to call
- * @param apiKey A Last.fm API key
- * @param params Parameters
- * @param session A Session instance or <code>null</code>
- * @return the result of the operation
- */
- private Result call(String method, String apiKey, Map<String, String> params, Session session) {
- params = new HashMap<String, String>(params); // create new Map in case
- // params is an immutable
- // Map
- InputStream inputStream = null;
-
- // no entry in cache, load from web
- if (inputStream == null) {
- // fill parameter map with apiKey and session info
- params.put(PARAM_API_KEY, apiKey);
- if (session != null) {
- params.put("sk", session.getKey());
- }
- try {
- HttpURLConnection urlConnection = openPostConnection(method, params);
- inputStream = getInputStreamFromConnection(urlConnection);
-
- if (inputStream == null) {
- this.lastResult = Result.createHttpErrorResult(urlConnection.getResponseCode(),
- urlConnection.getResponseMessage());
- return lastResult;
- }
- } catch (IOException e) {
- throw new CallException(e);
- }
- }
-
- try {
- Result result = createResultFromInputStream(inputStream);
- this.lastResult = result;
- return result;
- } catch (IOException e) {
- throw new CallException(e);
- } catch (SAXException e) {
- throw new CallException(e);
- }
- }
-
- /**
- * Creates a new {@link HttpURLConnection}, sets the proxy, if available,
- * and sets the User-Agent property.
- *
- * @param url URL to connect to
- * @return a new connection.
- * @throws IOException if an I/O exception occurs.
- */
- public HttpURLConnection openConnection(String url) throws IOException {
- URL u = new URL(url);
- HttpURLConnection urlConnection;
- urlConnection = (HttpURLConnection)u.openConnection();
- urlConnection.setRequestProperty("User-Agent", userAgent);
- urlConnection.setUseCaches(true);
- return urlConnection;
- }
-
- private HttpURLConnection openPostConnection(String method, Map<String, String> params)
- throws IOException {
- HttpURLConnection urlConnection = openConnection(apiRootUrl);
- urlConnection.setRequestMethod("POST");
- urlConnection.setDoOutput(true);
- urlConnection.setUseCaches(true);
- OutputStream outputStream = urlConnection.getOutputStream();
- BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream));
- String post = buildPostBody(method, params);
- writer.write(post);
- writer.close();
- return urlConnection;
- }
-
- private InputStream getInputStreamFromConnection(HttpURLConnection connection)
- throws IOException {
- int responseCode = connection.getResponseCode();
-
- if (responseCode == HttpURLConnection.HTTP_FORBIDDEN
- || responseCode == HttpURLConnection.HTTP_BAD_REQUEST) {
- return connection.getErrorStream();
- } else if (responseCode == HttpURLConnection.HTTP_OK) {
- return connection.getInputStream();
- }
-
- return null;
- }
-
- private Result createResultFromInputStream(InputStream inputStream) throws SAXException,
- IOException {
- Document document = newDocumentBuilder().parse(
- new InputSource(new InputStreamReader(inputStream, "UTF-8")));
- Element root = document.getDocumentElement(); // lfm element
- String statusString = root.getAttribute("status");
- Status status = "ok".equals(statusString) ? Status.OK : Status.FAILED;
- if (status == Status.FAILED) {
- Element errorElement = (Element)root.getElementsByTagName("error").item(0);
- int errorCode = Integer.parseInt(errorElement.getAttribute("code"));
- String message = errorElement.getTextContent();
- return Result.createRestErrorResult(errorCode, message);
- } else {
- return Result.createOkResult(document);
- }
- }
-
- private DocumentBuilder newDocumentBuilder() {
- try {
- DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
- return builderFactory.newDocumentBuilder();
- } catch (ParserConfigurationException e) {
- // better never happens
- throw new RuntimeException(e);
- }
- }
-
- private String buildPostBody(String method, Map<String, String> params, String... strings) {
- StringBuilder builder = new StringBuilder(100);
- builder.append("method=");
- builder.append(method);
- builder.append('&');
- for (Iterator<Entry<String, String>> it = params.entrySet().iterator(); it.hasNext();) {
- Entry<String, String> entry = it.next();
- builder.append(entry.getKey());
- builder.append('=');
- builder.append(encode(entry.getValue()));
- if (it.hasNext() || strings.length > 0)
- builder.append('&');
- }
- int count = 0;
- for (String string : strings) {
- builder.append(count % 2 == 0 ? string : encode(string));
- count++;
- if (count != strings.length) {
- if (count % 2 == 0) {
- builder.append('&');
- } else {
- builder.append('=');
- }
- }
- }
- return builder.toString();
- }
-}
diff --git a/src/com/andrew/apollo/lastfm/api/Image.java b/src/com/andrew/apollo/lastfm/api/Image.java
deleted file mode 100644
index 12b2dca..0000000
--- a/src/com/andrew/apollo/lastfm/api/Image.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Copyright (c) 2012, the Last.fm Java Project and Committers
- * All rights reserved.
- *
- * Redistribution and use of this software in source and binary forms, with or without modification, are
- * permitted provided that the following conditions are met:
- *
- * - Redistributions of source code must retain the above
- * copyright notice, this list of conditions and the
- * following disclaimer.
- *
- * - Redistributions in binary form must reproduce the above
- * copyright notice, this list of conditions and the
- * following disclaimer in the documentation and/or other
- * materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
- * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
- * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
- * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
- * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package com.andrew.apollo.lastfm.api;
-
-import java.util.Locale;
-
-import com.andrew.apollo.utils.DomElement;
-
-/**
- * An <code>Image</code> contains metadata and URLs for an artist's image.
- * Metadata contains title, votes, format and other. Images are available in
- * various sizes, see {@link ImageSize} for all sizes.
- *
- * @author Janni Kovacs
- * @see ImageSize
- * @see Artist#getImages(String, String)
- */
-public class Image extends ImageHolder {
-
- static final ItemFactory<Image> FACTORY = new ImageFactory();
-
- private String url;
-
- private Image() {
- }
-
- public String getUrl() {
- return url;
- }
-
- private static class ImageFactory implements ItemFactory<Image> {
- @Override
- public Image createItemFromElement(DomElement element) {
- Image i = new Image();
- i.url = element.getChildText("url");
- DomElement sizes = element.getChild("sizes");
- for (DomElement image : sizes.getChildren("size")) {
- // code copied from ImageHolder.loadImages
- String attribute = image.getAttribute("name");
- ImageSize size = null;
- if (attribute == null) {
- size = ImageSize.LARGESQUARE;
- } else {
- try {
- size = ImageSize.valueOf(attribute.toUpperCase(Locale.ENGLISH));
- } catch (IllegalArgumentException e) {
- // if they suddenly again introduce a new image size
- }
- }
- if (size != null)
- i.imageUrls.put(size, image.getText());
- }
- return i;
- }
- }
-}
diff --git a/src/com/andrew/apollo/lastfm/api/ImageHolder.java b/src/com/andrew/apollo/lastfm/api/ImageHolder.java
deleted file mode 100644
index 8bb73ff..0000000
--- a/src/com/andrew/apollo/lastfm/api/ImageHolder.java
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * Copyright (c) 2012, the Last.fm Java Project and Committers
- * All rights reserved.
- *
- * Redistribution and use of this software in source and binary forms, with or without modification, are
- * permitted provided that the following conditions are met:
- *
- * - Redistributions of source code must retain the above
- * copyright notice, this list of conditions and the
- * following disclaimer.
- *
- * - Redistributions in binary form must reproduce the above
- * copyright notice, this list of conditions and the
- * following disclaimer in the documentation and/or other
- * materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
- * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
- * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
- * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
- * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package com.andrew.apollo.lastfm.api;
-
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Set;
-
-import com.andrew.apollo.utils.DomElement;
-
-/**
- * Abstract superclass for all items that may contain images (such as
- * {@link Artist}s, {@link Album}s or {@link Track}s).
- *
- * @author Janni Kovacs
- */
-public abstract class ImageHolder {
-
- protected Map<ImageSize, String> imageUrls = new HashMap<ImageSize, String>();
-
- /**
- * Returns a Set of all {@link ImageSize}s available.
- *
- * @return all sizes
- */
- public Set<ImageSize> availableSizes() {
- return imageUrls.keySet();
- }
-
- /**
- * Returns the URL of the image in the specified size, or <code>null</code>
- * if not available.
- *
- * @param size The preferred size
- * @return an image URL
- */
- public String getImageURL(ImageSize size) {
- return imageUrls.get(size);
- }
-
- protected static void loadImages(ImageHolder holder, DomElement element) {
- Collection<DomElement> images = element.getChildren("image");
- for (DomElement image : images) {
- String attribute = image.getAttribute("size");
- ImageSize size = null;
- if (attribute == null) {
- size = ImageSize.LARGESQUARE;
- } else {
- try {
- size = ImageSize.valueOf(attribute.toUpperCase(Locale.ENGLISH));
- } catch (IllegalArgumentException e) {
- // if they suddenly again introduce a new image size
- }
- }
- if (size != null)
- holder.imageUrls.put(size, image.getText());
- }
- }
-}
diff --git a/src/com/andrew/apollo/lastfm/api/ImageSize.java b/src/com/andrew/apollo/lastfm/api/ImageSize.java
deleted file mode 100644
index 77361c7..0000000
--- a/src/com/andrew/apollo/lastfm/api/ImageSize.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright (c) 2012, the Last.fm Java Project and Committers
- * All rights reserved.
- *
- * Redistribution and use of this software in source and binary forms, with or without modification, are
- * permitted provided that the following conditions are met:
- *
- * - Redistributions of source code must retain the above
- * copyright notice, this list of conditions and the
- * following disclaimer.
- *
- * - Redistributions in binary form must reproduce the above
- * copyright notice, this list of conditions and the
- * following disclaimer in the documentation and/or other
- * materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
- * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
- * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
- * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
- * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package com.andrew.apollo.lastfm.api;
-
-/**
- * @author Janni Kovacs
- */
-public enum ImageSize {
-
- LARGE, LARGESQUARE, ORIGINAL
-
-}
diff --git a/src/com/andrew/apollo/lastfm/api/ItemFactory.java b/src/com/andrew/apollo/lastfm/api/ItemFactory.java
deleted file mode 100644
index 331b0c7..0000000
--- a/src/com/andrew/apollo/lastfm/api/ItemFactory.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright (c) 2012, the Last.fm Java Project and Committers
- * All rights reserved.
- *
- * Redistribution and use of this software in source and binary forms, with or without modification, are
- * permitted provided that the following conditions are met:
- *
- * - Redistributions of source code must retain the above
- * copyright notice, this list of conditions and the
- * following disclaimer.
- *
- * - Redistributions in binary form must reproduce the above
- * copyright notice, this list of conditions and the
- * following disclaimer in the documentation and/or other
- * materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
- * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
- * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
- * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
- * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package com.andrew.apollo.lastfm.api;
-
-import com.andrew.apollo.utils.DomElement;
-
-/**
- * An <code>ItemFactory</code> can be used to instantiate a value object - such as Artist, Album, Track, Tag - from an XML element. Use the
- * {@link ItemFactoryBuilder} to obtain item factories for a specific type.
- *
- * @author Janni Kovacs
- * @see com.andrew.apollo.lastfm.api.ItemFactoryBuilder
- * @see ResponseBuilder
- */
-interface ItemFactory<T> {
-
- /**
- * Create a new instance of the type <code>T</code>, based on the passed {@link DomElement}.
- *
- * @param element the XML element
- * @return a new object
- */
- public T createItemFromElement(DomElement element);
-
-}
diff --git a/src/com/andrew/apollo/lastfm/api/ItemFactoryBuilder.java b/src/com/andrew/apollo/lastfm/api/ItemFactoryBuilder.java
deleted file mode 100644
index de48796..0000000
--- a/src/com/andrew/apollo/lastfm/api/ItemFactoryBuilder.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Copyright (c) 2012, the Last.fm Java Project and Committers
- * All rights reserved.
- *
- * Redistribution and use of this software in source and binary forms, with or without modification, are
- * permitted provided that the following conditions are met:
- *
- * - Redistributions of source code must retain the above
- * copyright notice, this list of conditions and the
- * following disclaimer.
- *
- * - Redistributions in binary form must reproduce the above
- * copyright notice, this list of conditions and the
- * following disclaimer in the documentation and/or other
- * materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
- * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
- * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
- * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
- * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package com.andrew.apollo.lastfm.api;
-
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * The <code>ItemFactoryBuilder</code> can be used to obtain {@link ItemFactory
- * ItemFactories} for a specific type.
- *
- * @author Janni Kovacs
- * @see ItemFactory
- */
-final class ItemFactoryBuilder {
-
- private static final ItemFactoryBuilder INSTANCE = new ItemFactoryBuilder();
-
- @SuppressWarnings("rawtypes")
- private final Map<Class, ItemFactory> factories = new HashMap<Class, ItemFactory>();
-
- private ItemFactoryBuilder() {
- // register default factories
- addItemFactory(Album.class, Album.FACTORY);
- addItemFactory(Artist.class, Artist.FACTORY);
- addItemFactory(Image.class, Image.FACTORY);
- }
-
- /**
- * Retrieve the instance of the <code>ItemFactoryBuilder</code>.
- *
- * @return the instance
- */
- public static ItemFactoryBuilder getFactoryBuilder() {
- return INSTANCE;
- }
-
- public <T> void addItemFactory(Class<T> itemClass, ItemFactory<T> factory) {
- factories.put(itemClass, factory);
- }
-
- /**
- * Retrieves an {@link ItemFactory} for the given type, or <code>null</code>
- * if no such factory was registered.
- *
- * @param itemClass the type's Class object
- * @return the <code>ItemFactory</code> or <code>null</code>
- */
- @SuppressWarnings("unchecked")
- public <T> ItemFactory<T> getItemFactory(Class<T> itemClass) {
- return factories.get(itemClass);
- }
-}
diff --git a/src/com/andrew/apollo/lastfm/api/MusicEntry.java b/src/com/andrew/apollo/lastfm/api/MusicEntry.java
deleted file mode 100644
index f0359d3..0000000
--- a/src/com/andrew/apollo/lastfm/api/MusicEntry.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * Copyright (c) 2012, the Last.fm Java Project and Committers
- * All rights reserved.
- *
- * Redistribution and use of this software in source and binary forms, with or without modification, are
- * permitted provided that the following conditions are met:
- *
- * - Redistributions of source code must retain the above
- * copyright notice, this list of conditions and the
- * following disclaimer.
- *
- * - Redistributions in binary form must reproduce the above
- * copyright notice, this list of conditions and the
- * following disclaimer in the documentation and/or other
- * materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
- * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
- * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
- * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
- * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package com.andrew.apollo.lastfm.api;
-
-import java.util.ArrayList;
-import java.util.Collection;
-
-import com.andrew.apollo.utils.DomElement;
-
-
-/**
- * <code>MusicEntry</code> is the abstract superclass for {@link Track},
- * {@link Artist} and {@link Album}. It encapsulates data and provides methods
- * used in all subclasses, for example: name, playcount, images and more.
- *
- * @author Janni Kovacs
- */
-public abstract class MusicEntry extends ImageHolder {
-
- protected String name;
-
- protected String url;
-
- protected String mbid;
-
- protected String id;
-
- /**
- * This property is only available on hype charts, like
- * {@link Chart#getHypedArtists(String)} or
- * {@link de.umass.lastfm.Group#getHype(String, String)}
- */
- protected int percentageChange;
-
- protected Collection<String> tags = new ArrayList<String>();
-
- protected MusicEntry(String name, String url) {
- this(name, url, null, -1, -1, false);
- }
-
- protected MusicEntry(String name, String url, String mbid, int playcount, int listeners,
- boolean streamable) {
- this.name = name;
- this.url = url;
- this.mbid = mbid;
- }
-
- public String getMbid() {
- return mbid;
- }
-
- public String getName() {
- return name;
- }
-
- public String getId() {
- return id;
- }
-
- public String getUrl() {
- return url;
- }
-
- public Collection<String> getTags() {
- return tags;
- }
-
- @Override
- public String toString() {
- return this.getClass().getSimpleName() + "[" + "name='" + name + '\'' + ", id='" + id
- + '\'' + ", url='" + url + '\'' + ", mbid='" + mbid + '\'' + ']';
- }
-
- /**
- * Loads all generic information from an XML <code>DomElement</code> into
- * the given <code>MusicEntry</code> instance, i.e. the following tags:<br/>
- * <ul>
- * <li>playcount/plays</li>
- * <li>listeners</li>
- * <li>streamable</li>
- * <li>name</li>
- * <li>url</li>
- * <li>mbid</li>
- * <li>image</li>
- * <li>tags</li>
- * </ul>
- *
- * @param entry An entry
- * @param element XML source element
- */
- protected static void loadStandardInfo(MusicEntry entry, DomElement element) {
- // copy
- entry.name = element.getChildText("name");
- entry.url = element.getChildText("url");
- entry.mbid = element.getChildText("mbid");
- // images
- ImageHolder.loadImages(entry, element);
- }
-}
diff --git a/src/com/andrew/apollo/lastfm/api/PaginatedResult.java b/src/com/andrew/apollo/lastfm/api/PaginatedResult.java
deleted file mode 100644
index ebc1672..0000000
--- a/src/com/andrew/apollo/lastfm/api/PaginatedResult.java
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * Copyright (c) 2012, the Last.fm Java Project and Committers
- * All rights reserved.
- *
- * Redistribution and use of this software in source and binary forms, with or without modification, are
- * permitted provided that the following conditions are met:
- *
- * - Redistributions of source code must retain the above
- * copyright notice, this list of conditions and the
- * following disclaimer.
- *
- * - Redistributions in binary form must reproduce the above
- * copyright notice, this list of conditions and the
- * following disclaimer in the documentation and/or other
- * materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
- * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
- * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
- * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
- * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package com.andrew.apollo.lastfm.api;
-
-import java.util.Collection;
-import java.util.Iterator;
-
-/**
- * A <code>PaginatedResult</code> is returned by methods which result set might be so large that it needs
- * to be paginated. Each <code>PaginatedResult</code> contains the total number of result pages, the current
- * page and a <code>Collection</code> of entries for the current page.
- *
- * @author Janni Kovacs
- */
-public class PaginatedResult<T> implements Iterable<T> {
-
- private int page;
- private int totalPages;
- private Collection<T> pageResults;
-
- PaginatedResult(int page, int totalPages, Collection<T> pageResults) {
- this.page = page;
- this.totalPages = totalPages;
- this.pageResults = pageResults;
- }
-
- /**
- * Returns the page number of this result.
- *
- * @return page number
- */
- public int getPage() {
- return page;
- }
-
- /**
- * Returns a list of entries of the type <code>T</code> for this page.
- *
- * @return page results
- */
- public Collection<T> getPageResults() {
- return pageResults;
- }
-
- /**
- * Returns the total number of pages available.
- *
- * @return total pages
- */
- public int getTotalPages() {
- return totalPages;
- }
-
- /**
- * Returns <code>true</code> if this Result contains no elements, which is the case for service calls that would have returned a
- * <code>PaginatedResult</code> but fail.
- *
- * @return <code>true</code> if this result contains no elements
- */
- public boolean isEmpty() {
- return pageResults == null || pageResults.isEmpty();
- }
-
- public Iterator<T> iterator() {
- return getPageResults().iterator();
- }
-}
diff --git a/src/com/andrew/apollo/lastfm/api/ResponseBuilder.java b/src/com/andrew/apollo/lastfm/api/ResponseBuilder.java
deleted file mode 100644
index 0f60778..0000000
--- a/src/com/andrew/apollo/lastfm/api/ResponseBuilder.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * Copyright (c) 2012, the Last.fm Java Project and Committers
- * All rights reserved.
- *
- * Redistribution and use of this software in source and binary forms, with or without modification, are
- * permitted provided that the following conditions are met:
- *
- * - Redistributions of source code must retain the above
- * copyright notice, this list of conditions and the
- * following disclaimer.
- *
- * - Redistributions in binary form must reproduce the above
- * copyright notice, this list of conditions and the
- * following disclaimer in the documentation and/or other
- * materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
- * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
- * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
- * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
- * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package com.andrew.apollo.lastfm.api;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-
-import com.andrew.apollo.utils.DomElement;
-
-
-/**
- * This utility class can be used to generically generate Result objects (usually Lists or {@link PaginatedResult}s) from an XML response
- * using {@link ItemFactory ItemFactories}.
- *
- * @author Janni Kovacs
- */
-public final class ResponseBuilder {
-
- private ResponseBuilder() {
- }
-
- private static <T> ItemFactory<T> getItemFactory(Class<T> itemClass) {
- return ItemFactoryBuilder.getFactoryBuilder().getItemFactory(itemClass);
- }
-
- public static <T> Collection<T> buildCollection(Result result, Class<T> itemClass) {
- return buildCollection(result, getItemFactory(itemClass));
- }
-
- public static <T> Collection<T> buildCollection(Result result, ItemFactory<T> factory) {
- if (!result.isSuccessful())
- return Collections.emptyList();
- return buildCollection(result.getContentElement(), factory);
- }
-
- public static <T> Collection<T> buildCollection(DomElement element, Class<T> itemClass) {
- return buildCollection(element, getItemFactory(itemClass));
- }
-
- public static <T> Collection<T> buildCollection(DomElement element, ItemFactory<T> factory) {
- if (element == null)
- return Collections.emptyList();
- Collection<DomElement> children = element.getChildren();
- Collection<T> items = new ArrayList<T>(children.size());
- for (DomElement child : children) {
- items.add(factory.createItemFromElement(child));
- }
- return items;
- }
-
- public static <T> PaginatedResult<T> buildPaginatedResult(Result result, Class<T> itemClass) {
- return buildPaginatedResult(result, getItemFactory(itemClass));
- }
-
- public static <T> PaginatedResult<T> buildPaginatedResult(Result result, ItemFactory<T> factory) {
- if (!result.isSuccessful()) {
- return new PaginatedResult<T>(0, 0, Collections.<T>emptyList());
- }
-
- DomElement contentElement = result.getContentElement();
- return buildPaginatedResult(contentElement, contentElement, factory);
- }
-
- public static <T> PaginatedResult<T> buildPaginatedResult(DomElement contentElement, DomElement childElement, Class<T> itemClass) {
- return buildPaginatedResult(contentElement, childElement, getItemFactory(itemClass));
- }
-
- public static <T> PaginatedResult<T> buildPaginatedResult(DomElement contentElement, DomElement childElement, ItemFactory<T> factory) {
- Collection<T> items = buildCollection(childElement, factory);
-
- String totalPagesAttribute = contentElement.getAttribute("totalPages");
- if (totalPagesAttribute == null)
- totalPagesAttribute = contentElement.getAttribute("totalpages");
-
- int page = Integer.parseInt(contentElement.getAttribute("page"));
- int totalPages = Integer.parseInt(totalPagesAttribute);
-
- return new PaginatedResult<T>(page, totalPages, items);
- }
-
- public static <T> T buildItem(Result result, Class<T> itemClass) {
- return buildItem(result, getItemFactory(itemClass));
- }
-
- public static <T> T buildItem(Result result, ItemFactory<T> factory) {
- if (!result.isSuccessful())
- return null;
- return buildItem(result.getContentElement(), factory);
- }
-
- public static <T> T buildItem(DomElement element, Class<T> itemClass) {
- return buildItem(element, getItemFactory(itemClass));
- }
-
- private static <T> T buildItem(DomElement element, ItemFactory<T> factory) {
- return factory.createItemFromElement(element);
- }
-}
diff --git a/src/com/andrew/apollo/lastfm/api/Result.java b/src/com/andrew/apollo/lastfm/api/Result.java
deleted file mode 100644
index b6541eb..0000000
--- a/src/com/andrew/apollo/lastfm/api/Result.java
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * Copyright (c) 2012, the Last.fm Java Project and Committers
- * All rights reserved.
- *
- * Redistribution and use of this software in source and binary forms, with or without modification, are
- * permitted provided that the following conditions are met:
- *
- * - Redistributions of source code must retain the above
- * copyright notice, this list of conditions and the
- * following disclaimer.
- *
- * - Redistributions in binary form must reproduce the above
- * copyright notice, this list of conditions and the
- * following disclaimer in the documentation and/or other
- * materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
- * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
- * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
- * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
- * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.andrew.apollo.lastfm.api;
-
-import org.w3c.dom.Document;
-
-import com.andrew.apollo.utils.DomElement;
-
-
-/**
- * The <code>Result</code> class contains the response sent by the server, i.e. the status (either ok or failed),
- * an error code and message if failed and the xml response sent by the server.
- *
- * @author Janni Kovacs
- */
-public class Result {
-
- public enum Status {
- OK,
- FAILED
- }
-
- protected Status status;
- protected String errorMessage = null;
- protected int errorCode = -1;
- protected int httpErrorCode = -1;
-
- protected Document resultDocument;
-
- protected Result(Document resultDocument) {
- this.status = Status.OK;
- this.resultDocument = resultDocument;
- }
-
- protected Result(String errorMessage) {
- this.status = Status.FAILED;
- this.errorMessage = errorMessage;
- }
-
- static Result createOkResult(Document resultDocument) {
- return new Result(resultDocument);
- }
-
- static Result createHttpErrorResult(int httpErrorCode, String errorMessage) {
- Result r = new Result(errorMessage);
- r.httpErrorCode = httpErrorCode;
- return r;
- }
-
- static Result createRestErrorResult(int errorCode, String errorMessage) {
- Result r = new Result(errorMessage);
- r.errorCode = errorCode;
- return r;
- }
-
- /**
- * Returns if the operation was successful. Same as <code>getStatus() == Status.OK</code>.
- *
- * @return <code>true</code> if the operation was successful
- */
- public boolean isSuccessful() {
- return status == Status.OK;
- }
-
- public int getErrorCode() {
- return errorCode;
- }
-
- public int getHttpErrorCode() {
- return httpErrorCode;
- }
-
- public Status getStatus() {
- return status;
- }
-
- public Document getResultDocument() {
- return resultDocument;
- }
-
- public String getErrorMessage() {
- return errorMessage;
- }
-
- public DomElement getContentElement() {
- if (!isSuccessful())
- return null;
- return new DomElement(resultDocument.getDocumentElement()).getChild("*");
- }
-
- @Override
- public String toString() {
- return "Result[isSuccessful=" + isSuccessful() + ", errorCode=" + errorCode + ", httpErrorCode=" + httpErrorCode + ", errorMessage="
- + errorMessage + ", status=" + status+"]";
- }
-}
diff --git a/src/com/andrew/apollo/lastfm/api/Session.java b/src/com/andrew/apollo/lastfm/api/Session.java
deleted file mode 100644
index 5056e20..0000000
--- a/src/com/andrew/apollo/lastfm/api/Session.java
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * Copyright (c) 2012, the Last.fm Java Project and Committers
- * All rights reserved.
- *
- * Redistribution and use of this software in source and binary forms, with or without modification, are
- * permitted provided that the following conditions are met:
- *
- * - Redistributions of source code must retain the above
- * copyright notice, this list of conditions and the
- * following disclaimer.
- *
- * - Redistributions in binary form must reproduce the above
- * copyright notice, this list of conditions and the
- * following disclaimer in the documentation and/or other
- * materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
- * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
- * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
- * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
- * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package com.andrew.apollo.lastfm.api;
-
-import com.andrew.apollo.utils.DomElement;
-
-/**
- * Contains Session data relevant for making API calls which require
- * authentication. A <code>Session</code> instance is passed to all methods
- * requiring previous authentication.
- *
- * @author Janni Kovacs
- * @see de.umass.lastfm.Authenticator
- */
-public class Session {
-
- private String apiKey;
-
- private String secret;
-
- private String username;
-
- private String key;
-
- private boolean subscriber;
-
- private Session() {
- }
-
- /**
- * Restores a Session instance with the given session key.
- *
- * @param apiKey An api key
- * @param secret A secret
- * @param sessionKey The previously obtained session key
- * @return a Session instance
- */
- public static Session createSession(String apiKey, String secret, String sessionKey) {
- return createSession(apiKey, secret, sessionKey, null, false);
- }
-
- /**
- * Restores a Session instance with the given session key.
- *
- * @param apiKey An api key
- * @param secret A secret
- * @param sessionKey The previously obtained session key
- * @param username A Last.fm username
- * @param subscriber Subscriber status
- * @return a Session instance
- */
- public static Session createSession(String apiKey, String secret, String sessionKey,
- String username, boolean subscriber) {
- Session s = new Session();
- s.apiKey = apiKey;
- s.secret = secret;
- s.key = sessionKey;
- s.username = username;
- s.subscriber = subscriber;
- return s;
- }
-
- public String getSecret() {
- return secret;
- }
-
- public String getApiKey() {
- return apiKey;
- }
-
- public String getKey() {
- return key;
- }
-
- public boolean isSubscriber() {
- return subscriber;
- }
-
- public String getUsername() {
- return username;
- }
-
- @Override
- public String toString() {
- return "Session[" + "apiKey=" + apiKey + ", secret=" + secret + ", username=" + username
- + ", key=" + key + ", subscriber=" + subscriber + ']';
- }
-
- static Session sessionFromElement(DomElement element, String apiKey, String secret) {
- if (element == null)
- return null;
- String user = element.getChildText("name");
- String key = element.getChildText("key");
- boolean subsc = element.getChildText("subscriber").equals("1");
- return createSession(apiKey, secret, key, user, subsc);
- }
-}
diff --git a/src/com/andrew/apollo/list/fragments/ArtistAlbumsFragment.java b/src/com/andrew/apollo/list/fragments/ArtistAlbumsFragment.java
deleted file mode 100644
index b074b70..0000000
--- a/src/com/andrew/apollo/list/fragments/ArtistAlbumsFragment.java
+++ /dev/null
@@ -1,276 +0,0 @@
-/**
- *
- */
-
-package com.andrew.apollo.list.fragments;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.provider.BaseColumns;
-import android.provider.MediaStore.Audio;
-import android.provider.MediaStore.Audio.AlbumColumns;
-import android.support.v4.app.Fragment;
-import android.support.v4.app.LoaderManager.LoaderCallbacks;
-import android.support.v4.content.CursorLoader;
-import android.support.v4.content.Loader;
-import android.view.ContextMenu;
-import android.view.ContextMenu.ContextMenuInfo;
-import android.view.LayoutInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.AdapterView;
-import android.widget.AdapterView.OnItemClickListener;
-import android.widget.ImageView;
-import android.widget.ListView;
-import android.widget.TextView;
-
-import com.andrew.apollo.Constants;
-import com.andrew.apollo.R;
-import com.andrew.apollo.activities.TracksBrowser;
-import com.andrew.apollo.adapters.ArtistAlbumAdapter;
-import com.andrew.apollo.service.ApolloService;
-import com.andrew.apollo.tasks.GetCachedImages;
-import com.andrew.apollo.tasks.LastfmGetAlbumImages;
-import com.andrew.apollo.utils.ApolloUtils;
-import com.andrew.apollo.utils.MusicUtils;
-
-/**
- * @author Andrew Neal
- * @Note This is used in the @TracksBrowser after touching an artist from @ArtistsFragment
- */
-public class ArtistAlbumsFragment extends Fragment implements LoaderCallbacks<Cursor>,
- OnItemClickListener, Constants {
-
- // Adapter
- private ArtistAlbumAdapter mArtistAlbumAdapter;
-
- // Audio columns
- public static int mAlbumIdIndex, mAlbumNameIndex, mSongCountIndex, mArtistNameIndex;
-
- // ListView
- private ListView mListView;
-
- // Options
- private final int PLAY_SELECTION = 15;
-
- private final int ADD_TO_PLAYLIST = 16;
-
- private final int SEARCH = 17;
-
- // Album ID
- private String mCurrentAlbumId;
-
- // Cursor
- private Cursor mCursor;
-
- public ArtistAlbumsFragment() {
- }
-
- public ArtistAlbumsFragment(Bundle args) {
- setArguments(args);
- }
-
- @Override
- public void onActivityCreated(Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
- // AlbumAdapter
- mArtistAlbumAdapter = new ArtistAlbumAdapter(getActivity(), R.layout.listview_items, null,
- new String[] {}, new int[] {}, 0);
- mListView.setOnCreateContextMenuListener(this);
- mListView.setAdapter(mArtistAlbumAdapter);
- mListView.setOnItemClickListener(this);
-
- // Important!
- getLoaderManager().initLoader(0, null, this);
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
- View root = inflater.inflate(R.layout.listview, container, false);
- mListView = (ListView)root.findViewById(android.R.id.list);
-
- // Set the header for @TrackBrowser
- String header = getActivity().getResources().getString(R.string.album_header);
- int left = getActivity().getResources().getInteger(R.integer.listview_padding_left);
- int right = getActivity().getResources().getInteger(R.integer.listview_padding_right);
- ApolloUtils.listHeader(this, root, header);
- ApolloUtils.setListPadding(this, mListView, left, 0, right, 0);
- return root;
- }
-
- @Override
- public Loader<Cursor> onCreateLoader(int id, Bundle args) {
- String[] projection = {
- BaseColumns._ID, AlbumColumns.ALBUM, AlbumColumns.NUMBER_OF_SONGS,
- AlbumColumns.ARTIST
- };
- if (getArguments() != null) {
- long artistId = getArguments().getLong((BaseColumns._ID));
- Uri uri = Audio.Artists.Albums.getContentUri(EXTERNAL, artistId);
- String sortOrder = Audio.Albums.DEFAULT_SORT_ORDER;
- return new CursorLoader(getActivity(), uri, projection, null, null, sortOrder);
- }
- return null;
- }
-
- @Override
- public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
- // Check for database errors
- if (data == null) {
- return;
- }
-
- mAlbumIdIndex = data.getColumnIndexOrThrow(BaseColumns._ID);
- mAlbumNameIndex = data.getColumnIndexOrThrow(AlbumColumns.ALBUM);
- mSongCountIndex = data.getColumnIndexOrThrow(AlbumColumns.NUMBER_OF_SONGS);
- mArtistNameIndex = data.getColumnIndexOrThrow(AlbumColumns.ARTIST);
- mArtistAlbumAdapter.changeCursor(data);
- mCursor = data;
- }
-
- @Override
- public void onLoaderReset(Loader<Cursor> loader) {
- if (mArtistAlbumAdapter != null)
- mArtistAlbumAdapter.changeCursor(null);
- }
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- outState.putAll(getArguments() != null ? getArguments() : new Bundle());
- super.onSaveInstanceState(outState);
- }
-
- @Override
- public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
- tracksBrowser(id);
- }
-
- /**
- * Update the list as needed
- */
- private final BroadcastReceiver mMediaStatusReceiver = new BroadcastReceiver() {
-
- @Override
- public void onReceive(Context context, Intent intent) {
- if (mListView != null) {
- mArtistAlbumAdapter.notifyDataSetChanged();
- }
- }
-
- };
-
- @Override
- public void onStart() {
- super.onStart();
- IntentFilter filter = new IntentFilter();
- filter.addAction(ApolloService.META_CHANGED);
- filter.addAction(ApolloService.PLAYSTATE_CHANGED);
- getActivity().registerReceiver(mMediaStatusReceiver, filter);
- }
-
- @Override
- public void onStop() {
- getActivity().unregisterReceiver(mMediaStatusReceiver);
- super.onStop();
- }
-
- /**
- * @param index
- * @param id
- */
- private void tracksBrowser(long id) {
-
- String artistName = mCursor.getString(mArtistNameIndex);
- String albumName = mCursor.getString(mAlbumNameIndex);
-
- Bundle bundle = new Bundle();
- bundle.putString(MIME_TYPE, Audio.Albums.CONTENT_TYPE);
- bundle.putString(ALBUM_KEY, albumName);
- bundle.putString(ARTIST_KEY, artistName);
- bundle.putLong(BaseColumns._ID, id);
-
- Intent intent = new Intent(Intent.ACTION_VIEW);
- intent.setClass(getActivity(), TracksBrowser.class);
- intent.putExtras(bundle);
- getActivity().startActivity(intent);
- getActivity().finish();
- }
-
- @Override
- public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
- menu.add(0, PLAY_SELECTION, 0, getResources().getString(R.string.play_all));
- menu.add(0, ADD_TO_PLAYLIST, 0, getResources().getString(R.string.add_to_playlist));
- menu.add(0, SEARCH, 0, getResources().getString(R.string.search));
-
- mCurrentAlbumId = mCursor.getString(mCursor.getColumnIndexOrThrow(BaseColumns._ID));
-
- menu.setHeaderView(setHeaderLayout());
- super.onCreateContextMenu(menu, v, menuInfo);
- }
-
- @Override
- public boolean onContextItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case PLAY_SELECTION: {
- long[] list = MusicUtils.getSongListForAlbum(getActivity(),
- Long.parseLong(mCurrentAlbumId));
- MusicUtils.playAll(getActivity(), list, 0);
- break;
- }
- case ADD_TO_PLAYLIST: {
- Intent intent = new Intent(INTENT_ADD_TO_PLAYLIST);
- long[] list = MusicUtils.getSongListForAlbum(getActivity(),
- Long.parseLong(mCurrentAlbumId));
- intent.putExtra(INTENT_PLAYLIST_LIST, list);
- getActivity().startActivity(intent);
- break;
- }
- case SEARCH: {
- MusicUtils.doSearch(getActivity(), mCursor, mAlbumNameIndex);
- break;
- }
- default:
- break;
- }
- return super.onContextItemSelected(item);
- }
-
- /**
- * @return A custom ContextMenu header
- */
- public View setHeaderLayout() {
- // Get album name
- String albumName = mCursor.getString(mAlbumNameIndex);
- // Get artist name
- String artistName = mCursor.getString(mArtistNameIndex);
-
- // Inflate the header View
- LayoutInflater inflater = getActivity().getLayoutInflater();
- View header = inflater.inflate(R.layout.context_menu_header, null, false);
-
- // Artist image
- ImageView headerImage = (ImageView)header.findViewById(R.id.header_image);
-
- // Only download images we don't already have
- if (ApolloUtils.getImageURL(albumName, ALBUM_IMAGE, getActivity()) == null)
- new LastfmGetAlbumImages(getActivity(), null, 0).executeOnExecutor(
- AsyncTask.THREAD_POOL_EXECUTOR, artistName, albumName);
-
- // Get and set cached image
- new GetCachedImages(getActivity(), 1, headerImage).executeOnExecutor(
- AsyncTask.THREAD_POOL_EXECUTOR, albumName);
-
- // Set artist name
- TextView headerText = (TextView)header.findViewById(R.id.header_text);
- headerText.setText(albumName);
- headerText.setBackgroundColor(getResources().getColor(R.color.transparent_black));
- return header;
- }
-}
diff --git a/src/com/andrew/apollo/list/fragments/GenresFragment.java b/src/com/andrew/apollo/list/fragments/GenresFragment.java
deleted file mode 100644
index 3a686a5..0000000
--- a/src/com/andrew/apollo/list/fragments/GenresFragment.java
+++ /dev/null
@@ -1,165 +0,0 @@
-/**
- *
- */
-
-package com.andrew.apollo.list.fragments;
-
-import android.content.Intent;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.Bundle;
-import android.provider.BaseColumns;
-import android.provider.MediaStore.Audio;
-import android.support.v4.app.Fragment;
-import android.support.v4.app.LoaderManager.LoaderCallbacks;
-import android.support.v4.content.CursorLoader;
-import android.support.v4.content.Loader;
-import android.view.ContextMenu;
-import android.view.ContextMenu.ContextMenuInfo;
-import android.view.LayoutInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.AdapterView;
-import android.widget.AdapterView.OnItemClickListener;
-import android.widget.ListView;
-
-import com.andrew.apollo.Constants;
-import com.andrew.apollo.R;
-import com.andrew.apollo.activities.TracksBrowser;
-import com.andrew.apollo.adapters.GenreAdapter;
-import com.andrew.apollo.utils.MusicUtils;
-
-/**
- * @author Andrew Neal
- * @Note This is the fifth and final tab
- */
-public class GenresFragment extends Fragment implements LoaderCallbacks<Cursor>, Constants,
- OnItemClickListener {
-
- // Adapter
- private GenreAdapter mGenreAdapter;
-
- // ListView
- private ListView mListView;
-
- // Cursor
- private Cursor mCursor;
-
- // Current genre Id
- private String mCurrentGenreId;
-
- // Options
- private final int PLAY_SELECTION = 14;
-
- // Audio columns
- public static int mGenreIdIndex, mGenreNameIndex;
-
- // Bundle
- public GenresFragment() {
- }
-
- public GenresFragment(Bundle args) {
- setArguments(args);
- }
-
- @Override
- public void onActivityCreated(Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
- // GenreAdapter
- mGenreAdapter = new GenreAdapter(getActivity(), R.layout.listview_items, null,
- new String[] {}, new int[] {}, 0);
- mListView.setOnCreateContextMenuListener(this);
- mListView.setOnItemClickListener(this);
- mListView.setAdapter(mGenreAdapter);
-
- // Important!
- getLoaderManager().initLoader(0, null, this);
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
- View root = inflater.inflate(R.layout.listview, container, false);
- mListView = (ListView)root.findViewById(android.R.id.list);
- return root;
- }
-
- @Override
- public Loader<Cursor> onCreateLoader(int id, Bundle args) {
- String[] projection = new String[] {
- Audio.Genres._ID, Audio.Genres.NAME
- };
- Uri uri = Audio.Genres.EXTERNAL_CONTENT_URI;
- String sortOrder = Audio.Genres.DEFAULT_SORT_ORDER;
- return new CursorLoader(getActivity(), uri, projection, null, null, sortOrder);
- }
-
- @Override
- public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
- // Check for database errors
- if (data == null) {
- return;
- }
-
- mGenreIdIndex = data.getColumnIndexOrThrow(Audio.Genres._ID);
- mGenreNameIndex = data.getColumnIndexOrThrow(Audio.Genres.NAME);
- mGenreAdapter.changeCursor(data);
- mCursor = data;
- }
-
- @Override
- public void onLoaderReset(Loader<Cursor> loader) {
- if (mGenreAdapter != null)
- mGenreAdapter.changeCursor(null);
- }
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- outState.putAll(getArguments() != null ? getArguments() : new Bundle());
- super.onSaveInstanceState(outState);
- }
-
- @Override
- public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
- tracksBrowser(position, id);
- }
-
- private void tracksBrowser(int index, long id) {
-
- String genreKey = mCursor.getString(mGenreNameIndex);
-
- Bundle bundle = new Bundle();
- bundle.putString(MIME_TYPE, Audio.Genres.CONTENT_TYPE);
- bundle.putString(GENRE_KEY, genreKey);
- bundle.putLong(BaseColumns._ID, id);
-
- Intent intent = new Intent(getActivity(), TracksBrowser.class);
- intent.putExtras(bundle);
- getActivity().startActivity(intent);
- }
-
- @Override
- public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
- menu.add(0, PLAY_SELECTION, 0, getResources().getString(R.string.play_all));
-
- mCurrentGenreId = mCursor.getString(mCursor.getColumnIndexOrThrow(BaseColumns._ID));
-
- String title = mCursor.getString(mGenreNameIndex);
- menu.setHeaderTitle(title);
- super.onCreateContextMenu(menu, v, menuInfo);
- }
-
- @Override
- public boolean onContextItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case PLAY_SELECTION:
- long[] list = MusicUtils.getSongListForGenre(getActivity(),
- Long.parseLong(mCurrentGenreId));
- MusicUtils.playAll(getActivity(), list, 0);
- break;
- default:
- break;
- }
- return super.onContextItemSelected(item);
- }
-}
diff --git a/src/com/andrew/apollo/list/fragments/PlaylistsFragment.java b/src/com/andrew/apollo/list/fragments/PlaylistsFragment.java
deleted file mode 100644
index c5e829c..0000000
--- a/src/com/andrew/apollo/list/fragments/PlaylistsFragment.java
+++ /dev/null
@@ -1,197 +0,0 @@
-/**
- *
- */
-
-package com.andrew.apollo.list.fragments;
-
-import android.content.ContentUris;
-import android.content.Intent;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.Bundle;
-import android.provider.BaseColumns;
-import android.provider.MediaStore;
-import android.provider.MediaStore.Audio;
-import android.provider.MediaStore.Audio.PlaylistsColumns;
-import android.support.v4.app.Fragment;
-import android.support.v4.app.LoaderManager.LoaderCallbacks;
-import android.support.v4.content.CursorLoader;
-import android.support.v4.content.Loader;
-import android.view.ContextMenu;
-import android.view.ContextMenu.ContextMenuInfo;
-import android.view.LayoutInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.AdapterView;
-import android.widget.AdapterView.AdapterContextMenuInfo;
-import android.widget.AdapterView.OnItemClickListener;
-import android.widget.ListView;
-
-import com.andrew.apollo.Constants;
-import com.andrew.apollo.R;
-import com.andrew.apollo.activities.TracksBrowser;
-import com.andrew.apollo.adapters.PlaylistAdapter;
-import com.andrew.apollo.utils.MusicUtils;
-
-/**
- * @author Andrew Neal
- */
-public class PlaylistsFragment extends Fragment implements LoaderCallbacks<Cursor>, Constants,
- OnItemClickListener {
-
- // Adapter
- private PlaylistAdapter mPlaylistAdapter;
-
- // ListView
- private ListView mListView;
-
- // Cursor
- private Cursor mCursor;
-
- // Current playlist Id
- private String mCurrentPlaylistId;
-
- // Options
- private static final int PLAY_SELECTION = 11;
-
- private static final int DELETE_PLAYLIST = 12;
-
- private static final int RENAME_PLAYLIST = 13;
-
- // Aduio columns
- public static int mPlaylistNameIndex, mPlaylistIdIndex;
-
- // Bundle
- public PlaylistsFragment() {
- }
-
- public PlaylistsFragment(Bundle args) {
- setArguments(args);
- }
-
- @Override
- public void onActivityCreated(Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
- // Adapter
- mPlaylistAdapter = new PlaylistAdapter(getActivity(), R.layout.listview_items, null,
- new String[] {}, new int[] {}, 0);
- mListView.setOnCreateContextMenuListener(this);
- mListView.setAdapter(mPlaylistAdapter);
- mListView.setOnItemClickListener(this);
-
- getLoaderManager().initLoader(0, null, this);
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
- View root = inflater.inflate(R.layout.listview, container, false);
- mListView = (ListView)root.findViewById(android.R.id.list);
- return root;
- }
-
- @Override
- public Loader<Cursor> onCreateLoader(int id, Bundle args) {
- String[] projection = new String[] {
- BaseColumns._ID, PlaylistsColumns.NAME
- };
- Uri uri = Audio.Playlists.EXTERNAL_CONTENT_URI;
- String sortOrder = Audio.Playlists.DEFAULT_SORT_ORDER;
- return new CursorLoader(getActivity(), uri, projection, null, null, sortOrder);
- }
-
- @Override
- public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
- // Check for database errors
- if (data == null) {
- return;
- }
-
- mPlaylistIdIndex = data.getColumnIndexOrThrow(BaseColumns._ID);
- mPlaylistNameIndex = data.getColumnIndexOrThrow(PlaylistsColumns.NAME);
- mPlaylistAdapter.changeCursor(data);
- mCursor = data;
- }
-
- @Override
- public void onLoaderReset(Loader<Cursor> loader) {
- if (mPlaylistAdapter != null)
- mPlaylistAdapter.changeCursor(null);
- }
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- outState.putAll(getArguments() != null ? getArguments() : new Bundle());
- super.onSaveInstanceState(outState);
- }
-
- @Override
- public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
-
- AdapterContextMenuInfo mi = (AdapterContextMenuInfo)menuInfo;
-
- menu.add(0, PLAY_SELECTION, 0, getResources().getString(R.string.play_all));
-
- if (mi.id >= 0) {
- menu.add(0, RENAME_PLAYLIST, 0, getResources().getString(R.string.rename_playlist));
- menu.add(0, DELETE_PLAYLIST, 0, getResources().getString(R.string.delete_playlist));
- }
-
- mCurrentPlaylistId = mCursor.getString(mCursor.getColumnIndexOrThrow(BaseColumns._ID));
-
- String title = mCursor.getString(mPlaylistNameIndex);
- menu.setHeaderTitle(title);
- super.onCreateContextMenu(menu, v, menuInfo);
- }
-
- @Override
- public boolean onContextItemSelected(MenuItem item) {
- AdapterContextMenuInfo mi = (AdapterContextMenuInfo)item.getMenuInfo();
- switch (item.getItemId()) {
- case PLAY_SELECTION: {
- long[] list = MusicUtils.getSongListForPlaylist(getActivity(),
- Long.parseLong(mCurrentPlaylistId));
- MusicUtils.playAll(getActivity(), list, 0);
- break;
- }
- case RENAME_PLAYLIST: {
- Intent intent = new Intent(INTENT_RENAME_PLAYLIST);
- intent.putExtra(INTENT_KEY_RENAME, mi.id);
- getActivity().startActivity(intent);
- break;
- }
- case DELETE_PLAYLIST: {
- Uri uri = ContentUris.withAppendedId(
- MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, mi.id);
- getActivity().getContentResolver().delete(uri, null, null);
- break;
- }
- default:
- break;
- }
- return super.onContextItemSelected(item);
- }
-
- @Override
- public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
- tracksBrowser(id);
- }
-
- /**
- * @param id
- */
- private void tracksBrowser(long id) {
-
- String playlistName = mCursor.getString(mPlaylistNameIndex);
-
- Bundle bundle = new Bundle();
- bundle.putString(MIME_TYPE, Audio.Playlists.CONTENT_TYPE);
- bundle.putString(PLAYLIST_NAME, playlistName);
- bundle.putLong(BaseColumns._ID, id);
-
- Intent intent = new Intent(Intent.ACTION_VIEW);
- intent.setClass(getActivity(), TracksBrowser.class);
- intent.putExtras(bundle);
- getActivity().startActivity(intent);
- }
-}
diff --git a/src/com/andrew/apollo/list/fragments/RecentlyAddedFragment.java b/src/com/andrew/apollo/list/fragments/RecentlyAddedFragment.java
deleted file mode 100644
index 0d36d61..0000000
--- a/src/com/andrew/apollo/list/fragments/RecentlyAddedFragment.java
+++ /dev/null
@@ -1,166 +0,0 @@
-/**
- *
- */
-
-package com.andrew.apollo.list.fragments;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.Bundle;
-import android.provider.BaseColumns;
-import android.provider.MediaStore.Audio;
-import android.provider.MediaStore.Audio.AudioColumns;
-import android.provider.MediaStore.MediaColumns;
-import android.support.v4.app.Fragment;
-import android.support.v4.app.LoaderManager.LoaderCallbacks;
-import android.support.v4.content.CursorLoader;
-import android.support.v4.content.Loader;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.AdapterView;
-import android.widget.AdapterView.OnItemClickListener;
-import android.widget.ListView;
-
-import com.andrew.apollo.Constants;
-import com.andrew.apollo.NowPlayingCursor;
-import com.andrew.apollo.R;
-import com.andrew.apollo.adapters.RecentlyAddedAdapter;
-import com.andrew.apollo.service.ApolloService;
-import com.andrew.apollo.utils.MusicUtils;
-
-/**
- * @author Andrew Neal
- */
-public class RecentlyAddedFragment extends Fragment implements LoaderCallbacks<Cursor>,
- OnItemClickListener, Constants {
-
- // Adapter
- private RecentlyAddedAdapter mRecentlyAddedAdapter;
-
- // ListView
- private ListView mListView;
-
- // Cursor
- private Cursor mCursor;
-
- // Audio columns
- public static int mTitleIndex, mAlbumIndex, mArtistIndex, mMediaIdIndex;
-
- // Bundle
- public RecentlyAddedFragment() {
- }
-
- public RecentlyAddedFragment(Bundle args) {
- setArguments(args);
- }
-
- @Override
- public void onActivityCreated(Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
- // Adapter
- mRecentlyAddedAdapter = new RecentlyAddedAdapter(getActivity(), R.layout.listview_items,
- null, new String[] {}, new int[] {}, 0);
- mListView.setOnCreateContextMenuListener(this);
- mListView.setAdapter(mRecentlyAddedAdapter);
- mListView.setOnItemClickListener(this);
-
- // Important!
- getLoaderManager().initLoader(0, null, this);
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
- View root = inflater.inflate(R.layout.listview, container, false);
- mListView = (ListView)root.findViewById(android.R.id.list);
- return root;
- }
-
- @Override
- public Loader<Cursor> onCreateLoader(int id, Bundle args) {
- String[] projection = new String[] {
- BaseColumns._ID, MediaColumns.TITLE, AudioColumns.ALBUM, AudioColumns.ARTIST
- };
- StringBuilder where = new StringBuilder();
- String sortOrder = MediaColumns.DATE_ADDED + " DESC";
- Uri uri = Audio.Media.EXTERNAL_CONTENT_URI;
- int X = MusicUtils.getIntPref(getActivity(), NUMWEEKS, 5) * 3600 * 24 * 7;
- where = new StringBuilder();
- where.append(MediaColumns.TITLE + " != ''");
- where.append(" AND " + AudioColumns.IS_MUSIC + "=1");
- where.append(" AND " + MediaColumns.DATE_ADDED + ">"
- + (System.currentTimeMillis() / 1000 - X));
- return new CursorLoader(getActivity(), uri, projection, where.toString(), null, sortOrder);
- }
-
- @Override
- public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
- // Check for database errors
- if (data == null) {
- return;
- }
-
- mMediaIdIndex = data.getColumnIndexOrThrow(BaseColumns._ID);
- mTitleIndex = data.getColumnIndexOrThrow(MediaColumns.TITLE);
- mArtistIndex = data.getColumnIndexOrThrow(AudioColumns.ARTIST);
- mAlbumIndex = data.getColumnIndexOrThrow(AudioColumns.ALBUM);
- mRecentlyAddedAdapter.changeCursor(data);
- mCursor = data;
- }
-
- @Override
- public void onLoaderReset(Loader<Cursor> loader) {
- if (mRecentlyAddedAdapter != null)
- mRecentlyAddedAdapter.changeCursor(null);
- }
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- outState.putAll(getArguments() != null ? getArguments() : new Bundle());
- super.onSaveInstanceState(outState);
- }
-
- @Override
- public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
- if (mCursor instanceof NowPlayingCursor) {
- if (MusicUtils.mService != null) {
- MusicUtils.setQueuePosition(position);
- return;
- }
- }
- MusicUtils.playAll(getActivity(), mCursor, position);
- }
-
- /**
- * Update the list as needed
- */
- private final BroadcastReceiver mMediaStatusReceiver = new BroadcastReceiver() {
-
- @Override
- public void onReceive(Context context, Intent intent) {
- if (mListView != null) {
- mRecentlyAddedAdapter.notifyDataSetChanged();
- }
- }
-
- };
-
- @Override
- public void onStart() {
- super.onStart();
- IntentFilter filter = new IntentFilter();
- filter.addAction(ApolloService.META_CHANGED);
- filter.addAction(ApolloService.PLAYSTATE_CHANGED);
- getActivity().registerReceiver(mMediaStatusReceiver, filter);
- }
-
- @Override
- public void onStop() {
- getActivity().unregisterReceiver(mMediaStatusReceiver);
- super.onStop();
- }
-}
diff --git a/src/com/andrew/apollo/list/fragments/TracksFragment.java b/src/com/andrew/apollo/list/fragments/TracksFragment.java
deleted file mode 100644
index f199182..0000000
--- a/src/com/andrew/apollo/list/fragments/TracksFragment.java
+++ /dev/null
@@ -1,463 +0,0 @@
-/**
- *
- */
-
-package com.andrew.apollo.list.fragments;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.Bundle;
-import android.provider.BaseColumns;
-import android.provider.MediaStore.Audio;
-import android.provider.MediaStore.Audio.Albums;
-import android.provider.MediaStore.Audio.Artists;
-import android.provider.MediaStore.Audio.AudioColumns;
-import android.provider.MediaStore.Audio.Genres;
-import android.provider.MediaStore.Audio.Playlists;
-import android.provider.MediaStore.MediaColumns;
-import android.support.v4.app.Fragment;
-import android.support.v4.app.LoaderManager.LoaderCallbacks;
-import android.support.v4.content.CursorLoader;
-import android.support.v4.content.Loader;
-import android.view.ContextMenu;
-import android.view.ContextMenu.ContextMenuInfo;
-import android.view.LayoutInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.AdapterView;
-import android.widget.AdapterView.AdapterContextMenuInfo;
-import android.widget.AdapterView.OnItemClickListener;
-import android.widget.LinearLayout;
-import android.widget.ListView;
-import android.widget.TextView;
-
-import com.andrew.apollo.Constants;
-import com.andrew.apollo.NowPlayingCursor;
-import com.andrew.apollo.R;
-import com.andrew.apollo.adapters.TrackAdapter;
-import com.andrew.apollo.service.ApolloService;
-import com.andrew.apollo.utils.ApolloUtils;
-import com.andrew.apollo.utils.MusicUtils;
-
-/**
- * @author Andrew Neal
- */
-public class TracksFragment extends Fragment implements LoaderCallbacks<Cursor>,
- OnItemClickListener, Constants {
-
- // Adapter
- private TrackAdapter mTrackAdapter;
-
- // ListView
- private ListView mListView;
-
- // Cursor
- private Cursor mCursor;
-
- // Playlist ID
- private long mPlaylistId = -1;
-
- // Selected position
- private int mSelectedPosition;
-
- // Used to set ringtone
- private long mSelectedId;
-
- // Options
- private final int PLAY_SELECTION = 6;
-
- private final int USE_AS_RINGTONE = 7;
-
- private final int ADD_TO_PLAYLIST = 8;
-
- private final int SEARCH = 9;
-
- private final int REMOVE = 10;
-
- private boolean mEditMode = false;
-
- // Audio columns
- public static int mTitleIndex, mAlbumIndex, mArtistIndex, mMediaIdIndex;
-
- // Bundle
- public TracksFragment() {
- }
-
- public TracksFragment(Bundle args) {
- setArguments(args);
- }
-
- @Override
- public void onActivityCreated(Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
-
- isEditMode();
-
- // Adapter
- mTrackAdapter = new TrackAdapter(getActivity(), R.layout.listview_items, null,
- new String[] {}, new int[] {}, 0);
- mListView.setOnCreateContextMenuListener(this);
- mListView.setOnItemClickListener(this);
- mListView.setAdapter(mTrackAdapter);
-
- // Important!
- getLoaderManager().initLoader(0, null, this);
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
- View root = inflater.inflate(R.layout.listview, container, false);
- mListView = (ListView)root.findViewById(android.R.id.list);
-
- // Align the track list with the header, in other words,OCD.
- TextView mHeader = (TextView)root.findViewById(R.id.title);
- int eight = (int)getActivity().getResources().getDimension(
- R.dimen.list_separator_padding_left_right);
- mHeader.setPadding(eight, 0, 0, 0);
-
- // Set the header while in @TracksBrowser
- String header = getActivity().getResources().getString(R.string.track_header);
- int left = getActivity().getResources().getInteger(R.integer.listview_padding_left);
- int right = getActivity().getResources().getInteger(R.integer.listview_padding_right);
- ApolloUtils.listHeader(this, root, header);
- ApolloUtils.setListPadding(this, mListView, left, 0, right, 0);
-
- // Hide the extra spacing from the Bottom ActionBar in the queue
- // Fragment in @AudioPlayerHolder
- if (getArguments() != null) {
- mPlaylistId = getArguments().getLong(BaseColumns._ID);
- String mimeType = getArguments().getString(MIME_TYPE);
- if (Audio.Playlists.CONTENT_TYPE.equals(mimeType)) {
- switch ((int)mPlaylistId) {
- case (int)PLAYLIST_QUEUE:
- LinearLayout emptyness = (LinearLayout)root.findViewById(R.id.empty_view);
- emptyness.setVisibility(View.GONE);
- }
- }
- }
- return root;
- }
-
- @Override
- public Loader<Cursor> onCreateLoader(int id, Bundle args) {
- String[] projection = new String[] {
- BaseColumns._ID, MediaColumns.TITLE, AudioColumns.ALBUM, AudioColumns.ARTIST
- };
- StringBuilder where = new StringBuilder();
- String sortOrder = Audio.Media.DEFAULT_SORT_ORDER;
- where.append(AudioColumns.IS_MUSIC + "=1").append(" AND " + MediaColumns.TITLE + " != ''");
- Uri uri = Audio.Media.EXTERNAL_CONTENT_URI;
- if (getArguments() != null) {
- mPlaylistId = getArguments().getLong(BaseColumns._ID);
- String mimeType = getArguments().getString(MIME_TYPE);
- if (Audio.Playlists.CONTENT_TYPE.equals(mimeType)) {
- where = new StringBuilder();
- where.append(AudioColumns.IS_MUSIC + "=1");
- where.append(" AND " + MediaColumns.TITLE + " != ''");
- switch ((int)mPlaylistId) {
- case (int)PLAYLIST_QUEUE:
- uri = Audio.Media.EXTERNAL_CONTENT_URI;
- long[] mNowPlaying = MusicUtils.getQueue();
- if (mNowPlaying.length == 0)
- return null;
- where = new StringBuilder();
- where.append(BaseColumns._ID + " IN (");
- if (mNowPlaying == null || mNowPlaying.length <= 0)
- return null;
- for (long queue_id : mNowPlaying) {
- where.append(queue_id + ",");
- }
- where.deleteCharAt(where.length() - 1);
- where.append(")");
- break;
- case (int)PLAYLIST_FAVORITES:
- long favorites_id = MusicUtils.getFavoritesId(getActivity());
- projection = new String[] {
- Playlists.Members._ID, Playlists.Members.AUDIO_ID,
- MediaColumns.TITLE, AudioColumns.ALBUM, AudioColumns.ARTIST
- };
- uri = Playlists.Members.getContentUri(EXTERNAL, favorites_id);
- sortOrder = Playlists.Members.DEFAULT_SORT_ORDER;
- break;
- default:
- if (id < 0)
- return null;
- projection = new String[] {
- Playlists.Members._ID, Playlists.Members.AUDIO_ID,
- MediaColumns.TITLE, AudioColumns.ALBUM, AudioColumns.ARTIST,
- AudioColumns.DURATION
- };
-
- uri = Playlists.Members.getContentUri(EXTERNAL, mPlaylistId);
- sortOrder = Playlists.Members.DEFAULT_SORT_ORDER;
- break;
- }
- } else if (Audio.Genres.CONTENT_TYPE.equals(mimeType)) {
- long genreId = getArguments().getLong(BaseColumns._ID);
- uri = Genres.Members.getContentUri(EXTERNAL, genreId);
- projection = new String[] {
- BaseColumns._ID, MediaColumns.TITLE, AudioColumns.ALBUM,
- AudioColumns.ARTIST
- };
- where = new StringBuilder();
- where.append(AudioColumns.IS_MUSIC + "=1").append(
- " AND " + MediaColumns.TITLE + " != ''");
- sortOrder = Genres.Members.DEFAULT_SORT_ORDER;
- } else {
- if (Audio.Albums.CONTENT_TYPE.equals(mimeType)) {
- long albumId = getArguments().getLong(BaseColumns._ID);
- where.append(" AND " + AudioColumns.ALBUM_ID + "=" + albumId);
- sortOrder = Audio.Media.TRACK + ", " + sortOrder;
- } else if (Audio.Artists.CONTENT_TYPE.equals(mimeType)) {
- sortOrder = MediaColumns.TITLE;
- long artist_id = getArguments().getLong(BaseColumns._ID);
- where.append(" AND " + AudioColumns.ARTIST_ID + "=" + artist_id);
- }
- }
- }
- return new CursorLoader(getActivity(), uri, projection, where.toString(), null, sortOrder);
- }
-
- @Override
- public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
- // Check for database errors
- if (data == null) {
- return;
- }
-
- if (getArguments() != null
- && Playlists.CONTENT_TYPE.equals(getArguments().getString(MIME_TYPE))
- && (getArguments().getLong(BaseColumns._ID) >= 0 || getArguments().getLong(
- BaseColumns._ID) == PLAYLIST_FAVORITES)) {
- mMediaIdIndex = data.getColumnIndexOrThrow(Playlists.Members.AUDIO_ID);
- mTitleIndex = data.getColumnIndexOrThrow(MediaColumns.TITLE);
- mAlbumIndex = data.getColumnIndexOrThrow(AudioColumns.ALBUM);
- // FIXME
- // mArtistIndex =
- // data.getColumnIndexOrThrow(Playlists.Members.ARTIST);
- } else if (getArguments() != null
- && Genres.CONTENT_TYPE.equals(getArguments().getString(MIME_TYPE))) {
- mMediaIdIndex = data.getColumnIndexOrThrow(BaseColumns._ID);
- mTitleIndex = data.getColumnIndexOrThrow(MediaColumns.TITLE);
- mArtistIndex = data.getColumnIndexOrThrow(AudioColumns.ARTIST);
- mAlbumIndex = data.getColumnIndexOrThrow(AudioColumns.ALBUM);
- } else if (getArguments() != null
- && Artists.CONTENT_TYPE.equals(getArguments().getString(MIME_TYPE))) {
- mTitleIndex = data.getColumnIndexOrThrow(MediaColumns.TITLE);
- // mArtistIndex is "line2" of the ListView
- mArtistIndex = data.getColumnIndexOrThrow(AudioColumns.ALBUM);
- } else if (getArguments() != null
- && Albums.CONTENT_TYPE.equals(getArguments().getString(MIME_TYPE))) {
- mMediaIdIndex = data.getColumnIndexOrThrow(BaseColumns._ID);
- mTitleIndex = data.getColumnIndexOrThrow(MediaColumns.TITLE);
- mArtistIndex = data.getColumnIndexOrThrow(AudioColumns.ARTIST);
- } else {
- mMediaIdIndex = data.getColumnIndexOrThrow(BaseColumns._ID);
- mTitleIndex = data.getColumnIndexOrThrow(MediaColumns.TITLE);
- mArtistIndex = data.getColumnIndexOrThrow(AudioColumns.ARTIST);
- mAlbumIndex = data.getColumnIndexOrThrow(AudioColumns.ALBUM);
- }
- mTrackAdapter.changeCursor(data);
- mCursor = data;
- }
-
- @Override
- public void onLoaderReset(Loader<Cursor> loader) {
- if (mTrackAdapter != null)
- mTrackAdapter.changeCursor(null);
- }
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- outState.putAll(getArguments() != null ? getArguments() : new Bundle());
- super.onSaveInstanceState(outState);
- }
-
- @Override
- public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
- menu.add(0, PLAY_SELECTION, 0, getResources().getString(R.string.play_all));
- menu.add(0, ADD_TO_PLAYLIST, 0, getResources().getString(R.string.add_to_playlist));
- menu.add(0, USE_AS_RINGTONE, 0, getResources().getString(R.string.use_as_ringtone));
- if (mEditMode) {
- menu.add(0, REMOVE, 0, R.string.remove);
- }
- menu.add(0, SEARCH, 0, getResources().getString(R.string.search));
-
- AdapterContextMenuInfo mi = (AdapterContextMenuInfo)menuInfo;
- mSelectedPosition = mi.position;
- mCursor.moveToPosition(mSelectedPosition);
-
- try {
- mSelectedId = mCursor.getLong(mMediaIdIndex);
- } catch (IllegalArgumentException ex) {
- mSelectedId = mi.id;
- }
-
- String title = mCursor.getString(mTitleIndex);
- menu.setHeaderTitle(title);
- super.onCreateContextMenu(menu, v, menuInfo);
- }
-
- @Override
- public boolean onContextItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case PLAY_SELECTION:
- int position = mSelectedPosition;
- MusicUtils.playAll(getActivity(), mCursor, position);
- break;
- case ADD_TO_PLAYLIST: {
- Intent intent = new Intent(INTENT_ADD_TO_PLAYLIST);
- long[] list = new long[] {
- mSelectedId
- };
- intent.putExtra(INTENT_PLAYLIST_LIST, list);
- getActivity().startActivity(intent);
- break;
- }
- case USE_AS_RINGTONE:
- MusicUtils.setRingtone(getActivity(), mSelectedId);
- break;
- case REMOVE: {
- removePlaylistItem(mSelectedPosition);
- break;
- }
- case SEARCH: {
- MusicUtils.doSearch(getActivity(), mCursor, mTitleIndex);
- break;
- }
- default:
- break;
- }
- return super.onContextItemSelected(item);
- }
-
- @Override
- public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
- if (mCursor instanceof NowPlayingCursor) {
- if (MusicUtils.mService != null) {
- MusicUtils.setQueuePosition(position);
- return;
- }
- }
- MusicUtils.playAll(getActivity(), mCursor, position);
- }
-
- /**
- * Update the list as needed
- */
- private final BroadcastReceiver mMediaStatusReceiver = new BroadcastReceiver() {
-
- @Override
- public void onReceive(Context context, Intent intent) {
- if (mListView != null) {
- mTrackAdapter.notifyDataSetChanged();
- // Scroll to the currently playing track in the queue
- if (mPlaylistId == PLAYLIST_QUEUE)
- mListView.postDelayed(new Runnable() {
- @Override
- public void run() {
- mListView.setSelection(MusicUtils.getQueuePosition());
- }
- }, 100);
- }
- }
-
- };
-
- @Override
- public void onStart() {
- super.onStart();
-
- IntentFilter filter = new IntentFilter();
- filter.addAction(ApolloService.META_CHANGED);
- filter.addAction(ApolloService.QUEUE_CHANGED);
- filter.addAction(ApolloService.PLAYSTATE_CHANGED);
- getActivity().registerReceiver(mMediaStatusReceiver, filter);
- }
-
- @Override
- public void onStop() {
- getActivity().unregisterReceiver(mMediaStatusReceiver);
- super.onStop();
- }
-
- /**
- * @param which
- */
- private void removePlaylistItem(int which) {
-
- mCursor.moveToPosition(which);
- long id = mCursor.getLong(mMediaIdIndex);
- if (mPlaylistId >= 0) {
- Uri uri = Playlists.Members.getContentUri(EXTERNAL, mPlaylistId);
- getActivity().getContentResolver().delete(uri, Playlists.Members.AUDIO_ID + "=" + id,
- null);
- } else if (mPlaylistId == PLAYLIST_QUEUE) {
- MusicUtils.removeTrack(id);
- reloadQueueCursor();
- } else if (mPlaylistId == PLAYLIST_FAVORITES) {
- MusicUtils.removeFromFavorites(getActivity(), id);
- }
- mListView.invalidateViews();
- }
-
- /**
- * Reload the queue after we remove a track
- */
- private void reloadQueueCursor() {
- if (mPlaylistId == PLAYLIST_QUEUE) {
- String[] cols = new String[] {
- BaseColumns._ID, MediaColumns.TITLE, MediaColumns.DATA, AudioColumns.ALBUM,
- AudioColumns.ARTIST, AudioColumns.ARTIST_ID
- };
- StringBuilder selection = new StringBuilder();
- selection.append(AudioColumns.IS_MUSIC + "=1");
- selection.append(" AND " + MediaColumns.TITLE + " != ''");
- Uri uri = Audio.Media.EXTERNAL_CONTENT_URI;
- long[] mNowPlaying = MusicUtils.getQueue();
- if (mNowPlaying.length == 0) {
- }
- selection = new StringBuilder();
- selection.append(BaseColumns._ID + " IN (");
- for (int i = 0; i < mNowPlaying.length; i++) {
- selection.append(mNowPlaying[i]);
- if (i < mNowPlaying.length - 1) {
- selection.append(",");
- }
- }
- selection.append(")");
- mCursor = MusicUtils.query(getActivity(), uri, cols, selection.toString(), null, null);
- mTrackAdapter.changeCursor(mCursor);
- }
- }
-
- /**
- * Check if we're viewing the contents of a playlist
- */
- public void isEditMode() {
- if (getArguments() != null) {
- String mimetype = getArguments().getString(MIME_TYPE);
- if (Audio.Playlists.CONTENT_TYPE.equals(mimetype)) {
- mPlaylistId = getArguments().getLong(BaseColumns._ID);
- switch ((int)mPlaylistId) {
- case (int)PLAYLIST_QUEUE:
- mEditMode = true;
- break;
- case (int)PLAYLIST_FAVORITES:
- mEditMode = true;
- break;
- default:
- if (mPlaylistId > 0) {
- mEditMode = true;
- }
- break;
- }
- }
- }
- }
-}
diff --git a/src/com/andrew/apollo/loaders/AlbumLoader.java b/src/com/andrew/apollo/loaders/AlbumLoader.java
new file mode 100644
index 0000000..8bae715
--- /dev/null
+++ b/src/com/andrew/apollo/loaders/AlbumLoader.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.loaders;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.provider.BaseColumns;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Audio.AlbumColumns;
+
+import com.andrew.apollo.R;
+import com.andrew.apollo.model.Album;
+import com.andrew.apollo.utils.Lists;
+import com.andrew.apollo.utils.MusicUtils;
+import com.andrew.apollo.utils.PreferenceUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Used to query {@link MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI} and return
+ * the albums on a user's device.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class AlbumLoader extends WrappedAsyncTaskLoader<List<Album>> {
+
+ /**
+ * The result
+ */
+ private final ArrayList<Album> mAlbumsList = Lists.newArrayList();
+
+ /**
+ * The {@link Cursor} used to run the query.
+ */
+ private Cursor mCursor;
+
+ /**
+ * Constructor of <code>AlbumLoader</code>
+ *
+ * @param context The {@link Context} to use
+ */
+ public AlbumLoader(final Context context) {
+ super(context);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List<Album> loadInBackground() {
+ // Create the Cursor
+ mCursor = makeAlbumCursor(getContext());
+ // Gather the data
+ if (mCursor != null && mCursor.moveToFirst()) {
+ do {
+ // Copy the album id
+ final String id = mCursor.getString(0);
+
+ // Copy the album name
+ final String albumName = mCursor.getString(1);
+
+ // Copy the artist name
+ final String artist = mCursor.getString(2);
+
+ // Copy the number of songs
+ final String songCount = mCursor.getString(3);
+
+ // Copy the release year
+ final String year = mCursor.getString(4);
+
+ // Make the song label
+ final String songCountFormatted = MusicUtils.makeLabel(getContext(),
+ R.plurals.Nsongs, songCount);
+
+ // Create a new album
+ final Album album = new Album(id, albumName, artist, songCountFormatted, year);
+
+ // Add everything up
+ mAlbumsList.add(album);
+ } while (mCursor.moveToNext());
+ }
+ // Close the cursor
+ if (mCursor != null) {
+ mCursor.close();
+ mCursor = null;
+ }
+ return mAlbumsList;
+ }
+
+ /**
+ * Creates the {@link Cursor} used to run the query.
+ *
+ * @param context The {@link Context} to use.
+ * @return The {@link Cursor} used to run the album query.
+ */
+ public static final Cursor makeAlbumCursor(final Context context) {
+ return context.getContentResolver().query(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI,
+ new String[] {
+ /* 0 */
+ BaseColumns._ID,
+ /* 1 */
+ AlbumColumns.ALBUM,
+ /* 2 */
+ AlbumColumns.ARTIST,
+ /* 3 */
+ AlbumColumns.NUMBER_OF_SONGS,
+ /* 4 */
+ AlbumColumns.FIRST_YEAR
+ }, null, null, PreferenceUtils.getInstace(context).getAlbumSortOrder());
+ }
+}
diff --git a/src/com/andrew/apollo/loaders/AlbumSongLoader.java b/src/com/andrew/apollo/loaders/AlbumSongLoader.java
new file mode 100644
index 0000000..51f2b7c
--- /dev/null
+++ b/src/com/andrew/apollo/loaders/AlbumSongLoader.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.loaders;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.provider.BaseColumns;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Audio.AudioColumns;
+
+import com.andrew.apollo.model.Song;
+import com.andrew.apollo.utils.Lists;
+import com.andrew.apollo.utils.MusicUtils;
+import com.andrew.apollo.utils.PreferenceUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Used to query {@link MediaStore.Audio.Media.EXTERNAL_CONTENT_URI} and return
+ * the Song for a particular album.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class AlbumSongLoader extends WrappedAsyncTaskLoader<List<Song>> {
+
+ /**
+ * The result
+ */
+ private final ArrayList<Song> mSongList = Lists.newArrayList();
+
+ /**
+ * The {@link Cursor} used to run the query.
+ */
+ private Cursor mCursor;
+
+ /**
+ * The Id of the album the songs belong to.
+ */
+ private final Long mAlbumID;
+
+ /**
+ * Constructor of <code>AlbumSongHandler</code>
+ *
+ * @param context The {@link Context} to use.
+ * @param albumId The Id of the album the songs belong to.
+ */
+ public AlbumSongLoader(final Context context, final Long albumId) {
+ super(context);
+ mAlbumID = albumId;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List<Song> loadInBackground() {
+ // Create the Cursor
+ mCursor = makeAlbumSongCursor(getContext(), mAlbumID);
+ // Gather the data
+ if (mCursor != null && mCursor.moveToFirst()) {
+ do {
+ // Copy the song Id
+ final String id = mCursor.getString(0);
+
+ // Copy the song name
+ final String songName = mCursor.getString(1);
+
+ // Copy the artist name
+ final String artist = mCursor.getString(2);
+
+ // Copy the album name
+ final String album = mCursor.getString(3);
+
+ // Copy the duration
+ final String duration = mCursor.getString(4);
+
+ // Make the duration label
+ final int seconds = Integer.valueOf(duration != null ? duration : "0") / 1000;
+ final String durationFormatted = MusicUtils.makeTimeString(getContext(), seconds);
+
+ // Create a new song
+ final Song song = new Song(id, songName, artist, album, durationFormatted);
+
+ // Add everything up
+ mSongList.add(song);
+ } while (mCursor.moveToNext());
+ }
+ // Close the cursor
+ if (mCursor != null) {
+ mCursor.close();
+ mCursor = null;
+ }
+ return mSongList;
+ }
+
+ /**
+ * @param context The {@link Context} to use.
+ * @param albumId The Id of the album the songs belong to.
+ * @return The {@link Cursor} used to run the query.
+ */
+ public static final Cursor makeAlbumSongCursor(final Context context, final Long albumId) {
+ // Match the songs up with the artist
+ final StringBuilder selection = new StringBuilder();
+ selection.append(AudioColumns.IS_MUSIC + "=1");
+ selection.append(" AND " + AudioColumns.TITLE + " != ''");
+ selection.append(" AND " + AudioColumns.ALBUM_ID + "=" + albumId);
+ return context.getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+ new String[] {
+ /* 0 */
+ BaseColumns._ID,
+ /* 1 */
+ AudioColumns.TITLE,
+ /* 2 */
+ AudioColumns.ARTIST,
+ /* 3 */
+ AudioColumns.ALBUM,
+ /* 4 */
+ AudioColumns.DURATION
+ }, selection.toString(), null,
+ PreferenceUtils.getInstace(context).getAlbumSongSortOrder());
+ }
+
+}
diff --git a/src/com/andrew/apollo/loaders/ArtistAlbumLoader.java b/src/com/andrew/apollo/loaders/ArtistAlbumLoader.java
new file mode 100644
index 0000000..5db233f
--- /dev/null
+++ b/src/com/andrew/apollo/loaders/ArtistAlbumLoader.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.loaders;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.provider.BaseColumns;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Audio.AlbumColumns;
+
+import com.andrew.apollo.R;
+import com.andrew.apollo.model.Album;
+import com.andrew.apollo.utils.Lists;
+import com.andrew.apollo.utils.MusicUtils;
+import com.andrew.apollo.utils.PreferenceUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Used to query {@link MediaStore.Audio.Artists.Albums} and return the albums
+ * for a particular artist.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class ArtistAlbumLoader extends WrappedAsyncTaskLoader<List<Album>> {
+
+ /**
+ * The result
+ */
+ private final ArrayList<Album> mAlbumsList = Lists.newArrayList();
+
+ /**
+ * The {@link Cursor} used to run the query.
+ */
+ private Cursor mCursor;
+
+ /**
+ * The Id of the artist the albums belong to.
+ */
+ private final Long mArtistID;
+
+ /**
+ * Constructor of <code>ArtistAlbumHandler</code>
+ *
+ * @param context The {@link Context} to use.
+ * @param artistId The Id of the artist the albums belong to.
+ */
+ public ArtistAlbumLoader(final Context context, final Long artistId) {
+ super(context);
+ mArtistID = artistId;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List<Album> loadInBackground() {
+ // Create the Cursor
+ mCursor = makeArtistAlbumCursor(getContext(), mArtistID);
+ // Gather the dataS
+ if (mCursor != null && mCursor.moveToFirst()) {
+ do {
+ // Copy the album id
+ final String id = mCursor.getString(0);
+
+ // Copy the album name
+ final String albumName = mCursor.getString(1);
+
+ // Copy the artist name
+ final String artist = mCursor.getString(2);
+
+ // Copy the number of songs
+ final String songCount = mCursor.getString(3);
+
+ // Copy the release year
+ final String year = mCursor.getString(4);
+
+ // Make the song label
+ final String songCountFormatted = MusicUtils.makeLabel(getContext(),
+ R.plurals.Nsongs, songCount);
+
+ // Create a new album
+ final Album album = new Album(id, albumName, artist, songCountFormatted, year);
+
+ // Add everything up
+ mAlbumsList.add(album);
+ } while (mCursor.moveToNext());
+ }
+ // Close the cursor
+ if (mCursor != null) {
+ mCursor.close();
+ mCursor = null;
+ }
+ return mAlbumsList;
+ }
+
+ /**
+ * @param context The {@link Context} to use.
+ * @param artistId The Id of the artist the albums belong to.
+ */
+ public static final Cursor makeArtistAlbumCursor(final Context context, final Long artistId) {
+ return context.getContentResolver().query(
+ MediaStore.Audio.Artists.Albums.getContentUri("external", artistId), new String[] {
+ /* 0 */
+ BaseColumns._ID,
+ /* 1 */
+ AlbumColumns.ALBUM,
+ /* 2 */
+ AlbumColumns.ARTIST,
+ /* 3 */
+ AlbumColumns.NUMBER_OF_SONGS,
+ /* 4 */
+ AlbumColumns.FIRST_YEAR
+ }, null, null, PreferenceUtils.getInstace(context).getArtistAlbumSortOrder());
+ }
+}
diff --git a/src/com/andrew/apollo/loaders/ArtistLoader.java b/src/com/andrew/apollo/loaders/ArtistLoader.java
new file mode 100644
index 0000000..61a5a67
--- /dev/null
+++ b/src/com/andrew/apollo/loaders/ArtistLoader.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.loaders;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.provider.BaseColumns;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Audio.ArtistColumns;
+
+import com.andrew.apollo.R;
+import com.andrew.apollo.model.Artist;
+import com.andrew.apollo.utils.Lists;
+import com.andrew.apollo.utils.MusicUtils;
+import com.andrew.apollo.utils.PreferenceUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Used to query {@link MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI} and
+ * return the artists on a user's device.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class ArtistLoader extends WrappedAsyncTaskLoader<List<Artist>> {
+
+ /**
+ * The result
+ */
+ private final ArrayList<Artist> mArtistsList = Lists.newArrayList();
+
+ /**
+ * The {@link Cursor} used to run the query.
+ */
+ private Cursor mCursor;
+
+ /**
+ * Constructor of <code>ArtistLoader</code>
+ *
+ * @param context The {@link Context} to use
+ */
+ public ArtistLoader(final Context context) {
+ super(context);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List<Artist> loadInBackground() {
+ // Create the Cursor
+ mCursor = makeArtistCursor(getContext());
+ // Gather the data
+ if (mCursor != null && mCursor.moveToFirst()) {
+ do {
+ // Copy the artist id
+ final String id = mCursor.getString(0);
+
+ // Copy the artist name
+ final String artistName = mCursor.getString(1);
+
+ // Copy the number of albums
+ final String albumCount = mCursor.getString(2);
+
+ // Copy the number of songs
+ final String songCount = mCursor.getString(3);
+
+ // Make the album label
+ final String albumCountFormatted = MusicUtils.makeLabel(getContext(),
+ R.plurals.Nalbums, albumCount);
+
+ // Make the song label
+ final String songCountFormatted = MusicUtils.makeLabel(getContext(),
+ R.plurals.Nsongs, songCount);
+
+ // Create a new artist
+ final Artist artist = new Artist(id, artistName, songCountFormatted,
+ albumCountFormatted);
+
+ // Add everything up
+ mArtistsList.add(artist);
+ } while (mCursor.moveToNext());
+ }
+ // Close the cursor
+ if (mCursor != null) {
+ mCursor.close();
+ mCursor = null;
+ }
+ return mArtistsList;
+ }
+
+ /**
+ * Creates the {@link Cursor} used to run the query.
+ *
+ * @param context The {@link Context} to use.
+ * @return The {@link Cursor} used to run the artist query.
+ */
+ public static final Cursor makeArtistCursor(final Context context) {
+ return context.getContentResolver().query(MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI,
+ new String[] {
+ /* 0 */
+ BaseColumns._ID,
+ /* 1 */
+ ArtistColumns.ARTIST,
+ /* 2 */
+ ArtistColumns.NUMBER_OF_ALBUMS,
+ /* 3 */
+ ArtistColumns.NUMBER_OF_TRACKS
+ }, null, null, PreferenceUtils.getInstace(context).getArtistSortOrder());
+ }
+}
diff --git a/src/com/andrew/apollo/loaders/ArtistSongLoader.java b/src/com/andrew/apollo/loaders/ArtistSongLoader.java
new file mode 100644
index 0000000..0fac28c
--- /dev/null
+++ b/src/com/andrew/apollo/loaders/ArtistSongLoader.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.loaders;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.provider.BaseColumns;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Audio.AudioColumns;
+
+import com.andrew.apollo.model.Song;
+import com.andrew.apollo.utils.Lists;
+import com.andrew.apollo.utils.PreferenceUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Used to query {@link MediaStore.Audio.Media.EXTERNAL_CONTENT_URI} and return
+ * the songs for a particular artist.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class ArtistSongLoader extends WrappedAsyncTaskLoader<List<Song>> {
+
+ /**
+ * The result
+ */
+ private final ArrayList<Song> mSongList = Lists.newArrayList();
+
+ /**
+ * The {@link Cursor} used to run the query.
+ */
+ private Cursor mCursor;
+
+ /**
+ * The Id of the artist the songs belong to.
+ */
+ private final Long mArtistID;
+
+ /**
+ * Constructor of <code>ArtistSongLoader</code>
+ *
+ * @param context The {@link Context} to use.
+ * @param artistId The Id of the artist the songs belong to.
+ */
+ public ArtistSongLoader(final Context context, final Long artistId) {
+ super(context);
+ mArtistID = artistId;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List<Song> loadInBackground() {
+ // Create the Cursor
+ mCursor = makeArtistSongCursor(getContext(), mArtistID);
+ // Gather the data
+ if (mCursor != null && mCursor.moveToFirst()) {
+ do {
+ // Copy the song Id
+ final String id = mCursor.getString(0);
+
+ // Copy the song name
+ final String songName = mCursor.getString(1);
+
+ // Copy the artist name
+ final String artist = mCursor.getString(2);
+
+ // Copy the album name
+ final String album = mCursor.getString(3);
+
+ // Create a new song
+ final Song song = new Song(id, songName, artist, album, null);
+
+ // Add everything up
+ mSongList.add(song);
+ } while (mCursor.moveToNext());
+ }
+ // Close the cursor
+ if (mCursor != null) {
+ mCursor.close();
+ mCursor = null;
+ }
+ return mSongList;
+ }
+
+ /**
+ * @param context The {@link Context} to use.
+ * @param artistId The Id of the artist the songs belong to.
+ * @return The {@link Cursor} used to run the query.
+ */
+ public static final Cursor makeArtistSongCursor(final Context context, final Long artistId) {
+ // Match the songs up with the artist
+ final StringBuilder selection = new StringBuilder();
+ selection.append(AudioColumns.IS_MUSIC + "=1");
+ selection.append(" AND " + AudioColumns.TITLE + " != ''");
+ selection.append(" AND " + AudioColumns.ARTIST_ID + "=" + artistId);
+ return context.getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+ new String[] {
+ /* 0 */
+ BaseColumns._ID,
+ /* 1 */
+ AudioColumns.TITLE,
+ /* 2 */
+ AudioColumns.ARTIST,
+ /* 3 */
+ AudioColumns.ALBUM
+ }, selection.toString(), null,
+ PreferenceUtils.getInstace(context).getArtistSongSortOrder());
+ }
+
+}
diff --git a/src/com/andrew/apollo/loaders/AsyncHandler.java b/src/com/andrew/apollo/loaders/AsyncHandler.java
new file mode 100644
index 0000000..20723e1
--- /dev/null
+++ b/src/com/andrew/apollo/loaders/AsyncHandler.java
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+
+package com.andrew.apollo.loaders;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+
+/**
+ * Helper class for managing the background thread used to perform io operations
+ * and handle async broadcasts.
+ */
+public final class AsyncHandler {
+
+ private static final HandlerThread sHandlerThread = new HandlerThread("AsyncHandler");
+
+ private static final Handler sHandler;
+
+ static {
+ sHandlerThread.start();
+ sHandler = new Handler(sHandlerThread.getLooper());
+ }
+
+ /* This class is never initiated */
+ private AsyncHandler() {
+ }
+
+ /**
+ * @param r The {@link Runnable} to execute.
+ */
+ public static void post(final Runnable r) {
+ sHandler.post(r);
+ }
+
+}
diff --git a/src/com/andrew/apollo/loaders/FavoritesLoader.java b/src/com/andrew/apollo/loaders/FavoritesLoader.java
new file mode 100644
index 0000000..af8b654
--- /dev/null
+++ b/src/com/andrew/apollo/loaders/FavoritesLoader.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.loaders;
+
+import android.content.Context;
+import android.database.Cursor;
+
+import com.andrew.apollo.model.Song;
+import com.andrew.apollo.provider.FavoritesStore;
+import com.andrew.apollo.provider.FavoritesStore.FavoriteColumns;
+import com.andrew.apollo.utils.Lists;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Used to query the {@link FavoritesStore} for the tracks marked as favorites.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class FavoritesLoader extends WrappedAsyncTaskLoader<List<Song>> {
+
+ /**
+ * The result
+ */
+ private final ArrayList<Song> mSongList = Lists.newArrayList();
+
+ /**
+ * The {@link Cursor} used to run the query.
+ */
+ private Cursor mCursor;
+
+ /**
+ * Constructor of <code>FavoritesHandler</code>
+ *
+ * @param context The {@link Context} to use.
+ */
+ public FavoritesLoader(final Context context) {
+ super(context);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List<Song> loadInBackground() {
+ // Create the Cursor
+ mCursor = makeFavoritesCursor(getContext());
+ // Gather the data
+ if (mCursor != null && mCursor.moveToFirst()) {
+ do {
+
+ // Copy the song Id
+ final String id = mCursor.getString(mCursor
+ .getColumnIndexOrThrow(FavoriteColumns.ID));
+
+ // Copy the song name
+ final String songName = mCursor.getString(mCursor
+ .getColumnIndexOrThrow(FavoriteColumns.SONGNAME));
+
+ // Copy the artist name
+ final String artist = mCursor.getString(mCursor
+ .getColumnIndexOrThrow(FavoriteColumns.ARTISTNAME));
+
+ // Copy the album name
+ final String album = mCursor.getString(mCursor
+ .getColumnIndexOrThrow(FavoriteColumns.ALBUMNAME));
+
+ // Create a new song
+ final Song song = new Song(id, songName, artist, album, null);
+
+ // Add everything up
+ mSongList.add(song);
+ } while (mCursor.moveToNext());
+ }
+ // Close the cursor
+ if (mCursor != null) {
+ mCursor.close();
+ mCursor = null;
+ }
+ return mSongList;
+ }
+
+ /**
+ * @param context The {@link Context} to use.
+ * @return The {@link Cursor} used to run the favorites query.
+ */
+ public static final Cursor makeFavoritesCursor(final Context context) {
+ return FavoritesStore
+ .getInstance(context)
+ .getReadableDatabase()
+ .query(FavoriteColumns.NAME,
+ new String[] {
+ FavoriteColumns.ID + " as _id", FavoriteColumns.ID,
+ FavoriteColumns.SONGNAME, FavoriteColumns.ALBUMNAME,
+ FavoriteColumns.ARTISTNAME, FavoriteColumns.PLAYCOUNT
+ }, null, null, null, null, FavoriteColumns.PLAYCOUNT + " DESC");
+ }
+}
diff --git a/src/com/andrew/apollo/loaders/GenreLoader.java b/src/com/andrew/apollo/loaders/GenreLoader.java
new file mode 100644
index 0000000..46932ff
--- /dev/null
+++ b/src/com/andrew/apollo/loaders/GenreLoader.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.loaders;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.provider.BaseColumns;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Audio.GenresColumns;
+
+import com.andrew.apollo.model.Genre;
+import com.andrew.apollo.utils.Lists;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Used to query {@link MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI} and return
+ * the genres on a user's device.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class GenreLoader extends WrappedAsyncTaskLoader<List<Genre>> {
+
+ /**
+ * The result
+ */
+ private final ArrayList<Genre> mGenreList = Lists.newArrayList();
+
+ /**
+ * The {@link Cursor} used to run the query.
+ */
+ private Cursor mCursor;
+
+ /**
+ * Constructor of <code>GenreLoader</code>
+ *
+ * @param context The {@link Context} to use
+ */
+ public GenreLoader(final Context context) {
+ super(context);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List<Genre> loadInBackground() {
+ // Create the Cursor
+ mCursor = makeGenreCursor(getContext());
+ // Gather the data
+ if (mCursor != null && mCursor.moveToFirst()) {
+ do {
+ // Copy the genre id
+ final String id = mCursor.getString(0);
+
+ // Copy the genre name
+ final String name = mCursor.getString(1);
+
+ // Create a new genre
+ final Genre genre = new Genre(id, name);
+
+ // Add everything up
+ mGenreList.add(genre);
+ } while (mCursor.moveToNext());
+ }
+ // Close the cursor
+ if (mCursor != null) {
+ mCursor.close();
+ mCursor = null;
+ }
+ return mGenreList;
+ }
+
+ /**
+ * Creates the {@link Cursor} used to run the query.
+ *
+ * @param context The {@link Context} to use.
+ * @return The {@link Cursor} used to run the genre query.
+ */
+ public static final Cursor makeGenreCursor(final Context context) {
+ final StringBuilder selection = new StringBuilder();
+ selection.append(MediaStore.Audio.Genres.NAME + " != ''");
+ return context.getContentResolver().query(MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI,
+ new String[] {
+ /* 0 */
+ BaseColumns._ID,
+ /* 1 */
+ GenresColumns.NAME
+ }, selection.toString(), null, MediaStore.Audio.Genres.DEFAULT_SORT_ORDER);
+ }
+}
diff --git a/src/com/andrew/apollo/loaders/GenreSongLoader.java b/src/com/andrew/apollo/loaders/GenreSongLoader.java
new file mode 100644
index 0000000..0c5509a
--- /dev/null
+++ b/src/com/andrew/apollo/loaders/GenreSongLoader.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.loaders;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.provider.MediaStore;
+
+import com.andrew.apollo.model.Song;
+import com.andrew.apollo.utils.Lists;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Used to query {@link MediaStore.Audio.Genres.Members.EXTERNAL_CONTENT_URI}
+ * and return the songs for a particular genre.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class GenreSongLoader extends WrappedAsyncTaskLoader<List<Song>> {
+
+ /**
+ * The result
+ */
+ private final ArrayList<Song> mSongList = Lists.newArrayList();
+
+ /**
+ * The {@link Cursor} used to run the query.
+ */
+ private Cursor mCursor;
+
+ /**
+ * The Id of the genre the songs belong to.
+ */
+ private final Long mGenreID;
+
+ /**
+ * Constructor of <code>GenreSongHandler</code>
+ *
+ * @param context The {@link Context} to use.
+ * @param genreID The Id of the genre the songs belong to.
+ */
+ public GenreSongLoader(final Context context, final Long genreId) {
+ super(context);
+ mGenreID = genreId;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List<Song> loadInBackground() {
+ // Create the Cursor
+ mCursor = makeGenreSongCursor(getContext(), mGenreID);
+ // Gather the data
+ if (mCursor != null && mCursor.moveToFirst()) {
+ do {
+ // Copy the song Id
+ final String id = mCursor.getString(0);
+
+ // Copy the song name
+ final String songName = mCursor.getString(1);
+
+ // Copy the album name
+ final String album = mCursor.getString(2);
+
+ // Copy the artist name
+ final String artist = mCursor.getString(3);
+
+ // Create a new song
+ final Song song = new Song(id, songName, artist, album, null);
+
+ // Add everything up
+ mSongList.add(song);
+ } while (mCursor.moveToNext());
+ }
+ // Close the cursor
+ if (mCursor != null) {
+ mCursor.close();
+ mCursor = null;
+ }
+ return mSongList;
+ }
+
+ /**
+ * @param context The {@link Context} to use.
+ * @param genreId The Id of the genre the songs belong to.
+ * @return The {@link Cursor} used to run the query.
+ */
+ public static final Cursor makeGenreSongCursor(final Context context, final Long genreId) {
+ // Match the songs up with the genre
+ final StringBuilder selection = new StringBuilder();
+ selection.append(MediaStore.Audio.Genres.Members.IS_MUSIC + "=1");
+ selection.append(" AND " + MediaStore.Audio.Genres.Members.TITLE + "!=''"); //$NON-NLS-2$
+ return context.getContentResolver().query(
+ MediaStore.Audio.Genres.Members.getContentUri("external", genreId), new String[] {
+ /* 0 */
+ MediaStore.Audio.Genres.Members._ID,
+ /* 1 */
+ MediaStore.Audio.Genres.Members.TITLE,
+ /* 2 */
+ MediaStore.Audio.Genres.Members.ALBUM,
+ /* 3 */
+ MediaStore.Audio.Genres.Members.ARTIST
+ }, selection.toString(), null, MediaStore.Audio.Genres.Members.DEFAULT_SORT_ORDER);
+ }
+}
diff --git a/src/com/andrew/apollo/loaders/LastAddedLoader.java b/src/com/andrew/apollo/loaders/LastAddedLoader.java
new file mode 100644
index 0000000..51e42ae
--- /dev/null
+++ b/src/com/andrew/apollo/loaders/LastAddedLoader.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.loaders;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.provider.BaseColumns;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Audio.AudioColumns;
+
+import com.andrew.apollo.model.Song;
+import com.andrew.apollo.utils.Lists;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Used to query {@link MediaStore.Audio.Media.EXTERNAL_CONTENT_URI} and return
+ * the Song the user added over the past four of weeks.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class LastAddedLoader extends WrappedAsyncTaskLoader<List<Song>> {
+
+ /**
+ * The result
+ */
+ private final ArrayList<Song> mSongList = Lists.newArrayList();
+
+ /**
+ * The {@link Cursor} used to run the query.
+ */
+ private Cursor mCursor;
+
+ /**
+ * Constructor of <code>LastAddedHandler</code>
+ *
+ * @param context The {@link Context} to use.
+ */
+ public LastAddedLoader(final Context context) {
+ super(context);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List<Song> loadInBackground() {
+ // Create the Cursor
+ mCursor = makeLastAddedCursor(getContext());
+ // Gather the data
+ if (mCursor != null && mCursor.moveToFirst()) {
+ do {
+ // Copy the song Id
+ final String id = mCursor.getString(0);
+
+ // Copy the song name
+ final String songName = mCursor.getString(1);
+
+ // Copy the artist name
+ final String artist = mCursor.getString(2);
+
+ // Copy the album name
+ final String album = mCursor.getString(3);
+
+ // Create a new song
+ final Song song = new Song(id, songName, artist, album, null);
+
+ // Add everything up
+ mSongList.add(song);
+ } while (mCursor.moveToNext());
+ }
+ // Close the cursor
+ if (mCursor != null) {
+ mCursor.close();
+ mCursor = null;
+ }
+ return mSongList;
+ }
+
+ /**
+ * @param context The {@link Context} to use.
+ * @return The {@link Cursor} used to run the song query.
+ */
+ public static final Cursor makeLastAddedCursor(final Context context) {
+ final int fourWeeks = 4 * 3600 * 24 * 7;
+ final StringBuilder selection = new StringBuilder();
+ selection.append(AudioColumns.IS_MUSIC + "=1");
+ selection.append(" AND " + AudioColumns.TITLE + " != ''"); //$NON-NLS-2$
+ selection.append(" AND " + MediaStore.Audio.Media.DATE_ADDED + ">"); //$NON-NLS-2$
+ selection.append(System.currentTimeMillis() / 1000 - fourWeeks);
+ return context.getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+ new String[] {
+ /* 0 */
+ BaseColumns._ID,
+ /* 1 */
+ AudioColumns.TITLE,
+ /* 2 */
+ AudioColumns.ARTIST,
+ /* 3 */
+ AudioColumns.ALBUM
+ }, selection.toString(), null, MediaStore.Audio.Media.DATE_ADDED + " DESC");
+ }
+}
diff --git a/src/com/andrew/apollo/loaders/NowPlayingCursor.java b/src/com/andrew/apollo/loaders/NowPlayingCursor.java
new file mode 100644
index 0000000..4deb2f0
--- /dev/null
+++ b/src/com/andrew/apollo/loaders/NowPlayingCursor.java
@@ -0,0 +1,292 @@
+
+package com.andrew.apollo.loaders;
+
+import static com.andrew.apollo.utils.MusicUtils.mService;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.database.AbstractCursor;
+import android.database.Cursor;
+import android.os.RemoteException;
+import android.provider.BaseColumns;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Audio.AudioColumns;
+
+import com.andrew.apollo.utils.ApolloUtils;
+import com.andrew.apollo.utils.MusicUtils;
+
+import java.util.Arrays;
+
+/**
+ * A custom {@link Cursor} used to return the queue and allow for easy dragging
+ * and dropping of the items in it.
+ */
+@SuppressLint("NewApi")
+public class NowPlayingCursor extends AbstractCursor {
+
+ private static final String[] PROJECTION = new String[] {
+ /* 0 */
+ BaseColumns._ID,
+ /* 1 */
+ AudioColumns.TITLE,
+ /* 2 */
+ AudioColumns.ARTIST,
+ /* 3 */
+ AudioColumns.ALBUM
+ };
+
+ private final Context mContext;
+
+ private long[] mNowPlaying;
+
+ private long[] mCursorIndexes;
+
+ private int mSize;
+
+ private int mCurPos;
+
+ private Cursor mQueueCursor;
+
+ /**
+ * Constructor of <code>NowPlayingCursor</code>
+ *
+ * @param context The {@link Context} to use
+ */
+ public NowPlayingCursor(final Context context) {
+ mContext = context;
+ makeNowPlayingCursor();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getCount() {
+ return mSize;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onMove(final int oldPosition, final int newPosition) {
+ if (oldPosition == newPosition) {
+ return true;
+ }
+
+ if (mNowPlaying == null || mCursorIndexes == null || newPosition >= mNowPlaying.length) {
+ return false;
+ }
+
+ final long id = mNowPlaying[newPosition];
+ final int cursorIndex = Arrays.binarySearch(mCursorIndexes, id);
+ mQueueCursor.moveToPosition(cursorIndex);
+ mCurPos = newPosition;
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getString(final int column) {
+ try {
+ return mQueueCursor.getString(column);
+ } catch (final Exception ignored) {
+ onChange(true);
+ return "";
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public short getShort(final int column) {
+ return mQueueCursor.getShort(column);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getInt(final int column) {
+ try {
+ return mQueueCursor.getInt(column);
+ } catch (final Exception ignored) {
+ onChange(true);
+ return 0;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public long getLong(final int column) {
+ try {
+ return mQueueCursor.getLong(column);
+ } catch (final Exception ignored) {
+ onChange(true);
+ return 0;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public float getFloat(final int column) {
+ return mQueueCursor.getFloat(column);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public double getDouble(final int column) {
+ return mQueueCursor.getDouble(column);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getType(final int column) {
+ if (ApolloUtils.hasHoneycomb()) {
+ return mQueueCursor.getType(column);
+ }
+ return 0;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean isNull(final int column) {
+ return mQueueCursor.isNull(column);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String[] getColumnNames() {
+ return PROJECTION;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @SuppressWarnings("deprecation")
+ @Override
+ public void deactivate() {
+ if (mQueueCursor != null) {
+ mQueueCursor.deactivate();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean requery() {
+ makeNowPlayingCursor();
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void close() {
+ try {
+ if (mQueueCursor != null) {
+ mQueueCursor.close();
+ mQueueCursor = null;
+ }
+ } catch (final Exception close) {
+ }
+ super.close();
+ };
+
+ /**
+ * Actually makes the queue
+ */
+ private void makeNowPlayingCursor() {
+ mQueueCursor = null;
+ mNowPlaying = MusicUtils.getQueue();
+ mSize = mNowPlaying.length;
+ if (mSize == 0) {
+ return;
+ }
+
+ final StringBuilder selection = new StringBuilder();
+ selection.append(MediaStore.Audio.Media._ID + " IN (");
+ for (int i = 0; i < mSize; i++) {
+ selection.append(mNowPlaying[i]);
+ if (i < mSize - 1) {
+ selection.append(",");
+ }
+ }
+ selection.append(")");
+
+ mQueueCursor = mContext.getContentResolver().query(
+ MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, PROJECTION, selection.toString(),
+ null, MediaStore.Audio.Media._ID);
+
+ if (mQueueCursor == null) {
+ mSize = 0;
+ return;
+ }
+
+ final int playlistSize = mQueueCursor.getCount();
+ mCursorIndexes = new long[playlistSize];
+ mQueueCursor.moveToFirst();
+ final int columnIndex = mQueueCursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID);
+ for (int i = 0; i < playlistSize; i++) {
+ mCursorIndexes[i] = mQueueCursor.getLong(columnIndex);
+ mQueueCursor.moveToNext();
+ }
+ mQueueCursor.moveToFirst();
+ mCurPos = -1;
+
+ int removed = 0;
+ for (int i = mNowPlaying.length - 1; i >= 0; i--) {
+ final long trackId = mNowPlaying[i];
+ final int cursorIndex = Arrays.binarySearch(mCursorIndexes, trackId);
+ if (cursorIndex < 0) {
+ removed += MusicUtils.removeTrack(trackId);
+ }
+ }
+ if (removed > 0) {
+ mNowPlaying = MusicUtils.getQueue();
+ mSize = mNowPlaying.length;
+ if (mSize == 0) {
+ mCursorIndexes = null;
+ return;
+ }
+ }
+ }
+
+ /**
+ * @param which The position to remove
+ * @return True if sucessfull, false othersise
+ */
+ public boolean removeItem(final int which) {
+ try {
+ if (mService.removeTracks(which, which) == 0) {
+ return false;
+ }
+ int i = which;
+ mSize--;
+ while (i < mSize) {
+ mNowPlaying[i] = mNowPlaying[i + 1];
+ i++;
+ }
+ onMove(-1, mCurPos);
+ } catch (final RemoteException ignored) {
+ }
+ return true;
+ }
+}
diff --git a/src/com/andrew/apollo/loaders/PlaylistLoader.java b/src/com/andrew/apollo/loaders/PlaylistLoader.java
new file mode 100644
index 0000000..c147706
--- /dev/null
+++ b/src/com/andrew/apollo/loaders/PlaylistLoader.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.loaders;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.provider.BaseColumns;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Audio.PlaylistsColumns;
+
+import com.andrew.apollo.R;
+import com.andrew.apollo.model.Playlist;
+import com.andrew.apollo.utils.Lists;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Used to query {@link MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI} and
+ * return the playlists on a user's device.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class PlaylistLoader extends WrappedAsyncTaskLoader<List<Playlist>> {
+
+ /**
+ * The result
+ */
+ private final ArrayList<Playlist> mPlaylistList = Lists.newArrayList();
+
+ /**
+ * The {@link Cursor} used to run the query.
+ */
+ private Cursor mCursor;
+
+ /**
+ * Constructor of <code>PlaylistLoader</code>
+ *
+ * @param context The {@link Context} to use
+ */
+ public PlaylistLoader(final Context context) {
+ super(context);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List<Playlist> loadInBackground() {
+ // Add the deafult playlits to the adapter
+ makeDefaultPlaylists();
+
+ // Create the Cursor
+ mCursor = makePlaylistCursor(getContext());
+ // Gather the data
+ if (mCursor != null && mCursor.moveToFirst()) {
+ do {
+ // Copy the playlist id
+ final String id = mCursor.getString(0);
+
+ // Copy the playlist name
+ final String name = mCursor.getString(1);
+
+ // Create a new playlist
+ final Playlist playlist = new Playlist(id, name);
+
+ // Add everything up
+ mPlaylistList.add(playlist);
+ } while (mCursor.moveToNext());
+ }
+ // Close the cursor
+ if (mCursor != null) {
+ mCursor.close();
+ mCursor = null;
+ }
+ return mPlaylistList;
+ }
+
+ /* Adds the favorites and last added playlists */
+ private void makeDefaultPlaylists() {
+ final Resources resources = getContext().getResources();
+
+ /* Favorites list */
+ final Playlist favorites = new Playlist("-1",
+ resources.getString(R.string.playlist_favorites));
+ mPlaylistList.add(favorites);
+
+ /* Last added list */
+ final Playlist lastAdded = new Playlist("-2",
+ resources.getString(R.string.playlist_last_added));
+ mPlaylistList.add(lastAdded);
+ }
+
+ /**
+ * Creates the {@link Cursor} used to run the query.
+ *
+ * @param context The {@link Context} to use.
+ * @return The {@link Cursor} used to run the playlist query.
+ */
+ public static final Cursor makePlaylistCursor(final Context context) {
+ return context.getContentResolver().query(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
+ new String[] {
+ /* 0 */
+ BaseColumns._ID,
+ /* 1 */
+ PlaylistsColumns.NAME
+ }, null, null, MediaStore.Audio.Playlists.DEFAULT_SORT_ORDER);
+ }
+}
diff --git a/src/com/andrew/apollo/loaders/PlaylistSongLoader.java b/src/com/andrew/apollo/loaders/PlaylistSongLoader.java
new file mode 100644
index 0000000..aedb5fc
--- /dev/null
+++ b/src/com/andrew/apollo/loaders/PlaylistSongLoader.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.loaders;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Audio.AudioColumns;
+
+import com.andrew.apollo.model.Song;
+import com.andrew.apollo.utils.Lists;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Used to query {@link MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI} and
+ * return the songs for a particular playlist.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class PlaylistSongLoader extends WrappedAsyncTaskLoader<List<Song>> {
+
+ /**
+ * The result
+ */
+ private final ArrayList<Song> mSongList = Lists.newArrayList();
+
+ /**
+ * The {@link Cursor} used to run the query.
+ */
+ private Cursor mCursor;
+
+ /**
+ * The Id of the playlist the songs belong to.
+ */
+ private final Long mPlaylistID;
+
+ /**
+ * Constructor of <code>SongLoader</code>
+ *
+ * @param context The {@link Context} to use
+ * @param playlistID The Id of the playlist the songs belong to.
+ */
+ public PlaylistSongLoader(final Context context, final Long playlistId) {
+ super(context);
+ mPlaylistID = playlistId;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List<Song> loadInBackground() {
+ // Create the Cursor
+ mCursor = makePlaylistSongCursor(getContext(), mPlaylistID);
+ // Gather the data
+ if (mCursor != null && mCursor.moveToFirst()) {
+ do {
+ // Copy the song Id
+ final String id = mCursor.getString(mCursor
+ .getColumnIndexOrThrow(MediaStore.Audio.Playlists.Members.AUDIO_ID));
+
+ // Copy the song name
+ final String songName = mCursor.getString(mCursor
+ .getColumnIndexOrThrow(AudioColumns.TITLE));
+
+ // Copy the artist name
+ final String artist = mCursor.getString(mCursor
+ .getColumnIndexOrThrow(AudioColumns.ARTIST));
+
+ // Copy the album name
+ final String album = mCursor.getString(mCursor
+ .getColumnIndexOrThrow(AudioColumns.ALBUM));
+
+ // Create a new song
+ final Song song = new Song(id, songName, artist, album, null);
+
+ // Add everything up
+ mSongList.add(song);
+ } while (mCursor.moveToNext());
+ }
+ // Close the cursor
+ if (mCursor != null) {
+ mCursor.close();
+ mCursor = null;
+ }
+ return mSongList;
+ }
+
+ /**
+ * Creates the {@link Cursor} used to run the query.
+ *
+ * @param context The {@link Context} to use.
+ * @param playlistID The playlist the songs belong to.
+ * @return The {@link Cursor} used to run the song query.
+ */
+ public static final Cursor makePlaylistSongCursor(final Context context, final Long playlistID) {
+ final StringBuilder mSelection = new StringBuilder();
+ mSelection.append(AudioColumns.IS_MUSIC + "=1");
+ mSelection.append(" AND " + AudioColumns.TITLE + " != ''"); //$NON-NLS-2$
+ return context.getContentResolver().query(
+ MediaStore.Audio.Playlists.Members.getContentUri("external", playlistID),
+ new String[] {
+ /* 0 */
+ MediaStore.Audio.Playlists.Members._ID,
+ /* 1 */
+ MediaStore.Audio.Playlists.Members.AUDIO_ID,
+ /* 2 */
+ AudioColumns.TITLE,
+ /* 3 */
+ AudioColumns.ARTIST,
+ /* 4 */
+ AudioColumns.ALBUM
+ }, mSelection.toString(), null,
+ MediaStore.Audio.Playlists.Members.DEFAULT_SORT_ORDER);
+ }
+}
diff --git a/src/com/andrew/apollo/loaders/QueueLoader.java b/src/com/andrew/apollo/loaders/QueueLoader.java
new file mode 100644
index 0000000..0e073dd
--- /dev/null
+++ b/src/com/andrew/apollo/loaders/QueueLoader.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.loaders;
+
+import android.content.Context;
+import android.database.Cursor;
+
+import com.andrew.apollo.model.Song;
+import com.andrew.apollo.utils.Lists;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Used to return the current playlist or queue.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class QueueLoader extends WrappedAsyncTaskLoader<List<Song>> {
+
+ /**
+ * The result
+ */
+ private final ArrayList<Song> mSongList = Lists.newArrayList();
+
+ /**
+ * The {@link Cursor} used to run the query.
+ */
+ private NowPlayingCursor mCursor;
+
+ /**
+ * Constructor of <code>QueueLoader</code>
+ *
+ * @param context The {@link Context} to use
+ */
+ public QueueLoader(final Context context) {
+ super(context);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List<Song> loadInBackground() {
+ // Create the Cursor
+ mCursor = new NowPlayingCursor(getContext());
+ // Gather the data
+ if (mCursor != null && mCursor.moveToFirst()) {
+ do {
+ // Copy the song Id
+ final String id = mCursor.getString(0);
+
+ // Copy the song name
+ final String songName = mCursor.getString(1);
+
+ // Copy the artist name
+ final String artist = mCursor.getString(2);
+
+ // Copy the album name
+ final String album = mCursor.getString(3);
+
+ // Create a new song
+ final Song song = new Song(id, songName, artist, album, null);
+
+ // Add everything up
+ mSongList.add(song);
+ } while (mCursor.moveToNext());
+ }
+ // Close the cursor
+ if (mCursor != null) {
+ mCursor.close();
+ mCursor = null;
+ }
+ return mSongList;
+ }
+
+ /**
+ * Creates the {@link Cursor} used to run the query.
+ *
+ * @param context The {@link Context} to use.
+ * @return The {@link Cursor} used to run the song query.
+ */
+ public static final Cursor makeQueueCursor(final Context context) {
+ final Cursor cursor = new NowPlayingCursor(context);
+ return cursor;
+ }
+}
diff --git a/src/com/andrew/apollo/loaders/RecentLoader.java b/src/com/andrew/apollo/loaders/RecentLoader.java
new file mode 100644
index 0000000..00c4bdc
--- /dev/null
+++ b/src/com/andrew/apollo/loaders/RecentLoader.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.loaders;
+
+import android.content.Context;
+import android.database.Cursor;
+
+import com.andrew.apollo.R;
+import com.andrew.apollo.model.Album;
+import com.andrew.apollo.provider.RecentStore;
+import com.andrew.apollo.provider.RecentStore.RecentStoreColumns;
+import com.andrew.apollo.utils.Lists;
+import com.andrew.apollo.utils.MusicUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Used to query {@link RecentStore} and return the last listened to albums.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class RecentLoader extends WrappedAsyncTaskLoader<List<Album>> {
+
+ /**
+ * The result
+ */
+ private final ArrayList<Album> mAlbumsList = Lists.newArrayList();
+
+ /**
+ * The {@link Cursor} used to run the query.
+ */
+ private Cursor mCursor;
+
+ /**
+ * Constructor of <code>RecentLoader</code>
+ *
+ * @param context The {@link Context} to use
+ */
+ public RecentLoader(final Context context) {
+ super(context);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List<Album> loadInBackground() {
+ // Create the Cursor
+ mCursor = makeRecentCursor(getContext());
+ // Gather the data
+ if (mCursor != null && mCursor.moveToFirst()) {
+ do {
+ // Copy the album id
+ final String id = mCursor.getString(mCursor
+ .getColumnIndexOrThrow(RecentStoreColumns.ID));
+
+ // Copy the album name
+ final String albumName = mCursor.getString(mCursor
+ .getColumnIndexOrThrow(RecentStoreColumns.ALBUMNAME));
+
+ // Copy the artist name
+ final String artist = mCursor.getString(mCursor
+ .getColumnIndexOrThrow(RecentStoreColumns.ARTISTNAME));
+
+ // Copy the number of songs
+ final String songCount = mCursor.getString(mCursor
+ .getColumnIndexOrThrow(RecentStoreColumns.ALBUMSONGCOUNT));
+
+ // Copy the release year
+ final String year = mCursor.getString(mCursor
+ .getColumnIndexOrThrow(RecentStoreColumns.ALBUMYEAR));
+
+ // Make the song lable
+ final String songCountFormatted = MusicUtils.makeLabel(getContext(),
+ R.plurals.Nsongs, songCount);
+
+ // Create a new album
+ final Album album = new Album(id, albumName, artist, songCountFormatted, year);
+
+ // Add everything up
+ mAlbumsList.add(album);
+ } while (mCursor.moveToNext());
+ }
+ // Close the cursor
+ if (mCursor != null) {
+ mCursor.close();
+ mCursor = null;
+ }
+ return mAlbumsList;
+ }
+
+ /**
+ * Creates the {@link Cursor} used to run the query.
+ *
+ * @param context The {@link Context} to use.
+ * @return The {@link Cursor} used to run the album query.
+ */
+ public static final Cursor makeRecentCursor(final Context context) {
+ return RecentStore
+ .getInstance(context)
+ .getReadableDatabase()
+ .query(RecentStoreColumns.NAME,
+ new String[] {
+ RecentStoreColumns.ID + " as id", RecentStoreColumns.ID,
+ RecentStoreColumns.ALBUMNAME, RecentStoreColumns.ARTISTNAME,
+ RecentStoreColumns.ALBUMSONGCOUNT, RecentStoreColumns.ALBUMYEAR,
+ RecentStoreColumns.TIMEPLAYED
+ }, null, null, null, null, RecentStoreColumns.TIMEPLAYED + " DESC");
+ }
+}
diff --git a/src/com/andrew/apollo/loaders/SearchLoader.java b/src/com/andrew/apollo/loaders/SearchLoader.java
new file mode 100644
index 0000000..43d06ca
--- /dev/null
+++ b/src/com/andrew/apollo/loaders/SearchLoader.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.loaders;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.BaseColumns;
+import android.provider.MediaStore;
+import android.text.TextUtils;
+
+import com.andrew.apollo.model.Song;
+import com.andrew.apollo.utils.Lists;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class SearchLoader extends WrappedAsyncTaskLoader<List<Song>> {
+
+ /**
+ * The result
+ */
+ private final ArrayList<Song> mSongList = Lists.newArrayList();
+
+ /**
+ * The {@link Cursor} used to run the query.
+ */
+ private Cursor mCursor;
+
+ /**
+ * Constructor of <code>SongLoader</code>
+ *
+ * @param context The {@link Context} to use
+ * @param query The search query
+ */
+ public SearchLoader(final Context context, final String query) {
+ super(context);
+ // Create the Cursor
+ mCursor = makeSearchCursor(context, query);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List<Song> loadInBackground() {
+ // Gather the data
+ if (mCursor != null && mCursor.moveToFirst()) {
+ do {
+ // Copy the song Id
+ String id = null;
+
+ // Copy the song name
+ final String songName = mCursor.getString(mCursor
+ .getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE));
+
+ // Check for a song Id
+ if (!TextUtils.isEmpty(songName)) {
+ id = mCursor.getString(mCursor
+ .getColumnIndexOrThrow(MediaStore.Audio.Media._ID));
+ }
+
+ // Copy the album name
+ final String album = mCursor.getString(mCursor
+ .getColumnIndexOrThrow(MediaStore.Audio.Albums.ALBUM));
+
+ // Check for a album Id
+ if (TextUtils.isEmpty(id) && !TextUtils.isEmpty(album)) {
+ id = mCursor.getString(mCursor
+ .getColumnIndexOrThrow(MediaStore.Audio.Albums._ID));
+ }
+
+ // Copy the artist name
+ final String artist = mCursor.getString(mCursor
+ .getColumnIndexOrThrow(MediaStore.Audio.Artists.ARTIST));
+
+ // Check for a artist Id
+ if (TextUtils.isEmpty(id) && !TextUtils.isEmpty(artist)) {
+ id = mCursor.getString(mCursor
+ .getColumnIndexOrThrow(MediaStore.Audio.Artists._ID));
+ }
+
+ // Create a new song
+ final Song song = new Song(id, songName, artist, album, null);
+
+ // Add everything up
+ mSongList.add(song);
+ } while (mCursor.moveToNext());
+ }
+ // Close the cursor
+ if (mCursor != null) {
+ mCursor.close();
+ mCursor = null;
+ }
+ return mSongList;
+ }
+
+ /**
+ * * @param context The {@link Context} to use.
+ *
+ * @param query The user's query.
+ * @return The {@link Cursor} used to perform the search.
+ */
+ public static final Cursor makeSearchCursor(final Context context, final String query) {
+ return context.getContentResolver().query(
+ Uri.parse("content://media/external/audio/search/fancy/" + Uri.encode(query)),
+ new String[] {
+ BaseColumns._ID, MediaStore.Audio.Media.MIME_TYPE,
+ MediaStore.Audio.Artists.ARTIST, MediaStore.Audio.Albums.ALBUM,
+ MediaStore.Audio.Media.TITLE, "data1", "data2" //$NON-NLS-2$
+ }, null, null, null);
+ }
+
+}
diff --git a/src/com/andrew/apollo/loaders/SongLoader.java b/src/com/andrew/apollo/loaders/SongLoader.java
new file mode 100644
index 0000000..ab4e3eb
--- /dev/null
+++ b/src/com/andrew/apollo/loaders/SongLoader.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.loaders;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.provider.BaseColumns;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Audio.AudioColumns;
+
+import com.andrew.apollo.model.Song;
+import com.andrew.apollo.utils.Lists;
+import com.andrew.apollo.utils.PreferenceUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Used to query {@link MediaStore.Audio.Media.EXTERNAL_CONTENT_URI} and return
+ * the songs on a user's device.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class SongLoader extends WrappedAsyncTaskLoader<List<Song>> {
+
+ /**
+ * The result
+ */
+ private final ArrayList<Song> mSongList = Lists.newArrayList();
+
+ /**
+ * The {@link Cursor} used to run the query.
+ */
+ private Cursor mCursor;
+
+ /**
+ * Constructor of <code>SongLoader</code>
+ *
+ * @param context The {@link Context} to use
+ */
+ public SongLoader(final Context context) {
+ super(context);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List<Song> loadInBackground() {
+ // Create the Cursor
+ mCursor = makeSongCursor(getContext());
+ // Gather the data
+ if (mCursor != null && mCursor.moveToFirst()) {
+ do {
+ // Copy the song Id
+ final String id = mCursor.getString(0);
+
+ // Copy the song name
+ final String songName = mCursor.getString(1);
+
+ // Copy the artist name
+ final String artist = mCursor.getString(2);
+
+ // Copy the album name
+ final String album = mCursor.getString(3);
+
+ // Create a new song
+ final Song song = new Song(id, songName, artist, album, null);
+
+ // Add everything up
+ mSongList.add(song);
+ } while (mCursor.moveToNext());
+ }
+ // Close the cursor
+ if (mCursor != null) {
+ mCursor.close();
+ mCursor = null;
+ }
+ return mSongList;
+ }
+
+ /**
+ * Creates the {@link Cursor} used to run the query.
+ *
+ * @param context The {@link Context} to use.
+ * @return The {@link Cursor} used to run the song query.
+ */
+ public static final Cursor makeSongCursor(final Context context) {
+ final StringBuilder mSelection = new StringBuilder();
+ mSelection.append(AudioColumns.IS_MUSIC + "=1");
+ mSelection.append(" AND " + AudioColumns.TITLE + " != ''"); //$NON-NLS-2$
+ return context.getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+ new String[] {
+ /* 0 */
+ BaseColumns._ID,
+ /* 1 */
+ AudioColumns.TITLE,
+ /* 2 */
+ AudioColumns.ARTIST,
+ /* 3 */
+ AudioColumns.ALBUM
+ }, mSelection.toString(), null,
+ PreferenceUtils.getInstace(context).getSongSortOrder());
+ }
+}
diff --git a/src/com/andrew/apollo/loaders/WrappedAsyncTaskLoader.java b/src/com/andrew/apollo/loaders/WrappedAsyncTaskLoader.java
new file mode 100644
index 0000000..5496b5f
--- /dev/null
+++ b/src/com/andrew/apollo/loaders/WrappedAsyncTaskLoader.java
@@ -0,0 +1,70 @@
+
+package com.andrew.apollo.loaders;
+
+import android.content.Context;
+import android.support.v4.content.AsyncTaskLoader;
+
+/**
+ * <a href="http://code.google.com/p/android/issues/detail?id=14944">Issue
+ * 14944</a>
+ *
+ * @author Alexander Blom
+ */
+public abstract class WrappedAsyncTaskLoader<D> extends AsyncTaskLoader<D> {
+
+ private D mData;
+
+ /**
+ * Constructor of <code>WrappedAsyncTaskLoader</code>
+ *
+ * @param context The {@link Context} to use.
+ */
+ public WrappedAsyncTaskLoader(Context context) {
+ super(context);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void deliverResult(D data) {
+ if (!isReset()) {
+ this.mData = data;
+ super.deliverResult(data);
+ } else {
+ // An asynchronous query came in while the loader is stopped
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onStartLoading() {
+ if (this.mData != null) {
+ deliverResult(this.mData);
+ } else if (takeContentChanged() || this.mData == null) {
+ forceLoad();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onStopLoading() {
+ // Attempt to cancel the current load task if possible
+ cancelLoad();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onReset() {
+ super.onReset();
+ // Ensure the loader is stopped
+ onStopLoading();
+ this.mData = null;
+ }
+}
diff --git a/src/com/andrew/apollo/lyrics/LyricsProvider.java b/src/com/andrew/apollo/lyrics/LyricsProvider.java
new file mode 100644
index 0000000..0ca57be
--- /dev/null
+++ b/src/com/andrew/apollo/lyrics/LyricsProvider.java
@@ -0,0 +1,22 @@
+
+package com.andrew.apollo.lyrics;
+
+public interface LyricsProvider {
+
+ /**
+ * Gives the lyrics of the song, or null if they werent found
+ *
+ * @param artist Artist name
+ * @param song Song name
+ * @return Full lyrics as a {@link String}
+ */
+ public String getLyrics(String artist, String song);
+
+ /**
+ * Gives the name of the provider implementation
+ *
+ * @return The name of the lyrics provider
+ */
+ public String getProviderName();
+
+}
diff --git a/src/com/andrew/apollo/lyrics/LyricsProviderFactory.java b/src/com/andrew/apollo/lyrics/LyricsProviderFactory.java
new file mode 100644
index 0000000..2aff457
--- /dev/null
+++ b/src/com/andrew/apollo/lyrics/LyricsProviderFactory.java
@@ -0,0 +1,27 @@
+
+package com.andrew.apollo.lyrics;
+
+public final class LyricsProviderFactory {
+
+ /* This class is never initiated. */
+ public LyricsProviderFactory() {
+ }
+
+ /**
+ * @param filePath The path to save the lyrics.
+ * @return A new instance of {@link OfflineLyricsProvider}.
+ */
+ public static final LyricsProvider getOfflineProvider(String filePath) {
+ return new OfflineLyricsProvider(filePath);
+ }
+
+ /**
+ * @return The current lyrics provider.
+ */
+ public static final LyricsProvider getMainOnlineProvider() {
+ return new LyricsWikiProvider();
+ }
+
+ // TODO Implement more providers, and also a system to iterate over them
+
+}
diff --git a/src/com/andrew/apollo/lyrics/LyricsWikiProvider.java b/src/com/andrew/apollo/lyrics/LyricsWikiProvider.java
new file mode 100644
index 0000000..c894252
--- /dev/null
+++ b/src/com/andrew/apollo/lyrics/LyricsWikiProvider.java
@@ -0,0 +1,116 @@
+
+package com.andrew.apollo.lyrics;
+
+import android.util.Log;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+public class LyricsWikiProvider implements LyricsProvider {
+
+ // URL used to fetch the lyrics
+ private static final String LYRICS_URL = "http://lyrics.wikia.com/api.php?action=lyrics&fmt=json&func=getSong&artist=%1s&song=%1s";
+
+ // Currently, the only lyrics provider
+ public static final String PROVIDER_NAME = "LyricsWiki";
+
+ // Timeout duration
+ private static final int DEFAULT_HTTP_TIME = 15 * 1000;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getLyrics(String artist, String song) {
+ if (artist == null || song == null) {
+ return null;
+ }
+ String ret = null;
+ artist = artist.replace(" ", "%20");
+ song = song.replace(" ", "%20");
+ try {
+ // Get the lyrics URL
+ URL url = new URL(String.format(LYRICS_URL, artist, song));
+ final String urlString = getUrlAsString(url);
+ final String songURL = new JSONObject(urlString.replace("song = ", ""))
+ .getString("url");
+ if (songURL.endsWith("action=edit")) {
+ return null;
+ }
+
+ // And now get the full lyrics
+ url = new URL(songURL);
+ String html = getUrlAsString(url);
+ // TODO Clean this up
+ html = html.substring(html.indexOf("<div class='lyricbox'>"));
+ html = html.substring(html.indexOf("</div>") + 6);
+ html = html.substring(0, html.indexOf("<!--"));
+ // Replace new line html with characters
+ html = html.replace("<br />", "\n;");
+ // Now parse the html entities
+ final String[] htmlChars = html.split(";");
+ final StringBuilder builder = new StringBuilder();
+ String code = null;
+ char caracter;
+ for (final String s : htmlChars) {
+ if (s.equals("\n")) {
+ builder.append(s);
+ } else {
+ code = s.replaceAll("&#", "");
+ caracter = (char)Integer.valueOf(code).intValue();
+ builder.append(caracter);
+ }
+ }
+ // And that's it
+ ret = builder.toString();
+ } catch (final MalformedURLException e) {
+ Log.e("Apollo", "Lyrics not found in " + getProviderName(), e);
+ } catch (final IOException e) {
+ Log.e("Apollo", "Lyrics not found in " + getProviderName(), e);
+ } catch (final JSONException e) {
+ Log.e("Apollo", "Lyrics not found in " + getProviderName(), e);
+ } catch (final NumberFormatException e) {
+ Log.e("Apollo", "Lyrics not found in " + getProviderName(), e);
+ }
+ return ret;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getProviderName() {
+ return PROVIDER_NAME;
+ }
+
+ /**
+ * @param url The {@link URL} to fecth the lyrics from
+ * @return The {@link URL} used to fetch the lyrics as a {@link String}
+ * @throws IOException
+ */
+ public static final String getUrlAsString(final URL url) throws IOException {
+ // Perform a GET request for the lyrics
+ final HttpURLConnection httpURLConnection = (HttpURLConnection)url.openConnection();
+ httpURLConnection.setRequestMethod("GET");
+ httpURLConnection.setReadTimeout(DEFAULT_HTTP_TIME);
+ httpURLConnection.setUseCaches(false);
+ httpURLConnection.connect();
+ final InputStreamReader input = new InputStreamReader(httpURLConnection.getInputStream());
+ // Read the server output
+ final BufferedReader reader = new BufferedReader(input);
+ // Build the URL
+ final StringBuilder builder = new StringBuilder();
+ String line = null;
+ while ((line = reader.readLine()) != null) {
+ builder.append(line + "\n");
+ }
+ return builder.toString();
+ }
+}
diff --git a/src/com/andrew/apollo/lyrics/OfflineLyricsProvider.java b/src/com/andrew/apollo/lyrics/OfflineLyricsProvider.java
new file mode 100644
index 0000000..dac8738
--- /dev/null
+++ b/src/com/andrew/apollo/lyrics/OfflineLyricsProvider.java
@@ -0,0 +1,138 @@
+
+package com.andrew.apollo.lyrics;
+
+import org.jaudiotagger.audio.AudioFile;
+import org.jaudiotagger.audio.AudioFileIO;
+import org.jaudiotagger.audio.exceptions.CannotReadException;
+import org.jaudiotagger.audio.exceptions.CannotWriteException;
+import org.jaudiotagger.audio.exceptions.InvalidAudioFrameException;
+import org.jaudiotagger.audio.exceptions.ReadOnlyFileException;
+import org.jaudiotagger.tag.FieldKey;
+import org.jaudiotagger.tag.Tag;
+import org.jaudiotagger.tag.TagException;
+
+import java.io.File;
+import java.io.IOException;
+
+public class OfflineLyricsProvider implements LyricsProvider {
+
+ private File mAudioFile;
+
+ /**
+ * Constructor of <code>OfflineLyricsProvider</code>
+ *
+ * @param filePath The path to save the lyrics
+ */
+ public OfflineLyricsProvider(final String filePath) {
+ setTrackFile(filePath);
+ }
+
+ /**
+ * @param path The file to save our {@link File}
+ */
+ public void setTrackFile(final String path) {
+ if (path == null) {
+ return;
+ }
+ mAudioFile = new File(path);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getLyrics(final String artist, final String song) {
+ String lyrics = null;
+ try {
+ if (mAudioFile == null) {
+ return null;
+ }
+ if (mAudioFile.exists()) {
+ // Use jAudioTagger library to get the file's lyrics
+ final AudioFile file = AudioFileIO.read(mAudioFile);
+ final Tag tag = file.getTag();
+ lyrics = tag.getFirst(FieldKey.LYRICS);
+ }
+ } catch (final ReadOnlyFileException e) {
+ e.printStackTrace();
+ } catch (final CannotReadException e) {
+ e.printStackTrace();
+ } catch (final IOException e) {
+ e.printStackTrace();
+ } catch (final TagException e) {
+ e.printStackTrace();
+ } catch (final InvalidAudioFrameException e) {
+ e.printStackTrace();
+ } catch (final UnsupportedOperationException e) {
+ e.printStackTrace();
+ }
+ return lyrics;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getProviderName() {
+ return "File metadata";
+ }
+
+ /**
+ * @param lyrics The lyrics to save
+ * @param filePath The path to save them
+ */
+ public static void saveLyrics(final String lyrics, final String filePath) {
+ try {
+ final File file = new File(filePath);
+ if (file.exists()) {
+ // Use jAudioTagger library to set the file's lyrics
+ final AudioFile audioFile = AudioFileIO.read(file);
+ final Tag tag = audioFile.getTag();
+ tag.setField(FieldKey.LYRICS, lyrics);
+ audioFile.commit();
+ }
+ } catch (final ReadOnlyFileException e) {
+ e.printStackTrace();
+ } catch (final CannotReadException e) {
+ e.printStackTrace();
+ } catch (final IOException e) {
+ e.printStackTrace();
+ } catch (final TagException e) {
+ e.printStackTrace();
+ } catch (final InvalidAudioFrameException e) {
+ e.printStackTrace();
+ } catch (final CannotWriteException e) {
+ e.printStackTrace();
+ } catch (final NullPointerException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * @param filePath The path to the lyrics we're deleting
+ */
+ public static void deleteLyrics(final String filePath) {
+ try {
+ final File file = new File(filePath);
+ if (file.exists()) {
+ // Use jAudioTagger library to delete the file's lyrics
+ final AudioFile audioFile = AudioFileIO.read(file);
+ final Tag tag = audioFile.getTag();
+ tag.deleteField(FieldKey.LYRICS);
+ audioFile.commit();
+ }
+ } catch (final ReadOnlyFileException e) {
+ e.printStackTrace();
+ } catch (final CannotReadException e) {
+ e.printStackTrace();
+ } catch (final IOException e) {
+ e.printStackTrace();
+ } catch (final TagException e) {
+ e.printStackTrace();
+ } catch (final InvalidAudioFrameException e) {
+ e.printStackTrace();
+ } catch (final CannotWriteException e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/src/com/andrew/apollo/menu/BasePlaylistDialog.java b/src/com/andrew/apollo/menu/BasePlaylistDialog.java
new file mode 100644
index 0000000..aea5e7e
--- /dev/null
+++ b/src/com/andrew/apollo/menu/BasePlaylistDialog.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.menu;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.InputType;
+import android.text.TextWatcher;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.Button;
+import android.widget.EditText;
+
+import com.actionbarsherlock.app.SherlockDialogFragment;
+import com.andrew.apollo.R;
+import com.andrew.apollo.utils.MusicUtils;
+
+/**
+ * A simple base class for the playlist dialogs.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public abstract class BasePlaylistDialog extends SherlockDialogFragment {
+
+ /* The actual dialog */
+ protected AlertDialog mPlaylistDialog;
+
+ /* Used to make new playlist names */
+ protected EditText mPlaylist;
+
+ /* The dialog save button */
+ protected Button mSaveButton;
+
+ /* The dialog prompt */
+ protected String mPrompt;
+
+ /* The default edit text text */
+ protected String mDefaultname;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Dialog onCreateDialog(final Bundle savedInstanceState) {
+ // Initialize the alert dialog
+ mPlaylistDialog = new AlertDialog.Builder(getSherlockActivity()).create();
+ // Initialize the edit text
+ mPlaylist = new EditText(getSherlockActivity());
+ // To show the "done" button on the soft keyboard
+ mPlaylist.setSingleLine(true);
+ // All caps
+ mPlaylist.setInputType(mPlaylist.getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
+ | InputType.TYPE_TEXT_FLAG_CAP_WORDS);
+ // Set the save button action
+ mPlaylistDialog.setButton(Dialog.BUTTON_POSITIVE, getString(R.string.save),
+ new OnClickListener() {
+
+ @Override
+ public void onClick(final DialogInterface dialog, final int which) {
+ onSaveClick();
+ MusicUtils.refresh();
+ dialog.dismiss();
+ }
+ });
+ // Set the cancel button action
+ mPlaylistDialog.setButton(Dialog.BUTTON_NEGATIVE, getString(R.string.cancel),
+ new OnClickListener() {
+
+ @Override
+ public void onClick(final DialogInterface dialog, final int which) {
+ closeKeyboard();
+ MusicUtils.refresh();
+ dialog.dismiss();
+ }
+ });
+
+ mPlaylist.post(new Runnable() {
+
+ @Override
+ public void run() {
+ // Open up the soft keyboard
+ openKeyboard();
+ // Request focus to the edit text
+ mPlaylist.requestFocus();
+ // Select the playlist name
+ mPlaylist.selectAll();
+ };
+ });
+
+ initObjects(savedInstanceState);
+ mPlaylistDialog.setTitle(mPrompt);
+ mPlaylistDialog.setView(mPlaylist);
+ mPlaylist.setText(mDefaultname);
+ mPlaylist.setSelection(mDefaultname.length());
+ mPlaylist.addTextChangedListener(mTextWatcher);
+ mPlaylistDialog.show();
+ return mPlaylistDialog;
+ }
+
+ /**
+ * Opens the soft keyboard
+ */
+ protected void openKeyboard() {
+ final InputMethodManager mInputMethodManager = (InputMethodManager)getSherlockActivity()
+ .getSystemService(Context.INPUT_METHOD_SERVICE);
+ mInputMethodManager.toggleSoftInputFromWindow(mPlaylist.getApplicationWindowToken(),
+ InputMethodManager.SHOW_FORCED, 0);
+ }
+
+ /**
+ * Closes the soft keyboard
+ */
+ protected void closeKeyboard() {
+ final InputMethodManager mInputMethodManager = (InputMethodManager)getSherlockActivity()
+ .getSystemService(Context.INPUT_METHOD_SERVICE);
+ mInputMethodManager.hideSoftInputFromWindow(mPlaylist.getWindowToken(), 0);
+ }
+
+ /**
+ * Simple {@link TextWatcher}
+ */
+ private final TextWatcher mTextWatcher = new TextWatcher() {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onTextChanged(final CharSequence s, final int start, final int before,
+ final int count) {
+ onTextChangedListener();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void afterTextChanged(final Editable s) {
+ /* Nothing to do */
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void beforeTextChanged(final CharSequence s, final int start, final int count,
+ final int after) {
+ /* Nothing to do */
+ }
+ };
+
+ /**
+ * Initializes the prompt and default name
+ */
+ public abstract void initObjects(Bundle savedInstanceState);
+
+ /**
+ * Called when the save button of our {@link AlertDialog} is pressed
+ */
+ public abstract void onSaveClick();
+
+ /**
+ * Called in our {@link TextWatcher} during a text change
+ */
+ public abstract void onTextChangedListener();
+
+}
diff --git a/src/com/andrew/apollo/menu/CreateNewPlaylist.java b/src/com/andrew/apollo/menu/CreateNewPlaylist.java
new file mode 100644
index 0000000..0d7708a
--- /dev/null
+++ b/src/com/andrew/apollo/menu/CreateNewPlaylist.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.menu;
+
+import android.app.Dialog;
+import android.content.ContentResolver;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.provider.MediaStore;
+
+import com.andrew.apollo.R;
+import com.andrew.apollo.format.Capitalize;
+import com.andrew.apollo.utils.MusicUtils;
+
+/**
+ * @author Andrew Neal (andrewdneal@gmail.com) TODO - The playlist names are
+ * automatically capitalized to help when you want to play one via voice
+ * actions, but it really needs to work either way. As in, capitalized
+ * or not.
+ */
+public class CreateNewPlaylist extends BasePlaylistDialog {
+
+ // The playlist list
+ private long[] mPlaylistList = new long[] {};
+
+ /**
+ * @param list The list of tracks to add to the playlist
+ * @return A new instance of this dialog.
+ */
+ public static CreateNewPlaylist getInstance(final long[] list) {
+ final CreateNewPlaylist frag = new CreateNewPlaylist();
+ final Bundle args = new Bundle();
+ args.putLongArray("playlist_list", list);
+ frag.setArguments(args);
+ return frag;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onSaveInstanceState(final Bundle outcicle) {
+ outcicle.putString("defaultname", mPlaylist.getText().toString());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void initObjects(final Bundle savedInstanceState) {
+ mPlaylistList = getArguments().getLongArray("playlist_list");
+ mDefaultname = savedInstanceState != null ? savedInstanceState.getString("defaultname")
+ : makePlaylistName();
+ if (mDefaultname == null) {
+ getDialog().dismiss();
+ return;
+ }
+ final String prromptformat = getString(R.string.create_playlist_prompt);
+ mPrompt = String.format(prromptformat, mDefaultname);
+ }
+
+ @Override
+ public void onSaveClick() {
+ final String playlistName = mPlaylist.getText().toString();
+ if (playlistName != null && playlistName.length() > 0) {
+ final int playlistId = (int)MusicUtils.getIdForPlaylist(getSherlockActivity(),
+ playlistName);
+ if (playlistId >= 0) {
+ MusicUtils.clearPlaylist(getSherlockActivity(), playlistId);
+ MusicUtils.addToPlaylist(getSherlockActivity(), mPlaylistList, playlistId);
+ } else {
+ final long newId = MusicUtils.createPlaylist(getSherlockActivity(),
+ Capitalize.capitalize(playlistName));
+ MusicUtils.addToPlaylist(getSherlockActivity(), mPlaylistList, newId);
+ }
+ closeKeyboard();
+ getDialog().dismiss();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onTextChangedListener() {
+ final String playlistName = mPlaylist.getText().toString();
+ mSaveButton = mPlaylistDialog.getButton(Dialog.BUTTON_POSITIVE);
+ if (mSaveButton == null) {
+ return;
+ }
+ if (playlistName.trim().length() == 0) {
+ mSaveButton.setEnabled(false);
+ } else {
+ mSaveButton.setEnabled(true);
+ if (MusicUtils.getIdForPlaylist(getSherlockActivity(), playlistName) >= 0) {
+ mSaveButton.setText(R.string.overwrite);
+ } else {
+ mSaveButton.setText(R.string.save);
+ }
+ }
+ }
+
+ private String makePlaylistName() {
+ final String template = getString(R.string.new_playlist_name_template);
+ int num = 1;
+ final String[] projection = new String[] {
+ MediaStore.Audio.Playlists.NAME
+ };
+ final ContentResolver resolver = getSherlockActivity().getContentResolver();
+ final String selection = MediaStore.Audio.Playlists.NAME + " != ''";
+ Cursor cursor = resolver.query(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, projection,
+ selection, null, MediaStore.Audio.Playlists.NAME);
+ if (cursor == null) {
+ return null;
+ }
+
+ String suggestedname;
+ suggestedname = String.format(template, num++);
+ boolean done = false;
+ while (!done) {
+ done = true;
+ cursor.moveToFirst();
+ while (!cursor.isAfterLast()) {
+ final String playlistname = cursor.getString(0);
+ if (playlistname.compareToIgnoreCase(suggestedname) == 0) {
+ suggestedname = String.format(template, num++);
+ done = false;
+ }
+ cursor.moveToNext();
+ }
+ }
+ cursor.close();
+ cursor = null;
+ return suggestedname;
+ }
+}
diff --git a/src/com/andrew/apollo/menu/DeleteDialog.java b/src/com/andrew/apollo/menu/DeleteDialog.java
new file mode 100644
index 0000000..a90e4da
--- /dev/null
+++ b/src/com/andrew/apollo/menu/DeleteDialog.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.menu;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+
+import com.actionbarsherlock.app.SherlockDialogFragment;
+import com.andrew.apollo.Config;
+import com.andrew.apollo.R;
+import com.andrew.apollo.cache.ImageFetcher;
+import com.andrew.apollo.utils.ApolloUtils;
+import com.andrew.apollo.utils.MusicUtils;
+
+/**
+ * Alert dialog used to delete tracks.
+ * <p>
+ * TODO: Remove albums from the recents list upon deletion.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class DeleteDialog extends SherlockDialogFragment {
+
+ /**
+ * The item(s) to delete
+ */
+ private long[] mItemList;
+
+ /**
+ * The image cache
+ */
+ private ImageFetcher mFetcher;
+
+ /**
+ * Empty constructor as per the {@link Fragment} documentation
+ */
+ public DeleteDialog() {
+ }
+
+ /**
+ * @param title The title of the artist, album, or song to delete
+ * @param items The item(s) to delete
+ * @param key The key used to remove items from the cache.
+ * @return A new instance of the dialog
+ */
+ public static DeleteDialog newInstance(final String title, final long[] items, final String key) {
+ final DeleteDialog frag = new DeleteDialog();
+ final Bundle args = new Bundle();
+ args.putString(Config.NAME, title);
+ args.putLongArray("items", items);
+ args.putString("cachekey", key);
+ frag.setArguments(args);
+ return frag;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Dialog onCreateDialog(final Bundle savedInstanceState) {
+ final String delete = getString(R.string.context_menu_delete);
+ final Bundle arguments = getArguments();
+ // Get the image cache key
+ final String key = arguments.getString("cachekey");
+ // Get the track(s) to delete
+ mItemList = arguments.getLongArray("items");
+ // Get the dialog title
+ final String title = arguments.getString(Config.NAME);
+ // Initialize the image cache
+ mFetcher = ApolloUtils.getImageFetcher(getSherlockActivity());
+ // Build the dialog
+ return new AlertDialog.Builder(getSherlockActivity()).setTitle(delete + " " + title)
+ .setMessage(R.string.cannot_be_undone)
+ .setPositiveButton(delete, new OnClickListener() {
+
+ @Override
+ public void onClick(final DialogInterface dialog, final int which) {
+ // Remove the items from the image cache
+ mFetcher.removeFromCache(key);
+ // Delete the selected item(s)
+ MusicUtils.deleteTracks(getSherlockActivity(), mItemList);
+ dialog.dismiss();
+ }
+ }).setNegativeButton(R.string.cancel, new OnClickListener() {
+
+ @Override
+ public void onClick(final DialogInterface dialog, final int which) {
+ dialog.dismiss();
+ }
+ }).create();
+ }
+}
diff --git a/src/com/andrew/apollo/menu/FragmentMenuItems.java b/src/com/andrew/apollo/menu/FragmentMenuItems.java
new file mode 100644
index 0000000..e472151
--- /dev/null
+++ b/src/com/andrew/apollo/menu/FragmentMenuItems.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.menu;
+
+/**
+ * Several of the context menu items used in Apollo are reused. This class helps
+ * keep things tidy.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class FragmentMenuItems {
+
+ /* Removes a single album from the recents pages */
+ public static final int REMOVE_FROM_RECENT = 0;
+
+ /* Used to play the selected artist, album, song, playlist, or genre */
+ public static final int PLAY_SELECTION = 1;
+
+ /* Used to add to the qeueue */
+ public static final int ADD_TO_QUEUE = 2;
+
+ /* Used to add to a playlist */
+ public static final int ADD_TO_PLAYLIST = 3;
+
+ /* Used to add to the favorites cache */
+ public static final int ADD_TO_FAVORITES = 4;
+
+ /* Used to create a new playlist */
+ public static final int NEW_PLAYLIST = 5;
+
+ /* Used to rename a playlist */
+ public static final int RENAME_PLAYLIST = 6;
+
+ /* Used to add to a current playlist */
+ public static final int PLAYLIST_SELECTED = 7;
+
+ /* Used to show more content by an artist */
+ public static final int MORE_BY_ARTIST = 8;
+
+ /* Used to delete track(s) */
+ public static final int DELETE = 9;
+
+ /* Used to fetch an artist image */
+ public static final int FETCH_ARTIST_IMAGE = 10;
+
+ /* Used to fetch album art */
+ public static final int FETCH_ALBUM_ART = 11;
+
+ /* Used to set a track as a ringtone */
+ public static final int USE_AS_RINGTONE = 12;
+
+ /* Used to add an artist to the quickplay */
+ public static final int PINN_TO_QUICKPLAY = 13;
+
+ /* Used to remove a track from the favorites cache */
+ public static final int REMOVE_FROM_FAVORITES = 14;
+
+ /* Used to remove a track from a playlist */
+ public static final int REMOVE_FROM_PLAYLIST = 15;
+
+ /* Used to remove a track from the queue */
+ public static final int REMOVE_FROM_QUEUE = 16;
+
+ /* Used to remove a track from the Quickplay menu */
+ public static final int REMOVE_FROM_QUICKPLAY = 17;
+
+ /* Used to queue a track to be played next */
+ public static final int PLAY_NEXT = 18;
+
+}
diff --git a/src/com/andrew/apollo/menu/PhotoSelectionDialog.java b/src/com/andrew/apollo/menu/PhotoSelectionDialog.java
new file mode 100644
index 0000000..77ec216
--- /dev/null
+++ b/src/com/andrew/apollo/menu/PhotoSelectionDialog.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.menu;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.widget.ArrayAdapter;
+import android.widget.ListAdapter;
+
+import com.actionbarsherlock.app.SherlockDialogFragment;
+import com.andrew.apollo.Config;
+import com.andrew.apollo.R;
+import com.andrew.apollo.ui.activities.ProfileActivity;
+import com.andrew.apollo.utils.ApolloUtils;
+import com.andrew.apollo.utils.Lists;
+
+import java.util.ArrayList;
+
+/**
+ * Used when the user touches the image in the header in {@link ProfileActivity}
+ * . It provides an easy interface for them to choose a new image, use the old
+ * image, or search Google for one.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class PhotoSelectionDialog extends SherlockDialogFragment {
+
+ private static final int NEW_PHOTO = 0;
+
+ private static final int OLD_PHOTO = 1;
+
+ private static final int GOOGLE_SEARCH = 2;
+
+ private static final int FETCH_IMAGE = 3;
+
+ private final ArrayList<String> mChoices = Lists.newArrayList();
+
+ private static ProfileType mProfileType;
+
+ /**
+ * Empty constructor as per the {@link Fragment} documentation
+ */
+ public PhotoSelectionDialog() {
+ }
+
+ /**
+ * @param title The dialog title.
+ * @param value The MIME type
+ * @return A new instance of the dialog.
+ */
+ public static PhotoSelectionDialog newInstance(final String title, final ProfileType type) {
+ final PhotoSelectionDialog frag = new PhotoSelectionDialog();
+ final Bundle args = new Bundle();
+ args.putString(Config.NAME, title);
+ frag.setArguments(args);
+ mProfileType = type;
+ return frag;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Dialog onCreateDialog(final Bundle savedInstanceState) {
+ final String title = getArguments().getString(Config.NAME);
+ switch (mProfileType) {
+ case ARTIST:
+ setArtistChoices();
+ break;
+ case ALBUM:
+ setAlbumChoices();
+ break;
+ case OTHER:
+ setOtherChoices();
+ break;
+ default:
+ break;
+ }
+ // Dialog item Adapter
+ final ListAdapter adapter = new ArrayAdapter<String>(getSherlockActivity(),
+ android.R.layout.select_dialog_item, mChoices);
+ return new AlertDialog.Builder(getSherlockActivity()).setTitle(title)
+ .setAdapter(adapter, new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(final DialogInterface dialog, final int which) {
+ switch (which) {
+ case NEW_PHOTO:
+ ((ProfileActivity)getSherlockActivity()).selectNewPhoto();
+ break;
+ case OLD_PHOTO:
+ ((ProfileActivity)getSherlockActivity()).selectOldPhoto();
+ break;
+ case FETCH_IMAGE:
+ ((ProfileActivity)getSherlockActivity()).fetchAlbumArt();
+ break;
+ case GOOGLE_SEARCH:
+ ((ProfileActivity)getSherlockActivity()).googleSearch();
+ break;
+ default:
+ break;
+ }
+ }
+ }).create();
+ }
+
+ /**
+ * Adds the choices for the artist profile image.
+ */
+ private void setArtistChoices() {
+ // Select a photo from the gallery
+ mChoices.add(NEW_PHOTO, getString(R.string.new_photo));
+ if (ApolloUtils.isOnline(getSherlockActivity())) {
+ // Option to fetch the old artist image
+ mChoices.add(OLD_PHOTO, getString(R.string.context_menu_fetch_artist_image));
+ // Search Google for the artist name
+ mChoices.add(GOOGLE_SEARCH, getString(R.string.google_search));
+ }
+ }
+
+ /**
+ * Adds the choices for the album profile image.
+ */
+ private void setAlbumChoices() {
+ // Select a photo from the gallery
+ mChoices.add(NEW_PHOTO, getString(R.string.new_photo));
+ // Option to fetch the old album image
+ mChoices.add(OLD_PHOTO, getString(R.string.old_photo));
+ if (ApolloUtils.isOnline(getSherlockActivity())) {
+ // Search Google for the album name
+ mChoices.add(GOOGLE_SEARCH, getString(R.string.google_search));
+ // Option to fetch the album image
+ mChoices.add(FETCH_IMAGE, getString(R.string.context_menu_fetch_album_art));
+ }
+ }
+
+ /**
+ * Adds the choices for the genre and playlist images.
+ */
+ private void setOtherChoices() {
+ // Select a photo from the gallery
+ mChoices.add(NEW_PHOTO, getString(R.string.new_photo));
+ // Option to use the default image
+ mChoices.add(OLD_PHOTO, getString(R.string.use_default));
+ }
+
+ /**
+ * Easily detect the MIME type
+ */
+ public enum ProfileType {
+ ARTIST, ALBUM, OTHER
+ }
+}
diff --git a/src/com/andrew/apollo/menu/PlaylistDialog.java b/src/com/andrew/apollo/menu/PlaylistDialog.java
deleted file mode 100644
index 05159b8..0000000
--- a/src/com/andrew/apollo/menu/PlaylistDialog.java
+++ /dev/null
@@ -1,328 +0,0 @@
-/*
- * Copyright (C) 2011 The MusicMod Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.andrew.apollo.menu;
-
-import android.app.AlertDialog;
-import android.app.Dialog;
-import android.content.ContentResolver;
-import android.content.DialogInterface;
-import android.content.DialogInterface.OnCancelListener;
-import android.content.DialogInterface.OnClickListener;
-import android.content.DialogInterface.OnShowListener;
-import android.database.Cursor;
-import android.media.AudioManager;
-import android.os.Bundle;
-import android.provider.MediaStore;
-import android.provider.MediaStore.Audio;
-import android.support.v4.app.FragmentActivity;
-import android.text.Editable;
-import android.text.TextWatcher;
-import android.util.DisplayMetrics;
-import android.widget.Button;
-import android.widget.EditText;
-import android.widget.LinearLayout;
-import android.widget.Toast;
-
-import com.andrew.apollo.Constants;
-import com.andrew.apollo.R;
-import com.andrew.apollo.utils.MusicUtils;
-
-public class PlaylistDialog extends FragmentActivity implements Constants, TextWatcher,
- OnCancelListener, OnShowListener {
-
- private AlertDialog mPlaylistDialog;
-
- private String action;
-
- private EditText mPlaylist;
-
- private String mDefaultName, mOriginalName;
-
- private long mRenameId;
-
- private long[] mList = new long[] {};
-
- private final OnClickListener mRenamePlaylistListener = new OnClickListener() {
-
- @Override
- public void onClick(DialogInterface dialog, int which) {
-
- String name = mPlaylist.getText().toString();
- MusicUtils.renamePlaylist(PlaylistDialog.this, mRenameId, name);
- finish();
- }
- };
-
- private final OnClickListener mCreatePlaylistListener = new OnClickListener() {
-
- @Override
- public void onClick(DialogInterface dialog, int which) {
-
- String name = mPlaylist.getText().toString();
- if (name != null && name.length() > 0) {
- int id = idForplaylist(name);
- if (id >= 0) {
- MusicUtils.clearPlaylist(PlaylistDialog.this, id);
- MusicUtils.addToPlaylist(PlaylistDialog.this, mList, id);
- } else {
- long new_id = MusicUtils.createPlaylist(PlaylistDialog.this, name);
- if (new_id >= 0) {
- MusicUtils.addToPlaylist(PlaylistDialog.this, mList, new_id);
- }
- }
- finish();
- }
- }
- };
-
- @Override
- public void afterTextChanged(Editable s) {
-
- // don't care about this one
- }
-
- @Override
- public void beforeTextChanged(CharSequence s, int start, int count, int after) {
-
- // don't care about this one
- }
-
- @Override
- public void onCancel(DialogInterface dialog) {
-
- if (dialog == mPlaylistDialog) {
- finish();
- }
- }
-
- @Override
- public void onCreate(Bundle icicle) {
-
- super.onCreate(icicle);
- setContentView(new LinearLayout(this));
-
- action = getIntent().getAction();
-
- mRenameId = icicle != null ? icicle.getLong(INTENT_KEY_RENAME) : getIntent().getLongExtra(
- INTENT_KEY_RENAME, -1);
- mList = icicle != null ? icicle.getLongArray(INTENT_PLAYLIST_LIST) : getIntent()
- .getLongArrayExtra(INTENT_PLAYLIST_LIST);
- if (INTENT_RENAME_PLAYLIST.equals(action)) {
- mOriginalName = nameForId(mRenameId);
- mDefaultName = icicle != null ? icicle.getString(INTENT_KEY_DEFAULT_NAME)
- : mOriginalName;
- } else if (INTENT_CREATE_PLAYLIST.equals(action)) {
- mDefaultName = icicle != null ? icicle.getString(INTENT_KEY_DEFAULT_NAME)
- : makePlaylistName();
- mOriginalName = mDefaultName;
- }
-
- DisplayMetrics dm = new DisplayMetrics();
- dm = getResources().getDisplayMetrics();
-
- mPlaylistDialog = new AlertDialog.Builder(this).create();
- mPlaylistDialog.setVolumeControlStream(AudioManager.STREAM_MUSIC);
-
- if (action != null && mRenameId >= 0 && mOriginalName != null || mDefaultName != null) {
-
- mPlaylist = new EditText(this);
- mPlaylist.setSingleLine(true);
- mPlaylist.setText(mDefaultName);
- mPlaylist.setSelection(mDefaultName.length());
- mPlaylist.addTextChangedListener(this);
-
- mPlaylistDialog.setIcon(android.R.drawable.ic_dialog_info);
- String promptformat;
- String prompt = "";
- if (INTENT_RENAME_PLAYLIST.equals(action)) {
- promptformat = getString(R.string.rename_playlist);
- prompt = String.format(promptformat, mOriginalName, mDefaultName);
- } else if (INTENT_CREATE_PLAYLIST.equals(action)) {
- promptformat = getString(R.string.new_playlist);
- prompt = String.format(promptformat, mDefaultName);
- }
-
- mPlaylistDialog.setTitle(prompt);
- mPlaylistDialog.setView(mPlaylist, (int)(8 * dm.density), (int)(8 * dm.density),
- (int)(8 * dm.density), (int)(4 * dm.density));
- if (INTENT_RENAME_PLAYLIST.equals(action)) {
- mPlaylistDialog.setButton(Dialog.BUTTON_POSITIVE, getString(R.string.save),
- mRenamePlaylistListener);
- mPlaylistDialog.setOnShowListener(this);
- } else if (INTENT_CREATE_PLAYLIST.equals(action)) {
- mPlaylistDialog.setButton(Dialog.BUTTON_POSITIVE, getString(R.string.save),
- mCreatePlaylistListener);
- }
- mPlaylistDialog.setButton(Dialog.BUTTON_NEGATIVE, getString(android.R.string.cancel),
- new OnClickListener() {
-
- @Override
- public void onClick(DialogInterface dialog, int which) {
-
- finish();
- }
- });
- mPlaylistDialog.setOnCancelListener(this);
- mPlaylistDialog.show();
- } else {
- Toast.makeText(this, R.string.error, Toast.LENGTH_SHORT).show();
- finish();
- }
-
- }
-
- @Override
- public void onPause() {
-
- if (mPlaylistDialog != null && mPlaylistDialog.isShowing()) {
- mPlaylistDialog.dismiss();
- }
- super.onPause();
- }
-
- @Override
- public void onSaveInstanceState(Bundle outcicle) {
-
- if (INTENT_RENAME_PLAYLIST.equals(action)) {
- outcicle.putString(INTENT_KEY_DEFAULT_NAME, mPlaylist.getText().toString());
- outcicle.putLong(INTENT_KEY_RENAME, mRenameId);
- } else if (INTENT_CREATE_PLAYLIST.equals(action)) {
- outcicle.putString(INTENT_KEY_DEFAULT_NAME, mPlaylist.getText().toString());
- }
- }
-
- @Override
- public void onShow(DialogInterface dialog) {
-
- if (dialog == mPlaylistDialog) {
- setSaveButton();
- }
- }
-
- @Override
- public void onTextChanged(CharSequence s, int start, int before, int count) {
-
- setSaveButton();
- }
-
- private int idForplaylist(String name) {
-
- Cursor cursor = MusicUtils.query(this, Audio.Playlists.EXTERNAL_CONTENT_URI, new String[] {
- Audio.Playlists._ID
- }, Audio.Playlists.NAME + "=?", new String[] {
- name
- }, Audio.Playlists.NAME, 0);
- int id = -1;
- if (cursor != null) {
- cursor.moveToFirst();
- if (!cursor.isAfterLast()) {
- id = cursor.getInt(0);
- }
- cursor.close();
- }
-
- return id;
- }
-
- private String makePlaylistName() {
-
- String template = getString(R.string.new_playlist_name_template);
- int num = 1;
-
- String[] cols = new String[] {
- Audio.Playlists.NAME
- };
- ContentResolver resolver = getContentResolver();
- String whereclause = Audio.Playlists.NAME + " != ''";
- Cursor cursor = resolver.query(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, cols,
- whereclause, null, Audio.Playlists.NAME);
-
- if (cursor == null)
- return null;
-
- String suggestedname;
- suggestedname = String.format(template, num++);
-
- // Need to loop until we've made 1 full pass through without finding a
- // match. Looping more than once shouldn't happen very often, but will
- // happen if you have playlists named
- // "New Playlist 1"/10/2/3/4/5/6/7/8/9, where making only one pass would
- // result in "New Playlist 10" being erroneously picked for the new
- // name.
- boolean done = false;
- while (!done) {
- done = true;
- cursor.moveToFirst();
- while (!cursor.isAfterLast()) {
- String playlistname = cursor.getString(0);
- if (playlistname.compareToIgnoreCase(suggestedname) == 0) {
- suggestedname = String.format(template, num++);
- done = false;
- }
- cursor.moveToNext();
- }
- }
- cursor.close();
- return suggestedname;
- };
-
- private String nameForId(long id) {
-
- Cursor cursor = MusicUtils.query(this, Audio.Playlists.EXTERNAL_CONTENT_URI, new String[] {
- Audio.Playlists.NAME
- }, Audio.Playlists._ID + "=?", new String[] {
- Long.valueOf(id).toString()
- }, Audio.Playlists.NAME);
- String name = null;
- if (cursor != null) {
- cursor.moveToFirst();
- if (!cursor.isAfterLast()) {
- name = cursor.getString(0);
- }
- cursor.close();
- }
- return name;
- }
-
- private void setSaveButton() {
-
- String typedname = mPlaylist.getText().toString();
- Button button = mPlaylistDialog.getButton(Dialog.BUTTON_POSITIVE);
- if (button == null)
- return;
- if (typedname.trim().length() == 0 || PLAYLIST_NAME_FAVORITES.equals(typedname)) {
- button.setEnabled(false);
- } else {
- button.setEnabled(true);
- if (idForplaylist(typedname) >= 0 && !mOriginalName.equals(typedname)) {
- button.setText(R.string.overwrite);
- } else {
- button.setText(R.string.save);
- }
- }
- button.invalidate();
- }
-
- @Override
- protected void onResume() {
-
- super.onResume();
- if (mPlaylistDialog != null) {
- mPlaylistDialog.show();
- }
- }
-}
diff --git a/src/com/andrew/apollo/menu/PlaylistPicker.java b/src/com/andrew/apollo/menu/PlaylistPicker.java
deleted file mode 100644
index 8eb7afc..0000000
--- a/src/com/andrew/apollo/menu/PlaylistPicker.java
+++ /dev/null
@@ -1,121 +0,0 @@
-
-package com.andrew.apollo.menu;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-
-import android.app.AlertDialog;
-import android.content.DialogInterface;
-import android.content.Intent;
-import android.os.Bundle;
-import android.support.v4.app.FragmentActivity;
-import android.widget.LinearLayout;
-import android.widget.Toast;
-
-import com.andrew.apollo.R;
-import com.andrew.apollo.utils.MusicUtils;
-
-public class PlaylistPicker extends FragmentActivity implements DialogInterface.OnClickListener,
- DialogInterface.OnCancelListener, com.andrew.apollo.Constants {
-
- private AlertDialog mPlayListPickerDialog;
-
- List<Map<String, String>> mAllPlayLists = new ArrayList<Map<String, String>>();
-
- List<String> mPlayListNames = new ArrayList<String>();
-
- long[] mList = new long[] {};
-
- @Override
- public void onCancel(DialogInterface dialog) {
- finish();
- }
-
- @Override
- public void onClick(DialogInterface dialog, int which) {
-
- long listId = Long.valueOf(mAllPlayLists.get(which).get("id"));
- String listName = mAllPlayLists.get(which).get("name");
- Intent intent;
- boolean mCreateShortcut = getIntent().getAction().equals(Intent.ACTION_CREATE_SHORTCUT);
-
- if (mCreateShortcut) {
- final Intent shortcut = new Intent();
- // shortcut.setAction(INTENT_PLAY_SHORTCUT);
- shortcut.putExtra("id", listId);
-
- intent = new Intent();
- intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcut);
- intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, listName);
- intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
- Intent.ShortcutIconResource.fromContext(this, R.drawable.ic_launcher));
- setResult(RESULT_OK, intent);
- } else {
- if (listId >= 0) {
- MusicUtils.addToPlaylist(this, mList, listId);
- } else if (listId == PLAYLIST_QUEUE) {
- MusicUtils.addToCurrentPlaylist(this, mList);
- } else if (listId == PLAYLIST_NEW) {
- intent = new Intent(INTENT_CREATE_PLAYLIST);
- intent.putExtra(INTENT_PLAYLIST_LIST, mList);
- startActivity(intent);
- }
- }
- finish();
- }
-
- @Override
- public void onCreate(Bundle icicle) {
-
- super.onCreate(icicle);
- setContentView(new LinearLayout(this));
-
- if (getIntent().getAction() != null) {
-
- if (INTENT_ADD_TO_PLAYLIST.equals(getIntent().getAction())
- && getIntent().getLongArrayExtra(INTENT_PLAYLIST_LIST) != null) {
-
- MusicUtils.makePlaylistList(this, false, mAllPlayLists);
- mList = getIntent().getLongArrayExtra(INTENT_PLAYLIST_LIST);
- for (int i = 0; i < mAllPlayLists.size(); i++) {
- mPlayListNames.add(mAllPlayLists.get(i).get("name"));
- }
- mPlayListPickerDialog = new AlertDialog.Builder(this)
- .setTitle(R.string.add_to_playlist)
- .setItems(mPlayListNames.toArray(new CharSequence[mPlayListNames.size()]),
- this).setOnCancelListener(this).show();
- } else if (getIntent().getAction().equals(Intent.ACTION_CREATE_SHORTCUT)) {
- MusicUtils.makePlaylistList(this, true, mAllPlayLists);
- for (int i = 0; i < mAllPlayLists.size(); i++) {
- mPlayListNames.add(mAllPlayLists.get(i).get("name"));
- }
- mPlayListPickerDialog = new AlertDialog.Builder(this)
- .setItems(mPlayListNames.toArray(new CharSequence[mPlayListNames.size()]),
- this).setOnCancelListener(this).show();
- }
- } else {
- Toast.makeText(this, R.string.error, Toast.LENGTH_SHORT).show();
- finish();
- }
- }
-
- @Override
- public void onPause() {
-
- if (mPlayListPickerDialog != null && mPlayListPickerDialog.isShowing()) {
- mPlayListPickerDialog.dismiss();
- }
- super.onPause();
- }
-
- @Override
- protected void onResume() {
-
- super.onResume();
- if (mPlayListPickerDialog != null && !mPlayListPickerDialog.isShowing()) {
- mPlayListPickerDialog.show();
- }
- }
-
-}
diff --git a/src/com/andrew/apollo/menu/PlaylistPickerDialog.java b/src/com/andrew/apollo/menu/PlaylistPickerDialog.java
deleted file mode 100644
index 7920fc7..0000000
--- a/src/com/andrew/apollo/menu/PlaylistPickerDialog.java
+++ /dev/null
@@ -1,28 +0,0 @@
-
-package com.andrew.apollo.menu;
-
-import android.app.AlertDialog;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.DialogInterface.OnCancelListener;
-import android.content.DialogInterface.OnClickListener;
-
-public class PlaylistPickerDialog extends AlertDialog implements OnCancelListener, OnClickListener {
-
- public PlaylistPickerDialog(Context context) {
- super(context);
- }
-
- @Override
- public void onCancel(DialogInterface dialog) {
- // TODO Auto-generated method stub
-
- }
-
- @Override
- public void onClick(DialogInterface dialog, int which) {
- // TODO Auto-generated method stub
-
- }
-
-}
diff --git a/src/com/andrew/apollo/menu/RenamePlaylist.java b/src/com/andrew/apollo/menu/RenamePlaylist.java
new file mode 100644
index 0000000..fd976ee
--- /dev/null
+++ b/src/com/andrew/apollo/menu/RenamePlaylist.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.menu;
+
+import android.app.Dialog;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Audio;
+
+import com.andrew.apollo.R;
+import com.andrew.apollo.format.Capitalize;
+import com.andrew.apollo.utils.MusicUtils;
+
+/**
+ * Alert dialog used to rename playlits.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class RenamePlaylist extends BasePlaylistDialog {
+
+ private String mOriginalName;
+
+ private long mRenameId;
+
+ /**
+ * @param id The Id of the playlist to rename
+ * @return A new instance of this dialog.
+ */
+ public static RenamePlaylist getInstance(final Long id) {
+ final RenamePlaylist frag = new RenamePlaylist();
+ final Bundle args = new Bundle();
+ args.putLong("rename", id);
+ frag.setArguments(args);
+ return frag;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onSaveInstanceState(final Bundle outcicle) {
+ outcicle.putString("defaultname", mPlaylist.getText().toString());
+ outcicle.putLong("rename", mRenameId);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void initObjects(final Bundle savedInstanceState) {
+ mRenameId = savedInstanceState != null ? savedInstanceState.getLong("rename")
+ : getArguments().getLong("rename", -1);
+ mOriginalName = getPlaylistNameFromId(mRenameId);
+ mDefaultname = savedInstanceState != null ? savedInstanceState.getString("defaultname")
+ : mOriginalName;
+ if (mRenameId < 0 || mOriginalName == null || mDefaultname == null) {
+ getDialog().dismiss();
+ return;
+ }
+ final String promptformat = getString(R.string.create_playlist_prompt);
+ mPrompt = String.format(promptformat, mOriginalName, mDefaultname);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onSaveClick() {
+ final String playlistName = mPlaylist.getText().toString();
+ if (playlistName != null && playlistName.length() > 0) {
+ final ContentResolver resolver = getSherlockActivity().getContentResolver();
+ final ContentValues values = new ContentValues(1);
+ values.put(Audio.Playlists.NAME, Capitalize.capitalize(playlistName));
+ resolver.update(Audio.Playlists.EXTERNAL_CONTENT_URI, values,
+ MediaStore.Audio.Playlists._ID + "=?", new String[] {
+ Long.valueOf(mRenameId).toString()
+ });
+ closeKeyboard();
+ getDialog().dismiss();
+ }
+ }
+
+ @Override
+ public void onTextChangedListener() {
+ final String playlistName = mPlaylist.getText().toString();
+ mSaveButton = mPlaylistDialog.getButton(Dialog.BUTTON_POSITIVE);
+ if (mSaveButton == null) {
+ return;
+ }
+ if (playlistName.trim().length() == 0) {
+ mSaveButton.setEnabled(false);
+ } else {
+ mSaveButton.setEnabled(true);
+ if (MusicUtils.getIdForPlaylist(getSherlockActivity(), playlistName) >= 0) {
+ mSaveButton.setText(R.string.overwrite);
+ } else {
+ mSaveButton.setText(R.string.save);
+ }
+ }
+ }
+
+ /**
+ * @param id The Id of the playlist
+ * @return The name of the playlist
+ */
+ private String getPlaylistNameFromId(final long id) {
+ Cursor cursor = getSherlockActivity().getContentResolver().query(
+ MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, new String[] {
+ MediaStore.Audio.Playlists.NAME
+ }, MediaStore.Audio.Playlists._ID + "=?", new String[] {
+ Long.valueOf(id).toString()
+ }, MediaStore.Audio.Playlists.NAME);
+ String playlistName = null;
+ if (cursor != null) {
+ cursor.moveToFirst();
+ if (!cursor.isAfterLast()) {
+ playlistName = cursor.getString(0);
+ }
+ }
+ cursor.close();
+ cursor = null;
+ return playlistName;
+ }
+
+}
diff --git a/src/com/andrew/apollo/model/Album.java b/src/com/andrew/apollo/model/Album.java
new file mode 100644
index 0000000..96d7805
--- /dev/null
+++ b/src/com/andrew/apollo/model/Album.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.model;
+
+/**
+ * A class that represents an album.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class Album {
+
+ /**
+ * The unique Id of the album
+ */
+ public String mAlbumId;
+
+ /**
+ * The name of the album
+ */
+ public String mAlbumName;
+
+ /**
+ * The album artist
+ */
+ public String mArtistName;
+
+ /**
+ * The number of songs in the album
+ */
+ public String mSongNumber;
+
+ /**
+ * The year the album was released
+ */
+ public String mYear;
+
+ /**
+ * Constructor of <code>Album</code>
+ *
+ * @param albumId The Id of the album
+ * @param albumName The name of the album
+ * @param artistName The album artist
+ * @param songNumber The number of songs in the album
+ * @param albumYear The year the album was released
+ */
+ public Album(final String albumId, final String albumName, final String artistName,
+ final String songNumber, final String albumYear) {
+ super();
+ mAlbumId = albumId;
+ mAlbumName = albumName;
+ mArtistName = artistName;
+ mSongNumber = songNumber;
+ mYear = albumYear;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + (mAlbumId == null ? 0 : mAlbumId.hashCode());
+ result = prime * result + (mAlbumName == null ? 0 : mAlbumName.hashCode());
+ result = prime * result + (mArtistName == null ? 0 : mArtistName.hashCode());
+ result = prime * result + (mSongNumber == null ? 0 : mSongNumber.hashCode());
+ result = prime * result + (mYear == null ? 0 : mYear.hashCode());
+ return result;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final Album other = (Album)obj;
+ if (mAlbumId == null) {
+ if (other.mAlbumId != null) {
+ return false;
+ }
+ } else if (!mAlbumId.equals(other.mAlbumId)) {
+ return false;
+ }
+ if (mAlbumName == null) {
+ if (other.mAlbumName != null) {
+ return false;
+ }
+ } else if (!mAlbumName.equals(other.mAlbumName)) {
+ return false;
+ }
+ if (mArtistName == null) {
+ if (other.mArtistName != null) {
+ return false;
+ }
+ } else if (!mArtistName.equals(other.mArtistName)) {
+ return false;
+ }
+ if (mSongNumber == null) {
+ if (other.mSongNumber != null) {
+ return false;
+ }
+ } else if (!mSongNumber.equals(other.mSongNumber)) {
+ return false;
+ }
+ if (mYear == null) {
+ if (other.mYear != null) {
+ return false;
+ }
+ } else if (!mYear.equals(other.mYear)) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String toString() {
+ return mAlbumName;
+ }
+
+}
diff --git a/src/com/andrew/apollo/model/Artist.java b/src/com/andrew/apollo/model/Artist.java
new file mode 100644
index 0000000..62a6361
--- /dev/null
+++ b/src/com/andrew/apollo/model/Artist.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.model;
+
+/**
+ * A class that represents an artist.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class Artist {
+
+ /**
+ * The unique Id of the artist
+ */
+ public String mArtistId;
+
+ /**
+ * The artist name
+ */
+ public String mArtistName;
+
+ /**
+ * The number of albums for the artist
+ */
+ public String mAlbumNumber;
+
+ /**
+ * The number of songs for the artist
+ */
+ public String mSongNumber;
+
+ /**
+ * Constructor of <code>Artist</code>
+ *
+ * @param artistId The Id of the artist
+ * @param artistName The artist name
+ * @param songNumber The number of songs for the artist
+ * @param albumNumber The number of albums for the artist
+ */
+ public Artist(final String artistId, final String artistName, final String songNumber,
+ final String albumNumber) {
+ super();
+ mArtistId = artistId;
+ mArtistName = artistName;
+ mSongNumber = songNumber;
+ mAlbumNumber = albumNumber;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + (mAlbumNumber == null ? 0 : mAlbumNumber.hashCode());
+ result = prime * result + (mArtistId == null ? 0 : mArtistId.hashCode());
+ result = prime * result + (mArtistName == null ? 0 : mArtistName.hashCode());
+ result = prime * result + (mSongNumber == null ? 0 : mSongNumber.hashCode());
+ return result;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final Artist other = (Artist)obj;
+ if (mAlbumNumber == null) {
+ if (other.mAlbumNumber != null) {
+ return false;
+ }
+ } else if (!mAlbumNumber.equals(other.mAlbumNumber)) {
+ return false;
+ }
+ if (mArtistId == null) {
+ if (other.mArtistId != null) {
+ return false;
+ }
+ } else if (!mArtistId.equals(other.mArtistId)) {
+ return false;
+ }
+ if (mArtistName == null) {
+ if (other.mArtistName != null) {
+ return false;
+ }
+ } else if (!mArtistName.equals(other.mArtistName)) {
+ return false;
+ }
+ if (mSongNumber == null) {
+ if (other.mSongNumber != null) {
+ return false;
+ }
+ } else if (!mSongNumber.equals(other.mSongNumber)) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String toString() {
+ return mArtistName;
+ }
+
+}
diff --git a/src/com/andrew/apollo/model/Genre.java b/src/com/andrew/apollo/model/Genre.java
new file mode 100644
index 0000000..777ee4d
--- /dev/null
+++ b/src/com/andrew/apollo/model/Genre.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.model;
+
+/**
+ * A class that represents a genre.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class Genre {
+
+ /**
+ * The unique Id of the genre
+ */
+ public String mGenreId;
+
+ /**
+ * The genre name
+ */
+ public String mGenreName;
+
+ /**
+ * Constructor of <code>Genre</code>
+ *
+ * @param genreId The Id of the genre
+ * @param genreName The genre name
+ */
+ public Genre(final String genreId, final String genreName) {
+ super();
+ mGenreId = genreId;
+ mGenreName = genreName;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + (mGenreId == null ? 0 : mGenreId.hashCode());
+ result = prime * result + (mGenreName == null ? 0 : mGenreName.hashCode());
+ return result;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final Genre other = (Genre)obj;
+ if (mGenreId == null) {
+ if (other.mGenreId != null) {
+ return false;
+ }
+ } else if (!mGenreId.equals(other.mGenreId)) {
+ return false;
+ }
+ if (mGenreName == null) {
+ if (other.mGenreName != null) {
+ return false;
+ }
+ } else if (!mGenreName.equals(other.mGenreName)) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String toString() {
+ return mGenreName;
+ }
+
+}
diff --git a/src/com/andrew/apollo/model/Playlist.java b/src/com/andrew/apollo/model/Playlist.java
new file mode 100644
index 0000000..ac13076
--- /dev/null
+++ b/src/com/andrew/apollo/model/Playlist.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.model;
+
+/**
+ * A class that represents a playlist.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class Playlist {
+
+ /**
+ * The unique Id of the playlist
+ */
+ public String mPlaylistId;
+
+ /**
+ * The playlist name
+ */
+ public String mPlaylistName;
+
+ /**
+ * Constructor of <code>Genre</code>
+ *
+ * @param playlistId The Id of the playlist
+ * @param playlistName The playlist name
+ */
+ public Playlist(final String playlistId, final String playlistName) {
+ super();
+ mPlaylistId = playlistId;
+ mPlaylistName = playlistName;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + (mPlaylistId == null ? 0 : mPlaylistId.hashCode());
+ result = prime * result + (mPlaylistName == null ? 0 : mPlaylistName.hashCode());
+ return result;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final Playlist other = (Playlist)obj;
+ if (mPlaylistId == null) {
+ if (other.mPlaylistId != null) {
+ return false;
+ }
+ } else if (!mPlaylistId.equals(other.mPlaylistId)) {
+ return false;
+ }
+ if (mPlaylistName == null) {
+ if (other.mPlaylistName != null) {
+ return false;
+ }
+ } else if (!mPlaylistName.equals(other.mPlaylistName)) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String toString() {
+ return mPlaylistName;
+ }
+
+}
diff --git a/src/com/andrew/apollo/model/Song.java b/src/com/andrew/apollo/model/Song.java
new file mode 100644
index 0000000..c4d82cb
--- /dev/null
+++ b/src/com/andrew/apollo/model/Song.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.model;
+
+/**
+ * A class that represents a song.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class Song {
+
+ /**
+ * The unique Id of the song
+ */
+ public String mSongId;
+
+ /**
+ * The song name
+ */
+ public String mSongName;
+
+ /**
+ * The song artist
+ */
+ public String mArtistName;
+
+ /**
+ * The song album
+ */
+ public String mAlbumName;
+
+ /**
+ * The song duration
+ */
+ public String mDuration;
+
+ /**
+ * Constructor of <code>Song</code>
+ *
+ * @param songId The Id of the song
+ * @param songName The name of the song
+ * @param artistName The song artist
+ * @param albumName The song album
+ * @param duration The duration of a song
+ */
+ public Song(final String songId, final String songName, final String artistName,
+ final String albumName, final String duration) {
+ mSongId = new String(songId);
+ mSongName = songName;
+ mArtistName = artistName;
+ mAlbumName = albumName;
+ mDuration = duration;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + (mAlbumName == null ? 0 : mAlbumName.hashCode());
+ result = prime * result + (mArtistName == null ? 0 : mArtistName.hashCode());
+ result = prime * result + (mDuration == null ? 0 : mDuration.hashCode());
+ result = prime * result + (mSongId == null ? 0 : mSongId.hashCode());
+ result = prime * result + (mSongName == null ? 0 : mSongName.hashCode());
+ return result;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final Song other = (Song)obj;
+ if (mAlbumName == null) {
+ if (other.mAlbumName != null) {
+ return false;
+ }
+ } else if (!mAlbumName.equals(other.mAlbumName)) {
+ return false;
+ }
+ if (mArtistName == null) {
+ if (other.mArtistName != null) {
+ return false;
+ }
+ } else if (!mArtistName.equals(other.mArtistName)) {
+ return false;
+ }
+ if (mDuration == null) {
+ if (other.mDuration != null) {
+ return false;
+ }
+ } else if (!mDuration.equals(other.mDuration)) {
+ return false;
+ }
+ if (mSongId == null) {
+ if (other.mSongId != null) {
+ return false;
+ }
+ } else if (!mSongId.equals(other.mSongId)) {
+ return false;
+ }
+ if (mSongName == null) {
+ if (other.mSongName != null) {
+ return false;
+ }
+ } else if (!mSongName.equals(other.mSongName)) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String toString() {
+ return mSongName;
+ }
+}
diff --git a/src/com/andrew/apollo/preferences/SettingsFragment.java b/src/com/andrew/apollo/preferences/SettingsFragment.java
deleted file mode 100644
index 3f38b0a..0000000
--- a/src/com/andrew/apollo/preferences/SettingsFragment.java
+++ /dev/null
@@ -1,22 +0,0 @@
-
-package com.andrew.apollo.preferences;
-
-import android.os.Bundle;
-import android.preference.PreferenceFragment;
-
-import com.andrew.apollo.Constants;
-import com.andrew.apollo.R;
-
-public class SettingsFragment extends PreferenceFragment implements Constants {
-
- public SettingsFragment() {
- }
-
- @Override
- public void onActivityCreated(Bundle savedInstanceState) {
- // Load settings XML
- int preferencesResId = R.xml.settings;
- addPreferencesFromResource(preferencesResId);
- super.onActivityCreated(savedInstanceState);
- }
-}
diff --git a/src/com/andrew/apollo/preferences/SettingsHolder.java b/src/com/andrew/apollo/preferences/SettingsHolder.java
deleted file mode 100644
index f9896ce..0000000
--- a/src/com/andrew/apollo/preferences/SettingsHolder.java
+++ /dev/null
@@ -1,220 +0,0 @@
-/**
- *
- */
-
-package com.andrew.apollo.preferences;
-
-import java.util.List;
-
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.ServiceConnection;
-import android.content.SharedPreferences;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.IBinder;
-import android.preference.ListPreference;
-import android.preference.Preference;
-import android.preference.Preference.OnPreferenceChangeListener;
-import android.preference.PreferenceActivity;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import com.andrew.apollo.Constants;
-import com.andrew.apollo.IApolloService;
-import com.andrew.apollo.R;
-import com.andrew.apollo.activities.AudioPlayerHolder;
-import com.andrew.apollo.activities.MusicLibrary;
-import com.andrew.apollo.service.ApolloService;
-import com.andrew.apollo.service.ServiceToken;
-import com.andrew.apollo.utils.ApolloUtils;
-import com.andrew.apollo.utils.MusicUtils;
-import com.andrew.apollo.utils.ThemeUtils;
-import com.androidquery.AQuery;
-
-/**
- * @author Andrew Neal FIXME - Work on the IllegalStateException thrown when
- * using PreferenceFragment and theme chooser
- */
-@SuppressWarnings("deprecation")
-public class SettingsHolder extends PreferenceActivity implements ServiceConnection, Constants {
-
- // Service
- private ServiceToken mToken;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- // This should be called first thing
- super.onCreate(savedInstanceState);
-
- // Load settings XML
- int preferencesResId = R.xml.settings;
- addPreferencesFromResource(preferencesResId);
-
- // Load the theme chooser
- initThemeChooser();
-
- // ActionBar
- initActionBar();
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case android.R.id.home:
- finish();
- return true;
- }
- return super.onOptionsItemSelected(item);
- }
-
- @Override
- public void onServiceConnected(ComponentName name, IBinder obj) {
- MusicUtils.mService = IApolloService.Stub.asInterface(obj);
- }
-
- @Override
- public void onServiceDisconnected(ComponentName name) {
- MusicUtils.mService = null;
- }
-
- /**
- * Update the ActionBar as needed
- */
- private final BroadcastReceiver mMediaStatusReceiver = new BroadcastReceiver() {
-
- @Override
- public void onReceive(Context context, Intent intent) {
- // Update the ActionBar
- initActionBar();
- }
-
- };
-
- @Override
- protected void onStart() {
- // Bind to Service
- mToken = MusicUtils.bindToService(this, this);
-
- IntentFilter filter = new IntentFilter();
- filter.addAction(ApolloService.META_CHANGED);
- filter.addAction(ApolloService.QUEUE_CHANGED);
- filter.addAction(ApolloService.PLAYSTATE_CHANGED);
-
- registerReceiver(mMediaStatusReceiver, filter);
- super.onStart();
- }
-
- @Override
- protected void onStop() {
- // Unbind
- if (MusicUtils.mService != null)
- MusicUtils.unbindFromService(mToken);
-
- unregisterReceiver(mMediaStatusReceiver);
- super.onStop();
- }
-
- /**
- * Update the ActionBar
- */
- public void initActionBar() {
- // Custom ActionBar layout
- View view = getLayoutInflater().inflate(R.layout.custom_action_bar, null);
- // Show the ActionBar
- getActionBar().setCustomView(view);
- getActionBar().setTitle(R.string.settings);
- getActionBar().setDisplayHomeAsUpEnabled(true);
- getActionBar().setDisplayShowHomeEnabled(true);
- getActionBar().setDisplayShowCustomEnabled(true);
-
- ImageView mAlbumArt = (ImageView)view.findViewById(R.id.action_bar_album_art);
- TextView mTrackName = (TextView)view.findViewById(R.id.action_bar_track_name);
- TextView mAlbumName = (TextView)view.findViewById(R.id.action_bar_album_name);
-
- String url = ApolloUtils.getImageURL(MusicUtils.getAlbumName(), ALBUM_IMAGE, this);
- AQuery aq = new AQuery(this);
- mAlbumArt.setImageBitmap(aq.getCachedImage(url));
-
- mTrackName.setText(MusicUtils.getTrackName());
- mAlbumName.setText(MusicUtils.getAlbumName());
-
- view.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- Context context = v.getContext();
- context.startActivity(new Intent(context, AudioPlayerHolder.class));
- finish();
- }
- });
- }
-
- /**
- * @param v
- */
- public void applyTheme(View v) {
- ThemePreview themePreview = (ThemePreview)findPreference(THEME_PREVIEW);
- String packageName = themePreview.getValue().toString();
- ThemeUtils.setThemePackageName(this, packageName);
- Intent intent = new Intent();
- intent.setClass(this, MusicLibrary.class);
- intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
- startActivity(intent);
- finish();
- }
-
- /**
- * @param v
- */
- public void getThemes(View v) {
- Uri marketUri = Uri
- .parse("https://market.android.com/search?q=ApolloThemes&c=apps&featured=APP_STORE_SEARCH");
- Intent marketIntent = new Intent(Intent.ACTION_VIEW).setData(marketUri);
- startActivity(marketIntent);
- finish();
- }
-
- /**
- * Set up the theme chooser
- */
- public void initThemeChooser() {
- SharedPreferences sp = getPreferenceManager().getSharedPreferences();
- String themePackage = sp.getString(THEME_PACKAGE_NAME, APOLLO);
- ListPreference themeLp = (ListPreference)findPreference(THEME_PACKAGE_NAME);
- themeLp.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
- @Override
- public boolean onPreferenceChange(Preference preference, Object newValue) {
- ThemePreview themePreview = (ThemePreview)findPreference(THEME_PREVIEW);
- themePreview.setTheme(newValue.toString());
- return false;
- }
- });
-
- Intent intent = new Intent("com.andrew.apollo.THEMES");
- intent.addCategory("android.intent.category.DEFAULT");
- PackageManager pm = getPackageManager();
- List<ResolveInfo> themes = pm.queryIntentActivities(intent, 0);
- String[] entries = new String[themes.size() + 1];
- String[] values = new String[themes.size() + 1];
- entries[0] = APOLLO;
- values[0] = APOLLO;
- for (int i = 0; i < themes.size(); i++) {
- String appPackageName = (themes.get(i)).activityInfo.packageName.toString();
- String themeName = (themes.get(i)).loadLabel(pm).toString();
- entries[i + 1] = themeName;
- values[i + 1] = appPackageName;
- }
- themeLp.setEntries(entries);
- themeLp.setEntryValues(values);
- ThemePreview themePreview = (ThemePreview)findPreference(THEME_PREVIEW);
- themePreview.setTheme(themePackage);
- }
-}
diff --git a/src/com/andrew/apollo/preferences/ThemePreview.java b/src/com/andrew/apollo/preferences/ThemePreview.java
deleted file mode 100644
index 7a8ddf8..0000000
--- a/src/com/andrew/apollo/preferences/ThemePreview.java
+++ /dev/null
@@ -1,111 +0,0 @@
-
-package com.andrew.apollo.preferences;
-
-import android.content.Context;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.content.res.Resources;
-import android.graphics.drawable.Drawable;
-import android.preference.Preference;
-import android.text.Html;
-import android.text.method.LinkMovementMethod;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.Button;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import com.andrew.apollo.Constants;
-import com.andrew.apollo.R;
-
-public class ThemePreview extends Preference implements Constants {
- private CharSequence themeName;
-
- private CharSequence themePackageName;
-
- private CharSequence themeDescription;
-
- private Drawable themePreview;
-
- public ThemePreview(Context context) {
- super(context);
- }
-
- public ThemePreview(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- public ThemePreview(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- }
-
- @Override
- protected void onBindView(View view) {
- super.onBindView(view);
- if (themePackageName != null && themePackageName.toString().length() > 0) {
- TextView vThemeTitle = (TextView)view.findViewById(R.id.themeTitle);
- vThemeTitle.setText(themeName);
- TextView vThemeDescription = (TextView)view.findViewById(R.id.themeDescription);
- vThemeDescription.setMovementMethod(LinkMovementMethod.getInstance());
- vThemeDescription.setText(Html.fromHtml(themeDescription.toString()));
- ImageView vThemePreview = (ImageView)view.findViewById(R.id.themeIcon);
- if (themePreview != null)
- vThemePreview.setImageDrawable(themePreview);
- else
- vThemePreview.setImageResource(R.drawable.ic_launcher);
- vThemeTitle.setText(themeName);
-
- Button applyButton = (Button)view.findViewById(R.id.themeApply);
- applyButton.setEnabled(true);
- } else {
- Button applyButton = (Button)view.findViewById(R.id.themeApply);
- applyButton.setEnabled(false);
- }
- }
-
- /**
- * @param packageName
- */
- public void setTheme(CharSequence packageName) {
- themePackageName = packageName;
- themeName = null;
- themeDescription = null;
- if (themePreview != null)
- themePreview.setCallback(null);
- themePreview = null;
- if (!packageName.equals(APOLLO)) {
- Resources themeResources = null;
- try {
- themeResources = getContext().getPackageManager().getResourcesForApplication(
- packageName.toString());
- } catch (NameNotFoundException e) {
- e.printStackTrace();
- }
- if (themeResources != null) {
- int themeNameId = themeResources.getIdentifier(THEME_TITLE, "string",
- packageName.toString());
- if (themeNameId != 0) {
- themeName = themeResources.getString(themeNameId);
- }
- int themeDescriptionId = themeResources.getIdentifier(THEME_DESCRIPTION, "string",
- packageName.toString());
- if (themeDescriptionId != 0) {
- themeDescription = themeResources.getString(themeDescriptionId);
- }
- int themePreviewId = themeResources.getIdentifier(THEME_PREVIEW, "drawable",
- packageName.toString());
- if (themePreviewId != 0) {
- themePreview = themeResources.getDrawable(themePreviewId);
- }
- }
- }
- if (themeName == null)
- themeName = getContext().getResources().getString(R.string.apollo_themes);
- if (themeDescription == null)
- themeDescription = getContext().getResources().getString(R.string.themes);
- notifyChanged();
- }
-
- public CharSequence getValue() {
- return themePackageName;
- }
-}
diff --git a/src/com/andrew/apollo/provider/FavoritesStore.java b/src/com/andrew/apollo/provider/FavoritesStore.java
new file mode 100644
index 0000000..7365112
--- /dev/null
+++ b/src/com/andrew/apollo/provider/FavoritesStore.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.provider;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+/**
+ * This class is used to to create the database used to make the Favorites
+ * playlist.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+/**
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class FavoritesStore extends SQLiteOpenHelper {
+
+ /* Version constant to increment when the database should be rebuilt */
+ private static final int VERSION = 1;
+
+ /* Name of database file */
+ public static final String DATABASENAME = "favorites.db";
+
+ private static FavoritesStore sInstance = null;
+
+ /**
+ * Constructor of <code>FavoritesStore</code>
+ *
+ * @param context The {@link Context} to use
+ */
+ public FavoritesStore(final Context context) {
+ super(context, DATABASENAME, null, VERSION);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate(final SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE IF NOT EXISTS " + FavoriteColumns.NAME + " (" + FavoriteColumns.ID
+ + " LONG NOT NULL," + FavoriteColumns.SONGNAME + " TEXT NOT NULL,"
+ + FavoriteColumns.ALBUMNAME + " TEXT NOT NULL," + FavoriteColumns.ARTISTNAME
+ + " TEXT NOT NULL," + FavoriteColumns.PLAYCOUNT + " LONG NOT NULL);");
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) {
+ db.execSQL("DROP TABLE IF EXISTS " + FavoriteColumns.NAME);
+ onCreate(db);
+ }
+
+ /**
+ * @param context The {@link Context} to use
+ * @return A new instance of this class
+ */
+ public static final synchronized FavoritesStore getInstance(final Context context) {
+ if (sInstance == null) {
+ sInstance = new FavoritesStore(context.getApplicationContext());
+ }
+ return sInstance;
+ }
+
+ /**
+ * Used to store song Ids in our database
+ *
+ * @param songId The album's ID
+ * @param songName The song name
+ * @param albumName The album name
+ * @param artistName The artist name
+ */
+ public void addSongId(final Long songId, final String songName, final String albumName,
+ final String artistName) {
+ if (songId == null || songName == null || albumName == null || artistName == null) {
+ return;
+ }
+
+ final Long playCount = getPlayCount(songId);
+ final SQLiteDatabase database = getWritableDatabase();
+ final ContentValues values = new ContentValues(5);
+
+ database.beginTransaction();
+
+ values.put(FavoriteColumns.ID, songId);
+ values.put(FavoriteColumns.SONGNAME, songName);
+ values.put(FavoriteColumns.ALBUMNAME, albumName);
+ values.put(FavoriteColumns.ARTISTNAME, artistName);
+ values.put(FavoriteColumns.PLAYCOUNT, playCount != 0 ? playCount + 1 : 1);
+
+ database.delete(FavoriteColumns.NAME, FavoriteColumns.ID + " = ?", new String[] {
+ String.valueOf(songId)
+ });
+ database.insert(FavoriteColumns.NAME, null, values);
+ database.setTransactionSuccessful();
+ database.endTransaction();
+
+ }
+
+ /**
+ * Used to retrieve a single song Id from our database
+ *
+ * @param songId The song Id to reference
+ * @return The song Id
+ */
+ public Long getSongId(final Long songId) {
+ if (songId <= -1) {
+ return null;
+ }
+
+ final SQLiteDatabase database = getReadableDatabase();
+ final String[] projection = new String[] {
+ FavoriteColumns.ID, FavoriteColumns.SONGNAME, FavoriteColumns.ALBUMNAME,
+ FavoriteColumns.ARTISTNAME, FavoriteColumns.PLAYCOUNT
+ };
+ final String selection = FavoriteColumns.ID + "=?";
+ final String[] having = new String[] {
+ String.valueOf(songId)
+ };
+ Cursor cursor = database.query(FavoriteColumns.NAME, projection, selection, having, null,
+ null, null, null);
+ if (cursor != null && cursor.moveToFirst()) {
+ final Long id = cursor.getLong(cursor.getColumnIndexOrThrow(FavoriteColumns.ID));
+ cursor.close();
+ cursor = null;
+ return id;
+ }
+ if (cursor != null) {
+ cursor.close();
+ cursor = null;
+ }
+ return null;
+ }
+
+ /**
+ * Used to retrieve the play count
+ *
+ * @param songId The song Id to reference
+ * @return The play count for a song
+ */
+ public Long getPlayCount(final Long songId) {
+ if (songId <= -1) {
+ return null;
+ }
+
+ final SQLiteDatabase database = getReadableDatabase();
+ final String[] projection = new String[] {
+ FavoriteColumns.ID, FavoriteColumns.SONGNAME, FavoriteColumns.ALBUMNAME,
+ FavoriteColumns.ARTISTNAME, FavoriteColumns.PLAYCOUNT
+ };
+ final String selection = FavoriteColumns.ID + "=?";
+ final String[] having = new String[] {
+ String.valueOf(songId)
+ };
+ Cursor cursor = database.query(FavoriteColumns.NAME, projection, selection, having, null,
+ null, null, null);
+ if (cursor != null && cursor.moveToFirst()) {
+ final Long playCount = cursor.getLong(cursor
+ .getColumnIndexOrThrow(FavoriteColumns.PLAYCOUNT));
+ cursor.close();
+ cursor = null;
+ return playCount;
+ }
+ if (cursor != null) {
+ cursor.close();
+ cursor = null;
+ }
+
+ return (long)0;
+ }
+
+ /**
+ * Clear the cache.
+ *
+ * @param context The {@link Context} to use.
+ */
+ public static void deleteDatabase(final Context context) {
+ context.deleteDatabase(DATABASENAME);
+ }
+
+ /**
+ * Toggle the current song as favorite
+ */
+ public void toggleSong(final Long songId, final String songName, final String albumName,
+ final String artistName) {
+ if (getSongId(songId) == null) {
+ addSongId(songId, songName, albumName, artistName);
+ } else {
+ removeItem(songId);
+ }
+
+ }
+
+ /**
+ * @param item The song Id to remove
+ */
+ public void removeItem(final Long songId) {
+ final SQLiteDatabase database = getReadableDatabase();
+ database.delete(FavoriteColumns.NAME, FavoriteColumns.ID + " = ?", new String[] {
+ String.valueOf(songId)
+ });
+
+ }
+
+ public interface FavoriteColumns {
+
+ /* Table name */
+ public static final String NAME = "favorites";
+
+ /* Song IDs column */
+ public static final String ID = "songid";
+
+ /* Song name column */
+ public static final String SONGNAME = "songname";
+
+ /* Album name column */
+ public static final String ALBUMNAME = "albumname";
+
+ /* Artist name column */
+ public static final String ARTISTNAME = "artistname";
+
+ /* Play count column */
+ public static final String PLAYCOUNT = "playcount";
+ }
+
+}
diff --git a/src/com/andrew/apollo/provider/RecentStore.java b/src/com/andrew/apollo/provider/RecentStore.java
new file mode 100644
index 0000000..9cc8ac2
--- /dev/null
+++ b/src/com/andrew/apollo/provider/RecentStore.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.provider;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.text.TextUtils;
+
+import com.andrew.apollo.ui.activities.ProfileActivity;
+
+/**
+ * The {@link RecentlyListenedFragment} is used to display a a grid or list of
+ * recently listened to albums. In order to populate the this grid or list with
+ * the correct data, we keep a cache of the album ID, name, and time it was
+ * played to be retrieved later.
+ * <p>
+ * In {@link ProfileActivity}, when viewing the profile for an artist, the first
+ * image the carousel header is the last album the user listened to for that
+ * particular artist. That album is retrieved using
+ * {@link #getAlbumName(String)}.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class RecentStore extends SQLiteOpenHelper {
+
+ /* Version constant to increment when the database should be rebuilt */
+ private static final int VERSION = 1;
+
+ /* Name of database file */
+ public static final String DATABASENAME = "albumhistory.db";
+
+ private static RecentStore sInstance = null;
+
+ /**
+ * Constructor of <code>RecentStore</code>
+ *
+ * @param context The {@link Context} to use
+ */
+ public RecentStore(final Context context) {
+ super(context, DATABASENAME, null, VERSION);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate(final SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE IF NOT EXISTS " + RecentStoreColumns.NAME + " ("
+ + RecentStoreColumns.ID + " LONG NOT NULL," + RecentStoreColumns.ALBUMNAME
+ + " TEXT NOT NULL," + RecentStoreColumns.ARTISTNAME + " TEXT NOT NULL,"
+ + RecentStoreColumns.ALBUMSONGCOUNT + " TEXT NOT NULL,"
+ + RecentStoreColumns.ALBUMYEAR + " TEXT," + RecentStoreColumns.TIMEPLAYED
+ + " LONG NOT NULL);");
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) {
+ db.execSQL("DROP TABLE IF EXISTS " + RecentStoreColumns.NAME);
+ onCreate(db);
+ }
+
+ /**
+ * @param context The {@link Context} to use
+ * @return A new instance of this class.
+ */
+ public static final synchronized RecentStore getInstance(final Context context) {
+ if (sInstance == null) {
+ sInstance = new RecentStore(context.getApplicationContext());
+ }
+ return sInstance;
+ }
+
+ /**
+ * Used to store artist IDs in the database.
+ *
+ * @param albumIDdThe album's ID.
+ * @param albumName The album name.
+ * @param artistName The artist album name.
+ * @param songCount The number of tracks for the album.
+ * @param albumYear The year the album was released.
+ */
+ public void addAlbumId(final Long albumId, final String albumName, final String artistName,
+ final String songCount, final String albumYear) {
+ if (albumId == null || albumName == null || artistName == null || songCount == null) {
+ return;
+ }
+
+ final SQLiteDatabase database = getWritableDatabase();
+ final ContentValues values = new ContentValues(6);
+
+ database.beginTransaction();
+
+ values.put(RecentStoreColumns.ID, albumId);
+ values.put(RecentStoreColumns.ALBUMNAME, albumName);
+ values.put(RecentStoreColumns.ARTISTNAME, artistName);
+ values.put(RecentStoreColumns.ALBUMSONGCOUNT, songCount);
+ values.put(RecentStoreColumns.ALBUMYEAR, albumYear);
+ values.put(RecentStoreColumns.TIMEPLAYED, System.currentTimeMillis());
+
+ database.delete(RecentStoreColumns.NAME, RecentStoreColumns.ID + " = ?", new String[] {
+ String.valueOf(albumId)
+ });
+ database.insert(RecentStoreColumns.NAME, null, values);
+ database.setTransactionSuccessful();
+ database.endTransaction();
+
+ }
+
+ /**
+ * Used to retrieve the most recently listened album for an artist.
+ *
+ * @param key The key to reference.
+ * @return The most recently listened album for an artist.
+ */
+ public String getAlbumName(final String key) {
+ if (TextUtils.isEmpty(key)) {
+ return null;
+ }
+ final SQLiteDatabase database = getReadableDatabase();
+ final String[] projection = new String[] {
+ RecentStoreColumns.ID, RecentStoreColumns.ALBUMNAME, RecentStoreColumns.ARTISTNAME,
+ RecentStoreColumns.TIMEPLAYED
+ };
+ final String selection = RecentStoreColumns.ARTISTNAME + "=?";
+ final String[] having = new String[] {
+ key
+ };
+ Cursor cursor = database.query(RecentStoreColumns.NAME, projection, selection, having,
+ null, null, RecentStoreColumns.TIMEPLAYED + " DESC", null);
+ if (cursor != null && cursor.moveToFirst()) {
+ cursor.moveToFirst();
+ final String album = cursor.getString(cursor
+ .getColumnIndexOrThrow(RecentStoreColumns.ALBUMNAME));
+ cursor.close();
+ cursor = null;
+ return album;
+ }
+ if (cursor != null && !cursor.isClosed()) {
+ cursor.close();
+ cursor = null;
+ }
+
+ return null;
+ }
+
+ /**
+ * Clear the cache.
+ */
+ public void deleteDatabase() {
+ final SQLiteDatabase database = getReadableDatabase();
+ database.delete(RecentStoreColumns.NAME, null, null);
+ }
+
+ /**
+ * @param item The album Id to remove.
+ */
+ public void removeItem(final String albumId) {
+ final SQLiteDatabase database = getReadableDatabase();
+ database.delete(RecentStoreColumns.NAME, RecentStoreColumns.ID + " = ?", new String[] {
+ albumId
+ });
+
+ }
+
+ public interface RecentStoreColumns {
+
+ /* Table name */
+ public static final String NAME = "albumhistory";
+
+ /* Album IDs column */
+ public static final String ID = "albumid";
+
+ /* Album name column */
+ public static final String ALBUMNAME = "itemname";
+
+ /* Artist name column */
+ public static final String ARTISTNAME = "artistname";
+
+ /* Album song count column */
+ public static final String ALBUMSONGCOUNT = "albumsongcount";
+
+ /* Album year column. It's okay for this to be null */
+ public static final String ALBUMYEAR = "albumyear";
+
+ /* Time played column */
+ public static final String TIMEPLAYED = "timeplayed";
+ }
+}
diff --git a/src/com/andrew/apollo/recycler/RecycleHolder.java b/src/com/andrew/apollo/recycler/RecycleHolder.java
new file mode 100644
index 0000000..3d2ce83
--- /dev/null
+++ b/src/com/andrew/apollo/recycler/RecycleHolder.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.recycler;
+
+import android.view.View;
+import android.widget.AbsListView.RecyclerListener;
+
+import com.andrew.apollo.ui.MusicHolder;
+
+/**
+ * A @ {@link RecyclerListener} for {@link MusicHolder}'s views.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class RecycleHolder implements RecyclerListener {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onMovedToScrapHeap(final View view) {
+ MusicHolder holder = (MusicHolder)view.getTag();
+ if (holder == null) {
+ holder = new MusicHolder(view);
+ view.setTag(holder);
+ }
+
+ // Release mBackground's reference
+ if (holder.mBackground.get() != null) {
+ holder.mBackground.get().setImageDrawable(null);
+ holder.mBackground.get().setImageBitmap(null);
+ }
+
+ // Release mImage's reference
+ if (holder.mImage.get() != null) {
+ holder.mImage.get().setImageDrawable(null);
+ holder.mImage.get().setImageBitmap(null);
+ }
+
+ // Release mLineOne's reference
+ if (holder.mLineOne.get() != null) {
+ holder.mLineOne.get().setText(null);
+ }
+
+ // Release mLineTwo's reference
+ if (holder.mLineTwo.get() != null) {
+ holder.mLineTwo.get().setText(null);
+ }
+
+ // Release mLineThree's reference
+ if (holder.mLineThree.get() != null) {
+ holder.mLineThree.get().setText(null);
+ }
+ }
+
+}
diff --git a/src/com/andrew/apollo/service/ApolloService.java b/src/com/andrew/apollo/service/ApolloService.java
deleted file mode 100644
index bc2c142..0000000
--- a/src/com/andrew/apollo/service/ApolloService.java
+++ /dev/null
@@ -1,2280 +0,0 @@
-/* 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.
- */
-
-package com.andrew.apollo.service;
-
-import java.io.IOException;
-import java.lang.ref.WeakReference;
-import java.util.Random;
-import java.util.Vector;
-
-import android.app.Notification;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.app.Service;
-import android.appwidget.AppWidgetManager;
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
-import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.SharedPreferences;
-import android.content.SharedPreferences.Editor;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteException;
-import android.graphics.Bitmap;
-import android.media.AudioManager;
-import android.media.AudioManager.OnAudioFocusChangeListener;
-import android.media.MediaMetadataRetriever;
-import android.media.MediaPlayer;
-import android.media.RemoteControlClient;
-import android.media.RemoteControlClient.MetadataEditor;
-import android.media.audiofx.AudioEffect;
-import android.net.Uri;
-import android.os.Handler;
-import android.os.IBinder;
-import android.os.Message;
-import android.os.PowerManager;
-import android.os.PowerManager.WakeLock;
-import android.os.RemoteException;
-import android.os.SystemClock;
-import android.provider.BaseColumns;
-import android.provider.MediaStore;
-import android.provider.MediaStore.Audio;
-import android.provider.MediaStore.Audio.AudioColumns;
-import android.provider.MediaStore.MediaColumns;
-import android.util.Log;
-import android.view.KeyEvent;
-import android.view.View;
-import android.widget.RemoteViews;
-import android.widget.Toast;
-
-import com.andrew.apollo.Constants;
-import com.andrew.apollo.IApolloService;
-import com.andrew.apollo.R;
-import com.andrew.apollo.app.widgets.AppWidget11;
-import com.andrew.apollo.app.widgets.AppWidget41;
-import com.andrew.apollo.app.widgets.AppWidget42;
-import com.andrew.apollo.utils.ApolloUtils;
-import com.andrew.apollo.utils.MusicUtils;
-import com.andrew.apollo.utils.SharedPreferencesCompat;
-import com.androidquery.AQuery;
-
-public class ApolloService extends Service implements Constants {
- /**
- * used to specify whether enqueue() should start playing the new list of
- * files right away, next or once all the currently queued files have been
- * played
- */
- public static final int NOW = 1;
-
- public static final int NEXT = 2;
-
- public static final int LAST = 3;
-
- public static final int PLAYBACKSERVICE_STATUS = 1;
-
- public static final int SHUFFLE_NONE = 0;
-
- public static final int SHUFFLE_NORMAL = 1;
-
- public static final int SHUFFLE_AUTO = 2;
-
- public static final int REPEAT_NONE = 0;
-
- public static final int REPEAT_CURRENT = 1;
-
- public static final int REPEAT_ALL = 2;
-
- public static final String PLAYSTATE_CHANGED = "com.andrew.apollo.playstatechanged";
-
- public static final String META_CHANGED = "com.andrew.apollo.metachanged";
-
- public static final String FAVORITE_CHANGED = "com.andrew.apollo.favoritechanged";
-
- public static final String QUEUE_CHANGED = "com.andrew.apollo.queuechanged";
-
- public static final String REPEATMODE_CHANGED = "com.andrew.apollo.repeatmodechanged";
-
- public static final String SHUFFLEMODE_CHANGED = "com.andrew.apollo.shufflemodechanged";
-
- public static final String PROGRESSBAR_CHANGED = "com.andrew.apollo.progressbarchnaged";
-
- public static final String REFRESH_PROGRESSBAR = "com.andrew.apollo.refreshprogessbar";
-
- public static final String CYCLEREPEAT_ACTION = "com.andrew.apollo.musicservicecommand.cyclerepeat";
-
- public static final String TOGGLESHUFFLE_ACTION = "com.andrew.apollo.musicservicecommand.toggleshuffle";
-
- public static final String SERVICECMD = "com.andrew.apollo.musicservicecommand";
-
- public static final String CMDNAME = "command";
-
- public static final String CMDTOGGLEPAUSE = "togglepause";
-
- public static final String CMDSTOP = "stop";
-
- public static final String CMDPAUSE = "pause";
-
- public static final String CMDPLAY = "play";
-
- public static final String CMDPREVIOUS = "previous";
-
- public static final String CMDNEXT = "next";
-
- public static final String CMDNOTIF = "buttonId";
-
- public static final String CMDTOGGLEFAVORITE = "togglefavorite";
-
- public static final String CMDCYCLEREPEAT = "cyclerepeat";
-
- public static final String CMDTOGGLESHUFFLE = "toggleshuffle";
-
- public static final String TOGGLEPAUSE_ACTION = "com.andrew.apollo.musicservicecommand.togglepause";
-
- public static final String PAUSE_ACTION = "com.andrew.apollo.musicservicecommand.pause";
-
- public static final String PREVIOUS_ACTION = "com.andrew.apollo.musicservicecommand.previous";
-
- public static final String NEXT_ACTION = "com.andrew.apollo.musicservicecommand.next";
-
- private static final int TRACK_ENDED = 1;
-
- private static final int RELEASE_WAKELOCK = 2;
-
- private static final int SERVER_DIED = 3;
-
- private static final int FOCUSCHANGE = 4;
-
- private static final int FADEDOWN = 5;
-
- private static final int FADEUP = 6;
-
- private static final int MAX_HISTORY_SIZE = 100;
-
- private Notification status;
-
- private MultiPlayer mPlayer;
-
- private String mFileToPlay;
-
- private int mShuffleMode = SHUFFLE_NONE;
-
- private int mRepeatMode = REPEAT_NONE;
-
- private int mMediaMountedCount = 0;
-
- private long[] mAutoShuffleList = null;
-
- private long[] mPlayList = null;
-
- private int mPlayListLen = 0;
-
- private final Vector<Integer> mHistory = new Vector<Integer>(MAX_HISTORY_SIZE);
-
- private Cursor mCursor;
-
- private int mPlayPos = -1;
-
- private static final String LOGTAG = "MediaPlaybackService";
-
- private final Shuffler mRand = new Shuffler();
-
- private int mOpenFailedCounter = 0;
-
- String[] mCursorCols = new String[] {
- "audio._id AS _id", MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.ALBUM,
- MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.DATA,
- MediaStore.Audio.Media.MIME_TYPE, MediaStore.Audio.Media.ALBUM_ID,
- MediaStore.Audio.Media.ARTIST_ID, MediaStore.Audio.Media.IS_PODCAST,
- MediaStore.Audio.Media.BOOKMARK
- };
-
- private final static int IDCOLIDX = 0;
-
- private final static int PODCASTCOLIDX = 8;
-
- private final static int BOOKMARKCOLIDX = 9;
-
- private BroadcastReceiver mUnmountReceiver = null;
-
- private WakeLock mWakeLock;
-
- private int mServiceStartId = -1;
-
- private boolean mServiceInUse = false;
-
- private boolean mIsSupposedToBePlaying = false;
-
- private boolean mQuietMode = false;
-
- private AudioManager mAudioManager;
-
- private boolean mQueueIsSaveable = true;
-
- // used to track what type of audio focus loss caused the playback to pause
- private boolean mPausedByTransientLossOfFocus = false;
-
- private SharedPreferences mPreferences;
-
- // We use this to distinguish between different cards when saving/restoring
- // playlists.
- // This will have to change if we want to support multiple simultaneous
- // cards.
- private int mCardId;
-
- private final AppWidget11 mAppWidgetProvider1x1 = AppWidget11.getInstance();
-
- private final AppWidget42 mAppWidgetProvider4x2 = AppWidget42.getInstance();
-
- private final AppWidget41 mAppWidgetProvider4x1 = AppWidget41.getInstance();
-
- // interval after which we stop the service when idle
- private static final int IDLE_DELAY = 60000;
-
- private RemoteControlClient mRemoteControlClient;
-
- private final Handler mMediaplayerHandler = new Handler() {
- float mCurrentVolume = 1.0f;
-
- @Override
- public void handleMessage(Message msg) {
- switch (msg.what) {
- case FADEDOWN:
- mCurrentVolume -= .05f;
- if (mCurrentVolume > .2f) {
- mMediaplayerHandler.sendEmptyMessageDelayed(FADEDOWN, 10);
- } else {
- mCurrentVolume = .2f;
- }
- mPlayer.setVolume(mCurrentVolume);
- break;
- case FADEUP:
- mCurrentVolume += .01f;
- if (mCurrentVolume < 1.0f) {
- mMediaplayerHandler.sendEmptyMessageDelayed(FADEUP, 10);
- } else {
- mCurrentVolume = 1.0f;
- }
- mPlayer.setVolume(mCurrentVolume);
- break;
- case SERVER_DIED:
- if (mIsSupposedToBePlaying) {
- next(true);
- } else {
- // the server died when we were idle, so just
- // reopen the same song (it will start again
- // from the beginning though when the user
- // restarts)
- openCurrent();
- }
- break;
- case TRACK_ENDED:
- if (mRepeatMode == REPEAT_CURRENT) {
- seek(0);
- play();
- } else {
- next(false);
- }
- break;
- case RELEASE_WAKELOCK:
- mWakeLock.release();
- break;
-
- case FOCUSCHANGE:
- // This code is here so we can better synchronize it with
- // the code that
- // handles fade-in
- switch (msg.arg1) {
- case AudioManager.AUDIOFOCUS_LOSS:
- Log.v(LOGTAG, "AudioFocus: received AUDIOFOCUS_LOSS");
- if (isPlaying()) {
- mPausedByTransientLossOfFocus = false;
- }
- pause();
- break;
- case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
- mMediaplayerHandler.removeMessages(FADEUP);
- mMediaplayerHandler.sendEmptyMessage(FADEDOWN);
- break;
- case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
- Log.v(LOGTAG, "AudioFocus: received AUDIOFOCUS_LOSS_TRANSIENT");
- if (isPlaying()) {
- mPausedByTransientLossOfFocus = true;
- }
- pause();
- break;
- case AudioManager.AUDIOFOCUS_GAIN:
- Log.v(LOGTAG, "AudioFocus: received AUDIOFOCUS_GAIN");
- if (!isPlaying() && mPausedByTransientLossOfFocus) {
- mPausedByTransientLossOfFocus = false;
- mCurrentVolume = 0f;
- mPlayer.setVolume(mCurrentVolume);
- play(); // also queues a fade-in
- } else {
- mMediaplayerHandler.removeMessages(FADEDOWN);
- mMediaplayerHandler.sendEmptyMessage(FADEUP);
- }
- break;
- default:
- Log.e(LOGTAG, "Unknown audio focus change code");
- }
- break;
-
- default:
- break;
- }
- }
- };
-
- private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- String action = intent.getAction();
- String cmd = intent.getStringExtra("command");
- if (CMDNEXT.equals(cmd) || NEXT_ACTION.equals(action)) {
- next(true);
- } else if (CMDPREVIOUS.equals(cmd) || PREVIOUS_ACTION.equals(action)) {
- prev();
- } else if (CMDTOGGLEPAUSE.equals(cmd) || TOGGLEPAUSE_ACTION.equals(action)) {
- if (isPlaying()) {
- pause();
- mPausedByTransientLossOfFocus = false;
- } else {
- play();
- }
- } else if (CMDPAUSE.equals(cmd) || PAUSE_ACTION.equals(action)) {
- pause();
- mPausedByTransientLossOfFocus = false;
- } else if (CMDPLAY.equals(cmd)) {
- play();
- } else if (CMDSTOP.equals(cmd)) {
- pause();
- mPausedByTransientLossOfFocus = false;
- seek(0);
- } else if (CMDTOGGLEFAVORITE.equals(cmd)) {
- if (!isFavorite()) {
- addToFavorites();
- } else {
- removeFromFavorites();
- }
- } else if (CMDCYCLEREPEAT.equals(cmd) || CYCLEREPEAT_ACTION.equals(action)) {
- cycleRepeat();
- } else if (CMDTOGGLESHUFFLE.equals(cmd) || TOGGLESHUFFLE_ACTION.equals(action)) {
- toggleShuffle();
- } else if (AppWidget42.CMDAPPWIDGETUPDATE.equals(cmd)) {
- int[] appWidgetIds = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
- mAppWidgetProvider4x2.performUpdate(ApolloService.this, appWidgetIds);
- } else if (AppWidget41.CMDAPPWIDGETUPDATE.equals(cmd)) {
- int[] appWidgetIds = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
- mAppWidgetProvider4x1.performUpdate(ApolloService.this, appWidgetIds);
- } else if (AppWidget11.CMDAPPWIDGETUPDATE.equals(cmd)) {
- int[] appWidgetIds = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
- mAppWidgetProvider1x1.performUpdate(ApolloService.this, appWidgetIds);
- }
- }
- };
-
- private final OnAudioFocusChangeListener mAudioFocusListener = new OnAudioFocusChangeListener() {
- @Override
- public void onAudioFocusChange(int focusChange) {
- mMediaplayerHandler.obtainMessage(FOCUSCHANGE, focusChange, 0).sendToTarget();
- }
- };
-
- public ApolloService() {
- }
-
- @Override
- public void onCreate() {
- super.onCreate();
-
- mAudioManager = (AudioManager)getSystemService(Context.AUDIO_SERVICE);
- ComponentName rec = new ComponentName(getPackageName(),
- MediaButtonIntentReceiver.class.getName());
- mAudioManager.registerMediaButtonEventReceiver(rec);
- Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
- mediaButtonIntent.setComponent(rec);
- PendingIntent mediaPendingIntent = PendingIntent.getBroadcast(getApplicationContext(), 0,
- mediaButtonIntent, PendingIntent.FLAG_UPDATE_CURRENT);
- mRemoteControlClient = new RemoteControlClient(mediaPendingIntent);
- mAudioManager.registerRemoteControlClient(mRemoteControlClient);
-
- int flags = RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS
- | RemoteControlClient.FLAG_KEY_MEDIA_NEXT | RemoteControlClient.FLAG_KEY_MEDIA_PLAY
- | RemoteControlClient.FLAG_KEY_MEDIA_PAUSE
- | RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE
- | RemoteControlClient.FLAG_KEY_MEDIA_STOP;
- mRemoteControlClient.setTransportControlFlags(flags);
-
- mPreferences = getSharedPreferences(APOLLO_PREFERENCES, MODE_WORLD_READABLE
- | MODE_WORLD_WRITEABLE);
- mCardId = MusicUtils.getCardId(this);
-
- registerExternalStorageListener();
-
- // Needs to be done in this thread, since otherwise
- // ApplicationContext.getPowerManager() crashes.
- mPlayer = new MultiPlayer();
- mPlayer.setHandler(mMediaplayerHandler);
-
- reloadQueue();
- notifyChange(QUEUE_CHANGED);
- notifyChange(META_CHANGED);
-
- IntentFilter commandFilter = new IntentFilter();
- commandFilter.addAction(SERVICECMD);
- commandFilter.addAction(TOGGLEPAUSE_ACTION);
- commandFilter.addAction(PAUSE_ACTION);
- commandFilter.addAction(NEXT_ACTION);
- commandFilter.addAction(PREVIOUS_ACTION);
- commandFilter.addAction(CYCLEREPEAT_ACTION);
- commandFilter.addAction(TOGGLESHUFFLE_ACTION);
- registerReceiver(mIntentReceiver, commandFilter);
-
- PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
- mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, this.getClass().getName());
- mWakeLock.setReferenceCounted(false);
-
- // If the service was idle, but got killed before it stopped itself, the
- // system will relaunch it. Make sure it gets stopped again in that
- // case.
- Message msg = mDelayedStopHandler.obtainMessage();
- mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY);
- }
-
- @Override
- public void onDestroy() {
- // Check that we're not being destroyed while something is still
- // playing.
- if (mIsSupposedToBePlaying) {
- Log.e(LOGTAG, "Service being destroyed while still playing.");
- }
- // release all MediaPlayer resources, including the native player and
- // wakelocks
- Intent i = new Intent(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION);
- i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, getAudioSessionId());
- i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName());
- sendBroadcast(i);
- mPlayer.release();
- mPlayer = null;
-
- mAudioManager.abandonAudioFocus(mAudioFocusListener);
- mAudioManager.unregisterRemoteControlClient(mRemoteControlClient);
-
- // make sure there aren't any other messages coming
- mDelayedStopHandler.removeCallbacksAndMessages(null);
- mMediaplayerHandler.removeCallbacksAndMessages(null);
-
- if (mCursor != null) {
- mCursor.close();
- mCursor = null;
- }
-
- unregisterReceiver(mIntentReceiver);
- if (mUnmountReceiver != null) {
- unregisterReceiver(mUnmountReceiver);
- mUnmountReceiver = null;
- }
- mWakeLock.release();
- super.onDestroy();
- }
-
- private final char hexdigits[] = new char[] {
- '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
- };
-
- private void saveQueue(boolean full) {
- if (!mQueueIsSaveable) {
- return;
- }
-
- Editor ed = mPreferences.edit();
- // long start = System.currentTimeMillis();
- if (full) {
- StringBuilder q = new StringBuilder();
-
- // The current playlist is saved as a list of "reverse hexadecimal"
- // numbers, which we can generate faster than normal decimal or
- // hexadecimal numbers, which in turn allows us to save the playlist
- // more often without worrying too much about performance.
- // (saving the full state takes about 40 ms under no-load conditions
- // on the phone)
- int len = mPlayListLen;
- for (int i = 0; i < len; i++) {
- long n = mPlayList[i];
- if (n < 0) {
- continue;
- } else if (n == 0) {
- q.append("0;");
- } else {
- while (n != 0) {
- int digit = (int)(n & 0xf);
- n >>>= 4;
- q.append(hexdigits[digit]);
- }
- q.append(";");
- }
- }
- // Log.i("@@@@ service", "created queue string in " +
- // (System.currentTimeMillis() - start) + " ms");
- ed.putString("queue", q.toString());
- ed.putInt("cardid", mCardId);
- if (mShuffleMode != SHUFFLE_NONE) {
- // In shuffle mode we need to save the history too
- len = mHistory.size();
- q.setLength(0);
- for (int i = 0; i < len; i++) {
- int n = mHistory.get(i);
- if (n == 0) {
- q.append("0;");
- } else {
- while (n != 0) {
- int digit = (n & 0xf);
- n >>>= 4;
- q.append(hexdigits[digit]);
- }
- q.append(";");
- }
- }
- ed.putString("history", q.toString());
- }
- }
- ed.putInt("curpos", mPlayPos);
- if (mPlayer.isInitialized()) {
- ed.putLong("seekpos", mPlayer.position());
- }
- ed.putInt("repeatmode", mRepeatMode);
- ed.putInt("shufflemode", mShuffleMode);
- SharedPreferencesCompat.apply(ed);
-
- // Log.i("@@@@ service", "saved state in " + (System.currentTimeMillis()
- // - start) + " ms");
- }
-
- private void reloadQueue() {
- String q = null;
-
- int id = mCardId;
- if (mPreferences.contains("cardid")) {
- id = mPreferences.getInt("cardid", ~mCardId);
- }
- if (id == mCardId) {
- // Only restore the saved playlist if the card is still
- // the same one as when the playlist was saved
- q = mPreferences.getString("queue", "");
- }
- int qlen = q != null ? q.length() : 0;
- if (qlen > 1) {
- // Log.i("@@@@ service", "loaded queue: " + q);
- int plen = 0;
- int n = 0;
- int shift = 0;
- for (int i = 0; i < qlen; i++) {
- char c = q.charAt(i);
- if (c == ';') {
- ensurePlayListCapacity(plen + 1);
- mPlayList[plen] = n;
- plen++;
- n = 0;
- shift = 0;
- } else {
- if (c >= '0' && c <= '9') {
- n += ((c - '0') << shift);
- } else if (c >= 'a' && c <= 'f') {
- n += ((10 + c - 'a') << shift);
- } else {
- // bogus playlist data
- plen = 0;
- break;
- }
- shift += 4;
- }
- }
- mPlayListLen = plen;
-
- int pos = mPreferences.getInt("curpos", 0);
- if (pos < 0 || pos >= mPlayListLen) {
- // The saved playlist is bogus, discard it
- mPlayListLen = 0;
- return;
- }
- mPlayPos = pos;
-
- // When reloadQueue is called in response to a card-insertion,
- // we might not be able to query the media provider right away.
- // To deal with this, try querying for the current file, and if
- // that fails, wait a while and try again. If that too fails,
- // assume there is a problem and don't restore the state.
- Cursor crsr = MusicUtils.query(this, Audio.Media.EXTERNAL_CONTENT_URI, new String[] {
- "_id"
- }, "_id=" + mPlayList[mPlayPos], null, null);
- if (crsr == null || crsr.getCount() == 0) {
- // wait a bit and try again
- SystemClock.sleep(3000);
- crsr = getContentResolver().query(Audio.Media.EXTERNAL_CONTENT_URI, mCursorCols,
- "_id=" + mPlayList[mPlayPos], null, null);
- }
- if (crsr != null) {
- crsr.close();
- }
-
- // Make sure we don't auto-skip to the next song, since that
- // also starts playback. What could happen in that case is:
- // - music is paused
- // - go to UMS and delete some files, including the currently
- // playing one
- // - come back from UMS
- // (time passes)
- // - music app is killed for some reason (out of memory)
- // - music service is restarted, service restores state, doesn't
- // find
- // the "current" file, goes to the next and: playback starts on its
- // own, potentially at some random inconvenient time.
- mOpenFailedCounter = 20;
- mQuietMode = true;
- openCurrent();
- mQuietMode = false;
- if (!mPlayer.isInitialized()) {
- // couldn't restore the saved state
- mPlayListLen = 0;
- return;
- }
-
- long seekpos = mPreferences.getLong("seekpos", 0);
- seek(seekpos >= 0 && seekpos < duration() ? seekpos : 0);
- Log.d(LOGTAG, "restored queue, currently at position " + position() + "/" + duration()
- + " (requested " + seekpos + ")");
-
- int repmode = mPreferences.getInt("repeatmode", REPEAT_NONE);
- if (repmode != REPEAT_ALL && repmode != REPEAT_CURRENT) {
- repmode = REPEAT_NONE;
- }
- mRepeatMode = repmode;
-
- int shufmode = mPreferences.getInt("shufflemode", SHUFFLE_NONE);
- if (shufmode != SHUFFLE_AUTO && shufmode != SHUFFLE_NORMAL) {
- shufmode = SHUFFLE_NONE;
- }
- if (shufmode != SHUFFLE_NONE) {
- // in shuffle mode we need to restore the history too
- q = mPreferences.getString("history", "");
- qlen = q != null ? q.length() : 0;
- if (qlen > 1) {
- plen = 0;
- n = 0;
- shift = 0;
- mHistory.clear();
- for (int i = 0; i < qlen; i++) {
- char c = q.charAt(i);
- if (c == ';') {
- if (n >= mPlayListLen) {
- // bogus history data
- mHistory.clear();
- break;
- }
- mHistory.add(n);
- n = 0;
- shift = 0;
- } else {
- if (c >= '0' && c <= '9') {
- n += ((c - '0') << shift);
- } else if (c >= 'a' && c <= 'f') {
- n += ((10 + c - 'a') << shift);
- } else {
- // bogus history data
- mHistory.clear();
- break;
- }
- shift += 4;
- }
- }
- }
- }
- if (shufmode == SHUFFLE_AUTO) {
- if (!makeAutoShuffleList()) {
- shufmode = SHUFFLE_NONE;
- }
- }
- mShuffleMode = shufmode;
- }
- }
-
- @Override
- public IBinder onBind(Intent intent) {
- mDelayedStopHandler.removeCallbacksAndMessages(null);
- mServiceInUse = true;
- return mBinder;
- }
-
- @Override
- public void onRebind(Intent intent) {
- mDelayedStopHandler.removeCallbacksAndMessages(null);
- mServiceInUse = true;
- }
-
- @Override
- public int onStartCommand(Intent intent, int flags, int startId) {
- mServiceStartId = startId;
- mDelayedStopHandler.removeCallbacksAndMessages(null);
-
- if (intent != null) {
- String action = intent.getAction();
- String cmd = intent.getStringExtra("command");
-
- if (CMDNEXT.equals(cmd) || NEXT_ACTION.equals(action)) {
- next(true);
- } else if (CMDPREVIOUS.equals(cmd) || PREVIOUS_ACTION.equals(action)) {
- if (position() < 2000) {
- prev();
- } else {
- seek(0);
- play();
- }
- } else if (CMDTOGGLEPAUSE.equals(cmd) || TOGGLEPAUSE_ACTION.equals(action)) {
- if (mIsSupposedToBePlaying) {
- pause();
- mPausedByTransientLossOfFocus = false;
- } else {
- play();
- }
- } else if (CMDPAUSE.equals(cmd) || PAUSE_ACTION.equals(action)) {
- pause();
- mPausedByTransientLossOfFocus = false;
- } else if (CMDPLAY.equals(cmd)) {
- play();
- } else if (CMDSTOP.equals(cmd)) {
- pause();
- if (intent.getIntExtra(CMDNOTIF, 0) == 3) {
- stopForeground(true);
- }
- mPausedByTransientLossOfFocus = false;
- seek(0);
- } else if (CMDTOGGLEFAVORITE.equals(cmd)) {
- if (!isFavorite()) {
- addToFavorites();
- } else {
- removeFromFavorites();
- }
- } else if (CMDCYCLEREPEAT.equals(cmd) || CYCLEREPEAT_ACTION.equals(action)) {
- cycleRepeat();
- } else if (CMDTOGGLESHUFFLE.equals(cmd) || TOGGLESHUFFLE_ACTION.equals(action)) {
- toggleShuffle();
- }
- }
-
- // make sure the service will shut down on its own if it was
- // just started but not bound to and nothing is playing
- mDelayedStopHandler.removeCallbacksAndMessages(null);
- Message msg = mDelayedStopHandler.obtainMessage();
- mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY);
- return START_STICKY;
- }
-
- @Override
- public boolean onUnbind(Intent intent) {
- mServiceInUse = false;
-
- // Take a snapshot of the current playlist
- saveQueue(true);
-
- if (mIsSupposedToBePlaying || mPausedByTransientLossOfFocus) {
- // something is currently playing, or will be playing once
- // an in-progress action requesting audio focus ends, so don't stop
- // the service now.
- return true;
- }
-
- // If there is a playlist but playback is paused, then wait a while
- // before stopping the service, so that pause/resume isn't slow.
- // Also delay stopping the service if we're transitioning between
- // tracks.
- if (mPlayListLen > 0 || mMediaplayerHandler.hasMessages(TRACK_ENDED)) {
- Message msg = mDelayedStopHandler.obtainMessage();
- mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY);
- return true;
- }
-
- // No active playlist, OK to stop the service right now
- stopSelf(mServiceStartId);
- return true;
- }
-
- private final Handler mDelayedStopHandler = new Handler() {
- @Override
- public void handleMessage(Message msg) {
- // Check again to make sure nothing is playing right now
- if (isPlaying() || mPausedByTransientLossOfFocus || mServiceInUse
- || mMediaplayerHandler.hasMessages(TRACK_ENDED)) {
- return;
- }
- // save the queue again, because it might have changed
- // since the user exited the music app (because of
- // party-shuffle or because the play-position changed)
- saveQueue(true);
- stopSelf(mServiceStartId);
- }
- };
-
- /**
- * Called when we receive a ACTION_MEDIA_EJECT notification.
- *
- * @param storagePath path to mount point for the removed media
- */
- public void closeExternalStorageFiles(String storagePath) {
- // stop playback and clean up if the SD card is going to be unmounted.
- stop(true);
- notifyChange(QUEUE_CHANGED);
- notifyChange(META_CHANGED);
- }
-
- /**
- * Registers an intent to listen for ACTION_MEDIA_EJECT notifications. The
- * intent will call closeExternalStorageFiles() if the external media is
- * going to be ejected, so applications can clean up any files they have
- * open.
- */
- public void registerExternalStorageListener() {
- if (mUnmountReceiver == null) {
- mUnmountReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- String action = intent.getAction();
- if (action.equals(Intent.ACTION_MEDIA_EJECT)) {
- saveQueue(true);
- mQueueIsSaveable = false;
- closeExternalStorageFiles(intent.getData().getPath());
- } else if (action.equals(Intent.ACTION_MEDIA_MOUNTED)) {
- mMediaMountedCount++;
- mCardId = MusicUtils.getCardId(ApolloService.this);
- reloadQueue();
- mQueueIsSaveable = true;
- notifyChange(QUEUE_CHANGED);
- notifyChange(META_CHANGED);
- }
- }
- };
- IntentFilter iFilter = new IntentFilter();
- iFilter.addAction(Intent.ACTION_MEDIA_EJECT);
- iFilter.addAction(Intent.ACTION_MEDIA_MOUNTED);
- iFilter.addDataScheme(DATA_SCHEME);
- registerReceiver(mUnmountReceiver, iFilter);
- }
- }
-
- /**
- * Notify the change-receivers that something has changed. The intent that
- * is sent contains the following data for the currently playing track: "id"
- * - Integer: the database row ID "artist" - String: the name of the artist
- * "album" - String: the name of the album "track" - String: the name of the
- * track The intent has an action that is one of
- * "com.andrew.apollo.metachanged" "com.andrew.apollo.queuechanged",
- * "com.andrew.apollo.playbackcomplete" "com.andrew.apollo.playstatechanged"
- * respectively indicating that a new track has started playing, that the
- * playback queue has changed, that playback has stopped because the last
- * file in the list has been played, or that the play-state changed
- * (paused/resumed).
- */
- private void notifyChange(String what) {
-
- Intent i = new Intent(what);
- i.putExtra("id", Long.valueOf(getAudioId()));
- i.putExtra("artist", getArtistName());
- i.putExtra("album", getAlbumName());
- i.putExtra("track", getTrackName());
- i.putExtra("playing", mIsSupposedToBePlaying);
- i.putExtra("isfavorite", isFavorite());
- sendStickyBroadcast(i);
-
- if (what.equals(PLAYSTATE_CHANGED)) {
- mRemoteControlClient
- .setPlaybackState(mIsSupposedToBePlaying ? RemoteControlClient.PLAYSTATE_PLAYING
- : RemoteControlClient.PLAYSTATE_PAUSED);
- } else if (what.equals(META_CHANGED)) {
- RemoteControlClient.MetadataEditor ed = mRemoteControlClient.editMetadata(true);
- ed.putString(MediaMetadataRetriever.METADATA_KEY_TITLE, getTrackName());
- ed.putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, getAlbumName());
- ed.putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, getArtistName());
- ed.putLong(MediaMetadataRetriever.METADATA_KEY_DURATION, duration());
- AQuery aq = new AQuery(this);
- Bitmap b = aq
- .getCachedImage(ApolloUtils.getImageURL(getAlbumName(), ALBUM_IMAGE, this));
- if (b != null) {
- ed.putBitmap(MetadataEditor.BITMAP_KEY_ARTWORK, b);
- }
- ed.apply();
- }
-
- if (what.equals(QUEUE_CHANGED)) {
- saveQueue(true);
- } else {
- saveQueue(false);
- }
-
- mAppWidgetProvider1x1.notifyChange(this, what);
- mAppWidgetProvider4x1.notifyChange(this, what);
- mAppWidgetProvider4x2.notifyChange(this, what);
-
- }
-
- private void ensurePlayListCapacity(int size) {
- if (mPlayList == null || size > mPlayList.length) {
- // reallocate at 2x requested size so we don't
- // need to grow and copy the array for every
- // insert
- long[] newlist = new long[size * 2];
- int len = mPlayList != null ? mPlayList.length : mPlayListLen;
- for (int i = 0; i < len; i++) {
- newlist[i] = mPlayList[i];
- }
- mPlayList = newlist;
- }
- // FIXME: shrink the array when the needed size is much smaller
- // than the allocated size
- }
-
- // insert the list of songs at the specified position in the playlist
- private void addToPlayList(long[] list, int position) {
- int addlen = list.length;
- if (position < 0) { // overwrite
- mPlayListLen = 0;
- position = 0;
- }
- ensurePlayListCapacity(mPlayListLen + addlen);
- if (position > mPlayListLen) {
- position = mPlayListLen;
- }
-
- // move part of list after insertion point
- int tailsize = mPlayListLen - position;
- for (int i = tailsize; i > 0; i--) {
- mPlayList[position + i] = mPlayList[position + i - addlen];
- }
-
- // copy list into playlist
- for (int i = 0; i < addlen; i++) {
- mPlayList[position + i] = list[i];
- }
- mPlayListLen += addlen;
- if (mPlayListLen == 0) {
- mCursor.close();
- mCursor = null;
- notifyChange(META_CHANGED);
- }
- }
-
- /**
- * Appends a list of tracks to the current playlist. If nothing is playing
- * currently, playback will be started at the first track. If the action is
- * NOW, playback will switch to the first of the new tracks immediately.
- *
- * @param list The list of tracks to append.
- * @param action NOW, NEXT or LAST
- */
- public void enqueue(long[] list, int action) {
- synchronized (this) {
- if (action == NEXT && mPlayPos + 1 < mPlayListLen) {
- addToPlayList(list, mPlayPos + 1);
- notifyChange(QUEUE_CHANGED);
- } else {
- // action == LAST || action == NOW || mPlayPos + 1 ==
- // mPlayListLen
- addToPlayList(list, Integer.MAX_VALUE);
- notifyChange(QUEUE_CHANGED);
- if (action == NOW) {
- mPlayPos = mPlayListLen - list.length;
- openCurrent();
- play();
- notifyChange(META_CHANGED);
- return;
- }
- }
- if (mPlayPos < 0) {
- mPlayPos = 0;
- openCurrent();
- play();
- notifyChange(META_CHANGED);
- }
- }
- }
-
- /**
- * Replaces the current playlist with a new list, and prepares for starting
- * playback at the specified position in the list, or a random position if
- * the specified position is 0.
- *
- * @param list The new list of tracks.
- */
- public void open(long[] list, int position) {
- synchronized (this) {
- if (mShuffleMode == SHUFFLE_AUTO) {
- mShuffleMode = SHUFFLE_NORMAL;
- }
- long oldId = getAudioId();
- int listlength = list.length;
- boolean newlist = true;
- if (mPlayListLen == listlength) {
- // possible fast path: list might be the same
- newlist = false;
- for (int i = 0; i < listlength; i++) {
- if (list[i] != mPlayList[i]) {
- newlist = true;
- break;
- }
- }
- }
- if (newlist) {
- addToPlayList(list, -1);
- notifyChange(QUEUE_CHANGED);
- }
- if (position >= 0) {
- mPlayPos = position;
- } else {
- mPlayPos = mRand.nextInt(mPlayListLen);
- }
- mHistory.clear();
-
- saveBookmarkIfNeeded();
- openCurrent();
- if (oldId != getAudioId()) {
- notifyChange(META_CHANGED);
- }
- }
- }
-
- /**
- * Returns the current play list
- *
- * @return An array of integers containing the IDs of the tracks in the play
- * list
- */
- public long[] getQueue() {
- synchronized (this) {
- int len = mPlayListLen;
- long[] list = new long[len];
- for (int i = 0; i < len; i++) {
- list[i] = mPlayList[i];
- }
- return list;
- }
- }
-
- private void openCurrent() {
- synchronized (this) {
- if (mCursor != null) {
- mCursor.close();
- mCursor = null;
- }
-
- if (mPlayListLen == 0) {
- return;
- }
- stop(false);
-
- String id = String.valueOf(mPlayList[mPlayPos]);
-
- mCursor = getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
- mCursorCols, "_id=" + id, null, null);
- if (mCursor != null) {
- mCursor.moveToFirst();
- open(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + "/" + id);
- // go to bookmark if needed
- if (isPodcast()) {
- long bookmark = getBookmark();
- // Start playing a little bit before the bookmark,
- // so it's easier to get back in to the narrative.
- seek(bookmark - 5000);
- }
- }
- }
- }
-
- /**
- * Opens the specified file and readies it for playback.
- *
- * @param path The full path of the file to be opened.
- */
- public void open(String path) {
- synchronized (this) {
- if (path == null) {
- return;
- }
-
- // if mCursor is null, try to associate path with a database cursor
- if (mCursor == null) {
-
- ContentResolver resolver = getContentResolver();
- Uri uri;
- String where;
- String selectionArgs[];
- if (path.startsWith("content://media/")) {
- uri = Uri.parse(path);
- where = null;
- selectionArgs = null;
- } else {
- uri = MediaStore.Audio.Media.getContentUriForPath(path);
- where = MediaColumns.DATA + "=?";
- selectionArgs = new String[] {
- path
- };
- }
-
- try {
- mCursor = resolver.query(uri, mCursorCols, where, selectionArgs, null);
- if (mCursor != null) {
- if (mCursor.getCount() == 0) {
- mCursor.close();
- mCursor = null;
- } else {
- mCursor.moveToNext();
- ensurePlayListCapacity(1);
- mPlayListLen = 1;
- mPlayList[0] = mCursor.getLong(IDCOLIDX);
- mPlayPos = 0;
- }
- }
- } catch (UnsupportedOperationException ex) {
- }
- }
- mFileToPlay = path;
- mPlayer.setDataSource(mFileToPlay);
- if (!mPlayer.isInitialized()) {
- stop(true);
- if (mOpenFailedCounter++ < 10 && mPlayListLen > 1) {
- // beware: this ends up being recursive because next() calls
- // open() again.
- next(false);
- }
- if (!mPlayer.isInitialized() && mOpenFailedCounter != 0) {
- // need to make sure we only shows this once
- mOpenFailedCounter = 0;
- if (!mQuietMode) {
- Toast.makeText(this, "Error", Toast.LENGTH_SHORT).show();
- }
- }
-
- } else {
- mOpenFailedCounter = 0;
- }
- }
- }
-
- /**
- * Starts playback of a previously opened file.
- */
- public void play() {
- mAudioManager.requestAudioFocus(mAudioFocusListener, AudioManager.STREAM_MUSIC,
- AudioManager.AUDIOFOCUS_GAIN);
- mAudioManager.registerMediaButtonEventReceiver(new ComponentName(getPackageName(),
- MediaButtonIntentReceiver.class.getName()));
-
- AQuery aq = new AQuery(this);
- Bitmap b = aq.getCachedImage(ApolloUtils.getImageURL(getAlbumName(), ALBUM_IMAGE, this));
-
- if (mPlayer.isInitialized()) {
- // if we are at the end of the song, go to the next song first
-
- mPlayer.start();
- // make sure we fade in, in case a previous fadein was stopped
- // because
- // of another focus loss
- mMediaplayerHandler.removeMessages(FADEDOWN);
- mMediaplayerHandler.sendEmptyMessage(FADEUP);
-
- RemoteViews views = new RemoteViews(getPackageName(), R.layout.status_bar);
- if (b != null) {
- views.setViewVisibility(R.id.status_bar_icon, View.GONE);
- views.setViewVisibility(R.id.status_bar_album_art, View.VISIBLE);
- views.setImageViewBitmap(R.id.status_bar_album_art, b);
- } else {
- views.setViewVisibility(R.id.status_bar_icon, View.VISIBLE);
- views.setViewVisibility(R.id.status_bar_album_art, View.GONE);
- }
- ComponentName rec = new ComponentName(getPackageName(),
- MediaButtonIntentReceiver.class.getName());
- Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
- mediaButtonIntent.putExtra(CMDNOTIF, 1);
- mediaButtonIntent.setComponent(rec);
- KeyEvent mediaKey = new KeyEvent(KeyEvent.ACTION_DOWN,
- KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE);
- mediaButtonIntent.putExtra(Intent.EXTRA_KEY_EVENT, mediaKey);
- PendingIntent mediaPendingIntent = PendingIntent.getBroadcast(getApplicationContext(),
- 1, mediaButtonIntent, PendingIntent.FLAG_UPDATE_CURRENT);
- views.setOnClickPendingIntent(R.id.status_bar_play, mediaPendingIntent);
- mediaButtonIntent.putExtra(CMDNOTIF, 2);
- mediaKey = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT);
- mediaButtonIntent.putExtra(Intent.EXTRA_KEY_EVENT, mediaKey);
- mediaPendingIntent = PendingIntent.getBroadcast(getApplicationContext(), 2,
- mediaButtonIntent, PendingIntent.FLAG_UPDATE_CURRENT);
- views.setOnClickPendingIntent(R.id.status_bar_next, mediaPendingIntent);
- mediaButtonIntent.putExtra(CMDNOTIF, 3);
- mediaKey = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_STOP);
- mediaButtonIntent.putExtra(Intent.EXTRA_KEY_EVENT, mediaKey);
- mediaPendingIntent = PendingIntent.getBroadcast(getApplicationContext(), 3,
- mediaButtonIntent, PendingIntent.FLAG_UPDATE_CURRENT);
- views.setOnClickPendingIntent(R.id.status_bar_collapse, mediaPendingIntent);
- views.setImageViewResource(R.id.status_bar_play, R.drawable.apollo_holo_dark_pause);
-
- views.setTextViewText(R.id.status_bar_track_name, getTrackName());
- views.setTextViewText(R.id.status_bar_artist_name, getArtistName());
-
- status = new Notification();
- status.contentView = views;
- status.flags = Notification.FLAG_ONGOING_EVENT;
- status.icon = R.drawable.stat_notify_music;
- status.contentIntent = PendingIntent
- .getActivity(this, 0, new Intent("com.andrew.apollo.PLAYBACK_VIEWER")
- .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), 0);
- startForeground(PLAYBACKSERVICE_STATUS, status);
- if (!mIsSupposedToBePlaying) {
- mIsSupposedToBePlaying = true;
- notifyChange(PLAYSTATE_CHANGED);
- }
- } else if (mPlayListLen <= 0) {
- // This is mostly so that if you press 'play' on a bluetooth headset
- // without every having played anything before, it will still play
- // something.
- setShuffleMode(SHUFFLE_AUTO);
- }
- }
-
- private void stop(boolean remove_status_icon) {
- if (mPlayer.isInitialized()) {
- mPlayer.stop();
- }
- mFileToPlay = null;
- if (mCursor != null) {
- mCursor.close();
- mCursor = null;
- }
- if (remove_status_icon) {
- gotoIdleState();
- } else {
- stopForeground(false);
- }
- if (remove_status_icon) {
- mIsSupposedToBePlaying = false;
- }
- }
-
- /**
- * Stops playback.
- */
- public void stop() {
- stop(true);
- }
-
- /**
- * Pauses playback (call play() to resume)
- */
- public void pause() {
- synchronized (this) {
- mMediaplayerHandler.removeMessages(FADEUP);
- if (mIsSupposedToBePlaying) {
- mPlayer.pause();
- gotoIdleState();
- mIsSupposedToBePlaying = false;
- notifyChange(PLAYSTATE_CHANGED);
- saveBookmarkIfNeeded();
- }
- }
- }
-
- /**
- * Returns whether something is currently playing
- *
- * @return true if something is playing (or will be playing shortly, in case
- * we're currently transitioning between tracks), false if not.
- */
- public boolean isPlaying() {
- return mIsSupposedToBePlaying;
- }
-
- /*
- * Desired behavior for prev/next/shuffle: - NEXT will move to the next
- * track in the list when not shuffling, and to a track randomly picked from
- * the not-yet-played tracks when shuffling. If all tracks have already been
- * played, pick from the full set, but avoid picking the previously played
- * track if possible. - when shuffling, PREV will go to the previously
- * played track. Hitting PREV again will go to the track played before that,
- * etc. When the start of the history has been reached, PREV is a no-op.
- * When not shuffling, PREV will go to the sequentially previous track (the
- * difference with the shuffle-case is mainly that when not shuffling, the
- * user can back up to tracks that are not in the history). Example: When
- * playing an album with 10 tracks from the start, and enabling shuffle
- * while playing track 5, the remaining tracks (6-10) will be shuffled, e.g.
- * the final play order might be 1-2-3-4-5-8-10-6-9-7. When hitting 'prev' 8
- * times while playing track 7 in this example, the user will go to tracks
- * 9-6-10-8-5-4-3-2. If the user then hits 'next', a random track will be
- * picked again. If at any time user disables shuffling the next/previous
- * track will be picked in sequential order again.
- */
-
- public void prev() {
- synchronized (this) {
- if (mShuffleMode == SHUFFLE_NORMAL) {
- // go to previously-played track and remove it from the history
- int histsize = mHistory.size();
- if (histsize == 0) {
- // prev is a no-op
- return;
- }
- Integer pos = mHistory.remove(histsize - 1);
- mPlayPos = pos.intValue();
- } else {
- if (mPlayPos > 0) {
- mPlayPos--;
- } else {
- mPlayPos = mPlayListLen - 1;
- }
- }
- saveBookmarkIfNeeded();
- stop(false);
- openCurrent();
- play();
-
- notifyChange(META_CHANGED);
- }
- }
-
- public void next(boolean force) {
- synchronized (this) {
- if (mPlayListLen <= 0) {
- Log.d(LOGTAG, "No play queue");
- return;
- }
-
- if (mShuffleMode == SHUFFLE_NORMAL) {
-
- if (mPlayPos >= 0) {
- mHistory.add(mPlayPos);
- }
- if (mHistory.size() > MAX_HISTORY_SIZE) {
- mHistory.removeElementAt(0);
- }
-
- int numTracks = mPlayListLen;
- int[] tracks = new int[numTracks];
- for (int i = 0; i < numTracks; i++) {
- tracks[i] = i;
- }
-
- int numHistory = mHistory.size();
- int numUnplayed = numTracks;
- for (int i = 0; i < numHistory; i++) {
- int idx = mHistory.get(i).intValue();
- if (idx < numTracks && tracks[idx] >= 0) {
- numUnplayed--;
- tracks[idx] = -1;
- }
- }
-
- // 'numUnplayed' now indicates how many tracks have not yet
- // been played, and 'tracks' contains the indices of those
- // tracks.
- if (numUnplayed <= 0) {
- // everything's already been played
- if (mRepeatMode == REPEAT_ALL || force) {
- // pick from full set
- numUnplayed = numTracks;
- for (int i = 0; i < numTracks; i++) {
- tracks[i] = i;
- }
- } else {
- // all done
- gotoIdleState();
- if (mIsSupposedToBePlaying) {
- mIsSupposedToBePlaying = false;
- notifyChange(PLAYSTATE_CHANGED);
- }
- return;
- }
- }
- int skip = mRand.nextInt(numUnplayed);
- int cnt = -1;
- while (true) {
- while (tracks[++cnt] < 0)
- ;
- skip--;
- if (skip < 0) {
- break;
- }
- }
- mPlayPos = cnt;
- } else if (mShuffleMode == SHUFFLE_AUTO) {
- doAutoShuffleUpdate();
- mPlayPos++;
- } else {
- if (mPlayPos >= mPlayListLen - 1) {
- // we're at the end of the list
- if (mRepeatMode == REPEAT_NONE && !force) {
- // all done
- gotoIdleState();
- mIsSupposedToBePlaying = false;
- notifyChange(PLAYSTATE_CHANGED);
- return;
- } else if (mRepeatMode == REPEAT_ALL || force) {
- mPlayPos = 0;
- }
- } else {
- mPlayPos++;
- }
- }
- saveBookmarkIfNeeded();
- stop(false);
- openCurrent();
- play();
- notifyChange(META_CHANGED);
- }
- }
-
- public void cycleRepeat() {
- if (mRepeatMode == REPEAT_NONE) {
- setRepeatMode(REPEAT_ALL);
- } else if (mRepeatMode == REPEAT_ALL) {
- setRepeatMode(REPEAT_CURRENT);
- if (mShuffleMode != SHUFFLE_NONE) {
- setShuffleMode(SHUFFLE_NONE);
- }
- } else {
- setRepeatMode(REPEAT_NONE);
- }
- }
-
- public void toggleShuffle() {
- if (mShuffleMode == SHUFFLE_NONE) {
- setShuffleMode(SHUFFLE_NORMAL);
- if (mRepeatMode == REPEAT_CURRENT) {
- setRepeatMode(REPEAT_ALL);
- }
- } else if (mShuffleMode == SHUFFLE_NORMAL || mShuffleMode == SHUFFLE_AUTO) {
- setShuffleMode(SHUFFLE_NONE);
- }
- }
-
- private void gotoIdleState() {
- mDelayedStopHandler.removeCallbacksAndMessages(null);
- Message msg = mDelayedStopHandler.obtainMessage();
- mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY);
- stopForeground(false);
- if (status != null) {
- status.contentView.setImageViewResource(R.id.status_bar_play,
- mIsSupposedToBePlaying ? R.drawable.apollo_holo_dark_play
- : R.drawable.apollo_holo_dark_pause);
- NotificationManager mManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
- mManager.notify(PLAYBACKSERVICE_STATUS, status);
- }
- }
-
- private void saveBookmarkIfNeeded() {
- try {
- if (isPodcast()) {
- long pos = position();
- long bookmark = getBookmark();
- long duration = duration();
- if ((pos < bookmark && (pos + 10000) > bookmark)
- || (pos > bookmark && (pos - 10000) < bookmark)) {
- // The existing bookmark is close to the current
- // position, so don't update it.
- return;
- }
- if (pos < 15000 || (pos + 10000) > duration) {
- // if we're near the start or end, clear the bookmark
- pos = 0;
- }
-
- // write 'pos' to the bookmark field
- ContentValues values = new ContentValues();
- values.put(AudioColumns.BOOKMARK, pos);
- Uri uri = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
- mCursor.getLong(IDCOLIDX));
- getContentResolver().update(uri, values, null, null);
- }
- } catch (SQLiteException ex) {
- }
- }
-
- // Make sure there are at least 5 items after the currently playing item
- // and no more than 10 items before.
- private void doAutoShuffleUpdate() {
- boolean notify = false;
-
- // remove old entries
- if (mPlayPos > 10) {
- removeTracks(0, mPlayPos - 9);
- notify = true;
- }
- // add new entries if needed
- int to_add = 7 - (mPlayListLen - (mPlayPos < 0 ? -1 : mPlayPos));
- for (int i = 0; i < to_add; i++) {
- // pick something at random from the list
-
- int lookback = mHistory.size();
- int idx = -1;
- while (true) {
- idx = mRand.nextInt(mAutoShuffleList.length);
- if (!wasRecentlyUsed(idx, lookback)) {
- break;
- }
- lookback /= 2;
- }
- mHistory.add(idx);
- if (mHistory.size() > MAX_HISTORY_SIZE) {
- mHistory.remove(0);
- }
- ensurePlayListCapacity(mPlayListLen + 1);
- mPlayList[mPlayListLen++] = mAutoShuffleList[idx];
- notify = true;
- }
- if (notify) {
- notifyChange(QUEUE_CHANGED);
- }
- }
-
- // check that the specified idx is not in the history (but only look at at
- // most lookbacksize entries in the history)
- private boolean wasRecentlyUsed(int idx, int lookbacksize) {
-
- // early exit to prevent infinite loops in case idx == mPlayPos
- if (lookbacksize == 0) {
- return false;
- }
-
- int histsize = mHistory.size();
- if (histsize < lookbacksize) {
- lookbacksize = histsize;
- }
- int maxidx = histsize - 1;
- for (int i = 0; i < lookbacksize; i++) {
- long entry = mHistory.get(maxidx - i);
- if (entry == idx) {
- return true;
- }
- }
- return false;
- }
-
- // A simple variation of Random that makes sure that the
- // value it returns is not equal to the value it returned
- // previously, unless the interval is 1.
- private static class Shuffler {
- private int mPrevious;
-
- private final Random mRandom = new Random();
-
- public int nextInt(int interval) {
- int ret;
- do {
- ret = mRandom.nextInt(interval);
- } while (ret == mPrevious && interval > 1);
- mPrevious = ret;
- return ret;
- }
- };
-
- private boolean makeAutoShuffleList() {
- ContentResolver res = getContentResolver();
- Cursor c = null;
- try {
- c = res.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, new String[] {
- BaseColumns._ID
- }, AudioColumns.IS_MUSIC + "=1", null, null);
- if (c == null || c.getCount() == 0) {
- return false;
- }
- int len = c.getCount();
- long[] list = new long[len];
- for (int i = 0; i < len; i++) {
- c.moveToNext();
- list[i] = c.getLong(0);
- }
- mAutoShuffleList = list;
- return true;
- } catch (RuntimeException ex) {
- } finally {
- if (c != null) {
- c.close();
- }
- }
- return false;
- }
-
- /**
- * Removes the range of tracks specified from the play list. If a file
- * within the range is the file currently being played, playback will move
- * to the next file after the range.
- *
- * @param first The first file to be removed
- * @param last The last file to be removed
- * @return the number of tracks deleted
- */
- public int removeTracks(int first, int last) {
- int numremoved = removeTracksInternal(first, last);
- if (numremoved > 0) {
- notifyChange(QUEUE_CHANGED);
- }
- return numremoved;
- }
-
- private int removeTracksInternal(int first, int last) {
- synchronized (this) {
- if (last < first)
- return 0;
- if (first < 0)
- first = 0;
- if (last >= mPlayListLen)
- last = mPlayListLen - 1;
-
- boolean gotonext = false;
- if (first <= mPlayPos && mPlayPos <= last) {
- mPlayPos = first;
- gotonext = true;
- } else if (mPlayPos > last) {
- mPlayPos -= (last - first + 1);
- }
- int num = mPlayListLen - last - 1;
- for (int i = 0; i < num; i++) {
- mPlayList[first + i] = mPlayList[last + 1 + i];
- }
- mPlayListLen -= last - first + 1;
-
- if (gotonext) {
- if (mPlayListLen == 0) {
- stop(true);
- mPlayPos = -1;
- if (mCursor != null) {
- mCursor.close();
- mCursor = null;
- }
- } else {
- if (mPlayPos >= mPlayListLen) {
- mPlayPos = 0;
- }
- boolean wasPlaying = mIsSupposedToBePlaying;
- stop(false);
- openCurrent();
- if (wasPlaying) {
- play();
- }
- }
- notifyChange(META_CHANGED);
- }
- return last - first + 1;
- }
- }
-
- /**
- * Removes all instances of the track with the given id from the playlist.
- *
- * @param id The id to be removed
- * @return how many instances of the track were removed
- */
- public int removeTrack(long id) {
- int numremoved = 0;
- synchronized (this) {
- for (int i = 0; i < mPlayListLen; i++) {
- if (mPlayList[i] == id) {
- numremoved += removeTracksInternal(i, i);
- i--;
- }
- }
- }
- if (numremoved > 0) {
- notifyChange(QUEUE_CHANGED);
- }
- return numremoved;
- }
-
- public void setShuffleMode(int shufflemode) {
- synchronized (this) {
- if (mShuffleMode == shufflemode && mPlayListLen > 0) {
- return;
- }
- mShuffleMode = shufflemode;
- notifyChange(SHUFFLEMODE_CHANGED);
- if (mShuffleMode == SHUFFLE_AUTO) {
- if (makeAutoShuffleList()) {
- mPlayListLen = 0;
- doAutoShuffleUpdate();
- mPlayPos = 0;
- openCurrent();
- play();
- notifyChange(META_CHANGED);
- return;
- } else {
- // failed to build a list of files to shuffle
- mShuffleMode = SHUFFLE_NONE;
- }
- }
- saveQueue(false);
- }
- }
-
- public int getShuffleMode() {
- return mShuffleMode;
- }
-
- public void setRepeatMode(int repeatmode) {
- synchronized (this) {
- mRepeatMode = repeatmode;
- notifyChange(REPEATMODE_CHANGED);
- saveQueue(false);
- }
- }
-
- public int getRepeatMode() {
- return mRepeatMode;
- }
-
- public int getMediaMountedCount() {
- return mMediaMountedCount;
- }
-
- /**
- * Returns the path of the currently playing file, or null if no file is
- * currently playing.
- */
- public String getPath() {
- return mFileToPlay;
- }
-
- /**
- * Returns the rowid of the currently playing file, or -1 if no file is
- * currently playing.
- */
- public long getAudioId() {
- synchronized (this) {
- if (mPlayPos >= 0 && mPlayer.isInitialized()) {
- return mPlayList[mPlayPos];
- }
- }
- return -1;
- }
-
- /**
- * Returns the position in the queue
- *
- * @return the position in the queue
- */
- public int getQueuePosition() {
- synchronized (this) {
- return mPlayPos;
- }
- }
-
- /**
- * Starts playing the track at the given position in the queue.
- *
- * @param pos The position in the queue of the track that will be played.
- */
- public void setQueuePosition(int pos) {
- synchronized (this) {
- stop(false);
- mPlayPos = pos;
- openCurrent();
- play();
- notifyChange(META_CHANGED);
- if (mShuffleMode == SHUFFLE_AUTO) {
- doAutoShuffleUpdate();
- }
- }
- }
-
- public String getArtistName() {
- synchronized (this) {
- if (mCursor == null) {
- return null;
- }
- return mCursor.getString(mCursor.getColumnIndexOrThrow(AudioColumns.ARTIST));
- }
- }
-
- public long getArtistId() {
- synchronized (this) {
- if (mCursor == null) {
- return -1;
- }
- return mCursor.getLong(mCursor.getColumnIndexOrThrow(AudioColumns.ARTIST_ID));
- }
- }
-
- public String getAlbumName() {
- synchronized (this) {
- if (mCursor == null) {
- return null;
- }
- return mCursor.getString(mCursor.getColumnIndexOrThrow(AudioColumns.ALBUM));
- }
- }
-
- public long getAlbumId() {
- synchronized (this) {
- if (mCursor == null) {
- return -1;
- }
- return mCursor.getLong(mCursor.getColumnIndexOrThrow(AudioColumns.ALBUM_ID));
- }
- }
-
- public String getTrackName() {
- synchronized (this) {
- if (mCursor == null) {
- return null;
- }
- return mCursor.getString(mCursor.getColumnIndexOrThrow(MediaColumns.TITLE));
- }
- }
-
- private boolean isPodcast() {
- synchronized (this) {
- if (mCursor == null) {
- return false;
- }
- return (mCursor.getInt(PODCASTCOLIDX) > 0);
- }
- }
-
- private long getBookmark() {
- synchronized (this) {
- if (mCursor == null) {
- return 0;
- }
- return mCursor.getLong(BOOKMARKCOLIDX);
- }
- }
-
- /**
- * Returns the duration of the file in milliseconds. Currently this method
- * returns -1 for the duration of MIDI files.
- */
- public long duration() {
- if (mPlayer.isInitialized()) {
- return mPlayer.duration();
- }
- return -1;
- }
-
- /**
- * Returns the current playback position in milliseconds
- */
- public long position() {
- if (mPlayer.isInitialized()) {
- return mPlayer.position();
- }
- return -1;
- }
-
- /**
- * Seeks to the position specified.
- *
- * @param pos The position to seek to, in milliseconds
- */
- public long seek(long pos) {
- if (mPlayer.isInitialized()) {
- if (pos < 0)
- pos = 0;
- if (pos > mPlayer.duration())
- pos = mPlayer.duration();
- return mPlayer.seek(pos);
- }
- return -1;
- }
-
- /**
- * Sets the audio session ID.
- *
- * @param sessionId: the audio session ID.
- */
- public void setAudioSessionId(int sessionId) {
- synchronized (this) {
- mPlayer.setAudioSessionId(sessionId);
- }
- }
-
- /**
- * Returns the audio session ID.
- */
- public int getAudioSessionId() {
- synchronized (this) {
- return mPlayer.getAudioSessionId();
- }
- }
-
- public void toggleFavorite() {
- if (!isFavorite()) {
- addToFavorites();
- } else {
- removeFromFavorites();
- }
- }
-
- public boolean isFavorite() {
- if (getAudioId() >= 0)
- return isFavorite(getAudioId());
- return false;
- }
-
- public boolean isFavorite(long id) {
- return MusicUtils.isFavorite(this, id);
- }
-
- public void removeFromFavorites() {
- if (getAudioId() >= 0) {
- removeFromFavorites(getAudioId());
- }
- }
-
- public void removeFromFavorites(long id) {
- MusicUtils.removeFromFavorites(this, id);
- notifyChange(FAVORITE_CHANGED);
- }
-
- public void addToFavorites() {
- if (getAudioId() >= 0) {
- addToFavorites(getAudioId());
- }
- }
-
- public void addToFavorites(long id) {
- MusicUtils.addToFavorites(this, id);
- notifyChange(FAVORITE_CHANGED);
- }
-
- /**
- * Provides a unified interface for dealing with midi files and other media
- * files.
- */
- private class MultiPlayer {
- private MediaPlayer mMediaPlayer = new MediaPlayer();
-
- private Handler mHandler;
-
- private boolean mIsInitialized = false;
-
- public MultiPlayer() {
- mMediaPlayer.setWakeMode(ApolloService.this, PowerManager.PARTIAL_WAKE_LOCK);
- }
-
- public void setDataSource(String path) {
- try {
- mMediaPlayer.reset();
- mMediaPlayer.setOnPreparedListener(null);
- if (path.startsWith("content://")) {
- mMediaPlayer.setDataSource(ApolloService.this, Uri.parse(path));
- } else {
- mMediaPlayer.setDataSource(path);
- }
- mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
- mMediaPlayer.prepare();
- } catch (IOException ex) {
- mIsInitialized = false;
- return;
- } catch (IllegalArgumentException ex) {
- mIsInitialized = false;
- return;
- }
- mMediaPlayer.setOnCompletionListener(listener);
- mMediaPlayer.setOnErrorListener(errorListener);
- Intent i = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION);
- i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, getAudioSessionId());
- i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName());
- sendBroadcast(i);
- mIsInitialized = true;
- }
-
- public boolean isInitialized() {
- return mIsInitialized;
- }
-
- public void start() {
- mMediaPlayer.start();
- }
-
- public void stop() {
- mMediaPlayer.reset();
- mIsInitialized = false;
- }
-
- /**
- * You CANNOT use this player anymore after calling release()
- */
- public void release() {
- stop();
- mMediaPlayer.release();
- }
-
- public void pause() {
- mMediaPlayer.pause();
- }
-
- public void setHandler(Handler handler) {
- mHandler = handler;
- }
-
- MediaPlayer.OnCompletionListener listener = new MediaPlayer.OnCompletionListener() {
- @Override
- public void onCompletion(MediaPlayer mp) {
- // 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.
- // This temporary wakelock is released when the RELEASE_WAKELOCK
- // message is processed, but just in case, put a timeout on it.
- mWakeLock.acquire(30000);
- mHandler.sendEmptyMessage(TRACK_ENDED);
- mHandler.sendEmptyMessage(RELEASE_WAKELOCK);
- }
- };
-
- MediaPlayer.OnErrorListener errorListener = new MediaPlayer.OnErrorListener() {
- @Override
- public boolean onError(MediaPlayer mp, int what, int extra) {
- switch (what) {
- case MediaPlayer.MEDIA_ERROR_SERVER_DIED:
- mIsInitialized = false;
- mMediaPlayer.release();
- // Creating a new MediaPlayer and settings its wakemode
- // does not
- // require the media service, so it's OK to do this now,
- // while the
- // service is still being restarted
- mMediaPlayer = new MediaPlayer();
- mMediaPlayer
- .setWakeMode(ApolloService.this, PowerManager.PARTIAL_WAKE_LOCK);
- mHandler.sendMessageDelayed(mHandler.obtainMessage(SERVER_DIED), 2000);
- return true;
- default:
- Log.d("MultiPlayer", "Error: " + what + "," + extra);
- break;
- }
- return false;
- }
- };
-
- public long duration() {
- return mMediaPlayer.getDuration();
- }
-
- public long position() {
- return mMediaPlayer.getCurrentPosition();
- }
-
- public long seek(long whereto) {
- mMediaPlayer.seekTo((int)whereto);
- return whereto;
- }
-
- public void setVolume(float vol) {
- mMediaPlayer.setVolume(vol, vol);
- }
-
- public void setAudioSessionId(int sessionId) {
- mMediaPlayer.setAudioSessionId(sessionId);
- }
-
- public int getAudioSessionId() {
- return mMediaPlayer.getAudioSessionId();
- }
- }
-
- /*
- * By making this a static class with a WeakReference to the Service, we
- * ensure that the Service can be GCd even when the system process still has
- * a remote reference to the stub.
- */
- static class ServiceStub extends IApolloService.Stub {
- WeakReference<ApolloService> mService;
-
- ServiceStub(ApolloService service) {
- mService = new WeakReference<ApolloService>(service);
- }
-
- @Override
- public void openFile(String path) {
- mService.get().open(path);
- }
-
- @Override
- public void open(long[] list, int position) {
- mService.get().open(list, position);
- }
-
- @Override
- public int getQueuePosition() {
- return mService.get().getQueuePosition();
- }
-
- @Override
- public void setQueuePosition(int index) {
- mService.get().setQueuePosition(index);
- }
-
- @Override
- public boolean isPlaying() {
- return mService.get().isPlaying();
- }
-
- @Override
- public void stop() {
- mService.get().stop();
- }
-
- @Override
- public void pause() {
- mService.get().pause();
- }
-
- @Override
- public void play() {
- mService.get().play();
- }
-
- @Override
- public void prev() {
- mService.get().prev();
- }
-
- @Override
- public void next() {
- mService.get().next(true);
- }
-
- @Override
- public String getTrackName() {
- return mService.get().getTrackName();
- }
-
- @Override
- public String getAlbumName() {
- return mService.get().getAlbumName();
- }
-
- @Override
- public long getAlbumId() {
- return mService.get().getAlbumId();
- }
-
- @Override
- public String getArtistName() {
- return mService.get().getArtistName();
- }
-
- @Override
- public long getArtistId() {
- return mService.get().getArtistId();
- }
-
- @Override
- public void enqueue(long[] list, int action) {
- mService.get().enqueue(list, action);
- }
-
- @Override
- public long[] getQueue() {
- return mService.get().getQueue();
- }
-
- @Override
- public String getPath() {
- return mService.get().getPath();
- }
-
- @Override
- public long getAudioId() {
- return mService.get().getAudioId();
- }
-
- @Override
- public long position() {
- return mService.get().position();
- }
-
- @Override
- public long duration() {
- return mService.get().duration();
- }
-
- @Override
- public long seek(long pos) {
- return mService.get().seek(pos);
- }
-
- @Override
- public void setShuffleMode(int shufflemode) {
- mService.get().setShuffleMode(shufflemode);
- }
-
- @Override
- public int getShuffleMode() {
- return mService.get().getShuffleMode();
- }
-
- @Override
- public int removeTracks(int first, int last) {
- return mService.get().removeTracks(first, last);
- }
-
- @Override
- public int removeTrack(long id) {
- return mService.get().removeTrack(id);
- }
-
- @Override
- public void setRepeatMode(int repeatmode) {
- mService.get().setRepeatMode(repeatmode);
- }
-
- @Override
- public int getRepeatMode() {
- return mService.get().getRepeatMode();
- }
-
- @Override
- public int getMediaMountedCount() {
- return mService.get().getMediaMountedCount();
- }
-
- @Override
- public int getAudioSessionId() {
- return mService.get().getAudioSessionId();
- }
-
- @Override
- public void addToFavorites(long id) throws RemoteException {
- mService.get().addToFavorites(id);
- }
-
- @Override
- public void removeFromFavorites(long id) throws RemoteException {
- mService.get().removeFromFavorites(id);
- }
-
- @Override
- public boolean isFavorite(long id) throws RemoteException {
- return mService.get().isFavorite(id);
- }
-
- @Override
- public void toggleFavorite() throws RemoteException {
- mService.get().toggleFavorite();
- }
-
- }
-
- private final IBinder mBinder = new ServiceStub(this);
-
-}
diff --git a/src/com/andrew/apollo/service/ServiceBinder.java b/src/com/andrew/apollo/service/ServiceBinder.java
deleted file mode 100644
index 2dea385..0000000
--- a/src/com/andrew/apollo/service/ServiceBinder.java
+++ /dev/null
@@ -1,31 +0,0 @@
-
-package com.andrew.apollo.service;
-
-import android.content.ComponentName;
-import android.content.ServiceConnection;
-import android.os.IBinder;
-
-import com.andrew.apollo.IApolloService;
-import com.andrew.apollo.utils.MusicUtils;
-
-public class ServiceBinder implements ServiceConnection {
- private final ServiceConnection mCallback;
-
- public ServiceBinder(ServiceConnection callback) {
- mCallback = callback;
- }
-
- @Override
- public void onServiceConnected(ComponentName className, IBinder service) {
- MusicUtils.mService = IApolloService.Stub.asInterface(service);
- if (mCallback != null)
- mCallback.onServiceConnected(className, service);
- }
-
- @Override
- public void onServiceDisconnected(ComponentName className) {
- if (mCallback != null)
- mCallback.onServiceDisconnected(className);
- MusicUtils.mService = null;
- }
-}
diff --git a/src/com/andrew/apollo/service/ServiceToken.java b/src/com/andrew/apollo/service/ServiceToken.java
deleted file mode 100644
index 22be005..0000000
--- a/src/com/andrew/apollo/service/ServiceToken.java
+++ /dev/null
@@ -1,12 +0,0 @@
-
-package com.andrew.apollo.service;
-
-import android.content.ContextWrapper;
-
-public class ServiceToken {
- public ContextWrapper mWrappedContext;
-
- public ServiceToken(ContextWrapper context) {
- mWrappedContext = context;
- }
-}
diff --git a/src/com/andrew/apollo/tasks/BitmapFromURL.java b/src/com/andrew/apollo/tasks/BitmapFromURL.java
deleted file mode 100644
index a147f09..0000000
--- a/src/com/andrew/apollo/tasks/BitmapFromURL.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/**
- *
- */
-
-package com.andrew.apollo.tasks;
-
-import java.lang.ref.WeakReference;
-
-import android.graphics.Bitmap;
-import android.os.AsyncTask;
-import android.widget.ImageView;
-
-import com.andrew.apollo.utils.ApolloUtils;
-
-/**
- * @author Andrew Neal
- */
-public class BitmapFromURL extends AsyncTask<String, Integer, Bitmap> {
-
- private final WeakReference<ImageView> imageViewReference;
-
- private final ImageView mImageView;
-
- private WeakReference<Bitmap> bitmapReference;
-
- public BitmapFromURL(ImageView iv) {
- imageViewReference = new WeakReference<ImageView>(iv);
- mImageView = imageViewReference.get();
- }
-
- @Override
- protected Bitmap doInBackground(String... params) {
- bitmapReference = new WeakReference<Bitmap>(ApolloUtils.getBitmapFromURL(params[0]));
- return bitmapReference.get();
- }
-
- @Override
- protected void onPostExecute(Bitmap result) {
- if (result != null && mImageView != null)
- ApolloUtils.runnableBackground(mImageView, result);
- super.onPostExecute(result);
- }
-}
diff --git a/src/com/andrew/apollo/tasks/FetchAlbumImages.java b/src/com/andrew/apollo/tasks/FetchAlbumImages.java
deleted file mode 100644
index 64c6d96..0000000
--- a/src/com/andrew/apollo/tasks/FetchAlbumImages.java
+++ /dev/null
@@ -1,122 +0,0 @@
-/**
- *
- */
-
-package com.andrew.apollo.tasks;
-
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.provider.BaseColumns;
-import android.provider.MediaStore.Audio;
-import android.provider.MediaStore.Audio.AlbumColumns;
-
-import com.andrew.apollo.Constants;
-import com.andrew.apollo.utils.ApolloUtils;
-
-/**
- * @author Andrew Neal
- * @returns A String[] of all the artists and albums on a device in default
- * album order that are then fed into the Last.fm API
- */
-public class FetchAlbumImages {
-
- private final Context mContext;
-
- private final WeakReference<Context> contextReference;
-
- private final int choice;
-
- public FetchAlbumImages(Context context, int opt) {
- contextReference = new WeakReference<Context>(context);
- mContext = contextReference.get();
- choice = opt;
- }
-
- /**
- * @return album names in default album sort order
- */
- public String[] getAlbumArtists() {
- String[] projection = new String[] {
- BaseColumns._ID, AlbumColumns.ARTIST
- };
- String sortOrder = Audio.Albums.DEFAULT_SORT_ORDER;
- Uri uri = Audio.Albums.EXTERNAL_CONTENT_URI;
- Cursor c = mContext.getContentResolver().query(uri, projection, null, null, sortOrder);
- ArrayList<String> artistIds = new ArrayList<String>();
- if (c != null) {
- int count = c.getCount();
- if (count > 0) {
- final int ARTIST_IDX = c.getColumnIndex(AlbumColumns.ARTIST);
- for (int i = 0; i < count; i++) {
- c.moveToPosition(i);
- artistIds.add(c.getString(ARTIST_IDX));
- }
- }
- c.close();
- c = null;
- }
- return artistIds.toArray(new String[artistIds.size()]);
- }
-
- /**
- * @author Andrew Neal
- * @returns artist names in default album sort order that are then fed into
- * the Last.fm API along with @getAlbumArtists()
- */
- public class getAlbums extends AsyncTask<Void, Integer, String[]> implements Constants {
-
- @Override
- protected String[] doInBackground(Void... params) {
- String[] projection = new String[] {
- BaseColumns._ID, AlbumColumns.ALBUM
- };
- String sortOrder = Audio.Albums.DEFAULT_SORT_ORDER;
- Uri uri = Audio.Albums.EXTERNAL_CONTENT_URI;
- Cursor c = mContext.getContentResolver().query(uri, projection, null, null, sortOrder);
- ArrayList<String> artistIds = new ArrayList<String>();
- if (c != null) {
- int count = c.getCount();
- if (count > 0) {
- final int ARTIST_IDX = c.getColumnIndex(AlbumColumns.ALBUM);
- for (int i = 0; i < count; i++) {
- c.moveToPosition(i);
- artistIds.add(c.getString(ARTIST_IDX));
- }
- }
- c.close();
- c = null;
- }
- return artistIds.toArray(new String[artistIds.size()]);
- }
-
- @Override
- protected void onPostExecute(String[] result) {
- for (int i = 0; i < result.length; i++) {
- // Only download images we don't already have
- if (choice == 0 && result != null) {
- if (ApolloUtils.getImageURL(result[i], ALBUM_IMAGE, mContext) == null) {
- new LastfmGetAlbumImages(mContext, null, 0).executeOnExecutor(
- AsyncTask.THREAD_POOL_EXECUTOR, getAlbumArtists()[i], result[i]);
- }
- } else if (choice == 1 && result != null) {
- // Unless the user wants to grab new images
- new LastfmGetAlbumImages(mContext, null, 0).executeOnExecutor(
- AsyncTask.THREAD_POOL_EXECUTOR, getAlbumArtists()[i], result[i]);
- }
- }
- super.onPostExecute(result);
- }
- }
-
- /**
- * Fetch album art
- */
- public void runTask() {
- new getAlbums().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[])null);
- }
-}
diff --git a/src/com/andrew/apollo/tasks/FetchArtistImages.java b/src/com/andrew/apollo/tasks/FetchArtistImages.java
deleted file mode 100644
index 4800369..0000000
--- a/src/com/andrew/apollo/tasks/FetchArtistImages.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/**
- *
- */
-
-package com.andrew.apollo.tasks;
-
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.provider.BaseColumns;
-import android.provider.MediaStore.Audio;
-import android.provider.MediaStore.Audio.ArtistColumns;
-
-import com.andrew.apollo.Constants;
-import com.andrew.apollo.utils.ApolloUtils;
-
-/**
- * @author Andrew Neal
- * @returns A String[] of all the artists on a device in default artist order
- * that are then fed into the Last.fm API
- */
-public class FetchArtistImages extends AsyncTask<Void, Integer, String[]> implements Constants {
-
- private final WeakReference<Context> contextReference;
-
- private final int choice;
-
- public FetchArtistImages(Context context, int opt) {
- contextReference = new WeakReference<Context>(context);
- choice = opt;
- }
-
- @Override
- protected String[] doInBackground(Void... params) {
- String[] projection = new String[] {
- BaseColumns._ID, ArtistColumns.ARTIST
- };
- String sortOrder = Audio.Artists.DEFAULT_SORT_ORDER;
- Uri uri = Audio.Artists.EXTERNAL_CONTENT_URI;
- Cursor c = contextReference.get().getContentResolver()
- .query(uri, projection, null, null, sortOrder);
- ArrayList<String> artistIds = new ArrayList<String>();
- if (c != null) {
- int count = c.getCount();
- if (count > 0) {
- final int ARTIST_IDX = c.getColumnIndex(ArtistColumns.ARTIST);
- for (int i = 0; i < count; i++) {
- c.moveToPosition(i);
- artistIds.add(c.getString(ARTIST_IDX));
- }
- }
- c.close();
- c = null;
- }
- return artistIds.toArray(new String[artistIds.size()]);
- }
-
- @Override
- protected void onPostExecute(String[] result) {
- for (int i = 0; i < result.length; i++) {
- // Only download images we don't already have
- if (choice == 0 && result != null) {
- if (ApolloUtils.getImageURL(result[i], ARTIST_IMAGE, contextReference.get()) == null) {
- new LastfmGetArtistImages(contextReference.get()).executeOnExecutor(
- AsyncTask.THREAD_POOL_EXECUTOR, result[i]);
- }
- } else if (choice == 1 && result != null) {
- // Unless the user wants to grab new images
- new LastfmGetArtistImages(contextReference.get()).executeOnExecutor(
- AsyncTask.THREAD_POOL_EXECUTOR, result[i]);
- }
- }
- super.onPostExecute(result);
- }
-
- /**
- * Fetch artist images
- */
- public void runTask() {
- executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[])null);
- }
-}
diff --git a/src/com/andrew/apollo/tasks/GetCachedImages.java b/src/com/andrew/apollo/tasks/GetCachedImages.java
deleted file mode 100644
index 041c9dd..0000000
--- a/src/com/andrew/apollo/tasks/GetCachedImages.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/**
- *
- */
-
-package com.andrew.apollo.tasks;
-
-import java.lang.ref.WeakReference;
-
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.os.AsyncTask;
-import android.widget.ImageView;
-
-import com.andrew.apollo.Constants;
-import com.andrew.apollo.R;
-import com.andrew.apollo.utils.ApolloUtils;
-import com.androidquery.AQuery;
-
-/**
- * @author Andrew Neal Returns a cached image for @TracksBrowser
- */
-public class GetCachedImages extends AsyncTask<String, Integer, Bitmap> implements Constants {
-
- private final Context mContext;
-
- private final int choice;
-
- private final WeakReference<ImageView> imageViewReference;
-
- private final AQuery aquery;
-
- private final ImageView mImageView;
-
- private String url;
-
- private WeakReference<Bitmap> bitmapReference;
-
- private final WeakReference<Context> contextReference;
-
- public GetCachedImages(Context c, int opt, ImageView iv) {
- contextReference = new WeakReference<Context>(c);
- mContext = contextReference.get();
- choice = opt;
- imageViewReference = new WeakReference<ImageView>(iv);
- mImageView = imageViewReference.get();
-
- // AQuery
- aquery = new AQuery(mContext);
- }
-
- @Override
- protected Bitmap doInBackground(String... args) {
- if (choice == 0)
- url = ApolloUtils.getImageURL(args[0], ARTIST_IMAGE_ORIGINAL, mContext);
- if (choice == 1)
- url = ApolloUtils.getImageURL(args[0], ALBUM_IMAGE, mContext);
- bitmapReference = new WeakReference<Bitmap>(aquery.getCachedImage(url, 300));
- return bitmapReference.get();
- }
-
- @Override
- protected void onPostExecute(Bitmap result) {
- if (imageViewReference != null && result != null) {
- ApolloUtils.runnableBackground(mImageView, result);
- } else {
- result = aquery.getCachedImage(R.drawable.promo);
- ApolloUtils.runnableBackground(mImageView, result);
- }
- super.onPostExecute(result);
- }
-}
diff --git a/src/com/andrew/apollo/tasks/LastfmGetAlbumImages.java b/src/com/andrew/apollo/tasks/LastfmGetAlbumImages.java
deleted file mode 100644
index 20ca3bf..0000000
--- a/src/com/andrew/apollo/tasks/LastfmGetAlbumImages.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/**
- *
- */
-
-package com.andrew.apollo.tasks;
-
-import java.lang.ref.WeakReference;
-
-import android.app.Activity;
-import android.content.Context;
-import android.os.AsyncTask;
-import android.widget.ImageView;
-
-import com.andrew.apollo.Constants;
-import com.andrew.apollo.lastfm.api.Album;
-import com.andrew.apollo.lastfm.api.ImageSize;
-import com.andrew.apollo.utils.ApolloUtils;
-import com.androidquery.AQuery;
-
-/**
- * @author Andrew Neal
- * @returns A convenient image size that's perfect for a GridView.
- */
-public class LastfmGetAlbumImages extends AsyncTask<String, Integer, String> implements Constants {
-
- // URL to cache
- private String url = null;
-
- // AQuery
- private final AQuery aq;
-
- private final WeakReference<Context> contextReference;
-
- private final WeakReference<ImageView> imageviewReference;
-
- private final ImageView mImageView;
-
- private final int choice;
-
- private Album album;
-
- public LastfmGetAlbumImages(Context context, ImageView iv, int opt) {
- contextReference = new WeakReference<Context>(context);
- imageviewReference = new WeakReference<ImageView>(iv);
- mImageView = imageviewReference.get();
- choice = opt;
-
- // Initiate AQuery
- aq = new AQuery((Activity)contextReference.get(), iv);
- }
-
- @Override
- protected String doInBackground(String... name) {
- if (ApolloUtils.isOnline(contextReference.get()) && name[0] != null && name[1] != null) {
- try {
- album = Album.getInfo(name[0], name[1], LASTFM_API_KEY);
- url = album.getImageURL(ImageSize.LARGE);
- aq.cache(url, 0);
- ApolloUtils.setImageURL(name[1], url, ALBUM_IMAGE, contextReference.get());
- return url;
- } catch (Exception e) {
- return null;
- }
- } else {
- url = ApolloUtils.getImageURL(name[1], ALBUM_IMAGE, contextReference.get());
- }
- return url;
- }
-
- @Override
- protected void onPostExecute(String result) {
- if (result != null && mImageView != null && choice == 1)
- new BitmapFromURL(mImageView).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, result);
- super.onPostExecute(result);
- }
-}
diff --git a/src/com/andrew/apollo/tasks/LastfmGetArtistImages.java b/src/com/andrew/apollo/tasks/LastfmGetArtistImages.java
deleted file mode 100644
index 1cf3bf0..0000000
--- a/src/com/andrew/apollo/tasks/LastfmGetArtistImages.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/**
- *
- */
-
-package com.andrew.apollo.tasks;
-
-import java.lang.ref.WeakReference;
-import java.util.Iterator;
-
-import android.content.Context;
-import android.os.AsyncTask;
-
-import com.andrew.apollo.Constants;
-import com.andrew.apollo.lastfm.api.Artist;
-import com.andrew.apollo.lastfm.api.Image;
-import com.andrew.apollo.lastfm.api.ImageSize;
-import com.andrew.apollo.lastfm.api.PaginatedResult;
-import com.andrew.apollo.utils.ApolloUtils;
-import com.androidquery.AQuery;
-
-/**
- * @author Andrew Neal
- * @returns A convenient image size that's perfect for a GridView.
- */
-public class LastfmGetArtistImages extends AsyncTask<String, Integer, String> implements Constants {
-
- // URL to cache
- private String url = null;
-
- private PaginatedResult<Image> artist;
-
- // AQuery
- private final AQuery aq;
-
- private final WeakReference<Context> contextReference;
-
- public LastfmGetArtistImages(Context context) {
- contextReference = new WeakReference<Context>(context);
-
- // Initiate AQuery
- aq = new AQuery(contextReference.get());
- }
-
- @Override
- protected String doInBackground(String... artistname) {
- if (ApolloUtils.isOnline(contextReference.get()) && artistname[0] != null) {
- try {
- artist = Artist.getImages(artistname[0], 1, 1, LASTFM_API_KEY);
- Iterator<Image> iterator = artist.getPageResults().iterator();
- while (iterator.hasNext()) {
- Image mTemp = iterator.next();
- url = mTemp.getImageURL(ImageSize.LARGESQUARE);
- }
- aq.cache(url, 0);
- ApolloUtils.setImageURL(artistname[0], url, ARTIST_IMAGE, contextReference.get());
- return url;
- } catch (Exception e) {
- return null;
- }
- } else {
- url = ApolloUtils.getImageURL(artistname[0], ARTIST_IMAGE, contextReference.get());
- }
- return url;
- }
-}
diff --git a/src/com/andrew/apollo/tasks/LastfmGetArtistImagesOriginal.java b/src/com/andrew/apollo/tasks/LastfmGetArtistImagesOriginal.java
deleted file mode 100644
index 46f5eab..0000000
--- a/src/com/andrew/apollo/tasks/LastfmGetArtistImagesOriginal.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/**
- *
- */
-
-package com.andrew.apollo.tasks;
-
-import java.lang.ref.WeakReference;
-import java.util.Iterator;
-
-import android.content.Context;
-import android.os.AsyncTask;
-import android.widget.ImageView;
-
-import com.andrew.apollo.Constants;
-import com.andrew.apollo.lastfm.api.Artist;
-import com.andrew.apollo.lastfm.api.Image;
-import com.andrew.apollo.lastfm.api.ImageSize;
-import com.andrew.apollo.lastfm.api.PaginatedResult;
-import com.andrew.apollo.utils.ApolloUtils;
-import com.androidquery.AQuery;
-
-
-/**
- * @author Andrew Neal
- * @Note This is used to display artist images in @TracksBrowser
- */
-public class LastfmGetArtistImagesOriginal extends AsyncTask<String, Integer, String> implements
- Constants {
-
- // URL to cache
- private String url = null;
-
- private final ImageView mImageView;
-
- private final WeakReference<ImageView> imageviewReference;
-
- // AQuery
- private final AQuery aq;
-
- // Context
- private final Context mContext;
-
- private final WeakReference<Context> contextReference;
-
- public LastfmGetArtistImagesOriginal(Context context, ImageView iv) {
- contextReference = new WeakReference<Context>(context);
- mContext = contextReference.get();
- imageviewReference = new WeakReference<ImageView>(iv);
- mImageView = imageviewReference.get();
-
- // Initiate AQuery
- aq = new AQuery(mContext);
- }
-
- @Override
- protected String doInBackground(String... artistname) {
- if (ApolloUtils.isOnline(mContext)) {
- PaginatedResult<Image> artist = Artist.getImages(artistname[0], 1, 1, LASTFM_API_KEY);
- Iterator<Image> iterator = artist.getPageResults().iterator();
- while (iterator.hasNext()) {
- Image mTemp = iterator.next();
- url = mTemp.getImageURL(ImageSize.ORIGINAL);
- }
- aq.cache(url, 0);
- ApolloUtils.setImageURL(artistname[0], url, ARTIST_IMAGE_ORIGINAL, mContext);
- return url;
- } else {
- url = ApolloUtils.getImageURL(artistname[0], ARTIST_IMAGE_ORIGINAL, mContext);
- }
- return url;
- }
-
- @Override
- protected void onPostExecute(String result) {
- if (result != null && mImageView != null) {
- new BitmapFromURL(mImageView).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, result);
- }
- super.onPostExecute(result);
- }
-}
diff --git a/src/com/andrew/apollo/tasks/ViewHolderQueueTask.java b/src/com/andrew/apollo/tasks/ViewHolderQueueTask.java
deleted file mode 100644
index 5515409..0000000
--- a/src/com/andrew/apollo/tasks/ViewHolderQueueTask.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/**
- *
- */
-
-package com.andrew.apollo.tasks;
-
-import java.lang.ref.WeakReference;
-
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.os.AsyncTask;
-import android.widget.ImageView;
-
-import com.andrew.apollo.Constants;
-import com.andrew.apollo.utils.ApolloUtils;
-import com.andrew.apollo.views.ViewHolderQueue;
-import com.androidquery.AQuery;
-
-/**
- * @author Andrew Neal
- */
-public class ViewHolderQueueTask extends AsyncTask<String, Integer, Bitmap> implements Constants {
-
- private final ViewHolderQueue mViewHolderQueue;
-
- private final WeakReference<ImageView> imageViewReference;
-
- private final Context mContext;
-
- private final int mPosition;
-
- private final int choice;
-
- private final int holderChoice;
-
- private final AQuery aquery;
-
- private final ImageView mImageView;
-
- private String url;
-
- private WeakReference<Bitmap> bitmapReference;
-
- private final WeakReference<Context> contextReference;
-
- public ViewHolderQueueTask(ViewHolderQueue vh, int position, Context c, int opt, int holderOpt,
- ImageView iv) {
- mViewHolderQueue = vh;
- mPosition = position;
- contextReference = new WeakReference<Context>(c);
- mContext = contextReference.get();
- choice = opt;
- holderChoice = holderOpt;
- imageViewReference = new WeakReference<ImageView>(iv);
- mImageView = imageViewReference.get();
-
- // AQuery
- aquery = new AQuery(mContext);
- }
-
- @Override
- protected Bitmap doInBackground(String... args) {
- if (choice == 0)
- url = ApolloUtils.getImageURL(args[0], ARTIST_IMAGE, mContext);
- if (choice == 1)
- url = ApolloUtils.getImageURL(args[0], ALBUM_IMAGE, mContext);
- bitmapReference = new WeakReference<Bitmap>(aquery.getCachedImage(url));
- return bitmapReference.get();
- }
-
- @Override
- protected void onPostExecute(Bitmap result) {
- if (imageViewReference != null && holderChoice == 0
- && mViewHolderQueue.position == mPosition && mViewHolderQueue != null)
- aquery.id(mImageView).image(result);
- if (imageViewReference != null && holderChoice == 1
- && mViewHolderQueue.position == mPosition && mViewHolderQueue != null)
- aquery.id(mImageView).image(result);
- super.onPostExecute(result);
- }
-}
diff --git a/src/com/andrew/apollo/tasks/ViewHolderTask.java b/src/com/andrew/apollo/tasks/ViewHolderTask.java
deleted file mode 100644
index 9c3eac6..0000000
--- a/src/com/andrew/apollo/tasks/ViewHolderTask.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/**
- *
- */
-
-package com.andrew.apollo.tasks;
-
-import java.lang.ref.WeakReference;
-
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.os.AsyncTask;
-import android.widget.ImageView;
-
-import com.andrew.apollo.Constants;
-import com.andrew.apollo.R;
-import com.andrew.apollo.utils.ApolloUtils;
-import com.andrew.apollo.views.ViewHolderGrid;
-import com.andrew.apollo.views.ViewHolderList;
-import com.androidquery.AQuery;
-
-/**
- * @author Andrew Neal
- */
-public class ViewHolderTask extends AsyncTask<String, Integer, Bitmap> implements Constants {
-
- private final ViewHolderList mViewHolderList;
-
- private final ViewHolderGrid mViewHolderGrid;
-
- private final WeakReference<ImageView> imageViewReference;
-
- private final Context mContext;
-
- private final int mPosition;
-
- private final int choice;
-
- private final int holderChoice;
-
- private final AQuery aquery;
-
- private final ImageView mImageView;
-
- private final int albumart;
-
- private final WeakReference<Context> contextReference;
-
- private String url;
-
- private WeakReference<Bitmap> bitmapReference;
-
- public ViewHolderTask(ViewHolderList vh, ViewHolderGrid vhg, int position, Context c, int opt,
- int holderOpt, ImageView iv) {
- mViewHolderList = vh;
- mViewHolderGrid = vhg;
- mPosition = position;
- contextReference = new WeakReference<Context>(c);
- mContext = contextReference.get();
- choice = opt;
- holderChoice = holderOpt;
- imageViewReference = new WeakReference<ImageView>(iv);
- mImageView = imageViewReference.get();
- aquery = new AQuery(mContext);
-
- albumart = mContext.getResources().getInteger(R.integer.listview_album_art);
- }
-
- @Override
- protected Bitmap doInBackground(String... args) {
- if (choice == 0)
- url = ApolloUtils.getImageURL(args[0], ARTIST_IMAGE, mContext);
- if (choice == 1)
- url = ApolloUtils.getImageURL(args[0], ALBUM_IMAGE, mContext);
- bitmapReference = new WeakReference<Bitmap>(aquery.getCachedImage(url));
- if (holderChoice == 0) {
- return ApolloUtils.getResizedBitmap(bitmapReference.get(), albumart, albumart);
- } else if (holderChoice == 1) {
- return bitmapReference.get();
- }
- return null;
- }
-
- @Override
- protected void onPostExecute(Bitmap result) {
- if (result != null && imageViewReference != null && holderChoice == 0
- && mViewHolderList.position == mPosition && mViewHolderList != null)
- mImageView.setImageBitmap(result);
- if (result != null && imageViewReference != null && holderChoice == 1
- && mViewHolderGrid.position == mPosition && mViewHolderGrid != null)
- mImageView.setImageBitmap(result);
- super.onPostExecute(result);
- }
-}
diff --git a/src/com/andrew/apollo/ui/MusicHolder.java b/src/com/andrew/apollo/ui/MusicHolder.java
new file mode 100644
index 0000000..18951cd
--- /dev/null
+++ b/src/com/andrew/apollo/ui/MusicHolder.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.ui;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import com.andrew.apollo.R;
+import com.andrew.apollo.appwidgets.RecentWidgetService;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * Used to efficiently cache and recyle the {@link View}s used in the artist,
+ * album, song, playlist, and genre adapters.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class MusicHolder {
+
+ /**
+ * This is the overlay ontop of the background artist, playlist, or genre
+ * image
+ */
+ public WeakReference<RelativeLayout> mOverlay;
+
+ /**
+ * This is the background artist, playlist, or genre image
+ */
+ public WeakReference<ImageView> mBackground;
+
+ /**
+ * This is the artist or album image
+ */
+ public WeakReference<ImageView> mImage;
+
+ /**
+ * This is the first line displayed in the list or grid
+ *
+ * @see {@code #getView()} of a specific adapter for more detailed info
+ */
+ public WeakReference<TextView> mLineOne;
+
+ /**
+ * This is the second line displayed in the list or grid
+ *
+ * @see {@code #getView()} of a specific adapter for more detailed info
+ */
+ public WeakReference<TextView> mLineTwo;
+
+ /**
+ * This is the third line displayed in the list or grid
+ *
+ * @see {@code #getView()} of a specific adapter for more detailed info
+ */
+ public WeakReference<TextView> mLineThree;
+
+ /**
+ * Constructor of <code>ViewHolder</code>
+ *
+ * @param context The {@link Context} to use.
+ */
+ public MusicHolder(final View view) {
+ super();
+ // Initialize mOverlay
+ mOverlay = new WeakReference<RelativeLayout>(
+ (RelativeLayout)view.findViewById(R.id.image_background));
+
+ // Initialize mBackground
+ mBackground = new WeakReference<ImageView>(
+ (ImageView)view.findViewById(R.id.list_item_background));
+
+ // Initialize mImage
+ mImage = new WeakReference<ImageView>((ImageView)view.findViewById(R.id.image));
+
+ // Initialize mLineOne
+ mLineOne = new WeakReference<TextView>((TextView)view.findViewById(R.id.line_one));
+
+ // Initialize mLineTwo
+ mLineTwo = new WeakReference<TextView>((TextView)view.findViewById(R.id.line_two));
+
+ // Initialize mLineThree
+ mLineThree = new WeakReference<TextView>((TextView)view.findViewById(R.id.line_three));
+ }
+
+ /**
+ * @param view The {@link View} used to initialize content
+ */
+ public final static class DataHolder {
+
+ /**
+ * This is the ID of the item being loaded in the adapter
+ */
+ public String mItemId;
+
+ /**
+ * This is the first line displayed in the list or grid
+ *
+ * @see {@code #getView()} of a specific adapter for more detailed info
+ */
+ public String mLineOne;
+
+ /**
+ * This is the second line displayed in the list or grid
+ *
+ * @see {@code #getView()} of a specific adapter for more detailed info
+ */
+ public String mLineTwo;
+
+ /**
+ * This is the third line displayed in the list or grid
+ *
+ * @see {@code #getView()} of a specific adapter for more detailed info
+ */
+ public String mLineThree;
+
+ /**
+ * This is the album art bitmap used in {@link RecentWidgetService}.
+ */
+ public Bitmap mImage;
+
+ /**
+ * Constructor of <code>DataHolder</code>
+ */
+ public DataHolder() {
+ super();
+ }
+
+ }
+}
diff --git a/src/com/andrew/apollo/ui/activities/AudioPlayerActivity.java b/src/com/andrew/apollo/ui/activities/AudioPlayerActivity.java
new file mode 100644
index 0000000..4f8178d
--- /dev/null
+++ b/src/com/andrew/apollo/ui/activities/AudioPlayerActivity.java
@@ -0,0 +1,895 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.ui.activities;
+
+import static com.andrew.apollo.utils.MusicUtils.mService;
+
+import android.app.SearchManager;
+import android.app.SearchableInfo;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.media.AudioManager;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.SystemClock;
+import android.support.v4.view.ViewPager;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.Window;
+import android.view.animation.AnimationUtils;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+import android.widget.TextView;
+
+import com.actionbarsherlock.app.ActionBar;
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+import com.actionbarsherlock.widget.SearchView;
+import com.actionbarsherlock.widget.SearchView.OnQueryTextListener;
+import com.andrew.apollo.IApolloService;
+import com.andrew.apollo.MusicPlaybackService;
+import com.andrew.apollo.R;
+import com.andrew.apollo.adapters.PagerAdapter;
+import com.andrew.apollo.cache.ImageFetcher;
+import com.andrew.apollo.ui.fragments.LyricsFragment;
+import com.andrew.apollo.ui.fragments.QueueFragment;
+import com.andrew.apollo.utils.ApolloUtils;
+import com.andrew.apollo.utils.MusicUtils;
+import com.andrew.apollo.utils.MusicUtils.ServiceToken;
+import com.andrew.apollo.utils.NavUtils;
+import com.andrew.apollo.utils.ThemeUtils;
+import com.andrew.apollo.widgets.PlayPauseButton;
+import com.andrew.apollo.widgets.RepeatButton;
+import com.andrew.apollo.widgets.RepeatingImageButton;
+import com.andrew.apollo.widgets.ShuffleButton;
+import com.nineoldandroids.animation.ObjectAnimator;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * Apollo's "now playing" interface.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class AudioPlayerActivity extends SherlockFragmentActivity implements ServiceConnection,
+ OnSeekBarChangeListener {
+
+ // Message to refresh the time
+ private static final int REFRESH_TIME = 1;
+
+ // The service token
+ private ServiceToken mToken;
+
+ // Play and pause button
+ private PlayPauseButton mPlayPauseButton;
+
+ // Repeat button
+ private RepeatButton mRepeatButton;
+
+ // Shuffle button
+ private ShuffleButton mShuffleButton;
+
+ // Previous button
+ private RepeatingImageButton mPreviousButton;
+
+ // Next button
+ private RepeatingImageButton mNextButton;
+
+ // Track name
+ private TextView mTrackName;
+
+ // Artist name
+ private TextView mArtistName;
+
+ // Album art
+ private ImageView mAlbumArt;
+
+ // Tiny artwork
+ private ImageView mAlbumArtSmall;
+
+ // Current time
+ private TextView mCurrentTime;
+
+ // Total time
+ private TextView mTotalTime;
+
+ // Queue switch
+ private ImageView mQueueSwitch;
+
+ // Progess
+ private SeekBar mProgress;
+
+ // Broadcast receiver
+ private PlaybackStatus mPlaybackStatus;
+
+ // Handler used to update the current time
+ private TimeHandler mTimeHandler;
+
+ // View pager
+ private ViewPager mViewPager;
+
+ // Pager adpater
+ private PagerAdapter mPagerAdapter;
+
+ // ViewPager container
+ private FrameLayout mPageContainer;
+
+ // Header
+ private LinearLayout mAudioPlayerHeader;
+
+ // Image cache
+ private ImageFetcher mImageFetcher;
+
+ // Theme resources
+ private ThemeUtils mResources;
+
+ private long mPosOverride = -1;
+
+ private long mStartSeekPos = 0;
+
+ private long mLastSeekEventTime;
+
+ private boolean mIsPaused = false;
+
+ private boolean mFromTouch = false;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Title bar shows up in gingerbread, I'm too tired to figure out why.
+ if (!ApolloUtils.hasHoneycomb()) {
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+ }
+
+ // Initialze the theme resources
+ mResources = new ThemeUtils(this);
+ // Set the overflow style
+ mResources.setOverflowStyle(this);
+
+ // Fade it in
+ overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
+
+ // Control the media volume
+ setVolumeControlStream(AudioManager.STREAM_MUSIC);
+
+ // Bind Apollo's service
+ mToken = MusicUtils.bindToService(this, this);
+
+ // Initialize the image fetcher/cache
+ mImageFetcher = ApolloUtils.getImageFetcher(this);
+
+ // Initialize the handler used to update the current time
+ mTimeHandler = new TimeHandler(this);
+
+ // Initialize the broadcast receiver
+ mPlaybackStatus = new PlaybackStatus(this);
+
+ // Theme the action bar
+ final ActionBar actionBar = getSupportActionBar();
+ mResources.themeActionBar(actionBar, getString(R.string.app_name));
+ actionBar.setDisplayHomeAsUpEnabled(true);
+
+ // Set the layout
+ setContentView(R.layout.activity_player_base);
+
+ // Cache all the items
+ initPlaybackControls();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onServiceConnected(final ComponentName name, final IBinder service) {
+ mService = IApolloService.Stub.asInterface(service);
+ // Set the playback drawables
+ updatePlaybackControls();
+ // Current info
+ updateNowPlayingInfo();
+ // Update the favorites icon
+ invalidateOptionsMenu();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onServiceDisconnected(final ComponentName name) {
+ mService = null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onProgressChanged(final SeekBar bar, final int progress, final boolean fromuser) {
+ if (!fromuser || mService == null) {
+ return;
+ }
+ final long now = SystemClock.elapsedRealtime();
+ if (now - mLastSeekEventTime > 250) {
+ mLastSeekEventTime = now;
+ mPosOverride = MusicUtils.duration() * progress / 1000;
+ MusicUtils.seek(mPosOverride);
+ if (!mFromTouch) {
+ // refreshCurrentTime();
+ mPosOverride = -1;
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onStartTrackingTouch(final SeekBar bar) {
+ mLastSeekEventTime = 0;
+ mFromTouch = true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onStopTrackingTouch(final SeekBar bar) {
+ mPosOverride = -1;
+ mFromTouch = false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onPrepareOptionsMenu(final Menu menu) {
+ mResources.setFavoriteIcon(menu);
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onCreateOptionsMenu(final Menu menu) {
+ // Search view
+ getSupportMenuInflater().inflate(R.menu.search, menu);
+ // Theme the search icon
+ mResources.setSearchIcon(menu);
+
+ final SearchView searchView = (SearchView)menu.findItem(R.id.menu_search).getActionView();
+ // Add voice search
+ final SearchManager searchManager = (SearchManager)getSystemService(Context.SEARCH_SERVICE);
+ final SearchableInfo searchableInfo = searchManager.getSearchableInfo(getComponentName());
+ searchView.setSearchableInfo(searchableInfo);
+ // Perform the search
+ searchView.setOnQueryTextListener(new OnQueryTextListener() {
+
+ @Override
+ public boolean onQueryTextSubmit(final String query) {
+ // Open the search activity
+ NavUtils.openSearch(AudioPlayerActivity.this, query);
+ return true;
+ }
+
+ @Override
+ public boolean onQueryTextChange(final String newText) {
+ // Nothing to do
+ return false;
+ }
+ });
+
+ // Favorite action
+ getSupportMenuInflater().inflate(R.menu.favorite, menu);
+ // Shuffle all
+ getSupportMenuInflater().inflate(R.menu.shuffle, menu);
+ // Share, ringtone, and equalizer
+ getSupportMenuInflater().inflate(R.menu.audio_player, menu);
+ // Settings
+ getSupportMenuInflater().inflate(R.menu.activity_base, menu);
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ // Go back to the home activity
+ NavUtils.goHome(this);
+ return true;
+ case R.id.menu_shuffle:
+ // Shuffle all the songs
+ MusicUtils.shuffleAll(this);
+ // Refresh the queue
+ ((QueueFragment)mPagerAdapter.getFragment(0)).refreshQueue();
+ return true;
+ case R.id.menu_favorite:
+ // Toggle the current track as a favorite and update the menu
+ // item
+ MusicUtils.toggleFavorite();
+ invalidateOptionsMenu();
+ return true;
+ case R.id.menu_audio_player_ringtone:
+ // Set the current track as a ringtone
+ MusicUtils.setRingtone(this, MusicUtils.getCurrentAudioId());
+ return true;
+ case R.id.menu_audio_player_share:
+ // Share the current meta data
+ shareCurrentTrack();
+ return true;
+ case R.id.menu_audio_player_equalizer:
+ // Sound effects
+ NavUtils.openEffectsPanel(this);
+ return true;
+ case R.id.menu_download_lyrics:
+ updateLyrics(true);
+ hideAlbumArt();
+ return true;
+ case R.id.menu_settings:
+ // Settings
+ NavUtils.openSettings(this);
+ return true;
+ default:
+ break;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onBackPressed() {
+ super.onBackPressed();
+ NavUtils.goHome(this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onResume() {
+ super.onResume();
+ // Hide Apollo's notification
+ MusicUtils.killForegroundService(this);
+ // Set the playback drawables
+ updatePlaybackControls();
+ // Current info
+ updateNowPlayingInfo();
+ // Refresh the queue
+ ((QueueFragment)mPagerAdapter.getFragment(0)).refreshQueue();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onStart() {
+ super.onStart();
+ final IntentFilter filter = new IntentFilter();
+ // Play and pause changes
+ filter.addAction(MusicPlaybackService.PLAYSTATE_CHANGED);
+ // Shuffle and repeat changes
+ filter.addAction(MusicPlaybackService.SHUFFLEMODE_CHANGED);
+ filter.addAction(MusicPlaybackService.REPEATMODE_CHANGED);
+ // Track changes
+ filter.addAction(MusicPlaybackService.META_CHANGED);
+ // Update a list, probably the playlist fragment's
+ filter.addAction(MusicPlaybackService.REFRESH);
+ registerReceiver(mPlaybackStatus, filter);
+ // Refresh the current time
+ final long next = refreshCurrentTime();
+ queueNextRefresh(next);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onPause() {
+ super.onPause();
+ // Show Apollo's notification
+ if (MusicUtils.isPlaying() && ApolloUtils.isApplicationSentToBackground(this)) {
+ MusicUtils.startBackgroundService(this);
+ }
+ mImageFetcher.flush();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mIsPaused = false;
+ mTimeHandler.removeMessages(REFRESH_TIME);
+ // Unbind from the service
+ if (mService != null) {
+ MusicUtils.unbindFromService(mToken);
+ mToken = null;
+ }
+
+ // Unregister the receiver
+ try {
+ unregisterReceiver(mPlaybackStatus);
+ } catch (final Throwable e) {
+ //$FALL-THROUGH$
+ }
+ }
+
+ /**
+ * Initializes the items in the now playing screen
+ */
+ @SuppressWarnings("deprecation")
+ private void initPlaybackControls() {
+ // ViewPager container
+ mPageContainer = (FrameLayout)findViewById(R.id.audio_player_pager_container);
+ // Theme the pager container background
+ mPageContainer
+ .setBackgroundDrawable(mResources.getDrawable("audio_player_pager_container"));
+
+ // Now playing header
+ mAudioPlayerHeader = (LinearLayout)findViewById(R.id.audio_player_header);
+ // Opens the currently playing album profile
+ mAudioPlayerHeader.setOnClickListener(mOpenAlbumProfile);
+
+ // Used to hide the artwork and show the queue/lyrics
+ final FrameLayout mSwitch = (FrameLayout)findViewById(R.id.audio_player_switch);
+ mSwitch.setOnClickListener(mToggleHiddenPanel);
+
+ // Initialize the pager adapter
+ mPagerAdapter = new PagerAdapter(this);
+ // Queue
+ mPagerAdapter.add(QueueFragment.class, null);
+ // Lyrics
+ mPagerAdapter.add(LyricsFragment.class, null);
+
+ // Initialize the ViewPager
+ mViewPager = (ViewPager)findViewById(R.id.audio_player_pager);
+ // Attch the adapter
+ mViewPager.setAdapter(mPagerAdapter);
+ // Offscreen pager loading limit
+ mViewPager.setOffscreenPageLimit(mPagerAdapter.getCount() - 1);
+ // Play and pause button
+ mPlayPauseButton = (PlayPauseButton)findViewById(R.id.action_button_play);
+ // Shuffle button
+ mShuffleButton = (ShuffleButton)findViewById(R.id.action_button_shuffle);
+ // Repeat button
+ mRepeatButton = (RepeatButton)findViewById(R.id.action_button_repeat);
+ // Previous button
+ mPreviousButton = (RepeatingImageButton)findViewById(R.id.action_button_previous);
+ // Next button
+ mNextButton = (RepeatingImageButton)findViewById(R.id.action_button_next);
+ // Track name
+ mTrackName = (TextView)findViewById(R.id.audio_player_track_name);
+ // Artist name
+ mArtistName = (TextView)findViewById(R.id.audio_player_artist_name);
+ // Album art
+ mAlbumArt = (ImageView)findViewById(R.id.audio_player_album_art);
+ // Small album art
+ mAlbumArtSmall = (ImageView)findViewById(R.id.audio_player_switch_album_art);
+ // Current time
+ mCurrentTime = (TextView)findViewById(R.id.audio_player_current_time);
+ // Total time
+ mTotalTime = (TextView)findViewById(R.id.audio_player_total_time);
+ // Used to show and hide the queue and lyrics fragments
+ mQueueSwitch = (ImageView)findViewById(R.id.audio_player_switch_queue);
+ // Theme the queue switch icon
+ mQueueSwitch.setImageDrawable(mResources.getDrawable("btn_switch_queue"));
+ // Progress
+ mProgress = (SeekBar)findViewById(android.R.id.progress);
+
+ // Set the repeat listner for the previous button
+ mPreviousButton.setRepeatListener(mRewindListener);
+ // Set the repeat listner for the next button
+ mNextButton.setRepeatListener(mFastForwardListener);
+ // Update the progress
+ mProgress.setOnSeekBarChangeListener(this);
+ }
+
+ /**
+ * Sets the track name, album name, and album art.
+ */
+ private void updateNowPlayingInfo() {
+ // Set the track name
+ mTrackName.setText(MusicUtils.getTrackName());
+ // Set the artist name
+ mArtistName.setText(MusicUtils.getArtistName());
+ // Set the total time
+ mTotalTime.setText(MusicUtils.makeTimeString(this, MusicUtils.duration() / 1000));
+ // Set the album art
+ mImageFetcher.loadCurrentArtwork(mAlbumArt);
+ // Set the small artwork
+ mImageFetcher.loadCurrentArtwork(mAlbumArtSmall);
+ // Update the current time
+ queueNextRefresh(1);
+
+ }
+
+ /**
+ * Sets the correct drawable states for the playback controls.
+ */
+ private void updatePlaybackControls() {
+ // Set the play and pause image
+ mPlayPauseButton.updateState();
+ // Set the shuffle image
+ mShuffleButton.updateShuffleState();
+ // Set the repeat image
+ mRepeatButton.updateRepeatState();
+ }
+
+ /**
+ * Refreshes the lyrics and moves the view pager to the lyrics fragment.
+ */
+ public void updateLyrics(final boolean force) {
+ ((LyricsFragment)mPagerAdapter.getFragment(1)).fetchLyrics(force);
+ if (force && mViewPager.getCurrentItem() != 1) {
+ mViewPager.setCurrentItem(1, true);
+ }
+ }
+
+ /**
+ * @param delay When to update
+ */
+ private void queueNextRefresh(final long delay) {
+ if (!mIsPaused) {
+ final Message message = mTimeHandler.obtainMessage(REFRESH_TIME);
+ mTimeHandler.removeMessages(REFRESH_TIME);
+ mTimeHandler.sendMessageDelayed(message, delay);
+ }
+ }
+
+ /**
+ * Used to scan backwards in time through the curren track
+ *
+ * @param repcnt The repeat count
+ * @param delta The long press duration
+ */
+ private void scanBackward(final int repcnt, long delta) {
+ if (mService == null) {
+ return;
+ }
+ if (repcnt == 0) {
+ mStartSeekPos = MusicUtils.position();
+ mLastSeekEventTime = 0;
+ } else {
+ if (delta < 5000) {
+ // seek at 10x speed for the first 5 seconds
+ delta = delta * 10;
+ } else {
+ // seek at 40x after that
+ delta = 50000 + (delta - 5000) * 40;
+ }
+ long newpos = mStartSeekPos - delta;
+ if (newpos < 0) {
+ // move to previous track
+ MusicUtils.previous(this);
+ final long duration = MusicUtils.duration();
+ mStartSeekPos += duration;
+ newpos += duration;
+ }
+ if (delta - mLastSeekEventTime > 250 || repcnt < 0) {
+ MusicUtils.seek(newpos);
+ mLastSeekEventTime = delta;
+ }
+ if (repcnt >= 0) {
+ mPosOverride = newpos;
+ } else {
+ mPosOverride = -1;
+ }
+ refreshCurrentTime();
+ }
+ }
+
+ /**
+ * Used to scan forwards in time through the curren track
+ *
+ * @param repcnt The repeat count
+ * @param delta The long press duration
+ */
+ private void scanForward(final int repcnt, long delta) {
+ if (mService == null) {
+ return;
+ }
+ if (repcnt == 0) {
+ mStartSeekPos = MusicUtils.position();
+ mLastSeekEventTime = 0;
+ } else {
+ if (delta < 5000) {
+ // seek at 10x speed for the first 5 seconds
+ delta = delta * 10;
+ } else {
+ // seek at 40x after that
+ delta = 50000 + (delta - 5000) * 40;
+ }
+ long newpos = mStartSeekPos + delta;
+ final long duration = MusicUtils.duration();
+ if (newpos >= duration) {
+ // move to next track
+ MusicUtils.next();
+ mStartSeekPos -= duration; // is OK to go negative
+ newpos -= duration;
+ }
+ if (delta - mLastSeekEventTime > 250 || repcnt < 0) {
+ MusicUtils.seek(newpos);
+ mLastSeekEventTime = delta;
+ }
+ if (repcnt >= 0) {
+ mPosOverride = newpos;
+ } else {
+ mPosOverride = -1;
+ }
+ refreshCurrentTime();
+ }
+ }
+
+ /* Used to update the current time string */
+ private long refreshCurrentTime() {
+ if (mService == null) {
+ return 500;
+ }
+ try {
+ final long pos = mPosOverride < 0 ? MusicUtils.position() : mPosOverride;
+ if (pos >= 0 && MusicUtils.duration() > 0) {
+ mCurrentTime.setText(MusicUtils.makeTimeString(this, pos / 1000));
+ final int progress = (int)(1000 * pos / MusicUtils.duration());
+ mProgress.setProgress(progress);
+
+ if (MusicUtils.isPlaying()) {
+ mCurrentTime.setVisibility(View.VISIBLE);
+ } else {
+ // blink the counter
+ final int vis = mCurrentTime.getVisibility();
+ mCurrentTime.setVisibility(vis == View.INVISIBLE ? View.VISIBLE
+ : View.INVISIBLE);
+ return 500;
+ }
+ } else {
+ mCurrentTime.setText("--:--");
+ mProgress.setProgress(1000);
+ }
+ // calculate the number of milliseconds until the next full second,
+ // so
+ // the counter can be updated at just the right time
+ final long remaining = 1000 - pos % 1000;
+ // approximate how often we would need to refresh the slider to
+ // move it smoothly
+ int width = mProgress.getWidth();
+ if (width == 0) {
+ width = 320;
+ }
+ final long smoothrefreshtime = MusicUtils.duration() / width;
+ if (smoothrefreshtime > remaining) {
+ return remaining;
+ }
+ if (smoothrefreshtime < 20) {
+ return 20;
+ }
+ return smoothrefreshtime;
+ } catch (final Exception ignored) {
+
+ }
+ return 500;
+ }
+
+ /**
+ * @param v The view to animate
+ * @param alpha The alpha to apply
+ */
+ private void fade(final View v, final float alpha) {
+ final ObjectAnimator fade = ObjectAnimator.ofFloat(v, "alpha", alpha);
+ fade.setInterpolator(AnimationUtils.loadInterpolator(this,
+ android.R.anim.accelerate_decelerate_interpolator));
+ fade.setDuration(400);
+ fade.start();
+ }
+
+ /**
+ * Called to show the album art and hide the queue/lyrics
+ */
+ private void showAlbumArt() {
+ mPageContainer.setVisibility(View.INVISIBLE);
+ mAlbumArtSmall.setVisibility(View.GONE);
+ mQueueSwitch.setVisibility(View.VISIBLE);
+ // Fade out the pager container
+ fade(mPageContainer, 0f);
+ // Fade in the album art
+ fade(mAlbumArt, 1f);
+ }
+
+ /**
+ * Called to hide the album art and show the queue/lyrics
+ */
+ public void hideAlbumArt() {
+ mPageContainer.setVisibility(View.VISIBLE);
+ mQueueSwitch.setVisibility(View.GONE);
+ mAlbumArtSmall.setVisibility(View.VISIBLE);
+ // Fade out the artwork
+ fade(mAlbumArt, 0f);
+ // Fade in the pager container
+ fade(mPageContainer, 1f);
+ }
+
+ /**
+ * /** Used to shared what the user is currently listening to
+ */
+ private void shareCurrentTrack() {
+ if (MusicUtils.getTrackName() == null || MusicUtils.getArtistName() == null) {
+ return;
+ }
+ final Intent shareIntent = new Intent();
+ final String shareMessage = getString(R.string.now_listening_to) + " "
+ + MusicUtils.getTrackName() + " " + getString(R.string.by) + " "
+ + MusicUtils.getArtistName() + " " + getString(R.string.hash_apollo);
+
+ shareIntent.setAction(Intent.ACTION_SEND);
+ shareIntent.setType("text/plain");
+ shareIntent.putExtra(Intent.EXTRA_TEXT, shareMessage);
+ startActivity(Intent.createChooser(shareIntent, getString(R.string.share_track_using)));
+ }
+
+ /**
+ * Used to scan backwards through the track
+ */
+ private final RepeatingImageButton.RepeatListener mRewindListener = new RepeatingImageButton.RepeatListener() {
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onRepeat(final View v, final long howlong, final int repcnt) {
+ scanBackward(repcnt, howlong);
+ }
+ };
+
+ /**
+ * Used to scan ahead through the track
+ */
+ private final RepeatingImageButton.RepeatListener mFastForwardListener = new RepeatingImageButton.RepeatListener() {
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onRepeat(final View v, final long howlong, final int repcnt) {
+ scanForward(repcnt, howlong);
+ }
+ };
+
+ /**
+ * Switches from the large album art screen to show the queue and lyric
+ * fragments, then back again
+ */
+ private final OnClickListener mToggleHiddenPanel = new OnClickListener() {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onClick(final View v) {
+ if (mPageContainer.getVisibility() == View.VISIBLE) {
+ // Open the current album profile
+ mAudioPlayerHeader.setOnClickListener(mOpenAlbumProfile);
+ // Show the artwork, hide the queue and lyrics
+ showAlbumArt();
+ } else {
+ // Scroll to the current track
+ mAudioPlayerHeader.setOnClickListener(mScrollToCurrentSong);
+ // Show the queue and lyrics, hide the artwork
+ hideAlbumArt();
+ }
+ }
+ };
+
+ /**
+ * Opens to the current album profile
+ */
+ private final OnClickListener mOpenAlbumProfile = new OnClickListener() {
+
+ @Override
+ public void onClick(final View v) {
+ NavUtils.openAlbumProfile(AudioPlayerActivity.this, MusicUtils.getAlbumName(),
+ MusicUtils.getArtistName());
+ }
+ };
+
+ /**
+ * Scrolls the queue to the currently playing song
+ */
+ private final OnClickListener mScrollToCurrentSong = new OnClickListener() {
+
+ @Override
+ public void onClick(final View v) {
+ ((QueueFragment)mPagerAdapter.getFragment(0)).scrollToCurrentSong();
+ }
+ };
+
+ /**
+ * Used to update the current time string
+ */
+ private static final class TimeHandler extends Handler {
+
+ private final WeakReference<AudioPlayerActivity> mAudioPlayer;
+
+ /**
+ * Constructor of <code>TimeHandler</code>
+ */
+ public TimeHandler(final AudioPlayerActivity player) {
+ mAudioPlayer = new WeakReference<AudioPlayerActivity>(player);
+ }
+
+ @Override
+ public void handleMessage(final Message msg) {
+ switch (msg.what) {
+ case REFRESH_TIME:
+ final long next = mAudioPlayer.get().refreshCurrentTime();
+ mAudioPlayer.get().queueNextRefresh(next);
+ break;
+ default:
+ break;
+ }
+ }
+ };
+
+ /**
+ * Used to monitor the state of playback
+ */
+ private static final class PlaybackStatus extends BroadcastReceiver {
+
+ private final WeakReference<AudioPlayerActivity> mReference;
+
+ /**
+ * Constructor of <code>PlaybackStatus</code>
+ */
+ public PlaybackStatus(final AudioPlayerActivity activity) {
+ mReference = new WeakReference<AudioPlayerActivity>(activity);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onReceive(final Context context, final Intent intent) {
+ final String action = intent.getAction();
+ if (action.equals(MusicPlaybackService.META_CHANGED)) {
+ // Current info
+ mReference.get().updateNowPlayingInfo();
+ // Update the favorites icon
+ mReference.get().invalidateOptionsMenu();
+ // Update the lyrics
+ mReference.get().updateLyrics(false);
+ } else if (action.equals(MusicPlaybackService.PLAYSTATE_CHANGED)) {
+ // Set the play and pause image
+ mReference.get().mPlayPauseButton.updateState();
+ } else if (action.equals(MusicPlaybackService.REPEATMODE_CHANGED)
+ || action.equals(MusicPlaybackService.SHUFFLEMODE_CHANGED)) {
+ // Set the repeat image
+ mReference.get().mRepeatButton.updateRepeatState();
+ // Set the shuffle image
+ mReference.get().mShuffleButton.updateShuffleState();
+ }
+ }
+ }
+
+}
diff --git a/src/com/andrew/apollo/ui/activities/BaseActivity.java b/src/com/andrew/apollo/ui/activities/BaseActivity.java
new file mode 100644
index 0000000..4f02e43
--- /dev/null
+++ b/src/com/andrew/apollo/ui/activities/BaseActivity.java
@@ -0,0 +1,463 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.ui.activities;
+
+import static com.andrew.apollo.utils.MusicUtils.mService;
+
+import android.app.SearchManager;
+import android.app.SearchableInfo;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.media.AudioManager;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.view.View;
+import android.view.Window;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+import com.actionbarsherlock.widget.SearchView;
+import com.actionbarsherlock.widget.SearchView.OnQueryTextListener;
+import com.andrew.apollo.IApolloService;
+import com.andrew.apollo.MusicPlaybackService;
+import com.andrew.apollo.MusicStateListener;
+import com.andrew.apollo.R;
+import com.andrew.apollo.utils.ApolloUtils;
+import com.andrew.apollo.utils.Lists;
+import com.andrew.apollo.utils.MusicUtils;
+import com.andrew.apollo.utils.MusicUtils.ServiceToken;
+import com.andrew.apollo.utils.NavUtils;
+import com.andrew.apollo.utils.ThemeUtils;
+import com.andrew.apollo.widgets.PlayPauseButton;
+import com.andrew.apollo.widgets.RepeatButton;
+import com.andrew.apollo.widgets.ShuffleButton;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+
+/**
+ * A base {@link SherlockFragmentActivity} used to update the bottom bar and
+ * bind to Apollo's service.
+ * <p>
+ * {@link HomeActivity} extends from this skeleton.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public abstract class BaseActivity extends SherlockFragmentActivity implements ServiceConnection {
+
+ /**
+ * Playstate and meta change listener
+ */
+ private final ArrayList<MusicStateListener> mMusicStateListener = Lists.newArrayList();
+
+ /**
+ * The service token
+ */
+ private ServiceToken mToken;
+
+ /**
+ * Play and pause button (BAB)
+ */
+ private PlayPauseButton mPlayPauseButton;
+
+ /**
+ * Repeat button (BAB)
+ */
+ private RepeatButton mRepeatButton;
+
+ /**
+ * Shuffle button (BAB)
+ */
+ private ShuffleButton mShuffleButton;
+
+ /**
+ * Track name (BAB)
+ */
+ private TextView mTrackName;
+
+ /**
+ * Artist name (BAB)
+ */
+ private TextView mArtistName;
+
+ /**
+ * Album art (BAB)
+ */
+ private ImageView mAlbumArt;
+
+ /**
+ * Broadcast receiver
+ */
+ private PlaybackStatus mPlaybackStatus;
+
+ /**
+ * Keeps track of the back button being used
+ */
+ private boolean mIsBackPressed = false;
+
+ /**
+ * Theme resources
+ */
+ protected ThemeUtils mResources;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Title bar shows up in gingerbread, I'm too tired to figure out why.
+ if (!ApolloUtils.hasHoneycomb()) {
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+ }
+
+ // Initialze the theme resources
+ mResources = new ThemeUtils(this);
+
+ // Set the overflow style
+ mResources.setOverflowStyle(this);
+
+ // Fade it in
+ overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
+
+ // Control the media volume
+ setVolumeControlStream(AudioManager.STREAM_MUSIC);
+
+ // Bind Apollo's service
+ mToken = MusicUtils.bindToService(this, this);
+
+ // Initialize the broadcast receiver
+ mPlaybackStatus = new PlaybackStatus(this);
+
+ // Theme the action bar
+ mResources.themeActionBar(getSupportActionBar(), getString(R.string.app_name));
+
+ // Set the layout
+ setContentView(setContentView());
+
+ // Initialze the bottom action bar
+ initBottomActionBar();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onServiceConnected(final ComponentName name, final IBinder service) {
+ mService = IApolloService.Stub.asInterface(service);
+ // Set the playback drawables
+ updatePlaybackControls();
+ // Current info
+ updateBottomActionBarInfo();
+ // Update the favorites icon
+ invalidateOptionsMenu();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onServiceDisconnected(final ComponentName name) {
+ mService = null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onCreateOptionsMenu(final Menu menu) {
+ // Search view
+ getSupportMenuInflater().inflate(R.menu.search, menu);
+ // Settings
+ getSupportMenuInflater().inflate(R.menu.activity_base, menu);
+ // Theme the search icon
+ mResources.setSearchIcon(menu);
+
+ final SearchView searchView = (SearchView)menu.findItem(R.id.menu_search).getActionView();
+ // Add voice search
+ final SearchManager searchManager = (SearchManager)getSystemService(Context.SEARCH_SERVICE);
+ final SearchableInfo searchableInfo = searchManager.getSearchableInfo(getComponentName());
+ searchView.setSearchableInfo(searchableInfo);
+ // Perform the search
+ searchView.setOnQueryTextListener(new OnQueryTextListener() {
+
+ @Override
+ public boolean onQueryTextSubmit(final String query) {
+ // Open the search activity
+ NavUtils.openSearch(BaseActivity.this, query);
+ return true;
+ }
+
+ @Override
+ public boolean onQueryTextChange(final String newText) {
+ // Nothing to do
+ return false;
+ }
+ });
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.menu_settings:
+ // Settings
+ NavUtils.openSettings(this);
+ return true;
+
+ default:
+ break;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onResume() {
+ super.onResume();
+ // Hide Apollo's notification
+ MusicUtils.killForegroundService(this);
+ // Set the playback drawables
+ updatePlaybackControls();
+ // Current info
+ updateBottomActionBarInfo();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onStart() {
+ super.onStart();
+ final IntentFilter filter = new IntentFilter();
+ // Play and pause changes
+ filter.addAction(MusicPlaybackService.PLAYSTATE_CHANGED);
+ // Shuffle and repeat changes
+ filter.addAction(MusicPlaybackService.SHUFFLEMODE_CHANGED);
+ filter.addAction(MusicPlaybackService.REPEATMODE_CHANGED);
+ // Track changes
+ filter.addAction(MusicPlaybackService.META_CHANGED);
+ // Update a list, probably the playlist fragment's
+ filter.addAction(MusicPlaybackService.REFRESH);
+ registerReceiver(mPlaybackStatus, filter);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onPause() {
+ super.onPause();
+ // Show Apollo's notification
+ if (MusicUtils.isPlaying()) {
+ if (ApolloUtils.isApplicationSentToBackground(this) || mIsBackPressed
+ && this instanceof HomeActivity) {
+ MusicUtils.startBackgroundService(this);
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ // Unbind from the service
+ if (mToken != null) {
+ MusicUtils.unbindFromService(mToken);
+ mToken = null;
+ }
+
+ // Unregister the receiver
+ try {
+ unregisterReceiver(mPlaybackStatus);
+ } catch (final Throwable e) {
+ //$FALL-THROUGH$
+ }
+
+ // Remove any music status listeners
+ mMusicStateListener.clear();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onBackPressed() {
+ super.onBackPressed();
+ mIsBackPressed = true;
+ }
+
+ /**
+ * Initializes the items in the bottom action bar.
+ */
+ private void initBottomActionBar() {
+ // Play and pause button
+ mPlayPauseButton = (PlayPauseButton)findViewById(R.id.action_button_play);
+ // Shuffle button
+ mShuffleButton = (ShuffleButton)findViewById(R.id.action_button_shuffle);
+ // Repeat button
+ mRepeatButton = (RepeatButton)findViewById(R.id.action_button_repeat);
+ // Track name
+ mTrackName = (TextView)findViewById(R.id.bottom_action_bar_line_one);
+ // Artist name
+ mArtistName = (TextView)findViewById(R.id.bottom_action_bar_line_two);
+ // Album art
+ mAlbumArt = (ImageView)findViewById(R.id.bottom_action_bar_album_art);
+ // Open to the currently playing album profile
+ mAlbumArt.setOnClickListener(mOpenCurrentAlbumProfile);
+ // Bottom action bar
+ final LinearLayout bottomActionBar = (LinearLayout)findViewById(R.id.bottom_action_bar);
+ // Display the now playing screen or shuffle if this isn't anything
+ // playing
+ bottomActionBar.setOnClickListener(mOpenNowPlaying);
+ }
+
+ /**
+ * Sets the track name, album name, and album art.
+ */
+ private void updateBottomActionBarInfo() {
+ // Set the track name
+ mTrackName.setText(MusicUtils.getTrackName());
+ // Set the artist name
+ mArtistName.setText(MusicUtils.getArtistName());
+ // Set the album art
+ ApolloUtils.getImageFetcher(this).loadCurrentArtwork(mAlbumArt);
+ }
+
+ /**
+ * Sets the correct drawable states for the playback controls.
+ */
+ private void updatePlaybackControls() {
+ // Set the play and pause image
+ mPlayPauseButton.updateState();
+ // Set the shuffle image
+ mShuffleButton.updateShuffleState();
+ // Set the repeat image
+ mRepeatButton.updateRepeatState();
+ }
+
+ /**
+ * Opens the album profile of the currently playing album
+ */
+ private final View.OnClickListener mOpenCurrentAlbumProfile = new View.OnClickListener() {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onClick(final View v) {
+ NavUtils.openAlbumProfile(BaseActivity.this, MusicUtils.getAlbumName(),
+ MusicUtils.getArtistName());
+ if (BaseActivity.this instanceof ProfileActivity) {
+ finish();
+ }
+ }
+ };
+
+ /**
+ * Opens the now playing screen
+ */
+ private final View.OnClickListener mOpenNowPlaying = new View.OnClickListener() {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onClick(final View v) {
+ if (MusicUtils.getCurrentAudioId() != -1) {
+ NavUtils.openAudioPlayer(BaseActivity.this);
+ } else {
+ MusicUtils.shuffleAll(BaseActivity.this);
+ }
+ }
+ };
+
+ /**
+ * Used to monitor the state of playback
+ */
+ private final static class PlaybackStatus extends BroadcastReceiver {
+
+ private final WeakReference<BaseActivity> mReference;
+
+ /**
+ * Constructor of <code>PlaybackStatus</code>
+ */
+ public PlaybackStatus(final BaseActivity activity) {
+ mReference = new WeakReference<BaseActivity>(activity);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onReceive(final Context context, final Intent intent) {
+ final String action = intent.getAction();
+ if (action.equals(MusicPlaybackService.META_CHANGED)) {
+ // Current info
+ mReference.get().updateBottomActionBarInfo();
+ // Update the favorites icon
+ mReference.get().invalidateOptionsMenu();
+ // Let the listener know to the meta chnaged
+ for (final MusicStateListener listener : mReference.get().mMusicStateListener) {
+ if (listener != null) {
+ listener.onMetaChanged();
+ }
+ }
+ } else if (action.equals(MusicPlaybackService.PLAYSTATE_CHANGED)) {
+ // Set the play and pause image
+ mReference.get().mPlayPauseButton.updateState();
+ } else if (action.equals(MusicPlaybackService.REPEATMODE_CHANGED)
+ || action.equals(MusicPlaybackService.SHUFFLEMODE_CHANGED)) {
+ // Set the repeat image
+ mReference.get().mRepeatButton.updateRepeatState();
+ // Set the shuffle image
+ mReference.get().mShuffleButton.updateShuffleState();
+ } else if (action.equals(MusicPlaybackService.REFRESH)) {
+ // Let the listener know to update a list
+ for (final MusicStateListener listener : mReference.get().mMusicStateListener) {
+ if (listener != null) {
+ listener.restartLoader();
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * @param status The {@link MusicStateListener} to use
+ */
+ public void setMusicStateListenerListener(final MusicStateListener status) {
+ if (status != null) {
+ mMusicStateListener.add(status);
+ }
+ }
+
+ /**
+ * @return The resource ID to be inflated.
+ */
+ public abstract int setContentView();
+}
diff --git a/src/com/andrew/apollo/ui/activities/HomeActivity.java b/src/com/andrew/apollo/ui/activities/HomeActivity.java
new file mode 100644
index 0000000..c21444f
--- /dev/null
+++ b/src/com/andrew/apollo/ui/activities/HomeActivity.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.ui.activities;
+
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.view.ViewPager;
+
+import com.andrew.apollo.R;
+import com.andrew.apollo.ui.fragments.phone.MusicBrowserPhoneFragment;
+
+/**
+ * This class is used to display the {@link ViewPager} used to swipe between the
+ * main {@link Fragment}s used to browse the user's music.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class HomeActivity extends BaseActivity {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Load the music browser fragment
+ if (savedInstanceState == null) {
+ getSupportFragmentManager().beginTransaction()
+ .replace(R.id.activity_base_content, new MusicBrowserPhoneFragment()).commit();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int setContentView() {
+ return R.layout.activity_base;
+ }
+}
diff --git a/src/com/andrew/apollo/ui/activities/ProfileActivity.java b/src/com/andrew/apollo/ui/activities/ProfileActivity.java
new file mode 100644
index 0000000..c7937e1
--- /dev/null
+++ b/src/com/andrew/apollo/ui/activities/ProfileActivity.java
@@ -0,0 +1,685 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.ui.activities;
+
+import android.app.SearchManager;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.provider.MediaStore;
+import android.support.v4.view.ViewPager;
+import android.support.v4.view.ViewPager.OnPageChangeListener;
+import android.view.View;
+
+import com.actionbarsherlock.app.ActionBar;
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+import com.andrew.apollo.Config;
+import com.andrew.apollo.R;
+import com.andrew.apollo.adapters.PagerAdapter;
+import com.andrew.apollo.cache.ImageFetcher;
+import com.andrew.apollo.menu.PhotoSelectionDialog;
+import com.andrew.apollo.menu.PhotoSelectionDialog.ProfileType;
+import com.andrew.apollo.ui.fragments.profile.AlbumSongFragment;
+import com.andrew.apollo.ui.fragments.profile.ArtistAlbumFragment;
+import com.andrew.apollo.ui.fragments.profile.ArtistSongFragment;
+import com.andrew.apollo.ui.fragments.profile.FavoriteFragment;
+import com.andrew.apollo.ui.fragments.profile.GenreSongFragment;
+import com.andrew.apollo.ui.fragments.profile.LastAddedFragment;
+import com.andrew.apollo.ui.fragments.profile.PlaylistSongFragment;
+import com.andrew.apollo.utils.ApolloUtils;
+import com.andrew.apollo.utils.MusicUtils;
+import com.andrew.apollo.utils.NavUtils;
+import com.andrew.apollo.utils.PreferenceUtils;
+import com.andrew.apollo.utils.SortOrder;
+import com.andrew.apollo.widgets.ProfileTabCarousel;
+import com.andrew.apollo.widgets.ProfileTabCarousel.Listener;
+
+/**
+ * The {@link SherlockFragmentActivity} is used to display the data for specific
+ * artists, albums, playlists, and genres. This class is only used on phones.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class ProfileActivity extends BaseActivity implements OnPageChangeListener, Listener {
+
+ private static final int NEW_PHOTO = 1;
+
+ /**
+ * The Bundle to pass into the Fragments
+ */
+ private Bundle mArguments;
+
+ /**
+ * View pager
+ */
+ private ViewPager mViewPager;
+
+ /**
+ * Pager adpater
+ */
+ private PagerAdapter mPagerAdapter;
+
+ /**
+ * Profile header carousel
+ */
+ private ProfileTabCarousel mTabCarousel;
+
+ /**
+ * MIME type of the profile
+ */
+ private String mType;
+
+ /**
+ * Artist name passed into the class
+ */
+ private String mArtistName;
+
+ /**
+ * The main profile title
+ */
+ private String mProfileName;
+
+ /**
+ * Image cache
+ */
+ private ImageFetcher mImageFetcher;
+
+ private PreferenceUtils mPreferences;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Temporay until I can work out a nice landscape layout
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+
+ // Get the preferences
+ mPreferences = PreferenceUtils.getInstace(this);
+
+ // Initialze the image fetcher
+ mImageFetcher = ApolloUtils.getImageFetcher(this);
+
+ // Initialize the Bundle
+ mArguments = savedInstanceState != null ? savedInstanceState : getIntent().getExtras();
+ // Get the MIME type
+ mType = mArguments.getString(Config.MIME_TYPE);
+ // Get the profile title
+ mProfileName = mArguments.getString(Config.NAME);
+ // Get the artist name
+ if (isArtist() || isAlbum()) {
+ mArtistName = mArguments.getString(Config.ARTIST_NAME);
+ }
+
+ // Initialize the pager adapter
+ mPagerAdapter = new PagerAdapter(this);
+
+ // Initialze the carousel
+ mTabCarousel = (ProfileTabCarousel)findViewById(R.id.acivity_profile_base_tab_carousel);
+ mTabCarousel.reset();
+ mTabCarousel.getPhoto().setOnClickListener(new View.OnClickListener() {
+
+ @Override
+ public void onClick(final View v) {
+ ProfileType profileType;
+ if (isArtist()) {
+ profileType = ProfileType.ARTIST;
+ } else if (isAlbum()) {
+ profileType = ProfileType.ALBUM;
+ } else {
+ profileType = ProfileType.OTHER;
+ }
+ PhotoSelectionDialog.newInstance(isArtist() ? mArtistName : mProfileName,
+ profileType).show(getSupportFragmentManager(), "PhotoSelectionDialog");
+ }
+ });
+ // Set up the action bar
+ final ActionBar actionBar = getSupportActionBar();
+ actionBar.setDisplayHomeAsUpEnabled(true);
+
+ /* Set up the artist profile */
+ if (isArtist()) {
+ // Add the carousel images
+ mTabCarousel.setArtistProfileHeader(this, mArtistName);
+
+ // Artist profile fragments
+ mPagerAdapter.add(ArtistSongFragment.class, mArguments);
+ mPagerAdapter.add(ArtistAlbumFragment.class, mArguments);
+
+ // Action bar title
+ mResources.setTitle(mArtistName);
+
+ } else
+ // Set up the album profile
+ if (isAlbum()) {
+ // Add the carousel images
+ mTabCarousel.setAlbumProfileHeader(this, mProfileName, mArtistName);
+
+ // Album profile fragments
+ mPagerAdapter.add(AlbumSongFragment.class, mArguments);
+
+ // Action bar title = album name
+ mResources.setTitle(mProfileName);
+ // Action bar subtitle = year released
+ mResources.setSubtitle(mArguments.getString(Config.ALBUM_YEAR));
+ } else
+ // Set up the favorites profile
+ if (isFavorites()) {
+ // Add the carousel images
+ mTabCarousel.setPlaylistOrGenreProfileHeader(this, mProfileName);
+
+ // Favorite fragment
+ mPagerAdapter.add(FavoriteFragment.class, null);
+
+ // Action bar title = Favorites
+ mResources.setTitle(mProfileName);
+ } else
+ // Set up the last added profile
+ if (isLastAdded()) {
+ // Add the carousel images
+ mTabCarousel.setPlaylistOrGenreProfileHeader(this, mProfileName);
+
+ // Last added fragment
+ mPagerAdapter.add(LastAddedFragment.class, null);
+
+ // Action bar title = Last added
+ mResources.setTitle(mProfileName);
+ } else
+ // Set up the user playlist profile
+ if (isPlaylist()) {
+ // Add the carousel images
+ mTabCarousel.setPlaylistOrGenreProfileHeader(this, mProfileName);
+
+ // Playlist profile fragments
+ mPagerAdapter.add(PlaylistSongFragment.class, mArguments);
+
+ // Action bar title = playlist name
+ mResources.setTitle(mProfileName);
+ } else
+ // Set up the genre profile
+ if (isGenre()) {
+ // Add the carousel images
+ mTabCarousel.setPlaylistOrGenreProfileHeader(this, mProfileName);
+
+ // Genre profile fragments
+ mPagerAdapter.add(GenreSongFragment.class, mArguments);
+
+ // Action bar title = playlist name
+ mResources.setTitle(mProfileName);
+ }
+
+ // Initialize the ViewPager
+ mViewPager = (ViewPager)findViewById(R.id.acivity_profile_base_pager);
+ // Attch the adapter
+ mViewPager.setAdapter(mPagerAdapter);
+ // Offscreen limit
+ mViewPager.setOffscreenPageLimit(mPagerAdapter.getCount() - 1);
+ // Attach the page change listener
+ mViewPager.setOnPageChangeListener(this);
+ // Attach the carousel listener
+ mTabCarousel.setListener(this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mImageFetcher.flush();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int setContentView() {
+ return R.layout.activity_profile_base;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onPrepareOptionsMenu(final Menu menu) {
+ // Theme the add to home screen icon
+ mResources.setAddToHomeScreenIcon(menu);
+ // Set the shuffle all title to "play all" if a playlist.
+ final MenuItem shuffle = menu.findItem(R.id.menu_shuffle);
+ String title = null;
+ if (isFavorites() || isLastAdded() || isPlaylist()) {
+ title = getString(R.string.menu_play_all);
+ } else {
+ title = getString(R.string.menu_shuffle);
+ }
+ shuffle.setTitle(title);
+ return super.onPrepareOptionsMenu(menu);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onCreateOptionsMenu(final Menu menu) {
+ // Pin to Home screen
+ getSupportMenuInflater().inflate(R.menu.add_to_homescreen, menu);
+ // Shuffle
+ getSupportMenuInflater().inflate(R.menu.shuffle, menu);
+ // Sort orders
+ if (isArtistSongPage()) {
+ getSupportMenuInflater().inflate(R.menu.artist_song_sort_by, menu);
+ } else if (isArtistAlbumPage()) {
+ getSupportMenuInflater().inflate(R.menu.artist_album_sort_by, menu);
+ } else if (isAlbum()) {
+ getSupportMenuInflater().inflate(R.menu.album_song_sort_by, menu);
+ }
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ // If an album profile, go up to the artist profile
+ if (isAlbum()) {
+ NavUtils.openArtistProfile(this, mArtistName);
+ finish();
+ } else {
+ // Otherwise just go back
+ goBack();
+ }
+ return true;
+ case R.id.menu_add_to_homescreen:
+ // Place the artist, album, genre, or playlist onto the Home
+ // screen. Definitely one of my favorite features.
+ final String name = isArtist() ? mArtistName : mProfileName;
+ final Long id = mArguments.getLong(Config.ID);
+ ApolloUtils.createShortcutIntent(name, id, mType, this);
+ return true;
+ case R.id.menu_shuffle:
+ final String stringId = String.valueOf(mArguments.getLong(Config.ID));
+ long[] list = null;
+ if (isArtist()) {
+ list = MusicUtils.getSongListForArtist(this, stringId);
+ } else if (isAlbum()) {
+ list = MusicUtils.getSongListForAlbum(this, stringId);
+ } else if (isGenre()) {
+ list = MusicUtils.getSongListForGenre(this, stringId);
+ }
+ if (isPlaylist()) {
+ MusicUtils.playPlaylist(this, stringId);
+ } else if (isFavorites()) {
+ MusicUtils.playFavorites(this);
+ } else if (isLastAdded()) {
+ MusicUtils.playLastAdded(this);
+ } else {
+ if (list != null && list.length > 0) {
+ MusicUtils.playAll(this, list, 0, true);
+ }
+ }
+ return true;
+ case R.id.menu_sort_by_az:
+ if (isArtistSongPage()) {
+ mPreferences.setArtistSongSortOrder(SortOrder.ArtistSongSortOrder.SONG_A_Z);
+ getArtistSongFragment().refresh();
+ } else if (isArtistAlbumPage()) {
+ mPreferences.setArtistAlbumSortOrder(SortOrder.ArtistAlbumSortOrder.ALBUM_A_Z);
+ getArtistAlbumFragment().refresh();
+ } else {
+ mPreferences.setAlbumSongSortOrder(SortOrder.AlbumSongSortOrder.SONG_A_Z);
+ getAlbumSongFragment().refresh();
+ }
+ return true;
+ case R.id.menu_sort_by_za:
+ if (isArtistSongPage()) {
+ mPreferences.setArtistSongSortOrder(SortOrder.ArtistSongSortOrder.SONG_Z_A);
+ getArtistSongFragment().refresh();
+ } else if (isArtistAlbumPage()) {
+ mPreferences.setArtistAlbumSortOrder(SortOrder.ArtistAlbumSortOrder.ALBUM_Z_A);
+ getArtistAlbumFragment().refresh();
+ } else {
+ mPreferences.setAlbumSongSortOrder(SortOrder.AlbumSongSortOrder.SONG_Z_A);
+ getAlbumSongFragment().refresh();
+ }
+ return true;
+ case R.id.menu_sort_by_album:
+ if (isArtistSongPage()) {
+ mPreferences.setArtistSongSortOrder(SortOrder.ArtistSongSortOrder.SONG_ALBUM);
+ getArtistSongFragment().refresh();
+ }
+ return true;
+ case R.id.menu_sort_by_year:
+ if (isArtistSongPage()) {
+ mPreferences.setArtistSongSortOrder(SortOrder.ArtistSongSortOrder.SONG_YEAR);
+ getArtistSongFragment().refresh();
+ } else if (isArtistAlbumPage()) {
+ mPreferences.setArtistAlbumSortOrder(SortOrder.ArtistAlbumSortOrder.ALBUM_YEAR);
+ getArtistAlbumFragment().refresh();
+ }
+ return true;
+ case R.id.menu_sort_by_duration:
+ if (isArtistSongPage()) {
+ mPreferences
+ .setArtistSongSortOrder(SortOrder.ArtistSongSortOrder.SONG_DURATION);
+ getArtistSongFragment().refresh();
+ } else {
+ mPreferences.setAlbumSongSortOrder(SortOrder.AlbumSongSortOrder.SONG_DURATION);
+ getAlbumSongFragment().refresh();
+ }
+ return true;
+ case R.id.menu_sort_by_date_added:
+ if (isArtistSongPage()) {
+ mPreferences.setArtistSongSortOrder(SortOrder.ArtistSongSortOrder.SONG_DATE);
+ getArtistSongFragment().refresh();
+ }
+ return true;
+ case R.id.menu_sort_by_track_list:
+ mPreferences.setAlbumSongSortOrder(SortOrder.AlbumSongSortOrder.SONG_TRACK_LIST);
+ getAlbumSongFragment().refresh();
+ return true;
+ default:
+ break;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onSaveInstanceState(final Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putAll(mArguments);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onBackPressed() {
+ super.onBackPressed();
+ goBack();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onPageScrolled(final int position, final float positionOffset,
+ final int positionOffsetPixels) {
+ if (mViewPager.isFakeDragging()) {
+ return;
+ }
+
+ final int scrollToX = (int)((position + positionOffset) * mTabCarousel
+ .getAllowedHorizontalScrollLength());
+ mTabCarousel.scrollTo(scrollToX, 0);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onPageSelected(final int position) {
+ mTabCarousel.setCurrentTab(position);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onPageScrollStateChanged(final int state) {
+ if (state == ViewPager.SCROLL_STATE_IDLE) {
+ mTabCarousel.restoreYCoordinate(75, mViewPager.getCurrentItem());
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onTouchDown() {
+ if (!mViewPager.isFakeDragging()) {
+ mViewPager.beginFakeDrag();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onTouchUp() {
+ if (mViewPager.isFakeDragging()) {
+ mViewPager.endFakeDrag();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onScrollChanged(final int l, final int t, final int oldl, final int oldt) {
+ if (mViewPager.isFakeDragging()) {
+ mViewPager.fakeDragBy(oldl - l);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onTabSelected(final int position) {
+ mViewPager.setCurrentItem(position);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == NEW_PHOTO) {
+ if (resultCode == RESULT_OK) {
+ final Uri selectedImage = data.getData();
+ final String[] filePathColumn = {
+ MediaStore.Images.Media.DATA
+ };
+
+ Cursor cursor = getContentResolver().query(selectedImage, filePathColumn, null,
+ null, null);
+ if (cursor != null && cursor.moveToFirst()) {
+ final int columnIndex = cursor.getColumnIndex(filePathColumn[0]);
+ final String picturePath = cursor.getString(columnIndex);
+ cursor.close();
+ cursor = null;
+
+ String key = mProfileName;
+ if (isArtist()) {
+ key = mArtistName;
+ } else if (isAlbum()) {
+ key = mProfileName + Config.ALBUM_ART_SUFFIX;
+ }
+
+ final Bitmap bitmap = ImageFetcher.decodeSampledBitmapFromFile(picturePath);
+ mImageFetcher.addBitmapToCache(key, bitmap);
+ if (isAlbum()) {
+ mTabCarousel.getAlbumArt().setImageBitmap(bitmap);
+ } else {
+ mTabCarousel.getPhoto().setImageBitmap(bitmap);
+ }
+ }
+ } else {
+ selectOldPhoto();
+ }
+ }
+ }
+
+ /**
+ * Starts an activity for result that returns an image from the Gallery.
+ */
+ public void selectNewPhoto() {
+ // First remove the old image
+ removeFromCache();
+ // Now open the gallery
+ final Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null);
+ intent.setType("image/*");
+ startActivityForResult(intent, NEW_PHOTO);
+ // Make sure the notfication starts
+ if (MusicUtils.isPlaying()) {
+ MusicUtils.startBackgroundService(this);
+ }
+ }
+
+ /**
+ * Fetchs for the artist or album art, other wise sets the default header
+ * image.
+ */
+ public void selectOldPhoto() {
+ // First remove the old image
+ removeFromCache();
+ // Apply the old photo
+ if (isArtist()) {
+ mTabCarousel.setArtistProfileHeader(this, mArtistName);
+ } else if (isAlbum()) {
+ mTabCarousel.setAlbumProfileHeader(this, mProfileName, mArtistName);
+ } else {
+ mTabCarousel.setPlaylistOrGenreProfileHeader(this, mProfileName);
+ }
+ }
+
+ /**
+ * When the user chooses {@code #selectOldPhoto()} while viewing an album
+ * profile, the image is, most likely, reverted back to the locally found
+ * artwork. This is specifically for fetching the image from Last.fm.
+ */
+ public void fetchAlbumArt() {
+ // First remove the old image
+ removeFromCache();
+ // Fetch for the artwork
+ mTabCarousel.fetchAlbumPhoto(this, mProfileName);
+ }
+
+ /**
+ * Searches Google for the artist or album
+ */
+ public void googleSearch() {
+ String query = mProfileName;
+ if (isArtist()) {
+ query = mArtistName;
+ } else if (isAlbum()) {
+ query = mProfileName + " " + mArtistName;
+ }
+ final Intent googleSearch = new Intent(Intent.ACTION_WEB_SEARCH);
+ googleSearch.putExtra(SearchManager.QUERY, query);
+ startActivity(googleSearch);
+ // Make sure the notification starts.
+ MusicUtils.startBackgroundService(this);
+ }
+
+ /**
+ * Removes the header image from the cache.
+ */
+ private void removeFromCache() {
+ String key = mProfileName;
+ if (isArtist()) {
+ key = mArtistName;
+ } else if (isAlbum()) {
+ key = mProfileName + Config.ALBUM_ART_SUFFIX;
+ }
+ mImageFetcher.removeFromCache(key);
+ // Give the disk cache a little time before requesting a new image.
+ SystemClock.sleep(80);
+ }
+
+ /**
+ * Finishes the activity and overrides the default animation.
+ */
+ private void goBack() {
+ finish();
+ }
+
+ /**
+ * @return True if the MIME type is vnd.android.cursor.dir/artists, false
+ * otherwise.
+ */
+ private final boolean isArtist() {
+ return mType.equals(MediaStore.Audio.Artists.CONTENT_TYPE);
+ }
+
+ /**
+ * @return True if the MIME type is vnd.android.cursor.dir/albums, false
+ * otherwise.
+ */
+ private final boolean isAlbum() {
+ return mType.equals(MediaStore.Audio.Albums.CONTENT_TYPE);
+ }
+
+ /**
+ * @return True if the MIME type is vnd.android.cursor.dir/gere, false
+ * otherwise.
+ */
+ private final boolean isGenre() {
+ return mType.equals(MediaStore.Audio.Genres.CONTENT_TYPE);
+ }
+
+ /**
+ * @return True if the MIME type is vnd.android.cursor.dir/playlist, false
+ * otherwise.
+ */
+ private final boolean isPlaylist() {
+ return mType.equals(MediaStore.Audio.Playlists.CONTENT_TYPE);
+ }
+
+ /**
+ * @return True if the MIME type is "Favorites", false otherwise.
+ */
+ private final boolean isFavorites() {
+ return mType.equals(getString(R.string.playlist_favorites));
+ }
+
+ /**
+ * @return True if the MIME type is "LastAdded", false otherwise.
+ */
+ private final boolean isLastAdded() {
+ return mType.equals(getString(R.string.playlist_last_added));
+ }
+
+ private boolean isArtistSongPage() {
+ return isArtist() && mViewPager.getCurrentItem() == 0;
+ }
+
+ private boolean isArtistAlbumPage() {
+ return isArtist() && mViewPager.getCurrentItem() == 1;
+ }
+
+ private ArtistSongFragment getArtistSongFragment() {
+ return (ArtistSongFragment)mPagerAdapter.getFragment(0);
+ }
+
+ private ArtistAlbumFragment getArtistAlbumFragment() {
+ return (ArtistAlbumFragment)mPagerAdapter.getFragment(1);
+ }
+
+ private AlbumSongFragment getAlbumSongFragment() {
+ return (AlbumSongFragment)mPagerAdapter.getFragment(0);
+ }
+}
diff --git a/src/com/andrew/apollo/ui/activities/SearchActivity.java b/src/com/andrew/apollo/ui/activities/SearchActivity.java
new file mode 100644
index 0000000..13d88cc
--- /dev/null
+++ b/src/com/andrew/apollo/ui/activities/SearchActivity.java
@@ -0,0 +1,593 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.ui.activities;
+
+import static com.andrew.apollo.utils.MusicUtils.mService;
+
+import android.app.SearchManager;
+import android.app.SearchableInfo;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.database.Cursor;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.provider.BaseColumns;
+import android.provider.MediaStore;
+import android.support.v4.app.LoaderManager.LoaderCallbacks;
+import android.support.v4.content.CursorLoader;
+import android.support.v4.content.Loader;
+import android.support.v4.widget.CursorAdapter;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.AbsListView;
+import android.widget.AbsListView.OnScrollListener;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.FrameLayout;
+import android.widget.GridView;
+import android.widget.ImageView.ScaleType;
+import android.widget.TextView;
+
+import com.actionbarsherlock.app.ActionBar;
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+import com.actionbarsherlock.widget.SearchView;
+import com.actionbarsherlock.widget.SearchView.OnQueryTextListener;
+import com.andrew.apollo.IApolloService;
+import com.andrew.apollo.R;
+import com.andrew.apollo.cache.ImageFetcher;
+import com.andrew.apollo.format.PrefixHighlighter;
+import com.andrew.apollo.recycler.RecycleHolder;
+import com.andrew.apollo.ui.MusicHolder;
+import com.andrew.apollo.utils.ApolloUtils;
+import com.andrew.apollo.utils.MusicUtils;
+import com.andrew.apollo.utils.MusicUtils.ServiceToken;
+import com.andrew.apollo.utils.NavUtils;
+import com.andrew.apollo.utils.ThemeUtils;
+
+import java.util.Locale;
+
+/**
+ * Provides the search interface for Apollo.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class SearchActivity extends SherlockFragmentActivity implements LoaderCallbacks<Cursor>,
+ OnScrollListener, OnQueryTextListener, OnItemClickListener, ServiceConnection {
+ /**
+ * Grid view column count. ONE - list, TWO - normal grid
+ */
+ private static final int ONE = 1, TWO = 2;
+
+ /**
+ * The service token
+ */
+ private ServiceToken mToken;
+
+ /**
+ * The query
+ */
+ private String mFilterString;
+
+ /**
+ * Grid view
+ */
+ private GridView mGridView;
+
+ /**
+ * List view adapter
+ */
+ private SearchAdapter mAdapter;
+
+ // Used the filter the user's music
+ private SearchView mSearchView;
+
+ /**
+ * Theme resources
+ */
+ private ThemeUtils mResources;
+
+ /**
+ * {@inheritDoc}
+ */
+ @SuppressWarnings("deprecation")
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Title bar shows up in gingerbread, I'm too tired to figure out why.
+ if (!ApolloUtils.hasHoneycomb()) {
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+ }
+ // Initialze the theme resources
+ mResources = new ThemeUtils(this);
+ // Set the overflow style
+ mResources.setOverflowStyle(this);
+
+ // Fade it in
+ overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
+
+ // Control the media volume
+ setVolumeControlStream(AudioManager.STREAM_MUSIC);
+
+ // Bind Apollo's service
+ mToken = MusicUtils.bindToService(this, this);
+
+ // Theme the action bar
+ final ActionBar actionBar = getSupportActionBar();
+ mResources.themeActionBar(actionBar, getString(R.string.app_name));
+ actionBar.setDisplayHomeAsUpEnabled(true);
+
+ // Set the layout
+ setContentView(R.layout.grid_base);
+
+ // Give the background a little UI
+ final FrameLayout background = (FrameLayout)findViewById(R.id.grid_base_container);
+ background.setBackgroundDrawable(getResources().getDrawable(R.drawable.pager_background));
+
+ // Get the query
+ final String query = getIntent().getStringExtra(SearchManager.QUERY);
+ mFilterString = !TextUtils.isEmpty(query) ? query : null;
+
+ // Action bar subtitle
+ mResources.setSubtitle("\"" + mFilterString + "\"");
+
+ // Initialize the adapter
+ mAdapter = new SearchAdapter(this);
+ // Set the prefix
+ mAdapter.setPrefix(mFilterString);
+ // Initialze the list
+ mGridView = (GridView)findViewById(R.id.grid_base);
+ // Bind the data
+ mGridView.setAdapter(mAdapter);
+ // Recycle the data
+ mGridView.setRecyclerListener(new RecycleHolder());
+ // Seepd up scrolling
+ mGridView.setOnScrollListener(this);
+ mGridView.setOnItemClickListener(this);
+ if (ApolloUtils.isLandscape(this)) {
+ mGridView.setNumColumns(TWO);
+ } else {
+ mGridView.setNumColumns(ONE);
+ }
+ // Prepare the loader. Either re-connect with an existing one,
+ // or start a new one.
+ getSupportLoaderManager().initLoader(0, null, this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onNewIntent(final Intent intent) {
+ super.onNewIntent(intent);
+ final String query = intent.getStringExtra(SearchManager.QUERY);
+ mFilterString = !TextUtils.isEmpty(query) ? query : null;
+ // Set the prefix
+ mAdapter.setPrefix(mFilterString);
+ getSupportLoaderManager().restartLoader(0, null, this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onCreateOptionsMenu(final Menu menu) {
+ // Search view
+ getSupportMenuInflater().inflate(R.menu.search, menu);
+ // Theme the search icon
+ mResources.setSearchIcon(menu);
+
+ // Filter the list the user is looking it via SearchView
+ mSearchView = (SearchView)menu.findItem(R.id.menu_search).getActionView();
+ mSearchView.setOnQueryTextListener(this);
+
+ // Add voice search
+ final SearchManager searchManager = (SearchManager)getSystemService(Context.SEARCH_SERVICE);
+ final SearchableInfo searchableInfo = searchManager.getSearchableInfo(getComponentName());
+ mSearchView.setSearchableInfo(searchableInfo);
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onResume() {
+ super.onResume();
+ MusicUtils.killForegroundService(this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onPause() {
+ super.onPause();
+ if (MusicUtils.isPlaying() && ApolloUtils.isApplicationSentToBackground(this)) {
+ MusicUtils.startBackgroundService(this);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ // Unbind from the service
+ if (mService != null) {
+ MusicUtils.unbindFromService(mToken);
+ mToken = null;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ finish();
+ return true;
+ default:
+ break;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Loader<Cursor> onCreateLoader(final int id, final Bundle args) {
+ final Uri uri = Uri.parse("content://media/external/audio/search/fancy/"
+ + Uri.encode(mFilterString));
+ final String[] projection = new String[] {
+ BaseColumns._ID, MediaStore.Audio.Media.MIME_TYPE, MediaStore.Audio.Artists.ARTIST,
+ MediaStore.Audio.Albums.ALBUM, MediaStore.Audio.Media.TITLE, "data1", "data2"
+ };
+ return new CursorLoader(this, uri, projection, null, null, null);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoadFinished(final Loader<Cursor> loader, final Cursor data) {
+ if (data == null || data.isClosed() || data.getCount() <= 0) {
+ // Set the empty text
+ final TextView empty = (TextView)findViewById(R.id.empty);
+ empty.setText(getString(R.string.empty_search));
+ mGridView.setEmptyView(empty);
+ return;
+ }
+ // Swap the new cursor in. (The framework will take care of closing the
+ // old cursor once we return.)
+ mAdapter.swapCursor(data);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoaderReset(final Loader<Cursor> loader) {
+ // This is called when the last Cursor provided to onLoadFinished()
+ // above is about to be closed. We need to make sure we are no
+ // longer using it.
+ mAdapter.swapCursor(null);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onScrollStateChanged(final AbsListView view, final int scrollState) {
+ // Pause disk cache access to ensure smoother scrolling
+ if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_FLING
+ || scrollState == AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
+ mAdapter.setPauseDiskCache(true);
+ } else {
+ mAdapter.setPauseDiskCache(false);
+ mAdapter.notifyDataSetChanged();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onQueryTextSubmit(final String query) {
+ if (TextUtils.isEmpty(query)) {
+ return false;
+ }
+ // When the search is "committed" by the user, then hide the keyboard so
+ // the user can
+ // more easily browse the list of results.
+ if (mSearchView != null) {
+ final InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
+ if (imm != null) {
+ imm.hideSoftInputFromWindow(mSearchView.getWindowToken(), 0);
+ }
+ mSearchView.clearFocus();
+ }
+ // Action bar subtitle
+ mResources.setSubtitle("\"" + mFilterString + "\"");
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onQueryTextChange(final String newText) {
+ if (TextUtils.isEmpty(newText)) {
+ return false;
+ }
+ // Called when the action bar search text has changed. Update
+ // the search filter, and restart the loader to do a new query
+ // with this filter.
+ mFilterString = !TextUtils.isEmpty(newText) ? newText : null;
+ // Set the prefix
+ mAdapter.setPrefix(mFilterString);
+ getSupportLoaderManager().restartLoader(0, null, this);
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onItemClick(final AdapterView<?> parent, final View view, final int position,
+ final long id) {
+ Cursor cursor = mAdapter.getCursor();
+ cursor.moveToPosition(position);
+ if (cursor.isBeforeFirst() || cursor.isAfterLast()) {
+ return;
+ }
+ // Get the MIME type
+ final String mimeType = cursor.getString(cursor
+ .getColumnIndexOrThrow(MediaStore.Audio.Media.MIME_TYPE));
+
+ // If it's an artist, open the artist profile
+ if ("artist".equals(mimeType)) {
+ NavUtils.openArtistProfile(this,
+ cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Artists.ARTIST)));
+ } else if ("album".equals(mimeType)) {
+ // If it's an album, open the album profile
+ NavUtils.openAlbumProfile(this,
+ cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ALBUM)),
+ cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ARTIST)));
+ } else if (position >= 0 && id >= 0) {
+ // If it's a song, play it and leave
+ final long[] list = new long[] {
+ id
+ };
+ MusicUtils.playAll(this, list, 0, false);
+ }
+
+ // Close it up
+ cursor.close();
+ cursor = null;
+ // All done
+ finish();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onServiceConnected(final ComponentName name, final IBinder service) {
+ mService = IApolloService.Stub.asInterface(service);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onServiceDisconnected(final ComponentName name) {
+ mService = null;
+ }
+
+ /**
+ * Used to populate the list view with the search results.
+ */
+ private static final class SearchAdapter extends CursorAdapter {
+
+ /**
+ * Number of views (ImageView and TextView)
+ */
+ private static final int VIEW_TYPE_COUNT = 2;
+
+ /**
+ * Image cache and image fetcher
+ */
+ private final ImageFetcher mImageFetcher;
+
+ /**
+ * Highlights the query
+ */
+ private final PrefixHighlighter mHighlighter;
+
+ /**
+ * The prefix that's highlighted
+ */
+ private char[] mPrefix;
+
+ /**
+ * Constructor for <code>SearchAdapter</code>
+ *
+ * @param context The {@link Context} to use.
+ */
+ public SearchAdapter(final Context context) {
+ super(context, null, false);
+ // Initialize the cache & image fetcher
+ mImageFetcher = ApolloUtils.getImageFetcher((SherlockFragmentActivity)context);
+ // Create the prefix highlighter
+ mHighlighter = new PrefixHighlighter(context);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void bindView(final View convertView, final Context context, final Cursor cursor) {
+ /* Recycle ViewHolder's items */
+ MusicHolder holder = (MusicHolder)convertView.getTag();
+ if (holder == null) {
+ holder = new MusicHolder(convertView);
+ convertView.setTag(holder);
+ }
+
+ // Get the MIME type
+ final String mimetype = cursor.getString(cursor
+ .getColumnIndexOrThrow(MediaStore.Audio.Media.MIME_TYPE));
+
+ if (mimetype.equals("artist")) {
+ holder.mImage.get().setScaleType(ScaleType.CENTER_CROP);
+
+ // Get the artist name
+ final String artist = cursor.getString(cursor
+ .getColumnIndexOrThrow(MediaStore.Audio.Artists.ARTIST));
+ holder.mLineOne.get().setText(artist);
+
+ // Get the album count
+ final String albumCount = cursor.getString(cursor.getColumnIndexOrThrow("data1"));
+ holder.mLineTwo.get().setText(
+ MusicUtils.makeLabel(context, R.plurals.Nalbums, albumCount));
+
+ // Get the song count
+ final String songCount = cursor.getString(cursor.getColumnIndexOrThrow("data2"));
+ holder.mLineThree.get().setText(
+ MusicUtils.makeLabel(context, R.plurals.Nsongs, songCount));
+
+ // Asynchronously load the artist image into the adapter
+ mImageFetcher.loadArtistImage(artist, holder.mImage.get());
+
+ // Highlght the query
+ mHighlighter.setText(holder.mLineOne.get(), artist, mPrefix);
+ } else if (mimetype.equals("album")) {
+ holder.mImage.get().setScaleType(ScaleType.FIT_XY);
+
+ // Get the Id of the album
+ final String id = cursor.getString(cursor
+ .getColumnIndexOrThrow(MediaStore.Audio.Albums._ID));
+
+ // Get the album name
+ final String album = cursor.getString(cursor
+ .getColumnIndexOrThrow(MediaStore.Audio.Albums.ALBUM));
+ holder.mLineOne.get().setText(album);
+
+ // Get the artist name
+ final String artist = cursor.getString(cursor
+ .getColumnIndexOrThrow(MediaStore.Audio.Albums.ARTIST));
+ holder.mLineTwo.get().setText(artist);
+
+ // Asynchronously load the album images into the adapter
+ mImageFetcher.loadAlbumImage(artist, album, id, holder.mImage.get());
+ // Asynchronously load the artist image into the adapter
+ mImageFetcher.loadArtistImage(artist, holder.mBackground.get());
+
+ // Highlght the query
+ mHighlighter.setText(holder.mLineOne.get(), album, mPrefix);
+
+ } else if (mimetype.startsWith("audio/") || mimetype.equals("application/ogg")
+ || mimetype.equals("application/x-ogg")) {
+ holder.mImage.get().setScaleType(ScaleType.FIT_XY);
+ holder.mImage.get().setImageResource(R.drawable.header_temp);
+
+ // Get the track name
+ final String track = cursor.getString(cursor
+ .getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE));
+ holder.mLineOne.get().setText(track);
+
+ // Get the album name
+ final String album = cursor.getString(cursor
+ .getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM));
+ holder.mLineTwo.get().setText(album);
+
+ final String artist = cursor.getString(cursor
+ .getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST));
+ // Asynchronously load the artist image into the adapter
+ mImageFetcher.loadArtistImage(artist, holder.mBackground.get());
+ holder.mLineThree.get().setText(artist);
+
+ // Highlght the query
+ mHighlighter.setText(holder.mLineOne.get(), track, mPrefix);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View newView(final Context context, final Cursor cursor, final ViewGroup parent) {
+ return ((SherlockFragmentActivity)context).getLayoutInflater().inflate(
+ R.layout.list_item_detailed, parent, false);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getViewTypeCount() {
+ return VIEW_TYPE_COUNT;
+ }
+
+ /**
+ * @param pause True to temporarily pause the disk cache, false
+ * otherwise.
+ */
+ public void setPauseDiskCache(final boolean pause) {
+ if (mImageFetcher != null) {
+ mImageFetcher.setPauseDiskCache(pause);
+ }
+ }
+
+ /**
+ * @param prefix The query to filter.
+ */
+ public void setPrefix(final CharSequence prefix) {
+ if (!TextUtils.isEmpty(prefix)) {
+ mPrefix = prefix.toString().toUpperCase(Locale.getDefault()).toCharArray();
+ } else {
+ mPrefix = null;
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onScroll(final AbsListView view, final int firstVisibleItem,
+ final int visibleItemCount, final int totalItemCount) {
+ // Nothing to do
+ }
+
+}
diff --git a/src/com/andrew/apollo/ui/activities/SettingsActivity.java b/src/com/andrew/apollo/ui/activities/SettingsActivity.java
new file mode 100644
index 0000000..e22f565
--- /dev/null
+++ b/src/com/andrew/apollo/ui/activities/SettingsActivity.java
@@ -0,0 +1,291 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.ui.activities;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Bundle;
+import android.preference.CheckBoxPreference;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceChangeListener;
+import android.preference.Preference.OnPreferenceClickListener;
+
+import com.actionbarsherlock.app.SherlockPreferenceActivity;
+import com.actionbarsherlock.view.MenuItem;
+import com.andrew.apollo.MusicPlaybackService;
+import com.andrew.apollo.R;
+import com.andrew.apollo.cache.ImageCache;
+import com.andrew.apollo.ui.fragments.ThemeFragment;
+import com.andrew.apollo.utils.ApolloUtils;
+import com.andrew.apollo.utils.MusicUtils;
+import com.andrew.apollo.utils.PreferenceUtils;
+import com.andrew.apollo.widgets.ColorSchemeDialog;
+
+/**
+ * Settings.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+@SuppressWarnings("deprecation")
+public class SettingsActivity extends SherlockPreferenceActivity {
+
+ /**
+ * Image cache
+ */
+ private ImageCache mImageCache;
+
+ private PreferenceUtils mPreferences;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Fade it in
+ overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
+
+ // Get the preferences
+ mPreferences = PreferenceUtils.getInstace(this);
+
+ // Initialze the image cache
+ mImageCache = ImageCache.getInstance(this);
+
+ // UP
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+
+ // Add the preferences
+ addPreferencesFromResource(R.xml.settings);
+
+ // Interface settings
+ initInterface();
+ // Date settings
+ initData();
+ // Removes the cache entries
+ deleteCache();
+ // About
+ showOpenSourceLicenses();
+ // Update the version number
+ try {
+ final PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
+ findPreference("version").setSummary(packageInfo.versionName);
+ } catch (final NameNotFoundException e) {
+ findPreference("version").setSummary("?");
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ onBackPressed();
+ finish();
+ return true;
+ default:
+ break;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onResume() {
+ super.onResume();
+ MusicUtils.killForegroundService(this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onPause() {
+ super.onPause();
+ if (MusicUtils.isPlaying() && ApolloUtils.isApplicationSentToBackground(this)) {
+ MusicUtils.startBackgroundService(this);
+ }
+ }
+
+ /**
+ * Initializes the preferences under the "Interface" category
+ */
+ private void initInterface() {
+ // Color scheme picker
+ updateColorScheme();
+ // Open the theme chooser
+ openThemeChooser();
+ }
+
+ /**
+ * Initializes the preferences under the "Data" category
+ */
+ private void initData() {
+ // Only on Wi-Fi preference
+ onlyOnWiFi();
+ // Missing album art
+ downloadMissingArtwork();
+ // Missing artist images
+ downloadMissingArtistImages();
+ if (ApolloUtils.hasICS()) {
+ // Lockscreen controls
+ toggleLockscreenControls();
+ }
+ }
+
+ /**
+ * Shows the {@link ColorSchemeDialog} and then saves the changes.
+ */
+ private void updateColorScheme() {
+ final Preference colorScheme = findPreference("color_scheme");
+ colorScheme.setOnPreferenceClickListener(new OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(final Preference preference) {
+ ApolloUtils.showColorPicker(SettingsActivity.this);
+ return true;
+ }
+ });
+ }
+
+ /**
+ * Opens the {@link ThemeFragment}.
+ */
+ private void openThemeChooser() {
+ final Preference themeChooser = findPreference("theme_chooser");
+ themeChooser.setOnPreferenceClickListener(new OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(final Preference preference) {
+ final Intent themeChooserIntent = new Intent(SettingsActivity.this,
+ ThemesActivity.class);
+ startActivity(themeChooserIntent);
+ return true;
+ }
+ });
+ }
+
+ /**
+ * Toggles the only on Wi-Fi preference
+ */
+ private void onlyOnWiFi() {
+ final CheckBoxPreference onlyOnWiFi = (CheckBoxPreference)findPreference("only_on_wifi");
+ onlyOnWiFi.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
+
+ @Override
+ public boolean onPreferenceChange(final Preference preference, final Object newValue) {
+ mPreferences.setOnlyOnWifi((Boolean)newValue);
+ return true;
+ }
+ });
+ }
+
+ /**
+ * Toggles the download missing album art preference
+ */
+ private void downloadMissingArtwork() {
+ final CheckBoxPreference missingArtwork = (CheckBoxPreference)findPreference("album_images");
+ missingArtwork.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
+
+ @Override
+ public boolean onPreferenceChange(final Preference preference, final Object newValue) {
+ mPreferences.setDownloadMissingArtwork((Boolean)newValue);
+ return true;
+ }
+ });
+ }
+
+ /**
+ * Toggles the download missing artist imagages preference
+ */
+ private void downloadMissingArtistImages() {
+ final CheckBoxPreference missingArtistImages = (CheckBoxPreference)findPreference("artist_images");
+ missingArtistImages.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
+
+ @Override
+ public boolean onPreferenceChange(final Preference preference, final Object newValue) {
+ mPreferences.setDownloadMissingArtistImages((Boolean)newValue);
+ return true;
+ }
+ });
+ }
+
+ /**
+ * Toggles the lock screen controls
+ */
+ private void toggleLockscreenControls() {
+ final CheckBoxPreference lockscreenControls = (CheckBoxPreference)findPreference("lockscreen_controls");
+ lockscreenControls.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
+
+ @Override
+ public boolean onPreferenceChange(final Preference preference, final Object newValue) {
+ mPreferences.setLockscreenControls((Boolean)newValue);
+
+ // Let the service know
+ final Intent updateLockscreen = new Intent(SettingsActivity.this,
+ MusicPlaybackService.class);
+ updateLockscreen.setAction(MusicPlaybackService.UPDATE_LOCKSCREEN);
+ updateLockscreen
+ .putExtra(MusicPlaybackService.UPDATE_LOCKSCREEN, (Boolean)newValue);
+ startService(updateLockscreen);
+ return true;
+ }
+ });
+ }
+
+ /**
+ * Removes all of the cache entries.
+ */
+ private void deleteCache() {
+ final Preference deleteCache = findPreference("delete_cache");
+ deleteCache.setOnPreferenceClickListener(new OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(final Preference preference) {
+ new AlertDialog.Builder(SettingsActivity.this).setMessage(R.string.delete_warning)
+ .setPositiveButton(android.R.string.ok, new OnClickListener() {
+
+ @Override
+ public void onClick(final DialogInterface dialog, final int which) {
+ mImageCache.clearCaches();
+ }
+ }).setNegativeButton(R.string.cancel, new OnClickListener() {
+
+ @Override
+ public void onClick(final DialogInterface dialog, final int which) {
+ dialog.dismiss();
+ }
+ }).create().show();
+ return true;
+ }
+ });
+ }
+
+ /**
+ * Show the open source licenses
+ */
+ private void showOpenSourceLicenses() {
+ final Preference mOpenSourceLicenses = findPreference("open_source");
+ mOpenSourceLicenses.setOnPreferenceClickListener(new OnPreferenceClickListener() {
+
+ @Override
+ public boolean onPreferenceClick(final Preference preference) {
+ ApolloUtils.createOpenSourceDialog(SettingsActivity.this).show();
+ return true;
+ }
+ });
+ }
+}
diff --git a/src/com/andrew/apollo/ui/activities/ShortcutActivity.java b/src/com/andrew/apollo/ui/activities/ShortcutActivity.java
new file mode 100644
index 0000000..96d3355
--- /dev/null
+++ b/src/com/andrew/apollo/ui/activities/ShortcutActivity.java
@@ -0,0 +1,353 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.ui.activities;
+
+import static com.andrew.apollo.Config.MIME_TYPE;
+import static com.andrew.apollo.utils.MusicUtils.mService;
+
+import android.app.SearchManager;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.provider.MediaStore;
+import android.support.v4.app.LoaderManager.LoaderCallbacks;
+import android.support.v4.content.Loader;
+
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.andrew.apollo.Config;
+import com.andrew.apollo.IApolloService;
+import com.andrew.apollo.R;
+import com.andrew.apollo.format.Capitalize;
+import com.andrew.apollo.loaders.AsyncHandler;
+import com.andrew.apollo.loaders.LastAddedLoader;
+import com.andrew.apollo.loaders.SearchLoader;
+import com.andrew.apollo.model.Song;
+import com.andrew.apollo.utils.Lists;
+import com.andrew.apollo.utils.MusicUtils;
+import com.andrew.apollo.utils.MusicUtils.ServiceToken;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This class is opened when the user touches a Home screen shortcut or album
+ * art in an app-wdget, and then carries out the proper action. It is also
+ * responsible for processing voice queries and playing the spoken artist,
+ * album, song, playlist, or genre.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class ShortcutActivity extends SherlockFragmentActivity implements ServiceConnection {
+
+ /**
+ * If true, this class will begin playback and open
+ * {@link AudioPlayerActivity}, false will close the class after playback,
+ * which is what happens when a user starts playing something from an
+ * app-widget
+ */
+ public static final String OPEN_AUDIO_PLAYER = null;
+
+ /**
+ * Service token
+ */
+ private ServiceToken mToken;
+
+ /**
+ * Gather the intent action and extras
+ */
+ private Intent mIntent;
+
+ /**
+ * The list of songs to play
+ */
+ private long[] mList;
+
+ /**
+ * Used to shuffle the tracks or play them in order
+ */
+ private boolean mShouldShuffle;
+
+ /**
+ * Search query from a voice action
+ */
+ private String mVoiceQuery;
+
+ /**
+ * Used with the loader and voice queries
+ */
+ private final ArrayList<Song> mSong = Lists.newArrayList();
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Fade it in
+ overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
+
+ // Bind Apollo's service
+ mToken = MusicUtils.bindToService(this, this);
+
+ // Intiialize the intent
+ mIntent = getIntent();
+ // Get the voice search query
+ mVoiceQuery = Capitalize.capitalize(mIntent.getStringExtra(SearchManager.QUERY));
+
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onServiceConnected(final ComponentName name, final IBinder service) {
+ mService = IApolloService.Stub.asInterface(service);
+
+ // Don't do anything until the service has been connected
+ AsyncHandler.post(new Runnable() {
+
+ @Override
+ public void run() {
+ // Check for a voice query
+ if (mIntent.getAction().equals(Config.PLAY_FROM_SEARCH)) {
+ getSupportLoaderManager().initLoader(0, null, mSongAlbumArtistQuery);
+ } else
+ // Make sure everthing is good-to-go
+ if (mService != null) {
+
+ // Frist, check the artist MIME type
+ if (getType(MediaStore.Audio.Artists.CONTENT_TYPE)) {
+
+ // Shuffle the artist track list
+ mShouldShuffle = true;
+
+ // Get the artist song list
+ mList = MusicUtils.getSongListForArtist(ShortcutActivity.this, getId());
+ } else
+ // Second, check the album MIME type
+ if (getType(MediaStore.Audio.Albums.CONTENT_TYPE)) {
+
+ // Shuffle the album track list
+ mShouldShuffle = true;
+
+ // Get the album song list
+ mList = MusicUtils.getSongListForAlbum(ShortcutActivity.this, getId());
+ } else
+ // Third, check the genre MIME type
+ if (getType(MediaStore.Audio.Genres.CONTENT_TYPE)) {
+
+ // Shuffle the genre track list
+ mShouldShuffle = true;
+
+ // Get the genre song list
+ mList = MusicUtils.getSongListForGenre(ShortcutActivity.this, getId());
+ } else
+ // Fourth, check the playlist MIME type
+ if (getType(MediaStore.Audio.Playlists.CONTENT_TYPE)) {
+
+ // Don't shuffle the playlist track list
+ mShouldShuffle = false;
+
+ // Get the playlist song list
+ mList = MusicUtils.getSongListForPlaylist(ShortcutActivity.this, getId());
+ } else
+ // Check the Favorites playlist
+ if (getType(getString(R.string.playlist_favorites))) {
+
+ // Don't shuffle the Favorites track list
+ mShouldShuffle = false;
+
+ // Get the Favorites song list
+ mList = MusicUtils.getSongListForFavorites(ShortcutActivity.this);
+ } else
+ // Check for the Last added playlist
+ if (getType(getString(R.string.playlist_last_added))) {
+
+ // Don't shuffle the last added track list
+ mShouldShuffle = false;
+
+ // Get the Last added song list
+ Cursor cursor = LastAddedLoader.makeLastAddedCursor(ShortcutActivity.this);
+ mList = MusicUtils.getSongListForCursor(cursor);
+ cursor.close();
+ cursor = null;
+
+ }
+ // Finish up
+ allDone();
+ } else {
+ // TODO Show and error explaining why
+ }
+ }
+ });
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onServiceDisconnected(final ComponentName name) {
+ mService = null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ // Unbind from the service
+ if (mService != null) {
+ MusicUtils.unbindFromService(mToken);
+ mToken = null;
+ }
+ }
+
+ /**
+ * Uses the query from a voice search to try and play a song, then album,
+ * then artist. If all of those fail, it checks for playlists and genres via
+ * a {@link #mPlaylistGenreQuery}.
+ */
+ private final LoaderCallbacks<List<Song>> mSongAlbumArtistQuery = new LoaderCallbacks<List<Song>>() {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Loader<List<Song>> onCreateLoader(final int id, final Bundle args) {
+ return new SearchLoader(ShortcutActivity.this, mVoiceQuery);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoadFinished(final Loader<List<Song>> loader, final List<Song> data) {
+ // If the user searched for a playlist or genre, this list will
+ // return empty
+ if (data.isEmpty()) {
+ // Before running the playlist loader, try to play the
+ // "Favorites" playlist
+ if (isFavorite()) {
+ MusicUtils.playFavorites(ShortcutActivity.this);
+ }
+ // Finish up
+ allDone();
+ return;
+ }
+
+ // Start fresh
+ mSong.clear();
+ // Add the data to the adpater
+ for (final Song song : data) {
+ mSong.add(song);
+ }
+
+ // What's about to happen is similar to the above process. Apollo
+ // runs a
+ // series of checks to see if anything comes up. When it does, it
+ // assumes (pretty accurately) that it should begin to play that
+ // thing.
+ // The fancy search query used in {@link SearchLoader} is the key to
+ // this. It allows the user to perform very specific queries. i.e.
+ // "Listen to Ethio
+
+ final String song = mSong.get(0).mSongName;
+ final String album = mSong.get(0).mAlbumName;
+ final String artist = mSong.get(0).mArtistName;
+ // This tripes as the song, album, and artist Id
+ final String id = mSong.get(0).mSongId;
+ // First, try to play a song
+ if (mList == null && id != null && song != null) {
+ mList = new long[] {
+ Long.valueOf(id)
+ };
+ } else
+ // Second, try to play an album
+ if (mList == null && id != null && album != null) {
+ mList = MusicUtils.getSongListForAlbum(ShortcutActivity.this, id);
+ } else
+ // Third, try to play an artist
+ if (mList == null && id != null && artist != null) {
+ mList = MusicUtils.getSongListForArtist(ShortcutActivity.this, id);
+ }
+ // Finish up
+ allDone();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoaderReset(final Loader<List<Song>> loader) {
+ // Clear the data
+ mSong.clear();
+ }
+ };
+
+ /**
+ * Used to check the MIME types
+ *
+ * @param type The MIME type to check
+ * @return True if {@code type} is equal to the MIME type from the intent.
+ */
+ private boolean getType(final String type) {
+ return mIntent.getExtras().getString(MIME_TYPE).equals(type);
+ }
+
+ /**
+ * Used to find the Id supplied
+ *
+ * @return The Id passed into the activity
+ */
+ private String getId() {
+ return String.valueOf(mIntent.getExtras().getLong(Config.ID));
+ }
+
+ /**
+ * @return True if the user searched for the favorites playlist
+ */
+ private boolean isFavorite() {
+ final String favoritePlaylist = getString(R.string.playlist_favorites);
+ // Check to see if the user spoke the word "Favorites"
+ if (mVoiceQuery.equals(favoritePlaylist)) {
+ return true;
+ // Check to see if the user spoke the word "Favorite"
+ } else if (mVoiceQuery.equals(favoritePlaylist.substring(0, favoritePlaylist.length() - 1))) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Starts playback, open {@link AudioPlayerActivity} and finishes this one
+ */
+ private void allDone() {
+ final boolean shouldOpenAudioPlayer = mIntent.getBooleanExtra(OPEN_AUDIO_PLAYER, true);
+ // Play the list
+ if (mList != null && mList.length > 0) {
+ MusicUtils.playAll(this, mList, 0, mShouldShuffle);
+ }
+
+ // Open the now playing screen
+ if (shouldOpenAudioPlayer) {
+ final Intent intent = new Intent(this, AudioPlayerActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ }
+ // All done
+ finish();
+ }
+}
diff --git a/src/com/andrew/apollo/ui/activities/ThemesActivity.java b/src/com/andrew/apollo/ui/activities/ThemesActivity.java
new file mode 100644
index 0000000..c088480
--- /dev/null
+++ b/src/com/andrew/apollo/ui/activities/ThemesActivity.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.ui.activities;
+
+import android.os.Bundle;
+
+import com.actionbarsherlock.app.ActionBar;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+import com.andrew.apollo.R;
+import com.andrew.apollo.ui.fragments.ThemeFragment;
+
+/**
+ * A class the displays the {@link ThemeFragment}.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class ThemesActivity extends BaseActivity {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Set up the action bar
+ final ActionBar actionBar = getSupportActionBar();
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ actionBar.setTitle(getString(R.string.settings_theme_chooser_title));
+
+ // Transact the theme fragment
+ if (savedInstanceState == null) {
+ getSupportFragmentManager().beginTransaction()
+ .replace(R.id.activity_base_content, new ThemeFragment()).commit();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onPrepareOptionsMenu(final Menu menu) {
+ mResources.setShopIcon(menu);
+ return super.onPrepareOptionsMenu(menu);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onCreateOptionsMenu(final Menu menu) {
+ getSupportMenuInflater().inflate(R.menu.theme_shop, menu);
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ finish();
+ return true;
+ case R.id.menu_shop:
+ mResources.shopFor(this);
+ return true;
+ default:
+ break;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int setContentView() {
+ return R.layout.activity_base;
+ }
+}
diff --git a/src/com/andrew/apollo/ui/fragments/AlbumFragment.java b/src/com/andrew/apollo/ui/fragments/AlbumFragment.java
new file mode 100644
index 0000000..2be6326
--- /dev/null
+++ b/src/com/andrew/apollo/ui/fragments/AlbumFragment.java
@@ -0,0 +1,463 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.ui.fragments;
+
+import static com.andrew.apollo.utils.PreferenceUtils.ALBUM_LAYOUT;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.v4.app.LoaderManager.LoaderCallbacks;
+import android.support.v4.content.Loader;
+import android.text.TextUtils;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.SubMenu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.AbsListView.OnScrollListener;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.GridView;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.actionbarsherlock.app.SherlockFragment;
+import com.andrew.apollo.Config;
+import com.andrew.apollo.MusicStateListener;
+import com.andrew.apollo.R;
+import com.andrew.apollo.adapters.AlbumAdapter;
+import com.andrew.apollo.loaders.AlbumLoader;
+import com.andrew.apollo.menu.CreateNewPlaylist;
+import com.andrew.apollo.menu.DeleteDialog;
+import com.andrew.apollo.menu.FragmentMenuItems;
+import com.andrew.apollo.model.Album;
+import com.andrew.apollo.recycler.RecycleHolder;
+import com.andrew.apollo.ui.activities.BaseActivity;
+import com.andrew.apollo.utils.ApolloUtils;
+import com.andrew.apollo.utils.MusicUtils;
+import com.andrew.apollo.utils.NavUtils;
+import com.andrew.apollo.utils.PreferenceUtils;
+import com.viewpagerindicator.TitlePageIndicator;
+
+import java.util.List;
+
+/**
+ * This class is used to display all of the albums on a user's device.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class AlbumFragment extends SherlockFragment implements LoaderCallbacks<List<Album>>,
+ OnScrollListener, OnItemClickListener, MusicStateListener {
+
+ /**
+ * Used to keep context menu items from bleeding into other fragments
+ */
+ private static final int GROUP_ID = 3;
+
+ /**
+ * Grid view column count. ONE - list, TWO - normal grid, FOUR - landscape
+ */
+ private static final int ONE = 1, TWO = 2, FOUR = 4;
+
+ /**
+ * LoaderCallbacks identifier
+ */
+ private static final int LOADER = 0;
+
+ /**
+ * Fragment UI
+ */
+ private ViewGroup mRootView;
+
+ /**
+ * The adapter for the grid
+ */
+ private AlbumAdapter mAdapter;
+
+ /**
+ * The grid view
+ */
+ private GridView mGridView;
+
+ /**
+ * The list view
+ */
+ private ListView mListView;
+
+ /**
+ * Album song list
+ */
+ private long[] mAlbumList;
+
+ /**
+ * Represents an album
+ */
+ private Album mAlbum;
+
+ /**
+ * True if the list should execute {@code #restartLoader()}.
+ */
+ private boolean mShouldRefresh = false;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onAttach(final Activity activity) {
+ super.onAttach(activity);
+ // Register the music status listener
+ ((BaseActivity)activity).setMusicStateListenerListener(this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ int layout = R.layout.list_item_normal;
+ if (isSimpleLayout()) {
+ layout = R.layout.list_item_normal;
+ } else if (isDetailedLayout()) {
+ layout = R.layout.list_item_detailed;
+ } else {
+ layout = R.layout.grid_items_normal;
+ }
+ mAdapter = new AlbumAdapter(getSherlockActivity(), layout);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
+ final Bundle savedInstanceState) {
+ // The View for the fragment's UI
+ if (isSimpleLayout()) {
+ mRootView = (ViewGroup)inflater.inflate(R.layout.list_base, null);
+ initListView();
+ } else {
+ mRootView = (ViewGroup)inflater.inflate(R.layout.grid_base, null);
+ initGridView();
+ }
+ return mRootView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onActivityCreated(final Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ // Enable the options menu
+ setHasOptionsMenu(true);
+ // Start the loader
+ getLoaderManager().initLoader(LOADER, null, this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onPause() {
+ super.onPause();
+ mAdapter.flush();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreateContextMenu(final ContextMenu menu, final View v,
+ final ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+
+ // Get the position of the selected item
+ final AdapterContextMenuInfo info = (AdapterContextMenuInfo)menuInfo;
+ // Create a new album
+ mAlbum = mAdapter.getItem(info.position);
+ // Create a list of the album's songs
+ mAlbumList = MusicUtils.getSongListForAlbum(getSherlockActivity(), mAlbum.mAlbumId);
+
+ // Play the album
+ menu.add(GROUP_ID, FragmentMenuItems.PLAY_SELECTION, Menu.NONE,
+ getString(R.string.context_menu_play_selection));
+
+ // Add the album to the queue
+ menu.add(GROUP_ID, FragmentMenuItems.ADD_TO_QUEUE, Menu.NONE,
+ getString(R.string.add_to_queue));
+
+ // Add the album to a playlist
+ final SubMenu subMenu = menu.addSubMenu(GROUP_ID, FragmentMenuItems.ADD_TO_PLAYLIST,
+ Menu.NONE, R.string.add_to_playlist);
+ MusicUtils.makePlaylistMenu(getSherlockActivity(), GROUP_ID, subMenu, false);
+
+ // View more content by the album artist
+ menu.add(GROUP_ID, FragmentMenuItems.MORE_BY_ARTIST, Menu.NONE,
+ getString(R.string.context_menu_more_by_artist));
+
+ // Remove the album from the list
+ menu.add(GROUP_ID, FragmentMenuItems.DELETE, Menu.NONE,
+ getString(R.string.context_menu_delete));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onContextItemSelected(final MenuItem item) {
+ // Avoid leaking context menu selections
+ if (item.getGroupId() == GROUP_ID) {
+ switch (item.getItemId()) {
+ case FragmentMenuItems.PLAY_SELECTION:
+ MusicUtils.playAll(getSherlockActivity(), mAlbumList, 0, false);
+ return true;
+ case FragmentMenuItems.ADD_TO_QUEUE:
+ MusicUtils.addToQueue(getSherlockActivity(), mAlbumList);
+ return true;
+ case FragmentMenuItems.NEW_PLAYLIST:
+ CreateNewPlaylist.getInstance(mAlbumList).show(getFragmentManager(),
+ "CreatePlaylist");
+ return true;
+ case FragmentMenuItems.MORE_BY_ARTIST:
+ NavUtils.openArtistProfile(getSherlockActivity(), mAlbum.mArtistName);
+ return true;
+ case FragmentMenuItems.PLAYLIST_SELECTED:
+ final long id = item.getIntent().getLongExtra("playlist", 0);
+ MusicUtils.addToPlaylist(getSherlockActivity(), mAlbumList, id);
+ return true;
+ case FragmentMenuItems.DELETE:
+ mShouldRefresh = true;
+ final String album = mAlbum.mAlbumName;
+ DeleteDialog.newInstance(album, mAlbumList, album + Config.ALBUM_ART_SUFFIX)
+ .show(getFragmentManager(), "DeleteDialog");
+ return true;
+ default:
+ break;
+ }
+ }
+ return super.onContextItemSelected(item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onScrollStateChanged(final AbsListView view, final int scrollState) {
+ // Pause disk cache access to ensure smoother scrolling
+ if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_FLING
+ || scrollState == AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
+ mAdapter.setPauseDiskCache(true);
+ } else {
+ mAdapter.setPauseDiskCache(false);
+ mAdapter.notifyDataSetChanged();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onItemClick(final AdapterView<?> parent, final View view, final int position,
+ final long id) {
+ mAlbum = mAdapter.getItem(position);
+ NavUtils.openAlbumProfile(getSherlockActivity(), mAlbum.mAlbumName, mAlbum.mArtistName);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Loader<List<Album>> onCreateLoader(final int id, final Bundle args) {
+ return new AlbumLoader(getSherlockActivity());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoadFinished(final Loader<List<Album>> loader, final List<Album> data) {
+ // Check for any errors
+ if (data.isEmpty()) {
+ // Set the empty text
+ final TextView empty = (TextView)mRootView.findViewById(R.id.empty);
+ empty.setText(getString(R.string.empty_music));
+ if (isSimpleLayout()) {
+ mListView.setEmptyView(empty);
+ } else {
+ mGridView.setEmptyView(empty);
+ }
+ return;
+ }
+
+ // Start fresh
+ mAdapter.unload();
+ // Add the data to the adpater
+ for (final Album album : data) {
+ mAdapter.add(album);
+ }
+ // Build the cache
+ mAdapter.buildCache();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoaderReset(final Loader<List<Album>> loader) {
+ // Clear the data in the adapter
+ mAdapter.unload();
+ }
+
+ /**
+ * Scrolls the list to the currently playing album when the user touches the
+ * header in the {@link TitlePageIndicator}.
+ */
+ public void scrollToCurrentAlbum() {
+ final int currentAlbumPosition = getItemPositionByAlbum();
+
+ if (currentAlbumPosition != 0) {
+ if (isSimpleLayout()) {
+ mListView.setSelection(currentAlbumPosition);
+ } else {
+ mGridView.setSelection(currentAlbumPosition);
+ }
+ }
+ }
+
+ /**
+ * @return The position of an item in the list or grid based on the id of
+ * the currently playing album.
+ */
+ private int getItemPositionByAlbum() {
+ final String albumName = String.valueOf(MusicUtils.getCurrentAlbumId());
+ if (mAdapter == null || TextUtils.isEmpty(albumName)) {
+ return 0;
+ }
+ for (int i = 0; i < mAdapter.getCount(); i++) {
+ if (mAdapter.getItem(i).mAlbumId.equals(albumName)) {
+ return i;
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Restarts the loader.
+ */
+ public void refresh() {
+ // Wait a moment for the preference to change.
+ SystemClock.sleep(10);
+ getLoaderManager().restartLoader(LOADER, null, this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onScroll(final AbsListView view, final int firstVisibleItem,
+ final int visibleItemCount, final int totalItemCount) {
+ // Nothing to do
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void restartLoader() {
+ // Update the list when the user deletes any items
+ if (mShouldRefresh) {
+ getLoaderManager().restartLoader(LOADER, null, this);
+ }
+ mShouldRefresh = false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onMetaChanged() {
+ // Nothing to do
+ }
+
+ /**
+ * Sets up various helpers for both the list and grid
+ *
+ * @param list The list or grid
+ */
+ private void initAbsListView(final AbsListView list) {
+ // Release any references to the recycled Views
+ list.setRecyclerListener(new RecycleHolder());
+ // Listen for ContextMenus to be created
+ list.setOnCreateContextMenuListener(this);
+ // Show the albums and songs from the selected artist
+ list.setOnItemClickListener(this);
+ // To help make scrolling smooth
+ list.setOnScrollListener(this);
+ }
+
+ /**
+ * Sets up the list view
+ */
+ private void initListView() {
+ // Initialize the grid
+ mListView = (ListView)mRootView.findViewById(R.id.list_base);
+ // Set the data behind the list
+ mListView.setAdapter(mAdapter);
+ // Set up the helpers
+ initAbsListView(mListView);
+ mAdapter.setTouchPlay(true);
+ }
+
+ /**
+ * Sets up the grid view
+ */
+ private void initGridView() {
+ // Initialize the grid
+ mGridView = (GridView)mRootView.findViewById(R.id.grid_base);
+ // Set the data behind the grid
+ mGridView.setAdapter(mAdapter);
+ // Set up the helpers
+ initAbsListView(mGridView);
+ if (ApolloUtils.isLandscape(getSherlockActivity())) {
+ if (isDetailedLayout()) {
+ mAdapter.setLoadExtraData(true);
+ mGridView.setNumColumns(TWO);
+ } else {
+ mGridView.setNumColumns(FOUR);
+ }
+ } else {
+ if (isDetailedLayout()) {
+ mAdapter.setLoadExtraData(true);
+ mGridView.setNumColumns(ONE);
+ } else {
+ mGridView.setNumColumns(TWO);
+ }
+ }
+ }
+
+ private boolean isSimpleLayout() {
+ return PreferenceUtils.getInstace(getSherlockActivity()).isSimpleLayout(ALBUM_LAYOUT,
+ getSherlockActivity());
+ }
+
+ private boolean isDetailedLayout() {
+ return PreferenceUtils.getInstace(getSherlockActivity()).isDetailedLayout(ALBUM_LAYOUT,
+ getSherlockActivity());
+ }
+}
diff --git a/src/com/andrew/apollo/ui/fragments/ArtistFragment.java b/src/com/andrew/apollo/ui/fragments/ArtistFragment.java
new file mode 100644
index 0000000..128ce62
--- /dev/null
+++ b/src/com/andrew/apollo/ui/fragments/ArtistFragment.java
@@ -0,0 +1,461 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.ui.fragments;
+
+import static com.andrew.apollo.utils.PreferenceUtils.ARTIST_LAYOUT;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.LoaderManager.LoaderCallbacks;
+import android.support.v4.content.Loader;
+import android.text.TextUtils;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.SubMenu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.AbsListView.OnScrollListener;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.GridView;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.actionbarsherlock.app.SherlockFragment;
+import com.andrew.apollo.MusicStateListener;
+import com.andrew.apollo.R;
+import com.andrew.apollo.adapters.ArtistAdapter;
+import com.andrew.apollo.loaders.ArtistLoader;
+import com.andrew.apollo.menu.CreateNewPlaylist;
+import com.andrew.apollo.menu.DeleteDialog;
+import com.andrew.apollo.menu.FragmentMenuItems;
+import com.andrew.apollo.model.Artist;
+import com.andrew.apollo.recycler.RecycleHolder;
+import com.andrew.apollo.ui.activities.BaseActivity;
+import com.andrew.apollo.utils.ApolloUtils;
+import com.andrew.apollo.utils.MusicUtils;
+import com.andrew.apollo.utils.NavUtils;
+import com.andrew.apollo.utils.PreferenceUtils;
+import com.viewpagerindicator.TitlePageIndicator;
+
+import java.util.List;
+
+/**
+ * This class is used to display all of the artists on a user's device.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class ArtistFragment extends SherlockFragment implements LoaderCallbacks<List<Artist>>,
+ OnScrollListener, OnItemClickListener, MusicStateListener {
+
+ /**
+ * Used to keep context menu items from bleeding into other fragments
+ */
+ private static final int GROUP_ID = 2;
+
+ /**
+ * Grid view column count. ONE - list, TWO - normal grid, FOUR - landscape
+ */
+ private static final int ONE = 1, TWO = 2, FOUR = 4;
+
+ /**
+ * LoaderCallbacks identifier
+ */
+ private static final int LOADER = 0;
+
+ /**
+ * Fragment UI
+ */
+ private ViewGroup mRootView;
+
+ /**
+ * The adapter for the grid
+ */
+ private ArtistAdapter mAdapter;
+
+ /**
+ * The grid view
+ */
+ private GridView mGridView;
+
+ /**
+ * The list view
+ */
+ private ListView mListView;
+
+ /**
+ * Artist song list
+ */
+ private long[] mArtistList;
+
+ /**
+ * Represents an artist
+ */
+ private Artist mArtist;
+
+ /**
+ * True if the list should execute {@code #restartLoader()}.
+ */
+ private boolean mShouldRefresh = false;
+
+ /**
+ * Empty constructor as per the {@link Fragment} documentation
+ */
+ public ArtistFragment() {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onAttach(final Activity activity) {
+ super.onAttach(activity);
+ // Register the music status listener
+ ((BaseActivity)activity).setMusicStateListenerListener(this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Create the adpater
+ int layout = R.layout.grid_items_normal;
+ if (isSimpleLayout()) {
+ layout = R.layout.list_item_simple;
+ } else if (isDetailedLayout()) {
+ layout = R.layout.list_item_detailed;
+ } else {
+ layout = R.layout.grid_items_normal;
+ }
+ mAdapter = new ArtistAdapter(getSherlockActivity(), layout);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
+ final Bundle savedInstanceState) {
+ // The View for the fragment's UI
+ if (isSimpleLayout()) {
+ mRootView = (ViewGroup)inflater.inflate(R.layout.list_base, null);
+ initListView();
+ } else {
+ mRootView = (ViewGroup)inflater.inflate(R.layout.grid_base, null);
+ initGridView();
+ }
+ return mRootView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onActivityCreated(final Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ // Enable the options menu
+ setHasOptionsMenu(true);
+ // Start the loader
+ getLoaderManager().initLoader(LOADER, null, this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onPause() {
+ super.onPause();
+ mAdapter.flush();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreateContextMenu(final ContextMenu menu, final View v,
+ final ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+
+ // Get the position of the selected item
+ final AdapterContextMenuInfo info = (AdapterContextMenuInfo)menuInfo;
+ // Creat a new model
+ mArtist = mAdapter.getItem(info.position);
+ // Create a list of the artist's songs
+ mArtistList = MusicUtils.getSongListForArtist(getSherlockActivity(), mArtist.mArtistId);
+
+ // Play the artist
+ menu.add(GROUP_ID, FragmentMenuItems.PLAY_SELECTION, Menu.NONE,
+ getString(R.string.context_menu_play_selection));
+
+ // Add the artist to the queue
+ menu.add(GROUP_ID, FragmentMenuItems.ADD_TO_QUEUE, Menu.NONE,
+ getString(R.string.add_to_queue));
+
+ // Add the artist to a playlist
+ final SubMenu subMenu = menu.addSubMenu(GROUP_ID, FragmentMenuItems.ADD_TO_PLAYLIST,
+ Menu.NONE, R.string.add_to_playlist);
+ MusicUtils.makePlaylistMenu(getSherlockActivity(), GROUP_ID, subMenu, false);
+
+ // Delete the artist
+ menu.add(GROUP_ID, FragmentMenuItems.DELETE, Menu.NONE,
+ getString(R.string.context_menu_delete));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onContextItemSelected(final android.view.MenuItem item) {
+ // Avoid leaking context menu selections
+ if (item.getGroupId() == GROUP_ID) {
+ switch (item.getItemId()) {
+ case FragmentMenuItems.PLAY_SELECTION:
+ MusicUtils.playAll(getSherlockActivity(), mArtistList, 0, true);
+ return true;
+ case FragmentMenuItems.ADD_TO_QUEUE:
+ MusicUtils.addToQueue(getSherlockActivity(), mArtistList);
+ return true;
+ case FragmentMenuItems.NEW_PLAYLIST:
+ CreateNewPlaylist.getInstance(mArtistList).show(getFragmentManager(),
+ "CreatePlaylist");
+ return true;
+ case FragmentMenuItems.PLAYLIST_SELECTED:
+ final long id = item.getIntent().getLongExtra("playlist", 0);
+ MusicUtils.addToPlaylist(getSherlockActivity(), mArtistList, id);
+ return true;
+ case FragmentMenuItems.DELETE:
+ mShouldRefresh = true;
+ final String artist = mArtist.mArtistName;
+ DeleteDialog.newInstance(artist, mArtistList, artist).show(
+ getFragmentManager(), "DeleteDialog");
+ return true;
+ default:
+ break;
+ }
+ }
+ return super.onContextItemSelected(item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onScrollStateChanged(final AbsListView view, final int scrollState) {
+ // Pause disk cache access to ensure smoother scrolling
+ if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_FLING
+ || scrollState == AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
+ mAdapter.setPauseDiskCache(true);
+ } else {
+ mAdapter.setPauseDiskCache(false);
+ mAdapter.notifyDataSetChanged();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onItemClick(final AdapterView<?> parent, final View view, final int position,
+ final long id) {
+ mArtist = mAdapter.getItem(position);
+ NavUtils.openArtistProfile(getSherlockActivity(), mArtist.mArtistName);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Loader<List<Artist>> onCreateLoader(final int id, final Bundle args) {
+ return new ArtistLoader(getSherlockActivity());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoadFinished(final Loader<List<Artist>> loader, final List<Artist> data) {
+ // Check for any errors
+ if (data.isEmpty()) {
+ // Set the empty text
+ final TextView empty = (TextView)mRootView.findViewById(R.id.empty);
+ empty.setText(getString(R.string.empty_music));
+ if (isSimpleLayout()) {
+ mListView.setEmptyView(empty);
+ } else {
+ mGridView.setEmptyView(empty);
+ }
+ return;
+ }
+
+ // Start fresh
+ mAdapter.unload();
+ // Add the data to the adpater
+ for (final Artist artist : data) {
+ mAdapter.add(artist);
+ }
+ // Build the cache
+ mAdapter.buildCache();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoaderReset(final Loader<List<Artist>> loader) {
+ // Clear the data in the adapter
+ mAdapter.unload();
+ }
+
+ /**
+ * Scrolls the list to the currently playing artist when the user touches
+ * the header in the {@link TitlePageIndicator}.
+ */
+ public void scrollToCurrentArtist() {
+ final int currentArtistPosition = getItemPositionByArtist();
+
+ if (currentArtistPosition != 0) {
+ if (isSimpleLayout()) {
+ mListView.setSelection(currentArtistPosition);
+ } else {
+ mGridView.setSelection(currentArtistPosition);
+ }
+ }
+ }
+
+ /**
+ * @return The position of an item in the list or grid based on the name of
+ * the currently playing artist.
+ */
+ private int getItemPositionByArtist() {
+ final String artistName = String.valueOf(MusicUtils.getCurrentArtistId());
+ if (mAdapter == null || TextUtils.isEmpty(artistName)) {
+ return 0;
+ }
+ for (int i = 0; i < mAdapter.getCount(); i++) {
+ if (mAdapter.getItem(i).mArtistId.equals(artistName)) {
+ return i;
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Restarts the loader.
+ */
+ public void refresh() {
+ // Wait a moment for the preference to change.
+ SystemClock.sleep(10);
+ getLoaderManager().restartLoader(LOADER, null, this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onScroll(final AbsListView view, final int firstVisibleItem,
+ final int visibleItemCount, final int totalItemCount) {
+ // Nothing to do
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void restartLoader() {
+ // Update the list when the user deletes any items
+ if (mShouldRefresh) {
+ getLoaderManager().restartLoader(LOADER, null, this);
+ }
+ mShouldRefresh = false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onMetaChanged() {
+ // Nothing to do
+ }
+
+ /**
+ * Sets up various helpers for both the list and grid
+ *
+ * @param list The list or grid
+ */
+ private void initAbsListView(final AbsListView list) {
+ // Release any references to the recycled Views
+ list.setRecyclerListener(new RecycleHolder());
+ // Listen for ContextMenus to be created
+ list.setOnCreateContextMenuListener(this);
+ // Show the albums and songs from the selected artist
+ list.setOnItemClickListener(this);
+ // To help make scrolling smooth
+ list.setOnScrollListener(this);
+ }
+
+ /**
+ * Sets up the list view
+ */
+ private void initListView() {
+ // Initialize the grid
+ mListView = (ListView)mRootView.findViewById(R.id.list_base);
+ // Set the data behind the list
+ mListView.setAdapter(mAdapter);
+ // Set up the helpers
+ initAbsListView(mListView);
+ }
+
+ /**
+ * Sets up the grid view
+ */
+ private void initGridView() {
+ // Initialize the grid
+ mGridView = (GridView)mRootView.findViewById(R.id.grid_base);
+ // Set the data behind the grid
+ mGridView.setAdapter(mAdapter);
+ // Set up the helpers
+ initAbsListView(mGridView);
+ if (ApolloUtils.isLandscape(getSherlockActivity())) {
+ if (isDetailedLayout()) {
+ mAdapter.setLoadExtraData(true);
+ mGridView.setNumColumns(TWO);
+ } else {
+ mGridView.setNumColumns(FOUR);
+ }
+ } else {
+ if (isDetailedLayout()) {
+ mAdapter.setLoadExtraData(true);
+ mGridView.setNumColumns(ONE);
+ } else {
+ mGridView.setNumColumns(TWO);
+ }
+ }
+ }
+
+ private boolean isSimpleLayout() {
+ return PreferenceUtils.getInstace(getSherlockActivity()).isSimpleLayout(ARTIST_LAYOUT,
+ getSherlockActivity());
+ }
+
+ private boolean isDetailedLayout() {
+ return PreferenceUtils.getInstace(getSherlockActivity()).isDetailedLayout(ARTIST_LAYOUT,
+ getSherlockActivity());
+ }
+}
diff --git a/src/com/andrew/apollo/ui/fragments/GenreFragment.java b/src/com/andrew/apollo/ui/fragments/GenreFragment.java
new file mode 100644
index 0000000..177d442
--- /dev/null
+++ b/src/com/andrew/apollo/ui/fragments/GenreFragment.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.ui.fragments;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.provider.MediaStore;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.LoaderManager.LoaderCallbacks;
+import android.support.v4.content.Loader;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.actionbarsherlock.app.SherlockFragment;
+import com.andrew.apollo.Config;
+import com.andrew.apollo.R;
+import com.andrew.apollo.adapters.GenreAdapter;
+import com.andrew.apollo.loaders.GenreLoader;
+import com.andrew.apollo.menu.FragmentMenuItems;
+import com.andrew.apollo.model.Genre;
+import com.andrew.apollo.recycler.RecycleHolder;
+import com.andrew.apollo.ui.activities.ProfileActivity;
+import com.andrew.apollo.utils.MusicUtils;
+
+import java.util.List;
+
+/**
+ * This class is used to display all of the genres on a user's device.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class GenreFragment extends SherlockFragment implements LoaderCallbacks<List<Genre>>,
+ OnItemClickListener {
+
+ /**
+ * Used to keep context menu items from bleeding into other fragments
+ */
+ private static final int GROUP_ID = 5;
+
+ /**
+ * LoaderCallbacks identifier
+ */
+ private static final int LOADER = 0;
+
+ /**
+ * Fragment UI
+ */
+ private ViewGroup mRootView;
+
+ /**
+ * The adapter for the list
+ */
+ private GenreAdapter mAdapter;
+
+ /**
+ * The list view
+ */
+ private ListView mListView;
+
+ /**
+ * Genre song list
+ */
+ private long[] mGenreList;
+
+ /**
+ * Represents a genre
+ */
+ private Genre mGenre;
+
+ /**
+ * Empty constructor as per the {@link Fragment} documentation
+ */
+ public GenreFragment() {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Create the adpater
+ mAdapter = new GenreAdapter(getSherlockActivity(), R.layout.list_item_simple);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
+ final Bundle savedInstanceState) {
+ // The View for the fragment's UI
+ mRootView = (ViewGroup)inflater.inflate(R.layout.list_base, null);
+ // Initialize the list
+ mListView = (ListView)mRootView.findViewById(R.id.list_base);
+ // Set the data behind the list
+ mListView.setAdapter(mAdapter);
+ // Release any references to the recycled Views
+ mListView.setRecyclerListener(new RecycleHolder());
+ // Listen for ContextMenus to be created
+ mListView.setOnCreateContextMenuListener(this);
+ // Show the albums and songs from the selected genre
+ mListView.setOnItemClickListener(this);
+ return mRootView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onActivityCreated(final Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ // Enable the options menu
+ setHasOptionsMenu(true);
+ // Start the loader
+ getLoaderManager().initLoader(LOADER, null, this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreateContextMenu(final ContextMenu menu, final View v,
+ final ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ // Get the position of the selected item
+ final AdapterContextMenuInfo info = (AdapterContextMenuInfo)menuInfo;
+ // Create a new genre
+ mGenre = mAdapter.getItem(info.position);
+ // Create a list of the genre's songs
+ mGenreList = MusicUtils.getSongListForGenre(getSherlockActivity(), mGenre.mGenreId);
+
+ // Play the genre
+ menu.add(GROUP_ID, FragmentMenuItems.PLAY_SELECTION, Menu.NONE,
+ R.string.context_menu_play_selection);
+ // Add the genre to the queue
+ menu.add(GROUP_ID, FragmentMenuItems.ADD_TO_QUEUE, Menu.NONE, R.string.add_to_queue);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onContextItemSelected(final android.view.MenuItem item) {
+ if (item.getGroupId() == GROUP_ID) {
+ switch (item.getItemId()) {
+ case FragmentMenuItems.PLAY_SELECTION:
+ MusicUtils.playAll(getSherlockActivity(), mGenreList, 0, false);
+ return true;
+ case FragmentMenuItems.ADD_TO_QUEUE:
+ MusicUtils.addToQueue(getSherlockActivity(), mGenreList);
+ return true;
+ default:
+ break;
+ }
+ }
+ return super.onContextItemSelected(item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onItemClick(final AdapterView<?> parent, final View view, final int position,
+ final long id) {
+ mGenre = mAdapter.getItem(position);
+ // Create a new bundle to transfer the artist info
+ final Bundle bundle = new Bundle();
+ bundle.putLong(Config.ID, Long.valueOf(mGenre.mGenreId));
+ bundle.putString(Config.MIME_TYPE, MediaStore.Audio.Genres.CONTENT_TYPE);
+ bundle.putString(Config.NAME, mGenre.mGenreName);
+
+ // Create the intent to launch the profile activity
+ final Intent intent = new Intent(getSherlockActivity(), ProfileActivity.class);
+ intent.putExtras(bundle);
+ startActivity(intent);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Loader<List<Genre>> onCreateLoader(final int id, final Bundle args) {
+ return new GenreLoader(getSherlockActivity());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoadFinished(final Loader<List<Genre>> loader, final List<Genre> data) {
+ // Check for any errors
+ if (data.isEmpty()) {
+ // Set the empty text
+ final TextView empty = (TextView)mRootView.findViewById(R.id.empty);
+ empty.setText(getString(R.string.empty_music));
+ mListView.setEmptyView(empty);
+ return;
+ }
+
+ // Start fresh
+ mAdapter.unload();
+ // Add the data to the adpater
+ for (final Genre genre : data) {
+ mAdapter.add(genre);
+ }
+ // Build the cache
+ mAdapter.buildCache();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoaderReset(final Loader<List<Genre>> loader) {
+ // Clear the data in the adapter
+ mAdapter.unload();
+ }
+
+}
diff --git a/src/com/andrew/apollo/ui/fragments/LyricsFragment.java b/src/com/andrew/apollo/ui/fragments/LyricsFragment.java
new file mode 100644
index 0000000..edd7bee
--- /dev/null
+++ b/src/com/andrew/apollo/ui/fragments/LyricsFragment.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.ui.fragments;
+
+import android.annotation.SuppressLint;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import com.actionbarsherlock.app.SherlockFragment;
+import com.andrew.apollo.R;
+import com.andrew.apollo.lyrics.LyricsProvider;
+import com.andrew.apollo.lyrics.LyricsProviderFactory;
+import com.andrew.apollo.lyrics.OfflineLyricsProvider;
+import com.andrew.apollo.utils.ApolloUtils;
+import com.andrew.apollo.utils.MusicUtils;
+
+/**
+ * This {@link SherlockFragment} is used to display lyrics for the currently
+ * playing song.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+@SuppressLint("NewApi")
+public class LyricsFragment extends SherlockFragment {
+
+ // Lyrics
+ private TextView mLyrics;
+
+ // Progess
+ private ProgressBar mProgressBar;
+
+ private boolean mTryOnline = false;
+
+ /**
+ * Empty constructor as per the {@link Fragment} documentation
+ */
+ public LyricsFragment() {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
+ final Bundle savedInstanceState) {
+ // The View for the fragment's UI
+ final ViewGroup rootView = (ViewGroup)inflater.inflate(R.layout.lyrics_base, null);
+ // Initialize the lyrics text view
+ mLyrics = (TextView)rootView.findViewById(R.id.audio_player_lyrics);
+ // Enable text selection
+ if (ApolloUtils.hasHoneycomb()) {
+ mLyrics.setTextIsSelectable(true);
+ }
+ // Initialze the progess bar
+ mProgressBar = (ProgressBar)rootView.findViewById(R.id.audio_player_lyrics_progess);
+ return rootView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onActivityCreated(final Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ // Enable the options menu
+ setHasOptionsMenu(true);
+ }
+
+ /**
+ * Called to set the lyrics.
+ */
+ public void fetchLyrics(final boolean force) {
+ if (isAdded()) {
+ ApolloUtils.execute(false, new FetchLyrics(), force);
+ }
+ }
+
+ /**
+ * Save current lyrics in file metadata for offline use
+ */
+ private void saveLyrics(final String lyrics) {
+ ApolloUtils.execute(false, new AsyncTask<Void, Void, Void>() {
+
+ @Override
+ protected Void doInBackground(final Void... unused) {
+ final String path = MusicUtils.getFilePath();
+ if (path != null) {
+ OfflineLyricsProvider.saveLyrics(lyrics, path);
+ }
+ return null;
+ }
+ }, (Void[])null);
+ }
+
+ /**
+ * Used to fetch the lyrics for the currently playing song.
+ */
+ private final class FetchLyrics extends AsyncTask<Boolean, Void, String> {
+
+ private final String mArtist;
+
+ private final String mSong;
+
+ /**
+ * Constructor of <code>FetchLyrics</code>
+ */
+ public FetchLyrics() {
+ mArtist = MusicUtils.getArtistName();
+ mSong = MusicUtils.getTrackName();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onPreExecute() {
+ mTryOnline = false;
+ // Release the lyrics on track changes
+ mLyrics.setText(null);
+ mProgressBar.setVisibility(View.VISIBLE);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected String doInBackground(final Boolean... force) {
+ LyricsProvider provider = null;
+ String lyrics = null;
+
+ // First try offline, unless the user wants to fetch new lyrics
+ if (!force[0]) {
+ provider = LyricsProviderFactory.getOfflineProvider(MusicUtils.getFilePath());
+ lyrics = provider.getLyrics(null, null);
+ }
+
+ // Now try to fetch for them
+ if (lyrics == null && ApolloUtils.isOnline(getSherlockActivity())) {
+ mTryOnline = true;
+ provider = LyricsProviderFactory.getMainOnlineProvider();
+ lyrics = provider.getLyrics(mArtist, mSong);
+ }
+ return lyrics;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onPostExecute(final String result) {
+ if (!TextUtils.isEmpty(result) && isAdded()) {
+ // Set the lyrics
+ mLyrics.setText(result);
+ // Save the lyrics
+ saveLyrics(result);
+ } else {
+ if (mTryOnline) {
+ mLyrics.setText(getString(R.string.no_lyrics, mSong));
+ } else {
+ mLyrics.setText(getString(R.string.try_fetch_lyrics, mSong));
+ }
+ }
+ mProgressBar.setVisibility(View.GONE);
+ }
+ }
+}
diff --git a/src/com/andrew/apollo/ui/fragments/PlaylistFragment.java b/src/com/andrew/apollo/ui/fragments/PlaylistFragment.java
new file mode 100644
index 0000000..7ceff23
--- /dev/null
+++ b/src/com/andrew/apollo/ui/fragments/PlaylistFragment.java
@@ -0,0 +1,336 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.ui.fragments;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.MediaStore;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.LoaderManager.LoaderCallbacks;
+import android.support.v4.content.Loader;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ListView;
+
+import com.actionbarsherlock.app.SherlockFragment;
+import com.andrew.apollo.Config;
+import com.andrew.apollo.MusicStateListener;
+import com.andrew.apollo.R;
+import com.andrew.apollo.adapters.PlaylistAdapter;
+import com.andrew.apollo.loaders.PlaylistLoader;
+import com.andrew.apollo.menu.FragmentMenuItems;
+import com.andrew.apollo.menu.RenamePlaylist;
+import com.andrew.apollo.model.Playlist;
+import com.andrew.apollo.recycler.RecycleHolder;
+import com.andrew.apollo.ui.activities.BaseActivity;
+import com.andrew.apollo.ui.activities.ProfileActivity;
+import com.andrew.apollo.utils.MusicUtils;
+
+import java.util.List;
+
+/**
+ * This class is used to display all of the playlists on a user's device.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class PlaylistFragment extends SherlockFragment implements LoaderCallbacks<List<Playlist>>,
+ OnItemClickListener, MusicStateListener {
+
+ /**
+ * Used to keep context menu items from bleeding into other fragments
+ */
+ private static final int GROUP_ID = 0;
+
+ /**
+ * LoaderCallbacks identifier
+ */
+ private static final int LOADER = 0;
+
+ /**
+ * The adapter for the list
+ */
+ private PlaylistAdapter mAdapter;
+
+ /**
+ * The list view
+ */
+ private ListView mListView;
+
+ /**
+ * Represents a playlist
+ */
+ private Playlist mPlaylist;
+
+ /**
+ * Empty constructor as per the {@link Fragment} documentation
+ */
+ public PlaylistFragment() {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onAttach(final Activity activity) {
+ super.onAttach(activity);
+ // Register the music status listener
+ ((BaseActivity)activity).setMusicStateListenerListener(this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Create the adpater
+ mAdapter = new PlaylistAdapter(getSherlockActivity(), R.layout.list_item_simple);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
+ final Bundle savedInstanceState) {
+ // The View for the fragment's UI
+ final ViewGroup rootView = (ViewGroup)inflater.inflate(R.layout.list_base, null);
+ // Initialize the list
+ mListView = (ListView)rootView.findViewById(R.id.list_base);
+ // Set the data behind the grid
+ mListView.setAdapter(mAdapter);
+ // Release any references to the recycled Views
+ mListView.setRecyclerListener(new RecycleHolder());
+ // Listen for ContextMenus to be created
+ mListView.setOnCreateContextMenuListener(this);
+ // Play the selected song
+ mListView.setOnItemClickListener(this);
+ return rootView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onActivityCreated(final Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ // Enable the options menu
+ setHasOptionsMenu(true);
+ // Start the loader
+ getLoaderManager().initLoader(LOADER, null, this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreateContextMenu(final ContextMenu menu, final View v,
+ final ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+
+ // Get the position of the selected item
+ final AdapterContextMenuInfo info = (AdapterContextMenuInfo)menuInfo;
+ final int mPosition = info.position;
+ // Create a new playlist
+ mPlaylist = mAdapter.getItem(mPosition);
+
+ // Play the playlist
+ menu.add(GROUP_ID, FragmentMenuItems.PLAY_SELECTION, Menu.NONE,
+ R.string.context_menu_play_selection);
+
+ // Add the playlist to the queue
+ menu.add(GROUP_ID, FragmentMenuItems.ADD_TO_QUEUE, Menu.NONE, R.string.add_to_queue);
+
+ // Delete and rename (user made playlists)
+ if (info.position > 1) {
+ menu.add(GROUP_ID, FragmentMenuItems.RENAME_PLAYLIST, Menu.NONE,
+ R.string.context_menu_rename_playlist);
+
+ menu.add(GROUP_ID, FragmentMenuItems.DELETE, Menu.NONE, R.string.context_menu_delete);
+
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onContextItemSelected(final android.view.MenuItem item) {
+ if (item.getGroupId() == GROUP_ID) {
+ final AdapterContextMenuInfo info = (AdapterContextMenuInfo)item.getMenuInfo();
+ switch (item.getItemId()) {
+ case FragmentMenuItems.PLAY_SELECTION:
+ if (info.position == 0) {
+ MusicUtils.playFavorites(getSherlockActivity());
+ } else if (info.position == 1) {
+ MusicUtils.playLastAdded(getSherlockActivity());
+ } else {
+ MusicUtils.playPlaylist(getSherlockActivity(), mPlaylist.mPlaylistId);
+ }
+ return true;
+ case FragmentMenuItems.ADD_TO_QUEUE:
+ long[] list = null;
+ if (info.position == 0) {
+ list = MusicUtils.getSongListForFavorites(getSherlockActivity());
+ } else if (info.position == 1) {
+ list = MusicUtils.getSongListForLastAdded(getSherlockActivity());
+ } else {
+ list = MusicUtils.getSongListForPlaylist(getSherlockActivity(),
+ mPlaylist.mPlaylistId);
+ }
+ MusicUtils.addToQueue(getSherlockActivity(), list);
+ return true;
+ case FragmentMenuItems.RENAME_PLAYLIST:
+ RenamePlaylist.getInstance(Long.valueOf(mPlaylist.mPlaylistId)).show(
+ getFragmentManager(), "RenameDialog");
+ return true;
+ case FragmentMenuItems.DELETE:
+ buildDeleteDialog().show();
+ return true;
+ default:
+ break;
+ }
+ }
+ return super.onContextItemSelected(item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onItemClick(final AdapterView<?> parent, final View view, final int position,
+ final long id) {
+ final Bundle bundle = new Bundle();
+ mPlaylist = mAdapter.getItem(position);
+ String playlistName;
+ // Favorites list
+ if (position == 0) {
+ playlistName = getString(R.string.playlist_favorites);
+ bundle.putString(Config.MIME_TYPE, getString(R.string.playlist_favorites));
+ // Last added
+ } else if (position == 1) {
+ playlistName = getString(R.string.playlist_last_added);
+ bundle.putString(Config.MIME_TYPE, getString(R.string.playlist_last_added));
+ } else {
+ // User created
+ playlistName = mPlaylist.mPlaylistName;
+ bundle.putString(Config.MIME_TYPE, MediaStore.Audio.Playlists.CONTENT_TYPE);
+ bundle.putLong(Config.ID, Long.valueOf(mPlaylist.mPlaylistId));
+ }
+
+ bundle.putString(Config.NAME, playlistName);
+
+ // Create the intent to launch the profile activity
+ final Intent intent = new Intent(getSherlockActivity(), ProfileActivity.class);
+ intent.putExtras(bundle);
+ startActivity(intent);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Loader<List<Playlist>> onCreateLoader(final int id, final Bundle args) {
+ return new PlaylistLoader(getSherlockActivity());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoadFinished(final Loader<List<Playlist>> loader, final List<Playlist> data) {
+ // Check for any errors
+ if (data.isEmpty()) {
+ return;
+ }
+
+ // Start fresh
+ mAdapter.unload();
+ // Add the data to the adpater
+ for (final Playlist playlist : data) {
+ mAdapter.add(playlist);
+ }
+ // Build the cache
+ mAdapter.buildCache();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoaderReset(final Loader<List<Playlist>> loader) {
+ // Clear the data in the adapter
+ mAdapter.unload();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void restartLoader() {
+ // Refresh the list when a playlist is deleted or renamed
+ getLoaderManager().restartLoader(LOADER, null, this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onMetaChanged() {
+ // Nothing to do
+ }
+
+ /**
+ * Create a new {@link AlertDialog} for easy playlist deletion
+ *
+ * @param context The {@link Context} to use
+ * @param title The title of the playlist being deleted
+ * @param id The ID of the playlist being deleted
+ * @return A new {@link AlertDialog} used to delete playlists
+ */
+ private final AlertDialog buildDeleteDialog() {
+ return new AlertDialog.Builder(getSherlockActivity())
+ .setTitle(getString(R.string.context_menu_delete) + " " + mPlaylist.mPlaylistName)
+ .setPositiveButton(R.string.context_menu_delete, new OnClickListener() {
+
+ @Override
+ public void onClick(final DialogInterface dialog, final int which) {
+ final Uri mUri = ContentUris.withAppendedId(
+ MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
+ Long.valueOf(mPlaylist.mPlaylistId));
+ getSherlockActivity().getContentResolver().delete(mUri, null, null);
+ MusicUtils.refresh();
+ }
+ }).setNegativeButton(R.string.cancel, new OnClickListener() {
+
+ @Override
+ public void onClick(final DialogInterface dialog, final int which) {
+ dialog.dismiss();
+ }
+ }).setMessage(R.string.cannot_be_undone).create();
+ }
+
+}
diff --git a/src/com/andrew/apollo/ui/fragments/QueueFragment.java b/src/com/andrew/apollo/ui/fragments/QueueFragment.java
new file mode 100644
index 0000000..d143415
--- /dev/null
+++ b/src/com/andrew/apollo/ui/fragments/QueueFragment.java
@@ -0,0 +1,409 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.ui.fragments;
+
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.LoaderManager.LoaderCallbacks;
+import android.support.v4.content.Loader;
+import android.text.TextUtils;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.SubMenu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+
+import com.actionbarsherlock.app.SherlockFragment;
+import com.actionbarsherlock.view.MenuInflater;
+import com.andrew.apollo.R;
+import com.andrew.apollo.adapters.SongAdapter;
+import com.andrew.apollo.dragdrop.DragSortListView;
+import com.andrew.apollo.dragdrop.DragSortListView.DragScrollProfile;
+import com.andrew.apollo.dragdrop.DragSortListView.DropListener;
+import com.andrew.apollo.dragdrop.DragSortListView.RemoveListener;
+import com.andrew.apollo.loaders.NowPlayingCursor;
+import com.andrew.apollo.loaders.QueueLoader;
+import com.andrew.apollo.menu.CreateNewPlaylist;
+import com.andrew.apollo.menu.FragmentMenuItems;
+import com.andrew.apollo.model.Song;
+import com.andrew.apollo.provider.FavoritesStore;
+import com.andrew.apollo.recycler.RecycleHolder;
+import com.andrew.apollo.utils.MusicUtils;
+import com.andrew.apollo.utils.NavUtils;
+import com.viewpagerindicator.TitlePageIndicator;
+
+import java.util.List;
+
+/**
+ * This class is used to display all of the songs in the queue.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class QueueFragment extends SherlockFragment implements LoaderCallbacks<List<Song>>,
+ OnItemClickListener, DropListener, RemoveListener, DragScrollProfile {
+
+ /**
+ * Used to keep context menu items from bleeding into other fragments
+ */
+ private static final int GROUP_ID = 13;
+
+ /**
+ * LoaderCallbacks identifier
+ */
+ private static final int LOADER = 0;
+
+ /**
+ * The adapter for the list
+ */
+ private SongAdapter mAdapter;
+
+ /**
+ * The list view
+ */
+ private DragSortListView mListView;
+
+ /**
+ * Represents a song
+ */
+ private Song mSong;
+
+ /**
+ * Position of a context menu item
+ */
+ private int mSelectedPosition;
+
+ /**
+ * Id of a context menu item
+ */
+ private long mSelectedId;
+
+ /**
+ * Song, album, and artist name used in the context menu
+ */
+ private String mSongName, mAlbumName, mArtistName;
+
+ /**
+ * Empty constructor as per the {@link Fragment} documentation
+ */
+ public QueueFragment() {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Create the adpater
+ mAdapter = new SongAdapter(getSherlockActivity(), R.layout.edit_track_list_item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
+ final Bundle savedInstanceState) {
+ // The View for the fragment's UI
+ final ViewGroup rootView = (ViewGroup)inflater.inflate(R.layout.list_base, null);
+ // Initialize the list
+ mListView = (DragSortListView)rootView.findViewById(R.id.list_base);
+ // Set the data behind the list
+ mListView.setAdapter(mAdapter);
+ // Release any references to the recycled Views
+ mListView.setRecyclerListener(new RecycleHolder());
+ // Listen for ContextMenus to be created
+ mListView.setOnCreateContextMenuListener(this);
+ // Play the selected song
+ mListView.setOnItemClickListener(this);
+ // Set the drop listener
+ mListView.setDropListener(this);
+ // Set the swipe to remove listener
+ mListView.setRemoveListener(this);
+ // Quick scroll while dragging
+ mListView.setDragScrollProfile(this);
+ return rootView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onActivityCreated(final Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ // Enable the options menu
+ setHasOptionsMenu(true);
+ // Start the loader
+ getLoaderManager().initLoader(LOADER, null, this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreateOptionsMenu(final com.actionbarsherlock.view.Menu menu,
+ final MenuInflater inflater) {
+ inflater.inflate(R.menu.queue, menu);
+ super.onCreateOptionsMenu(menu, inflater);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onOptionsItemSelected(final com.actionbarsherlock.view.MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.menu_save_queue:
+ NowPlayingCursor queue = (NowPlayingCursor)QueueLoader
+ .makeQueueCursor(getSherlockActivity());
+ CreateNewPlaylist.getInstance(MusicUtils.getSongListForCursor(queue)).show(
+ getFragmentManager(), "CreatePlaylist");
+ queue.close();
+ queue = null;
+ return true;
+ case R.id.menu_clear_queue:
+ MusicUtils.clearQueue();
+ NavUtils.goHome(getSherlockActivity());
+ return true;
+ default:
+ break;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreateContextMenu(final ContextMenu menu, final View v,
+ final ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ // Get the position of the selected item
+ final AdapterContextMenuInfo info = (AdapterContextMenuInfo)menuInfo;
+ mSelectedPosition = info.position;
+ // Creat a new song
+ mSong = mAdapter.getItem(mSelectedPosition);
+ mSelectedId = Long.valueOf(mSong.mSongId);
+ mSongName = mSong.mSongName;
+ mAlbumName = mSong.mAlbumName;
+ mArtistName = mSong.mArtistName;
+
+ // Play the song next
+ menu.add(GROUP_ID, FragmentMenuItems.PLAY_NEXT, Menu.NONE,
+ getString(R.string.context_menu_play_next));
+
+ // Add the song to the queue
+ menu.add(GROUP_ID, FragmentMenuItems.ADD_TO_QUEUE, Menu.NONE,
+ getString(R.string.add_to_queue));
+
+ // Add the song to a playlist
+ final SubMenu subMenu = menu.addSubMenu(GROUP_ID, FragmentMenuItems.ADD_TO_PLAYLIST,
+ Menu.NONE, R.string.add_to_playlist);
+ MusicUtils.makePlaylistMenu(getSherlockActivity(), GROUP_ID, subMenu, true);
+
+ // View more content by the song artist
+ menu.add(GROUP_ID, FragmentMenuItems.MORE_BY_ARTIST, Menu.NONE,
+ getString(R.string.context_menu_more_by_artist));
+
+ // Make the song a ringtone
+ menu.add(GROUP_ID, FragmentMenuItems.USE_AS_RINGTONE, Menu.NONE,
+ getString(R.string.context_menu_use_as_ringtone));
+
+ // Delete the song
+ // menu.add(GROUP_ID, FragmentMenuItems.DELETE, Menu.NONE,
+ // getString(R.string.context_menu_delete));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onContextItemSelected(final android.view.MenuItem item) {
+ if (item.getGroupId() == GROUP_ID) {
+ switch (item.getItemId()) {
+ case FragmentMenuItems.PLAY_NEXT:
+ NowPlayingCursor queue = (NowPlayingCursor)QueueLoader
+ .makeQueueCursor(getSherlockActivity());
+ queue.removeItem(mSelectedPosition);
+ queue.close();
+ queue = null;
+ MusicUtils.playNext(new long[] {
+ mSelectedId
+ });
+ getLoaderManager().restartLoader(LOADER, null, this);
+ return true;
+ case FragmentMenuItems.ADD_TO_QUEUE:
+ MusicUtils.addToQueue(getSherlockActivity(), new long[] {
+ mSelectedId
+ });
+ return true;
+ case FragmentMenuItems.ADD_TO_FAVORITES:
+ FavoritesStore.getInstance(getSherlockActivity()).addSongId(
+ Long.valueOf(mSelectedId), mSongName, mAlbumName, mArtistName);
+ return true;
+ case FragmentMenuItems.NEW_PLAYLIST:
+ CreateNewPlaylist.getInstance(new long[] {
+ mSelectedId
+ }).show(getFragmentManager(), "CreatePlaylist");
+ return true;
+ case FragmentMenuItems.PLAYLIST_SELECTED:
+ final long mPlaylistId = item.getIntent().getLongExtra("playlist", 0);
+ MusicUtils.addToPlaylist(getSherlockActivity(), new long[] {
+ mSelectedId
+ }, mPlaylistId);
+ return true;
+ case FragmentMenuItems.MORE_BY_ARTIST:
+ NavUtils.openArtistProfile(getSherlockActivity(), mArtistName);
+ return true;
+ case FragmentMenuItems.USE_AS_RINGTONE:
+ MusicUtils.setRingtone(getSherlockActivity(), mSelectedId);
+ return true;
+ // case FragmentMenuItems.DELETE:
+ // return true;
+ default:
+ break;
+ }
+ }
+ return super.onContextItemSelected(item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onItemClick(final AdapterView<?> parent, final View view, final int position,
+ final long id) {
+ // When selecting a track from the queue, just jump there instead of
+ // reloading the queue. This is both faster, and prevents accidentally
+ // dropping out of party shuffle.
+ MusicUtils.setQueuePosition(position);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Loader<List<Song>> onCreateLoader(final int id, final Bundle args) {
+ return new QueueLoader(getSherlockActivity());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoadFinished(final Loader<List<Song>> loader, final List<Song> data) {
+ // Check for any errors
+ if (data.isEmpty()) {
+ return;
+ }
+
+ // Start fresh
+ mAdapter.unload();
+ // Add the data to the adpater
+ for (final Song song : data) {
+ mAdapter.add(song);
+ }
+ // Build the cache
+ mAdapter.buildCache();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoaderReset(final Loader<List<Song>> loader) {
+ // Clear the data in the adapter
+ mAdapter.unload();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public float getSpeed(final float w, final long t) {
+ if (w > 0.8f) {
+ return mAdapter.getCount() / 0.001f;
+ } else {
+ return 10.0f * w;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void remove(final int which) {
+ mSong = mAdapter.getItem(which);
+ mAdapter.remove(mSong);
+ mAdapter.notifyDataSetChanged();
+ MusicUtils.removeTrack(Long.valueOf(mSong.mSongId));
+ // Build the cache
+ mAdapter.buildCache();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void drop(final int from, final int to) {
+ mSong = mAdapter.getItem(from);
+ mAdapter.remove(mSong);
+ mAdapter.insert(mSong, to);
+ mAdapter.notifyDataSetChanged();
+ MusicUtils.moveQueueItem(from, to);
+ // Build the cache
+ mAdapter.buildCache();
+ }
+
+ /**
+ * Scrolls the list to the currently playing song when the user touches the
+ * header in the {@link TitlePageIndicator}.
+ */
+ public void scrollToCurrentSong() {
+ final int currentSongPosition = getItemPositionBySong();
+
+ if (currentSongPosition != 0) {
+ mListView.setSelection(currentSongPosition);
+ }
+ }
+
+ /**
+ * @return The position of an item in the list based on the name of the
+ * currently playing song.
+ */
+ private int getItemPositionBySong() {
+ final String trackName = String.valueOf(MusicUtils.getCurrentAudioId());
+ if (mAdapter == null || TextUtils.isEmpty(trackName)) {
+ return 0;
+ }
+ for (int i = 0; i < mAdapter.getCount(); i++) {
+ if (mAdapter.getItem(i).mSongId.equals(trackName)) {
+ return i;
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Called to restart the loader callbacks
+ */
+ public void refreshQueue() {
+ if (isAdded()) {
+ getLoaderManager().restartLoader(LOADER, null, this);
+ }
+ }
+}
diff --git a/src/com/andrew/apollo/ui/fragments/RecentFragment.java b/src/com/andrew/apollo/ui/fragments/RecentFragment.java
new file mode 100644
index 0000000..8469e21
--- /dev/null
+++ b/src/com/andrew/apollo/ui/fragments/RecentFragment.java
@@ -0,0 +1,429 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.ui.fragments;
+
+import static com.andrew.apollo.utils.PreferenceUtils.RECENT_LAYOUT;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.support.v4.app.LoaderManager.LoaderCallbacks;
+import android.support.v4.content.Loader;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.SubMenu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.AbsListView.OnScrollListener;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.GridView;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.actionbarsherlock.app.SherlockFragment;
+import com.andrew.apollo.Config;
+import com.andrew.apollo.MusicStateListener;
+import com.andrew.apollo.R;
+import com.andrew.apollo.adapters.AlbumAdapter;
+import com.andrew.apollo.loaders.RecentLoader;
+import com.andrew.apollo.menu.CreateNewPlaylist;
+import com.andrew.apollo.menu.DeleteDialog;
+import com.andrew.apollo.menu.FragmentMenuItems;
+import com.andrew.apollo.model.Album;
+import com.andrew.apollo.provider.RecentStore;
+import com.andrew.apollo.recycler.RecycleHolder;
+import com.andrew.apollo.ui.activities.BaseActivity;
+import com.andrew.apollo.utils.ApolloUtils;
+import com.andrew.apollo.utils.MusicUtils;
+import com.andrew.apollo.utils.NavUtils;
+import com.andrew.apollo.utils.PreferenceUtils;
+
+import java.util.List;
+
+/**
+ * This class is used to display all of the recently listened to albums by the
+ * user.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class RecentFragment extends SherlockFragment implements LoaderCallbacks<List<Album>>,
+ OnScrollListener, OnItemClickListener, MusicStateListener {
+
+ /**
+ * Used to keep context menu items from bleeding into other fragments
+ */
+ private static final int GROUP_ID = 3;
+
+ /**
+ * Grid view column count. ONE - list, TWO - normal grid, FOUR - landscape
+ */
+ private static final int ONE = 1, TWO = 2, FOUR = 4;
+
+ /**
+ * LoaderCallbacks identifier
+ */
+ private static final int LOADER = 0;
+
+ /**
+ * Fragment UI
+ */
+ private ViewGroup mRootView;
+
+ /**
+ * The adapter for the grid
+ */
+ private AlbumAdapter mAdapter;
+
+ /**
+ * The grid view
+ */
+ private GridView mGridView;
+
+ /**
+ * The list view
+ */
+ private ListView mListView;
+
+ /**
+ * Album song list
+ */
+ private long[] mAlbumList;
+
+ /**
+ * Represents an album
+ */
+ private Album mAlbum;
+
+ /**
+ * True if the list should execute {@code #restartLoader()}.
+ */
+ private boolean mShouldRefresh = false;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onAttach(final Activity activity) {
+ super.onAttach(activity);
+ // Register the music status listener
+ ((BaseActivity)activity).setMusicStateListenerListener(this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ int layout = R.layout.list_item_normal;
+ if (isSimpleLayout()) {
+ layout = R.layout.list_item_normal;
+ } else if (isDetailedLayout()) {
+ layout = R.layout.list_item_detailed;
+ } else {
+ layout = R.layout.grid_items_normal;
+ }
+ mAdapter = new AlbumAdapter(getSherlockActivity(), layout);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
+ final Bundle savedInstanceState) {
+ // The View for the fragment's UI
+ if (isSimpleLayout()) {
+ mRootView = (ViewGroup)inflater.inflate(R.layout.list_base, null);
+ initListView();
+ } else {
+ mRootView = (ViewGroup)inflater.inflate(R.layout.grid_base, null);
+ initGridView();
+ }
+ return mRootView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onActivityCreated(final Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ // Enable the options menu
+ setHasOptionsMenu(true);
+ // Start the loader
+ getLoaderManager().initLoader(LOADER, null, this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onPause() {
+ super.onPause();
+ mAdapter.flush();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreateContextMenu(final ContextMenu menu, final View v,
+ final ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+
+ // Get the position of the selected item
+ final AdapterContextMenuInfo info = (AdapterContextMenuInfo)menuInfo;
+ // Create a new album
+ mAlbum = mAdapter.getItem(info.position);
+ // Create a list of the album's songs
+ mAlbumList = MusicUtils.getSongListForAlbum(getSherlockActivity(), mAlbum.mAlbumId);
+
+ // Play the album
+ menu.add(GROUP_ID, FragmentMenuItems.PLAY_SELECTION, Menu.NONE,
+ getString(R.string.context_menu_play_selection));
+
+ // Add the album to the queue
+ menu.add(GROUP_ID, FragmentMenuItems.ADD_TO_QUEUE, Menu.NONE,
+ getString(R.string.add_to_queue));
+
+ // Add the album to a playlist
+ final SubMenu subMenu = menu.addSubMenu(GROUP_ID, FragmentMenuItems.ADD_TO_PLAYLIST,
+ Menu.NONE, R.string.add_to_playlist);
+ MusicUtils.makePlaylistMenu(getSherlockActivity(), GROUP_ID, subMenu, false);
+
+ // View more content by the album artist
+ menu.add(GROUP_ID, FragmentMenuItems.MORE_BY_ARTIST, Menu.NONE,
+ getString(R.string.context_menu_more_by_artist));
+
+ // Remove the album from the list
+ menu.add(GROUP_ID, FragmentMenuItems.REMOVE_FROM_RECENT, Menu.NONE,
+ getString(R.string.context_menu_remove_from_recent));
+
+ // Delete the album
+ menu.add(GROUP_ID, FragmentMenuItems.DELETE, Menu.NONE,
+ getString(R.string.context_menu_delete));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onContextItemSelected(final MenuItem item) {
+ // Avoid leaking context menu selections
+ if (item.getGroupId() == GROUP_ID) {
+ switch (item.getItemId()) {
+ case FragmentMenuItems.PLAY_SELECTION:
+ MusicUtils.playAll(getSherlockActivity(), mAlbumList, 0, false);
+ return true;
+ case FragmentMenuItems.ADD_TO_QUEUE:
+ MusicUtils.addToQueue(getSherlockActivity(), mAlbumList);
+ return true;
+ case FragmentMenuItems.NEW_PLAYLIST:
+ CreateNewPlaylist.getInstance(mAlbumList).show(getFragmentManager(),
+ "CreatePlaylist");
+ return true;
+ case FragmentMenuItems.MORE_BY_ARTIST:
+ NavUtils.openArtistProfile(getSherlockActivity(), mAlbum.mArtistName);
+ return true;
+ case FragmentMenuItems.PLAYLIST_SELECTED:
+ final long id = item.getIntent().getLongExtra("playlist", 0);
+ MusicUtils.addToPlaylist(getSherlockActivity(), mAlbumList, id);
+ return true;
+ case FragmentMenuItems.REMOVE_FROM_RECENT:
+ mShouldRefresh = true;
+ RecentStore.getInstance(getSherlockActivity()).removeItem(mAlbum.mAlbumId);
+ MusicUtils.refresh();
+ return true;
+ case FragmentMenuItems.DELETE:
+ mShouldRefresh = true;
+ final String album = mAlbum.mAlbumName;
+ DeleteDialog.newInstance(album, mAlbumList, album + Config.ALBUM_ART_SUFFIX)
+ .show(getFragmentManager(), "DeleteDialog");
+ return true;
+ default:
+ break;
+ }
+ }
+ return super.onContextItemSelected(item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onScrollStateChanged(final AbsListView view, final int scrollState) {
+ // Pause disk cache access to ensure smoother scrolling
+ if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_FLING
+ || scrollState == AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
+ mAdapter.setPauseDiskCache(true);
+ } else {
+ mAdapter.setPauseDiskCache(false);
+ mAdapter.notifyDataSetChanged();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onItemClick(final AdapterView<?> parent, final View view, final int position,
+ final long id) {
+ mAlbum = mAdapter.getItem(position);
+ NavUtils.openAlbumProfile(getSherlockActivity(), mAlbum.mAlbumName, mAlbum.mArtistName);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Loader<List<Album>> onCreateLoader(final int id, final Bundle args) {
+ return new RecentLoader(getSherlockActivity());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoadFinished(final Loader<List<Album>> loader, final List<Album> data) {
+ // Check for any errors
+ if (data.isEmpty()) {
+ // Set the empty text
+ final TextView empty = (TextView)mRootView.findViewById(R.id.empty);
+ empty.setText(getString(R.string.empty_recent));
+ if (isSimpleLayout()) {
+ mListView.setEmptyView(empty);
+ } else {
+ mGridView.setEmptyView(empty);
+ }
+ return;
+ }
+
+ // Start fresh
+ mAdapter.unload();
+ // Add the data to the adpater
+ for (final Album album : data) {
+ mAdapter.add(album);
+ }
+ // Build the cache
+ mAdapter.buildCache();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoaderReset(final Loader<List<Album>> loader) {
+ // Clear the data in the adapter
+ mAdapter.unload();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onScroll(final AbsListView view, final int firstVisibleItem,
+ final int visibleItemCount, final int totalItemCount) {
+ // Nothing to do
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void restartLoader() {
+ // Update the list when the user deletes any items
+ if (mShouldRefresh) {
+ getLoaderManager().restartLoader(LOADER, null, this);
+ }
+ mShouldRefresh = false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onMetaChanged() {
+ getLoaderManager().restartLoader(LOADER, null, this);
+ }
+
+ /**
+ * Sets up various helpers for both the list and grid
+ *
+ * @param list The list or grid
+ */
+ private void initAbsListView(final AbsListView list) {
+ // Release any references to the recycled Views
+ list.setRecyclerListener(new RecycleHolder());
+ // Listen for ContextMenus to be created
+ list.setOnCreateContextMenuListener(this);
+ // Show the albums and songs from the selected artist
+ list.setOnItemClickListener(this);
+ // To help make scrolling smooth
+ list.setOnScrollListener(this);
+ }
+
+ /**
+ * Sets up the list view
+ */
+ private void initListView() {
+ // Initialize the grid
+ mListView = (ListView)mRootView.findViewById(R.id.list_base);
+ // Set the data behind the list
+ mListView.setAdapter(mAdapter);
+ // Set up the helpers
+ initAbsListView(mListView);
+ mAdapter.setTouchPlay(true);
+ }
+
+ /**
+ * Sets up the grid view
+ */
+ private void initGridView() {
+ // Initialize the grid
+ mGridView = (GridView)mRootView.findViewById(R.id.grid_base);
+ // Set the data behind the grid
+ mGridView.setAdapter(mAdapter);
+ // Set up the helpers
+ initAbsListView(mGridView);
+ if (ApolloUtils.isLandscape(getSherlockActivity())) {
+ if (isDetailedLayout()) {
+ mAdapter.setLoadExtraData(true);
+ mGridView.setNumColumns(TWO);
+ } else {
+ mGridView.setNumColumns(FOUR);
+ }
+ } else {
+ if (isDetailedLayout()) {
+ mAdapter.setLoadExtraData(true);
+ mGridView.setNumColumns(ONE);
+ } else {
+ mGridView.setNumColumns(TWO);
+ }
+ }
+ }
+
+ private boolean isSimpleLayout() {
+ return PreferenceUtils.getInstace(getSherlockActivity()).isSimpleLayout(RECENT_LAYOUT,
+ getSherlockActivity());
+ }
+
+ private boolean isDetailedLayout() {
+ return PreferenceUtils.getInstace(getSherlockActivity()).isDetailedLayout(RECENT_LAYOUT,
+ getSherlockActivity());
+ }
+}
diff --git a/src/com/andrew/apollo/ui/fragments/SongFragment.java b/src/com/andrew/apollo/ui/fragments/SongFragment.java
new file mode 100644
index 0000000..c24147c
--- /dev/null
+++ b/src/com/andrew/apollo/ui/fragments/SongFragment.java
@@ -0,0 +1,372 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.ui.fragments;
+
+import android.app.Activity;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.LoaderManager.LoaderCallbacks;
+import android.support.v4.content.Loader;
+import android.text.TextUtils;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.SubMenu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.actionbarsherlock.app.SherlockFragment;
+import com.andrew.apollo.MusicStateListener;
+import com.andrew.apollo.R;
+import com.andrew.apollo.adapters.SongAdapter;
+import com.andrew.apollo.loaders.SongLoader;
+import com.andrew.apollo.menu.CreateNewPlaylist;
+import com.andrew.apollo.menu.DeleteDialog;
+import com.andrew.apollo.menu.FragmentMenuItems;
+import com.andrew.apollo.model.Song;
+import com.andrew.apollo.provider.FavoritesStore;
+import com.andrew.apollo.recycler.RecycleHolder;
+import com.andrew.apollo.ui.activities.BaseActivity;
+import com.andrew.apollo.utils.MusicUtils;
+import com.andrew.apollo.utils.NavUtils;
+import com.viewpagerindicator.TitlePageIndicator;
+
+import java.util.List;
+
+/**
+ * This class is used to display all of the songs on a user's device.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class SongFragment extends SherlockFragment implements LoaderCallbacks<List<Song>>,
+ OnItemClickListener, MusicStateListener {
+
+ /**
+ * Used to keep context menu items from bleeding into other fragments
+ */
+ private static final int GROUP_ID = 4;
+
+ /**
+ * LoaderCallbacks identifier
+ */
+ private static final int LOADER = 0;
+
+ /**
+ * Fragment UI
+ */
+ private ViewGroup mRootView;
+
+ /**
+ * The adapter for the list
+ */
+ private SongAdapter mAdapter;
+
+ /**
+ * The list view
+ */
+ private ListView mListView;
+
+ /**
+ * Represents a song
+ */
+ private Song mSong;
+
+ /**
+ * Position of a context menu item
+ */
+ private int mSelectedPosition;
+
+ /**
+ * Id of a context menu item
+ */
+ private long mSelectedId;
+
+ /**
+ * Song, album, and artist name used in the context menu
+ */
+ private String mSongName, mAlbumName, mArtistName;
+
+ /**
+ * True if the list should execute {@code #restartLoader()}.
+ */
+ private boolean mShouldRefresh = false;
+
+ /**
+ * Empty constructor as per the {@link Fragment} documentation
+ */
+ public SongFragment() {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onAttach(final Activity activity) {
+ super.onAttach(activity);
+ // Register the music status listener
+ ((BaseActivity)activity).setMusicStateListenerListener(this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Create the adpater
+ mAdapter = new SongAdapter(getSherlockActivity(), R.layout.list_item_simple);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
+ final Bundle savedInstanceState) {
+ // The View for the fragment's UI
+ mRootView = (ViewGroup)inflater.inflate(R.layout.list_base, null);
+ // Initialize the list
+ mListView = (ListView)mRootView.findViewById(R.id.list_base);
+ // Set the data behind the list
+ mListView.setAdapter(mAdapter);
+ // Release any references to the recycled Views
+ mListView.setRecyclerListener(new RecycleHolder());
+ // Listen for ContextMenus to be created
+ mListView.setOnCreateContextMenuListener(this);
+ // Play the selected song
+ mListView.setOnItemClickListener(this);
+ return mRootView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onActivityCreated(final Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ // Enable the options menu
+ setHasOptionsMenu(true);
+ // Start the loader
+ getLoaderManager().initLoader(LOADER, null, this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreateContextMenu(final ContextMenu menu, final View v,
+ final ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ // Get the position of the selected item
+ final AdapterContextMenuInfo info = (AdapterContextMenuInfo)menuInfo;
+ mSelectedPosition = info.position;
+ // Creat a new song
+ mSong = mAdapter.getItem(mSelectedPosition);
+ mSelectedId = Long.valueOf(mSong.mSongId);
+ mSongName = mSong.mSongName;
+ mAlbumName = mSong.mAlbumName;
+ mArtistName = mSong.mArtistName;
+
+ // Play the song
+ menu.add(GROUP_ID, FragmentMenuItems.PLAY_SELECTION, Menu.NONE,
+ getString(R.string.context_menu_play_selection));
+
+ // Add the song to the queue
+ menu.add(GROUP_ID, FragmentMenuItems.ADD_TO_QUEUE, Menu.NONE,
+ getString(R.string.add_to_queue));
+
+ // Add the song to a playlist
+ final SubMenu subMenu = menu.addSubMenu(GROUP_ID, FragmentMenuItems.ADD_TO_PLAYLIST,
+ Menu.NONE, R.string.add_to_playlist);
+ MusicUtils.makePlaylistMenu(getSherlockActivity(), GROUP_ID, subMenu, true);
+
+ // View more content by the song artist
+ menu.add(GROUP_ID, FragmentMenuItems.MORE_BY_ARTIST, Menu.NONE,
+ getString(R.string.context_menu_more_by_artist));
+
+ // Make the song a ringtone
+ menu.add(GROUP_ID, FragmentMenuItems.USE_AS_RINGTONE, Menu.NONE,
+ getString(R.string.context_menu_use_as_ringtone));
+
+ // Delete the song
+ menu.add(GROUP_ID, FragmentMenuItems.DELETE, Menu.NONE,
+ getString(R.string.context_menu_delete));
+ }
+
+ @Override
+ public boolean onContextItemSelected(final android.view.MenuItem item) {
+ if (item.getGroupId() == GROUP_ID) {
+ switch (item.getItemId()) {
+ case FragmentMenuItems.PLAY_SELECTION:
+ MusicUtils.playAll(getSherlockActivity(), new long[] {
+ mSelectedId
+ }, 0, false);
+ return true;
+ case FragmentMenuItems.ADD_TO_QUEUE:
+ MusicUtils.addToQueue(getSherlockActivity(), new long[] {
+ mSelectedId
+ });
+ return true;
+ case FragmentMenuItems.ADD_TO_FAVORITES:
+ FavoritesStore.getInstance(getSherlockActivity()).addSongId(
+ Long.valueOf(mSelectedId), mSongName, mAlbumName, mArtistName);
+ return true;
+ case FragmentMenuItems.NEW_PLAYLIST:
+ CreateNewPlaylist.getInstance(new long[] {
+ mSelectedId
+ }).show(getFragmentManager(), "CreatePlaylist");
+ return true;
+ case FragmentMenuItems.PLAYLIST_SELECTED:
+ final long mPlaylistId = item.getIntent().getLongExtra("playlist", 0);
+ MusicUtils.addToPlaylist(getSherlockActivity(), new long[] {
+ mSelectedId
+ }, mPlaylistId);
+ return true;
+ case FragmentMenuItems.MORE_BY_ARTIST:
+ NavUtils.openArtistProfile(getSherlockActivity(), mArtistName);
+ return true;
+ case FragmentMenuItems.USE_AS_RINGTONE:
+ MusicUtils.setRingtone(getSherlockActivity(), mSelectedId);
+ return true;
+ case FragmentMenuItems.DELETE:
+ mShouldRefresh = true;
+ DeleteDialog.newInstance(mSong.mSongName, new long[] {
+ mSelectedId
+ }, null).show(getFragmentManager(), "DeleteDialog");
+ return true;
+ default:
+ break;
+ }
+ }
+ return super.onContextItemSelected(item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onItemClick(final AdapterView<?> parent, final View view, final int position,
+ final long id) {
+ Cursor cursor = SongLoader.makeSongCursor(getSherlockActivity());
+ final long[] list = MusicUtils.getSongListForCursor(cursor);
+ MusicUtils.playAll(getSherlockActivity(), list, position, false);
+ cursor.close();
+ cursor = null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Loader<List<Song>> onCreateLoader(final int id, final Bundle args) {
+ return new SongLoader(getSherlockActivity());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoadFinished(final Loader<List<Song>> loader, final List<Song> data) {
+ // Check for any errors
+ if (data.isEmpty()) {
+ // Set the empty text
+ final TextView empty = (TextView)mRootView.findViewById(R.id.empty);
+ empty.setText(getString(R.string.empty_music));
+ mListView.setEmptyView(empty);
+ return;
+ }
+
+ // Start fresh
+ mAdapter.unload();
+ // Add the data to the adpater
+ for (final Song song : data) {
+ mAdapter.add(song);
+ }
+ // Build the cache
+ mAdapter.buildCache();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoaderReset(final Loader<List<Song>> loader) {
+ // Clear the data in the adapter
+ mAdapter.unload();
+ }
+
+ /**
+ * Scrolls the list to the currently playing song when the user touches the
+ * header in the {@link TitlePageIndicator}.
+ */
+ public void scrollToCurrentSong() {
+ final int currentSongPosition = getItemPositionBySong();
+
+ if (currentSongPosition != 0) {
+ mListView.setSelection(currentSongPosition);
+ }
+ }
+
+ /**
+ * @return The position of an item in the list based on the name of the
+ * currently playing song.
+ */
+ private int getItemPositionBySong() {
+ final String trackName = String.valueOf(MusicUtils.getCurrentAudioId());
+ if (mAdapter == null || TextUtils.isEmpty(trackName)) {
+ return 0;
+ }
+ for (int i = 0; i < mAdapter.getCount(); i++) {
+ if (mAdapter.getItem(i).mSongId.equals(trackName)) {
+ return i;
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Restarts the loader.
+ */
+ public void refresh() {
+ // Wait a moment for the preference to change.
+ SystemClock.sleep(10);
+ getLoaderManager().restartLoader(LOADER, null, this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void restartLoader() {
+ // Update the list when the user deletes any items
+ if (mShouldRefresh) {
+ getLoaderManager().restartLoader(LOADER, null, this);
+ }
+ mShouldRefresh = false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onMetaChanged() {
+ // Nothing to do
+ }
+}
diff --git a/src/com/andrew/apollo/ui/fragments/ThemeFragment.java b/src/com/andrew/apollo/ui/fragments/ThemeFragment.java
new file mode 100644
index 0000000..2f91004
--- /dev/null
+++ b/src/com/andrew/apollo/ui/fragments/ThemeFragment.java
@@ -0,0 +1,320 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.ui.fragments;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.GridView;
+
+import com.actionbarsherlock.app.SherlockFragment;
+import com.actionbarsherlock.view.Menu;
+import com.andrew.apollo.R;
+import com.andrew.apollo.recycler.RecycleHolder;
+import com.andrew.apollo.ui.MusicHolder;
+import com.andrew.apollo.utils.ThemeUtils;
+import com.devspark.appmsg.Crouton;
+
+import java.util.List;
+
+/**
+ * Used to show all of the available themes on a user's device.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class ThemeFragment extends SherlockFragment implements OnItemClickListener {
+
+ private static final int OPEN_IN_PLAY_STORE = 0;
+
+ private GridView mGridView;
+
+ private PackageManager mPackageManager;
+
+ private List<ResolveInfo> mThemes;
+
+ private String[] mEntries;
+
+ private String[] mValues;
+
+ private Drawable[] mThemePreview;
+
+ private Resources mThemeResources;
+
+ private String mThemePackageName;
+
+ private String mThemeName;
+
+ private ThemesAdapter mAdapter;
+
+ private ThemeUtils mTheme;
+
+ /**
+ * Empty constructor as per the {@link Fragment} documentation
+ */
+ public ThemeFragment() {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
+ final Bundle savedInstanceState) {
+ // The View for the fragment's UI
+ final ViewGroup rootView = (ViewGroup)inflater.inflate(R.layout.grid_base, null);
+ // Initialize the grid
+ mGridView = (GridView)rootView.findViewById(R.id.grid_base);
+ // Release any reference to the recycled Views
+ mGridView.setRecyclerListener(new RecycleHolder());
+ // Set the new theme
+ mGridView.setOnItemClickListener(this);
+ // Listen for ContextMenus to be created
+ mGridView.setOnCreateContextMenuListener(this);
+ if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
+ // Limit the columns to one in portrait mode
+ mGridView.setNumColumns(1);
+ } else {
+ // And two for landscape
+ mGridView.setNumColumns(2);
+ }
+ return rootView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onActivityCreated(final Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ final Intent apolloThemeIntent = new Intent("com.andrew.apollo.THEMES");
+ apolloThemeIntent.addCategory("android.intent.category.DEFAULT");
+
+ mPackageManager = getSherlockActivity().getPackageManager();
+ mThemes = mPackageManager.queryIntentActivities(apolloThemeIntent, 0);
+ mEntries = new String[mThemes.size() + 1];
+ mValues = new String[mThemes.size() + 1];
+ mThemePreview = new Drawable[mThemes.size() + 1];
+
+ // Default items
+ mEntries[0] = getString(R.string.app_name);
+ // mValues[0] = ThemeUtils.APOLLO_PACKAGE;
+ mThemePreview[0] = getResources().getDrawable(R.drawable.theme_preview);
+
+ for (int i = 0; i < mThemes.size(); i++) {
+ mThemePackageName = mThemes.get(i).activityInfo.packageName.toString();
+ mThemeName = mThemes.get(i).loadLabel(mPackageManager).toString();
+ mEntries[i + 1] = mThemeName;
+ mValues[i + 1] = mThemePackageName;
+
+ // Theme resources
+ try {
+ mThemeResources = mPackageManager.getResourcesForApplication(mThemePackageName
+ .toString());
+ } catch (final NameNotFoundException ignored) {
+ }
+
+ // Theme preview
+ final int previewId = mThemeResources.getIdentifier("theme_preview", "drawable", //$NON-NLS-2$
+ mThemePackageName.toString());
+ if (previewId != 0) {
+ mThemePreview[i + 1] = mThemeResources.getDrawable(previewId);
+ }
+ }
+
+ // Initialize the Adapter
+ mAdapter = new ThemesAdapter(getSherlockActivity(), R.layout.fragment_themes_base);
+ // Bind the data
+ mGridView.setAdapter(mAdapter);
+
+ // Get the theme utils
+ mTheme = new ThemeUtils(getSherlockActivity());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreateContextMenu(final ContextMenu menu, final View v,
+ final ContextMenuInfo menuInfo) {
+ final AdapterContextMenuInfo info = (AdapterContextMenuInfo)menuInfo;
+ if (info.position > 0) {
+ // Open to the theme's Play Store page
+ menu.add(Menu.NONE, OPEN_IN_PLAY_STORE, Menu.NONE,
+ getString(R.string.context_menu_open_in_play_store));
+ }
+ super.onCreateContextMenu(menu, v, menuInfo);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onContextItemSelected(final android.view.MenuItem item) {
+ final AdapterContextMenuInfo info = (AdapterContextMenuInfo)item.getMenuInfo();
+ switch (item.getItemId()) {
+ case OPEN_IN_PLAY_STORE:
+ ThemeUtils.openAppPage(getSherlockActivity(), mValues[info.position]);
+ return true;
+ default:
+ break;
+ }
+ return super.onContextItemSelected(item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onItemClick(final AdapterView<?> parent, final View view, final int position,
+ final long id) {
+ mTheme.setThemePackageName(mValues[position]);
+ Crouton.makeText(getSherlockActivity(),
+ mEntries[position] + " " + getString(R.string.theme_set), Crouton.STYLE_CONFIRM)
+ .show();
+ }
+
+ /**
+ * Populates the {@link GridView} with the available themes
+ */
+ private class ThemesAdapter extends ArrayAdapter<ResolveInfo> {
+
+ /**
+ * Number of views (ImageView and TextView)
+ */
+ private static final int VIEW_TYPE_COUNT = 2;
+
+ /**
+ * The resource ID of the layout to inflate
+ */
+ private final int mLayoutID;
+
+ /**
+ * Used to cache the theme info
+ */
+ private DataHolder[] mData;
+
+ /**
+ * Constructor of <code>ThemesAdapter</code>
+ *
+ * @param context The {@link Context} to use.
+ * @param layoutID The resource ID of the view to inflate.
+ */
+ public ThemesAdapter(final Context context, final int layoutID) {
+ super(context, 0);
+ // Get the layout ID
+ mLayoutID = layoutID;
+ // Build the cache
+ buildCache();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getCount() {
+ return mEntries.length;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View getView(final int position, View convertView, final ViewGroup parent) {
+
+ /* Recycle ViewHolder's items */
+ MusicHolder holder;
+ if (convertView == null) {
+ convertView = LayoutInflater.from(getContext()).inflate(mLayoutID, parent, false);
+ holder = new MusicHolder(convertView);
+ convertView.setTag(holder);
+ } else {
+ holder = (MusicHolder)convertView.getTag();
+ }
+
+ // Retrieve the data holder
+ final DataHolder dataHolder = mData[position];
+
+ // Set the theme preview
+ holder.mImage.get().setImageDrawable(dataHolder.mPreview);
+ // Set the theme name
+ holder.mLineOne.get().setText(dataHolder.mName);
+ return convertView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getViewTypeCount() {
+ return VIEW_TYPE_COUNT;
+ }
+
+ /**
+ * Method used to cache the data used to populate the list or grid. The
+ * idea is to cache everything before
+ * {@code #getView(int, View, ViewGroup)} is called.
+ */
+ private void buildCache() {
+ mData = new DataHolder[getCount()];
+ for (int i = 0; i < getCount(); i++) {
+ // Build the data holder
+ mData[i] = new DataHolder();
+ // Theme names (line one)
+ mData[i].mName = mEntries[i];
+ // Theme preview
+ mData[i].mPreview = mThemePreview[i];
+ }
+ }
+
+ }
+
+ /**
+ * @param view The {@link View} used to initialize content
+ */
+ public final static class DataHolder {
+
+ public String mName;
+
+ public Drawable mPreview;
+
+ /**
+ * Constructor of <code>DataHolder</code>
+ */
+ public DataHolder() {
+ super();
+ }
+ }
+}
diff --git a/src/com/andrew/apollo/ui/fragments/phone/MusicBrowserPhoneFragment.java b/src/com/andrew/apollo/ui/fragments/phone/MusicBrowserPhoneFragment.java
new file mode 100644
index 0000000..19c9424
--- /dev/null
+++ b/src/com/andrew/apollo/ui/fragments/phone/MusicBrowserPhoneFragment.java
@@ -0,0 +1,345 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.ui.fragments.phone;
+
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.view.ViewPager;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.actionbarsherlock.app.SherlockFragment;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuInflater;
+import com.actionbarsherlock.view.MenuItem;
+import com.andrew.apollo.R;
+import com.andrew.apollo.adapters.PagerAdapter;
+import com.andrew.apollo.adapters.PagerAdapter.MusicFragments;
+import com.andrew.apollo.ui.fragments.AlbumFragment;
+import com.andrew.apollo.ui.fragments.ArtistFragment;
+import com.andrew.apollo.ui.fragments.SongFragment;
+import com.andrew.apollo.utils.MusicUtils;
+import com.andrew.apollo.utils.NavUtils;
+import com.andrew.apollo.utils.PreferenceUtils;
+import com.andrew.apollo.utils.SortOrder;
+import com.andrew.apollo.utils.ThemeUtils;
+import com.viewpagerindicator.TitlePageIndicator;
+import com.viewpagerindicator.TitlePageIndicator.OnCenterItemClickListener;
+
+/**
+ * This class is used to hold the {@link ViewPager} used for swiping between the
+ * playlists, recent, artists, albums, songs, and genre {@link SherlockFragment}
+ * s for phones.
+ *
+ * @NOTE: The reason the sort orders are taken care of in this fragment rather
+ * than the individual fragments is to keep from showing all of the menu
+ * items on tablet interfaces. That being said, I have a tablet interface
+ * worked out, but I'm going to keep it in the Play Store version of
+ * Apollo for a couple of weeks or so before merging it with CM.
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class MusicBrowserPhoneFragment extends SherlockFragment implements
+ OnCenterItemClickListener {
+
+ /**
+ * Pager
+ */
+ private ViewPager mViewPager;
+
+ /**
+ * VP's adapter
+ */
+ private PagerAdapter mPagerAdapter;
+
+ /**
+ * Theme resources
+ */
+ private ThemeUtils mResources;
+
+ private PreferenceUtils mPreferences;
+
+ /**
+ * Empty constructor as per the {@link Fragment} documentation
+ */
+ public MusicBrowserPhoneFragment() {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Get the preferences
+ mPreferences = PreferenceUtils.getInstace(getSherlockActivity());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
+ final Bundle savedInstanceState) {
+ // The View for the fragment's UI
+ final ViewGroup rootView = (ViewGroup)inflater.inflate(
+ R.layout.fragment_music_browser_phone, container, false);
+
+ // Initialize the adapter
+ mPagerAdapter = new PagerAdapter(getSherlockActivity());
+ final MusicFragments[] mFragments = MusicFragments.values();
+ for (final MusicFragments mFragment : mFragments) {
+ mPagerAdapter.add(mFragment.getFragmentClass(), null);
+ }
+
+ // Initialize the ViewPager
+ mViewPager = (ViewPager)rootView.findViewById(R.id.fragment_home_phone_pager);
+ // Attch the adapter
+ mViewPager.setAdapter(mPagerAdapter);
+ // Offscreen pager loading limit
+ mViewPager.setOffscreenPageLimit(mPagerAdapter.getCount() - 1);
+ // Start on the last page the user was on
+ mViewPager.setCurrentItem(mPreferences.getStartPage());
+
+ // Initialze the TPI
+ final TitlePageIndicator pageIndicator = (TitlePageIndicator)rootView
+ .findViewById(R.id.fragment_home_phone_pager_titles);
+ // Attach the ViewPager
+ pageIndicator.setViewPager(mViewPager);
+ // Scroll to the current artist, album, or song
+ pageIndicator.setOnCenterItemClickListener(this);
+ return rootView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onActivityCreated(final Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ // Initialze the theme resources
+ mResources = new ThemeUtils(getSherlockActivity());
+ // Enable the options menu
+ setHasOptionsMenu(true);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onPause() {
+ super.onPause();
+ // Save the last page the use was on
+ mPreferences.setStartPage(mViewPager.getCurrentItem());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onPrepareOptionsMenu(final Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+ mResources.setFavoriteIcon(menu);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
+ super.onCreateOptionsMenu(menu, inflater);
+ // Favorite action
+ inflater.inflate(R.menu.favorite, menu);
+ // Shuffle all
+ inflater.inflate(R.menu.shuffle, menu);
+ // Sort orders
+ if (isRecentPage()) {
+ inflater.inflate(R.menu.view_as, menu);
+ } else if (isArtistPage()) {
+ inflater.inflate(R.menu.artist_sort_by, menu);
+ inflater.inflate(R.menu.view_as, menu);
+ } else if (isAlbumPage()) {
+ inflater.inflate(R.menu.album_sort_by, menu);
+ inflater.inflate(R.menu.view_as, menu);
+ } else if (isSongPage()) {
+ inflater.inflate(R.menu.song_sort_by, menu);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.menu_shuffle:
+ // Shuffle all the songs
+ MusicUtils.shuffleAll(getSherlockActivity());
+ return true;
+ case R.id.menu_favorite:
+ // Toggle the current track as a favorite and update the menu
+ // item
+ MusicUtils.toggleFavorite();
+ getSherlockActivity().invalidateOptionsMenu();
+ return true;
+ case R.id.menu_sort_by_az:
+ if (isArtistPage()) {
+ mPreferences.setArtistSortOrder(SortOrder.ArtistSortOrder.ARTIST_A_Z);
+ getArtistFragment().refresh();
+ } else if (isAlbumPage()) {
+ mPreferences.setAlbumSortOrder(SortOrder.AlbumSortOrder.ALBUM_A_Z);
+ getAlbumFragment().refresh();
+ } else if (isSongPage()) {
+ mPreferences.setSongSortOrder(SortOrder.SongSortOrder.SONG_A_Z);
+ getSongFragment().refresh();
+ }
+ return true;
+ case R.id.menu_sort_by_za:
+ if (isArtistPage()) {
+ mPreferences.setArtistSortOrder(SortOrder.ArtistSortOrder.ARTIST_Z_A);
+ getArtistFragment().refresh();
+ } else if (isAlbumPage()) {
+ mPreferences.setAlbumSortOrder(SortOrder.AlbumSortOrder.ALBUM_Z_A);
+ getAlbumFragment().refresh();
+ } else if (isSongPage()) {
+ mPreferences.setSongSortOrder(SortOrder.SongSortOrder.SONG_Z_A);
+ getSongFragment().refresh();
+ }
+ return true;
+ case R.id.menu_sort_by_artist:
+ if (isAlbumPage()) {
+ mPreferences.setAlbumSortOrder(SortOrder.AlbumSortOrder.ALBUM_ARTIST);
+ getAlbumFragment().refresh();
+ } else if (isSongPage()) {
+ mPreferences.setSongSortOrder(SortOrder.SongSortOrder.SONG_ARTIST);
+ getSongFragment().refresh();
+ }
+ return true;
+ case R.id.menu_sort_by_album:
+ if (isSongPage()) {
+ mPreferences.setSongSortOrder(SortOrder.SongSortOrder.SONG_ALBUM);
+ getSongFragment().refresh();
+ }
+ return true;
+ case R.id.menu_sort_by_year:
+ if (isAlbumPage()) {
+ mPreferences.setAlbumSortOrder(SortOrder.AlbumSortOrder.ALBUM_YEAR);
+ getAlbumFragment().refresh();
+ } else if (isSongPage()) {
+ mPreferences.setSongSortOrder(SortOrder.SongSortOrder.SONG_YEAR);
+ getSongFragment().refresh();
+ }
+ return true;
+ case R.id.menu_sort_by_duration:
+ if (isSongPage()) {
+ mPreferences.setSongSortOrder(SortOrder.SongSortOrder.SONG_DURATION);
+ getSongFragment().refresh();
+ }
+ return true;
+ case R.id.menu_sort_by_number_of_songs:
+ if (isArtistPage()) {
+ mPreferences
+ .setArtistSortOrder(SortOrder.ArtistSortOrder.ARTIST_NUMBER_OF_SONGS);
+ getArtistFragment().refresh();
+ } else if (isAlbumPage()) {
+ mPreferences.setAlbumSortOrder(SortOrder.AlbumSortOrder.ALBUM_NUMBER_OF_SONGS);
+ getAlbumFragment().refresh();
+ }
+ return true;
+ case R.id.menu_sort_by_number_of_albums:
+ if (isArtistPage()) {
+ mPreferences
+ .setArtistSortOrder(SortOrder.ArtistSortOrder.ARTIST_NUMBER_OF_ALBUMS);
+ getArtistFragment().refresh();
+ }
+ return true;
+ case R.id.menu_view_as_simple:
+ if (isRecentPage()) {
+ mPreferences.setRecentLayout("simple");
+ } else if (isArtistPage()) {
+ mPreferences.setArtistLayout("simple");
+ } else if (isAlbumPage()) {
+ mPreferences.setAlbumLayout("simple");
+ }
+ NavUtils.goHome(getSherlockActivity());
+ return true;
+ case R.id.menu_view_as_detailed:
+ if (isRecentPage()) {
+ mPreferences.setRecentLayout("detailed");
+ } else if (isArtistPage()) {
+ mPreferences.setArtistLayout("detailed");
+ } else if (isAlbumPage()) {
+ mPreferences.setAlbumLayout("detailed");
+ }
+ NavUtils.goHome(getSherlockActivity());
+ return true;
+ case R.id.menu_view_as_grid:
+ if (isRecentPage()) {
+ mPreferences.setRecentLayout("grid");
+ } else if (isArtistPage()) {
+ mPreferences.setArtistLayout("grid");
+ } else if (isAlbumPage()) {
+ mPreferences.setAlbumLayout("grid");
+ }
+ NavUtils.goHome(getSherlockActivity());
+ return true;
+ default:
+ break;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCenterItemClick(final int position) {
+ // If on the artist fragment, scrolls to the current artist
+ if (position == 2) {
+ getArtistFragment().scrollToCurrentArtist();
+ // If on the album fragment, scrolls to the current album
+ } else if (position == 3) {
+ getAlbumFragment().scrollToCurrentAlbum();
+ // If on the song fragment, scrolls to the current song
+ } else if (position == 4) {
+ getSongFragment().scrollToCurrentSong();
+ }
+ }
+
+ private boolean isArtistPage() {
+ return mViewPager.getCurrentItem() == 2;
+ }
+
+ private ArtistFragment getArtistFragment() {
+ return (ArtistFragment)mPagerAdapter.getFragment(2);
+ }
+
+ private boolean isAlbumPage() {
+ return mViewPager.getCurrentItem() == 3;
+ }
+
+ private AlbumFragment getAlbumFragment() {
+ return (AlbumFragment)mPagerAdapter.getFragment(3);
+ }
+
+ private boolean isSongPage() {
+ return mViewPager.getCurrentItem() == 4;
+ }
+
+ private SongFragment getSongFragment() {
+ return (SongFragment)mPagerAdapter.getFragment(4);
+ }
+
+ private boolean isRecentPage() {
+ return mViewPager.getCurrentItem() == 1;
+ }
+}
diff --git a/src/com/andrew/apollo/ui/fragments/profile/AlbumSongFragment.java b/src/com/andrew/apollo/ui/fragments/profile/AlbumSongFragment.java
new file mode 100644
index 0000000..c51a8e9
--- /dev/null
+++ b/src/com/andrew/apollo/ui/fragments/profile/AlbumSongFragment.java
@@ -0,0 +1,331 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.ui.fragments.profile;
+
+import android.app.Activity;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.LoaderManager.LoaderCallbacks;
+import android.support.v4.content.Loader;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.SubMenu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ListView;
+
+import com.actionbarsherlock.app.SherlockFragment;
+import com.andrew.apollo.Config;
+import com.andrew.apollo.R;
+import com.andrew.apollo.adapters.ProfileSongAdapter;
+import com.andrew.apollo.loaders.AlbumSongLoader;
+import com.andrew.apollo.menu.CreateNewPlaylist;
+import com.andrew.apollo.menu.DeleteDialog;
+import com.andrew.apollo.menu.FragmentMenuItems;
+import com.andrew.apollo.model.Song;
+import com.andrew.apollo.provider.FavoritesStore;
+import com.andrew.apollo.recycler.RecycleHolder;
+import com.andrew.apollo.utils.MusicUtils;
+import com.andrew.apollo.widgets.ProfileTabCarousel;
+import com.andrew.apollo.widgets.VerticalScrollListener;
+
+import java.util.List;
+
+/**
+ * This class is used to display all of the songs from a particular album.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class AlbumSongFragment extends SherlockFragment implements LoaderCallbacks<List<Song>>,
+ OnItemClickListener {
+
+ /**
+ * Used to keep context menu items from bleeding into other fragments
+ */
+ private static final int GROUP_ID = 11;
+
+ /**
+ * LoaderCallbacks identifier
+ */
+ private static final int LOADER = 0;
+
+ /**
+ * The adapter for the list
+ */
+ private ProfileSongAdapter mAdapter;
+
+ /**
+ * The list view
+ */
+ private ListView mListView;
+
+ /**
+ * Represents a song
+ */
+ private Song mSong;
+
+ /**
+ * Position of a context menu item
+ */
+ private int mSelectedPosition;
+
+ /**
+ * Id of a context menu item
+ */
+ private long mSelectedId;
+
+ /**
+ * Song, album, and artist name used in the context menu
+ */
+ private String mSongName, mAlbumName, mArtistName;
+
+ /**
+ * Profile header
+ */
+ private ProfileTabCarousel mProfileTabCarousel;
+
+ /**
+ * Empty constructor as per the {@link Fragment} documentation
+ */
+ public AlbumSongFragment() {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onAttach(final Activity activity) {
+ super.onAttach(activity);
+ mProfileTabCarousel = (ProfileTabCarousel)activity
+ .findViewById(R.id.acivity_profile_base_tab_carousel);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Create the adpater
+ mAdapter = new ProfileSongAdapter(getSherlockActivity(), R.layout.list_item_simple, true);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
+ final Bundle savedInstanceState) {
+ // The View for the fragment's UI
+ final ViewGroup rootView = (ViewGroup)inflater.inflate(R.layout.list_base, null);
+ // Initialize the list
+ mListView = (ListView)rootView.findViewById(R.id.list_base);
+ // Set the data behind the list
+ mListView.setAdapter(mAdapter);
+ // Release any references to the recycled Views
+ mListView.setRecyclerListener(new RecycleHolder());
+ // Listen for ContextMenus to be created
+ mListView.setOnCreateContextMenuListener(this);
+ // Play the selected song
+ mListView.setOnItemClickListener(this);
+ // To help make scrolling smooth
+ mListView.setOnScrollListener(new VerticalScrollListener(null, mProfileTabCarousel, 0));
+ // Remove the scrollbars and padding for the fast scroll
+ mListView.setVerticalScrollBarEnabled(false);
+ mListView.setFastScrollEnabled(false);
+ mListView.setPadding(0, 0, 0, 0);
+ return rootView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onActivityCreated(final Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ // Enable the options menu
+ setHasOptionsMenu(true);
+ // Start the loader
+ final Bundle arguments = getArguments();
+ if (arguments != null) {
+ getLoaderManager().initLoader(LOADER, arguments, this);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onSaveInstanceState(final Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putAll(getArguments() != null ? getArguments() : new Bundle());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreateContextMenu(final ContextMenu menu, final View v,
+ final ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ // Get the position of the selected item
+ final AdapterContextMenuInfo info = (AdapterContextMenuInfo)menuInfo;
+ mSelectedPosition = info.position - 1;
+ // Creat a new song
+ mSong = mAdapter.getItem(mSelectedPosition);
+ mSelectedId = Long.valueOf(mSong.mSongId);
+ mSongName = mSong.mSongName;
+ mAlbumName = mSong.mAlbumName;
+ mArtistName = mSong.mArtistName;
+
+ // Play the song
+ menu.add(GROUP_ID, FragmentMenuItems.PLAY_SELECTION, Menu.NONE,
+ getString(R.string.context_menu_play_selection));
+
+ // Add the song to the queue
+ menu.add(GROUP_ID, FragmentMenuItems.ADD_TO_QUEUE, Menu.NONE,
+ getString(R.string.add_to_queue));
+
+ // Add the song to a playlist
+ final SubMenu subMenu = menu.addSubMenu(GROUP_ID, FragmentMenuItems.ADD_TO_PLAYLIST,
+ Menu.NONE, R.string.add_to_playlist);
+ MusicUtils.makePlaylistMenu(getSherlockActivity(), GROUP_ID, subMenu, true);
+
+ // Make the song a ringtone
+ menu.add(GROUP_ID, FragmentMenuItems.USE_AS_RINGTONE, Menu.NONE,
+ getString(R.string.context_menu_use_as_ringtone));
+
+ // Delete the song
+ menu.add(GROUP_ID, FragmentMenuItems.DELETE, Menu.NONE,
+ getString(R.string.context_menu_delete));
+ }
+
+ @Override
+ public boolean onContextItemSelected(final android.view.MenuItem item) {
+ if (item.getGroupId() == GROUP_ID) {
+ switch (item.getItemId()) {
+ case FragmentMenuItems.PLAY_SELECTION:
+ MusicUtils.playAll(getSherlockActivity(), new long[] {
+ mSelectedId
+ }, 0, false);
+ return true;
+ case FragmentMenuItems.ADD_TO_QUEUE:
+ MusicUtils.addToQueue(getSherlockActivity(), new long[] {
+ mSelectedId
+ });
+ return true;
+ case FragmentMenuItems.ADD_TO_FAVORITES:
+ FavoritesStore.getInstance(getSherlockActivity()).addSongId(
+ Long.valueOf(mSelectedId), mSongName, mAlbumName, mArtistName);
+ return true;
+ case FragmentMenuItems.NEW_PLAYLIST:
+ CreateNewPlaylist.getInstance(new long[] {
+ mSelectedId
+ }).show(getFragmentManager(), "CreatePlaylist");
+ return true;
+ case FragmentMenuItems.PLAYLIST_SELECTED:
+ final long mPlaylistId = item.getIntent().getLongExtra("playlist", 0);
+ MusicUtils.addToPlaylist(getSherlockActivity(), new long[] {
+ mSelectedId
+ }, mPlaylistId);
+ return true;
+ case FragmentMenuItems.USE_AS_RINGTONE:
+ MusicUtils.setRingtone(getSherlockActivity(), mSelectedId);
+ return true;
+ case FragmentMenuItems.DELETE:
+ DeleteDialog.newInstance(mSong.mSongName, new long[] {
+ mSelectedId
+ }, null).show(getFragmentManager(), "DeleteDialog");
+ refresh();
+ return true;
+ default:
+ break;
+ }
+ }
+ return super.onContextItemSelected(item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onItemClick(final AdapterView<?> parent, final View view, final int position,
+ final long id) {
+ if (position == 0) {
+ return;
+ }
+ Cursor cursor = AlbumSongLoader.makeAlbumSongCursor(getSherlockActivity(), getArguments()
+ .getLong(Config.ID));
+ final long[] list = MusicUtils.getSongListForCursor(cursor);
+ MusicUtils.playAll(getSherlockActivity(), list, position - 1, false);
+ cursor.close();
+ cursor = null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Loader<List<Song>> onCreateLoader(final int id, final Bundle args) {
+ return new AlbumSongLoader(getSherlockActivity(), args.getLong(Config.ID));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoadFinished(final Loader<List<Song>> loader, final List<Song> data) {
+ // Check for any errors
+ if (data.isEmpty()) {
+ return;
+ }
+
+ // Start fresh
+ mAdapter.unload();
+ // Return the correct count
+ mAdapter.setCount(data);
+ // Add the data to the adpater
+ for (final Song song : data) {
+ mAdapter.add(song);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoaderReset(final Loader<List<Song>> loader) {
+ // Clear the data in the adapter
+ mAdapter.unload();
+ }
+
+ /**
+ * Restarts the loader.
+ */
+ public void refresh() {
+ // Scroll to the stop of the list before restarting the loader.
+ // Otherwise, if the user has scrolled enough to move the header, it
+ // becomes misplaced and needs to be reset.
+ mListView.setSelection(0);
+ // Wait a moment for the preference to change.
+ SystemClock.sleep(10);
+ mAdapter.notifyDataSetChanged();
+ getLoaderManager().restartLoader(LOADER, getArguments(), this);
+ }
+}
diff --git a/src/com/andrew/apollo/ui/fragments/profile/ArtistAlbumFragment.java b/src/com/andrew/apollo/ui/fragments/profile/ArtistAlbumFragment.java
new file mode 100644
index 0000000..d357358
--- /dev/null
+++ b/src/com/andrew/apollo/ui/fragments/profile/ArtistAlbumFragment.java
@@ -0,0 +1,332 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.ui.fragments.profile;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.LoaderManager.LoaderCallbacks;
+import android.support.v4.content.Loader;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.SubMenu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ListView;
+
+import com.actionbarsherlock.app.SherlockFragment;
+import com.andrew.apollo.Config;
+import com.andrew.apollo.R;
+import com.andrew.apollo.adapters.ArtistAlbumAdapter;
+import com.andrew.apollo.loaders.ArtistAlbumLoader;
+import com.andrew.apollo.menu.CreateNewPlaylist;
+import com.andrew.apollo.menu.DeleteDialog;
+import com.andrew.apollo.menu.FragmentMenuItems;
+import com.andrew.apollo.model.Album;
+import com.andrew.apollo.recycler.RecycleHolder;
+import com.andrew.apollo.utils.MusicUtils;
+import com.andrew.apollo.utils.NavUtils;
+import com.andrew.apollo.widgets.ProfileTabCarousel;
+import com.andrew.apollo.widgets.VerticalScrollListener;
+import com.andrew.apollo.widgets.VerticalScrollListener.ScrollableHeader;
+
+import java.util.List;
+
+/**
+ * This class is used to display all of the albums from a particular artist.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class ArtistAlbumFragment extends SherlockFragment implements LoaderCallbacks<List<Album>>,
+ OnItemClickListener {
+
+ /**
+ * Used to keep context menu items from bleeding into other fragments
+ */
+ private static final int GROUP_ID = 10;
+
+ /**
+ * LoaderCallbacks identifier
+ */
+ private static final int LOADER = 0;
+
+ /**
+ * The adapter for the grid
+ */
+ private ArtistAlbumAdapter mAdapter;
+
+ /**
+ * The list view
+ */
+ private ListView mListView;
+
+ /**
+ * Album song list
+ */
+ private long[] mAlbumList;
+
+ /**
+ * Represents an album
+ */
+ private Album mAlbum;
+
+ /**
+ * Profile header
+ */
+ private ProfileTabCarousel mProfileTabCarousel;
+
+ /**
+ * Empty constructor as per the {@link Fragment} documentation
+ */
+ public ArtistAlbumFragment() {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onAttach(final Activity activity) {
+ super.onAttach(activity);
+ mProfileTabCarousel = (ProfileTabCarousel)activity
+ .findViewById(R.id.acivity_profile_base_tab_carousel);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Create the adpater
+ mAdapter = new ArtistAlbumAdapter(getSherlockActivity(),
+ R.layout.list_item_detailed_no_background);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
+ final Bundle savedInstanceState) {
+ // The View for the fragment's UI
+ final ViewGroup rootView = (ViewGroup)inflater.inflate(R.layout.list_base, null);
+ // Initialize the list
+ mListView = (ListView)rootView.findViewById(R.id.list_base);
+ // Set the data behind the grid
+ mListView.setAdapter(mAdapter);
+ // Release any references to the recycled Views
+ mListView.setRecyclerListener(new RecycleHolder());
+ // Listen for ContextMenus to be created
+ mListView.setOnCreateContextMenuListener(this);
+ // Show the songs from the selected album
+ mListView.setOnItemClickListener(this);
+ // To help make scrolling smooth
+ mListView.setOnScrollListener(new VerticalScrollListener(mScrollableHeader,
+ mProfileTabCarousel, 1));
+ // Remove the scrollbars and padding for the fast scroll
+ mListView.setVerticalScrollBarEnabled(false);
+ mListView.setFastScrollEnabled(false);
+ mListView.setPadding(0, 0, 0, 0);
+ return rootView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onActivityCreated(final Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ // Enable the options menu
+ setHasOptionsMenu(true);
+ // Start the loader
+ final Bundle arguments = getArguments();
+ if (arguments != null) {
+ getLoaderManager().initLoader(LOADER, arguments, this);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onPause() {
+ super.onPause();
+ mAdapter.flush();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onSaveInstanceState(final Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putAll(getArguments() != null ? getArguments() : new Bundle());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreateContextMenu(final ContextMenu menu, final View v,
+ final ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+
+ // Get the position of the selected item
+ final AdapterContextMenuInfo info = (AdapterContextMenuInfo)menuInfo;
+ // Create a new album
+ mAlbum = mAdapter.getItem(info.position - 1);
+ // Create a list of the album's songs
+ mAlbumList = MusicUtils.getSongListForAlbum(getSherlockActivity(), mAlbum.mAlbumId);
+
+ // Play the album
+ menu.add(GROUP_ID, FragmentMenuItems.PLAY_SELECTION, Menu.NONE,
+ getString(R.string.context_menu_play_selection));
+
+ // Add the album to the queue
+ menu.add(GROUP_ID, FragmentMenuItems.ADD_TO_QUEUE, Menu.NONE,
+ getString(R.string.add_to_queue));
+
+ // Add the album to a playlist
+ final SubMenu subMenu = menu.addSubMenu(GROUP_ID, FragmentMenuItems.ADD_TO_PLAYLIST,
+ Menu.NONE, R.string.add_to_playlist);
+ MusicUtils.makePlaylistMenu(getSherlockActivity(), GROUP_ID, subMenu, false);
+
+ // Delete the album
+ menu.add(GROUP_ID, FragmentMenuItems.DELETE, Menu.NONE,
+ getString(R.string.context_menu_delete));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onContextItemSelected(final MenuItem item) {
+ // Avoid leaking context menu selections
+ if (item.getGroupId() == GROUP_ID) {
+ switch (item.getItemId()) {
+ case FragmentMenuItems.PLAY_SELECTION:
+ MusicUtils.playAll(getSherlockActivity(), mAlbumList, 0, false);
+ return true;
+ case FragmentMenuItems.ADD_TO_QUEUE:
+ MusicUtils.addToQueue(getSherlockActivity(), mAlbumList);
+ return true;
+ case FragmentMenuItems.NEW_PLAYLIST:
+ CreateNewPlaylist.getInstance(mAlbumList).show(getFragmentManager(),
+ "CreatePlaylist");
+ return true;
+ case FragmentMenuItems.PLAYLIST_SELECTED:
+ final long id = item.getIntent().getLongExtra("playlist", 0);
+ MusicUtils.addToPlaylist(getSherlockActivity(), mAlbumList, id);
+ return true;
+ case FragmentMenuItems.DELETE:
+ DeleteDialog.newInstance(mAlbum.mAlbumName, mAlbumList, null).show(
+ getFragmentManager(), "DeleteDialog");
+ refresh();
+ return true;
+ default:
+ break;
+ }
+ }
+ return super.onContextItemSelected(item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onItemClick(final AdapterView<?> parent, final View view, final int position,
+ final long id) {
+ if (position == 0) {
+ return;
+ }
+ mAlbum = mAdapter.getItem(position - 1);
+ NavUtils.openAlbumProfile(getSherlockActivity(), mAlbum.mAlbumName, mAlbum.mArtistName);
+ getSherlockActivity().finish();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Loader<List<Album>> onCreateLoader(final int id, final Bundle args) {
+ return new ArtistAlbumLoader(getSherlockActivity(), args.getLong(Config.ID));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoadFinished(final Loader<List<Album>> loader, final List<Album> data) {
+ // Check for any errors
+ if (data.isEmpty()) {
+ return;
+ }
+
+ // Start fresh
+ mAdapter.unload();
+ // Return the correct count
+ mAdapter.setCount(data);
+ // Add the data to the adpater
+ for (final Album album : data) {
+ mAdapter.add(album);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoaderReset(final Loader<List<Album>> loader) {
+ // Clear the data in the adapter
+ mAdapter.unload();
+ }
+
+ // Pause disk cache access to ensure smoother scrolling
+ private final ScrollableHeader mScrollableHeader = new ScrollableHeader() {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onScrollStateChanged(final AbsListView view, final int scrollState) {
+ if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_FLING
+ || scrollState == AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
+ mAdapter.setPauseDiskCache(true);
+ } else {
+ mAdapter.setPauseDiskCache(false);
+ mAdapter.notifyDataSetChanged();
+ }
+ }
+ };
+
+ /**
+ * Restarts the loader.
+ */
+ public void refresh() {
+ // Scroll to the stop of the list before restarting the loader.
+ // Otherwise, if the user has scrolled enough to move the header, it
+ // becomes misplaced and needs to be reset.
+ mListView.setSelection(0);
+ // Wait a moment for the preference to change.
+ SystemClock.sleep(10);
+ mAdapter.notifyDataSetChanged();
+ getLoaderManager().restartLoader(LOADER, getArguments(), this);
+ }
+}
diff --git a/src/com/andrew/apollo/ui/fragments/profile/ArtistSongFragment.java b/src/com/andrew/apollo/ui/fragments/profile/ArtistSongFragment.java
new file mode 100644
index 0000000..948043a
--- /dev/null
+++ b/src/com/andrew/apollo/ui/fragments/profile/ArtistSongFragment.java
@@ -0,0 +1,331 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.ui.fragments.profile;
+
+import android.app.Activity;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.LoaderManager.LoaderCallbacks;
+import android.support.v4.content.Loader;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.SubMenu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ListView;
+
+import com.actionbarsherlock.app.SherlockFragment;
+import com.andrew.apollo.Config;
+import com.andrew.apollo.R;
+import com.andrew.apollo.adapters.ProfileSongAdapter;
+import com.andrew.apollo.loaders.ArtistSongLoader;
+import com.andrew.apollo.menu.CreateNewPlaylist;
+import com.andrew.apollo.menu.DeleteDialog;
+import com.andrew.apollo.menu.FragmentMenuItems;
+import com.andrew.apollo.model.Song;
+import com.andrew.apollo.provider.FavoritesStore;
+import com.andrew.apollo.recycler.RecycleHolder;
+import com.andrew.apollo.utils.MusicUtils;
+import com.andrew.apollo.widgets.ProfileTabCarousel;
+import com.andrew.apollo.widgets.VerticalScrollListener;
+
+import java.util.List;
+
+/**
+ * This class is used to display all of the songs from a particular artist.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class ArtistSongFragment extends SherlockFragment implements LoaderCallbacks<List<Song>>,
+ OnItemClickListener {
+
+ /**
+ * Used to keep context menu items from bleeding into other fragments
+ */
+ private static final int GROUP_ID = 9;
+
+ /**
+ * LoaderCallbacks identifier
+ */
+ private static final int LOADER = 0;
+
+ /**
+ * The adapter for the list
+ */
+ private ProfileSongAdapter mAdapter;
+
+ /**
+ * The list view
+ */
+ private ListView mListView;
+
+ /**
+ * Represents a song
+ */
+ private Song mSong;
+
+ /**
+ * Position of a context menu item
+ */
+ private int mSelectedPosition;
+
+ /**
+ * Id of a context menu item
+ */
+ private long mSelectedId;
+
+ /**
+ * Song, album, and artist name used in the context menu
+ */
+ private String mSongName, mAlbumName, mArtistName;
+
+ /**
+ * Profile header
+ */
+ private ProfileTabCarousel mProfileTabCarousel;
+
+ /**
+ * Empty constructor as per the {@link Fragment} documentation
+ */
+ public ArtistSongFragment() {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onAttach(final Activity activity) {
+ super.onAttach(activity);
+ mProfileTabCarousel = (ProfileTabCarousel)activity
+ .findViewById(R.id.acivity_profile_base_tab_carousel);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Create the adpater
+ mAdapter = new ProfileSongAdapter(getSherlockActivity(), R.layout.list_item_simple);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
+ final Bundle savedInstanceState) {
+ // The View for the fragment's UI
+ final ViewGroup rootView = (ViewGroup)inflater.inflate(R.layout.list_base, null);
+ // Initialize the list
+ mListView = (ListView)rootView.findViewById(R.id.list_base);
+ // Set the data behind the list
+ mListView.setAdapter(mAdapter);
+ // Release any references to the recycled Views
+ mListView.setRecyclerListener(new RecycleHolder());
+ // Listen for ContextMenus to be created
+ mListView.setOnCreateContextMenuListener(this);
+ // Play the selected song
+ mListView.setOnItemClickListener(this);
+ // To help make scrolling smooth
+ mListView.setOnScrollListener(new VerticalScrollListener(null, mProfileTabCarousel, 0));
+ // Remove the scrollbars and padding for the fast scroll
+ mListView.setVerticalScrollBarEnabled(false);
+ mListView.setFastScrollEnabled(false);
+ mListView.setPadding(0, 0, 0, 0);
+ return rootView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onActivityCreated(final Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ // Enable the options menu
+ setHasOptionsMenu(true);
+ // Start the loader
+ final Bundle arguments = getArguments();
+ if (arguments != null) {
+ getLoaderManager().initLoader(LOADER, arguments, this);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onSaveInstanceState(final Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putAll(getArguments() != null ? getArguments() : new Bundle());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreateContextMenu(final ContextMenu menu, final View v,
+ final ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ // Get the position of the selected item
+ final AdapterContextMenuInfo info = (AdapterContextMenuInfo)menuInfo;
+ mSelectedPosition = info.position - 1;
+ // Creat a new song
+ mSong = mAdapter.getItem(mSelectedPosition);
+ mSelectedId = Long.valueOf(mSong.mSongId);
+ mSongName = mSong.mSongName;
+ mAlbumName = mSong.mAlbumName;
+ mArtistName = mSong.mArtistName;
+
+ // Play the song
+ menu.add(GROUP_ID, FragmentMenuItems.PLAY_SELECTION, Menu.NONE,
+ getString(R.string.context_menu_play_selection));
+
+ // Add the song to the queue
+ menu.add(GROUP_ID, FragmentMenuItems.ADD_TO_QUEUE, Menu.NONE,
+ getString(R.string.add_to_queue));
+
+ // Add the song to a playlist
+ final SubMenu subMenu = menu.addSubMenu(GROUP_ID, FragmentMenuItems.ADD_TO_PLAYLIST,
+ Menu.NONE, R.string.add_to_playlist);
+ MusicUtils.makePlaylistMenu(getSherlockActivity(), GROUP_ID, subMenu, true);
+
+ // Make the song a ringtone
+ menu.add(GROUP_ID, FragmentMenuItems.USE_AS_RINGTONE, Menu.NONE,
+ getString(R.string.context_menu_use_as_ringtone));
+
+ // Delete the song
+ menu.add(GROUP_ID, FragmentMenuItems.DELETE, Menu.NONE,
+ getString(R.string.context_menu_delete));
+ }
+
+ @Override
+ public boolean onContextItemSelected(final android.view.MenuItem item) {
+ if (item.getGroupId() == GROUP_ID) {
+ switch (item.getItemId()) {
+ case FragmentMenuItems.PLAY_SELECTION:
+ MusicUtils.playAll(getSherlockActivity(), new long[] {
+ mSelectedId
+ }, 0, false);
+ return true;
+ case FragmentMenuItems.ADD_TO_QUEUE:
+ MusicUtils.addToQueue(getSherlockActivity(), new long[] {
+ mSelectedId
+ });
+ return true;
+ case FragmentMenuItems.ADD_TO_FAVORITES:
+ FavoritesStore.getInstance(getSherlockActivity()).addSongId(
+ Long.valueOf(mSelectedId), mSongName, mAlbumName, mArtistName);
+ return true;
+ case FragmentMenuItems.NEW_PLAYLIST:
+ CreateNewPlaylist.getInstance(new long[] {
+ mSelectedId
+ }).show(getFragmentManager(), "CreatePlaylist");
+ return true;
+ case FragmentMenuItems.PLAYLIST_SELECTED:
+ final long mPlaylistId = item.getIntent().getLongExtra("playlist", 0);
+ MusicUtils.addToPlaylist(getSherlockActivity(), new long[] {
+ mSelectedId
+ }, mPlaylistId);
+ return true;
+ case FragmentMenuItems.USE_AS_RINGTONE:
+ MusicUtils.setRingtone(getSherlockActivity(), mSelectedId);
+ return true;
+ case FragmentMenuItems.DELETE:
+ DeleteDialog.newInstance(mSong.mSongName, new long[] {
+ mSelectedId
+ }, null).show(getFragmentManager(), "DeleteDialog");
+ refresh();
+ return true;
+ default:
+ break;
+ }
+ }
+ return super.onContextItemSelected(item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onItemClick(final AdapterView<?> parent, final View view, final int position,
+ final long id) {
+ if (position == 0) {
+ return;
+ }
+ Cursor cursor = ArtistSongLoader.makeArtistSongCursor(getSherlockActivity(), getArguments()
+ .getLong(Config.ID));
+ final long[] list = MusicUtils.getSongListForCursor(cursor);
+ MusicUtils.playAll(getSherlockActivity(), list, position - 1, false);
+ cursor.close();
+ cursor = null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Loader<List<Song>> onCreateLoader(final int id, final Bundle args) {
+ return new ArtistSongLoader(getSherlockActivity(), args.getLong(Config.ID));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoadFinished(final Loader<List<Song>> loader, final List<Song> data) {
+ // Check for any errors
+ if (data.isEmpty()) {
+ return;
+ }
+
+ // Start fresh
+ mAdapter.unload();
+ // Return the correct count
+ mAdapter.setCount(data);
+ // Add the data to the adpater
+ for (final Song song : data) {
+ mAdapter.add(song);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoaderReset(final Loader<List<Song>> loader) {
+ // Clear the data in the adapter
+ mAdapter.unload();
+ }
+
+ /**
+ * Restarts the loader.
+ */
+ public void refresh() {
+ // Scroll to the stop of the list before restarting the loader.
+ // Otherwise, if the user has scrolled enough to move the header, it
+ // becomes misplaced and needs to be reset.
+ mListView.setSelection(0);
+ // Wait a moment for the preference to change.
+ SystemClock.sleep(10);
+ mAdapter.notifyDataSetChanged();
+ getLoaderManager().restartLoader(LOADER, getArguments(), this);
+ }
+}
diff --git a/src/com/andrew/apollo/ui/fragments/profile/FavoriteFragment.java b/src/com/andrew/apollo/ui/fragments/profile/FavoriteFragment.java
new file mode 100644
index 0000000..273c89e
--- /dev/null
+++ b/src/com/andrew/apollo/ui/fragments/profile/FavoriteFragment.java
@@ -0,0 +1,328 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.ui.fragments.profile;
+
+import android.app.Activity;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.LoaderManager.LoaderCallbacks;
+import android.support.v4.content.Loader;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.SubMenu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.actionbarsherlock.app.SherlockFragment;
+import com.andrew.apollo.R;
+import com.andrew.apollo.adapters.ProfileSongAdapter;
+import com.andrew.apollo.loaders.FavoritesLoader;
+import com.andrew.apollo.menu.CreateNewPlaylist;
+import com.andrew.apollo.menu.DeleteDialog;
+import com.andrew.apollo.menu.FragmentMenuItems;
+import com.andrew.apollo.model.Song;
+import com.andrew.apollo.provider.FavoritesStore;
+import com.andrew.apollo.recycler.RecycleHolder;
+import com.andrew.apollo.utils.MusicUtils;
+import com.andrew.apollo.utils.NavUtils;
+import com.andrew.apollo.widgets.ProfileTabCarousel;
+import com.andrew.apollo.widgets.VerticalScrollListener;
+
+import java.util.List;
+
+/**
+ * This class is used to display all of the songs in {@link FavoritesStore
+ * }.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class FavoriteFragment extends SherlockFragment implements LoaderCallbacks<List<Song>>,
+ OnItemClickListener {
+
+ /**
+ * Used to keep context menu items from bleeding into other fragments
+ */
+ private static final int GROUP_ID = 6;
+
+ /**
+ * LoaderCallbacks identifier
+ */
+ private static final int LOADER = 0;
+
+ /**
+ * Fragment UI
+ */
+ private ViewGroup mRootView;
+
+ /**
+ * The adapter for the list
+ */
+ private ProfileSongAdapter mAdapter;
+
+ /**
+ * The list view
+ */
+ private ListView mListView;
+
+ /**
+ * Represents a song
+ */
+ private Song mSong;
+
+ /**
+ * Position of a context menu item
+ */
+ private int mSelectedPosition;
+
+ /**
+ * Id of a context menu item
+ */
+ private long mSelectedId;
+
+ /**
+ * Artist name used in the context menu
+ */
+ private String mArtistName;
+
+ /**
+ * Profile header
+ */
+ private ProfileTabCarousel mProfileTabCarousel;
+
+ /**
+ * Empty constructor as per the {@link Fragment} documentation
+ */
+ public FavoriteFragment() {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onAttach(final Activity activity) {
+ super.onAttach(activity);
+ mProfileTabCarousel = (ProfileTabCarousel)activity
+ .findViewById(R.id.acivity_profile_base_tab_carousel);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Create the adpater
+ mAdapter = new ProfileSongAdapter(getSherlockActivity(), R.layout.list_item_simple);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
+ final Bundle savedInstanceState) {
+ // The View for the fragment's UI
+ mRootView = (ViewGroup)inflater.inflate(R.layout.list_base, null);
+ // Initialize the list
+ mListView = (ListView)mRootView.findViewById(R.id.list_base);
+ // Set the data behind the list
+ mListView.setAdapter(mAdapter);
+ // Release any references to the recycled Views
+ mListView.setRecyclerListener(new RecycleHolder());
+ // Listen for ContextMenus to be created
+ mListView.setOnCreateContextMenuListener(this);
+ // Play the selected song
+ mListView.setOnItemClickListener(this);
+ // To help make scrolling smooth
+ mListView.setOnScrollListener(new VerticalScrollListener(null, mProfileTabCarousel, 0));
+ // Remove the scrollbars and padding for the fast scroll
+ mListView.setVerticalScrollBarEnabled(false);
+ mListView.setFastScrollEnabled(false);
+ mListView.setPadding(0, 0, 0, 0);
+ return mRootView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onActivityCreated(final Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ // Enable the options menu
+ setHasOptionsMenu(true);
+ // Start the loader
+ getLoaderManager().initLoader(LOADER, null, this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreateContextMenu(final ContextMenu menu, final View v,
+ final ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ // Get the position of the selected item
+ final AdapterContextMenuInfo info = (AdapterContextMenuInfo)menuInfo;
+ mSelectedPosition = info.position - 1;
+ // Creat a new song
+ mSong = mAdapter.getItem(mSelectedPosition);
+ mSelectedId = Long.valueOf(mSong.mSongId);
+ mArtistName = mSong.mArtistName;
+
+ // Play the song
+ menu.add(GROUP_ID, FragmentMenuItems.PLAY_SELECTION, Menu.NONE,
+ getString(R.string.context_menu_play_selection));
+
+ // Add the song to the queue
+ menu.add(GROUP_ID, FragmentMenuItems.ADD_TO_QUEUE, Menu.NONE,
+ getString(R.string.add_to_queue));
+
+ // Add the song to a playlist
+ final SubMenu subMenu = menu.addSubMenu(GROUP_ID, FragmentMenuItems.ADD_TO_PLAYLIST,
+ Menu.NONE, R.string.add_to_playlist);
+ MusicUtils.makePlaylistMenu(getSherlockActivity(), GROUP_ID, subMenu, false);
+
+ // View more content by the song artist
+ menu.add(GROUP_ID, FragmentMenuItems.MORE_BY_ARTIST, Menu.NONE,
+ getString(R.string.context_menu_more_by_artist));
+
+ // Make the song a ringtone
+ menu.add(GROUP_ID, FragmentMenuItems.USE_AS_RINGTONE, Menu.NONE,
+ getString(R.string.context_menu_use_as_ringtone));
+
+ // Remove from favorites
+ menu.add(GROUP_ID, FragmentMenuItems.REMOVE_FROM_FAVORITES, Menu.NONE,
+ getString(R.string.remove_from_favorites));
+
+ // Delete the song
+ menu.add(GROUP_ID, FragmentMenuItems.DELETE, Menu.NONE,
+ getString(R.string.context_menu_delete));
+ }
+
+ @Override
+ public boolean onContextItemSelected(final android.view.MenuItem item) {
+ if (item.getGroupId() == GROUP_ID) {
+ switch (item.getItemId()) {
+ case FragmentMenuItems.PLAY_SELECTION:
+ MusicUtils.playAll(getSherlockActivity(), new long[] {
+ mSelectedId
+ }, 0, false);
+ return true;
+ case FragmentMenuItems.ADD_TO_QUEUE:
+ MusicUtils.addToQueue(getSherlockActivity(), new long[] {
+ mSelectedId
+ });
+ return true;
+ case FragmentMenuItems.NEW_PLAYLIST:
+ CreateNewPlaylist.getInstance(new long[] {
+ mSelectedId
+ }).show(getFragmentManager(), "CreatePlaylist");
+ return true;
+ case FragmentMenuItems.PLAYLIST_SELECTED:
+ final long mPlaylistId = item.getIntent().getLongExtra("playlist", 0);
+ MusicUtils.addToPlaylist(getSherlockActivity(), new long[] {
+ mSelectedId
+ }, mPlaylistId);
+ return true;
+ case FragmentMenuItems.MORE_BY_ARTIST:
+ NavUtils.openArtistProfile(getSherlockActivity(), mArtistName);
+ return true;
+ case FragmentMenuItems.USE_AS_RINGTONE:
+ MusicUtils.setRingtone(getSherlockActivity(), mSelectedId);
+ return true;
+ case FragmentMenuItems.REMOVE_FROM_FAVORITES:
+ FavoritesStore.getInstance(getSherlockActivity()).removeItem(
+ Long.valueOf(mSelectedId));
+ getLoaderManager().restartLoader(LOADER, null, this);
+ return true;
+ case FragmentMenuItems.DELETE:
+ DeleteDialog.newInstance(mSong.mSongName, new long[] {
+ mSelectedId
+ }, null).show(getFragmentManager(), "DeleteDialog");
+ SystemClock.sleep(10);
+ mAdapter.notifyDataSetChanged();
+ getLoaderManager().restartLoader(LOADER, null, this);
+ return true;
+ default:
+ break;
+ }
+ }
+ return super.onContextItemSelected(item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onItemClick(final AdapterView<?> parent, final View view, final int position,
+ final long id) {
+ if (position == 0) {
+ return;
+ }
+ Cursor cursor = FavoritesLoader.makeFavoritesCursor(getSherlockActivity());
+ final long[] list = MusicUtils.getSongListForCursor(cursor);
+ MusicUtils.playAll(getSherlockActivity(), list, position - 1, false);
+ cursor.close();
+ cursor = null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Loader<List<Song>> onCreateLoader(final int id, final Bundle args) {
+ return new FavoritesLoader(getSherlockActivity());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoadFinished(final Loader<List<Song>> loader, final List<Song> data) {
+ // Check for any errors
+ if (data.isEmpty()) {
+ // Set the empty text
+ final TextView empty = (TextView)mRootView.findViewById(R.id.empty);
+ empty.setText(getString(R.string.empty_favorite));
+ mListView.setEmptyView(empty);
+ return;
+ }
+
+ // Start fresh
+ mAdapter.unload();
+ // Return the correct count
+ mAdapter.setCount(data);
+ // Add the data to the adpater
+ for (final Song song : data) {
+ mAdapter.add(song);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoaderReset(final Loader<List<Song>> loader) {
+ // Clear the data in the adapter
+ mAdapter.unload();
+ }
+
+}
diff --git a/src/com/andrew/apollo/ui/fragments/profile/GenreSongFragment.java b/src/com/andrew/apollo/ui/fragments/profile/GenreSongFragment.java
new file mode 100644
index 0000000..e431189
--- /dev/null
+++ b/src/com/andrew/apollo/ui/fragments/profile/GenreSongFragment.java
@@ -0,0 +1,338 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.ui.fragments.profile;
+
+import android.app.Activity;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.LoaderManager.LoaderCallbacks;
+import android.support.v4.content.Loader;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.SubMenu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ListView;
+
+import com.actionbarsherlock.app.SherlockFragment;
+import com.andrew.apollo.Config;
+import com.andrew.apollo.R;
+import com.andrew.apollo.adapters.ProfileSongAdapter;
+import com.andrew.apollo.loaders.GenreSongLoader;
+import com.andrew.apollo.menu.CreateNewPlaylist;
+import com.andrew.apollo.menu.DeleteDialog;
+import com.andrew.apollo.menu.FragmentMenuItems;
+import com.andrew.apollo.model.Song;
+import com.andrew.apollo.provider.FavoritesStore;
+import com.andrew.apollo.recycler.RecycleHolder;
+import com.andrew.apollo.utils.MusicUtils;
+import com.andrew.apollo.utils.NavUtils;
+import com.andrew.apollo.widgets.ProfileTabCarousel;
+import com.andrew.apollo.widgets.VerticalScrollListener;
+
+import java.util.List;
+
+/**
+ * This class is used to display all of the songs from a particular playlist.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class GenreSongFragment extends SherlockFragment implements LoaderCallbacks<List<Song>>,
+ OnItemClickListener {
+
+ /**
+ * Used to keep context menu items from bleeding into other fragments
+ */
+ private static final int GROUP_ID = 12;
+
+ /**
+ * LoaderCallbacks identifier
+ */
+ private static final int LOADER = 0;
+
+ /**
+ * The adapter for the list
+ */
+ private ProfileSongAdapter mAdapter;
+
+ /**
+ * The list view
+ */
+ private ListView mListView;
+
+ /**
+ * Represents a song
+ */
+ private Song mSong;
+
+ /**
+ * Position of a context menu item
+ */
+ private int mSelectedPosition;
+
+ /**
+ * Id of a context menu item
+ */
+ private long mSelectedId;
+
+ /**
+ * Song, album, and artist name used in the context menu
+ */
+ private String mSongName, mAlbumName, mArtistName;
+
+ /**
+ * Profile header
+ */
+ private ProfileTabCarousel mProfileTabCarousel;
+
+ /**
+ * Empty constructor as per the {@link Fragment} documentation
+ */
+ public GenreSongFragment() {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onAttach(final Activity activity) {
+ super.onAttach(activity);
+ mProfileTabCarousel = (ProfileTabCarousel)activity
+ .findViewById(R.id.acivity_profile_base_tab_carousel);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Create the adpater
+ mAdapter = new ProfileSongAdapter(getSherlockActivity(), R.layout.list_item_simple);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
+ final Bundle savedInstanceState) {
+ // The View for the fragment's UI
+ final ViewGroup rootView = (ViewGroup)inflater.inflate(R.layout.list_base, null);
+ // Initialize the list
+ mListView = (ListView)rootView.findViewById(R.id.list_base);
+ // Set the data behind the list
+ mListView.setAdapter(mAdapter);
+ // Release any references to the recycled Views
+ mListView.setRecyclerListener(new RecycleHolder());
+ // Listen for ContextMenus to be created
+ mListView.setOnCreateContextMenuListener(this);
+ // Play the selected song
+ mListView.setOnItemClickListener(this);
+ // To help make scrolling smooth
+ mListView.setOnScrollListener(new VerticalScrollListener(null, mProfileTabCarousel, 0));
+ // Remove the scrollbars and padding for the fast scroll
+ mListView.setVerticalScrollBarEnabled(false);
+ mListView.setFastScrollEnabled(false);
+ mListView.setPadding(0, 0, 0, 0);
+ return rootView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onActivityCreated(final Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ // Enable the options menu
+ setHasOptionsMenu(true);
+ // Start the loader
+ final Bundle arguments = getArguments();
+ if (arguments != null) {
+ getLoaderManager().initLoader(LOADER, arguments, this);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onSaveInstanceState(final Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putAll(getArguments() != null ? getArguments() : new Bundle());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreateContextMenu(final ContextMenu menu, final View v,
+ final ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ // Get the position of the selected item
+ final AdapterContextMenuInfo info = (AdapterContextMenuInfo)menuInfo;
+ mSelectedPosition = info.position - 1;
+ // Creat a new song
+ mSong = mAdapter.getItem(mSelectedPosition);
+ mSelectedId = Long.valueOf(mSong.mSongId);
+ mSongName = mSong.mSongName;
+ mAlbumName = mSong.mAlbumName;
+ mArtistName = mSong.mArtistName;
+
+ // Play the song
+ menu.add(GROUP_ID, FragmentMenuItems.PLAY_SELECTION, Menu.NONE,
+ getString(R.string.context_menu_play_selection));
+
+ // Add the song to the queue
+ menu.add(GROUP_ID, FragmentMenuItems.ADD_TO_QUEUE, Menu.NONE,
+ getString(R.string.add_to_queue));
+
+ // Add the song to a playlist
+ final SubMenu subMenu = menu.addSubMenu(GROUP_ID, FragmentMenuItems.ADD_TO_PLAYLIST,
+ Menu.NONE, R.string.add_to_playlist);
+ MusicUtils.makePlaylistMenu(getSherlockActivity(), GROUP_ID, subMenu, true);
+
+ // View more content by the song artist
+ menu.add(GROUP_ID, FragmentMenuItems.MORE_BY_ARTIST, Menu.NONE,
+ getString(R.string.context_menu_more_by_artist));
+
+ // Make the song a ringtone
+ menu.add(GROUP_ID, FragmentMenuItems.USE_AS_RINGTONE, Menu.NONE,
+ getString(R.string.context_menu_use_as_ringtone));
+
+ // Delete the song
+ menu.add(GROUP_ID, FragmentMenuItems.DELETE, Menu.NONE,
+ getString(R.string.context_menu_delete));
+ }
+
+ @Override
+ public boolean onContextItemSelected(final android.view.MenuItem item) {
+ if (item.getGroupId() == GROUP_ID) {
+ switch (item.getItemId()) {
+ case FragmentMenuItems.PLAY_SELECTION:
+ MusicUtils.playAll(getSherlockActivity(), new long[] {
+ mSelectedId
+ }, 0, false);
+ return true;
+ case FragmentMenuItems.ADD_TO_QUEUE:
+ MusicUtils.addToQueue(getSherlockActivity(), new long[] {
+ mSelectedId
+ });
+ return true;
+ case FragmentMenuItems.ADD_TO_FAVORITES:
+ FavoritesStore.getInstance(getSherlockActivity()).addSongId(
+ Long.valueOf(mSelectedId), mSongName, mAlbumName, mArtistName);
+ return true;
+ case FragmentMenuItems.NEW_PLAYLIST:
+ CreateNewPlaylist.getInstance(new long[] {
+ mSelectedId
+ }).show(getFragmentManager(), "CreatePlaylist");
+ return true;
+ case FragmentMenuItems.PLAYLIST_SELECTED:
+ final long mPlaylistId = item.getIntent().getLongExtra("playlist", 0);
+ MusicUtils.addToPlaylist(getSherlockActivity(), new long[] {
+ mSelectedId
+ }, mPlaylistId);
+ return true;
+ case FragmentMenuItems.MORE_BY_ARTIST:
+ NavUtils.openArtistProfile(getSherlockActivity(), mArtistName);
+ return true;
+ case FragmentMenuItems.USE_AS_RINGTONE:
+ MusicUtils.setRingtone(getSherlockActivity(), mSelectedId);
+ return true;
+ case FragmentMenuItems.DELETE:
+ DeleteDialog.newInstance(mSong.mSongName, new long[] {
+ mSelectedId
+ }, null).show(getFragmentManager(), "DeleteDialog");
+ refresh();
+ return true;
+ default:
+ break;
+ }
+ }
+ return super.onContextItemSelected(item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onItemClick(final AdapterView<?> parent, final View view, final int position,
+ final long id) {
+ if (position == 0) {
+ return;
+ }
+ Cursor cursor = GenreSongLoader.makeGenreSongCursor(getSherlockActivity(), getArguments()
+ .getLong(Config.ID));
+ final long[] list = MusicUtils.getSongListForCursor(cursor);
+ MusicUtils.playAll(getSherlockActivity(), list, position - 1, false);
+ cursor.close();
+ cursor = null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Loader<List<Song>> onCreateLoader(final int id, final Bundle args) {
+ return new GenreSongLoader(getSherlockActivity(), args.getLong(Config.ID));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoadFinished(final Loader<List<Song>> loader, final List<Song> data) {
+ // Check for any errors
+ if (data.isEmpty()) {
+ return;
+ }
+
+ // Start fresh
+ mAdapter.unload();
+ // Return the correct count
+ mAdapter.setCount(data);
+ // Add the data to the adpater
+ for (final Song song : data) {
+ mAdapter.add(song);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoaderReset(final Loader<List<Song>> loader) {
+ // Clear the data in the adapter
+ mAdapter.unload();
+ }
+
+ /**
+ * Restarts the loader.
+ */
+ public void refresh() {
+ // Scroll to the stop of the list before restarting the loader.
+ // Otherwise, if the user has scrolled enough to move the header, it
+ // becomes misplaced and needs to be reset.
+ mListView.setSelection(0);
+ // Wait a moment for the preference to change.
+ SystemClock.sleep(10);
+ getLoaderManager().restartLoader(LOADER, getArguments(), this);
+ }
+}
diff --git a/src/com/andrew/apollo/ui/fragments/profile/LastAddedFragment.java b/src/com/andrew/apollo/ui/fragments/profile/LastAddedFragment.java
new file mode 100644
index 0000000..9fafeb8
--- /dev/null
+++ b/src/com/andrew/apollo/ui/fragments/profile/LastAddedFragment.java
@@ -0,0 +1,325 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.ui.fragments.profile;
+
+import android.app.Activity;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.LoaderManager.LoaderCallbacks;
+import android.support.v4.content.Loader;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.SubMenu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.actionbarsherlock.app.SherlockFragment;
+import com.andrew.apollo.R;
+import com.andrew.apollo.adapters.ProfileSongAdapter;
+import com.andrew.apollo.loaders.LastAddedLoader;
+import com.andrew.apollo.menu.CreateNewPlaylist;
+import com.andrew.apollo.menu.DeleteDialog;
+import com.andrew.apollo.menu.FragmentMenuItems;
+import com.andrew.apollo.model.Song;
+import com.andrew.apollo.provider.FavoritesStore;
+import com.andrew.apollo.recycler.RecycleHolder;
+import com.andrew.apollo.utils.MusicUtils;
+import com.andrew.apollo.utils.NavUtils;
+import com.andrew.apollo.widgets.ProfileTabCarousel;
+import com.andrew.apollo.widgets.VerticalScrollListener;
+
+import java.util.List;
+
+/**
+ * This class is used to display all of the songs the user put on their device
+ * within the last four weeks.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class LastAddedFragment extends SherlockFragment implements LoaderCallbacks<List<Song>>,
+ OnItemClickListener {
+
+ /**
+ * Used to keep context menu items from bleeding into other fragments
+ */
+ private static final int GROUP_ID = 7;
+
+ /**
+ * LoaderCallbacks identifier
+ */
+ private static final int LOADER = 0;
+
+ /**
+ * Fragment UI
+ */
+ private ViewGroup mRootView;
+
+ /**
+ * The adapter for the list
+ */
+ private ProfileSongAdapter mAdapter;
+
+ /**
+ * The list view
+ */
+ private ListView mListView;
+
+ /**
+ * Represents a song
+ */
+ private Song mSong;
+
+ /**
+ * Position of a context menu item
+ */
+ private int mSelectedPosition;
+
+ /**
+ * Id of a context menu item
+ */
+ private long mSelectedId;
+
+ /**
+ * Song, album, and artist name used in the context menu
+ */
+ private String mSongName, mAlbumName, mArtistName;
+
+ /**
+ * Profile header
+ */
+ private ProfileTabCarousel mProfileTabCarousel;
+
+ /**
+ * Empty constructor as per the {@link Fragment} documentation
+ */
+ public LastAddedFragment() {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onAttach(final Activity activity) {
+ super.onAttach(activity);
+ mProfileTabCarousel = (ProfileTabCarousel)activity
+ .findViewById(R.id.acivity_profile_base_tab_carousel);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Create the adpater
+ mAdapter = new ProfileSongAdapter(getSherlockActivity(), R.layout.list_item_simple);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
+ final Bundle savedInstanceState) {
+ // The View for the fragment's UI
+ mRootView = (ViewGroup)inflater.inflate(R.layout.list_base, null);
+ // Initialize the list
+ mListView = (ListView)mRootView.findViewById(R.id.list_base);
+ // Set the data behind the list
+ mListView.setAdapter(mAdapter);
+ // Release any references to the recycled Views
+ mListView.setRecyclerListener(new RecycleHolder());
+ // Listen for ContextMenus to be created
+ mListView.setOnCreateContextMenuListener(this);
+ // Play the selected song
+ mListView.setOnItemClickListener(this);
+ // To help make scrolling smooth
+ mListView.setOnScrollListener(new VerticalScrollListener(null, mProfileTabCarousel, 0));
+ // Remove the scrollbars and padding for the fast scroll
+ mListView.setVerticalScrollBarEnabled(false);
+ mListView.setFastScrollEnabled(false);
+ mListView.setPadding(0, 0, 0, 0);
+ return mRootView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onActivityCreated(final Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ // Enable the options menu
+ setHasOptionsMenu(true);
+ // Start the loader
+ getLoaderManager().initLoader(LOADER, null, this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreateContextMenu(final ContextMenu menu, final View v,
+ final ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ // Get the position of the selected item
+ final AdapterContextMenuInfo info = (AdapterContextMenuInfo)menuInfo;
+ mSelectedPosition = info.position - 1;
+ // Creat a new song
+ mSong = mAdapter.getItem(mSelectedPosition);
+ mSelectedId = Long.valueOf(mSong.mSongId);
+ mSongName = mSong.mSongName;
+ mAlbumName = mSong.mAlbumName;
+ mArtistName = mSong.mArtistName;
+
+ // Play the song
+ menu.add(GROUP_ID, FragmentMenuItems.PLAY_SELECTION, Menu.NONE,
+ getString(R.string.context_menu_play_selection));
+
+ // Add the song to the queue
+ menu.add(GROUP_ID, FragmentMenuItems.ADD_TO_QUEUE, Menu.NONE,
+ getString(R.string.add_to_queue));
+
+ // Add the song to a playlist
+ final SubMenu subMenu = menu.addSubMenu(GROUP_ID, FragmentMenuItems.ADD_TO_PLAYLIST,
+ Menu.NONE, R.string.add_to_playlist);
+ MusicUtils.makePlaylistMenu(getSherlockActivity(), GROUP_ID, subMenu, false);
+
+ // View more content by the song artist
+ menu.add(GROUP_ID, FragmentMenuItems.MORE_BY_ARTIST, Menu.NONE,
+ getString(R.string.context_menu_more_by_artist));
+
+ // Make the song a ringtone
+ menu.add(GROUP_ID, FragmentMenuItems.USE_AS_RINGTONE, Menu.NONE,
+ getString(R.string.context_menu_use_as_ringtone));
+
+ // Delete the song
+ menu.add(GROUP_ID, FragmentMenuItems.DELETE, Menu.NONE,
+ getString(R.string.context_menu_delete));
+ }
+
+ @Override
+ public boolean onContextItemSelected(final android.view.MenuItem item) {
+ if (item.getGroupId() == GROUP_ID) {
+ switch (item.getItemId()) {
+ case FragmentMenuItems.PLAY_SELECTION:
+ MusicUtils.playAll(getSherlockActivity(), new long[] {
+ mSelectedId
+ }, 0, false);
+ return true;
+ case FragmentMenuItems.ADD_TO_QUEUE:
+ MusicUtils.addToQueue(getSherlockActivity(), new long[] {
+ mSelectedId
+ });
+ return true;
+ case FragmentMenuItems.ADD_TO_FAVORITES:
+ FavoritesStore.getInstance(getSherlockActivity()).addSongId(
+ Long.valueOf(mSelectedId), mSongName, mAlbumName, mArtistName);
+ return true;
+ case FragmentMenuItems.NEW_PLAYLIST:
+ CreateNewPlaylist.getInstance(new long[] {
+ mSelectedId
+ }).show(getFragmentManager(), "CreatePlaylist");
+ return true;
+ case FragmentMenuItems.PLAYLIST_SELECTED:
+ final long mPlaylistId = item.getIntent().getLongExtra("playlist", 0);
+ MusicUtils.addToPlaylist(getSherlockActivity(), new long[] {
+ mSelectedId
+ }, mPlaylistId);
+ return true;
+ case FragmentMenuItems.MORE_BY_ARTIST:
+ NavUtils.openArtistProfile(getSherlockActivity(), mArtistName);
+ return true;
+ case FragmentMenuItems.USE_AS_RINGTONE:
+ MusicUtils.setRingtone(getSherlockActivity(), mSelectedId);
+ return true;
+ case FragmentMenuItems.DELETE:
+ DeleteDialog.newInstance(mSong.mSongName, new long[] {
+ mSelectedId
+ }, null).show(getFragmentManager(), "DeleteDialog");
+ SystemClock.sleep(10);
+ mAdapter.notifyDataSetChanged();
+ getLoaderManager().restartLoader(LOADER, null, this);
+ return true;
+ default:
+ break;
+ }
+ }
+ return super.onContextItemSelected(item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onItemClick(final AdapterView<?> parent, final View view, final int position,
+ final long id) {
+ if (position == 0) {
+ return;
+ }
+ Cursor cursor = LastAddedLoader.makeLastAddedCursor(getSherlockActivity());
+ final long[] list = MusicUtils.getSongListForCursor(cursor);
+ MusicUtils.playAll(getSherlockActivity(), list, position - 1, false);
+ cursor.close();
+ cursor = null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Loader<List<Song>> onCreateLoader(final int id, final Bundle args) {
+ return new LastAddedLoader(getSherlockActivity());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoadFinished(final Loader<List<Song>> loader, final List<Song> data) {
+ // Check for any errors
+ if (data.isEmpty()) {
+ // Set the empty text
+ final TextView empty = (TextView)mRootView.findViewById(R.id.empty);
+ empty.setText(getString(R.string.empty_last_added));
+ mListView.setEmptyView(empty);
+ return;
+ }
+
+ // Start fresh
+ mAdapter.unload();
+ // Return the correct count
+ mAdapter.setCount(data);
+ // Add the data to the adpater
+ for (final Song song : data) {
+ mAdapter.add(song);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoaderReset(final Loader<List<Song>> loader) {
+ // Clear the data in the adapter
+ mAdapter.unload();
+ }
+
+}
diff --git a/src/com/andrew/apollo/ui/fragments/profile/PlaylistSongFragment.java b/src/com/andrew/apollo/ui/fragments/profile/PlaylistSongFragment.java
new file mode 100644
index 0000000..b838a79
--- /dev/null
+++ b/src/com/andrew/apollo/ui/fragments/profile/PlaylistSongFragment.java
@@ -0,0 +1,385 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.ui.fragments.profile;
+
+import android.app.Activity;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.provider.MediaStore;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.LoaderManager.LoaderCallbacks;
+import android.support.v4.content.Loader;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.SubMenu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+
+import com.actionbarsherlock.app.SherlockFragment;
+import com.andrew.apollo.Config;
+import com.andrew.apollo.R;
+import com.andrew.apollo.adapters.ProfileSongAdapter;
+import com.andrew.apollo.dragdrop.DragSortListView;
+import com.andrew.apollo.dragdrop.DragSortListView.DragScrollProfile;
+import com.andrew.apollo.dragdrop.DragSortListView.DropListener;
+import com.andrew.apollo.dragdrop.DragSortListView.RemoveListener;
+import com.andrew.apollo.loaders.PlaylistSongLoader;
+import com.andrew.apollo.menu.CreateNewPlaylist;
+import com.andrew.apollo.menu.DeleteDialog;
+import com.andrew.apollo.menu.FragmentMenuItems;
+import com.andrew.apollo.model.Song;
+import com.andrew.apollo.provider.FavoritesStore;
+import com.andrew.apollo.recycler.RecycleHolder;
+import com.andrew.apollo.utils.MusicUtils;
+import com.andrew.apollo.utils.NavUtils;
+import com.andrew.apollo.widgets.ProfileTabCarousel;
+import com.andrew.apollo.widgets.VerticalScrollListener;
+
+import java.util.List;
+
+/**
+ * This class is used to display all of the songs from a particular playlist.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class PlaylistSongFragment extends SherlockFragment implements LoaderCallbacks<List<Song>>,
+ OnItemClickListener, DropListener, RemoveListener, DragScrollProfile {
+
+ /**
+ * Used to keep context menu items from bleeding into other fragments
+ */
+ private static final int GROUP_ID = 8;
+
+ /**
+ * LoaderCallbacks identifier
+ */
+ private static final int LOADER = 0;
+
+ /**
+ * The adapter for the list
+ */
+ private ProfileSongAdapter mAdapter;
+
+ /**
+ * The list view
+ */
+ private DragSortListView mListView;
+
+ /**
+ * Represents a song
+ */
+ private Song mSong;
+
+ /**
+ * Position of a context menu item
+ */
+ private int mSelectedPosition;
+
+ /**
+ * Id of a context menu item
+ */
+ private long mSelectedId;
+
+ /**
+ * Song, album, and artist name used in the context menu
+ */
+ private String mSongName, mAlbumName, mArtistName;
+
+ /**
+ * Profile header
+ */
+ private ProfileTabCarousel mProfileTabCarousel;
+
+ /**
+ * The Id of the playlist the songs belong to
+ */
+ private long mPlaylistId;
+
+ /**
+ * Empty constructor as per the {@link Fragment} documentation
+ */
+ public PlaylistSongFragment() {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onAttach(final Activity activity) {
+ super.onAttach(activity);
+ mProfileTabCarousel = (ProfileTabCarousel)activity
+ .findViewById(R.id.acivity_profile_base_tab_carousel);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Create the adpater
+ mAdapter = new ProfileSongAdapter(getSherlockActivity(), R.layout.edit_track_list_item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
+ final Bundle savedInstanceState) {
+ // The View for the fragment's UI
+ final ViewGroup rootView = (ViewGroup)inflater.inflate(R.layout.list_base, null);
+ // Initialize the list
+ mListView = (DragSortListView)rootView.findViewById(R.id.list_base);
+ // Set the data behind the list
+ mListView.setAdapter(mAdapter);
+ // Release any references to the recycled Views
+ mListView.setRecyclerListener(new RecycleHolder());
+ // Listen for ContextMenus to be created
+ mListView.setOnCreateContextMenuListener(this);
+ // Play the selected song
+ mListView.setOnItemClickListener(this);
+ // Set the drop listener
+ mListView.setDropListener(this);
+ // Set the swipe to remove listener
+ mListView.setRemoveListener(this);
+ // Quick scroll while dragging
+ mListView.setDragScrollProfile(this);
+ // To help make scrolling smooth
+ mListView.setOnScrollListener(new VerticalScrollListener(null, mProfileTabCarousel, 0));
+ // Remove the scrollbars and padding for the fast scroll
+ mListView.setVerticalScrollBarEnabled(false);
+ mListView.setFastScrollEnabled(false);
+ mListView.setPadding(0, 0, 0, 0);
+ return rootView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onActivityCreated(final Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ // Enable the options menu
+ setHasOptionsMenu(true);
+ // Start the loader
+ final Bundle arguments = getArguments();
+ if (arguments != null) {
+ mPlaylistId = arguments.getLong(Config.ID);
+ getLoaderManager().initLoader(LOADER, arguments, this);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onSaveInstanceState(final Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putAll(getArguments() != null ? getArguments() : new Bundle());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreateContextMenu(final ContextMenu menu, final View v,
+ final ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ // Get the position of the selected item
+ final AdapterContextMenuInfo info = (AdapterContextMenuInfo)menuInfo;
+ mSelectedPosition = info.position - 1;
+ // Creat a new song
+ mSong = mAdapter.getItem(mSelectedPosition);
+ mSelectedId = Long.valueOf(mSong.mSongId);
+ mSongName = mSong.mSongName;
+ mAlbumName = mSong.mAlbumName;
+ mArtistName = mSong.mArtistName;
+
+ // Play the song
+ menu.add(GROUP_ID, FragmentMenuItems.PLAY_SELECTION, Menu.NONE,
+ getString(R.string.context_menu_play_selection));
+
+ // Add the song to the queue
+ menu.add(GROUP_ID, FragmentMenuItems.ADD_TO_QUEUE, Menu.NONE,
+ getString(R.string.add_to_queue));
+
+ // Add the song to a playlist
+ final SubMenu subMenu = menu.addSubMenu(GROUP_ID, FragmentMenuItems.ADD_TO_PLAYLIST,
+ Menu.NONE, R.string.add_to_playlist);
+ MusicUtils.makePlaylistMenu(getSherlockActivity(), GROUP_ID, subMenu, true);
+
+ // View more content by the song artist
+ menu.add(GROUP_ID, FragmentMenuItems.MORE_BY_ARTIST, Menu.NONE,
+ getString(R.string.context_menu_more_by_artist));
+
+ // Make the song a ringtone
+ menu.add(GROUP_ID, FragmentMenuItems.USE_AS_RINGTONE, Menu.NONE,
+ getString(R.string.context_menu_use_as_ringtone));
+
+ // Delete the song
+ menu.add(GROUP_ID, FragmentMenuItems.DELETE, Menu.NONE,
+ getString(R.string.context_menu_delete));
+ }
+
+ @Override
+ public boolean onContextItemSelected(final android.view.MenuItem item) {
+ if (item.getGroupId() == GROUP_ID) {
+ switch (item.getItemId()) {
+ case FragmentMenuItems.PLAY_SELECTION:
+ MusicUtils.playAll(getSherlockActivity(), new long[] {
+ mSelectedId
+ }, 0, false);
+ return true;
+ case FragmentMenuItems.ADD_TO_QUEUE:
+ MusicUtils.addToQueue(getSherlockActivity(), new long[] {
+ mSelectedId
+ });
+ return true;
+ case FragmentMenuItems.ADD_TO_FAVORITES:
+ FavoritesStore.getInstance(getSherlockActivity()).addSongId(
+ Long.valueOf(mSelectedId), mSongName, mAlbumName, mArtistName);
+ return true;
+ case FragmentMenuItems.NEW_PLAYLIST:
+ CreateNewPlaylist.getInstance(new long[] {
+ mSelectedId
+ }).show(getFragmentManager(), "CreatePlaylist");
+ return true;
+ case FragmentMenuItems.PLAYLIST_SELECTED:
+ final long mPlaylistId = item.getIntent().getLongExtra("playlist", 0);
+ MusicUtils.addToPlaylist(getSherlockActivity(), new long[] {
+ mSelectedId
+ }, mPlaylistId);
+ return true;
+ case FragmentMenuItems.MORE_BY_ARTIST:
+ NavUtils.openArtistProfile(getSherlockActivity(), mArtistName);
+ return true;
+ case FragmentMenuItems.USE_AS_RINGTONE:
+ MusicUtils.setRingtone(getSherlockActivity(), mSelectedId);
+ return true;
+ case FragmentMenuItems.DELETE:
+ DeleteDialog.newInstance(mSong.mSongName, new long[] {
+ mSelectedId
+ }, null).show(getFragmentManager(), "DeleteDialog");
+ SystemClock.sleep(10);
+ mAdapter.notifyDataSetChanged();
+ getLoaderManager().restartLoader(LOADER, null, this);
+ return true;
+ default:
+ break;
+ }
+ }
+ return super.onContextItemSelected(item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onItemClick(final AdapterView<?> parent, final View view, final int position,
+ final long id) {
+ if (position == 0) {
+ return;
+ }
+ Cursor cursor = PlaylistSongLoader.makePlaylistSongCursor(getSherlockActivity(),
+ getArguments().getLong(Config.ID));
+ final long[] list = MusicUtils.getSongListForCursor(cursor);
+ MusicUtils.playAll(getSherlockActivity(), list, position - 1, false);
+ cursor.close();
+ cursor = null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Loader<List<Song>> onCreateLoader(final int id, final Bundle args) {
+ return new PlaylistSongLoader(getSherlockActivity(), mPlaylistId);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoadFinished(final Loader<List<Song>> loader, final List<Song> data) {
+ // Check for any errors
+ if (data.isEmpty()) {
+ return;
+ }
+
+ // Start fresh
+ mAdapter.unload();
+ // Return the correct count
+ mAdapter.setCount(data);
+ // Add the data to the adpater
+ for (final Song song : data) {
+ mAdapter.add(song);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLoaderReset(final Loader<List<Song>> loader) {
+ // Clear the data in the adapter
+ mAdapter.unload();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public float getSpeed(final float w, final long t) {
+ if (w > 0.8f) {
+ return mAdapter.getCount() / 0.001f;
+ } else {
+ return 10.0f * w;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void remove(final int which) {
+ mSong = mAdapter.getItem(which - 1);
+ mAdapter.remove(mSong);
+ mAdapter.notifyDataSetChanged();
+ final Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", mPlaylistId);
+ getSherlockActivity().getContentResolver().delete(uri,
+ MediaStore.Audio.Playlists.Members.AUDIO_ID + "=" + Long.valueOf(mSong.mSongId),
+ null);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void drop(final int from, final int to) {
+ final int realFrom = from - 1;
+ final int realTo = to - 1;
+ mSong = mAdapter.getItem(realFrom);
+ mAdapter.remove(mSong);
+ mAdapter.insert(mSong, realTo);
+ mAdapter.notifyDataSetChanged();
+ MediaStore.Audio.Playlists.Members.moveItem(getSherlockActivity().getContentResolver(),
+ mPlaylistId, realFrom, realTo);
+ }
+}
diff --git a/src/com/andrew/apollo/ui/widgets/BottomActionBar.java b/src/com/andrew/apollo/ui/widgets/BottomActionBar.java
deleted file mode 100644
index 7141456..0000000
--- a/src/com/andrew/apollo/ui/widgets/BottomActionBar.java
+++ /dev/null
@@ -1,126 +0,0 @@
-/**
- *
- */
-
-package com.andrew.apollo.ui.widgets;
-
-import android.app.Activity;
-import android.content.Context;
-import android.content.Intent;
-import android.os.AsyncTask;
-import android.os.RemoteException;
-import android.util.AttributeSet;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.view.View.OnLongClickListener;
-import android.widget.ImageButton;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import com.andrew.apollo.Constants;
-import com.andrew.apollo.R;
-import com.andrew.apollo.activities.AudioPlayerHolder;
-import com.andrew.apollo.activities.QuickQueue;
-import com.andrew.apollo.tasks.GetCachedImages;
-import com.andrew.apollo.utils.MusicUtils;
-import com.andrew.apollo.utils.ThemeUtils;
-
-/**
- * @author Andrew Neal
- */
-public class BottomActionBar extends LinearLayout implements OnClickListener, OnLongClickListener,
- Constants {
-
- public BottomActionBar(Context context) {
- super(context);
- }
-
- public BottomActionBar(Context context, AttributeSet attrs) {
- super(context, attrs);
- setOnClickListener(this);
- setOnLongClickListener(this);
- }
-
- public BottomActionBar(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- }
-
- /**
- * Updates the bottom ActionBar's info
- *
- * @param activity
- * @throws RemoteException
- */
- public void updateBottomActionBar(Activity activity) {
- View bottomActionBar = activity.findViewById(R.id.bottom_action_bar);
- if (bottomActionBar == null) {
- return;
- }
-
- if (MusicUtils.mService != null && MusicUtils.getCurrentAudioId() != -1) {
-
- // Track name
- TextView mTrackName = (TextView)bottomActionBar
- .findViewById(R.id.bottom_action_bar_track_name);
- mTrackName.setText(MusicUtils.getTrackName());
-
- // Artist name
- TextView mArtistName = (TextView)bottomActionBar
- .findViewById(R.id.bottom_action_bar_artist_name);
- mArtistName.setText(MusicUtils.getArtistName());
-
- // Album art
- ImageView mAlbumArt = (ImageView)bottomActionBar
- .findViewById(R.id.bottom_action_bar_album_art);
-
- new GetCachedImages(activity, 1, mAlbumArt).executeOnExecutor(
- AsyncTask.THREAD_POOL_EXECUTOR, MusicUtils.getAlbumName());
-
- // Favorite image
- ImageButton mFavorite = (ImageButton)bottomActionBar
- .findViewById(R.id.bottom_action_bar_item_one);
-
- MusicUtils.setFavoriteImage(mFavorite);
-
- // Divider
- ImageView mDivider = (ImageView)activity
- .findViewById(R.id.bottom_action_bar_info_divider);
-
- ImageButton mSearch = (ImageButton)bottomActionBar
- .findViewById(R.id.bottom_action_bar_item_two);
-
- ImageButton mOverflow = (ImageButton)bottomActionBar
- .findViewById(R.id.bottom_action_bar_item_three);
-
- // Theme chooser
- ThemeUtils.setTextColor(activity, mTrackName, "bottom_action_bar_text_color");
- ThemeUtils.setTextColor(activity, mArtistName, "bottom_action_bar_text_color");
- ThemeUtils.setBackgroundColor(activity, mDivider, "bottom_action_bar_info_divider");
- ThemeUtils.setImageButton(activity, mSearch, "apollo_search");
- ThemeUtils.setImageButton(activity, mOverflow, "apollo_overflow");
- }
- }
-
- @Override
- public void onClick(View v) {
- switch (v.getId()) {
- case R.id.bottom_action_bar:
- Intent intent = new Intent();
- intent.setClass(v.getContext(), AudioPlayerHolder.class);
- intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
- v.getContext().startActivity(intent);
- break;
- default:
- break;
- }
-
- }
-
- @Override
- public boolean onLongClick(View v) {
- Context context = v.getContext();
- context.startActivity(new Intent(context, QuickQueue.class));
- return true;
- }
-}
diff --git a/src/com/andrew/apollo/ui/widgets/BottomActionBarItem.java b/src/com/andrew/apollo/ui/widgets/BottomActionBarItem.java
deleted file mode 100644
index dd02758..0000000
--- a/src/com/andrew/apollo/ui/widgets/BottomActionBarItem.java
+++ /dev/null
@@ -1,154 +0,0 @@
-/**
- *
- */
-
-package com.andrew.apollo.ui.widgets;
-
-import android.app.Activity;
-import android.content.Context;
-import android.content.Intent;
-import android.database.Cursor;
-import android.media.audiofx.AudioEffect;
-import android.net.Uri;
-import android.provider.BaseColumns;
-import android.provider.MediaStore.Audio;
-import android.provider.MediaStore.Audio.AudioColumns;
-import android.util.AttributeSet;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.view.View.OnLongClickListener;
-import android.widget.ImageButton;
-import android.widget.PopupMenu;
-import android.widget.PopupMenu.OnMenuItemClickListener;
-import android.widget.Toast;
-
-import com.andrew.apollo.R;
-import com.andrew.apollo.preferences.SettingsHolder;
-import com.andrew.apollo.tasks.FetchAlbumImages;
-import com.andrew.apollo.tasks.FetchArtistImages;
-import com.andrew.apollo.utils.MusicUtils;
-
-/**
- * @author Andrew Neal
- */
-public class BottomActionBarItem extends ImageButton implements OnLongClickListener,
- OnClickListener, OnMenuItemClickListener {
-
- private final Context mContext;
-
- private static final int EFFECTS_PANEL = 0;
-
- public BottomActionBarItem(Context context) {
- super(context);
- mContext = context;
- }
-
- public BottomActionBarItem(Context context, AttributeSet attrs) {
- super(context, attrs);
- setOnLongClickListener(this);
- setOnClickListener(this);
- mContext = context;
- }
-
- public BottomActionBarItem(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- mContext = context;
- }
-
- @Override
- public boolean onLongClick(View v) {
- Toast.makeText(getContext(), v.getContentDescription(), Toast.LENGTH_SHORT).show();
- return true;
- }
-
- @Override
- public void onClick(View v) {
- switch (v.getId()) {
- case R.id.bottom_action_bar_item_one:
- MusicUtils.toggleFavorite();
- MusicUtils.setFavoriteImage(this);
- break;
- case R.id.bottom_action_bar_item_two:
- ((Activity)mContext).onSearchRequested();
- break;
- case R.id.bottom_action_bar_item_three:
- showPopup(v);
- break;
- default:
- break;
- }
- }
-
- /**
- * @param v
- */
- private void showPopup(View v) {
- PopupMenu popup = new PopupMenu(getContext(), v);
- popup.setOnMenuItemClickListener(this);
- popup.inflate(R.menu.overflow_library);
- popup.show();
- }
-
- @Override
- public boolean onMenuItemClick(MenuItem item) {
- switch (item.getItemId()) {
- case R.id.settings:
- mContext.startActivity(new Intent(mContext, SettingsHolder.class));
- break;
- case R.id.equalizer:
- Intent i = new Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL);
- i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, MusicUtils.getCurrentAudioId());
- ((Activity)mContext).startActivityForResult(i, EFFECTS_PANEL);
- break;
- case R.id.shuffle_all:
- // TODO Only shuffle the tracks that are shown
- shuffleAll();
- break;
- // case R.id.fetch_artwork:
- // initAlbumImages();
- // break;
- // case R.id.fetch_artist_images:
- // initArtistImages();
- // break;
- default:
- break;
- }
- return false;
- }
-
- /**
- * Manually re-fetch artist imgages. Maybe the user wants to update them or
- * something went wrong the first time around.
- */
- public void initArtistImages() {
- FetchArtistImages getArtistImages = new FetchArtistImages(mContext, 1);
- getArtistImages.runTask();
- }
-
- /**
- * Manually fetch all of the album art.
- */
- public void initAlbumImages() {
- FetchAlbumImages getAlbumImages = new FetchAlbumImages(mContext, 1);
- getAlbumImages.runTask();
- }
-
- /**
- * Shuffle all the tracks
- */
- public void shuffleAll() {
- Uri uri = Audio.Media.EXTERNAL_CONTENT_URI;
- String[] projection = new String[] {
- BaseColumns._ID
- };
- String selection = AudioColumns.IS_MUSIC + "=1";
- String sortOrder = Audio.Media.DEFAULT_SORT_ORDER;
- Cursor cursor = MusicUtils.query(mContext, uri, projection, selection, null, sortOrder);
- if (cursor != null) {
- MusicUtils.shuffleAll(mContext, cursor);
- cursor.close();
- cursor = null;
- }
- }
-}
diff --git a/src/com/andrew/apollo/ui/widgets/RepeatingImageButton.java b/src/com/andrew/apollo/ui/widgets/RepeatingImageButton.java
deleted file mode 100644
index 1c73614..0000000
--- a/src/com/andrew/apollo/ui/widgets/RepeatingImageButton.java
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- * 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.
- */
-
-package com.andrew.apollo.ui.widgets;
-
-import android.content.Context;
-import android.os.SystemClock;
-import android.util.AttributeSet;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
-import android.view.View;
-import android.widget.ImageButton;
-
-/**
- * A button that will repeatedly call a 'listener' method as long as the button
- * is pressed.
- */
-public class RepeatingImageButton extends ImageButton {
-
- private long mStartTime;
-
- private int mRepeatCount;
-
- private RepeatListener mListener;
-
- private long mInterval = 500;
-
- public RepeatingImageButton(Context context) {
- this(context, null);
- }
-
- public RepeatingImageButton(Context context, AttributeSet attrs) {
- this(context, attrs, android.R.attr.imageButtonStyle);
- }
-
- public RepeatingImageButton(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- setFocusable(true);
- setLongClickable(true);
- }
-
- /**
- * Sets the listener to be called while the button is pressed and the
- * interval in milliseconds with which it will be called.
- *
- * @param l The listener that will be called
- * @param interval The interval in milliseconds for calls
- */
- public void setRepeatListener(RepeatListener l, long interval) {
- mListener = l;
- mInterval = interval;
- }
-
- @Override
- public boolean performLongClick() {
- mStartTime = SystemClock.elapsedRealtime();
- mRepeatCount = 0;
- post(mRepeater);
- return true;
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- if (event.getAction() == MotionEvent.ACTION_UP) {
- // remove the repeater, but call the hook one more time
- removeCallbacks(mRepeater);
- if (mStartTime != 0) {
- doRepeat(true);
- mStartTime = 0;
- }
- }
- return super.onTouchEvent(event);
- }
-
- @Override
- public boolean onKeyDown(int keyCode, KeyEvent event) {
- switch (keyCode) {
- case KeyEvent.KEYCODE_DPAD_CENTER:
- case KeyEvent.KEYCODE_ENTER:
- // need to call super to make long press work, but return
- // true so that the application doesn't get the down event.
- super.onKeyDown(keyCode, event);
- return true;
- }
- return super.onKeyDown(keyCode, event);
- }
-
- @Override
- public boolean onKeyUp(int keyCode, KeyEvent event) {
- switch (keyCode) {
- case KeyEvent.KEYCODE_DPAD_CENTER:
- case KeyEvent.KEYCODE_ENTER:
- // remove the repeater, but call the hook one more time
- removeCallbacks(mRepeater);
- if (mStartTime != 0) {
- doRepeat(true);
- mStartTime = 0;
- }
- }
- return super.onKeyUp(keyCode, event);
- }
-
- private final Runnable mRepeater = new Runnable() {
- @Override
- public void run() {
- doRepeat(false);
- if (isPressed()) {
- postDelayed(this, mInterval);
- }
- }
- };
-
- private void doRepeat(boolean last) {
- long now = SystemClock.elapsedRealtime();
- if (mListener != null) {
- mListener.onRepeat(this, now - mStartTime, last ? -1 : mRepeatCount++);
- }
- }
-
- public interface RepeatListener {
-
- void onRepeat(View v, long duration, int repeatcount);
- }
-}
diff --git a/src/com/andrew/apollo/ui/widgets/ScrollableTabView.java b/src/com/andrew/apollo/ui/widgets/ScrollableTabView.java
deleted file mode 100644
index 3b9142c..0000000
--- a/src/com/andrew/apollo/ui/widgets/ScrollableTabView.java
+++ /dev/null
@@ -1,198 +0,0 @@
-/*
- * Copyright (C) 2011 Andreas Stuetz <andreas.stuetz@gmail.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.
- */
-
-package com.andrew.apollo.ui.widgets;
-
-import java.util.ArrayList;
-
-import android.content.Context;
-import android.graphics.drawable.Drawable;
-import android.support.v4.view.ViewPager;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.HorizontalScrollView;
-import android.widget.LinearLayout;
-
-import com.andrew.apollo.adapters.TabAdapter;
-
-/**
- * I'm using a custom tab view in place of ActionBarTabs entirely for the theme
- * engine.
- */
-public class ScrollableTabView extends HorizontalScrollView implements
- ViewPager.OnPageChangeListener {
-
- private final Context mContext;
-
- private ViewPager mPager;
-
- private TabAdapter mAdapter;
-
- private final LinearLayout mContainer;
-
- private final ArrayList<View> mTabs = new ArrayList<View>();
-
- private Drawable mDividerDrawable;
-
- private final int mDividerColor = 0xFF636363;
-
- private int mDividerMarginTop = 12;
-
- private int mDividerMarginBottom = 12;
-
- private int mDividerWidth = 1;
-
- public ScrollableTabView(Context context) {
- this(context, null);
- }
-
- public ScrollableTabView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public ScrollableTabView(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs);
-
- this.mContext = context;
-
- mDividerMarginTop = (int)(getResources().getDisplayMetrics().density * mDividerMarginTop);
- mDividerMarginBottom = (int)(getResources().getDisplayMetrics().density * mDividerMarginBottom);
- mDividerWidth = (int)(getResources().getDisplayMetrics().density * mDividerWidth);
-
- this.setHorizontalScrollBarEnabled(false);
- this.setHorizontalFadingEdgeEnabled(false);
-
- mContainer = new LinearLayout(context);
- LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
- android.view.ViewGroup.LayoutParams.MATCH_PARENT,
- android.view.ViewGroup.LayoutParams.MATCH_PARENT);
- mContainer.setLayoutParams(params);
- mContainer.setOrientation(LinearLayout.HORIZONTAL);
-
- this.addView(mContainer);
-
- }
-
- public void setAdapter(TabAdapter adapter) {
- this.mAdapter = adapter;
-
- if (mPager != null && mAdapter != null)
- initTabs();
- }
-
- public void setViewPager(ViewPager pager) {
- this.mPager = pager;
- mPager.setOnPageChangeListener(this);
-
- if (mPager != null && mAdapter != null)
- initTabs();
- }
-
- private void initTabs() {
-
- mContainer.removeAllViews();
- mTabs.clear();
-
- if (mAdapter == null)
- return;
-
- for (int i = 0; i < mPager.getAdapter().getCount(); i++) {
-
- final int index = i;
-
- View tab = mAdapter.getView(i);
- mContainer.addView(tab);
-
- tab.setFocusable(true);
-
- mTabs.add(tab);
-
- if (i != mPager.getAdapter().getCount() - 1) {
- mContainer.addView(getSeparator());
- }
-
- tab.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- if (mPager.getCurrentItem() == index) {
- selectTab(index);
- } else {
- mPager.setCurrentItem(index, true);
- }
- }
- });
-
- }
-
- selectTab(mPager.getCurrentItem());
- }
-
- @Override
- public void onPageScrollStateChanged(int state) {
- }
-
- @Override
- public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
- }
-
- @Override
- public void onPageSelected(int position) {
- selectTab(position);
- }
-
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
- super.onLayout(changed, l, t, r, b);
-
- if (changed)
- selectTab(mPager.getCurrentItem());
- }
-
- private View getSeparator() {
- View v = new View(mContext);
-
- LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(mDividerWidth,
- android.view.ViewGroup.LayoutParams.MATCH_PARENT);
- params.setMargins(0, mDividerMarginTop, 0, mDividerMarginBottom);
- v.setLayoutParams(params);
-
- if (mDividerDrawable != null)
- v.setBackgroundDrawable(mDividerDrawable);
- else
- v.setBackgroundColor(mDividerColor);
-
- return v;
- }
-
- private void selectTab(int position) {
-
- for (int i = 0, pos = 0; i < mContainer.getChildCount(); i += 2, pos++) {
- View tab = mContainer.getChildAt(i);
- tab.setSelected(pos == position);
- }
-
- View selectedTab = mContainer.getChildAt(position * 2);
-
- final int w = selectedTab.getMeasuredWidth();
- final int l = selectedTab.getLeft();
-
- final int x = l - this.getWidth() / 2 + w / 2;
-
- smoothScrollTo(x, this.getScrollY());
-
- }
-
-}
diff --git a/src/com/andrew/apollo/utils/ApolloUtils.java b/src/com/andrew/apollo/utils/ApolloUtils.java
index 9704343..a360ede 100644
--- a/src/com/andrew/apollo/utils/ApolloUtils.java
+++ b/src/com/andrew/apollo/utils/ApolloUtils.java
@@ -1,327 +1,431 @@
-/**
- *
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.utils;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.HttpURLConnection;
-import java.net.URL;
-
-import android.app.ActionBar;
+import android.annotation.SuppressLint;
+import android.app.ActivityManager;
+import android.app.ActivityManager.RunningTaskInfo;
+import android.app.AlertDialog;
+import android.content.ComponentName;
import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
-import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
-import android.graphics.Canvas;
-import android.graphics.Matrix;
-import android.graphics.drawable.AnimationDrawable;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
+import android.graphics.Color;
+import android.graphics.Rect;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
-import android.net.Uri;
-import android.provider.MediaStore.Audio;
-import android.support.v4.app.Fragment;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.provider.MediaStore;
+import android.support.v4.app.FragmentActivity;
+import android.util.Log;
+import android.view.Gravity;
import android.view.View;
-import android.widget.ImageView;
-import android.widget.ListView;
-import android.widget.TextView;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+import android.webkit.WebView;
import android.widget.Toast;
-import com.andrew.apollo.Constants;
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.andrew.apollo.Config;
import com.andrew.apollo.R;
-import com.androidquery.util.AQUtility;
+import com.andrew.apollo.cache.ImageCache;
+import com.andrew.apollo.cache.ImageFetcher;
+import com.andrew.apollo.ui.activities.ShortcutActivity;
+import com.andrew.apollo.widgets.ColorPickerView;
+import com.andrew.apollo.widgets.ColorSchemeDialog;
+import com.devspark.appmsg.Crouton;
+
+import java.lang.ref.WeakReference;
+import java.util.List;
/**
- * @author Andrew Neal
- * @Note Various methods used to help with specific Apollo statements
+ * Mostly general and UI helpers.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
*/
-public class ApolloUtils implements Constants {
+public final class ApolloUtils {
/**
- * Used to fit a Bitmap nicely inside a View
- *
- * @param view
- * @param bitmap
+ * The threshold used calculate if a color is light or dark
*/
- public static void setBackground(View view, Bitmap bitmap) {
-
- if (bitmap == null) {
- view.setBackgroundResource(0);
- return;
- }
-
- int vwidth = view.getWidth();
- int vheight = view.getHeight();
- int bwidth = bitmap.getWidth();
- int bheight = bitmap.getHeight();
-
- float scalex = (float)vwidth / bwidth;
- float scaley = (float)vheight / bheight;
- float scale = Math.max(scalex, scaley) * 1.0f;
-
- Bitmap.Config config = Bitmap.Config.ARGB_8888;
- Bitmap background = Bitmap.createBitmap(vwidth, vheight, config);
+ private static final int BRIGHTNESS_THRESHOLD = 130;
- Canvas canvas = new Canvas(background);
-
- Matrix matrix = new Matrix();
- matrix.setTranslate(-bwidth / 2, -bheight / 2);
- matrix.postScale(scale, scale);
- matrix.postTranslate(vwidth / 2, vheight / 2);
-
- canvas.drawBitmap(bitmap, matrix, null);
-
- view.setBackgroundDrawable(new BitmapDrawable(view.getResources(), background));
+ /* This class is never initiated */
+ public ApolloUtils() {
}
/**
- * @param view
- * @param bitmap This is to avoid Bitmap's IllegalArgumentException
+ * Used to determine if the current device is a Google TV
+ *
+ * @param context The {@link Context} to use
+ * @return True if the device has Google TV, false otherwise
*/
- public static void runnableBackground(final ImageView view, final Bitmap bitmap) {
- view.post(new Runnable() {
-
- @Override
- public void run() {
- ApolloUtils.setBackground(view, bitmap);
- }
- });
+ public static final boolean isGoogleTV(final Context context) {
+ return context.getPackageManager().hasSystemFeature("com.google.android.tv");
}
/**
- * @param context
- * @return whether there is an active data connection
+ * Used to determine if the device is running Froyo or greater
+ *
+ * @return True if the device is running Froyo or greater, false otherwise
*/
- public static boolean isOnline(Context context) {
- boolean state = false;
- ConnectivityManager cm = (ConnectivityManager)context
- .getSystemService(Context.CONNECTIVITY_SERVICE);
-
- NetworkInfo wifiNetwork = cm.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
- if (wifiNetwork != null) {
- state = wifiNetwork.isConnectedOrConnecting();
- }
-
- NetworkInfo mobileNetwork = cm.getNetworkInfo(ConnectivityManager.TYPE_MOBILE);
- if (mobileNetwork != null) {
- state = mobileNetwork.isConnectedOrConnecting();
- }
-
- NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
- if (activeNetwork != null) {
- state = activeNetwork.isConnectedOrConnecting();
- }
- return state;
+ public static final boolean hasFroyo() {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO;
}
/**
- * Sets cached image URLs
+ * Used to determine if the device is running Gingerbread or greater
*
- * @param artistName
- * @param url
- * @param key
- * @param context
+ * @return True if the device is running Gingerbread or greater, false
+ * otherwise
*/
- public static void setImageURL(String name, String url, String key, Context context) {
- SharedPreferences settings = context.getSharedPreferences(key, 0);
- SharedPreferences.Editor editor = settings.edit();
- editor.putString(name, url);
- editor.commit();
+ public static final boolean hasGingerbread() {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD;
}
/**
- * @param name
- * @param key
- * @param context
- * @return cached image URLs
+ * Used to determine if the device is running Honeycomb or greater
+ *
+ * @return True if the device is running Honeycomb or greater, false
+ * otherwise
*/
- public static String getImageURL(String name, String key, Context context) {
- SharedPreferences settings = context.getSharedPreferences(key, 0);
- return settings.getString(name, null);
+ public static final boolean hasHoneycomb() {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB;
}
/**
- * @param context
- * @return if a Tablet is the device being used
+ * Used to determine if the device is running Honeycomb-MR1 or greater
+ *
+ * @return True if the device is running Honeycomb-MR1 or greater, false
+ * otherwise
*/
- public static boolean isTablet(Context context) {
- return (context.getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_LARGE;
+ public static final boolean hasHoneycombMR1() {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1;
}
/**
- * UP accordance without the icon
+ * Used to determine if the device is running ICS or greater
*
- * @param actionBar
+ * @return True if the device is running Ice Cream Sandwich or greater,
+ * false otherwise
*/
- public static void showUpTitleOnly(ActionBar actionBar) {
- actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_TITLE,
- ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_TITLE
- | ActionBar.DISPLAY_SHOW_HOME);
+ public static final boolean hasICS() {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH;
}
/**
- * @param bitmap
- * @param newHeight
- * @param newWidth
- * @return a scaled Bitmap
+ * Used to determine if the device is running Jelly Bean or greater
+ *
+ * @return True if the device is running Jelly Bean or greater, false
+ * otherwise
*/
- public static Bitmap getResizedBitmap(Bitmap bitmap, int newHeight, int newWidth) {
+ public static final boolean hasJellyBean() {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
+ }
- if (bitmap == null) {
- return null;
- }
+ /**
+ * Used to determine if the device is a tablet or not
+ *
+ * @param context The {@link Context} to use.
+ * @return True if the device is a tablet, false otherwise.
+ */
+ public static final boolean isTablet(final Context context) {
+ return (context.getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_LARGE;
+ }
- int width = bitmap.getWidth();
- int height = bitmap.getHeight();
- float scaleWidth = ((float)newWidth) / width;
- float scaleHeight = ((float)newHeight) / height;
- Matrix matrix = new Matrix();
- matrix.postScale(scaleWidth, scaleHeight);
- Bitmap resizedBitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, false);
- return resizedBitmap;
+ /**
+ * Used to determine if the device is currently in landscape mode
+ *
+ * @param context The {@link Context} to use.
+ * @return True if the device is in landscape mode, false otherwise.
+ */
+ public static final boolean isLandscape(final Context context) {
+ return context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
}
/**
- * Header used in the track browser
+ * Execute an {@link AsyncTask} on a thread pool
*
- * @param fragment
- * @param view
- * @param string
+ * @param forceSerial True to force the task to run in serial order
+ * @param task Task to execute
+ * @param args Optional arguments to pass to
+ * {@link AsyncTask#execute(Object[])}
+ * @param <T> Task argument type
*/
- public static void listHeader(Fragment fragment, View view, String string) {
- if (fragment.getArguments() != null) {
- TextView mHeader = (TextView)view.findViewById(R.id.title);
- String mimetype = fragment.getArguments().getString(MIME_TYPE);
- if (Audio.Artists.CONTENT_TYPE.equals(mimetype)) {
- mHeader.setVisibility(View.VISIBLE);
- mHeader.setText(string);
- } else if (Audio.Albums.CONTENT_TYPE.equals(mimetype)) {
- mHeader.setVisibility(View.VISIBLE);
- mHeader.setText(string);
- }
+ @SuppressLint("NewApi")
+ public static <T> void execute(final boolean forceSerial, final AsyncTask<T, ?, ?> task,
+ final T... args) {
+ final WeakReference<AsyncTask<T, ?, ?>> taskReference = new WeakReference<AsyncTask<T, ?, ?>>(
+ task);
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.DONUT) {
+ throw new UnsupportedOperationException(
+ "This class can only be used on API 4 and newer.");
+ }
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB || forceSerial) {
+ taskReference.get().execute(args);
+ } else {
+ taskReference.get().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, args);
}
}
/**
- * Sets the ListView paddingLeft for the header
+ * Used to determine if there is an active data connection and what type of
+ * connection it is if there is one
*
- * @param fragment
- * @param mListView
+ * @param context The {@link Context} to use
+ * @return True if there is an active data connection, false otherwise.
+ * Also, if the user has checked to only download via Wi-Fi in the
+ * settings, the mobile data and other network connections aren't
+ * returned at all
*/
- public static void setListPadding(Fragment fragment, ListView mListView, int left, int top,
- int right, int bottom) {
- if (fragment.getArguments() != null) {
- String mimetype = fragment.getArguments().getString(MIME_TYPE);
- if (Audio.Albums.CONTENT_TYPE.equals(mimetype)) {
- mListView.setPadding(AQUtility.dip2pixel(fragment.getActivity(), left), top,
- AQUtility.dip2pixel(fragment.getActivity(), right), bottom);
- } else if (Audio.Artists.CONTENT_TYPE.equals(mimetype)) {
- mListView.setPadding(AQUtility.dip2pixel(fragment.getActivity(), left), top,
- AQUtility.dip2pixel(fragment.getActivity(), right), bottom);
+ public static final boolean isOnline(final Context context) {
+ /*
+ * This sort of handles a sudden configuration change, but I think it
+ * should be dealt with in a more professional way.
+ */
+ if (context == null) {
+ return false;
+ }
+
+ boolean state = false;
+ final boolean onlyOnWifi = PreferenceUtils.getInstace(context).onlyOnWifi();
+
+ /* Monitor network connections */
+ final ConnectivityManager connectivityManager = (ConnectivityManager)context
+ .getSystemService(Context.CONNECTIVITY_SERVICE);
+
+ /* Wi-Fi connection */
+ final NetworkInfo wifiNetwork = connectivityManager
+ .getNetworkInfo(ConnectivityManager.TYPE_WIFI);
+ if (wifiNetwork != null) {
+ state = wifiNetwork.isConnectedOrConnecting();
+ }
+
+ /* Mobile data connection */
+ final NetworkInfo mbobileNetwork = connectivityManager
+ .getNetworkInfo(ConnectivityManager.TYPE_MOBILE);
+ if (mbobileNetwork != null) {
+ if (!onlyOnWifi) {
+ state = mbobileNetwork.isConnectedOrConnecting();
+ }
+ }
+
+ /* Other networks */
+ final NetworkInfo activeNetwork = connectivityManager.getActiveNetworkInfo();
+ if (activeNetwork != null) {
+ if (!onlyOnWifi) {
+ state = activeNetwork.isConnectedOrConnecting();
}
}
- }
- // Returns if we're viewing an album
- public static boolean isAlbum(String mimeType) {
- return Audio.Albums.CONTENT_TYPE.equals(mimeType);
+ return state;
}
- // Returns if we're viewing an artists albums
- public static boolean isArtist(String mimeType) {
- return Audio.Artists.CONTENT_TYPE.equals(mimeType);
+ /**
+ * Display a {@link Toast} letting the user know what an item does when long
+ * pressed.
+ *
+ * @param view The {@link View} to copy the content description from.
+ */
+ public static void showCheatSheet(final View view) {
+
+ final int[] screenPos = new int[2]; // origin is device display
+ final Rect displayFrame = new Rect(); // includes decorations (e.g.
+ // status bar)
+ view.getLocationOnScreen(screenPos);
+ view.getWindowVisibleDisplayFrame(displayFrame);
+
+ final Context context = view.getContext();
+ final int viewWidth = view.getWidth();
+ final int viewHeight = view.getHeight();
+ final int viewCenterX = screenPos[0] + viewWidth / 2;
+ final int screenWidth = context.getResources().getDisplayMetrics().widthPixels;
+ final int estimatedToastHeight = (int)(48 * context.getResources().getDisplayMetrics().density);
+
+ final Toast cheatSheet = Toast.makeText(context, view.getContentDescription(),
+ Toast.LENGTH_SHORT);
+ final boolean showBelow = screenPos[1] < estimatedToastHeight;
+ if (showBelow) {
+ // Show below
+ // Offsets are after decorations (e.g. status bar) are factored in
+ cheatSheet.setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL, viewCenterX
+ - screenWidth / 2, screenPos[1] - displayFrame.top + viewHeight);
+ } else {
+ // Show above
+ // Offsets are after decorations (e.g. status bar) are factored in
+ cheatSheet.setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, viewCenterX
+ - screenWidth / 2, displayFrame.bottom - screenPos[1]);
+ }
+ cheatSheet.show();
}
- // Returns if we're viewing a genre
- public static boolean isGenre(String mimeType) {
- return Audio.Genres.CONTENT_TYPE.equals(mimeType);
+ /**
+ * @param context The {@link Context} to use.
+ * @return An {@link AlertDialog} used to show the open source licenses used
+ * in Apollo.
+ */
+ public static final AlertDialog createOpenSourceDialog(final Context context) {
+ final WebView webView = new WebView(context);
+ webView.loadUrl("file:///android_asset/licenses.html");
+ return new AlertDialog.Builder(context).setTitle(R.string.settings_open_source_licenses)
+ .setView(webView)
+ .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(final DialogInterface dialog, final int whichButton) {
+ dialog.dismiss();
+ }
+ }).create();
}
/**
- * @param artistName
- * @param id
- * @param key
- * @param context
+ * Calculate whether a color is light or dark, based on a commonly known
+ * brightness formula.
+ *
+ * @see {@literal http://en.wikipedia.org/wiki/HSV_color_space%23Lightness}
*/
- public static void setArtistId(String artistName, long id, String key, Context context) {
- SharedPreferences settings = context.getSharedPreferences(key, 0);
- SharedPreferences.Editor editor = settings.edit();
- editor.putLong(artistName, id);
- editor.commit();
+ public static final boolean isColorDark(final int color) {
+ return (30 * Color.red(color) + 59 * Color.green(color) + 11 * Color.blue(color)) / 100 <= BRIGHTNESS_THRESHOLD;
}
/**
- * @param artistName
- * @param key
- * @param context
- * @return artist ID
+ * Runs a piece of code after the next layout run
+ *
+ * @param view The {@link View} used.
+ * @param runnable The {@link Runnable} used after the next layout run
*/
- public static Long getArtistId(String artistName, String key, Context context) {
- SharedPreferences settings = context.getSharedPreferences(key, 0);
- return settings.getLong(artistName, 0);
+ @SuppressLint("NewApi")
+ public static void doAfterLayout(final View view, final Runnable runnable) {
+ final OnGlobalLayoutListener listener = new OnGlobalLayoutListener() {
+ @SuppressWarnings("deprecation")
+ @Override
+ public void onGlobalLayout() {
+ /* Layout pass done, unregister for further events */
+ if (hasJellyBean()) {
+ view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ } else {
+ view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
+ }
+ runnable.run();
+ }
+ };
+ view.getViewTreeObserver().addOnGlobalLayoutListener(listener);
}
/**
- * @param artistName
+ * Creates a new instance of the {@link ImageCache} and {@link ImageFetcher}
+ *
+ * @param activity The {@link FragmentActivity} to use.
+ * @return A new {@link ImageFetcher} used to fetch images asynchronously.
*/
- public static void shopFor(Context mContext, String artistName) {
- String str = "https://market.android.com/search?q=%s&c=music&featured=MUSIC_STORE_SEARCH";
- Intent shopIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(String.format(str,
- Uri.encode(artistName))));
- shopIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- shopIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
- mContext.startActivity(shopIntent);
+ public static final ImageFetcher getImageFetcher(final SherlockFragmentActivity activity) {
+ final ImageFetcher imageFetcher = ImageFetcher.getInstance(activity);
+ imageFetcher.setImageCache(ImageCache.findOrCreateCache(activity));
+ return imageFetcher;
}
/**
- * @param src
- * @return Bitmap fro URL
+ * Used to create shortcuts for an artist, album, or playlist that is then
+ * placed on the default launcher homescreen
+ *
+ * @param displayName The shortcut name
+ * @param id The ID of the artist, album, playlist, or genre
+ * @param mimeType The MIME type of the shortcut
+ * @param context The {@link Context} to use to
*/
- public static Bitmap getBitmapFromURL(String src) {
+ public static void createShortcutIntent(final String displayName, final Long id,
+ final String mimeType, final SherlockFragmentActivity context) {
try {
- URL url = new URL(src);
- HttpURLConnection connection = (HttpURLConnection)url.openConnection();
- connection.setDoInput(true);
- connection.connect();
- InputStream input = connection.getInputStream();
- Bitmap myBitmap = BitmapFactory.decodeStream(input);
- return myBitmap;
- } catch (IOException e) {
- e.printStackTrace();
- return null;
+ final ImageFetcher fetcher = getImageFetcher(context);
+ Bitmap bitmap = null;
+ if (mimeType.equals(MediaStore.Audio.Albums.CONTENT_TYPE)) {
+ bitmap = fetcher.getCachedBitmap(displayName + Config.ALBUM_ART_SUFFIX);
+ } else {
+ bitmap = fetcher.getCachedBitmap(displayName);
+ }
+ if (bitmap == null) {
+ bitmap = BitmapFactory.decodeResource(context.getResources(),
+ R.drawable.default_artwork);
+ }
+
+ // Intent used when the icon is touched
+ final Intent shortcutIntent = new Intent(context, ShortcutActivity.class);
+ shortcutIntent.setAction(Intent.ACTION_VIEW);
+ shortcutIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ shortcutIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ shortcutIntent.putExtra(Config.ID, id);
+ shortcutIntent.putExtra(Config.NAME, displayName);
+ shortcutIntent.putExtra(Config.MIME_TYPE, mimeType);
+
+ // Intent that actually sets the shortcut
+ final Intent intent = new Intent();
+ intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, BitmapUtils.resizeAndCropCenter(bitmap, 96));
+ intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
+ intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, displayName);
+ intent.setAction("com.android.launcher.action.INSTALL_SHORTCUT");
+ context.sendBroadcast(intent);
+ Crouton.makeText(context,
+ displayName + " " + context.getString(R.string.pinned_to_home_screen),
+ Crouton.STYLE_CONFIRM).show();
+ } catch (final Exception e) {
+ Log.e("ApolloUtils", "createShortcutIntent - " + e);
+ Crouton.makeText(
+ context,
+ displayName + " "
+ + context.getString(R.string.could_not_be_pinned_to_home_screen),
+ Crouton.STYLE_ALERT).show();
}
}
/**
- * @param message
+ * Shows the {@link ColorPickerView}
+ *
+ * @param context The {@link Context} to use.
*/
- public static void showToast(int message, Toast mToast, Context context) {
- if (mToast == null) {
- mToast = Toast.makeText(context, "", Toast.LENGTH_SHORT);
- }
- mToast.setText(context.getString(message));
- mToast.show();
+ public static void showColorPicker(final Context context) {
+ final ColorSchemeDialog colorPickerView = new ColorSchemeDialog(context);
+ colorPickerView.setButton(AlertDialog.BUTTON_POSITIVE,
+ context.getString(android.R.string.ok), new OnClickListener() {
+
+ @Override
+ public void onClick(final DialogInterface dialog, final int which) {
+ PreferenceUtils.getInstace(context).setDefaultThemeColor(
+ colorPickerView.getColor());
+ }
+ });
+ colorPickerView.setButton(AlertDialog.BUTTON_NEGATIVE, context.getString(R.string.cancel),
+ (DialogInterface.OnClickListener)null);
+ colorPickerView.show();
}
/**
- * @param context
- * @return meow
+ * Used to know if Apollo was sent into the background
+ *
+ * @param context The {@link Context} to use
*/
- public static AnimationDrawable getNyanCat(Context context) {
- final AnimationDrawable animation = new AnimationDrawable();
- for (int i = 0; i < 12; i++) {
- try {
- animation.addFrame(Drawable.createFromStream(
- context.getAssets().open("Frame" + i + ".png"), null), 75);
- } catch (IOException e) {
+ public static final boolean isApplicationSentToBackground(final Context context) {
+ final ActivityManager activityManager = (ActivityManager)context
+ .getSystemService(Context.ACTIVITY_SERVICE);
+ final List<RunningTaskInfo> tasks = activityManager.getRunningTasks(1);
+ if (!tasks.isEmpty()) {
+ final ComponentName topActivity = tasks.get(0).topActivity;
+ if (!topActivity.getPackageName().equals(context.getPackageName())) {
+ return true;
}
}
- animation.setOneShot(false);
- return animation;
+ return false;
}
+
}
diff --git a/src/com/andrew/apollo/utils/BitmapUtils.java b/src/com/andrew/apollo/utils/BitmapUtils.java
new file mode 100644
index 0000000..9901476
--- /dev/null
+++ b/src/com/andrew/apollo/utils/BitmapUtils.java
@@ -0,0 +1,322 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.utils;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.ColorMatrix;
+import android.graphics.ColorMatrixColorFilter;
+import android.graphics.Paint;
+
+/**
+ * {@link Bitmap} specific helpers.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public final class BitmapUtils {
+
+ /* Initial blur radius. */
+ private static final int DEFAULT_BLUR_RADIUS = 8;
+
+ /** This class is never instantiated */
+ private BitmapUtils() {
+ }
+
+ /**
+ * Takes a bitmap and creates a new slightly blurry version of it.
+ *
+ * @param sentBitmap The {@link Bitmap} to blur.
+ * @return A blurred version of the given {@link Bitmap}.
+ */
+ public static final Bitmap createBlurredBitmap(final Bitmap sentBitmap) {
+ if (sentBitmap == null) {
+ return null;
+ }
+
+ // Stack Blur v1.0 from
+ // http://www.quasimondo.com/StackBlurForCanvas/StackBlurDemo.html
+ //
+ // Java Author: Mario Klingemann <mario at quasimondo.com>
+ // http://incubator.quasimondo.com
+ // created Feburary 29, 2004
+ // Android port : Yahel Bouaziz <yahel at kayenko.com>
+ // http://www.kayenko.com
+ // ported april 5th, 2012
+
+ // This is a compromise between Gaussian Blur and Box blur
+ // It creates much better looking blurs than Box Blur, but is
+ // 7x faster than my Gaussian Blur implementation.
+ //
+ // I called it Stack Blur because this describes best how this
+ // filter works internally: it creates a kind of moving stack
+ // of colors whilst scanning through the image. Thereby it
+ // just has to add one new block of color to the right side
+ // of the stack and remove the leftmost color. The remaining
+ // colors on the topmost layer of the stack are either added on
+ // or reduced by one, depending on if they are on the right or
+ // on the left side of the stack.
+ //
+ // If you are using this algorithm in your code please add
+ // the following line:
+ //
+ // Stack Blur Algorithm by Mario Klingemann <mario@quasimondo.com>
+
+ final Bitmap mBitmap = sentBitmap.copy(sentBitmap.getConfig(), true);
+
+ final int w = mBitmap.getWidth();
+ final int h = mBitmap.getHeight();
+
+ final int[] pix = new int[w * h];
+ mBitmap.getPixels(pix, 0, w, 0, 0, w, h);
+
+ final int wm = w - 1;
+ final int hm = h - 1;
+ final int wh = w * h;
+ final int div = DEFAULT_BLUR_RADIUS + DEFAULT_BLUR_RADIUS + 1;
+
+ final int r[] = new int[wh];
+ final int g[] = new int[wh];
+ final int b[] = new int[wh];
+ final int vmin[] = new int[Math.max(w, h)];
+ int rsum, gsum, bsum, x, y, i, p, yp, yi, yw;
+
+ int divsum = div + 1 >> 1;
+ divsum *= divsum;
+ final int dv[] = new int[256 * divsum];
+ for (i = 0; i < 256 * divsum; i++) {
+ dv[i] = i / divsum;
+ }
+
+ yw = yi = 0;
+
+ final int[][] stack = new int[div][3];
+ int stackpointer;
+ int stackstart;
+ int[] sir;
+ int rbs;
+ final int r1 = DEFAULT_BLUR_RADIUS + 1;
+ int routsum, goutsum, boutsum;
+ int rinsum, ginsum, binsum;
+
+ for (y = 0; y < h; y++) {
+ rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0;
+ for (i = -DEFAULT_BLUR_RADIUS; i <= DEFAULT_BLUR_RADIUS; i++) {
+ p = pix[yi + Math.min(wm, Math.max(i, 0))];
+ sir = stack[i + DEFAULT_BLUR_RADIUS];
+ sir[0] = (p & 0xff0000) >> 16;
+ sir[1] = (p & 0x00ff00) >> 8;
+ sir[2] = p & 0x0000ff;
+ rbs = r1 - Math.abs(i);
+ rsum += sir[0] * rbs;
+ gsum += sir[1] * rbs;
+ bsum += sir[2] * rbs;
+ if (i > 0) {
+ rinsum += sir[0];
+ ginsum += sir[1];
+ binsum += sir[2];
+ } else {
+ routsum += sir[0];
+ goutsum += sir[1];
+ boutsum += sir[2];
+ }
+ }
+ stackpointer = DEFAULT_BLUR_RADIUS;
+
+ for (x = 0; x < w; x++) {
+
+ r[yi] = dv[rsum];
+ g[yi] = dv[gsum];
+ b[yi] = dv[bsum];
+
+ rsum -= routsum;
+ gsum -= goutsum;
+ bsum -= boutsum;
+
+ stackstart = stackpointer - DEFAULT_BLUR_RADIUS + div;
+ sir = stack[stackstart % div];
+
+ routsum -= sir[0];
+ goutsum -= sir[1];
+ boutsum -= sir[2];
+
+ if (y == 0) {
+ vmin[x] = Math.min(x + DEFAULT_BLUR_RADIUS + 1, wm);
+ }
+ p = pix[yw + vmin[x]];
+
+ sir[0] = (p & 0xff0000) >> 16;
+ sir[1] = (p & 0x00ff00) >> 8;
+ sir[2] = p & 0x0000ff;
+
+ rinsum += sir[0];
+ ginsum += sir[1];
+ binsum += sir[2];
+
+ rsum += rinsum;
+ gsum += ginsum;
+ bsum += binsum;
+
+ stackpointer = (stackpointer + 1) % div;
+ sir = stack[stackpointer % div];
+
+ routsum += sir[0];
+ goutsum += sir[1];
+ boutsum += sir[2];
+
+ rinsum -= sir[0];
+ ginsum -= sir[1];
+ binsum -= sir[2];
+
+ yi++;
+ }
+ yw += w;
+ }
+ for (x = 0; x < w; x++) {
+ rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0;
+ yp = -DEFAULT_BLUR_RADIUS * w;
+ for (i = -DEFAULT_BLUR_RADIUS; i <= DEFAULT_BLUR_RADIUS; i++) {
+ yi = Math.max(0, yp) + x;
+
+ sir = stack[i + DEFAULT_BLUR_RADIUS];
+
+ sir[0] = r[yi];
+ sir[1] = g[yi];
+ sir[2] = b[yi];
+
+ rbs = r1 - Math.abs(i);
+
+ rsum += r[yi] * rbs;
+ gsum += g[yi] * rbs;
+ bsum += b[yi] * rbs;
+
+ if (i > 0) {
+ rinsum += sir[0];
+ ginsum += sir[1];
+ binsum += sir[2];
+ } else {
+ routsum += sir[0];
+ goutsum += sir[1];
+ boutsum += sir[2];
+ }
+
+ if (i < hm) {
+ yp += w;
+ }
+ }
+ yi = x;
+ stackpointer = DEFAULT_BLUR_RADIUS;
+ for (y = 0; y < h; y++) {
+ pix[yi] = 0xff000000 | dv[rsum] << 16 | dv[gsum] << 8 | dv[bsum];
+
+ rsum -= routsum;
+ gsum -= goutsum;
+ bsum -= boutsum;
+
+ stackstart = stackpointer - DEFAULT_BLUR_RADIUS + div;
+ sir = stack[stackstart % div];
+
+ routsum -= sir[0];
+ goutsum -= sir[1];
+ boutsum -= sir[2];
+
+ if (x == 0) {
+ vmin[y] = Math.min(y + r1, hm) * w;
+ }
+ p = x + vmin[y];
+
+ sir[0] = r[p];
+ sir[1] = g[p];
+ sir[2] = b[p];
+
+ rinsum += sir[0];
+ ginsum += sir[1];
+ binsum += sir[2];
+
+ rsum += rinsum;
+ gsum += ginsum;
+ bsum += binsum;
+
+ stackpointer = (stackpointer + 1) % div;
+ sir = stack[stackpointer];
+
+ routsum += sir[0];
+ goutsum += sir[1];
+ boutsum += sir[2];
+
+ rinsum -= sir[0];
+ ginsum -= sir[1];
+ binsum -= sir[2];
+
+ yi += w;
+ }
+ }
+
+ mBitmap.setPixels(pix, 0, w, 0, 0, w, h);
+ return mBitmap;
+ }
+
+ /**
+ * This is only used when the launcher shortcut is created.
+ *
+ * @param bitmap The artist, album, genre, or playlist image that's going to
+ * be cropped.
+ * @param size The new size.
+ * @return A {@link Bitmap} that has been resized and cropped for a launcher
+ * shortcut.
+ */
+ public static final Bitmap resizeAndCropCenter(final Bitmap bitmap, final int size) {
+ final int w = bitmap.getWidth();
+ final int h = bitmap.getHeight();
+ if (w == size && h == size) {
+ return bitmap;
+ }
+
+ final float mScale = (float)size / Math.min(w, h);
+
+ final Bitmap mTarget = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
+ final int mWidth = Math.round(mScale * bitmap.getWidth());
+ final int mHeight = Math.round(mScale * bitmap.getHeight());
+ final Canvas mCanvas = new Canvas(mTarget);
+ mCanvas.translate((size - mWidth) / 2f, (size - mHeight) / 2f);
+ mCanvas.scale(mScale, mScale);
+ final Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG);
+ mCanvas.drawBitmap(bitmap, 0, 0, paint);
+ return mTarget;
+ }
+
+ /**
+ * Used to remove the saturation (if saturate) and slightly enlarge a
+ * {@link Bitmap}.
+ *
+ * @param bitmap The {@link Bitmap} to filer.
+ */
+ public static final Bitmap createSaturatedBitmap(final Bitmap bitmap) {
+ if (bitmap == null) {
+ return null;
+ }
+
+ final Bitmap mBitmap = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(),
+ Bitmap.Config.RGB_565);
+ final Canvas mCanvas = new Canvas(mBitmap);
+ final Paint mPaint = new Paint();
+ final ColorMatrix mColorMatrix = new ColorMatrix();
+ mColorMatrix.setSaturation(0);
+ final ColorMatrix mDarkMatrix = new ColorMatrix();
+ mDarkMatrix.setScale(0.3f, 0.3f, 0.3f, 1.0f);
+ mColorMatrix.postConcat(mDarkMatrix);
+ final ColorMatrixColorFilter mFilter = new ColorMatrixColorFilter(mColorMatrix);
+ mPaint.setColorFilter(mFilter);
+ mCanvas.drawBitmap(bitmap, 0, 0, mPaint);
+ return mBitmap;
+ }
+
+}
diff --git a/src/com/andrew/apollo/utils/DomElement.java b/src/com/andrew/apollo/utils/DomElement.java
deleted file mode 100644
index 611faf0..0000000
--- a/src/com/andrew/apollo/utils/DomElement.java
+++ /dev/null
@@ -1,167 +0,0 @@
-/*
- * Copyright (c) 2012, the Last.fm Java Project and Committers
- * All rights reserved.
- *
- * Redistribution and use of this software in source and binary forms, with or without modification, are
- * permitted provided that the following conditions are met:
- *
- * - Redistributions of source code must retain the above
- * copyright notice, this list of conditions and the
- * following disclaimer.
- *
- * - Redistributions in binary form must reproduce the above
- * copyright notice, this list of conditions and the
- * following disclaimer in the documentation and/or other
- * materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
- * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
- * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
- * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
- * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.andrew.apollo.utils;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import org.w3c.dom.Element;
-import org.w3c.dom.Node;
-import org.w3c.dom.NodeList;
-
-/**
- * <code>DomElement</code> wraps around an {@link Element} and provides convenience methods.
- *
- * @author Janni Kovacs
- */
-public class DomElement {
- private Element e;
-
- /**
- * Creates a new wrapper around the given {@link Element}.
- *
- * @param elem An w3c Element
- */
- public DomElement(Element elem) {
- this.e = elem;
- }
-
- /**
- * @return the original Element
- */
- public Element getElement() {
- return e;
- }
-
- /**
- * Tests if this element has an attribute with the specified name.
- *
- * @param name Name of the attribute.
- * @return <code>true</code> if this element has an attribute with the specified name.
- */
- public boolean hasAttribute(String name) {
- return e.hasAttribute(name);
- }
-
- /**
- * Returns the attribute value to a given attribute name or <code>null</code> if the attribute doesn't exist.
- *
- * @param name The attribute's name
- * @return Attribute value or <code>null</code>
- */
- public String getAttribute(String name) {
- return e.hasAttribute(name) ? e.getAttribute(name) : null;
- }
-
- /**
- * @return the text content of the element
- */
- public String getText() {
- // XXX e.getTextContent() doesn't exsist under Android (Lukasz Wisniewski)
- /// getTextContent() is now available in at least Android 2.2 if not earlier, so we'll keep using that
- // return e.hasChildNodes() ? e.getFirstChild().getNodeValue() : null;
- return e.getTextContent();
- }
-
- /**
- * Checks if this element has a child element with the given name.
- *
- * @param name The child's name
- * @return <code>true</code> if this element has a child element with the given name
- */
- public boolean hasChild(String name) {
- NodeList list = e.getElementsByTagName(name);
- for (int i = 0, j = list.getLength(); i < j; i++) {
- Node item = list.item(i);
- if (item.getParentNode() == e)
- return true;
- }
- return false;
- }
-
- /**
- * Returns the child element with the given name or <code>null</code> if it doesn't exist.
- *
- * @param name The child's name
- * @return the child element or <code>null</code>
- */
- public DomElement getChild(String name) {
- NodeList list = e.getElementsByTagName(name);
- if (list.getLength() == 0)
- return null;
- for (int i = 0, j = list.getLength(); i < j; i++) {
- Node item = list.item(i);
- if (item.getParentNode() == e)
- return new DomElement((Element) item);
- }
- return null;
- }
-
- /**
- * Returns the text content of a child node with the given name. If no such child exists or the child
- * does not have text content, <code>null</code> is returned.
- *
- * @param name The child's name
- * @return the child's text content or <code>null</code>
- */
- public String getChildText(String name) {
- DomElement child = getChild(name);
- return child != null ? child.getText() : null;
- }
-
- /**
- * @return all children of this element
- */
- public List<DomElement> getChildren() {
- return getChildren("*");
- }
-
- /**
- * Returns all children of this element with the given tag name.
- *
- * @param name The children's tag name
- * @return all matching children
- */
- public List<DomElement> getChildren(String name) {
- List<DomElement> l = new ArrayList<DomElement>();
- NodeList list = e.getElementsByTagName(name);
- for (int i = 0; i < list.getLength(); i++) {
- Node node = list.item(i);
- if (node.getParentNode() == e)
- l.add(new DomElement((Element) node));
- }
- return l;
- }
-
- /**
- * Returns this element's tag name.
- *
- * @return the tag name
- */
- public String getTagName() {
- return e.getTagName();
- }
-}
diff --git a/src/com/andrew/apollo/utils/Lists.java b/src/com/andrew/apollo/utils/Lists.java
new file mode 100644
index 0000000..48b1b48
--- /dev/null
+++ b/src/com/andrew/apollo/utils/Lists.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2012 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 com.andrew.apollo.utils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedList;
+
+/**
+ * Provides static methods for creating {@code List} instances easily, and other
+ * utility methods for working with lists.
+ */
+public final class Lists {
+
+ /** This class is never instantiated */
+ public Lists() {
+ }
+
+ /**
+ * Creates an empty {@code ArrayList} instance.
+ * <p>
+ * <b>Note:</b> if you only need an <i>immutable</i> empty List, use
+ * {@link Collections#emptyList} instead.
+ *
+ * @return a newly-created, initially-empty {@code ArrayList}
+ */
+ public static final <E> ArrayList<E> newArrayList() {
+ return new ArrayList<E>();
+ }
+
+ /**
+ * Creates an empty {@code LinkedList} instance.
+ * <p>
+ * <b>Note:</b> if you only need an <i>immutable</i> empty List, use
+ * {@link Collections#emptyList} instead.
+ *
+ * @return a newly-created, initially-empty {@code LinkedList}
+ */
+ public static final <E> LinkedList<E> newLinkedList() {
+ return new LinkedList<E>();
+ }
+
+}
diff --git a/src/com/andrew/apollo/utils/MapUtilities.java b/src/com/andrew/apollo/utils/MapUtilities.java
deleted file mode 100644
index 1a25077..0000000
--- a/src/com/andrew/apollo/utils/MapUtilities.java
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * Copyright (c) 2012, the Last.fm Java Project and Committers
- * All rights reserved.
- *
- * Redistribution and use of this software in source and binary forms, with or without modification, are
- * permitted provided that the following conditions are met:
- *
- * - Redistributions of source code must retain the above
- * copyright notice, this list of conditions and the
- * following disclaimer.
- *
- * - Redistributions in binary form must reproduce the above
- * copyright notice, this list of conditions and the
- * following disclaimer in the documentation and/or other
- * materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
- * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
- * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
- * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
- * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.andrew.apollo.utils;
-
-import java.util.Map;
-
-/**
- * Utility class to perform various operations on Maps.
- *
- * @author Adrian Woodhead
- */
-public final class MapUtilities {
-
- private MapUtilities() {
- }
-
- /**
- * Puts the passed key and value into the map only if the value is not null.
- *
- * @param map Map to add key and value to.
- * @param key Map key.
- * @param value Map value, if null will not be added to map.
- */
- public static void nullSafePut(Map<String, String> map, String key, String value) {
- if (value != null) {
- map.put(key, value);
- }
- }
-
- /**
- * Puts the passed key and value into the map only if the value is not null.
- *
- * @param map Map to add key and value to.
- * @param key Map key.
- * @param value Map value, if null will not be added to map.
- */
- public static void nullSafePut(Map<String, String> map, String key, Integer value) {
- if (value != null) {
- map.put(key, value.toString());
- }
- }
-
- /**
- * Puts the passed key and value into the map only if the value is not -1.
- *
- * @param map Map to add key and value to.
- * @param key Map key.
- * @param value Map value, if -1 will not be added to map.
- */
- public static void nullSafePut(Map<String, String> map, String key, int value) {
- if (value != -1) {
- map.put(key, Integer.toString(value));
- }
- }
-
- /**
- * Puts the passed key and value into the map only if the value is not -1.
- *
- * @param map Map to add key and value to.
- * @param key Map key.
- * @param value Map value, if -1 will not be added to map.
- */
- public static void nullSafePut(Map<String, String> map, String key, double value) {
- if (value != -1) {
- map.put(key, Double.toString(value));
- }
- }
-}
diff --git a/src/com/andrew/apollo/utils/MusicUtils.java b/src/com/andrew/apollo/utils/MusicUtils.java
index be25227..4e6d4a2 100644
--- a/src/com/andrew/apollo/utils/MusicUtils.java
+++ b/src/com/andrew/apollo/utils/MusicUtils.java
@@ -1,15 +1,18 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.utils;
-import java.util.Arrays;
-import java.util.Formatter;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-
import android.app.Activity;
-import android.app.SearchManager;
+import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
@@ -17,280 +20,575 @@ import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.ServiceConnection;
-import android.content.SharedPreferences;
-import android.content.res.Resources;
import android.database.Cursor;
import android.net.Uri;
+import android.os.IBinder;
import android.os.RemoteException;
import android.provider.BaseColumns;
import android.provider.MediaStore;
-import android.provider.MediaStore.Audio;
import android.provider.MediaStore.Audio.AlbumColumns;
import android.provider.MediaStore.Audio.ArtistColumns;
import android.provider.MediaStore.Audio.AudioColumns;
-import android.provider.MediaStore.Audio.Genres;
-import android.provider.MediaStore.Audio.GenresColumns;
import android.provider.MediaStore.Audio.Playlists;
import android.provider.MediaStore.Audio.PlaylistsColumns;
import android.provider.MediaStore.MediaColumns;
import android.provider.Settings;
-import android.support.v4.app.FragmentActivity;
-import android.widget.ImageButton;
-import android.widget.Toast;
+import android.util.Log;
+import android.view.Menu;
+import android.view.SubMenu;
-import com.andrew.apollo.Constants;
import com.andrew.apollo.IApolloService;
+import com.andrew.apollo.MusicPlaybackService;
import com.andrew.apollo.R;
-import com.andrew.apollo.service.ApolloService;
-import com.andrew.apollo.service.ServiceBinder;
-import com.andrew.apollo.service.ServiceToken;
+import com.andrew.apollo.loaders.FavoritesLoader;
+import com.andrew.apollo.loaders.LastAddedLoader;
+import com.andrew.apollo.loaders.PlaylistLoader;
+import com.andrew.apollo.loaders.SongLoader;
+import com.andrew.apollo.menu.FragmentMenuItems;
+import com.andrew.apollo.provider.FavoritesStore;
+import com.andrew.apollo.provider.FavoritesStore.FavoriteColumns;
+import com.andrew.apollo.provider.RecentStore;
+import com.devspark.appmsg.Crouton;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.Formatter;
+import java.util.Locale;
+import java.util.WeakHashMap;
/**
- * Various methods used to help with specific music statements
+ * A collection of helpers directly related to music or Apollo's service.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
*/
-public class MusicUtils implements Constants {
-
- // Used to make number of albums/songs/time strings
- private final static StringBuilder sFormatBuilder = new StringBuilder();
-
- private final static Formatter sFormatter = new Formatter(sFormatBuilder, Locale.getDefault());
+public final class MusicUtils {
public static IApolloService mService = null;
- private static HashMap<Context, ServiceBinder> sConnectionMap = new HashMap<Context, ServiceBinder>();
+ private static final WeakHashMap<Context, ServiceBinder> mConnectionMap;
- private final static long[] sEmptyList = new long[0];
+ private static final long[] sEmptyList;
- private static final Object[] sTimeArgs = new Object[5];
+ private static ContentValues[] mContentValuesCache = null;
- private static ContentValues[] sContentValuesCache = null;
+ static {
+ mConnectionMap = new WeakHashMap<Context, ServiceBinder>();
+ sEmptyList = new long[0];
+ }
- /**
- * @param context
- * @return
- */
- public static ServiceToken bindToService(FragmentActivity context) {
- return bindToService(context, null);
+ /* This class is never initiated */
+ public MusicUtils() {
}
/**
- * @param context
- * @param callback
- * @return
+ * @param context The {@link Context} to use
+ * @param callback The {@link ServiceConnection} to use
+ * @return The new instance of {@link ServiceToken}
*/
- public static ServiceToken bindToService(Context context, ServiceConnection callback) {
+ public static final ServiceToken bindToService(final Context context,
+ final ServiceConnection callback) {
Activity realActivity = ((Activity)context).getParent();
if (realActivity == null) {
realActivity = (Activity)context;
}
- ContextWrapper cw = new ContextWrapper(realActivity);
- cw.startService(new Intent(cw, ApolloService.class));
- ServiceBinder sb = new ServiceBinder(callback);
- if (cw.bindService((new Intent()).setClass(cw, ApolloService.class), sb, 0)) {
- sConnectionMap.put(cw, sb);
- return new ServiceToken(cw);
+ final ContextWrapper contextWrapper = new ContextWrapper(realActivity);
+ contextWrapper.startService(new Intent(contextWrapper, MusicPlaybackService.class));
+ final ServiceBinder binder = new ServiceBinder(callback);
+ if (contextWrapper.bindService(
+ new Intent().setClass(contextWrapper, MusicPlaybackService.class), binder, 0)) {
+ mConnectionMap.put(contextWrapper, binder);
+ return new ServiceToken(contextWrapper);
}
return null;
}
/**
- * @param token
+ * @param token The {@link ServiceToken} to unbind from
*/
- public static void unbindFromService(ServiceToken token) {
+ public static void unbindFromService(final ServiceToken token) {
if (token == null) {
return;
}
- ContextWrapper cw = token.mWrappedContext;
- ServiceBinder sb = sConnectionMap.remove(cw);
- if (sb == null) {
+ final ContextWrapper mContextWrapper = token.mWrappedContext;
+ final ServiceBinder mBinder = mConnectionMap.remove(mContextWrapper);
+ if (mBinder == null) {
return;
}
- cw.unbindService(sb);
- if (sConnectionMap.isEmpty()) {
+ mContextWrapper.unbindService(mBinder);
+ if (mConnectionMap.isEmpty()) {
mService = null;
}
}
+ public static final class ServiceBinder implements ServiceConnection {
+ private final ServiceConnection mCallback;
+
+ /**
+ * Constructor of <code>ServiceBinder</code>
+ *
+ * @param context The {@link ServiceConnection} to use
+ */
+ public ServiceBinder(final ServiceConnection callback) {
+ mCallback = callback;
+ }
+
+ @Override
+ public void onServiceConnected(final ComponentName className, final IBinder service) {
+ mService = IApolloService.Stub.asInterface(service);
+ if (mCallback != null) {
+ mCallback.onServiceConnected(className, service);
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(final ComponentName className) {
+ if (mCallback != null) {
+ mCallback.onServiceDisconnected(className);
+ }
+ mService = null;
+ }
+ }
+
+ public static final class ServiceToken {
+ public ContextWrapper mWrappedContext;
+
+ /**
+ * Constructor of <code>ServiceToken</code>
+ *
+ * @param context The {@link ContextWrapper} to use
+ */
+ public ServiceToken(final ContextWrapper context) {
+ mWrappedContext = context;
+ }
+ }
+
/**
- * @param context
- * @param numalbums
- * @param numsongs
- * @param isUnknown
- * @return a string based on the number of albums for an artist or songs for
- * an album
+ * Used to make number of labels for the number of artists, albums, songs,
+ * genres, and playlists.
+ *
+ * @param context The {@link Context} to use.
+ * @param pluralInt The ID of the plural string to use.
+ * @param number The number of artists, albums, songs, genres, or playlists.
+ * @return A {@link String} used as a label for the number of artists,
+ * albums, songs, genres, and playlists.
*/
- public static String makeAlbumsLabel(Context mContext, int numalbums, int numsongs,
- boolean isUnknown) {
+ public static final String makeLabel(final Context context, final int pluralInt,
+ final String number) {
+ try {
+ final StringBuilder formatBuilder = new StringBuilder();
+ final Formatter formatter = new Formatter(formatBuilder, Locale.getDefault());
+ final StringBuilder builder = new StringBuilder();
+ final String quantity = context.getResources()
+ .getQuantityText(pluralInt, Integer.valueOf(number)).toString();
+ formatBuilder.setLength(0);
+ formatter.format(quantity, Integer.valueOf(number));
+ builder.append(formatBuilder);
+ final String label = builder.toString();
+ return label != null ? label : null;
+ } catch (final IndexOutOfBoundsException fixme) {
+ return null;
+ }
+ }
- StringBuilder songs_albums = new StringBuilder();
+ /**
+ * * Used to create a formatted time string for the duration of tracks.
+ *
+ * @param context The {@link Context} to use.
+ * @param secs The track in seconds.
+ * @return Duration of a track that's properly formatted.
+ */
+ public static final String makeTimeString(final Context context, final long secs) {
+ final StringBuilder formatBuilder = new StringBuilder();
+ final Formatter formatter = new Formatter(formatBuilder, Locale.getDefault());
+ final String durationFormat = context.getResources().getString(
+ secs < 3600 ? R.string.durationformatshort : R.string.durationformatlong);
+ final Object[] mTimeArgs = new Object[5];
+ formatBuilder.setLength(0);
+ mTimeArgs[0] = secs / 3600;
+ mTimeArgs[1] = secs / 60;
+ mTimeArgs[2] = secs / 60 % 60;
+ mTimeArgs[3] = secs;
+ mTimeArgs[4] = secs % 60;
+ final String mTime = formatter.format(durationFormat, mTimeArgs).toString();
+ return mTime;
+ }
- Resources r = mContext.getResources();
- if (isUnknown) {
- String f = r.getQuantityText(R.plurals.Nsongs, numsongs).toString();
- sFormatBuilder.setLength(0);
- sFormatter.format(f, Integer.valueOf(numsongs));
- songs_albums.append(sFormatBuilder);
- } else {
- String f = r.getQuantityText(R.plurals.Nalbums, numalbums).toString();
- sFormatBuilder.setLength(0);
- sFormatter.format(f, Integer.valueOf(numalbums));
- songs_albums.append(sFormatBuilder);
- songs_albums.append("\n");
+ /**
+ * Changes to the next track
+ */
+ public static void next() {
+ try {
+ if (mService != null) {
+ mService.next();
+ }
+ } catch (final RemoteException ignored) {
}
- return songs_albums.toString();
}
/**
- * @param mContext
- * @return
+ * Changes to the previous track.
+ *
+ * @NOTE The AIDL isn't used here in order to properly use the previous
+ * action. When the user is shuffling, because {@link
+ * MusicPlaybackService.#openCurrentAndNext()} is used, the user won't
+ * be able to travel to the previously skipped track. To remedy this,
+ * {@link MusicPlaybackService.#openCurrent()} is called in {@link
+ * MusicPlaybackService.#prev()}. {@code #startService(Intent intent)}
+ * is called here to specifically invoke the onStartCommand used by
+ * {@link MusicPlaybackService}, which states if the current position
+ * less than 2000 ms, start the track over, otherwise move to the
+ * previously listened track.
*/
- public static int getCardId(Context mContext) {
+ public static void previous(final Context context) {
+ final Intent previous = new Intent(context, MusicPlaybackService.class);
+ previous.setAction(MusicPlaybackService.PREVIOUS_ACTION);
+ context.startService(previous);
+ }
- ContentResolver res = mContext.getContentResolver();
- Cursor c = res.query(Uri.parse("content://media/external/fs_id"), null, null, null, null);
- int id = -1;
- if (c != null) {
- c.moveToFirst();
- id = c.getInt(0);
- c.close();
+ /**
+ * Plays or pauses the music.
+ */
+ public static void playOrPause() {
+ try {
+ if (mService != null) {
+ if (mService.isPlaying()) {
+ mService.pause();
+ } else {
+ mService.play();
+ }
+ }
+ } catch (final Exception ignored) {
+ }
+ }
+
+ /**
+ * Cycles through the repeat options.
+ */
+ public static void cycleRepeat() {
+ try {
+ if (mService != null) {
+ switch (mService.getRepeatMode()) {
+ case MusicPlaybackService.REPEAT_NONE:
+ mService.setRepeatMode(MusicPlaybackService.REPEAT_ALL);
+ break;
+ case MusicPlaybackService.REPEAT_ALL:
+ mService.setRepeatMode(MusicPlaybackService.REPEAT_CURRENT);
+ if (mService.getShuffleMode() != MusicPlaybackService.SHUFFLE_NONE) {
+ mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NONE);
+ }
+ break;
+ default:
+ mService.setRepeatMode(MusicPlaybackService.REPEAT_NONE);
+ break;
+ }
+ }
+ } catch (final RemoteException ignored) {
}
- return id;
}
/**
- * @param context
- * @param uri
- * @param projection
- * @param selection
- * @param selectionArgs
- * @param sortOrder
- * @param limit
- * @return
+ * Cycles through the shuffle options.
*/
- public static Cursor query(Context context, Uri uri, String[] projection, String selection,
- String[] selectionArgs, String sortOrder, int limit) {
+ public static void cycleShuffle() {
try {
- ContentResolver resolver = context.getContentResolver();
- if (resolver == null) {
- return null;
+ if (mService != null) {
+ switch (mService.getShuffleMode()) {
+ case MusicPlaybackService.SHUFFLE_NONE:
+ mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NORMAL);
+ if (mService.getRepeatMode() == MusicPlaybackService.REPEAT_CURRENT) {
+ mService.setRepeatMode(MusicPlaybackService.REPEAT_ALL);
+ }
+ break;
+ case MusicPlaybackService.SHUFFLE_NORMAL:
+ mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NONE);
+ break;
+ case MusicPlaybackService.SHUFFLE_AUTO:
+ mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NONE);
+ break;
+ default:
+ break;
+ }
}
- if (limit > 0) {
- uri = uri.buildUpon().appendQueryParameter("limit", "" + limit).build();
+ } catch (final RemoteException ignored) {
+ }
+ }
+
+ /**
+ * @return True if we're playing music, false otherwise.
+ */
+ public static final boolean isPlaying() {
+ if (mService != null) {
+ try {
+ return mService.isPlaying();
+ } catch (final RemoteException ignored) {
}
- return resolver.query(uri, projection, selection, selectionArgs, sortOrder);
- } catch (UnsupportedOperationException ex) {
- return null;
}
+ return false;
}
/**
- * @param context
- * @param uri
- * @param projection
- * @param selection
- * @param selectionArgs
- * @param sortOrder
- * @return
+ * @return The current shuffle mode.
*/
- public static Cursor query(Context context, Uri uri, String[] projection, String selection,
- String[] selectionArgs, String sortOrder) {
- return query(context, uri, projection, selection, selectionArgs, sortOrder, 0);
+ public static final int getShuffleMode() {
+ if (mService != null) {
+ try {
+ return mService.getShuffleMode();
+ } catch (final RemoteException ignored) {
+ }
+ }
+ return 0;
}
/**
- * @param context
- * @param cursor
+ * @return The current repeat mode.
*/
- public static void shuffleAll(Context context, Cursor cursor) {
- playAll(context, cursor, 0, true);
+ public static final int getRepeatMode() {
+ if (mService != null) {
+ try {
+ return mService.getRepeatMode();
+ } catch (final RemoteException ignored) {
+ }
+ }
+ return 0;
}
/**
- * @param context
- * @param cursor
+ * @return The current track name.
*/
- public static void playAll(Context context, Cursor cursor) {
- playAll(context, cursor, 0, false);
+ public static final String getTrackName() {
+ if (mService != null) {
+ try {
+ return mService.getTrackName();
+ } catch (final RemoteException ignored) {
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @return The current artist name.
+ */
+ public static final String getArtistName() {
+ if (mService != null) {
+ try {
+ return mService.getArtistName();
+ } catch (final RemoteException ignored) {
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @return The current album name.
+ */
+ public static final String getAlbumName() {
+ if (mService != null) {
+ try {
+ return mService.getAlbumName();
+ } catch (final RemoteException ignored) {
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @return The current album Id.
+ */
+ public static final long getCurrentAlbumId() {
+ if (mService != null) {
+ try {
+ return mService.getAlbumId();
+ } catch (final RemoteException ignored) {
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * @return The current song Id.
+ */
+ public static final long getCurrentAudioId() {
+ if (mService != null) {
+ try {
+ return mService.getAudioId();
+ } catch (final RemoteException ignored) {
+ }
+ }
+ return -1;
}
/**
- * @param context
- * @param cursor
- * @param position
+ * @return The current artist Id.
*/
- public static void playAll(Context context, Cursor cursor, int position) {
- playAll(context, cursor, position, false);
+ public static final long getCurrentArtistId() {
+ if (mService != null) {
+ try {
+ return mService.getArtistId();
+ } catch (final RemoteException ignored) {
+ }
+ }
+ return -1;
}
/**
- * @param context
- * @param list
- * @param position
+ * @return The queue.
*/
- public static void playAll(Context context, long[] list, int position) {
- playAll(context, list, position, false);
+ public static final long[] getQueue() {
+ try {
+ if (mService != null) {
+ return mService.getQueue();
+ } else {
+ }
+ } catch (final RemoteException ignored) {
+ }
+ return sEmptyList;
}
/**
- * @param context
- * @param cursor
- * @param position
- * @param force_shuffle
+ * @param id The ID of the track to remove.
+ * @return removes track from a playlist or the queue.
*/
- private static void playAll(Context context, Cursor cursor, int position, boolean force_shuffle) {
+ public static final int removeTrack(final long id) {
+ try {
+ if (mService != null) {
+ return mService.removeTrack(id);
+ }
+ } catch (final RemoteException ingored) {
+ }
+ return 0;
+ }
- long[] list = getSongListForCursor(cursor);
- playAll(context, list, position, force_shuffle);
+ /**
+ * @return The position of the current track in the queue.
+ */
+ public static final int getQueuePosition() {
+ try {
+ if (mService != null) {
+ return mService.getQueuePosition();
+ }
+ } catch (final RemoteException ignored) {
+ }
+ return 0;
}
/**
- * @param cursor
- * @return
+ * @param cursor The {@link Cursor} used to perform our query.
+ * @return The song list for a MIME type.
*/
- public static long[] getSongListForCursor(Cursor cursor) {
+ public static final long[] getSongListForCursor(Cursor cursor) {
if (cursor == null) {
return sEmptyList;
}
- int len = cursor.getCount();
- long[] list = new long[len];
+ final int len = cursor.getCount();
+ final long[] list = new long[len];
cursor.moveToFirst();
- int colidx = -1;
+ int columnIndex = -1;
try {
- colidx = cursor.getColumnIndexOrThrow(Audio.Playlists.Members.AUDIO_ID);
- } catch (IllegalArgumentException ex) {
- colidx = cursor.getColumnIndexOrThrow(BaseColumns._ID);
+ columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Playlists.Members.AUDIO_ID);
+ } catch (final IllegalArgumentException notaplaylist) {
+ columnIndex = cursor.getColumnIndexOrThrow(BaseColumns._ID);
}
for (int i = 0; i < len; i++) {
- list[i] = cursor.getLong(colidx);
+ list[i] = cursor.getLong(columnIndex);
cursor.moveToNext();
}
+ cursor.close();
+ cursor = null;
return list;
}
/**
- * @param context
- * @param list
- * @param position
- * @param force_shuffle
+ * @param context The {@link Context} to use.
+ * @param id The ID of the artist.
+ * @return The song list for an artist.
+ */
+ public static final long[] getSongListForArtist(final Context context, final String id) {
+ final String[] projection = new String[] {
+ BaseColumns._ID
+ };
+ final String selection = AudioColumns.ARTIST_ID + "=" + id + " AND "
+ + AudioColumns.IS_MUSIC + "=1";
+ Cursor cursor = context.getContentResolver().query(
+ MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection, null,
+ AudioColumns.ALBUM_KEY + "," + AudioColumns.TRACK);
+ if (cursor != null) {
+ final long[] mList = getSongListForCursor(cursor);
+ cursor.close();
+ cursor = null;
+ return mList;
+ }
+ return sEmptyList;
+ }
+
+ /**
+ * @param context The {@link Context} to use.
+ * @param id The ID of the album.
+ * @return The song list for an album.
+ */
+ public static final long[] getSongListForAlbum(final Context context, final String id) {
+ final String[] projection = new String[] {
+ BaseColumns._ID
+ };
+ final String selection = AudioColumns.ALBUM_ID + "=" + id + " AND " + AudioColumns.IS_MUSIC
+ + "=1";
+ Cursor cursor = context.getContentResolver().query(
+ MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection, null,
+ AudioColumns.TRACK + ", " + MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
+ if (cursor != null) {
+ final long[] mList = getSongListForCursor(cursor);
+ cursor.close();
+ cursor = null;
+ return mList;
+ }
+ return sEmptyList;
+ }
+
+ /**
+ * @param context The {@link Context} to use.
+ * @param id The ID of the genre.
+ * @return The song list for an genre.
+ */
+ public static final long[] getSongListForGenre(final Context context, final String id) {
+ final String[] projection = new String[] {
+ BaseColumns._ID
+ };
+ final StringBuilder selection = new StringBuilder();
+ selection.append(AudioColumns.IS_MUSIC + "=1");
+ selection.append(" AND " + MediaColumns.TITLE + "!=''");
+ final Uri uri = MediaStore.Audio.Genres.Members.getContentUri("external", Long.valueOf(id));
+ Cursor cursor = context.getContentResolver().query(uri, projection, selection.toString(),
+ null, null);
+ if (cursor != null) {
+ final long[] mList = getSongListForCursor(cursor);
+ cursor.close();
+ cursor = null;
+ return mList;
+ }
+ return sEmptyList;
+ }
+
+ /**
+ * @param context The {@link Context} to use.
+ * @param list The list of songs to play.
+ * @param position Specify where to start.
+ * @param forceShuffle True to force a shuffle, false otherwise.
*/
- private static void playAll(Context context, long[] list, int position, boolean force_shuffle) {
+ public static void playAll(final Context context, final long[] list, int position,
+ final boolean forceShuffle) {
if (list.length == 0 || mService == null) {
return;
}
try {
- if (force_shuffle) {
- mService.setShuffleMode(ApolloService.SHUFFLE_NORMAL);
+ if (forceShuffle) {
+ mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NORMAL);
+ } else {
+ mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NONE);
}
- long curid = mService.getAudioId();
- int curpos = mService.getQueuePosition();
- if (position != -1 && curpos == position && curid == list[position]) {
- // The selected file is the file that's currently playing;
- // figure out if we need to restart with a new playlist,
- // or just launch the playback activity.
- long[] playlist = mService.getQueue();
+ final long currentId = mService.getAudioId();
+ final int currentQueuePosition = getQueuePosition();
+ if (position != -1 && currentQueuePosition == position && currentId == list[position]) {
+ final long[] playlist = getQueue();
if (Arrays.equals(list, playlist)) {
- // we don't need to set a new list, but we should resume
- // playback if needed
mService.play();
return;
}
@@ -298,861 +596,750 @@ public class MusicUtils implements Constants {
if (position < 0) {
position = 0;
}
- mService.open(list, force_shuffle ? -1 : position);
+ mService.open(list, forceShuffle ? -1 : position);
mService.play();
- } catch (RemoteException ex) {
- ex.printStackTrace();
+ } catch (final RemoteException ignored) {
}
}
/**
- * @return
+ * @param list The list to enqueue.
*/
- public static long[] getQueue() {
-
- if (mService == null)
- return sEmptyList;
-
+ public static void playNext(final long[] list) {
+ if (mService == null) {
+ return;
+ }
try {
- return mService.getQueue();
- } catch (RemoteException e) {
- e.printStackTrace();
+ mService.enqueue(list, MusicPlaybackService.NEXT);
+ } catch (final RemoteException ignored) {
}
- return sEmptyList;
}
/**
- * @param context
- * @param name
- * @param def
- * @return number of weeks used to create the Recent tab
+ * @param context The {@link Context} to use.
*/
- public static int getIntPref(Context context, String name, int def) {
- SharedPreferences prefs = context.getSharedPreferences(context.getPackageName(),
- Context.MODE_PRIVATE);
- return prefs.getInt(name, def);
+ public static void shuffleAll(final Context context) {
+ Cursor cursor = SongLoader.makeSongCursor(context);
+ final long[] mTrackList = getSongListForCursor(cursor);
+ final int position = 0;
+ if (mTrackList.length == 0 || mService == null) {
+ return;
+ }
+ try {
+ mService.setShuffleMode(MusicPlaybackService.SHUFFLE_NORMAL);
+ final long mCurrentId = mService.getAudioId();
+ final int mCurrentQueuePosition = getQueuePosition();
+ if (position != -1 && mCurrentQueuePosition == position
+ && mCurrentId == mTrackList[position]) {
+ final long[] mPlaylist = getQueue();
+ if (Arrays.equals(mTrackList, mPlaylist)) {
+ mService.play();
+ return;
+ }
+ }
+ mService.open(mTrackList, -1);
+ mService.play();
+ cursor.close();
+ cursor = null;
+ } catch (final RemoteException ignored) {
+ }
}
/**
- * @param context
- * @param id
- * @return
+ * Returns The ID for a playlist.
+ *
+ * @param context The {@link Context} to use.
+ * @param name The name of the playlist.
+ * @return The ID for a playlist.
*/
- public static long[] getSongListForArtist(Context context, long id) {
- final String[] projection = new String[] {
- BaseColumns._ID
- };
- String selection = AudioColumns.ARTIST_ID + "=" + id + " AND " + AudioColumns.IS_MUSIC
- + "=1";
- String sortOrder = AudioColumns.ALBUM_KEY + "," + AudioColumns.TRACK;
- Uri uri = Audio.Media.EXTERNAL_CONTENT_URI;
- Cursor cursor = query(context, uri, projection, selection, null, sortOrder);
+ public static final long getIdForPlaylist(final Context context, final String name) {
+ Cursor cursor = context.getContentResolver().query(
+ MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, new String[] {
+ BaseColumns._ID
+ }, PlaylistsColumns.NAME + "=?", new String[] {
+ name
+ }, PlaylistsColumns.NAME);
+ int id = -1;
if (cursor != null) {
- long[] list = getSongListForCursor(cursor);
+ cursor.moveToFirst();
+ if (!cursor.isAfterLast()) {
+ id = cursor.getInt(0);
+ }
cursor.close();
- return list;
+ cursor = null;
}
- return sEmptyList;
+ return id;
}
/**
- * @param context
- * @param id
- * @return
+ * Returns the Id for an artist.
+ *
+ * @param context The {@link Context} to use.
+ * @param name The name of the artist.
+ * @return The ID for an artist.
*/
- public static long[] getSongListForAlbum(Context context, long id) {
- final String[] projection = new String[] {
- BaseColumns._ID
- };
- String selection = AudioColumns.ALBUM_ID + "=" + id + " AND " + AudioColumns.IS_MUSIC
- + "=1";
- String sortOrder = AudioColumns.TRACK;
- Uri uri = Audio.Media.EXTERNAL_CONTENT_URI;
- Cursor cursor = query(context, uri, projection, selection, null, sortOrder);
+ public static final long getIdForArtist(final Context context, final String name) {
+ Cursor cursor = context.getContentResolver().query(
+ MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI, new String[] {
+ BaseColumns._ID
+ }, ArtistColumns.ARTIST + "=?", new String[] {
+ name
+ }, ArtistColumns.ARTIST);
+ int id = -1;
if (cursor != null) {
- long[] list = getSongListForCursor(cursor);
+ cursor.moveToFirst();
+ if (!cursor.isAfterLast()) {
+ id = cursor.getInt(0);
+ }
cursor.close();
- return list;
+ cursor = null;
}
- return sEmptyList;
+ return id;
}
/**
- * @param context
- * @param id
- * @return
+ * Returns the ID for an album.
+ *
+ * @param context The {@link Context} to use.
+ * @param name The name of the album.
+ * @return The ID for an album.
*/
- public static long[] getSongListForGenre(Context context, long id) {
- String[] projection = new String[] {
- BaseColumns._ID
- };
- StringBuilder selection = new StringBuilder();
- selection.append(AudioColumns.IS_MUSIC + "=1");
- selection.append(" AND " + MediaColumns.TITLE + "!=''");
- Uri uri = Genres.Members.getContentUri(EXTERNAL, id);
- Cursor cursor = context.getContentResolver().query(uri, projection, selection.toString(),
- null, null);
+ public static final long getIdForAlbum(final Context context, final String name) {
+ Cursor cursor = context.getContentResolver().query(
+ MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, new String[] {
+ BaseColumns._ID
+ }, AlbumColumns.ALBUM + "=?", new String[] {
+ name
+ }, AlbumColumns.ALBUM);
+ int id = -1;
if (cursor != null) {
- long[] list = getSongListForCursor(cursor);
+ cursor.moveToFirst();
+ if (!cursor.isAfterLast()) {
+ id = cursor.getInt(0);
+ }
cursor.close();
- return list;
+ cursor = null;
}
- return sEmptyList;
+ return id;
}
/**
- * @param context
- * @param id
- * @return
+ * Returns the artist name for a album.
+ *
+ * @param context The {@link Context} to use.
+ * @param name The name of the album.
+ * @return The artist for an album.
*/
- public static long[] getSongListForPlaylist(Context context, long id) {
- final String[] projection = new String[] {
- Audio.Playlists.Members.AUDIO_ID
- };
- String sortOrder = Playlists.Members.DEFAULT_SORT_ORDER;
- Uri uri = Playlists.Members.getContentUri(EXTERNAL, id);
- Cursor cursor = query(context, uri, projection, null, null, sortOrder);
+ public static final String getAlbumArtist(final Context context, final String name) {
+ Cursor cursor = context.getContentResolver().query(
+ MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, new String[] {
+ AlbumColumns.ARTIST
+ }, AlbumColumns.ALBUM + "=?", new String[] {
+ name
+ }, AlbumColumns.ALBUM);
+ String artistName = null;
if (cursor != null) {
- long[] list = getSongListForCursor(cursor);
+ cursor.moveToFirst();
+ if (!cursor.isAfterLast()) {
+ artistName = cursor.getString(0);
+ }
cursor.close();
- return list;
+ cursor = null;
+ }
+ return artistName;
+ }
+
+ /* */
+ public static void makeInsertItems(final long[] ids, final int offset, int len, final int base) {
+ if (offset + len > ids.length) {
+ len = ids.length - offset;
+ }
+
+ if (mContentValuesCache == null || mContentValuesCache.length != len) {
+ mContentValuesCache = new ContentValues[len];
+ }
+ for (int i = 0; i < len; i++) {
+ if (mContentValuesCache[i] == null) {
+ mContentValuesCache[i] = new ContentValues();
+ }
+ mContentValuesCache[i].put(Playlists.Members.PLAY_ORDER, base + offset + i);
+ mContentValuesCache[i].put(Playlists.Members.AUDIO_ID, ids[offset + i]);
}
- return sEmptyList;
}
/**
- * @param context
- * @param name
- * @return
+ * @param context The {@link Context} to use.
+ * @param name The name of the new playlist.
+ * @return A new playlist ID.
*/
- public static long createPlaylist(Context context, String name) {
-
+ public static final long createPlaylist(final Context context, final String name) {
if (name != null && name.length() > 0) {
- ContentResolver resolver = context.getContentResolver();
- String[] cols = new String[] {
+ final ContentResolver resolver = context.getContentResolver();
+ final String[] projection = new String[] {
PlaylistsColumns.NAME
};
- String whereclause = PlaylistsColumns.NAME + " = '" + name + "'";
- Cursor cur = resolver.query(Audio.Playlists.EXTERNAL_CONTENT_URI, cols, whereclause,
- null, null);
- if (cur.getCount() <= 0) {
- ContentValues values = new ContentValues(1);
+ final String selection = PlaylistsColumns.NAME + " = '" + name + "'";
+ Cursor cursor = resolver.query(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
+ projection, selection, null, null);
+ if (cursor.getCount() <= 0) {
+ final ContentValues values = new ContentValues(1);
values.put(PlaylistsColumns.NAME, name);
- Uri uri = resolver.insert(Audio.Playlists.EXTERNAL_CONTENT_URI, values);
+ final Uri uri = resolver.insert(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
+ values);
return Long.parseLong(uri.getLastPathSegment());
}
+ if (cursor != null) {
+ cursor.close();
+ cursor = null;
+ }
return -1;
}
return -1;
}
/**
- * @param context
- * @return
+ * @param context The {@link Context} to use.
+ * @param playlistId The playlist ID.
*/
- public static long getFavoritesId(Context context) {
- long favorites_id = -1;
- String favorites_where = PlaylistsColumns.NAME + "='" + "Favorites" + "'";
- String[] favorites_cols = new String[] {
- BaseColumns._ID
+ public static void clearPlaylist(final Context context, final int playlistId) {
+ final Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId);
+ context.getContentResolver().delete(uri, null, null);
+ return;
+ }
+
+ /**
+ * @param context The {@link Context} to use.
+ * @param ids The id of the song(s) to add.
+ * @param playlistid The id of the playlist being added to.
+ */
+ public static void addToPlaylist(final Context context, final long[] ids, final long playlistid) {
+ final int size = ids.length;
+ final ContentResolver resolver = context.getContentResolver();
+ final String[] projection = new String[] {
+ "count(*)"
};
- Uri favorites_uri = Audio.Playlists.EXTERNAL_CONTENT_URI;
- Cursor cursor = query(context, favorites_uri, favorites_cols, favorites_where, null, null);
- if (cursor.getCount() <= 0) {
- favorites_id = createPlaylist(context, "Favorites");
- } else {
- cursor.moveToFirst();
- favorites_id = cursor.getLong(0);
- cursor.close();
+ final Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistid);
+ Cursor cursor = resolver.query(uri, projection, null, null, null);
+ cursor.moveToFirst();
+ final int base = cursor.getInt(0);
+ cursor.close();
+ cursor = null;
+ int numinserted = 0;
+ for (int offSet = 0; offSet < size; offSet += 1000) {
+ makeInsertItems(ids, offSet, 1000, base);
+ numinserted += resolver.bulkInsert(uri, mContentValuesCache);
}
- return favorites_id;
+ final String message = context.getResources().getQuantityString(
+ R.plurals.NNNtrackstoplaylist, numinserted, numinserted);
+ Crouton.makeText((Activity)context, message, Crouton.STYLE_CONFIRM).show();
}
/**
- * @param context
- * @param id
+ * @param context The {@link Context} to use.
+ * @param list The list to enqueue.
*/
- public static void setRingtone(Context context, long id) {
- ContentResolver resolver = context.getContentResolver();
- // Set the flag in the database to mark this as a ringtone
- Uri ringUri = ContentUris.withAppendedId(Audio.Media.EXTERNAL_CONTENT_URI, id);
+ public static void addToQueue(final Context context, final long[] list) {
+ if (mService == null) {
+ return;
+ }
try {
- ContentValues values = new ContentValues(2);
+ mService.enqueue(list, MusicPlaybackService.LAST);
+ final String message = context.getResources().getQuantityString(
+ R.plurals.NNNtrackstoqueue, list.length, Integer.valueOf(list.length));
+ Crouton.makeText((Activity)context, message, Crouton.STYLE_CONFIRM).show();
+ } catch (final RemoteException ignored) {
+ }
+ }
+
+ /**
+ * @param context The {@link Context} to use
+ * @param id The song ID.
+ */
+ public static void setRingtone(final Context context, final long id) {
+ final ContentResolver resolver = context.getContentResolver();
+ final Uri uri = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id);
+ try {
+ final ContentValues values = new ContentValues(2);
values.put(AudioColumns.IS_RINGTONE, "1");
values.put(AudioColumns.IS_ALARM, "1");
- resolver.update(ringUri, values, null, null);
- } catch (UnsupportedOperationException ex) {
- // most likely the card just got unmounted
+ resolver.update(uri, values, null, null);
+ } catch (final UnsupportedOperationException ingored) {
return;
}
- String[] cols = new String[] {
+ final String[] projection = new String[] {
BaseColumns._ID, MediaColumns.DATA, MediaColumns.TITLE
};
- String where = BaseColumns._ID + "=" + id;
- Cursor cursor = query(context, Audio.Media.EXTERNAL_CONTENT_URI, cols, where, null, null);
+ final String selection = BaseColumns._ID + "=" + id;
+ Cursor cursor = resolver.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection,
+ selection, null, null);
try {
if (cursor != null && cursor.getCount() == 1) {
- // Set the system setting to make this the current ringtone
cursor.moveToFirst();
- Settings.System.putString(resolver, Settings.System.RINGTONE, ringUri.toString());
- String message = context.getString(R.string.set_as_ringtone, cursor.getString(2));
- Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
+ Settings.System.putString(resolver, Settings.System.RINGTONE, uri.toString());
+ final String message = context.getString(R.string.set_as_ringtone,
+ cursor.getString(2));
+ Crouton.makeText((Activity)context, message, Crouton.STYLE_CONFIRM).show();
}
} finally {
if (cursor != null) {
cursor.close();
+ cursor = null;
}
}
}
/**
- * @param context
- * @param plid
+ * @param context The {@link Context} to use.
+ * @param name The name of the album.
+ * @return The song count for an album.
*/
- public static void clearPlaylist(Context context, int plid) {
- Uri uri = Audio.Playlists.Members.getContentUri(EXTERNAL, plid);
- context.getContentResolver().delete(uri, null, null);
- return;
- }
-
- /**
- * @param context
- * @param ids
- * @param playlistid
- */
- public static void addToPlaylist(Context context, long[] ids, long playlistid) {
-
- if (ids == null) {
- } else {
- int size = ids.length;
- ContentResolver resolver = context.getContentResolver();
- // need to determine the number of items currently in the playlist,
- // so the play_order field can be maintained.
- String[] cols = new String[] {
- "count(*)"
- };
- Uri uri = Audio.Playlists.Members.getContentUri(EXTERNAL, playlistid);
- Cursor cur = resolver.query(uri, cols, null, null, null);
- cur.moveToFirst();
- int base = cur.getInt(0);
- cur.close();
- int numinserted = 0;
- for (int i = 0; i < size; i += 1000) {
- makeInsertItems(ids, i, 1000, base);
- numinserted += resolver.bulkInsert(uri, sContentValuesCache);
+ public static final String getSongCountForAlbum(final Context context, final String name) {
+ if (name == null) {
+ return null;
+ }
+ Cursor cursor = context.getContentResolver().query(
+ MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, new String[] {
+ AlbumColumns.NUMBER_OF_SONGS
+ }, AlbumColumns.ALBUM + "=?", new String[] {
+ name
+ }, AlbumColumns.ALBUM);
+ String songCount = null;
+ if (cursor != null) {
+ cursor.moveToFirst();
+ if (!cursor.isAfterLast()) {
+ songCount = cursor.getString(0);
}
- String message = context.getResources().getQuantityString(
- R.plurals.NNNtrackstoplaylist, numinserted, numinserted);
- Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
+ cursor.close();
+ cursor = null;
}
+ return songCount;
}
/**
- * @param ids
- * @param offset
- * @param len
- * @param base
+ * @param context The {@link Context} to use.
+ * @param name The name of the album.
+ * @return The release date for an album.
*/
- private static void makeInsertItems(long[] ids, int offset, int len, int base) {
-
- // adjust 'len' if would extend beyond the end of the source array
- if (offset + len > ids.length) {
- len = ids.length - offset;
- }
- // allocate the ContentValues array, or reallocate if it is the wrong
- // size
- if (sContentValuesCache == null || sContentValuesCache.length != len) {
- sContentValuesCache = new ContentValues[len];
+ public static final String getReleaseDateForAlbum(final Context context, final String name) {
+ if (name == null) {
+ return null;
}
- // fill in the ContentValues array with the right values for this pass
- for (int i = 0; i < len; i++) {
- if (sContentValuesCache[i] == null) {
- sContentValuesCache[i] = new ContentValues();
+ Cursor cursor = context.getContentResolver().query(
+ MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, new String[] {
+ AlbumColumns.FIRST_YEAR
+ }, AlbumColumns.ALBUM + "=?", new String[] {
+ name
+ }, AlbumColumns.ALBUM);
+ String releaseDate = null;
+ if (cursor != null) {
+ cursor.moveToFirst();
+ if (!cursor.isAfterLast()) {
+ releaseDate = cursor.getString(0);
}
-
- sContentValuesCache[i].put(Playlists.Members.PLAY_ORDER, base + offset + i);
- sContentValuesCache[i].put(Playlists.Members.AUDIO_ID, ids[offset + i]);
+ cursor.close();
+ cursor = null;
}
+ return releaseDate;
}
/**
- * Toggle favorites
+ * @return The path to the currently playing file as {@link String}
*/
- public static void toggleFavorite() {
-
- if (mService == null)
- return;
+ public static final String getFilePath() {
try {
- mService.toggleFavorite();
- } catch (RemoteException e) {
- e.printStackTrace();
+ if (mService != null) {
+ return mService.getPath();
+ }
+ } catch (final RemoteException ignored) {
}
+ return null;
}
/**
- * @param context
- * @param id
+ * @param from The index the item is currently at.
+ * @param to The index the item is moving to.
*/
- public static void addToFavorites(Context context, long id) {
-
- long favorites_id;
-
- if (id < 0) {
-
- } else {
- ContentResolver resolver = context.getContentResolver();
-
- String favorites_where = PlaylistsColumns.NAME + "='" + PLAYLIST_NAME_FAVORITES + "'";
- String[] favorites_cols = new String[] {
- BaseColumns._ID
- };
- Uri favorites_uri = Audio.Playlists.EXTERNAL_CONTENT_URI;
- Cursor cursor = resolver.query(favorites_uri, favorites_cols, favorites_where, null,
- null);
- if (cursor.getCount() <= 0) {
- favorites_id = createPlaylist(context, PLAYLIST_NAME_FAVORITES);
+ public static void moveQueueItem(final int from, final int to) {
+ try {
+ if (mService != null) {
+ mService.moveQueueItem(from, to);
} else {
- cursor.moveToFirst();
- favorites_id = cursor.getLong(0);
- cursor.close();
}
-
- String[] cols = new String[] {
- Playlists.Members.AUDIO_ID
- };
- Uri uri = Playlists.Members.getContentUri(EXTERNAL, favorites_id);
- Cursor cur = resolver.query(uri, cols, null, null, null);
-
- int base = cur.getCount();
- cur.moveToFirst();
- while (!cur.isAfterLast()) {
- if (cur.getLong(0) == id)
- return;
- cur.moveToNext();
- }
- cur.close();
-
- ContentValues values = new ContentValues();
- values.put(Playlists.Members.AUDIO_ID, id);
- values.put(Playlists.Members.PLAY_ORDER, base + 1);
- resolver.insert(uri, values);
+ } catch (final RemoteException ignored) {
}
}
/**
- * @param context
- * @param id
- * @return
+ * Toggles the current song as a favorite.
*/
- public static boolean isFavorite(Context context, long id) {
-
- long favorites_id;
-
- if (id < 0) {
-
- } else {
- ContentResolver resolver = context.getContentResolver();
-
- String favorites_where = PlaylistsColumns.NAME + "='" + PLAYLIST_NAME_FAVORITES + "'";
- String[] favorites_cols = new String[] {
- BaseColumns._ID
- };
- Uri favorites_uri = Audio.Playlists.EXTERNAL_CONTENT_URI;
- Cursor cursor = resolver.query(favorites_uri, favorites_cols, favorites_where, null,
- null);
- if (cursor.getCount() <= 0) {
- favorites_id = createPlaylist(context, PLAYLIST_NAME_FAVORITES);
- } else {
- cursor.moveToFirst();
- favorites_id = cursor.getLong(0);
- cursor.close();
- }
-
- String[] cols = new String[] {
- Playlists.Members.AUDIO_ID
- };
- Uri uri = Playlists.Members.getContentUri(EXTERNAL, favorites_id);
- Cursor cur = resolver.query(uri, cols, null, null, null);
-
- cur.moveToFirst();
- while (!cur.isAfterLast()) {
- if (cur.getLong(0) == id) {
- cur.close();
- return true;
- }
- cur.moveToNext();
+ public static void toggleFavorite() {
+ try {
+ if (mService != null) {
+ mService.toggleFavorite();
}
- cur.close();
- return false;
+ } catch (final RemoteException ignored) {
}
- return false;
}
/**
- * @param context
- * @param id
+ * @return True if the current song is a favorite, false otherwise.
*/
- public static void removeFromFavorites(Context context, long id) {
- long favorites_id;
- if (id < 0) {
- } else {
- ContentResolver resolver = context.getContentResolver();
- String favorites_where = PlaylistsColumns.NAME + "='" + PLAYLIST_NAME_FAVORITES + "'";
- String[] favorites_cols = new String[] {
- BaseColumns._ID
- };
- Uri favorites_uri = Audio.Playlists.EXTERNAL_CONTENT_URI;
- Cursor cursor = resolver.query(favorites_uri, favorites_cols, favorites_where, null,
- null);
- if (cursor.getCount() <= 0) {
- favorites_id = createPlaylist(context, PLAYLIST_NAME_FAVORITES);
- } else {
- cursor.moveToFirst();
- favorites_id = cursor.getLong(0);
- cursor.close();
+ public static final boolean isFavorite() {
+ try {
+ if (mService != null) {
+ return mService.isFavorite();
}
- Uri uri = Playlists.Members.getContentUri(EXTERNAL, favorites_id);
- resolver.delete(uri, Playlists.Members.AUDIO_ID + "=" + id, null);
+ } catch (final RemoteException ignored) {
}
+ return false;
}
/**
- * @param mService
- * @param mImageButton
- * @param id
+ * @param context The {@link Context} to sue
+ * @param playlistId The playlist Id
+ * @return The track list for a playlist
*/
- public static void setFavoriteImage(ImageButton mImageButton) {
- try {
- if (MusicUtils.mService.isFavorite(MusicUtils.mService.getAudioId())) {
- mImageButton.setImageResource(R.drawable.apollo_holo_light_favorite_selected);
- } else {
- mImageButton.setImageResource(R.drawable.apollo_holo_light_favorite_normal);
- // Theme chooser
- ThemeUtils.setImageButton(mImageButton.getContext(), mImageButton,
- "apollo_favorite_normal");
- }
- } catch (RemoteException e) {
- e.printStackTrace();
+ public static final long[] getSongListForPlaylist(final Context context, final String playlistId) {
+ final String[] projection = new String[] {
+ MediaStore.Audio.Playlists.Members.AUDIO_ID
+ };
+ Cursor cursor = context.getContentResolver().query(
+ MediaStore.Audio.Playlists.Members.getContentUri("external",
+ Long.valueOf(playlistId)), projection, null, null,
+ MediaStore.Audio.Playlists.Members.DEFAULT_SORT_ORDER);
+
+ if (cursor != null) {
+ final long[] list = getSongListForCursor(cursor);
+ cursor.close();
+ cursor = null;
+ return list;
}
+ return sEmptyList;
}
/**
- * @param mContext
- * @param id
- * @param name
+ * Plays a user created playlist.
+ *
+ * @param context The {@link Context} to use.
+ * @param playlistId The playlist Id.
*/
- public static void renamePlaylist(Context mContext, long id, String name) {
-
- if (name != null && name.length() > 0) {
- ContentResolver resolver = mContext.getContentResolver();
- ContentValues values = new ContentValues(1);
- values.put(PlaylistsColumns.NAME, name);
- resolver.update(Audio.Playlists.EXTERNAL_CONTENT_URI, values, BaseColumns._ID + "=?",
- new String[] {
- String.valueOf(id)
- });
- Toast.makeText(mContext, "Playlist renamed", Toast.LENGTH_SHORT).show();
+ public static void playPlaylist(final Context context, final String playlistId) {
+ final long[] playlistList = getSongListForPlaylist(context, playlistId);
+ if (playlistList != null) {
+ playAll(context, playlistList, -1, false);
}
}
/**
- * @param mContext
- * @param list
+ * @param cursor The {@link Cursor} used to gather the list in our favorites
+ * database
+ * @return The song list for the favorite playlist
*/
- public static void addToCurrentPlaylist(Context mContext, long[] list) {
-
- if (mService == null)
- return;
+ public final static long[] getSongListForFavoritesCursor(Cursor cursor) {
+ if (cursor == null) {
+ return sEmptyList;
+ }
+ final int len = cursor.getCount();
+ final long[] list = new long[len];
+ cursor.moveToFirst();
+ int colidx = -1;
try {
- mService.enqueue(list, ApolloService.LAST);
- String message = mContext.getResources().getQuantityString(
- R.plurals.NNNtrackstoplaylist, list.length, Integer.valueOf(list.length));
- Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show();
- } catch (RemoteException ex) {
+ colidx = cursor.getColumnIndexOrThrow(FavoriteColumns.ID);
+ } catch (final Exception ignored) {
+ }
+ for (int i = 0; i < len; i++) {
+ list[i] = cursor.getLong(colidx);
+ cursor.moveToNext();
}
+ cursor.close();
+ cursor = null;
+ return list;
}
/**
- * @param context
- * @param secs
- * @return time String
+ * @param context The {@link Context} to use
+ * @return The song list from our favorites database
*/
- public static String makeTimeString(Context context, long secs) {
-
- String durationformat = context.getString(secs < 3600 ? R.string.durationformatshort
- : R.string.durationformatlong);
-
- /*
- * Provide multiple arguments so the format can be changed easily by
- * modifying the xml.
- */
- sFormatBuilder.setLength(0);
-
- final Object[] timeArgs = sTimeArgs;
- timeArgs[0] = secs / 3600;
- timeArgs[1] = secs / 60;
- timeArgs[2] = secs / 60 % 60;
- timeArgs[3] = secs;
- timeArgs[4] = secs % 60;
-
- return sFormatter.format(durationformat, timeArgs).toString();
+ public final static long[] getSongListForFavorites(final Context context) {
+ Cursor cursor = FavoritesLoader.makeFavoritesCursor(context);
+ if (cursor != null) {
+ final long[] list = getSongListForFavoritesCursor(cursor);
+ cursor.close();
+ cursor = null;
+ return list;
+ }
+ return sEmptyList;
}
/**
- * @return current album ID
+ * Play the songs that have been marked as favorites.
+ *
+ * @param context The {@link Context} to use
*/
- public static long getCurrentAlbumId() {
-
- if (mService != null) {
- try {
- return mService.getAlbumId();
- } catch (RemoteException ex) {
- }
- }
- return -1;
+ public static void playFavorites(final Context context) {
+ playAll(context, getSongListForFavorites(context), 0, false);
}
/**
- * @return current artist ID
+ * @param context The {@link Context} to use
+ * @return The song list for the last added playlist
*/
- public static long getCurrentArtistId() {
-
- if (MusicUtils.mService != null) {
- try {
- return mService.getArtistId();
- } catch (RemoteException ex) {
+ public static final long[] getSongListForLastAdded(final Context context) {
+ final Cursor cursor = LastAddedLoader.makeLastAddedCursor(context);
+ if (cursor != null) {
+ final int count = cursor.getCount();
+ final long[] list = new long[count];
+ for (int i = 0; i < count; i++) {
+ cursor.moveToNext();
+ list[i] = cursor.getLong(0);
}
+ return list;
}
- return -1;
+ return sEmptyList;
}
/**
- * @return current track ID
+ * Plays the last added songs from the past two weeks.
+ *
+ * @param context The {@link Context} to use
*/
- public static long getCurrentAudioId() {
+ public static void playLastAdded(final Context context) {
+ playAll(context, getSongListForLastAdded(context), 0, false);
+ }
- if (MusicUtils.mService != null) {
- try {
- return mService.getAudioId();
- } catch (RemoteException ex) {
+ /**
+ * Creates a sub menu used to add items to a new playlist or an existsing
+ * one.
+ *
+ * @param context The {@link Context} to use.
+ * @param groupId The group Id of the menu.
+ * @param subMenu The {@link SubMenu} to add to.
+ * @param showFavorites True if we should show the option to add to the
+ * Favorites cache.
+ */
+ public static void makePlaylistMenu(final Context context, final int groupId,
+ final SubMenu subMenu, final boolean showFavorites) {
+ subMenu.clear();
+ if (showFavorites) {
+ subMenu.add(groupId, FragmentMenuItems.ADD_TO_FAVORITES, Menu.NONE,
+ R.string.add_to_favorites);
+ }
+ subMenu.add(groupId, FragmentMenuItems.NEW_PLAYLIST, Menu.NONE, R.string.new_playlist);
+ Cursor cursor = PlaylistLoader.makePlaylistCursor(context);
+ if (cursor != null && cursor.getCount() > 0 && cursor.moveToFirst()) {
+ while (!cursor.isAfterLast()) {
+ final Intent intent = new Intent();
+ intent.putExtra("playlist", getIdForPlaylist(context, cursor.getString(1)));
+ subMenu.add(groupId, FragmentMenuItems.PLAYLIST_SELECTED, Menu.NONE,
+ cursor.getString(1)).setIntent(intent);
+ cursor.moveToNext();
}
}
- return -1;
+ if (cursor != null) {
+ cursor.close();
+ cursor = null;
+ }
}
/**
- * @return current artist name
+ * Called when one of the lists should refresh or requery.
*/
- public static String getArtistName() {
-
- if (mService != null) {
- try {
- return mService.getArtistName();
- } catch (RemoteException ex) {
+ public static void refresh() {
+ try {
+ if (mService != null) {
+ mService.refresh();
}
+ } catch (final RemoteException ignored) {
}
- return null;
}
/**
- * @return current album name
+ * Queries {@link RecentStore} for the last album played by an artist
+ *
+ * @param context The {@link Context} to use
+ * @param artistName The artist name
+ * @return The last album name played by an artist
*/
- public static String getAlbumName() {
+ public static final String getLastAlbumForArtist(final Context context, final String artistName) {
+ return RecentStore.getInstance(context).getAlbumName(artistName);
+ }
+ /**
+ * Seeks the current track to a desired position
+ *
+ * @param position The position to seek to
+ */
+ public static void seek(final long position) {
if (mService != null) {
try {
- return mService.getAlbumName();
- } catch (RemoteException ex) {
+ mService.seek(position);
+ } catch (final RemoteException ignored) {
}
}
- return null;
}
/**
- * @return current track name
+ * @return The current position time of the track
*/
- public static String getTrackName() {
-
+ public static final long position() {
if (mService != null) {
try {
- return mService.getTrackName();
- } catch (RemoteException ex) {
+ return mService.position();
+ } catch (final RemoteException ignored) {
}
}
- return null;
+ return 0;
}
/**
- * @return duration of a track
+ * @return The total length of the current track
*/
- public static long getDuration() {
+ public static final long duration() {
if (mService != null) {
try {
return mService.duration();
- } catch (RemoteException e) {
+ } catch (final RemoteException ignored) {
}
}
return 0;
}
/**
- * Create a Search Chooser
+ * @param position The position to move the queue to
*/
- public static void doSearch(Context mContext, Cursor mCursor, int index) {
- CharSequence title = null;
- Intent i = new Intent();
- i.setAction(MediaStore.INTENT_ACTION_MEDIA_SEARCH);
- i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- String query = mCursor.getString(index);
- title = "";
- i.putExtra("", query);
- title = title + " " + query;
- title = "Search " + title;
- i.putExtra(SearchManager.QUERY, query);
- mContext.startActivity(Intent.createChooser(i, title));
- }
-
- /**
- * @param id
- * @return removes track from a playlist
- */
- public static int removeTrack(long id) {
- if (mService == null)
- return 0;
-
- try {
- return mService.removeTrack(id);
- } catch (RemoteException e) {
- e.printStackTrace();
+ public static void setQueuePosition(final int position) {
+ if (mService != null) {
+ try {
+ mService.setQueuePosition(position);
+ } catch (final RemoteException ignored) {
+ }
}
- return 0;
}
/**
- * @param index
+ * Clears the qeueue
*/
- public static void setQueuePosition(int index) {
- if (mService == null)
- return;
+ public static void clearQueue() {
try {
- mService.setQueuePosition(index);
- } catch (RemoteException e) {
- }
- }
-
- public static String getArtistName(Context mContext, long artist_id, boolean default_name) {
- String where = BaseColumns._ID + "=" + artist_id;
- String[] cols = new String[] {
- ArtistColumns.ARTIST
- };
- Uri uri = Audio.Artists.EXTERNAL_CONTENT_URI;
- Cursor cursor = mContext.getContentResolver().query(uri, cols, where, null, null);
- if (cursor.getCount() <= 0) {
- if (default_name)
- return mContext.getString(R.string.unknown);
- else
- return MediaStore.UNKNOWN_STRING;
- } else {
- cursor.moveToFirst();
- String name = cursor.getString(0);
- cursor.close();
- if (name == null || MediaStore.UNKNOWN_STRING.equals(name)) {
- if (default_name)
- return mContext.getString(R.string.unknown);
- else
- return MediaStore.UNKNOWN_STRING;
- }
- return name;
+ mService.removeTracks(0, Integer.MAX_VALUE);
+ } catch (final RemoteException ignored) {
}
}
/**
- * @param mContext
- * @param album_id
- * @param default_name
- * @return album name
+ * Used to build and show a notification when Apollo is sent into the
+ * background
+ *
+ * @param context The {@link Context} to use.
*/
- public static String getAlbumName(Context mContext, long album_id, boolean default_name) {
- String where = BaseColumns._ID + "=" + album_id;
- String[] cols = new String[] {
- AlbumColumns.ALBUM
- };
- Uri uri = Audio.Albums.EXTERNAL_CONTENT_URI;
- Cursor cursor = mContext.getContentResolver().query(uri, cols, where, null, null);
- if (cursor.getCount() <= 0) {
- if (default_name)
- return mContext.getString(R.string.unknown);
- else
- return MediaStore.UNKNOWN_STRING;
- } else {
- cursor.moveToFirst();
- String name = cursor.getString(0);
- cursor.close();
- if (name == null || MediaStore.UNKNOWN_STRING.equals(name)) {
- if (default_name)
- return mContext.getString(R.string.unknown);
- else
- return MediaStore.UNKNOWN_STRING;
- }
- return name;
- }
+ public static void startBackgroundService(final Context context) {
+ final Intent startBackground = new Intent(context, MusicPlaybackService.class);
+ startBackground.setAction(MusicPlaybackService.START_BACKGROUND);
+ context.startService(startBackground);
}
/**
- * @param playlist_id
- * @return playlist name
+ * Used to kill the current foreground notification
+ *
+ * @param context The {@link Cotext} to use.
*/
- public static String getPlaylistName(Context mContext, long playlist_id) {
- String where = BaseColumns._ID + "=" + playlist_id;
- String[] cols = new String[] {
- PlaylistsColumns.NAME
- };
- Uri uri = Audio.Playlists.EXTERNAL_CONTENT_URI;
- Cursor cursor = mContext.getContentResolver().query(uri, cols, where, null, null);
- if (cursor.getCount() <= 0)
- return "";
- cursor.moveToFirst();
- String name = cursor.getString(0);
- cursor.close();
- return name;
+ public static void killForegroundService(final Context context) {
+ final Intent killForeground = new Intent(context, MusicPlaybackService.class);
+ killForeground.setAction(MusicPlaybackService.KILL_FOREGROUND);
+ context.startService(killForeground);
}
/**
- * @param mContext
- * @param genre_id
- * @param default_name
- * @return genre name
+ * @param context The {@link Cotext} to use.
+ * @return True if the mediascanner is running, false otherwise.
*/
- public static String getGenreName(Context mContext, long genre_id, boolean default_name) {
- String where = BaseColumns._ID + "=" + genre_id;
- String[] cols = new String[] {
- GenresColumns.NAME
- };
- Uri uri = Audio.Genres.EXTERNAL_CONTENT_URI;
- Cursor cursor = mContext.getContentResolver().query(uri, cols, where, null, null);
- if (cursor.getCount() <= 0) {
- if (default_name)
- return mContext.getString(R.string.unknown);
- else
- return MediaStore.UNKNOWN_STRING;
- } else {
- cursor.moveToFirst();
- String name = cursor.getString(0);
- cursor.close();
- if (name == null || MediaStore.UNKNOWN_STRING.equals(name)) {
- if (default_name)
- return mContext.getString(R.string.unknown);
- else
- return MediaStore.UNKNOWN_STRING;
+ public static boolean isMediaScannerScanning(final Context context) {
+ boolean result = false;
+ final Cursor cursor = context.getContentResolver().query(MediaStore.getMediaScannerUri(),
+ new String[] {
+ MediaStore.MEDIA_SCANNER_VOLUME
+ }, null, null, null);
+ if (cursor != null) {
+ if (cursor.getCount() == 1) {
+ cursor.moveToFirst();
+ result = "external".equals(cursor.getString(0));
}
- return name;
- }
- }
-
- /**
- * @param genre
- * @return parsed genre name
- */
- public static String parseGenreName(Context mContext, String genre) {
- int genre_id = -1;
-
- if (genre == null || genre.trim().length() <= 0)
- return mContext.getResources().getString(R.string.unknown);
-
- try {
- genre_id = Integer.parseInt(genre);
- } catch (NumberFormatException e) {
- return genre;
- }
- if (genre_id >= 0 && genre_id < GENRES_DB.length)
- return GENRES_DB[genre_id];
- else
- return mContext.getResources().getString(R.string.unknown);
- }
-
- /**
- * @return if music is playing
- */
- public static boolean isPlaying() {
- if (mService == null)
- return false;
-
- try {
- mService.isPlaying();
- } catch (RemoteException e) {
+ cursor.close();
}
- return false;
+ return result;
}
/**
- * @return current track's queue position
+ * Perminately deletes item(s) from the user's device
+ *
+ * @param context The {@link Context} to use.
+ * @param list The item(s) to delete.
*/
- public static int getQueuePosition() {
- if (mService == null)
- return 0;
- try {
- return mService.getQueuePosition();
- } catch (RemoteException e) {
+ public static void deleteTracks(final Context context, final long[] list) {
+ final String[] projection = new String[] {
+ BaseColumns._ID, MediaColumns.DATA, AudioColumns.ALBUM_ID
+ };
+ final StringBuilder selection = new StringBuilder();
+ selection.append(BaseColumns._ID + " IN (");
+ for (int i = 0; i < list.length; i++) {
+ selection.append(list[i]);
+ if (i < list.length - 1) {
+ selection.append(",");
+ }
}
- return 0;
- }
-
- /**
- * @param mContext
- * @param create_shortcut
- * @param list
- */
- public static void makePlaylistList(Context mContext, boolean create_shortcut,
- List<Map<String, String>> list) {
+ selection.append(")");
+ final Cursor c = context.getContentResolver().query(
+ MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection.toString(),
+ null, null);
+ if (c != null) {
+ // Step 1: Remove selected tracks from the current playlist, as well
+ // as from the album art cache
+ c.moveToFirst();
+ while (!c.isAfterLast()) {
+ // Remove from current playlist
+ final long id = c.getLong(0);
+ removeTrack(id);
+ // Remove from the favorites playlist
+ FavoritesStore.getInstance(context).removeItem(id);
+ // Remove any items in the recents database
+ RecentStore.getInstance(context).removeItem(String.valueOf(c.getLong(2)));
+ c.moveToNext();
+ }
- Map<String, String> map;
+ // Step 2: Remove selected tracks from the database
+ context.getContentResolver().delete(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+ selection.toString(), null);
- String[] cols = new String[] {
- Audio.Playlists._ID, Audio.Playlists.NAME
- };
- StringBuilder where = new StringBuilder();
-
- ContentResolver resolver = mContext.getContentResolver();
- if (resolver == null) {
- System.out.println("resolver = null");
- } else {
- where.append(Audio.Playlists.NAME + " != ''");
- where.append(" AND " + Audio.Playlists.NAME + " != '" + PLAYLIST_NAME_FAVORITES + "'");
- Cursor cur = resolver.query(Audio.Playlists.EXTERNAL_CONTENT_URI, cols,
- where.toString(), null, Audio.Playlists.NAME);
- list.clear();
-
- // map = new HashMap<String, String>();
- // map.put("id", String.valueOf(PLAYLIST_FAVORITES));
- // map.put("name", mContext.getString(R.string.favorite));
- // list.add(map);
-
- map = new HashMap<String, String>();
- map.put("id", String.valueOf(PLAYLIST_QUEUE));
- map.put("name", mContext.getString(R.string.queue));
- list.add(map);
-
- map = new HashMap<String, String>();
- map.put("id", String.valueOf(PLAYLIST_NEW));
- map.put("name", mContext.getString(R.string.new_playlist));
- list.add(map);
-
- if (cur != null && cur.getCount() > 0) {
- cur.moveToFirst();
- while (!cur.isAfterLast()) {
- map = new HashMap<String, String>();
- map.put("id", String.valueOf(cur.getLong(0)));
- map.put("name", cur.getString(1));
- list.add(map);
- cur.moveToNext();
+ // Step 3: Remove files from card
+ c.moveToFirst();
+ while (!c.isAfterLast()) {
+ final String name = c.getString(1);
+ final File f = new File(name);
+ try { // File.delete can throw a security exception
+ if (!f.delete()) {
+ // I'm not sure if we'd ever get here (deletion would
+ // have to fail, but no exception thrown)
+ Log.e("MusicUtils", "Failed to delete file " + name);
+ }
+ c.moveToNext();
+ } catch (final SecurityException ex) {
+ c.moveToNext();
}
}
- if (cur != null) {
- cur.close();
- }
+ c.close();
}
- }
+ final String message = context.getResources().getQuantityString(R.plurals.NNNtracksdeleted,
+ list.length, Integer.valueOf(list.length));
+
+ Crouton.makeText((Activity)context, message, Crouton.STYLE_CONFIRM).show();
+ // We deleted a number of tracks, which could affect any number of
+ // things
+ // in the media content domain, so update everything.
+ context.getContentResolver().notifyChange(Uri.parse("content://media"), null);
+ // Notify the lists to update
+ refresh();
+ }
}
diff --git a/src/com/andrew/apollo/utils/NavUtils.java b/src/com/andrew/apollo/utils/NavUtils.java
new file mode 100644
index 0000000..1e3cb74
--- /dev/null
+++ b/src/com/andrew/apollo/utils/NavUtils.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.utils;
+
+import android.app.Activity;
+import android.app.SearchManager;
+import android.content.Intent;
+import android.media.audiofx.AudioEffect;
+import android.os.Bundle;
+import android.provider.MediaStore;
+
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.andrew.apollo.Config;
+import com.andrew.apollo.R;
+import com.andrew.apollo.ui.activities.AudioPlayerActivity;
+import com.andrew.apollo.ui.activities.HomeActivity;
+import com.andrew.apollo.ui.activities.ProfileActivity;
+import com.andrew.apollo.ui.activities.SearchActivity;
+import com.andrew.apollo.ui.activities.SettingsActivity;
+import com.devspark.appmsg.Crouton;
+
+/**
+ * Various navigation helpers.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public final class NavUtils {
+
+ /**
+ * Opens the profile of an artist.
+ *
+ * @param context The {@link SherlockFragmentActivity} to use.
+ * @param artistName The name of the artist
+ */
+ public static void openArtistProfile(final SherlockFragmentActivity context,
+ final String artistName) {
+
+ // Create a new bundle to transfer the artist info
+ final Bundle bundle = new Bundle();
+ bundle.putLong(Config.ID, MusicUtils.getIdForArtist(context, artistName));
+ bundle.putString(Config.MIME_TYPE, MediaStore.Audio.Artists.CONTENT_TYPE);
+ bundle.putString(Config.ARTIST_NAME, artistName);
+
+ // Create the intent to launch the profile activity
+ final Intent intent = new Intent(context, ProfileActivity.class);
+ intent.putExtras(bundle);
+ context.startActivity(intent);
+ }
+
+ /**
+ * Opens the profile of an album.
+ *
+ * @param context The {@link SherlockFragmentActivity} to use.
+ * @param albumName The name of the album
+ * @param artistName The name of the album artist
+ */
+ public static void openAlbumProfile(final SherlockFragmentActivity context,
+ final String albumName, final String artistName) {
+
+ // Create a new bundle to transfer the album info
+ final Bundle bundle = new Bundle();
+ bundle.putString(Config.ALBUM_YEAR, MusicUtils.getReleaseDateForAlbum(context, albumName));
+ bundle.putString(Config.ARTIST_NAME, artistName);
+ bundle.putString(Config.MIME_TYPE, MediaStore.Audio.Albums.CONTENT_TYPE);
+ bundle.putLong(Config.ID, MusicUtils.getIdForAlbum(context, albumName));
+ bundle.putString(Config.NAME, albumName);
+
+ // Create the intent to launch the profile activity
+ final Intent intent = new Intent(context, ProfileActivity.class);
+ intent.putExtras(bundle);
+ context.startActivity(intent);
+ }
+
+ /**
+ * Opens the sound effects panel or DSP manager in CM
+ *
+ * @param context The {@link SherlockFragmentActivity} to use.
+ */
+ public static void openEffectsPanel(final SherlockFragmentActivity context) {
+ try {
+ final Intent effects = new Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL);
+ effects.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, MusicUtils.getCurrentAudioId());
+ context.startActivity(effects);
+ // Make sure the notification starts
+ MusicUtils.startBackgroundService(context);
+ } catch (final Exception notFound) {
+ Crouton.makeText(context, context.getString(R.string.no_effects_for_you),
+ Crouton.STYLE_ALERT);
+ }
+ }
+
+ /**
+ * Opens to {@link SettingsActivity}.
+ *
+ * @param activity The {@link SherlockFragmentActivity} to use.
+ */
+ public static void openSettings(final SherlockFragmentActivity activity) {
+ final Intent intent = new Intent(activity, SettingsActivity.class);
+ activity.startActivity(intent);
+ }
+
+ /**
+ * Opens to {@link AudioPlayerActivity}.
+ *
+ * @param activity The {@link Activity} to use.
+ */
+ public static void openAudioPlayer(final Activity activity) {
+ final Intent intent = new Intent(activity, AudioPlayerActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ activity.startActivity(intent);
+ activity.finish();
+ }
+
+ /**
+ * Opens to {@link SearchActivity}.
+ *
+ * @param activity The {@link Activity} to use.
+ * @param query The search query.
+ */
+ public static void openSearch(final Activity activity, final String query) {
+ final Bundle bundle = new Bundle();
+ final Intent intent = new Intent(activity, SearchActivity.class);
+ intent.putExtra(SearchManager.QUERY, query);
+ intent.putExtras(bundle);
+ activity.startActivity(intent);
+ }
+
+ /**
+ * Opens to {@link HomeActivity}.
+ *
+ * @param activity The {@link Activity} to use.
+ */
+ public static void goHome(final Activity activity) {
+ final Intent intent = new Intent(activity, HomeActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ activity.startActivity(intent);
+ activity.finish();
+ }
+}
diff --git a/src/com/andrew/apollo/utils/PreferenceUtils.java b/src/com/andrew/apollo/utils/PreferenceUtils.java
new file mode 100644
index 0000000..705e112
--- /dev/null
+++ b/src/com/andrew/apollo/utils/PreferenceUtils.java
@@ -0,0 +1,467 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.utils;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.AsyncTask;
+import android.preference.PreferenceManager;
+
+import com.andrew.apollo.R;
+import com.andrew.apollo.ui.fragments.AlbumFragment;
+import com.andrew.apollo.ui.fragments.ArtistFragment;
+import com.andrew.apollo.ui.fragments.SongFragment;
+import com.andrew.apollo.ui.fragments.phone.MusicBrowserPhoneFragment;
+import com.andrew.apollo.ui.fragments.profile.AlbumSongFragment;
+import com.andrew.apollo.ui.fragments.profile.ArtistAlbumFragment;
+import com.andrew.apollo.ui.fragments.profile.ArtistSongFragment;
+
+/**
+ * A collection of helpers designed to get and set various preferences across
+ * Apollo.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public final class PreferenceUtils {
+
+ /* Default start page (Artist page) */
+ public static final int DEFFAULT_PAGE = 2;
+
+ /* Saves the last page the pager was on in {@link MusicBrowserPhoneFragment} */
+ public static final String START_PAGE = "start_page";
+
+ // Sort order for the artist list
+ public static final String ARTIST_SORT_ORDER = "artist_sort_order";
+
+ // Sort order for the artist song list
+ public static final String ARTIST_SONG_SORT_ORDER = "artist_song_sort_order";
+
+ // Sort order for the artist album list
+ public static final String ARTIST_ALBUM_SORT_ORDER = "artist_album_sort_order";
+
+ // Sort order for the album list
+ public static final String ALBUM_SORT_ORDER = "album_sort_order";
+
+ // Sort order for the album song list
+ public static final String ALBUM_SONG_SORT_ORDER = "album_song_sort_order";
+
+ // Sort order for the song list
+ public static final String SONG_SORT_ORDER = "song_sort_order";
+
+ // Sets the type of layout to use for the artist list
+ public static final String ARTIST_LAYOUT = "artist_layout";
+
+ // Sets the type of layout to use for the album list
+ public static final String ALBUM_LAYOUT = "album_layout";
+
+ // Sets the type of layout to use for the recent list
+ public static final String RECENT_LAYOUT = "recent_layout";
+
+ // Key used to download images only on Wi-Fi
+ public static final String ONLY_ON_WIFI = "only_on_wifi";
+
+ // Key that gives permissions to download missing album covers
+ public static final String DOWNLOAD_MISSING_ARTWORK = "download_missing_artwork";
+
+ // Key that gives permissions to download missing artist images
+ public static final String DOWNLOAD_MISSING_ARTIST_IMAGES = "download_missing_artist_images";
+
+ // Enables lock screen controls on Honeycomb and above
+ public static final String USE_LOCKSREEN_CONTROLS = "use_lockscreen_controls";
+
+ // Key used to set the overall theme color
+ public static final String DEFAULT_THEME_COLOR = "default_theme_color";
+
+ private static PreferenceUtils sInstance;
+
+ private final SharedPreferences mPreferences;
+
+ /**
+ * Constructor for <code>PreferenceUtils</code>
+ *
+ * @param context The {@link Context} to use.
+ */
+ public PreferenceUtils(final Context context) {
+ mPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+ }
+
+ /**
+ * @param context The {@link Context} to use.
+ * @return A singelton of this class
+ */
+ public static final PreferenceUtils getInstace(final Context context) {
+ if (sInstance == null) {
+ sInstance = new PreferenceUtils(context.getApplicationContext());
+ }
+ return sInstance;
+ }
+
+ /**
+ * Saves the current page the user is on when they close the app.
+ *
+ * @param value The last page the pager was on when the onDestroy is called
+ * in {@link MusicBrowserPhoneFragment}.
+ */
+ public void setStartPage(final int value) {
+ ApolloUtils.execute(false, new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(final Void... unused) {
+ final SharedPreferences.Editor editor = mPreferences.edit();
+ editor.putInt(START_PAGE, value);
+ SharedPreferencesCompat.apply(editor);
+
+ return null;
+ }
+ }, (Void[])null);
+ }
+
+ /**
+ * Returns the last page the user was on when the app was exited.
+ *
+ * @return The page to start on when the app is opened.
+ */
+ public final int getStartPage() {
+ return mPreferences.getInt(START_PAGE, DEFFAULT_PAGE);
+ }
+
+ /**
+ * Sets the new theme color.
+ *
+ * @param value The new theme color to use.
+ */
+ public void setDefaultThemeColor(final int value) {
+ ApolloUtils.execute(false, new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(final Void... unused) {
+ final SharedPreferences.Editor editor = mPreferences.edit();
+ editor.putInt(DEFAULT_THEME_COLOR, value);
+ SharedPreferencesCompat.apply(editor);
+
+ return null;
+ }
+ }, (Void[])null);
+ }
+
+ /**
+ * Returns the current theme color.
+ *
+ * @param context The {@link Context} to use.
+ * @return The default theme color.
+ */
+ public final int getDefaultThemeColor(final Context context) {
+ return mPreferences.getInt(DEFAULT_THEME_COLOR,
+ context.getResources().getColor(R.color.holo_blue_light));
+ }
+
+ /**
+ * @return True if the user has checked to only download images on Wi-Fi,
+ * false otherwise
+ */
+ public final boolean onlyOnWifi() {
+ return mPreferences.getBoolean(ONLY_ON_WIFI, true);
+ }
+
+ /**
+ * @param value True if the user only wants to download images on Wi-Fi,
+ * false otherwise
+ */
+ public void setOnlyOnWifi(final boolean value) {
+ ApolloUtils.execute(false, new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(final Void... unused) {
+ final SharedPreferences.Editor editor = mPreferences.edit();
+ editor.putBoolean(ONLY_ON_WIFI, value);
+ SharedPreferencesCompat.apply(editor);
+
+ return null;
+ }
+ }, (Void[])null);
+ }
+
+ /**
+ * @return True if the user has checked to download missing album covers,
+ * false otherwise.
+ */
+ public final boolean downloadMissingArtwork() {
+ return mPreferences.getBoolean(DOWNLOAD_MISSING_ARTWORK, true);
+ }
+
+ /**
+ * @param context The {@link Context} to use
+ * @param value True if the user only wants to download missing album
+ * covers, false otherwise.
+ */
+ public void setDownloadMissingArtwork(final boolean value) {
+ ApolloUtils.execute(false, new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(final Void... unused) {
+ final SharedPreferences.Editor editor = mPreferences.edit();
+ editor.putBoolean(DOWNLOAD_MISSING_ARTWORK, value);
+ SharedPreferencesCompat.apply(editor);
+
+ return null;
+ }
+ }, (Void[])null);
+ }
+
+ /**
+ * @return True if the user has checked to download missing artist images,
+ * false otherwise.
+ */
+ public final boolean downloadMissingArtistImages() {
+ return mPreferences.getBoolean(DOWNLOAD_MISSING_ARTIST_IMAGES, true);
+ }
+
+ /**
+ * @param context The {@link Context} to use
+ * @param value True if the user only wants to download missing artist
+ * images , false otherwise.
+ */
+ public void setDownloadMissingArtistImages(final boolean value) {
+ ApolloUtils.execute(false, new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(final Void... unused) {
+ final SharedPreferences.Editor editor = mPreferences.edit();
+ editor.putBoolean(DOWNLOAD_MISSING_ARTIST_IMAGES, value);
+ SharedPreferencesCompat.apply(editor);
+
+ return null;
+ }
+ }, (Void[])null);
+ }
+
+ /**
+ * @return True if the user has checked to use lockscreen controls, false
+ * otherwise.
+ */
+ public final boolean enableLockscreenControls() {
+ return mPreferences.getBoolean(USE_LOCKSREEN_CONTROLS, true);
+ }
+
+ /**
+ * @param value True if the user has checked to use lockscreen controls,
+ * false otherwise.
+ */
+ public void setLockscreenControls(final boolean value) {
+ ApolloUtils.execute(false, new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(final Void... unused) {
+ final SharedPreferences.Editor editor = mPreferences.edit();
+ editor.putBoolean(USE_LOCKSREEN_CONTROLS, value);
+ SharedPreferencesCompat.apply(editor);
+
+ return null;
+ }
+ }, (Void[])null);
+ }
+
+ /**
+ * Saves the sort order for a list.
+ *
+ * @param key Which sort order to change
+ * @param value The new sort order
+ */
+ private void setSortOrder(final String key, final String value) {
+ ApolloUtils.execute(false, new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(final Void... unused) {
+ final SharedPreferences.Editor editor = mPreferences.edit();
+ editor.putString(key, value);
+ SharedPreferencesCompat.apply(editor);
+
+ return null;
+ }
+ }, (Void[])null);
+ }
+
+ /**
+ * Sets the sort order for the artist list.
+ *
+ * @param value The new sort order
+ */
+ public void setArtistSortOrder(final String value) {
+ setSortOrder(ARTIST_SORT_ORDER, value);
+ }
+
+ /**
+ * @return The sort order used for the artist list in {@link ArtistFragment}
+ */
+ public final String getArtistSortOrder() {
+ return mPreferences.getString(ARTIST_SORT_ORDER, SortOrder.ArtistSortOrder.ARTIST_A_Z);
+ }
+
+ /**
+ * Sets the sort order for the artist song list.
+ *
+ * @param value The new sort order
+ */
+ public void setArtistSongSortOrder(final String value) {
+ setSortOrder(ARTIST_SONG_SORT_ORDER, value);
+ }
+
+ /**
+ * @return The sort order used for the artist song list in
+ * {@link ArtistSongFragment}
+ */
+ public final String getArtistSongSortOrder() {
+ return mPreferences.getString(ARTIST_SONG_SORT_ORDER,
+ SortOrder.ArtistSongSortOrder.SONG_A_Z);
+ }
+
+ /**
+ * Sets the sort order for the artist album list.
+ *
+ * @param value The new sort order
+ */
+ public void setArtistAlbumSortOrder(final String value) {
+ setSortOrder(ARTIST_ALBUM_SORT_ORDER, value);
+ }
+
+ /**
+ * @return The sort order used for the artist album list in
+ * {@link ArtistAlbumFragment}
+ */
+ public final String getArtistAlbumSortOrder() {
+ return mPreferences.getString(ARTIST_ALBUM_SORT_ORDER,
+ SortOrder.ArtistAlbumSortOrder.ALBUM_A_Z);
+ }
+
+ /**
+ * Sets the sort order for the album list.
+ *
+ * @param value The new sort order
+ */
+ public void setAlbumSortOrder(final String value) {
+ setSortOrder(ALBUM_SORT_ORDER, value);
+ }
+
+ /**
+ * @return The sort order used for the album list in {@link AlbumFragment}
+ */
+ public final String getAlbumSortOrder() {
+ return mPreferences.getString(ALBUM_SORT_ORDER, SortOrder.AlbumSortOrder.ALBUM_A_Z);
+ }
+
+ /**
+ * Sets the sort order for the album song list.
+ *
+ * @param value The new sort order
+ */
+ public void setAlbumSongSortOrder(final String value) {
+ setSortOrder(ALBUM_SONG_SORT_ORDER, value);
+ }
+
+ /**
+ * @return The sort order used for the album song in
+ * {@link AlbumSongFragment}
+ */
+ public final String getAlbumSongSortOrder() {
+ return mPreferences.getString(ALBUM_SONG_SORT_ORDER,
+ SortOrder.AlbumSongSortOrder.SONG_TRACK_LIST);
+ }
+
+ /**
+ * Sets the sort order for the song list.
+ *
+ * @param value The new sort order
+ */
+ public void setSongSortOrder(final String value) {
+ setSortOrder(SONG_SORT_ORDER, value);
+ }
+
+ /**
+ * @return The sort order used for the song list in {@link SongFragment}
+ */
+ public final String getSongSortOrder() {
+ return mPreferences.getString(SONG_SORT_ORDER, SortOrder.SongSortOrder.SONG_A_Z);
+ }
+
+ /**
+ * Saves the layout type for a list
+ *
+ * @param key Which layout to change
+ * @param value The new layout type
+ */
+ private void setLayoutType(final String key, final String value) {
+ ApolloUtils.execute(false, new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(final Void... unused) {
+ final SharedPreferences.Editor editor = mPreferences.edit();
+ editor.putString(key, value);
+ SharedPreferencesCompat.apply(editor);
+
+ return null;
+ }
+ }, (Void[])null);
+ }
+
+ /**
+ * Sets the layout type for the artist list
+ *
+ * @param value The new layout type
+ */
+ public void setArtistLayout(final String value) {
+ setLayoutType(ARTIST_LAYOUT, value);
+ }
+
+ /**
+ * Sets the layout type for the album list
+ *
+ * @param value The new layout type
+ */
+ public void setAlbumLayout(final String value) {
+ setLayoutType(ALBUM_LAYOUT, value);
+ }
+
+ /**
+ * Sets the layout type for the recent list
+ *
+ * @param value The new layout type
+ */
+ public void setRecentLayout(final String value) {
+ setLayoutType(RECENT_LAYOUT, value);
+ }
+
+ /**
+ * @param context The {@link Context} to use.
+ * @param which Which list to check.
+ * @return True if the layout type is the simple layout, false otherwise.
+ */
+ public boolean isSimpleLayout(final String which, final Context context) {
+ final String simple = "simple";
+ final String defaultValue = "grid";
+ return mPreferences.getString(which, defaultValue).equals(simple);
+ }
+
+ /**
+ * @param context The {@link Context} to use.
+ * @param which Which list to check.
+ * @return True if the layout type is the simple layout, false otherwise.
+ */
+ public boolean isDetailedLayout(final String which, final Context context) {
+ final String detailed = "detailed";
+ final String defaultValue = "grid";
+ return mPreferences.getString(which, defaultValue).equals(detailed);
+ }
+
+ /**
+ * @param context The {@link Context} to use.
+ * @param which Which list to check.
+ * @return True if the layout type is the simple layout, false otherwise.
+ */
+ public boolean isGridLayout(final String which, final Context context) {
+ final String grid = "grid";
+ final String defaultValue = "simple";
+ return mPreferences.getString(which, defaultValue).equals(grid);
+ }
+
+}
diff --git a/src/com/andrew/apollo/utils/SharedPreferencesCompat.java b/src/com/andrew/apollo/utils/SharedPreferencesCompat.java
index 7cb0bc5..3b44733 100644
--- a/src/com/andrew/apollo/utils/SharedPreferencesCompat.java
+++ b/src/com/andrew/apollo/utils/SharedPreferencesCompat.java
@@ -1,53 +1,52 @@
/*
- * 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.
+ * 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.
*/
package com.andrew.apollo.utils;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
/**
- * Reflection utils to call SharedPreferences$Editor.apply when possible,
- * falling back to commit when apply isn't available.
+ * Reflection utils to call {@link SharedPreferences.Editor} apply() when
+ * possible, falling back to commit when apply isn't available.
*/
-public class SharedPreferencesCompat {
- private static final Method sApplyMethod = findApplyMethod();
+public final class SharedPreferencesCompat {
+
+ private final static Method mApplyMethod = findApplyMethod();
- private static Method findApplyMethod() {
+ /**
+ * @return The apply() method from {@link SharedPreferences.Editor}.
+ */
+ private final static Method findApplyMethod() {
try {
- Class<Editor> cls = SharedPreferences.Editor.class;
- return cls.getMethod("apply");
- } catch (NoSuchMethodException unused) {
- //$FALL-THROUGH$
+ final Class<Editor> class1 = SharedPreferences.Editor.class;
+ return class1.getMethod("apply");
+ } catch (final NoSuchMethodException ignored) {
}
return null;
}
- public static void apply(SharedPreferences.Editor editor) {
- if (sApplyMethod != null) {
+ /**
+ * @param editor The {@link SharedPreferences.Editor} to use.
+ */
+ public static void apply(final SharedPreferences.Editor editor) {
+ if (mApplyMethod != null) {
try {
- sApplyMethod.invoke(editor);
+ mApplyMethod.invoke(editor);
return;
- } catch (InvocationTargetException unused) {
- //$FALL-THROUGH$
- } catch (IllegalAccessException unused) {
- //$FALL-THROUGH$
+ } catch (final InvocationTargetException ignored) {
+ } catch (final IllegalAccessException ignored) {
}
}
editor.commit();
diff --git a/src/com/andrew/apollo/utils/SortOrder.java b/src/com/andrew/apollo/utils/SortOrder.java
new file mode 100644
index 0000000..047fcad
--- /dev/null
+++ b/src/com/andrew/apollo/utils/SortOrder.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.utils;
+
+import android.provider.MediaStore;
+
+/**
+ * Holds all of the sort orders for each list type.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public final class SortOrder {
+
+ /** This class is never instantiated */
+ public SortOrder() {
+ }
+
+ /**
+ * Artist sort order entries.
+ */
+ public static interface ArtistSortOrder {
+ /* Artist sort order A-Z */
+ public final static String ARTIST_A_Z = MediaStore.Audio.Artists.DEFAULT_SORT_ORDER;
+
+ /* Artist sort order Z-A */
+ public final static String ARTIST_Z_A = ARTIST_A_Z + " DESC";
+
+ /* Artist sort order number of songs */
+ public final static String ARTIST_NUMBER_OF_SONGS = MediaStore.Audio.Artists.NUMBER_OF_TRACKS
+ + " DESC";
+
+ /* Artist sort order number of albums */
+ public final static String ARTIST_NUMBER_OF_ALBUMS = MediaStore.Audio.Artists.NUMBER_OF_ALBUMS
+ + " DESC";
+ }
+
+ /**
+ * Album sort order entries.
+ */
+ public static interface AlbumSortOrder {
+ /* Album sort order A-Z */
+ public final static String ALBUM_A_Z = MediaStore.Audio.Albums.DEFAULT_SORT_ORDER;
+
+ /* Album sort order Z-A */
+ public final static String ALBUM_Z_A = ALBUM_A_Z + " DESC";
+
+ /* Album sort order songs */
+ public final static String ALBUM_NUMBER_OF_SONGS = MediaStore.Audio.Albums.NUMBER_OF_SONGS
+ + " DESC";
+
+ /* Album sort order artist */
+ public final static String ALBUM_ARTIST = MediaStore.Audio.Albums.ARTIST;
+
+ /* Album sort order year */
+ public final static String ALBUM_YEAR = MediaStore.Audio.Albums.FIRST_YEAR + " DESC";
+
+ }
+
+ /**
+ * Song sort order entries.
+ */
+ public static interface SongSortOrder {
+ /* Song sort order A-Z */
+ public final static String SONG_A_Z = MediaStore.Audio.Media.DEFAULT_SORT_ORDER;
+
+ /* Song sort order Z-A */
+ public final static String SONG_Z_A = SONG_A_Z + " DESC";
+
+ /* Song sort order artist */
+ public final static String SONG_ARTIST = MediaStore.Audio.Media.ARTIST;
+
+ /* Song sort order album */
+ public final static String SONG_ALBUM = MediaStore.Audio.Media.ALBUM;
+
+ /* Song sort order year */
+ public final static String SONG_YEAR = MediaStore.Audio.Media.YEAR + " DESC";
+
+ /* Song sort order duration */
+ public final static String SONG_DURATION = MediaStore.Audio.Media.DURATION + " DESC";
+
+ /* Song sort order date */
+ public final static String SONG_DATE = MediaStore.Audio.Media.DATE_ADDED + " DESC";
+ }
+
+ /**
+ * Album song sort order entries.
+ */
+ public static interface AlbumSongSortOrder {
+ /* Album song sort order A-Z */
+ public final static String SONG_A_Z = MediaStore.Audio.Media.DEFAULT_SORT_ORDER;
+
+ /* Album song sort order Z-A */
+ public final static String SONG_Z_A = SONG_A_Z + " DESC";
+
+ /* Album song sort order track list */
+ public final static String SONG_TRACK_LIST = MediaStore.Audio.Media.TRACK + ", "
+ + MediaStore.Audio.Media.DEFAULT_SORT_ORDER;
+
+ /* Album song sort order duration */
+ public final static String SONG_DURATION = SongSortOrder.SONG_DURATION;
+ }
+
+ /**
+ * Artist song sort order entries.
+ */
+ public static interface ArtistSongSortOrder {
+ /* Artist song sort order A-Z */
+ public final static String SONG_A_Z = MediaStore.Audio.Media.DEFAULT_SORT_ORDER;
+
+ /* Artist song sort order Z-A */
+ public final static String SONG_Z_A = SONG_A_Z + " DESC";
+
+ /* Artist song sort order album */
+ public final static String SONG_ALBUM = MediaStore.Audio.Media.ALBUM;
+
+ /* Artist song sort order year */
+ public final static String SONG_YEAR = MediaStore.Audio.Media.YEAR + " DESC";
+
+ /* Artist song sort order duration */
+ public final static String SONG_DURATION = MediaStore.Audio.Media.DURATION + " DESC";
+
+ /* Artist song sort order date */
+ public final static String SONG_DATE = MediaStore.Audio.Media.DATE_ADDED + " DESC";
+ }
+
+ /**
+ * Artist album sort order entries.
+ */
+ public static interface ArtistAlbumSortOrder {
+ /* Artist album sort order A-Z */
+ public final static String ALBUM_A_Z = MediaStore.Audio.Albums.DEFAULT_SORT_ORDER;
+
+ /* Artist album sort order Z-A */
+ public final static String ALBUM_Z_A = ALBUM_A_Z + " DESC";
+
+ /* Artist album sort order songs */
+ public final static String ALBUM_NUMBER_OF_SONGS = MediaStore.Audio.Artists.Albums.NUMBER_OF_SONGS
+ + " DESC";
+
+ /* Artist album sort order year */
+ public final static String ALBUM_YEAR = MediaStore.Audio.Artists.Albums.FIRST_YEAR
+ + " DESC";
+ }
+
+}
diff --git a/src/com/andrew/apollo/utils/Stopwatch.java b/src/com/andrew/apollo/utils/Stopwatch.java
new file mode 100644
index 0000000..e72f40b
--- /dev/null
+++ b/src/com/andrew/apollo/utils/Stopwatch.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2008 The Guava Authors 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 com.andrew.apollo.utils;
+
+import static java.util.concurrent.TimeUnit.MICROSECONDS;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import android.annotation.TargetApi;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * An object that measures elapsed time in nanoseconds. It is useful to measure
+ * elapsed time using this class instead of direct calls to
+ * {@link System#nanoTime} for a few reasons:
+ * <ul>
+ * <li>An alternate time source can be substituted, for testing or performance
+ * reasons.
+ * <li>As documented by {@code nanoTime}, the value returned has no absolute
+ * meaning, and can only be interpreted as relative to another timestamp
+ * returned by {@code nanoTime} at a different time. {@code Stopwatch} is a more
+ * effective abstraction because it exposes only these relative values, not the
+ * absolute ones.
+ * </ul>
+ * <p>
+ * Basic usage:
+ *
+ * <pre>
+ * Stopwatch stopwatch = new Stopwatch().{@link #start start}();
+ * doSomething();
+ * stopwatch.{@link #stop stop}(); // optional
+ *
+ * long millis = stopwatch.{@link #elapsedMillis elapsedMillis}();
+ *
+ * log.info("that took: " + stopwatch); // formatted string like "12.3 ms"
+ * </pre>
+ * <p>
+ * Stopwatch methods are not idempotent; it is an error to start or stop a
+ * stopwatch that is already in the desired state.
+ * <p>
+ * When testing code that uses this class, use the
+ * {@linkplain #Stopwatch(Ticker) alternate constructor} to supply a fake or
+ * mock ticker. <!-- TODO(kevinb): restore the "such as" --> This allows you to
+ * simulate any valid behavior of the stopwatch.
+ * <p>
+ * <b>Note:</b> This class is not thread-safe.
+ *
+ * @author Kevin Bourrillion
+ * @since 10.0
+ */
+@TargetApi(9)
+public final class Stopwatch {
+
+ private final Ticker mTicker;
+
+ private boolean mIsRunning;
+
+ private long mElapsedNanos;
+
+ private long mStartTick;
+
+ /**
+ * Creates (but does not start) a new stopwatch using
+ * {@link System#nanoTime} as its time source.
+ */
+ public Stopwatch() {
+ this(Ticker.systemTicker());
+ }
+
+ /**
+ * Creates (but does not start) a new stopwatch, using the specified time
+ * source.
+ */
+ public Stopwatch(final Ticker ticker) {
+ mTicker = checkNotNull(ticker);
+ }
+
+ /**
+ * Returns {@code true} if {@link #start()} has been called on this
+ * stopwatch, and {@link #stop()} has not been called since the last call to
+ * {@code start()}.
+ */
+ public boolean isRunning() {
+ return mIsRunning;
+ }
+
+ /**
+ * Starts the stopwatch.
+ *
+ * @return this {@code Stopwatch} instance
+ * @throws IllegalStateException if the stopwatch is already running.
+ */
+ public Stopwatch start() {
+ checkState(!mIsRunning);
+ mIsRunning = true;
+ mStartTick = mTicker.read();
+ return this;
+ }
+
+ /**
+ * Stops the stopwatch. Future reads will return the fixed duration that had
+ * elapsed up to this point.
+ *
+ * @return this {@code Stopwatch} instance
+ * @throws IllegalStateException if the stopwatch is already stopped.
+ */
+ public Stopwatch stop() {
+ final long mTick = mTicker.read();
+ checkState(mIsRunning);
+ mIsRunning = false;
+ mElapsedNanos += mTick - mStartTick;
+ return this;
+ }
+
+ /**
+ * Sets the elapsed time for this stopwatch to zero, and places it in a
+ * stopped state.
+ *
+ * @return this {@code Stopwatch} instance
+ */
+ public Stopwatch reset() {
+ mElapsedNanos = 0;
+ mIsRunning = false;
+ return this;
+ }
+
+ private long elapsedNanos() {
+ return mIsRunning ? mTicker.read() - mStartTick + mElapsedNanos : mElapsedNanos;
+ }
+
+ /**
+ * Returns the current elapsed time shown on this stopwatch, expressed in
+ * the desired time unit, with any fraction rounded down.
+ * <p>
+ * Note that the overhead of measurement can be more than a microsecond, so
+ * it is generally not useful to specify {@link TimeUnit#NANOSECONDS}
+ * precision here.
+ */
+ public long elapsedTime(final TimeUnit desiredUnit) {
+ return desiredUnit.convert(elapsedNanos(), NANOSECONDS);
+ }
+
+ /**
+ * Returns the current elapsed time shown on this stopwatch, expressed in
+ * milliseconds, with any fraction rounded down. This is identical to
+ * {@code elapsedTime(TimeUnit.MILLISECONDS)}.
+ */
+ public long elapsedMillis() {
+ return elapsedTime(MILLISECONDS);
+ }
+
+ /**
+ * Returns a string representation of the current elapsed time.
+ */
+ @Override
+ public String toString() {
+ return toString(4);
+ }
+
+ /**
+ * Returns a string representation of the current elapsed time, choosing an
+ * appropriate unit and using the specified number of significant figures.
+ * For example, at the instant when {@code elapsedTime(NANOSECONDS)} would
+ * return {1234567}, {@code toString(4)} returns {@code "1.235 ms"}.
+ *
+ * @deprecated Use {@link #toString()} instead. This method is scheduled to
+ * be removed in Guava release 15.0.
+ */
+ @Deprecated
+ public String toString(final int significantDigits) {
+ final long mNanos = elapsedNanos();
+
+ final TimeUnit mUnit = chooseUnit(mNanos);
+ final double mValue = (double)mNanos / NANOSECONDS.convert(1, mUnit);
+
+ /* Too bad this functionality is not exposed as a regular method call */
+ return String.format("%." + significantDigits + "g %s", mValue, abbreviate(mUnit));
+ }
+
+ private static TimeUnit chooseUnit(final long nanos) {
+ if (SECONDS.convert(nanos, NANOSECONDS) > 0) {
+ return SECONDS;
+ }
+ if (MILLISECONDS.convert(nanos, NANOSECONDS) > 0) {
+ return MILLISECONDS;
+ }
+ if (MICROSECONDS.convert(nanos, NANOSECONDS) > 0) {
+ return MICROSECONDS;
+ }
+ return NANOSECONDS;
+ }
+
+ private static String abbreviate(final TimeUnit unit) {
+ switch (unit) {
+ case NANOSECONDS:
+ return "ns";
+ case MICROSECONDS:
+ return "\u03bcs";
+ case MILLISECONDS:
+ return "ms";
+ case SECONDS:
+ return "s";
+ default:
+ throw new AssertionError();
+ }
+ }
+
+ /**
+ * Ensures the truth of an expression involving the state of the calling
+ * instance, but not involving any parameters to the calling method.
+ *
+ * @param expression a boolean expression
+ * @throws IllegalStateException if {@code expression} is false
+ */
+ public static void checkState(final boolean expression) {
+ if (!expression) {
+ throw new IllegalStateException();
+ }
+ }
+
+ /**
+ * Ensures that an object reference passed as a parameter to the calling
+ * method is not null.
+ *
+ * @param reference an object reference
+ * @return the non-null reference that was validated
+ * @throws NullPointerException if {@code reference} is null
+ */
+ public static <T> T checkNotNull(final T reference) {
+ if (reference == null) {
+ throw new NullPointerException();
+ }
+ return reference;
+ }
+
+}
diff --git a/src/com/andrew/apollo/utils/StringUtilities.java b/src/com/andrew/apollo/utils/StringUtilities.java
deleted file mode 100644
index 2047f27..0000000
--- a/src/com/andrew/apollo/utils/StringUtilities.java
+++ /dev/null
@@ -1,189 +0,0 @@
-/*
- * Copyright (c) 2012, the Last.fm Java Project and Committers
- * All rights reserved.
- *
- * Redistribution and use of this software in source and binary forms, with or without modification, are
- * permitted provided that the following conditions are met:
- *
- * - Redistributions of source code must retain the above
- * copyright notice, this list of conditions and the
- * following disclaimer.
- *
- * - Redistributions in binary form must reproduce the above
- * copyright notice, this list of conditions and the
- * following disclaimer in the documentation and/or other
- * materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
- * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
- * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
- * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
- * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.andrew.apollo.utils;
-
-import java.io.UnsupportedEncodingException;
-import java.net.URLDecoder;
-import java.net.URLEncoder;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.regex.Pattern;
-
-/**
- * Utilitiy class with methods to calculate an md5 hash and to encode URLs.
- *
- * @author Janni Kovacs
- */
-public final class StringUtilities {
-
- private static MessageDigest digest;
- private static Pattern MBID_PATTERN = Pattern
- .compile("^[0-9a-f]{8}\\-[0-9a-f]{4}\\-[0-9a-f]{4}\\-[0-9a-f]{4}\\-[0-9a-f]{12}$",
- Pattern.CASE_INSENSITIVE);
- private static final Pattern MD5_PATTERN = Pattern.compile("[a-fA-F0-9]{32}");
-
- static {
- try {
- digest = MessageDigest.getInstance("MD5");
- } catch (NoSuchAlgorithmException e) {
- // better never happens
- }
- }
-
- /**
- * Returns a 32 chararacter hexadecimal representation of an MD5 hash of the given String.
- *
- * @param s the String to hash
- * @return the md5 hash
- */
- public static String md5(String s) {
- try {
- byte[] bytes = digest.digest(s.getBytes("UTF-8"));
- StringBuilder b = new StringBuilder(32);
- for (byte aByte : bytes) {
- String hex = Integer.toHexString((int) aByte & 0xFF);
- if (hex.length() == 1)
- b.append('0');
- b.append(hex);
- }
- return b.toString();
- } catch (UnsupportedEncodingException e) {
- // utf-8 always available
- }
- return null;
- }
-
- /**
- * URL Encodes the given String <code>s</code> using the UTF-8 character encoding.
- *
- * @param s a String
- * @return url encoded string
- */
- public static String encode(String s) {
- if(s == null)
- return null;
- try {
- return URLEncoder.encode(s, "UTF-8");
- } catch (UnsupportedEncodingException e) {
- // utf-8 always available
- }
- return null;
- }
-
- /**
- * Decodes an URL encoded String <code>s</code> using the UTF-8 character encoding.
- *
- * @param s an encoded String
- * @return the decoded String
- */
- public static String decode(String s) {
- if(s == null)
- return null;
- try {
- return URLDecoder.decode(s, "UTF-8");
- } catch (UnsupportedEncodingException e) {
- // utf-8 always available
- }
- return null;
- }
-
- /**
- * Checks if the supplied String <i>may</i> be a Musicbrainz ID. This method returns <code>true</code> for Strings that are
- * exactly 36 characters long and match the MBID pattern <code>[0-9a-f]{8}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{12}</code>.
- *
- * @param nameOrMbid a possible MBID
- * @return <code>true</code> if this String <i>may</i> be a MBID
- */
- public static boolean isMbid(String nameOrMbid) {
- // example: bfcc6d75-a6a5-4bc6-8282-47aec8531818
- return nameOrMbid != null && nameOrMbid.length() == 36 && MBID_PATTERN.matcher(nameOrMbid).matches();
- }
-
- /**
- * Creates a Map out of an array with Strings.
- *
- * @param strings input strings, key-value alternating
- * @return a parameter map
- */
- public static Map<String, String> map(String... strings) {
- if (strings.length % 2 != 0)
- throw new IllegalArgumentException("strings.length % 2 != 0");
- Map<String, String> mp = new HashMap<String, String>();
- for (int i = 0; i < strings.length; i += 2) {
- mp.put(strings[i], strings[i + 1]);
- }
- return mp;
- }
-
- /**
- * Strips all characters from a String, that might be invalid to be used in file names.
- * By default <tt>: / \ < > | ? " *</tt> are all replaced by <tt>-</tt>.
- * Note that this is no guarantee that the returned name will be definately valid.
- *
- * @param s the String to clean up
- * @return the cleaned up String
- */
- public static String cleanUp(String s) {
- return s.replaceAll("[*:/\\\\?|<>\"]", "-");
- }
-
- /**
- * Tests if the given string <i>might</i> already be a 32-char md5 string.
- *
- * @param s String to test
- * @return <code>true</code> if the given String might be a md5 string
- */
- public static boolean isMD5(String s) {
- return s.length() == 32 && MD5_PATTERN.matcher(s).matches();
- }
-
- /**
- * Converts a Last.fm boolean result string to a boolean.
- *
- * @param resultString A Last.fm boolean result string.
- * @return <code>true</code> if the given String represents a true, <code>false</code> otherwise.
- */
- public static boolean convertToBoolean(String resultString) {
- return "1".equals(resultString);
- }
-
- /**
- * Converts from a boolean to a Last.fm boolean result string.
- *
- * @param value A boolean value.
- * @return A string representing a Last.fm boolean.
- */
- public static String convertFromBoolean(boolean value) {
- if (value) {
- return "1";
- } else {
- return "0";
- }
- }
-
-}
diff --git a/src/com/andrew/apollo/utils/ThemeUtils.java b/src/com/andrew/apollo/utils/ThemeUtils.java
index c1daa44..bd24d10 100644
--- a/src/com/andrew/apollo/utils/ThemeUtils.java
+++ b/src/com/andrew/apollo/utils/ThemeUtils.java
@@ -1,303 +1,391 @@
-/**
- *
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.utils;
-import android.app.ActionBar;
+import android.app.Activity;
import android.content.Context;
+import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
-import android.support.v4.view.ViewPager;
-import android.view.MenuItem;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.preference.PreferenceManager;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
import android.view.View;
-import android.widget.ImageButton;
-import android.widget.ImageView;
-import android.widget.SeekBar;
import android.widget.TextView;
-import com.andrew.apollo.Constants;
+import com.actionbarsherlock.app.ActionBar;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+import com.andrew.apollo.R;
/**
- * @author Andrew Neal TODO - clean this up
+ * In order to implement the theme chooser for Apollo, this class returns a
+ * {@link Resources} object that can be used like normal. In other words, when
+ * {@code getDrawable()} or {@code getColor()} is called, the object returned is
+ * from the current theme package name and because all of the theme resource
+ * identifiers are the same as all of Apollo's resources a little less code is
+ * used to implement the theme chooser.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
*/
-public class ThemeUtils implements Constants {
+public class ThemeUtils {
/**
- * @param context
- * @param default_theme
- * @return theme package name
+ * Used to searc the "Apps" section of the Play Store for "Apollo Themes".
*/
- public static String getThemePackageName(Context context, String default_theme) {
- SharedPreferences sp = context.getSharedPreferences(APOLLO_PREFERENCES, 0);
- return sp.getString(THEME_PACKAGE_NAME, default_theme);
- }
+ private static final String SEARCH_URI = "https://market.android.com/search?q=%s&c=apps&featured=APP_STORE_SEARCH";
/**
- * @param context
- * @param packageName
+ * Used to search the Play Store for a specific theme.
*/
- public static void setThemePackageName(Context context, String packageName) {
- SharedPreferences sp = context.getSharedPreferences(APOLLO_PREFERENCES, 0);
- SharedPreferences.Editor editor = sp.edit();
- editor.putString(THEME_PACKAGE_NAME, packageName);
- editor.commit();
- }
+ private static final String APP_URI = "market://details?id=";
/**
- * @param themeResources
- * @param themePackage
- * @param item_name
- * @param item
- * @param themeType
+ * Default package name.
*/
- public static void loadThemeResource(Resources themeResources, String themePackage,
- String item_name, View item, int themeType) {
- Drawable d = null;
- if (themeResources != null) {
- int resource_id = themeResources.getIdentifier(item_name, "drawable", themePackage);
- if (resource_id != 0) {
- try {
- d = themeResources.getDrawable(resource_id);
- } catch (Resources.NotFoundException e) {
- return;
- }
- if (themeType == THEME_ITEM_FOREGROUND && item instanceof ImageView) {
- Drawable tmp = ((ImageView)item).getDrawable();
- if (tmp != null) {
- tmp.setCallback(null);
- tmp = null;
- }
- ((ImageView)item).setImageDrawable(d);
- } else {
- Drawable tmp = item.getBackground();
- if (tmp != null) {
- tmp.setCallback(null);
- tmp = null;
- }
- item.setBackgroundDrawable(d);
- }
- }
+ public static final String APOLLO_PACKAGE = "com.andrew.apollo";
+
+ /**
+ * Current theme package name.
+ */
+ public static final String PACKAGE_NAME = "theme_package_name";
+
+ /**
+ * Used to get and set the theme package name.
+ */
+ private final SharedPreferences mPreferences;
+
+ /**
+ * The theme package name.
+ */
+ private final String mThemePackage;
+
+ /**
+ * The keyword to use when search for different themes.
+ */
+ private static String sApolloSearch;
+
+ /**
+ * This is the current theme color as set by the color picker.
+ */
+ private final int mCurrentThemeColor;
+
+ /**
+ * Package manager
+ */
+ private final PackageManager mPackageManager;
+
+ /**
+ * Custom action bar layout
+ */
+ private final View mActionBarLayout;
+
+ /**
+ * The theme resources.
+ */
+ private Resources mResources;
+
+ /**
+ * Constructor for <code>ThemeUtils</code>
+ *
+ * @param context The {@link Context} to use.
+ */
+ public ThemeUtils(final Context context) {
+ // Get the search query
+ sApolloSearch = context.getString(R.string.apollo_themes_shop_key);
+ // Get the preferences
+ mPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+ // Get the theme package name
+ mThemePackage = getThemePackageName();
+ // Initialze the package manager
+ mPackageManager = context.getPackageManager();
+ try {
+ // Find the theme resources
+ mResources = mPackageManager.getResourcesForApplication(mThemePackage);
+ } catch (final Exception e) {
+ // If the user isn't using a theme, then the resources should be
+ // Apollo's.
+ setThemePackageName(APOLLO_PACKAGE);
}
+ // Get the current theme color
+ mCurrentThemeColor = PreferenceUtils.getInstace(context).getDefaultThemeColor(context);
+ // Inflate the custom layout
+ mActionBarLayout = LayoutInflater.from(context).inflate(R.layout.action_bar, null);
}
/**
- * @param mContext
- * @param view
- * @param resourceName
- * @param themeType
+ * Set the new theme package name.
+ *
+ * @param packageName The package name of the theme to be set.
*/
- public static void initThemeChooser(Context mContext, View view, String resourceName,
- int themeType) {
- String themePackage = getThemePackageName(mContext, APOLLO);
- PackageManager pm = mContext.getPackageManager();
- Resources themeResources = null;
- if (!themePackage.equals(APOLLO)) {
- try {
- themeResources = pm.getResourcesForApplication(themePackage);
- } catch (NameNotFoundException e) {
- setThemePackageName(mContext, APOLLO);
+ public void setThemePackageName(final String packageName) {
+ ApolloUtils.execute(false, new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(final Void... unused) {
+ final SharedPreferences.Editor editor = mPreferences.edit();
+ editor.putString(PACKAGE_NAME, packageName);
+ SharedPreferencesCompat.apply(editor);
+ return null;
}
- }
+ }, (Void[])null);
+ }
- if (themeResources != null)
- loadThemeResource(themeResources, themePackage, resourceName, view, themeType);
+ /**
+ * Return the current theme package name.
+ *
+ * @return The default theme package name.
+ */
+ public final String getThemePackageName() {
+ return mPreferences.getString(PACKAGE_NAME, APOLLO_PACKAGE);
}
/**
- * @param mContext
- * @param view
- * @param resourceName
+ * Used to return a color from the theme resources.
+ *
+ * @param resourceName The name of the color to return. i.e.
+ * "action_bar_color".
+ * @return A new color from the theme resources.
*/
- public static void setTextColor(Context mContext, TextView view, String resourceName) {
- String themePackage = getThemePackageName(mContext, APOLLO);
- PackageManager pm = mContext.getPackageManager();
- Resources themeResources = null;
- if (!themePackage.equals(APOLLO)) {
- try {
- themeResources = pm.getResourcesForApplication(themePackage);
- } catch (NameNotFoundException e) {
- setThemePackageName(mContext, APOLLO);
- }
- }
- if (themeResources != null) {
- int resourceID = themeResources.getIdentifier(resourceName, "color", themePackage);
- if (resourceID != 0) {
- view.setTextColor(themeResources.getColor(resourceID));
- }
+ public int getColor(final String resourceName) {
+ final int resourceId = mResources.getIdentifier(resourceName, "color", mThemePackage);
+ try {
+ return mResources.getColor(resourceId);
+ } catch (final Resources.NotFoundException e) {
+ // If the theme designer wants to allow the user to theme a
+ // particular object via the color picker, they just remove the
+ // resource item from the themeconfig.xml file.
}
+ return mCurrentThemeColor;
}
/**
- * @param mContext
- * @param view
- * @param resourceName
+ * Used to return a drawable from the theme resources.
+ *
+ * @param resourceName The name of the drawable to return. i.e.
+ * "pager_background".
+ * @return A new color from the theme resources.
*/
- public static void setBackgroundColor(Context mContext, View view, String resourceName) {
- String themePackage = getThemePackageName(mContext, APOLLO);
- PackageManager pm = mContext.getPackageManager();
- Resources themeResources = null;
- if (!themePackage.equals(APOLLO)) {
- try {
- themeResources = pm.getResourcesForApplication(themePackage);
- } catch (NameNotFoundException e) {
- setThemePackageName(mContext, APOLLO);
- }
- }
- if (themeResources != null) {
- int resourceID = themeResources.getIdentifier(resourceName, "color", themePackage);
- if (resourceID != 0) {
- view.setBackgroundColor(themeResources.getColor(resourceID));
- }
+ public Drawable getDrawable(final String resourceName) {
+ final int resourceId = mResources.getIdentifier(resourceName, "drawable", mThemePackage);
+ try {
+ return mResources.getDrawable(resourceId);
+ } catch (final Resources.NotFoundException e) {
+ //$FALL-THROUGH$
}
+ return null;
}
/**
- * @param mContext
- * @param view
- * @param resourceName
+ * Used to tell if the action bar's backgrond color is dark or light and
+ * depending on which the proper overflow icon is set from a style.
+ *
+ * @return True if the action bar color is dark, false if light.
*/
- public static void setImageButton(Context mContext, ImageButton view, String resourceName) {
- String themePackage = getThemePackageName(mContext, APOLLO);
- PackageManager pm = mContext.getPackageManager();
- Resources themeResources = null;
- if (!themePackage.equals(APOLLO)) {
- try {
- themeResources = pm.getResourcesForApplication(themePackage);
- } catch (NameNotFoundException e) {
- setThemePackageName(mContext, APOLLO);
- }
- }
- if (themeResources != null) {
- int resourceID = themeResources.getIdentifier(resourceName, "drawable", themePackage);
- if (resourceID != 0) {
- view.setImageDrawable(themeResources.getDrawable(resourceID));
- }
- }
+ public boolean isActionBarDark() {
+ return ApolloUtils.isColorDark(getColor("action_bar"));
}
/**
- * @param mContext
- * @param view
- * @param resourceName
+ * Sets the corret overflow icon in the action bar depending on whether or
+ * not the current action bar color is dark or light.
+ *
+ * @param app The {@link Activity} used to set the theme.
*/
- public static void setMarginDrawable(Context mContext, ViewPager view, String resourceName) {
- String themePackage = getThemePackageName(mContext, APOLLO);
- PackageManager pm = mContext.getPackageManager();
- Resources themeResources = null;
- if (!themePackage.equals(APOLLO)) {
- try {
- themeResources = pm.getResourcesForApplication(themePackage);
- } catch (NameNotFoundException e) {
- setThemePackageName(mContext, APOLLO);
- }
- }
- if (themeResources != null) {
- int resourceID = themeResources.getIdentifier(resourceName, "drawable", themePackage);
- if (resourceID != 0) {
- view.setPageMarginDrawable(themeResources.getDrawable(resourceID));
- }
+ public void setOverflowStyle(final Activity app) {
+ if (isActionBarDark()) {
+ app.setTheme(R.style.Apollo_Theme_Dark);
+ } else {
+ app.setTheme(R.style.Apollo_Theme_Light);
}
}
/**
- * @param mContext
- * @param view
- * @param resourceName
+ * This is used to set the color of a {@link MenuItem}. For instance, when
+ * the current song is a favorite, the favorite icon will use the current
+ * theme color.
+ *
+ * @param menuItem The {@link MenuItem} to set.
+ * @param resourceColorName The color theme resource key.
+ * @param resourceDrawableName The drawable theme resource key.
*/
- public static void setActionBarBackground(Context mContext, ActionBar view, String resourceName) {
- String themePackage = getThemePackageName(mContext, APOLLO);
- PackageManager pm = mContext.getPackageManager();
- Resources themeResources = null;
- if (!themePackage.equals(APOLLO)) {
- try {
- themeResources = pm.getResourcesForApplication(themePackage);
- } catch (NameNotFoundException e) {
- setThemePackageName(mContext, APOLLO);
- }
- }
- if (themeResources != null) {
- int resourceID = themeResources.getIdentifier(resourceName, "drawable", themePackage);
- if (resourceID != 0) {
- view.setBackgroundDrawable(themeResources.getDrawable(resourceID));
- }
+ public void setMenuItemColor(final MenuItem menuItem, final String resourceColorName,
+ final String resourceDrawableName) {
+
+ final Drawable maskDrawable = getDrawable(resourceDrawableName);
+ if (!(maskDrawable instanceof BitmapDrawable)) {
+ return;
}
+
+ final Bitmap maskBitmap = ((BitmapDrawable)maskDrawable).getBitmap();
+ final int width = maskBitmap.getWidth();
+ final int height = maskBitmap.getHeight();
+
+ final Bitmap outBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ final Canvas canvas = new Canvas(outBitmap);
+ canvas.drawBitmap(maskBitmap, 0, 0, null);
+
+ final Paint maskedPaint = new Paint();
+ maskedPaint.setColor(getColor(resourceColorName));
+ maskedPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP));
+
+ canvas.drawRect(0, 0, width, height, maskedPaint);
+
+ final BitmapDrawable outDrawable = new BitmapDrawable(mResources, outBitmap);
+ menuItem.setIcon(outDrawable);
}
/**
- * @param mContext
- * @param view
- * @param resourceName
+ * Sets the {@link MenuItem} icon for the favorites action.
+ *
+ * @param context The {@link Context} to use.
+ * @param favorite The favorites action.
*/
- public static void setActionBarItem(Context mContext, MenuItem view, String resourceName) {
- String themePackage = getThemePackageName(mContext, APOLLO);
- PackageManager pm = mContext.getPackageManager();
- Resources themeResources = null;
- if (!themePackage.equals(APOLLO)) {
- try {
- themeResources = pm.getResourcesForApplication(themePackage);
- } catch (NameNotFoundException e) {
- setThemePackageName(mContext, APOLLO);
- }
- }
- if (themeResources != null) {
- int resourceID = themeResources.getIdentifier(resourceName, "drawable", themePackage);
- if (resourceID != 0) {
- view.setIcon(themeResources.getDrawable(resourceID));
- }
+ public void setFavoriteIcon(final Menu favorite) {
+ final MenuItem favoriteAction = favorite.findItem(R.id.menu_favorite);
+ final String favoriteIconId = "ic_action_favorite";
+ if (MusicUtils.isFavorite()) {
+ setMenuItemColor(favoriteAction, "favorite_selected", favoriteIconId);
+ } else {
+ setMenuItemColor(favoriteAction, "favorite_normal", favoriteIconId);
}
}
/**
- * @param mContext
- * @param view
- * @param resourceName
+ * Sets the {@link MenuItem} icon for the search action.
+ *
+ * @param context The {@link Context} to use.
+ * @param search The Menu used to find the "menu_search" action.
*/
- public static void setProgessDrawable(Context mContext, SeekBar view, String resourceName) {
- String themePackage = getThemePackageName(mContext, APOLLO);
- PackageManager pm = mContext.getPackageManager();
- Resources themeResources = null;
- if (!themePackage.equals(APOLLO)) {
- try {
- themeResources = pm.getResourcesForApplication(themePackage);
- } catch (NameNotFoundException e) {
- setThemePackageName(mContext, APOLLO);
- }
- }
- if (themeResources != null) {
- int resourceID = themeResources.getIdentifier(resourceName, "drawable", themePackage);
- if (resourceID != 0) {
- view.setProgressDrawable(themeResources.getDrawable(resourceID));
- }
- }
+ public void setSearchIcon(final Menu search) {
+ final MenuItem searchAction = search.findItem(R.id.menu_search);
+ final String searchIconId = "ic_action_search";
+ setMenuItemColor(searchAction, "search_action", searchIconId);
}
/**
- * @param mContext
- * @return which overflow icon to use
+ * Sets the {@link MenuItem} icon for the shop action.
+ *
+ * @param context The {@link Context} to use.
+ * @param search The Menu used to find the "menu_shop" action.
*/
- public static boolean overflowLight(Context mContext) {
- String themePackage = getThemePackageName(mContext, APOLLO);
- PackageManager pm = mContext.getPackageManager();
- Resources themeResources = null;
- if (!themePackage.equals(APOLLO)) {
- try {
- themeResources = pm.getResourcesForApplication(themePackage);
- } catch (NameNotFoundException e) {
- setThemePackageName(mContext, APOLLO);
- }
+ public void setShopIcon(final Menu search) {
+ final MenuItem shopAction = search.findItem(R.id.menu_shop);
+ final String shopIconId = "ic_action_shop";
+ setMenuItemColor(shopAction, "shop_action", shopIconId);
+ }
+
+ /**
+ * Sets the {@link MenuItem} icon for the add to Home screen action.
+ *
+ * @param context The {@link Context} to use.
+ * @param search The Menu used to find the "add_to_homescreen" item.
+ */
+ public void setAddToHomeScreenIcon(final Menu search) {
+ final MenuItem pinnAction = search.findItem(R.id.menu_add_to_homescreen);
+ final String pinnIconId = "ic_action_pinn_to_home";
+ setMenuItemColor(pinnAction, "pinn_to_action", pinnIconId);
+ }
+
+ /**
+ * Builds a custom layout and applies it to the action bar, then themes the
+ * background, title, and subtitle.
+ *
+ * @param actionBar The {@link ActionBar} to use.
+ * @param resources The {@link ThemeUtils} used to theme the background,
+ * title, and subtitle.
+ * @param title The title for the action bar
+ * @param subtitle The subtitle for the action bar.
+ */
+ public void themeActionBar(final ActionBar actionBar, final String title) {
+ // Set the custom layout
+ actionBar.setCustomView(mActionBarLayout);
+ actionBar.setDisplayShowCustomEnabled(true);
+ actionBar.setDisplayShowTitleEnabled(false);
+
+ // Theme the action bar background
+ actionBar.setBackgroundDrawable(getDrawable("action_bar"));
+
+ // Theme the title
+ setTitle(title);
+ }
+
+ /**
+ * Themes the action bar subtitle
+ *
+ * @param subtitle The subtitle to use
+ */
+ public void setTitle(final String title) {
+ if (!TextUtils.isEmpty(title)) {
+ // Get the title text view
+ final TextView actionBarTitle = (TextView)mActionBarLayout
+ .findViewById(R.id.action_bar_title);
+ // Theme the title
+ actionBarTitle.setTextColor(getColor("action_bar_title"));
+ // Set the title
+ actionBarTitle.setText(title);
}
- if (themeResources != null) {
- int resourceID = themeResources.getIdentifier("overflow.light", "bool", themePackage);
- if (resourceID != 0) {
- Boolean overflow = themeResources.getBoolean(resourceID);
- if (overflow)
- return true;
- }
+ }
+
+ /**
+ * Themes the action bar subtitle
+ *
+ * @param subtitle The subtitle to use
+ */
+ public void setSubtitle(final String subtitle) {
+ if (!TextUtils.isEmpty(subtitle)) {
+ final TextView actionBarSubtitle = (TextView)mActionBarLayout
+ .findViewById(R.id.action_bar_subtitle);
+ actionBarSubtitle.setVisibility(View.VISIBLE);
+ // Theme the subtitle
+ actionBarSubtitle.setTextColor(getColor("action_bar_subtitle"));
+ // Set the subtitle
+ actionBarSubtitle.setText(subtitle);
}
- return false;
+ }
+
+ /**
+ * Used to search the Play Store for "Apollo Themes".
+ *
+ * @param context The {@link Context} to use.
+ */
+ public void shopFor(final Context context) {
+ final Intent shopIntent = new Intent(Intent.ACTION_VIEW);
+ shopIntent.setData(Uri.parse(String.format(SEARCH_URI, Uri.encode(sApolloSearch))));
+ shopIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ shopIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ context.startActivity(shopIntent);
+ }
+
+ /**
+ * Used to search the Play Store for a specific app.
+ *
+ * @param context The {@link Context} to use.
+ * @param themeName The theme name to search for.
+ */
+ public static void openAppPage(final Context context, final String themeName) {
+ final Intent shopIntent = new Intent(Intent.ACTION_VIEW);
+ shopIntent.setData(Uri.parse(APP_URI + themeName));
+ shopIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ shopIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ context.startActivity(shopIntent);
}
}
diff --git a/src/com/andrew/apollo/utils/Ticker.java b/src/com/andrew/apollo/utils/Ticker.java
new file mode 100644
index 0000000..9af25a2
--- /dev/null
+++ b/src/com/andrew/apollo/utils/Ticker.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2011 The Guava Authors 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 com.andrew.apollo.utils;
+
+/**
+ * A time source; returns a time value representing the number of nanoseconds
+ * elapsed since some fixed but arbitrary point in time. Note that most users
+ * should use {@link Stopwatch} instead of interacting with this class directly.
+ * <p>
+ * <b>Warning:</b> this interface can only be used to measure elapsed time, not
+ * wall time.
+ *
+ * @author Kevin Bourrillion
+ * @since 10.0 (<a
+ * href="http://code.google.com/p/guava-libraries/wiki/Compatibility"
+ * >mostly source-compatible</a> since 9.0)
+ */
+public abstract class Ticker {
+
+ /**
+ * Constructor for use by subclasses.
+ */
+ protected Ticker() {
+ }
+
+ /**
+ * Returns the number of nanoseconds elapsed since this ticker's fixed point
+ * of reference.
+ */
+ public abstract long read();
+
+ /**
+ * A ticker that reads the current time using {@link System#nanoTime}.
+ *
+ * @since 10.0
+ */
+ public static Ticker systemTicker() {
+ return SYSTEM_TICKER;
+ }
+
+ private static final Ticker SYSTEM_TICKER = new Ticker() {
+ @Override
+ public long read() {
+ return System.nanoTime();
+ }
+ };
+}
diff --git a/src/com/andrew/apollo/views/ViewHolderGrid.java b/src/com/andrew/apollo/views/ViewHolderGrid.java
deleted file mode 100644
index cce2fc6..0000000
--- a/src/com/andrew/apollo/views/ViewHolderGrid.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/**
- *
- */
-
-package com.andrew.apollo.views;
-
-import android.view.View;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import com.andrew.apollo.R;
-
-/**
- * @author Andrew Neal
- */
-public class ViewHolderGrid {
-
- public final ImageView mViewHolderImage, mPeakOne, mPeakTwo;
-
- public final TextView mViewHolderLineOne;
-
- public final TextView mViewHolderLineTwo;
-
- public int position;
-
- public final LinearLayout mInfoHolder;
-
- public ViewHolderGrid(View view) {
- mViewHolderImage = (ImageView)view.findViewById(R.id.gridview_image);
- mViewHolderLineOne = (TextView)view.findViewById(R.id.gridview_line_one);
- mViewHolderLineTwo = (TextView)view.findViewById(R.id.gridview_line_two);
- mPeakOne = (ImageView)view.findViewById(R.id.peak_one);
- mPeakTwo = (ImageView)view.findViewById(R.id.peak_two);
- mInfoHolder = (LinearLayout)view.findViewById(R.id.gridview_info_holder);
- mInfoHolder.setBackgroundColor(view.getResources().getColor(R.color.transparent_black));
- }
-
-}
diff --git a/src/com/andrew/apollo/views/ViewHolderList.java b/src/com/andrew/apollo/views/ViewHolderList.java
deleted file mode 100644
index d264467..0000000
--- a/src/com/andrew/apollo/views/ViewHolderList.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/**
- *
- */
-
-package com.andrew.apollo.views;
-
-import android.view.View;
-import android.widget.FrameLayout;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import com.andrew.apollo.Constants;
-import com.andrew.apollo.R;
-import com.andrew.apollo.utils.ThemeUtils;
-
-/**
- * @author Andrew Neal
- */
-public class ViewHolderList implements Constants {
-
- public final ImageView mViewHolderImage, mPeakOne, mPeakTwo, mQuickContextDivider,
- mQuickContextTip;
-
- public final TextView mViewHolderLineOne;
-
- public final TextView mViewHolderLineTwo;
-
- public int position;
-
- public final FrameLayout mQuickContext;
-
- public ViewHolderList(View view) {
- mViewHolderImage = (ImageView)view.findViewById(R.id.listview_item_image);
- mViewHolderLineOne = (TextView)view.findViewById(R.id.listview_item_line_one);
- mViewHolderLineTwo = (TextView)view.findViewById(R.id.listview_item_line_two);
- mQuickContext = (FrameLayout)view.findViewById(R.id.track_list_context_frame);
- mPeakOne = (ImageView)view.findViewById(R.id.peak_one);
- mPeakTwo = (ImageView)view.findViewById(R.id.peak_two);
- mQuickContextDivider = (ImageView)view.findViewById(R.id.quick_context_line);
- mQuickContextTip = (ImageView)view.findViewById(R.id.quick_context_tip);
-
- // Theme chooser
- ThemeUtils.setTextColor(view.getContext(), mViewHolderLineOne, "list_view_text_color");
- ThemeUtils.setTextColor(view.getContext(), mViewHolderLineTwo, "list_view_text_color");
- ThemeUtils.setBackgroundColor(view.getContext(), mQuickContextDivider,
- "list_view_quick_context_menu_button_divider");
- }
-}
diff --git a/src/com/andrew/apollo/views/ViewHolderQueue.java b/src/com/andrew/apollo/views/ViewHolderQueue.java
deleted file mode 100644
index f998553..0000000
--- a/src/com/andrew/apollo/views/ViewHolderQueue.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/**
- *
- */
-
-package com.andrew.apollo.views;
-
-import android.view.View;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import com.andrew.apollo.R;
-
-/**
- * @author Andrew Neal
- */
-public class ViewHolderQueue {
-
- public final ImageView mArtistImage, mPeakOne, mPeakTwo;
-
- public final ImageView mAlbumArt;
-
- public final TextView mTrackName;
-
- public int position;
-
- public ViewHolderQueue(View view) {
- mArtistImage = (ImageView)view.findViewById(R.id.queue_artist_image);
- mAlbumArt = (ImageView)view.findViewById(R.id.queue_album_art);
- mTrackName = (TextView)view.findViewById(R.id.queue_track_name);
- mPeakOne = (ImageView)view.findViewById(R.id.peak_one);
- mPeakTwo = (ImageView)view.findViewById(R.id.peak_two);
- }
-
-}
diff --git a/src/com/andrew/apollo/widgets/AlphaPatternDrawable.java b/src/com/andrew/apollo/widgets/AlphaPatternDrawable.java
new file mode 100644
index 0000000..be525bf
--- /dev/null
+++ b/src/com/andrew/apollo/widgets/AlphaPatternDrawable.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2010 Daniel Nilsson Copyright (C) 2012 THe CyanogenMod Project
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by
+ * applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
+ * OF ANY KIND, either express or implied. See the License for the specific
+ * language governing permissions and limitations under the License.
+ */
+
+package com.andrew.apollo.widgets;
+
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+
+/**
+ * This drawable that draws a simple white and gray chess board pattern. It's
+ * pattern you will often see as a background behind a partly transparent image
+ * in many applications.
+ *
+ * @author Daniel Nilsson
+ */
+public class AlphaPatternDrawable extends Drawable {
+
+ private final Paint mPaint = new Paint();
+
+ private final Paint mPaintWhite = new Paint();
+
+ private final Paint mPaintGray = new Paint();
+
+ private int mRectangleSize = 10;
+
+ private int numRectanglesHorizontal;
+
+ private int numRectanglesVertical;
+
+ /* Bitmap in which the pattern will be cached. */
+ private Bitmap mBitmap;
+
+ /**/
+ public AlphaPatternDrawable(final int rectangleSize) {
+ mRectangleSize = rectangleSize;
+ mPaintWhite.setColor(0xffffffff);
+ mPaintGray.setColor(0xffcbcbcb);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void draw(final Canvas canvas) {
+ canvas.drawBitmap(mBitmap, null, getBounds(), mPaint);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getOpacity() {
+ return 0;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setAlpha(final int alpha) {
+ throw new UnsupportedOperationException("Alpha is not supported by this drawable.");
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setColorFilter(final ColorFilter cf) {
+ throw new UnsupportedOperationException("ColorFilter is not supported by this drawable.");
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onBoundsChange(final Rect bounds) {
+ super.onBoundsChange(bounds);
+
+ final int mHeight = bounds.height();
+ final int mWidth = bounds.width();
+
+ numRectanglesHorizontal = (int)Math.ceil((mWidth / mRectangleSize));
+ numRectanglesVertical = (int)Math.ceil(mHeight / mRectangleSize);
+
+ generatePatternBitmap();
+ }
+
+ /**
+ * This will generate a bitmap with the pattern as big as the rectangle we
+ * were allow to draw on. We do this to cache the bitmap so we don't need to
+ * recreate it each time draw() is called since it takes a few milliseconds.
+ */
+ private void generatePatternBitmap() {
+
+ if (getBounds().width() <= 0 || getBounds().height() <= 0) {
+ return;
+ }
+
+ mBitmap = Bitmap.createBitmap(getBounds().width(), getBounds().height(), Config.ARGB_8888);
+ final Canvas mCanvas = new Canvas(mBitmap);
+
+ final Rect mRect = new Rect();
+ boolean mVerticalStartWhite = true;
+ for (int i = 0; i <= numRectanglesVertical; i++) {
+ boolean mIsWhite = mVerticalStartWhite;
+ for (int j = 0; j <= numRectanglesHorizontal; j++) {
+ mRect.top = i * mRectangleSize;
+ mRect.left = j * mRectangleSize;
+ mRect.bottom = mRect.top + mRectangleSize;
+ mRect.right = mRect.left + mRectangleSize;
+
+ mCanvas.drawRect(mRect, mIsWhite ? mPaintWhite : mPaintGray);
+
+ mIsWhite = !mIsWhite;
+ }
+ mVerticalStartWhite = !mVerticalStartWhite;
+ }
+ }
+}
diff --git a/src/com/andrew/apollo/widgets/AlphaTouchInterceptorOverlay.java b/src/com/andrew/apollo/widgets/AlphaTouchInterceptorOverlay.java
new file mode 100644
index 0000000..2e4e484
--- /dev/null
+++ b/src/com/andrew/apollo/widgets/AlphaTouchInterceptorOverlay.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.andrew.apollo.widgets;
+
+import android.content.Context;
+import android.view.View;
+import android.widget.FrameLayout;
+
+/**
+ * A View that other Views can use to create a touch-interceptor layer above
+ * their other sub-views. This layer can be enabled and disabled; when enabled,
+ * clicks are intercepted and passed to a listener. Also supports an alpha layer
+ * to dim the content underneath. By default, the alpha layer is the same View
+ * as the touch-interceptor layer. However, for some use-cases, you want a few
+ * Views to not be dimmed, but still have touches intercepted (for example,
+ * {@link CarouselTab}'s label appears above the alpha layer). In this case, you
+ * can specify the View to use as the alpha layer via setAlphaLayer(); in this
+ * case you are responsible for managing the z-order of the alpha-layer with
+ * respect to your other sub-views. Typically, you would not use this class
+ * directly, but rather use another class that uses it, for example
+ * {@link FrameLayoutWithOverlay}.
+ */
+public class AlphaTouchInterceptorOverlay extends FrameLayout {
+
+ private final View mInterceptorLayer;
+
+ private float mAlpha = 0.0f;
+
+ private View mAlphaLayer;
+
+ /**
+ * @param context The {@link Context} to use.
+ */
+ public AlphaTouchInterceptorOverlay(final Context context) {
+ super(context);
+
+ mInterceptorLayer = new View(context);
+ mInterceptorLayer.setBackgroundColor(0);
+ addView(mInterceptorLayer);
+
+ mAlphaLayer = this;
+ }
+
+ /**
+ * Set the View that the overlay will use as its alpha-layer. If none is set
+ * it will use itself. Only necessary to set this if some child views need
+ * to appear above the alpha-layer but below the touch-interceptor.
+ */
+ public void setAlphaLayer(final View alphaLayer) {
+ if (mAlphaLayer == alphaLayer) {
+ return;
+ }
+
+ /* We're no longer the alpha-layer, so make ourself invisible. */
+ if (mAlphaLayer == this) {
+ setAlphaOnViewBackground(this, 0.0f);
+ }
+
+ mAlphaLayer = alphaLayer == null ? this : alphaLayer;
+ setAlphaLayerValue(mAlpha);
+ }
+
+ /** Sets the alpha value on the alpha layer. */
+ public void setAlphaLayerValue(final float alpha) {
+ mAlpha = alpha;
+ if (mAlphaLayer != null) {
+ setAlphaOnViewBackground(mAlphaLayer, mAlpha);
+ }
+ }
+
+ /** Delegate to interceptor-layer. */
+ public void setOverlayOnClickListener(final OnClickListener listener) {
+ mInterceptorLayer.setOnClickListener(listener);
+ }
+
+ /** Delegate to interceptor-layer. */
+ public void setOverlayClickable(final boolean clickable) {
+ mInterceptorLayer.setClickable(clickable);
+ }
+
+ /**
+ * Sets an alpha value on the view.
+ */
+ public static void setAlphaOnViewBackground(final View view, final float alpha) {
+ if (view != null) {
+ view.setBackgroundColor((int)(clamp(alpha, 0.0f, 1.0f) * 255) << 24);
+ }
+ }
+
+ /**
+ * If the input value lies outside of the specified range, return the nearer
+ * bound. Otherwise, return the input value, unchanged.
+ */
+ public static float clamp(final float input, final float lowerBound, final float upperBound) {
+ if (input < lowerBound) {
+ return lowerBound;
+ } else if (input > upperBound) {
+ return upperBound;
+ }
+ return input;
+ }
+
+}
diff --git a/src/com/andrew/apollo/widgets/CarouselTab.java b/src/com/andrew/apollo/widgets/CarouselTab.java
new file mode 100644
index 0000000..6e82521
--- /dev/null
+++ b/src/com/andrew/apollo/widgets/CarouselTab.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.widgets;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.andrew.apollo.Config;
+import com.andrew.apollo.R;
+import com.andrew.apollo.cache.ImageFetcher;
+import com.andrew.apollo.utils.ApolloUtils;
+import com.andrew.apollo.utils.BitmapUtils;
+import com.andrew.apollo.utils.MusicUtils;
+
+/**
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+@SuppressLint("NewApi")
+public class CarouselTab extends FrameLayoutWithOverlay {
+
+ private ImageView mPhoto;
+
+ private ImageView mAlbumArt;
+
+ private TextView mLabelView;
+
+ private View mAlphaLayer;
+
+ private View mColorstrip;
+
+ private final ImageFetcher mFetcher;
+
+ /**
+ * @param context The {@link Context} to use
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ */
+ public CarouselTab(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ mFetcher = ApolloUtils.getImageFetcher((SherlockFragmentActivity)context);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mPhoto = (ImageView)findViewById(R.id.profile_tab_photo);
+ mAlbumArt = (ImageView)findViewById(R.id.profile_tab_album_art);
+ mLabelView = (TextView)findViewById(R.id.profile_tab_label);
+ mAlphaLayer = findViewById(R.id.profile_tab_alpha_overlay);
+ mColorstrip = findViewById(R.id.profile_tab_colorstrip);
+ // Set the alpha layer
+ setAlphaLayer(mAlphaLayer);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setSelected(final boolean selected) {
+ super.setSelected(selected);
+ if (selected) {
+ mColorstrip.setVisibility(View.VISIBLE);
+ } else {
+ mColorstrip.setVisibility(View.GONE);
+ }
+ }
+
+ /**
+ * Used to set the artist image in the artist profile.
+ *
+ * @param context The {@link Context} to use.
+ * @param artist The name of the artist in the profile the user is viewing.
+ */
+ public void setArtistPhoto(final SherlockFragmentActivity context, final String artist) {
+ if (!TextUtils.isEmpty(artist)) {
+ mFetcher.loadArtistImage(artist, mPhoto);
+ } else {
+ setDefault(context);
+ }
+ }
+
+ /**
+ * Used to blur the artist image in the album profile.
+ *
+ * @param context The {@link Context} to use.
+ * @param artist The artist nmae used to fetch the cached artist image.
+ * @param album The album name used to fetch the album art in case the
+ * artist image is missing.
+ */
+ public void blurPhoto(final SherlockFragmentActivity context, final String artist,
+ final String album) {
+ // First check for the artist image
+ Bitmap artistImage = mFetcher.getCachedBitmap(artist);
+ // Second check for cached artwork
+ if (artistImage == null) {
+ artistImage = mFetcher.getCachedArtwork(album);
+ }
+ // If all else, use the default image
+ if (artistImage == null) {
+ artistImage = BitmapFactory.decodeResource(getResources(), R.drawable.theme_preview);
+ }
+ final Bitmap blur = BitmapUtils.createBlurredBitmap(artistImage);
+ mPhoto.setImageBitmap(blur);
+ }
+
+ /**
+ * Used to set the album art in the album profile.
+ *
+ * @param context The {@link Context} to use.
+ * @param album The name of the album in the profile the user is viewing.
+ */
+ public void setAlbumPhoto(final SherlockFragmentActivity context, final String album) {
+ if (!TextUtils.isEmpty(album)) {
+ mAlbumArt.setVisibility(View.VISIBLE);
+ mFetcher.loadAlbumImage(MusicUtils.getAlbumArtist(context, album), album,
+ String.valueOf(MusicUtils.getIdForAlbum(context, album)), mAlbumArt);
+ } else {
+ setDefault(context);
+ }
+ }
+
+ /**
+ * Used to fetch for the album art via Last.fm.
+ *
+ * @param context The {@link Context} to use.
+ * @param album The name of the album in the profile the user is viewing.
+ */
+ public void fetchAlbumPhoto(final SherlockFragmentActivity context, final String album) {
+ if (!TextUtils.isEmpty(album)) {
+ mFetcher.removeFromCache(album + Config.ALBUM_ART_SUFFIX);
+ mFetcher.loadAlbumImage(MusicUtils.getAlbumArtist(context, album), album, null,
+ mAlbumArt);
+ } else {
+ setDefault(context);
+ }
+ }
+
+ /**
+ * Used to set the album art in the artist profile.
+ *
+ * @param context The {@link Context} to use.
+ * @param artist The name of the artist in the profile the user is viewing.
+ */
+ public void setArtistAlbumPhoto(final SherlockFragmentActivity context, final String artist) {
+ final String lastAlbum = MusicUtils.getLastAlbumForArtist(context, artist);
+ if (!TextUtils.isEmpty(lastAlbum)) {
+ // Set the last album the artist played
+ mFetcher.loadAlbumImage(artist, lastAlbum,
+ String.valueOf(MusicUtils.getIdForAlbum(context, lastAlbum)), mPhoto);
+ // Play the album
+ mPhoto.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(final View v) {
+ final long[] albumList = MusicUtils.getSongListForAlbum(getContext(),
+ String.valueOf(MusicUtils.getIdForAlbum(context, lastAlbum)));
+ MusicUtils.playAll(getContext(), albumList, 0, false);
+ }
+ });
+ } else {
+ setDefault(context);
+ }
+ }
+
+ /**
+ * Used to set the header image for playlists and genres.
+ *
+ * @param context The {@link Context} to use.
+ * @param profileName The key used to fetch the image.
+ */
+ public void setPlaylistOrGenrePhoto(final SherlockFragmentActivity context,
+ final String profileName) {
+ if (!TextUtils.isEmpty(profileName)) {
+ final Bitmap image = mFetcher.getCachedBitmap(profileName);
+ if (image != null) {
+ mPhoto.setImageBitmap(image);
+ } else {
+ setDefault(context);
+ }
+ } else {
+ setDefault(context);
+ }
+ }
+
+ /**
+ * @param context The {@link Context} to use.
+ */
+ public void setDefault(final Context context) {
+ mPhoto.setImageDrawable(context.getResources().getDrawable(R.drawable.header_temp));
+ }
+
+ /**
+ * @param label The string to set as the labe.
+ */
+ public void setLabel(final String label) {
+ mLabelView.setText(label);
+ }
+
+ /**
+ * Selects the label view.
+ */
+ public void showSelectedState() {
+ mLabelView.setSelected(true);
+ }
+
+ /**
+ * Deselects the label view.
+ */
+ public void showDeselectedState() {
+ mLabelView.setSelected(false);
+ }
+
+ /**
+ * @return The {@link ImageView} used to set the header photo.
+ */
+ public ImageView getPhoto() {
+ return mPhoto;
+ }
+
+ /**
+ * @return The {@link ImageView} used to set the album art .
+ */
+ public ImageView getAlbumArt() {
+ return mAlbumArt;
+ }
+
+}
diff --git a/src/com/andrew/apollo/widgets/ColorPanelView.java b/src/com/andrew/apollo/widgets/ColorPanelView.java
new file mode 100644
index 0000000..afeb3b7
--- /dev/null
+++ b/src/com/andrew/apollo/widgets/ColorPanelView.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2010 Daniel Nilsson Copyright (C) 2012 THe CyanogenMod Project
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by
+ * applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
+ * OF ANY KIND, either express or implied. See the License for the specific
+ * language governing permissions and limitations under the License.
+ */
+
+package com.andrew.apollo.widgets;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.View;
+
+/**
+ * This class draws a panel which which will be filled with a color which can be
+ * set. It can be used to show the currently selected color which you will get
+ * from the {@link ColorPickerView}.
+ *
+ * @author Daniel Nilsson
+ */
+public class ColorPanelView extends View {
+
+ /**
+ * The width in pixels of the border surrounding the color panel.
+ */
+ private final static float BORDER_WIDTH_PX = 1;
+
+ private static float mDensity = 1f;
+
+ private int mBorderColor = 0xff6E6E6E;
+
+ private int mColor = 0xff000000;
+
+ private Paint mBorderPaint;
+
+ private Paint mColorPaint;
+
+ private RectF mDrawingRect;
+
+ private RectF mColorRect;
+
+ private AlphaPatternDrawable mAlphaPattern;
+
+ public ColorPanelView(final Context context) {
+ this(context, null);
+ }
+
+ public ColorPanelView(final Context context, final AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ColorPanelView(final Context context, final AttributeSet attrs, final int defStyle) {
+ super(context, attrs, defStyle);
+ init();
+ }
+
+ private void init() {
+ mBorderPaint = new Paint();
+ mColorPaint = new Paint();
+ mDensity = getContext().getResources().getDisplayMetrics().density;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onDraw(final Canvas canvas) {
+ final RectF rect = mColorRect;
+ if (BORDER_WIDTH_PX > 0) {
+ mBorderPaint.setColor(mBorderColor);
+ canvas.drawRect(mDrawingRect, mBorderPaint);
+ }
+
+ if (mAlphaPattern != null) {
+ mAlphaPattern.draw(canvas);
+ }
+
+ mColorPaint.setColor(mColor);
+ canvas.drawRect(rect, mColorPaint);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
+ final int width = MeasureSpec.getSize(widthMeasureSpec);
+ final int height = MeasureSpec.getSize(heightMeasureSpec);
+ setMeasuredDimension(width, height);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onSizeChanged(final int w, final int h, final int oldw, final int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ mDrawingRect = new RectF();
+ mDrawingRect.left = getPaddingLeft();
+ mDrawingRect.right = w - getPaddingRight();
+ mDrawingRect.top = getPaddingTop();
+ mDrawingRect.bottom = h - getPaddingBottom();
+
+ setUpColorRect();
+ }
+
+ private void setUpColorRect() {
+ final RectF dRect = mDrawingRect;
+
+ final float left = dRect.left + BORDER_WIDTH_PX;
+ final float top = dRect.top + BORDER_WIDTH_PX;
+ final float bottom = dRect.bottom - BORDER_WIDTH_PX;
+ final float right = dRect.right - BORDER_WIDTH_PX;
+
+ mColorRect = new RectF(left, top, right, bottom);
+
+ mAlphaPattern = new AlphaPatternDrawable((int)(5 * mDensity));
+
+ mAlphaPattern.setBounds(Math.round(mColorRect.left), Math.round(mColorRect.top),
+ Math.round(mColorRect.right), Math.round(mColorRect.bottom));
+ }
+
+ /**
+ * Set the color that should be shown by this view.
+ *
+ * @param color
+ */
+ public void setColor(final int color) {
+ mColor = color;
+ invalidate();
+ }
+
+ /**
+ * Get the color currently show by this view.
+ *
+ * @return
+ */
+ public int getColor() {
+ return mColor;
+ }
+
+ /**
+ * Set the color of the border surrounding the panel.
+ *
+ * @param color
+ */
+ public void setBorderColor(final int color) {
+ mBorderColor = color;
+ invalidate();
+ }
+
+ /**
+ * Get the color of the border surrounding the panel.
+ */
+ public int getBorderColor() {
+ return mBorderColor;
+ }
+
+}
diff --git a/src/com/andrew/apollo/widgets/ColorPickerView.java b/src/com/andrew/apollo/widgets/ColorPickerView.java
new file mode 100644
index 0000000..7ff2f42
--- /dev/null
+++ b/src/com/andrew/apollo/widgets/ColorPickerView.java
@@ -0,0 +1,947 @@
+/*
+ * Copyright (C) 2010 Daniel Nilsson Copyright (C) 2012 THe CyanogenMod Project
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by
+ * applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
+ * OF ANY KIND, either express or implied. See the License for the specific
+ * language governing permissions and limitations under the License.
+ */
+
+package com.andrew.apollo.widgets;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ComposeShader;
+import android.graphics.LinearGradient;
+import android.graphics.Paint;
+import android.graphics.Paint.Align;
+import android.graphics.Paint.Style;
+import android.graphics.Point;
+import android.graphics.PorterDuff;
+import android.graphics.RectF;
+import android.graphics.Shader;
+import android.graphics.Shader.TileMode;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+
+/**
+ * Displays a color picker to the user and allow them to select a color. A
+ * slider for the alpha channel is also available. Enable it by setting
+ * setAlphaSliderVisible(boolean) to true.
+ *
+ * @author Daniel Nilsson
+ */
+public class ColorPickerView extends View {
+
+ public interface OnColorChangedListener {
+ public void onColorChanged(int color);
+ }
+
+ private final static int PANEL_SAT_VAL = 0;
+
+ private final static int PANEL_HUE = 1;
+
+ private final static int PANEL_ALPHA = 2;
+
+ /**
+ * The width in pixels of the border surrounding all color panels.
+ */
+ private final static float BORDER_WIDTH_PX = 1;
+
+ /**
+ * The width in dp of the hue panel.
+ */
+ private float HUE_PANEL_WIDTH = 30f;
+
+ /**
+ * The height in dp of the alpha panel
+ */
+ private float ALPHA_PANEL_HEIGHT = 20f;
+
+ /**
+ * The distance in dp between the different color panels.
+ */
+ private float PANEL_SPACING = 10f;
+
+ /**
+ * The radius in dp of the color palette tracker circle.
+ */
+ private float PALETTE_CIRCLE_TRACKER_RADIUS = 5f;
+
+ /**
+ * The dp which the tracker of the hue or alpha panel will extend outside of
+ * its bounds.
+ */
+ private float RECTANGLE_TRACKER_OFFSET = 2f;
+
+ private static float mDensity = 1f;
+
+ private OnColorChangedListener mListener;
+
+ private Paint mSatValPaint;
+
+ private Paint mSatValTrackerPaint;
+
+ private Paint mHuePaint;
+
+ private Paint mHueTrackerPaint;
+
+ private Paint mAlphaPaint;
+
+ private Paint mAlphaTextPaint;
+
+ private Paint mBorderPaint;
+
+ private Shader mValShader;
+
+ private Shader mSatShader;
+
+ private Shader mHueShader;
+
+ private Shader mAlphaShader;
+
+ private int mAlpha = 0xff;
+
+ private float mHue = 360f;
+
+ private float mSat = 0f;
+
+ private float mVal = 0f;
+
+ private String mAlphaSliderText = "Alpha";
+
+ private int mSliderTrackerColor = 0xff1c1c1c;
+
+ private int mBorderColor = 0xff6E6E6E;
+
+ private boolean mShowAlphaPanel = false;
+
+ /*
+ * To remember which panel that has the "focus" when processing hardware
+ * button data.
+ */
+ private int mLastTouchedPanel = PANEL_SAT_VAL;
+
+ /**
+ * Offset from the edge we must have or else the finger tracker will get
+ * clipped when it is drawn outside of the view.
+ */
+ private float mDrawingOffset;
+
+ /*
+ * Distance form the edges of the view of where we are allowed to draw.
+ */
+ private RectF mDrawingRect;
+
+ private RectF mSatValRect;
+
+ private RectF mHueRect;
+
+ private RectF mAlphaRect;
+
+ private AlphaPatternDrawable mAlphaPattern;
+
+ private Point mStartTouchPoint = null;
+
+ public ColorPickerView(final Context context) {
+ this(context, null);
+ }
+
+ public ColorPickerView(final Context context, final AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ColorPickerView(final Context context, final AttributeSet attrs, final int defStyle) {
+ super(context, attrs, defStyle);
+ init();
+ }
+
+ private void init() {
+ mDensity = getContext().getResources().getDisplayMetrics().density;
+ PALETTE_CIRCLE_TRACKER_RADIUS *= mDensity;
+ RECTANGLE_TRACKER_OFFSET *= mDensity;
+ HUE_PANEL_WIDTH *= mDensity;
+ ALPHA_PANEL_HEIGHT *= mDensity;
+ PANEL_SPACING = PANEL_SPACING * mDensity;
+
+ mDrawingOffset = calculateRequiredOffset();
+
+ initPaintTools();
+
+ // Needed for receiving track ball motion events.
+ setFocusable(true);
+ setFocusableInTouchMode(true);
+ }
+
+ private void initPaintTools() {
+
+ mSatValPaint = new Paint();
+ mSatValTrackerPaint = new Paint();
+ mHuePaint = new Paint();
+ mHueTrackerPaint = new Paint();
+ mAlphaPaint = new Paint();
+ mAlphaTextPaint = new Paint();
+ mBorderPaint = new Paint();
+
+ mSatValTrackerPaint.setStyle(Style.STROKE);
+ mSatValTrackerPaint.setStrokeWidth(2f * mDensity);
+ mSatValTrackerPaint.setAntiAlias(true);
+
+ mHueTrackerPaint.setColor(mSliderTrackerColor);
+ mHueTrackerPaint.setStyle(Style.STROKE);
+ mHueTrackerPaint.setStrokeWidth(2f * mDensity);
+ mHueTrackerPaint.setAntiAlias(true);
+
+ mAlphaTextPaint.setColor(0xff1c1c1c);
+ mAlphaTextPaint.setTextSize(14f * mDensity);
+ mAlphaTextPaint.setAntiAlias(true);
+ mAlphaTextPaint.setTextAlign(Align.CENTER);
+ mAlphaTextPaint.setFakeBoldText(true);
+
+ }
+
+ private float calculateRequiredOffset() {
+ float offset = Math.max(PALETTE_CIRCLE_TRACKER_RADIUS, RECTANGLE_TRACKER_OFFSET);
+ offset = Math.max(offset, BORDER_WIDTH_PX * mDensity);
+
+ return offset * 1.5f;
+ }
+
+ private int[] buildHueColorArray() {
+
+ final int[] hue = new int[361];
+
+ int count = 0;
+ for (int i = hue.length - 1; i >= 0; i--, count++) {
+ hue[count] = Color.HSVToColor(new float[] {
+ i, 1f, 1f
+ });
+ }
+
+ return hue;
+ }
+
+ @Override
+ protected void onDraw(final Canvas canvas) {
+
+ if (mDrawingRect.width() <= 0 || mDrawingRect.height() <= 0) {
+ return;
+ }
+
+ drawSatValPanel(canvas);
+ drawHuePanel(canvas);
+ drawAlphaPanel(canvas);
+
+ }
+
+ private void drawSatValPanel(final Canvas canvas) {
+
+ final RectF rect = mSatValRect;
+
+ if (BORDER_WIDTH_PX > 0) {
+ mBorderPaint.setColor(mBorderColor);
+ canvas.drawRect(mDrawingRect.left, mDrawingRect.top, rect.right + BORDER_WIDTH_PX,
+ rect.bottom + BORDER_WIDTH_PX, mBorderPaint);
+ }
+
+ if (mValShader == null) {
+ mValShader = new LinearGradient(rect.left, rect.top, rect.left, rect.bottom,
+ 0xffffffff, 0xff000000, TileMode.CLAMP);
+ }
+
+ final int rgb = Color.HSVToColor(new float[] {
+ mHue, 1f, 1f
+ });
+
+ mSatShader = new LinearGradient(rect.left, rect.top, rect.right, rect.top, 0xffffffff, rgb,
+ TileMode.CLAMP);
+ final ComposeShader mShader = new ComposeShader(mValShader, mSatShader,
+ PorterDuff.Mode.MULTIPLY);
+ mSatValPaint.setShader(mShader);
+
+ canvas.drawRect(rect, mSatValPaint);
+
+ final Point p = satValToPoint(mSat, mVal);
+
+ mSatValTrackerPaint.setColor(0xff000000);
+ canvas.drawCircle(p.x, p.y, PALETTE_CIRCLE_TRACKER_RADIUS - 1f * mDensity,
+ mSatValTrackerPaint);
+
+ mSatValTrackerPaint.setColor(0xffdddddd);
+ canvas.drawCircle(p.x, p.y, PALETTE_CIRCLE_TRACKER_RADIUS, mSatValTrackerPaint);
+
+ }
+
+ private void drawHuePanel(final Canvas canvas) {
+
+ final RectF rect = mHueRect;
+
+ if (BORDER_WIDTH_PX > 0) {
+ mBorderPaint.setColor(mBorderColor);
+ canvas.drawRect(rect.left - BORDER_WIDTH_PX, rect.top - BORDER_WIDTH_PX, rect.right
+ + BORDER_WIDTH_PX, rect.bottom + BORDER_WIDTH_PX, mBorderPaint);
+ }
+
+ if (mHueShader == null) {
+ mHueShader = new LinearGradient(rect.left, rect.top, rect.left, rect.bottom,
+ buildHueColorArray(), null, TileMode.CLAMP);
+ mHuePaint.setShader(mHueShader);
+ }
+
+ canvas.drawRect(rect, mHuePaint);
+
+ final float rectHeight = 4 * mDensity / 2;
+
+ final Point p = hueToPoint(mHue);
+
+ final RectF r = new RectF();
+ r.left = rect.left - RECTANGLE_TRACKER_OFFSET;
+ r.right = rect.right + RECTANGLE_TRACKER_OFFSET;
+ r.top = p.y - rectHeight;
+ r.bottom = p.y + rectHeight;
+
+ canvas.drawRoundRect(r, 2, 2, mHueTrackerPaint);
+
+ }
+
+ private void drawAlphaPanel(final Canvas canvas) {
+
+ if (!mShowAlphaPanel || mAlphaRect == null || mAlphaPattern == null) {
+ return;
+ }
+
+ final RectF rect = mAlphaRect;
+
+ if (BORDER_WIDTH_PX > 0) {
+ mBorderPaint.setColor(mBorderColor);
+ canvas.drawRect(rect.left - BORDER_WIDTH_PX, rect.top - BORDER_WIDTH_PX, rect.right
+ + BORDER_WIDTH_PX, rect.bottom + BORDER_WIDTH_PX, mBorderPaint);
+ }
+
+ mAlphaPattern.draw(canvas);
+
+ final float[] hsv = new float[] {
+ mHue, mSat, mVal
+ };
+ final int color = Color.HSVToColor(hsv);
+ final int acolor = Color.HSVToColor(0, hsv);
+
+ mAlphaShader = new LinearGradient(rect.left, rect.top, rect.right, rect.top, color, acolor,
+ TileMode.CLAMP);
+
+ mAlphaPaint.setShader(mAlphaShader);
+
+ canvas.drawRect(rect, mAlphaPaint);
+
+ if (mAlphaSliderText != null && mAlphaSliderText != "") {
+ canvas.drawText(mAlphaSliderText, rect.centerX(), rect.centerY() + 4 * mDensity,
+ mAlphaTextPaint);
+ }
+
+ final float rectWidth = 4 * mDensity / 2;
+
+ final Point p = alphaToPoint(mAlpha);
+
+ final RectF r = new RectF();
+ r.left = p.x - rectWidth;
+ r.right = p.x + rectWidth;
+ r.top = rect.top - RECTANGLE_TRACKER_OFFSET;
+ r.bottom = rect.bottom + RECTANGLE_TRACKER_OFFSET;
+
+ canvas.drawRoundRect(r, 2, 2, mHueTrackerPaint);
+
+ }
+
+ private Point hueToPoint(final float hue) {
+
+ final RectF rect = mHueRect;
+ final float height = rect.height();
+
+ final Point p = new Point();
+
+ p.y = (int)(height - hue * height / 360f + rect.top);
+ p.x = (int)rect.left;
+
+ return p;
+ }
+
+ private Point satValToPoint(final float sat, final float val) {
+
+ final RectF rect = mSatValRect;
+ final float height = rect.height();
+ final float width = rect.width();
+
+ final Point p = new Point();
+
+ p.x = (int)(sat * width + rect.left);
+ p.y = (int)((1f - val) * height + rect.top);
+
+ return p;
+ }
+
+ private Point alphaToPoint(final int alpha) {
+
+ final RectF rect = mAlphaRect;
+ final float width = rect.width();
+
+ final Point p = new Point();
+
+ p.x = (int)(width - alpha * width / 0xff + rect.left);
+ p.y = (int)rect.top;
+
+ return p;
+
+ }
+
+ private float[] pointToSatVal(float x, float y) {
+
+ final RectF rect = mSatValRect;
+ final float[] result = new float[2];
+
+ final float width = rect.width();
+ final float height = rect.height();
+
+ if (x < rect.left) {
+ x = 0f;
+ } else if (x > rect.right) {
+ x = width;
+ } else {
+ x = x - rect.left;
+ }
+
+ if (y < rect.top) {
+ y = 0f;
+ } else if (y > rect.bottom) {
+ y = height;
+ } else {
+ y = y - rect.top;
+ }
+
+ result[0] = 1.f / width * x;
+ result[1] = 1.f - 1.f / height * y;
+
+ return result;
+ }
+
+ private float pointToHue(float y) {
+
+ final RectF rect = mHueRect;
+
+ final float height = rect.height();
+
+ if (y < rect.top) {
+ y = 0f;
+ } else if (y > rect.bottom) {
+ y = height;
+ } else {
+ y = y - rect.top;
+ }
+
+ return 360f - y * 360f / height;
+ }
+
+ private int pointToAlpha(int x) {
+
+ final RectF rect = mAlphaRect;
+ final int width = (int)rect.width();
+
+ if (x < rect.left) {
+ x = 0;
+ } else if (x > rect.right) {
+ x = width;
+ } else {
+ x = x - (int)rect.left;
+ }
+
+ return 0xff - x * 0xff / width;
+
+ }
+
+ @Override
+ public boolean onTrackballEvent(final MotionEvent event) {
+
+ final float x = event.getX();
+ final float y = event.getY();
+
+ boolean update = false;
+
+ if (event.getAction() == MotionEvent.ACTION_MOVE) {
+
+ switch (mLastTouchedPanel) {
+
+ case PANEL_SAT_VAL:
+
+ float sat,
+ val;
+
+ sat = mSat + x / 50f;
+ val = mVal - y / 50f;
+
+ if (sat < 0f) {
+ sat = 0f;
+ } else if (sat > 1f) {
+ sat = 1f;
+ }
+
+ if (val < 0f) {
+ val = 0f;
+ } else if (val > 1f) {
+ val = 1f;
+ }
+
+ mSat = sat;
+ mVal = val;
+
+ update = true;
+
+ break;
+
+ case PANEL_HUE:
+
+ float hue = mHue - y * 10f;
+
+ if (hue < 0f) {
+ hue = 0f;
+ } else if (hue > 360f) {
+ hue = 360f;
+ }
+
+ mHue = hue;
+
+ update = true;
+
+ break;
+
+ case PANEL_ALPHA:
+
+ if (!mShowAlphaPanel || mAlphaRect == null) {
+ update = false;
+ } else {
+
+ int alpha = (int)(mAlpha - x * 10);
+
+ if (alpha < 0) {
+ alpha = 0;
+ } else if (alpha > 0xff) {
+ alpha = 0xff;
+ }
+
+ mAlpha = alpha;
+
+ update = true;
+ }
+
+ break;
+ }
+
+ }
+
+ if (update) {
+
+ if (mListener != null) {
+ mListener.onColorChanged(Color.HSVToColor(mAlpha, new float[] {
+ mHue, mSat, mVal
+ }));
+ }
+
+ invalidate();
+ return true;
+ }
+
+ return super.onTrackballEvent(event);
+ }
+
+ @Override
+ public boolean onTouchEvent(final MotionEvent event) {
+
+ boolean update = false;
+
+ switch (event.getAction()) {
+
+ case MotionEvent.ACTION_DOWN:
+
+ mStartTouchPoint = new Point((int)event.getX(), (int)event.getY());
+
+ update = moveTrackersIfNeeded(event);
+
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+
+ update = moveTrackersIfNeeded(event);
+
+ break;
+
+ case MotionEvent.ACTION_UP:
+
+ mStartTouchPoint = null;
+
+ update = moveTrackersIfNeeded(event);
+
+ break;
+
+ }
+
+ if (update) {
+
+ if (mListener != null) {
+ mListener.onColorChanged(Color.HSVToColor(mAlpha, new float[] {
+ mHue, mSat, mVal
+ }));
+ }
+
+ invalidate();
+ return true;
+ }
+
+ return super.onTouchEvent(event);
+ }
+
+ private boolean moveTrackersIfNeeded(final MotionEvent event) {
+
+ if (mStartTouchPoint == null) {
+ return false;
+ }
+
+ boolean update = false;
+
+ final int startX = mStartTouchPoint.x;
+ final int startY = mStartTouchPoint.y;
+
+ if (mHueRect.contains(startX, startY)) {
+ mLastTouchedPanel = PANEL_HUE;
+
+ mHue = pointToHue(event.getY());
+
+ update = true;
+ } else if (mSatValRect.contains(startX, startY)) {
+
+ mLastTouchedPanel = PANEL_SAT_VAL;
+
+ final float[] result = pointToSatVal(event.getX(), event.getY());
+
+ mSat = result[0];
+ mVal = result[1];
+
+ update = true;
+ } else if (mAlphaRect != null && mAlphaRect.contains(startX, startY)) {
+
+ mLastTouchedPanel = PANEL_ALPHA;
+
+ mAlpha = pointToAlpha((int)event.getX());
+
+ update = true;
+ }
+
+ return update;
+ }
+
+ @Override
+ protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
+
+ int width = 0;
+ int height = 0;
+
+ final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+
+ int widthAllowed = MeasureSpec.getSize(widthMeasureSpec);
+ int heightAllowed = MeasureSpec.getSize(heightMeasureSpec);
+
+ widthAllowed = chooseWidth(widthMode, widthAllowed);
+ heightAllowed = chooseHeight(heightMode, heightAllowed);
+
+ if (!mShowAlphaPanel) {
+ height = (int)(widthAllowed - PANEL_SPACING - HUE_PANEL_WIDTH);
+
+ // If calculated height (based on the width) is more than the
+ // allowed height.
+ if (height > heightAllowed) {
+ height = heightAllowed;
+ width = (int)(height + PANEL_SPACING + HUE_PANEL_WIDTH);
+ } else {
+ width = widthAllowed;
+ }
+ } else {
+
+ width = (int)(heightAllowed - ALPHA_PANEL_HEIGHT + HUE_PANEL_WIDTH);
+
+ if (width > widthAllowed) {
+ width = widthAllowed;
+ height = (int)(widthAllowed - HUE_PANEL_WIDTH + ALPHA_PANEL_HEIGHT);
+ } else {
+ height = heightAllowed;
+ }
+
+ }
+
+ setMeasuredDimension(width, height);
+ }
+
+ private int chooseWidth(final int mode, final int size) {
+ if (mode == MeasureSpec.AT_MOST || mode == MeasureSpec.EXACTLY) {
+ return size;
+ } else { // (mode == MeasureSpec.UNSPECIFIED)
+ return getPrefferedWidth();
+ }
+ }
+
+ private int chooseHeight(final int mode, final int size) {
+ if (mode == MeasureSpec.AT_MOST || mode == MeasureSpec.EXACTLY) {
+ return size;
+ } else { // (mode == MeasureSpec.UNSPECIFIED)
+ return getPrefferedHeight();
+ }
+ }
+
+ private int getPrefferedWidth() {
+
+ int width = getPrefferedHeight();
+
+ if (mShowAlphaPanel) {
+ width -= PANEL_SPACING + ALPHA_PANEL_HEIGHT;
+ }
+
+ return (int)(width + HUE_PANEL_WIDTH + PANEL_SPACING);
+
+ }
+
+ private int getPrefferedHeight() {
+
+ int height = (int)(200 * mDensity);
+
+ if (mShowAlphaPanel) {
+ height += PANEL_SPACING + ALPHA_PANEL_HEIGHT;
+ }
+
+ return height;
+ }
+
+ @Override
+ protected void onSizeChanged(final int w, final int h, final int oldw, final int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+
+ mDrawingRect = new RectF();
+ mDrawingRect.left = mDrawingOffset + getPaddingLeft();
+ mDrawingRect.right = w - mDrawingOffset - getPaddingRight();
+ mDrawingRect.top = mDrawingOffset + getPaddingTop();
+ mDrawingRect.bottom = h - mDrawingOffset - getPaddingBottom();
+
+ setUpSatValRect();
+ setUpHueRect();
+ setUpAlphaRect();
+ }
+
+ private void setUpSatValRect() {
+
+ final RectF dRect = mDrawingRect;
+ float panelSide = dRect.height() - BORDER_WIDTH_PX * 2;
+
+ if (mShowAlphaPanel) {
+ panelSide -= PANEL_SPACING + ALPHA_PANEL_HEIGHT;
+ }
+
+ final float left = dRect.left + BORDER_WIDTH_PX;
+ final float top = dRect.top + BORDER_WIDTH_PX;
+ final float bottom = top + panelSide;
+ final float right = left + panelSide;
+
+ mSatValRect = new RectF(left, top, right, bottom);
+ }
+
+ private void setUpHueRect() {
+ final RectF dRect = mDrawingRect;
+
+ final float left = dRect.right - HUE_PANEL_WIDTH + BORDER_WIDTH_PX;
+ final float top = dRect.top + BORDER_WIDTH_PX;
+ final float bottom = dRect.bottom - BORDER_WIDTH_PX
+ - (mShowAlphaPanel ? PANEL_SPACING + ALPHA_PANEL_HEIGHT : 0);
+ final float right = dRect.right - BORDER_WIDTH_PX;
+
+ mHueRect = new RectF(left, top, right, bottom);
+ }
+
+ private void setUpAlphaRect() {
+
+ if (!mShowAlphaPanel) {
+ return;
+ }
+
+ final RectF dRect = mDrawingRect;
+
+ final float left = dRect.left + BORDER_WIDTH_PX;
+ final float top = dRect.bottom - ALPHA_PANEL_HEIGHT + BORDER_WIDTH_PX;
+ final float bottom = dRect.bottom - BORDER_WIDTH_PX;
+ final float right = dRect.right - BORDER_WIDTH_PX;
+
+ mAlphaRect = new RectF(left, top, right, bottom);
+
+ mAlphaPattern = new AlphaPatternDrawable((int)(5 * mDensity));
+ mAlphaPattern.setBounds(Math.round(mAlphaRect.left), Math.round(mAlphaRect.top),
+ Math.round(mAlphaRect.right), Math.round(mAlphaRect.bottom));
+
+ }
+
+ /**
+ * Set a OnColorChangedListener to get notified when the color selected by
+ * the user has changed.
+ *
+ * @param listener
+ */
+ public void setOnColorChangedListener(final OnColorChangedListener listener) {
+ mListener = listener;
+ }
+
+ /**
+ * Set the color of the border surrounding all panels.
+ *
+ * @param color
+ */
+ public void setBorderColor(final int color) {
+ mBorderColor = color;
+ invalidate();
+ }
+
+ /**
+ * Get the color of the border surrounding all panels.
+ */
+ public int getBorderColor() {
+ return mBorderColor;
+ }
+
+ /**
+ * Get the current color this view is showing.
+ *
+ * @return the current color.
+ */
+ public int getColor() {
+ return Color.HSVToColor(mAlpha, new float[] {
+ mHue, mSat, mVal
+ });
+ }
+
+ /**
+ * Set the color the view should show.
+ *
+ * @param color The color that should be selected.
+ */
+ public void setColor(final int color) {
+ setColor(color, false);
+ }
+
+ /**
+ * Set the color this view should show.
+ *
+ * @param color The color that should be selected.
+ * @param callback If you want to get a callback to your
+ * OnColorChangedListener.
+ */
+ public void setColor(final int color, final boolean callback) {
+
+ final int alpha = Color.alpha(color);
+ final int red = Color.red(color);
+ final int blue = Color.blue(color);
+ final int green = Color.green(color);
+
+ final float[] hsv = new float[3];
+
+ Color.RGBToHSV(red, green, blue, hsv);
+
+ mAlpha = alpha;
+ mHue = hsv[0];
+ mSat = hsv[1];
+ mVal = hsv[2];
+
+ if (callback && mListener != null) {
+ mListener.onColorChanged(Color.HSVToColor(mAlpha, new float[] {
+ mHue, mSat, mVal
+ }));
+ }
+
+ invalidate();
+ }
+
+ /**
+ * Get the drawing offset of the color picker view. The drawing offset is
+ * the distance from the side of a panel to the side of the view minus the
+ * padding. Useful if you want to have your own panel below showing the
+ * currently selected color and want to align it perfectly.
+ *
+ * @return The offset in pixels.
+ */
+ public float getDrawingOffset() {
+ return mDrawingOffset;
+ }
+
+ /**
+ * Set if the user is allowed to adjust the alpha panel. Default is false.
+ * If it is set to false no alpha will be set.
+ *
+ * @param visible
+ */
+ public void setAlphaSliderVisible(final boolean visible) {
+
+ if (mShowAlphaPanel != visible) {
+ mShowAlphaPanel = visible;
+
+ /*
+ * Reset all shader to force a recreation. Otherwise they will not
+ * look right after the size of the view has changed.
+ */
+ mValShader = null;
+ mSatShader = null;
+ mHueShader = null;
+ mAlphaShader = null;
+
+ requestLayout();
+ }
+
+ }
+
+ public void setSliderTrackerColor(final int color) {
+ mSliderTrackerColor = color;
+
+ mHueTrackerPaint.setColor(mSliderTrackerColor);
+
+ invalidate();
+ }
+
+ public int getSliderTrackerColor() {
+ return mSliderTrackerColor;
+ }
+
+ /**
+ * Set the text that should be shown in the alpha slider. Set to null to
+ * disable text.
+ *
+ * @param res string resource id.
+ */
+ public void setAlphaSliderText(final int res) {
+ final String text = getContext().getString(res);
+ setAlphaSliderText(text);
+ }
+
+ /**
+ * Set the text that should be shown in the alpha slider. Set to null to
+ * disable text.
+ *
+ * @param text Text that should be shown.
+ */
+ public void setAlphaSliderText(final String text) {
+ mAlphaSliderText = text;
+ invalidate();
+ }
+
+ /**
+ * Get the current value of the text that will be shown in the alpha slider.
+ *
+ * @return
+ */
+ public String getAlphaSliderText() {
+ return mAlphaSliderText;
+ }
+}
diff --git a/src/com/andrew/apollo/widgets/ColorSchemeDialog.java b/src/com/andrew/apollo/widgets/ColorSchemeDialog.java
new file mode 100644
index 0000000..a3ffc62
--- /dev/null
+++ b/src/com/andrew/apollo/widgets/ColorSchemeDialog.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.widgets;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.PixelFormat;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
+
+import com.andrew.apollo.R;
+import com.andrew.apollo.utils.PreferenceUtils;
+import com.andrew.apollo.widgets.ColorPickerView.OnColorChangedListener;
+
+import java.util.Locale;
+
+/**
+ * Shows the {@link ColorPanelView} in a new {@link AlertDialog}.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class ColorSchemeDialog extends AlertDialog implements
+ ColorPickerView.OnColorChangedListener {
+
+ private final int mCurrentColor;
+
+ private final OnColorChangedListener mListener = this;;
+
+ private LayoutInflater mInflater;
+
+ private ColorPickerView mColorPicker;
+
+ private Button mOldColor;
+
+ private Button mNewColor;
+
+ private View mRootView;
+
+ private EditText mHexValue;
+
+ /**
+ * Constructor of <code>ColorSchemeDialog</code>
+ *
+ * @param context The {@link Contxt} to use.
+ */
+ public ColorSchemeDialog(final Context context) {
+ super(context);
+ getWindow().setFormat(PixelFormat.RGBA_8888);
+ mCurrentColor = PreferenceUtils.getInstace(context).getDefaultThemeColor(context);
+ setUp(mCurrentColor);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.andrew.apollo.widgets.ColorPickerView.OnColorChangedListener#
+ * onColorChanged(int)
+ */
+ @Override
+ public void onColorChanged(final int color) {
+ if (mHexValue != null) {
+ mHexValue.setText(padLeft(Integer.toHexString(color).toUpperCase(Locale.getDefault()),
+ '0', 8));
+ }
+ mNewColor.setBackgroundColor(color);
+ }
+
+ private String padLeft(final String string, final char padChar, final int size) {
+ if (string.length() >= size) {
+ return string;
+ }
+ final StringBuilder result = new StringBuilder();
+ for (int i = string.length(); i < size; i++) {
+ result.append(padChar);
+ }
+ result.append(string);
+ return result.toString();
+ }
+
+ /**
+ * Initialzes the presets and color picker
+ *
+ * @param color The color to use.
+ */
+ private void setUp(final int color) {
+ mInflater = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mRootView = mInflater.inflate(R.layout.color_scheme_dialog, null);
+
+ mColorPicker = (ColorPickerView)mRootView.findViewById(R.id.color_picker_view);
+ mOldColor = (Button)mRootView.findViewById(R.id.color_scheme_dialog_old_color);
+ mOldColor.setOnClickListener(mPresetListener);
+ mNewColor = (Button)mRootView.findViewById(R.id.color_scheme_dialog_new_color);
+ setUpPresets(R.id.color_scheme_dialog_preset_one);
+ setUpPresets(R.id.color_scheme_dialog_preset_two);
+ setUpPresets(R.id.color_scheme_dialog_preset_three);
+ setUpPresets(R.id.color_scheme_dialog_preset_four);
+ setUpPresets(R.id.color_scheme_dialog_preset_five);
+ setUpPresets(R.id.color_scheme_dialog_preset_six);
+ setUpPresets(R.id.color_scheme_dialog_preset_seven);
+ setUpPresets(R.id.color_scheme_dialog_preset_eight);
+ mHexValue = (EditText)mRootView.findViewById(R.id.color_scheme_dialog_hex_value);
+ mHexValue.addTextChangedListener(new TextWatcher() {
+
+ @Override
+ public void onTextChanged(final CharSequence s, final int start, final int before,
+ final int count) {
+ try {
+ mColorPicker.setColor(Color.parseColor("#"
+ + mHexValue.getText().toString().toUpperCase(Locale.getDefault())));
+ mNewColor.setBackgroundColor(Color.parseColor("#"
+ + mHexValue.getText().toString().toUpperCase(Locale.getDefault())));
+ } catch (final Exception ignored) {
+ }
+ }
+
+ @Override
+ public void beforeTextChanged(final CharSequence s, final int start, final int count,
+ final int after) {
+ /* Nothing to do */
+ }
+
+ @Override
+ public void afterTextChanged(final Editable s) {
+ /* Nothing to do */
+ }
+ });
+
+ mColorPicker.setOnColorChangedListener(this);
+ mOldColor.setBackgroundColor(color);
+ mColorPicker.setColor(color, true);
+
+ setTitle(R.string.color_picker_title);
+ setView(mRootView);
+ }
+
+ /**
+ * @param color The color resource.
+ * @return A new color from Apollo's resources.
+ */
+ private int getColor(final int color) {
+ return getContext().getResources().getColor(color);
+ }
+
+ /**
+ * @return {@link ColorPickerView}'s current color
+ */
+ public int getColor() {
+ return mColorPicker.getColor();
+ }
+
+ /**
+ * @param which The Id of the preset color
+ */
+ private void setUpPresets(final int which) {
+ final Button preset = (Button)mRootView.findViewById(which);
+ if (preset != null) {
+ preset.setOnClickListener(mPresetListener);
+ }
+ }
+
+ /**
+ * Sets up the preset buttons
+ */
+ private final View.OnClickListener mPresetListener = new View.OnClickListener() {
+
+ @Override
+ public void onClick(final View v) {
+ switch (v.getId()) {
+ case R.id.color_scheme_dialog_preset_one:
+ mColorPicker.setColor(getColor(R.color.holo_blue_light));
+ break;
+ case R.id.color_scheme_dialog_preset_two:
+ mColorPicker.setColor(getColor(R.color.holo_green_light));
+ break;
+ case R.id.color_scheme_dialog_preset_three:
+ mColorPicker.setColor(getColor(R.color.holo_orange_dark));
+ break;
+ case R.id.color_scheme_dialog_preset_four:
+ mColorPicker.setColor(getColor(R.color.holo_orange_light));
+ break;
+ case R.id.color_scheme_dialog_preset_five:
+ mColorPicker.setColor(getColor(R.color.holo_purple));
+ break;
+ case R.id.color_scheme_dialog_preset_six:
+ mColorPicker.setColor(getColor(R.color.holo_red_light));
+ break;
+ case R.id.color_scheme_dialog_preset_seven:
+ mColorPicker.setColor(getColor(R.color.white));
+ break;
+ case R.id.color_scheme_dialog_preset_eight:
+ mColorPicker.setColor(getColor(R.color.black));
+ break;
+ case R.id.color_scheme_dialog_old_color:
+ mColorPicker.setColor(mCurrentColor);
+ break;
+ default:
+ break;
+ }
+ if (mListener != null) {
+ mListener.onColorChanged(getColor());
+ }
+ }
+ };
+
+}
diff --git a/src/com/andrew/apollo/widgets/CompatActionBarNavHandler.java b/src/com/andrew/apollo/widgets/CompatActionBarNavHandler.java
new file mode 100644
index 0000000..6440e4e
--- /dev/null
+++ b/src/com/andrew/apollo/widgets/CompatActionBarNavHandler.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.andrew.apollo.widgets;
+
+import android.support.v4.app.FragmentTransaction;
+
+import com.actionbarsherlock.app.ActionBar.OnNavigationListener;
+import com.actionbarsherlock.app.ActionBar.Tab;
+import com.actionbarsherlock.app.ActionBar.TabListener;
+
+/**
+ * Adapter for action bar navigation events. This class implements an adapter
+ * that facilitates handling of action bar navigation events. An instance of
+ * this class must be installed as a TabListener or OnNavigationListener on an
+ * Action Bar, and it will relay the navigation events to a configured listener
+ * (a {@link CompatActionBarNavListener}). This class should only be instanced
+ * and used on Android platforms that support the Action Bar, that is, SDK level
+ * 11 and above.
+ */
+public class CompatActionBarNavHandler implements TabListener, OnNavigationListener {
+
+ /* The listener that we notify of navigation events */
+ private final CompatActionBarNavListener mNavListener;
+
+ /**
+ * Constructs an instance with the given listener.
+ *
+ * @param listener the listener to notify when a navigation event occurs.
+ */
+ public CompatActionBarNavHandler(final CompatActionBarNavListener listener) {
+ mNavListener = listener;
+ }
+
+ /**
+ * Called by framework when a tab is selected. This will cause a navigation
+ * event to be delivered to the configured listener.
+ */
+ @Override
+ public void onTabSelected(final Tab tab, final FragmentTransaction ft) {
+ mNavListener.onCategorySelected(tab.getPosition());
+ }
+
+ /**
+ * Called by framework when a item on the navigation menu is selected. This
+ * will cause a navigation event to be delivered to the configured listener.
+ */
+ @Override
+ public boolean onNavigationItemSelected(final int itemPosition, final long itemId) {
+ mNavListener.onCategorySelected(itemPosition);
+ return true;
+ }
+
+ /**
+ * Called by framework when a tab is re-selected. That is, it was already
+ * selected and is tapped on again. This is not used in our app.
+ */
+ @Override
+ public void onTabReselected(final Tab tab, final FragmentTransaction ft) {
+ /* Nothing to do */
+ }
+
+ /**
+ * Called by framework when a tab is unselected. Not used in our app.
+ */
+ @Override
+ public void onTabUnselected(final Tab tab, final FragmentTransaction ft) {
+ /* Nothing to do */
+ }
+
+}
diff --git a/src/com/andrew/apollo/widgets/CompatActionBarNavListener.java b/src/com/andrew/apollo/widgets/CompatActionBarNavListener.java
new file mode 100644
index 0000000..c9eb0c5
--- /dev/null
+++ b/src/com/andrew/apollo/widgets/CompatActionBarNavListener.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.andrew.apollo.widgets;
+
+/**
+ * A listener that listens to navigation events. Represents a listener for
+ * navigation events delivered by {@link CompatActionBarNavHandler}.
+ */
+public interface CompatActionBarNavListener {
+ /**
+ * Signals that the given category was selected.
+ *
+ * @param catIndex the selected category's index.
+ */
+ public void onCategorySelected(int catIndex);
+}
diff --git a/src/com/andrew/apollo/widgets/FrameLayoutWithOverlay.java b/src/com/andrew/apollo/widgets/FrameLayoutWithOverlay.java
new file mode 100644
index 0000000..c217fda
--- /dev/null
+++ b/src/com/andrew/apollo/widgets/FrameLayoutWithOverlay.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.andrew.apollo.widgets;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+/**
+ * A FrameLayout whose contents are kept beneath an
+ * {@link AlphaTouchInterceptorOverlay}. If necessary, you can specify your own
+ * alpha-layer and manually manage its z-order.
+ */
+public class FrameLayoutWithOverlay extends FrameLayout {
+
+ private final AlphaTouchInterceptorOverlay mOverlay;
+
+ /**
+ * @param context The {@link Context} to use
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ */
+ public FrameLayoutWithOverlay(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+
+ /* Programmatically create touch-interceptor View. */
+ mOverlay = new AlphaTouchInterceptorOverlay(context);
+
+ addView(mOverlay);
+ }
+
+ /**
+ * After adding the View, bring the overlay to the front to ensure it's
+ * always on top.
+ */
+ @Override
+ public void addView(final View child, final int index, final ViewGroup.LayoutParams params) {
+ super.addView(child, index, params);
+ mOverlay.bringToFront();
+ }
+
+ /**
+ * Delegate to overlay: set the View that it will use as its alpha-layer. If
+ * none is set, the overlay will use its own alpha layer. Only necessary to
+ * set this if some child views need to appear above the alpha-layer.
+ */
+ protected void setAlphaLayer(final View layer) {
+ mOverlay.setAlphaLayer(layer);
+ }
+
+ /** Delegate to overlay: set the alpha value on the alpha layer. */
+ public void setAlphaLayerValue(final float alpha) {
+ mOverlay.setAlphaLayerValue(alpha);
+ }
+
+ /** Delegate to overlay. */
+ public void setOverlayOnClickListener(final OnClickListener listener) {
+ mOverlay.setOverlayOnClickListener(listener);
+ }
+
+ /** Delegate to overlay. */
+ public void setOverlayClickable(final boolean clickable) {
+ mOverlay.setOverlayClickable(clickable);
+ }
+}
diff --git a/src/com/andrew/apollo/widgets/LayoutSuppressingImageView.java b/src/com/andrew/apollo/widgets/LayoutSuppressingImageView.java
new file mode 100644
index 0000000..af5a203
--- /dev/null
+++ b/src/com/andrew/apollo/widgets/LayoutSuppressingImageView.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2012 Android Open Source Project Licensed under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
+ * or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+package com.andrew.apollo.widgets;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+
+/**
+ * A custom {@link ImageView} that improves the performance by not passing
+ * requestLayout() to its parent, taking advantage of knowing that image size
+ * won't change once set.
+ */
+public class LayoutSuppressingImageView extends ImageView {
+
+ /**
+ * @param context The {@link Context} to use
+ * @param attrs The attributes of the XML tag that is inflating the view
+ */
+ public LayoutSuppressingImageView(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void requestLayout() {
+ forceLayout();
+ }
+
+}
diff --git a/src/com/andrew/apollo/widgets/PlayPauseButton.java b/src/com/andrew/apollo/widgets/PlayPauseButton.java
new file mode 100644
index 0000000..90be366
--- /dev/null
+++ b/src/com/andrew/apollo/widgets/PlayPauseButton.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.widgets;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnLongClickListener;
+import android.widget.ImageButton;
+
+import com.andrew.apollo.R;
+import com.andrew.apollo.utils.ApolloUtils;
+import com.andrew.apollo.utils.MusicUtils;
+import com.andrew.apollo.utils.ThemeUtils;
+import com.andrew.apollo.widgets.theme.HoloSelector;
+
+/**
+ * A custom {@link ImageButton} that represents the "play and pause" button.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class PlayPauseButton extends ImageButton implements OnClickListener, OnLongClickListener {
+
+ /**
+ * Play button theme resource
+ */
+ private static final String PLAY = "btn_playback_play";
+
+ /**
+ * Pause button theme resource
+ */
+ private static final String PAUSE = "btn_playback_pause";
+
+ /**
+ * The resources to use.
+ */
+ private final ThemeUtils mResources;
+
+ /**
+ * @param context The {@link Context} to use
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ */
+ @SuppressWarnings("deprecation")
+ public PlayPauseButton(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ // Initialze the theme resources
+ mResources = new ThemeUtils(context);
+ // Theme the selector
+ setBackgroundDrawable(new HoloSelector(context));
+ // Control playback (play/pause)
+ setOnClickListener(this);
+ // Show the cheat sheet
+ setOnLongClickListener(this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onClick(final View v) {
+ MusicUtils.playOrPause();
+ updateState();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onLongClick(final View view) {
+ if (TextUtils.isEmpty(view.getContentDescription())) {
+ return false;
+ } else {
+ ApolloUtils.showCheatSheet(view);
+ return true;
+ }
+ }
+
+ /**
+ * Sets the correct drawable for playback.
+ */
+ public void updateState() {
+ if (MusicUtils.isPlaying()) {
+ setContentDescription(getResources().getString(R.string.accessibility_pause));
+ setImageDrawable(mResources.getDrawable(PAUSE));
+ } else {
+ setContentDescription(getResources().getString(R.string.accessibility_play));
+ setImageDrawable(mResources.getDrawable(PLAY));
+ }
+ }
+
+}
diff --git a/src/com/andrew/apollo/widgets/ProfileTabCarousel.java b/src/com/andrew/apollo/widgets/ProfileTabCarousel.java
new file mode 100644
index 0000000..3808f22
--- /dev/null
+++ b/src/com/andrew/apollo/widgets/ProfileTabCarousel.java
@@ -0,0 +1,533 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.widgets;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.res.Resources;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnTouchListener;
+import android.view.animation.AnimationUtils;
+import android.widget.HorizontalScrollView;
+import android.widget.ImageView;
+
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.andrew.apollo.R;
+import com.andrew.apollo.ui.activities.ProfileActivity;
+import com.andrew.apollo.utils.ApolloUtils;
+import com.nineoldandroids.animation.Animator;
+import com.nineoldandroids.animation.Animator.AnimatorListener;
+import com.nineoldandroids.animation.ObjectAnimator;
+
+/**
+ * A custom {@link HorizontalScrollView} that displays up to two "tabs" in the
+ * {@link ProfileActivity}. If the second tab is visible, a fraction of it will
+ * overflow slightly onto the screen.
+ */
+@SuppressLint("NewApi")
+public class ProfileTabCarousel extends HorizontalScrollView implements OnTouchListener {
+
+ /**
+ * Number of tabs
+ */
+ private static final int TAB_COUNT = 2;
+
+ /**
+ * First tab index
+ */
+ private static final int TAB_INDEX_FIRST = 0;
+
+ /**
+ * Second tab index
+ */
+ private static final int TAB_INDEX_SECOND = 1;
+
+ /**
+ * Alpha layer to be set on the lable view
+ */
+ private static final float MAX_ALPHA = 0.6f;
+
+ /**
+ * Y coordinate of the tab at the given index was selected
+ */
+ private static final float[] mYCoordinateArray = new float[TAB_COUNT];
+
+ /**
+ * Tab width as defined as a fraction of the screen width
+ */
+ private final float tabWidthScreenWidthFraction;
+
+ /**
+ * Tab height as defined as a fraction of the screen width
+ */
+ private final float tabHeightScreenWidthFraction;
+
+ /**
+ * Height of the tab label
+ */
+ private final int mTabDisplayLabelHeight;
+
+ /**
+ * Height in pixels of the shadow under the tab carousel
+ */
+ private final int mTabShadowHeight;
+
+ /**
+ * First tab click listener
+ */
+ private final TabClickListener mTabOneTouchInterceptListener = new TabClickListener(
+ TAB_INDEX_FIRST);
+
+ /**
+ * Second tab click listener
+ */
+ private final TabClickListener mTabTwoTouchInterceptListener = new TabClickListener(
+ TAB_INDEX_SECOND);
+
+ /**
+ * The last scrolled position
+ */
+ private int mLastScrollPosition = Integer.MIN_VALUE;
+
+ /**
+ * Allowed horizontal scroll length
+ */
+ private int mAllowedHorizontalScrollLength = Integer.MIN_VALUE;
+
+ /**
+ * Allowed vertical scroll length
+ */
+ private int mAllowedVerticalScrollLength = Integer.MIN_VALUE;
+
+ /**
+ * Current tab index
+ */
+ private int mCurrentTab = TAB_INDEX_FIRST;
+
+ /**
+ * Factor to scale scroll-amount sent to listeners
+ */
+ private float mScrollScaleFactor = 1.0f;
+
+ private boolean mScrollToCurrentTab = false;
+
+ private boolean mTabCarouselIsAnimating;
+
+ private boolean mEnableSwipe;
+
+ private CarouselTab mFirstTab;
+
+ private CarouselTab mSecondTab;
+
+ private Listener mListener;
+
+ /**
+ * @param context The {@link Context} to use
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ */
+ public ProfileTabCarousel(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ setOnTouchListener(this);
+ final Resources mResources = context.getResources();
+ mTabDisplayLabelHeight = mResources
+ .getDimensionPixelSize(R.dimen.profile_photo_shadow_height);
+ mTabShadowHeight = mResources.getDimensionPixelSize(R.dimen.profile_carousel_label_height);
+ tabWidthScreenWidthFraction = mResources.getFraction(
+ R.fraction.tab_width_screen_percentage, 1, 1);
+ tabHeightScreenWidthFraction = mResources.getFraction(
+ R.fraction.tab_height_screen_percentage, 1, 1);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mFirstTab = (CarouselTab)findViewById(R.id.profile_tab_carousel_tab_one);
+ mFirstTab.setOverlayOnClickListener(mTabOneTouchInterceptListener);
+ mSecondTab = (CarouselTab)findViewById(R.id.profile_tab_carousel_tab_two);
+ mSecondTab.setOverlayOnClickListener(mTabTwoTouchInterceptListener);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
+ final int screenWidth = MeasureSpec.getSize(widthMeasureSpec);
+ final int tabWidth = Math.round(tabWidthScreenWidthFraction * screenWidth);
+
+ mAllowedHorizontalScrollLength = tabWidth * TAB_COUNT - screenWidth;
+ if (mAllowedHorizontalScrollLength == 0) {
+ mScrollScaleFactor = 1.0f;
+ } else {
+ mScrollScaleFactor = screenWidth / mAllowedHorizontalScrollLength;
+ }
+
+ final int tabHeight = Math.round(screenWidth * tabHeightScreenWidthFraction)
+ + mTabShadowHeight;
+ if (getChildCount() > 0) {
+ final View child = getChildAt(0);
+
+ // Add 1 dip of separation between the tabs
+ final int seperatorPixels = (int)(TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP, 1, getResources().getDisplayMetrics()) + 0.5f);
+
+ if (mEnableSwipe) {
+ child.measure(
+ MeasureSpec.makeMeasureSpec(TAB_COUNT * tabWidth + (TAB_COUNT - 1)
+ * seperatorPixels, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(tabHeight, MeasureSpec.EXACTLY));
+ } else {
+ child.measure(MeasureSpec.makeMeasureSpec(screenWidth, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(tabHeight, MeasureSpec.EXACTLY));
+ }
+ }
+ mAllowedVerticalScrollLength = tabHeight - mTabDisplayLabelHeight - mTabShadowHeight;
+ setMeasuredDimension(resolveSize(screenWidth, widthMeasureSpec),
+ resolveSize(tabHeight, heightMeasureSpec));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @SuppressLint("DrawAllocation")
+ @Override
+ protected void onLayout(final boolean changed, final int l, final int t, final int r,
+ final int b) {
+ super.onLayout(changed, l, t, r, b);
+ if (!mScrollToCurrentTab) {
+ return;
+ }
+ mScrollToCurrentTab = false;
+ ApolloUtils.doAfterLayout(this, new Runnable() {
+ @Override
+ public void run() {
+ scrollTo(mCurrentTab == TAB_INDEX_FIRST ? 0 : mAllowedHorizontalScrollLength, 0);
+ updateAlphaLayers();
+ }
+ });
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onScrollChanged(final int x, final int y, final int oldX, final int oldY) {
+ super.onScrollChanged(x, y, oldX, oldY);
+ if (mLastScrollPosition == x) {
+ return;
+ }
+ final int scaledL = (int)(x * mScrollScaleFactor);
+ final int oldScaledL = (int)(oldX * mScrollScaleFactor);
+ mListener.onScrollChanged(scaledL, y, oldScaledL, oldY);
+ mLastScrollPosition = x;
+ updateAlphaLayers();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onTouch(final View v, final MotionEvent event) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ mListener.onTouchDown();
+ return true;
+ case MotionEvent.ACTION_UP:
+ mListener.onTouchUp();
+ return true;
+ }
+ return super.onTouchEvent(event);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onInterceptTouchEvent(final MotionEvent ev) {
+ final boolean mInterceptTouch = super.onInterceptTouchEvent(ev);
+ if (mInterceptTouch) {
+ mListener.onTouchDown();
+ }
+ return mInterceptTouch;
+ }
+
+ /**
+ * Reset the carousel to the start position (i.e. because new data will be
+ * loaded in for a different contact).
+ */
+ public void reset() {
+ scrollTo(0, 0);
+ setCurrentTab(TAB_INDEX_FIRST);
+ moveToYCoordinate(TAB_INDEX_FIRST, 0);
+ }
+
+ /**
+ * Set the current tab that should be restored when the view is first laid
+ * out.
+ */
+ public void restoreCurrentTab(final int position) {
+ setCurrentTab(position);
+ mScrollToCurrentTab = true;
+ }
+
+ /**
+ * Restore the Y position of this view to the last manually requested value.
+ * This can be done after the parent has been re-laid out again, where this
+ * view's position could have been lost if the view laid outside its
+ * parent's bounds.
+ */
+ public void restoreYCoordinate(final int duration, final int tabIndex) {
+ final float storedYCoordinate = getStoredYCoordinateForTab(tabIndex);
+
+ final ObjectAnimator tabCarouselAnimator = ObjectAnimator.ofFloat(this, "y",
+ storedYCoordinate);
+ tabCarouselAnimator.addListener(mTabCarouselAnimatorListener);
+ tabCarouselAnimator.setInterpolator(AnimationUtils.loadInterpolator(getContext(),
+ android.R.anim.accelerate_decelerate_interpolator));
+ tabCarouselAnimator.setDuration(duration);
+ tabCarouselAnimator.start();
+ }
+
+ /**
+ * Request that the view move to the given Y coordinate. Also store the Y
+ * coordinate as the last requested Y coordinate for the given tabIndex.
+ */
+ public void moveToYCoordinate(final int tabIndex, final float y) {
+ storeYCoordinate(tabIndex, y);
+ restoreYCoordinate(0, tabIndex);
+ }
+
+ /**
+ * Store this information as the last requested Y coordinate for the given
+ * tabIndex.
+ */
+ public void storeYCoordinate(final int tabIndex, final float y) {
+ mYCoordinateArray[tabIndex] = y;
+ }
+
+ /**
+ * Returns the stored Y coordinate of this view the last time the user was
+ * on the selected tab given by tabIndex.
+ */
+ public float getStoredYCoordinateForTab(final int tabIndex) {
+ return mYCoordinateArray[tabIndex];
+ }
+
+ /**
+ * Returns the number of pixels that this view can be scrolled horizontally.
+ */
+ public int getAllowedHorizontalScrollLength() {
+ return mAllowedHorizontalScrollLength;
+ }
+
+ /**
+ * Returns the number of pixels that this view can be scrolled vertically
+ * while still allowing the tab labels to still show.
+ */
+ public int getAllowedVerticalScrollLength() {
+ return mAllowedVerticalScrollLength;
+ }
+
+ /**
+ * Sets the correct alpha layers over the tabs.
+ */
+ private void updateAlphaLayers() {
+ float alpha = mLastScrollPosition * MAX_ALPHA / mAllowedHorizontalScrollLength;
+ alpha = AlphaTouchInterceptorOverlay.clamp(alpha, 0.0f, 1.0f);
+ mFirstTab.setAlphaLayerValue(alpha);
+ mSecondTab.setAlphaLayerValue(MAX_ALPHA - alpha);
+ }
+
+ /**
+ * @return The {@link ImageView} in the first index.
+ */
+ public ImageView getPhoto() {
+ return mFirstTab.getPhoto();
+ }
+
+ /**
+ * @return The {@link ImageView} used to set the album art.
+ */
+ public ImageView getAlbumArt() {
+ return mFirstTab.getAlbumArt();
+ }
+
+ /**
+ * This listener keeps track of whether the tab carousel animation is
+ * currently going on or not, in order to prevent other simultaneous changes
+ * to the Y position of the tab carousel which can cause flicker.
+ */
+ private final AnimatorListener mTabCarouselAnimatorListener = new AnimatorListener() {
+
+ @Override
+ public void onAnimationCancel(final Animator animation) {
+ mTabCarouselIsAnimating = false;
+ }
+
+ @Override
+ public void onAnimationEnd(final Animator animation) {
+ mTabCarouselIsAnimating = false;
+ }
+
+ @Override
+ public void onAnimationRepeat(final Animator animation) {
+ mTabCarouselIsAnimating = true;
+ }
+
+ @Override
+ public void onAnimationStart(final Animator animation) {
+ mTabCarouselIsAnimating = true;
+ }
+ };
+
+ /**
+ * @return True if the carousel is currently animating, false otherwise
+ */
+ public boolean isTabCarouselIsAnimating() {
+ return mTabCarouselIsAnimating;
+ }
+
+ /**
+ * Updates the tab selection.
+ */
+ public void setCurrentTab(final int position) {
+ final CarouselTab selected, deselected;
+
+ switch (position) {
+ case TAB_INDEX_FIRST:
+ selected = mFirstTab;
+ deselected = mSecondTab;
+ break;
+ case TAB_INDEX_SECOND:
+ selected = mSecondTab;
+ deselected = mFirstTab;
+ break;
+ default:
+ throw new IllegalStateException("Invalid tab position " + position);
+ }
+ selected.setSelected(true);
+ selected.showSelectedState();
+ selected.setOverlayClickable(false);
+ deselected.setSelected(false);
+ deselected.showDeselectedState();
+ deselected.setOverlayClickable(true);
+ mCurrentTab = position;
+ }
+
+ /**
+ * Set the given {@link Listener} to handle carousel events.
+ */
+ public void setListener(final Listener listener) {
+ mListener = listener;
+ }
+
+ /**
+ * Sets the artist image header
+ *
+ * @param context The {@link SherlockFragmentActivity} to use
+ * @param artistName The artist name used to find the cached artist image
+ * and used to find the last album played by the artist
+ */
+ public void setArtistProfileHeader(final SherlockFragmentActivity context,
+ final String artistName) {
+ mFirstTab.setLabel(getResources().getString(R.string.page_songs));
+ mSecondTab.setLabel(getResources().getString(R.string.page_albums));
+ mFirstTab.setArtistPhoto(context, artistName);
+ mSecondTab.setArtistAlbumPhoto(context, artistName);
+ mEnableSwipe = true;
+ }
+
+ /**
+ * Sets the album image header
+ *
+ * @param context The {@link SherlockFragmentActivity} to use
+ * @param albumName The key used to find the cached album art
+ * @param artistName The artist name used to find the cached artist image
+ */
+ public void setAlbumProfileHeader(final SherlockFragmentActivity context,
+ final String albumName, final String artistName) {
+ mFirstTab.setLabel(getResources().getString(R.string.page_songs));
+ mFirstTab.setAlbumPhoto(context, albumName);
+ mFirstTab.blurPhoto(context, artistName, albumName);
+ mSecondTab.setVisibility(View.GONE);
+ mEnableSwipe = false;
+ }
+
+ /**
+ * Sets the playlist or genre image header
+ *
+ * @param context The {@link SherlockFragmentActivity} to use
+ * @param profileName The key used to find the cached image for a playlist
+ * or genre
+ */
+ public void setPlaylistOrGenreProfileHeader(final SherlockFragmentActivity context,
+ final String profileName) {
+ mFirstTab.setDefault(context);
+ mFirstTab.setLabel(getResources().getString(R.string.page_songs));
+ mFirstTab.setPlaylistOrGenrePhoto(context, profileName);
+ mSecondTab.setVisibility(View.GONE);
+ mEnableSwipe = false;
+ }
+
+ /**
+ * Used to fetch for the album art via Last.fm.
+ *
+ * @param context The {@link Context} to use.
+ * @param album The name of the album in the profile the user is viewing.
+ */
+ public void fetchAlbumPhoto(final SherlockFragmentActivity context, final String albumName) {
+ mFirstTab.fetchAlbumPhoto(context, albumName);
+ }
+
+ /**
+ * @return The main {@link ImageView} for the first tab
+ */
+ public ImageView getHeaderPhoto() {
+ return mFirstTab.getPhoto();
+ }
+
+ /** When clicked, selects the corresponding tab. */
+ private final class TabClickListener implements OnClickListener {
+ private final int mTab;
+
+ public TabClickListener(final int tab) {
+ super();
+ mTab = tab;
+ }
+
+ @Override
+ public void onClick(final View v) {
+ mListener.onTabSelected(mTab);
+ }
+ }
+
+ /**
+ * Interface for callbacks invoked when the user interacts with the
+ * carousel.
+ */
+ public interface Listener {
+ public void onTouchDown();
+
+ public void onTouchUp();
+
+ public void onScrollChanged(int l, int t, int oldl, int oldt);
+
+ public void onTabSelected(int position);
+ }
+
+}
diff --git a/src/com/andrew/apollo/widgets/RepeatButton.java b/src/com/andrew/apollo/widgets/RepeatButton.java
new file mode 100644
index 0000000..82572c8
--- /dev/null
+++ b/src/com/andrew/apollo/widgets/RepeatButton.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.widgets;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnLongClickListener;
+import android.widget.ImageButton;
+
+import com.andrew.apollo.MusicPlaybackService;
+import com.andrew.apollo.R;
+import com.andrew.apollo.utils.ApolloUtils;
+import com.andrew.apollo.utils.MusicUtils;
+import com.andrew.apollo.utils.ThemeUtils;
+import com.andrew.apollo.widgets.theme.HoloSelector;
+
+/**
+ * A custom {@link ImageButton} that represents the "repeat" button.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class RepeatButton extends ImageButton implements OnClickListener, OnLongClickListener {
+
+ /**
+ * Repeat one theme resource
+ */
+ private static final String REPEAT_ALL = "btn_playback_repeat_all";
+
+ /**
+ * Repeat one theme resource
+ */
+ private static final String REPEAT_CURRENT = "btn_playback_repeat_one";
+
+ /**
+ * Repeat one theme resource
+ */
+ private static final String REPEAT_NONE = "btn_playback_repeat";
+
+ /**
+ * The resources to use.
+ */
+ private final ThemeUtils mResources;
+
+ /**
+ * @param context The {@link Context} to use
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ */
+ @SuppressWarnings("deprecation")
+ public RepeatButton(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ // Initialze the theme resources
+ mResources = new ThemeUtils(context);
+ // Set the selector
+ setBackgroundDrawable(new HoloSelector(context));
+ // Control playback (cycle repeat modes)
+ setOnClickListener(this);
+ // Show the cheat sheet
+ setOnLongClickListener(this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onClick(final View v) {
+ MusicUtils.cycleRepeat();
+ updateRepeatState();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onLongClick(final View view) {
+ if (TextUtils.isEmpty(view.getContentDescription())) {
+ return false;
+ } else {
+ ApolloUtils.showCheatSheet(view);
+ return true;
+ }
+ }
+
+ /**
+ * Sets the correct drawable for the repeat state.
+ */
+ public void updateRepeatState() {
+ switch (MusicUtils.getRepeatMode()) {
+ case MusicPlaybackService.REPEAT_ALL:
+ setContentDescription(getResources().getString(R.string.accessibility_repeat_all));
+ setImageDrawable(mResources.getDrawable(REPEAT_ALL));
+ break;
+ case MusicPlaybackService.REPEAT_CURRENT:
+ setContentDescription(getResources().getString(R.string.accessibility_repeat_one));
+ setImageDrawable(mResources.getDrawable(REPEAT_CURRENT));
+ break;
+ case MusicPlaybackService.REPEAT_NONE:
+ setContentDescription(getResources().getString(R.string.accessibility_repeat));
+ setImageDrawable(mResources.getDrawable(REPEAT_NONE));
+ break;
+ default:
+ break;
+ }
+ }
+}
diff --git a/src/com/andrew/apollo/widgets/RepeatingImageButton.java b/src/com/andrew/apollo/widgets/RepeatingImageButton.java
new file mode 100644
index 0000000..1fdc6d1
--- /dev/null
+++ b/src/com/andrew/apollo/widgets/RepeatingImageButton.java
@@ -0,0 +1,216 @@
+/*
+ * 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.
+ */
+
+package com.andrew.apollo.widgets;
+
+import android.content.Context;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.ImageButton;
+
+import com.andrew.apollo.R;
+import com.andrew.apollo.utils.ApolloUtils;
+import com.andrew.apollo.utils.MusicUtils;
+import com.andrew.apollo.utils.ThemeUtils;
+import com.andrew.apollo.widgets.theme.HoloSelector;
+
+/**
+ * A {@link ImageButton} that will repeatedly call a 'listener' method as long
+ * as the button is pressed, otherwise functions like a typecal
+ * {@link ImageButton}
+ */
+public class RepeatingImageButton extends ImageButton implements OnClickListener {
+
+ /**
+ * Next button theme resource
+ */
+ private static final String NEXT = "btn_playback_next";
+
+ /**
+ * Previous button theme resource
+ */
+ private static final String PREVIOUS = "btn_playback_previous";
+
+ private static final long sInterval = 400;
+
+ /**
+ * The resources to use.
+ */
+ private final ThemeUtils mResources;
+
+ private long mStartTime;
+
+ private int mRepeatCount;
+
+ private RepeatListener mListener;
+
+ /**
+ * @param context The {@link Context} to use
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ */
+ @SuppressWarnings("deprecation")
+ public RepeatingImageButton(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ // Initialze the theme resources
+ mResources = new ThemeUtils(context);
+ // Theme the selector
+ setBackgroundDrawable(new HoloSelector(context));
+ setFocusable(true);
+ setLongClickable(true);
+ setOnClickListener(this);
+ updateState();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onClick(final View view) {
+ switch (view.getId()) {
+ case R.id.action_button_previous:
+ MusicUtils.previous(getContext());
+ break;
+ case R.id.action_button_next:
+ MusicUtils.next();
+ default:
+ break;
+ }
+ }
+
+ /**
+ * Sets the listener to be called while the button is pressed and the
+ * interval in milliseconds with which it will be called.
+ *
+ * @param l The listener that will be called
+ * @param interval The interval in milliseconds for calls
+ */
+ public void setRepeatListener(final RepeatListener l) {
+ mListener = l;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean performLongClick() {
+ if (mListener == null) {
+ ApolloUtils.showCheatSheet(this);
+ }
+ mStartTime = SystemClock.elapsedRealtime();
+ mRepeatCount = 0;
+ post(mRepeater);
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onTouchEvent(final MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_UP) {
+ /* Remove the repeater, but call the hook one more time */
+ removeCallbacks(mRepeater);
+ if (mStartTime != 0) {
+ doRepeat(true);
+ mStartTime = 0;
+ }
+ }
+ return super.onTouchEvent(event);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onKeyDown(final int keyCode, final KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ case KeyEvent.KEYCODE_ENTER:
+ /*
+ * Need to call super to make long press work, but return true
+ * so that the application doesn't get the down event
+ */
+ super.onKeyDown(keyCode, event);
+ return true;
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onKeyUp(final int keyCode, final KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ case KeyEvent.KEYCODE_ENTER:
+ /* Remove the repeater, but call the hook one more time */
+ removeCallbacks(mRepeater);
+ if (mStartTime != 0) {
+ doRepeat(true);
+ mStartTime = 0;
+ }
+ }
+ return super.onKeyUp(keyCode, event);
+ }
+
+ private final Runnable mRepeater = new Runnable() {
+ @Override
+ public void run() {
+ doRepeat(false);
+ if (isPressed()) {
+ postDelayed(this, sInterval);
+ }
+ }
+ };
+
+ /**
+ * @param shouldRepeat If True the repeat count stops at -1, false if to add
+ * incrementally add the repeat count
+ */
+ private void doRepeat(final boolean shouldRepeat) {
+ final long now = SystemClock.elapsedRealtime();
+ if (mListener != null) {
+ mListener.onRepeat(this, now - mStartTime, shouldRepeat ? -1 : mRepeatCount++);
+ }
+ }
+
+ /**
+ * Sets the correct drawable for playback.
+ */
+ public void updateState() {
+ switch (getId()) {
+ case R.id.action_button_next:
+ setImageDrawable(mResources.getDrawable(NEXT));
+ break;
+ case R.id.action_button_previous:
+ setImageDrawable(mResources.getDrawable(PREVIOUS));
+ break;
+ default:
+ break;
+ }
+ }
+
+ public interface RepeatListener {
+
+ /**
+ * @param v View to be set
+ * @param duration Duration of the long press
+ * @param repeatcount The number of repeat counts
+ */
+ void onRepeat(View v, long duration, int repeatcount);
+ }
+
+}
diff --git a/src/com/andrew/apollo/widgets/SeparatedListAdapter.java b/src/com/andrew/apollo/widgets/SeparatedListAdapter.java
new file mode 100644
index 0000000..f4f6a3e
--- /dev/null
+++ b/src/com/andrew/apollo/widgets/SeparatedListAdapter.java
@@ -0,0 +1,157 @@
+
+package com.andrew.apollo.widgets;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Adapter;
+import android.widget.ArrayAdapter;
+import android.widget.BaseAdapter;
+
+import com.andrew.apollo.R;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public class SeparatedListAdapter extends BaseAdapter {
+
+ public final Map<String, Adapter> mSections = new LinkedHashMap<String, Adapter>();
+
+ public final ArrayAdapter<String> mHeaders;
+
+ public final static int TYPE_SECTION_HEADER = 0;
+
+ /**
+ * Constructor of <code>SeparatedListAdapter</code>
+ *
+ * @param context The {@link Context} to use.
+ */
+ public SeparatedListAdapter(final Context context) {
+ mHeaders = new ArrayAdapter<String>(context, R.layout.list_header);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Object getItem(int position) {
+ for (final Object section : mSections.keySet()) {
+ final Adapter adapter = mSections.get(section);
+ final int size = adapter.getCount() + 1;
+
+ // check if position inside this section
+ if (position == 0) {
+ return section;
+ }
+ if (position < size) {
+ return adapter.getItem(position - 1);
+ }
+
+ // otherwise jump into next section
+ position -= size;
+ }
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getCount() {
+ // total together all mSections, plus one for each section header
+ int total = 0;
+ for (final Adapter adapter : mSections.values()) {
+ total += adapter.getCount() + 1;
+ }
+ return total;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getViewTypeCount() {
+ // assume that mHeaders count as one, then total all mSections
+ int total = 1;
+ for (final Adapter adapter : mSections.values()) {
+ total += adapter.getViewTypeCount();
+ }
+ return total;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getItemViewType(int position) {
+ int type = 1;
+ for (final Object section : mSections.keySet()) {
+ final Adapter adapter = mSections.get(section);
+ final int size = adapter.getCount() + 1;
+
+ // check if position inside this section
+ if (position == 0) {
+ return TYPE_SECTION_HEADER;
+ }
+ if (position < size) {
+ return type + adapter.getItemViewType(position - 1);
+ }
+
+ // otherwise jump into next section
+ position -= size;
+ type += adapter.getViewTypeCount();
+ }
+ return -1;
+ }
+
+ public boolean areAllItemsSelectable() {
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean isEnabled(final int position) {
+ return getItemViewType(position) != TYPE_SECTION_HEADER;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View getView(int position, final View convertView, final ViewGroup parent) {
+ int sectionnum = 0;
+ for (final Object section : mSections.keySet()) {
+ final Adapter adapter = mSections.get(section);
+ final int size = adapter.getCount() + 1;
+
+ // check if position inside this section
+ if (position == 0) {
+ return mHeaders.getView(sectionnum, convertView, parent);
+ }
+ if (position < size) {
+ return adapter.getView(position - 1, convertView, parent);
+ }
+
+ // otherwise jump into next section
+ position -= size;
+ sectionnum++;
+ }
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public long getItemId(final int position) {
+ return position;
+ }
+
+ public void addSection(final String section, final Adapter adapter) {
+ mHeaders.add(section);
+ mSections.put(section, adapter);
+ }
+
+}
diff --git a/src/com/andrew/apollo/widgets/ShowHideMasterLayout.java b/src/com/andrew/apollo/widgets/ShowHideMasterLayout.java
new file mode 100644
index 0000000..c498eab
--- /dev/null
+++ b/src/com/andrew/apollo/widgets/ShowHideMasterLayout.java
@@ -0,0 +1,398 @@
+/*
+ * Copyright 2012 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 com.andrew.apollo.widgets;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+
+/**
+ * A layout that supports the Show/Hide pattern for portrait tablet layouts. See
+ * <a href=
+ * "http://developer.android.com/design/patterns/multi-pane-layouts.html#orientation"
+ * >Android Design &gt; Patterns &gt; Multi-pane Layouts & gt; Compound Views
+ * and Orientation Changes</a> for more details on this pattern. This layout
+ * should normally be used in association with the Up button. Specifically, show
+ * the master pane using {@link #showMaster(boolean, int)} when the Up button is
+ * pressed. If the master pane is visible, defer to normal Up behavior.
+ * <p>
+ * TODO: swiping should be more tactile and actually follow the user's finger.
+ * <p>
+ * Requires API level 11
+ */
+@TargetApi(Build.VERSION_CODES.HONEYCOMB)
+public class ShowHideMasterLayout extends ViewGroup implements Animator.AnimatorListener {
+
+ /**
+ * A flag for {@link #showMaster(boolean, int)} indicating that the change
+ * in visiblity should not be animated.
+ */
+ public final static int FLAG_IMMEDIATE = 0x1;
+
+ private View sMasterView;
+
+ private View mDetailView;
+
+ private OnMasterVisibilityChangedListener mOnMasterVisibilityChangedListener;
+
+ private GestureDetector mGestureDetector;
+
+ private Runnable mShowMasterCompleteRunnable;
+
+ private boolean mFirstShow = true;
+
+ private boolean mMasterVisible = true;
+
+ private boolean mFlingToExposeMaster;
+
+ private boolean mIsAnimating;
+
+ /* The last measured master width, including its margins */
+ private int mTranslateAmount;
+
+ public interface OnMasterVisibilityChangedListener {
+ public void onMasterVisibilityChanged(boolean visible);
+ }
+
+ public ShowHideMasterLayout(final Context context) {
+ super(context);
+ init();
+ }
+
+ public ShowHideMasterLayout(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ public ShowHideMasterLayout(final Context context, final AttributeSet attrs, final int defStyle) {
+ super(context, attrs, defStyle);
+ init();
+ }
+
+ private void init() {
+ mGestureDetector = new GestureDetector(getContext(), mGestureListener);
+ }
+
+ @Override
+ public LayoutParams generateLayoutParams(final AttributeSet attrs) {
+ return new MarginLayoutParams(getContext(), attrs);
+ }
+
+ @Override
+ protected LayoutParams generateLayoutParams(final LayoutParams p) {
+ return new MarginLayoutParams(p);
+ }
+
+ @Override
+ protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
+ final int mCount = getChildCount();
+
+ /* Measure once to find the maximum child size */
+ int sMaxHeight = 0;
+ int sMaxWidth = 0;
+ int mChildState = 0;
+
+ for (int i = 0; i < mCount; i++) {
+ final View mChild = getChildAt(i);
+ if (mChild.getVisibility() == GONE) {
+ continue;
+ }
+
+ measureChildWithMargins(mChild, widthMeasureSpec, 0, heightMeasureSpec, 0);
+ final MarginLayoutParams mLayoutParams = (MarginLayoutParams)mChild.getLayoutParams();
+ sMaxWidth = Math.max(sMaxWidth, mChild.getMeasuredWidth() + mLayoutParams.leftMargin
+ + mLayoutParams.rightMargin);
+ sMaxHeight = Math.max(sMaxHeight, mChild.getMeasuredHeight() + mLayoutParams.topMargin
+ + mLayoutParams.bottomMargin);
+ mChildState = combineMeasuredStates(mChildState, mChild.getMeasuredState());
+ }
+
+ /* Account for padding too */
+ sMaxWidth += getPaddingLeft() + getPaddingRight();
+ sMaxHeight += getPaddingLeft() + getPaddingRight();
+
+ /* Check against our minimum height and width */
+ sMaxHeight = Math.max(sMaxHeight, getSuggestedMinimumHeight());
+ sMaxWidth = Math.max(sMaxWidth, getSuggestedMinimumWidth());
+
+ /* Set our own measured size */
+ setMeasuredDimension(
+ resolveSizeAndState(sMaxWidth, widthMeasureSpec, mChildState),
+ resolveSizeAndState(sMaxHeight, heightMeasureSpec,
+ mChildState << MEASURED_HEIGHT_STATE_SHIFT));
+
+ /* Measure children for them to set their measured dimensions */
+ for (int i = 0; i < mCount; i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() == GONE) {
+ continue;
+ }
+
+ final MarginLayoutParams mLayoutParams = (MarginLayoutParams)child.getLayoutParams();
+
+ int mChildWidthMeasureSpec;
+ int mChildHeightMeasureSpec;
+
+ if (mLayoutParams.width == LayoutParams.MATCH_PARENT) {
+ mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth()
+ - getPaddingLeft() - getPaddingRight() - mLayoutParams.leftMargin
+ - mLayoutParams.rightMargin, MeasureSpec.EXACTLY);
+ } else {
+ mChildWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, getPaddingLeft()
+ + getPaddingRight() + mLayoutParams.leftMargin + mLayoutParams.rightMargin,
+ mLayoutParams.width);
+ }
+
+ if (mLayoutParams.height == LayoutParams.MATCH_PARENT) {
+ mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight()
+ - getPaddingTop() - getPaddingBottom() - mLayoutParams.topMargin
+ - mLayoutParams.bottomMargin, MeasureSpec.EXACTLY);
+ } else {
+ mChildHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
+ getPaddingTop() + getPaddingBottom() + mLayoutParams.topMargin
+ + mLayoutParams.bottomMargin, mLayoutParams.height);
+ }
+
+ child.measure(mChildWidthMeasureSpec, mChildHeightMeasureSpec);
+ }
+ }
+
+ @Override
+ protected void onLayout(final boolean changed, final int l, final int t, final int r,
+ final int b) {
+ updateChildReferences();
+
+ if (sMasterView == null || mDetailView == null) {
+ return;
+ }
+
+ final int sMasterWidth = sMasterView.getMeasuredWidth();
+ final MarginLayoutParams sMasterLp = (MarginLayoutParams)sMasterView.getLayoutParams();
+ final MarginLayoutParams mDetailLp = (MarginLayoutParams)mDetailView.getLayoutParams();
+
+ mTranslateAmount = sMasterWidth + sMasterLp.leftMargin + sMasterLp.rightMargin;
+
+ sMasterView.layout(l + sMasterLp.leftMargin, t + sMasterLp.topMargin, l
+ + sMasterLp.leftMargin + sMasterWidth, b - sMasterLp.bottomMargin);
+
+ mDetailView.layout(l + mDetailLp.leftMargin + mTranslateAmount, t + mDetailLp.topMargin, r
+ - mDetailLp.rightMargin + mTranslateAmount, b - mDetailLp.bottomMargin);
+
+ /* Update translationX values */
+ if (!mIsAnimating) {
+ final float mTranslationX = mMasterVisible ? 0 : -mTranslateAmount;
+ sMasterView.setTranslationX(mTranslationX);
+ mDetailView.setTranslationX(mTranslationX);
+ }
+ }
+
+ private void updateChildReferences() {
+ final int mChildCount = getChildCount();
+ sMasterView = mChildCount > 0 ? getChildAt(0) : null;
+ mDetailView = mChildCount > 1 ? getChildAt(1) : null;
+ }
+
+ /**
+ * Allow or disallow the user to flick right on the detail pane to expose
+ * the master pane.
+ *
+ * @param enabled Whether or not to enable this interaction.
+ */
+ public void setFlingToExposeMasterEnabled(final boolean enabled) {
+ mFlingToExposeMaster = enabled;
+ }
+
+ /**
+ * Request the given listener be notified when the master pane is shown or
+ * hidden.
+ *
+ * @param listener The listener to notify when the master pane is shown or
+ * hidden.
+ */
+ public void setOnMasterVisibilityChangedListener(
+ final OnMasterVisibilityChangedListener listener) {
+ mOnMasterVisibilityChangedListener = listener;
+ }
+
+ /**
+ * Returns whether or not the master pane is visible.
+ *
+ * @return True if the master pane is visible.
+ */
+ public boolean isMasterVisible() {
+ return mMasterVisible;
+ }
+
+ /**
+ * Calls {@link #showMaster(boolean, int, Runnable)} with a null runnable.
+ */
+ public void showMaster(final boolean show, final int flags) {
+ showMaster(show, flags, null);
+ }
+
+ /**
+ * Shows or hides the master pane.
+ *
+ * @param show Whether or not to show the master pane.
+ * @param flags {@link #FLAG_IMMEDIATE} to show/hide immediately, or 0 to
+ * animate.
+ * @param completeRunnable An optional runnable to run when any animations
+ * related to this are complete.
+ */
+ public void showMaster(final boolean show, final int flags, final Runnable completeRunnable) {
+ if (!mFirstShow && mMasterVisible == show) {
+ return;
+ }
+
+ mShowMasterCompleteRunnable = completeRunnable;
+ mFirstShow = false;
+
+ mMasterVisible = show;
+ if (mOnMasterVisibilityChangedListener != null) {
+ mOnMasterVisibilityChangedListener.onMasterVisibilityChanged(show);
+ }
+
+ updateChildReferences();
+
+ if (sMasterView == null || mDetailView == null) {
+ return;
+ }
+
+ final float mTranslationX = show ? 0 : -mTranslateAmount;
+
+ if ((flags & FLAG_IMMEDIATE) != 0) {
+ sMasterView.setTranslationX(mTranslationX);
+ mDetailView.setTranslationX(mTranslationX);
+ if (mShowMasterCompleteRunnable != null) {
+ mShowMasterCompleteRunnable.run();
+ mShowMasterCompleteRunnable = null;
+ }
+ } else {
+ final long mDuration = getResources()
+ .getInteger(android.R.integer.config_shortAnimTime);
+
+ /* Animate if we have Honeycomb APIs, don't animate otherwise */
+ mIsAnimating = true;
+ final AnimatorSet mAnimatorSet = new AnimatorSet();
+ sMasterView.setLayerType(LAYER_TYPE_HARDWARE, null);
+ mDetailView.setLayerType(LAYER_TYPE_HARDWARE, null);
+ mAnimatorSet.play(
+ ObjectAnimator.ofFloat(sMasterView, "translationX", mTranslationX).setDuration(
+ mDuration)).with(
+ ObjectAnimator.ofFloat(mDetailView, "translationX", mTranslationX).setDuration(
+ mDuration));
+ mAnimatorSet.addListener(this);
+ mAnimatorSet.start();
+ }
+ }
+
+ @Override
+ public void requestDisallowInterceptTouchEvent(final boolean disallowIntercept) {
+ // Really bad hack... we really shouldn't do this.
+ // super.requestDisallowInterceptTouchEvent(disallowIntercept);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(final MotionEvent event) {
+ if (mFlingToExposeMaster && !mMasterVisible) {
+ mGestureDetector.onTouchEvent(event);
+ }
+
+ if (event.getAction() == MotionEvent.ACTION_DOWN && sMasterView != null && mMasterVisible) {
+ if (event.getX() > mTranslateAmount) {
+ return true;
+ }
+ }
+ return super.onInterceptTouchEvent(event);
+ }
+
+ @Override
+ public boolean onTouchEvent(final MotionEvent event) {
+ if (mFlingToExposeMaster && !mMasterVisible && mGestureDetector.onTouchEvent(event)) {
+ return true;
+ }
+
+ if (event.getAction() == MotionEvent.ACTION_DOWN && sMasterView != null && mMasterVisible) {
+ if (event.getX() > mTranslateAmount) {
+ showMaster(false, 0);
+ return true;
+ }
+ }
+ return super.onTouchEvent(event);
+ }
+
+ @Override
+ public void onAnimationEnd(final Animator animator) {
+ mIsAnimating = false;
+ sMasterView.setLayerType(LAYER_TYPE_NONE, null);
+ mDetailView.setLayerType(LAYER_TYPE_NONE, null);
+ requestLayout();
+ if (mShowMasterCompleteRunnable != null) {
+ mShowMasterCompleteRunnable.run();
+ mShowMasterCompleteRunnable = null;
+ }
+ }
+
+ @Override
+ public void onAnimationCancel(final Animator animator) {
+ mIsAnimating = false;
+ sMasterView.setLayerType(LAYER_TYPE_NONE, null);
+ mDetailView.setLayerType(LAYER_TYPE_NONE, null);
+ requestLayout();
+ if (mShowMasterCompleteRunnable != null) {
+ mShowMasterCompleteRunnable.run();
+ mShowMasterCompleteRunnable = null;
+ }
+ }
+
+ private final GestureDetector.OnGestureListener mGestureListener = new GestureDetector.SimpleOnGestureListener() {
+ @Override
+ public boolean onDown(final MotionEvent e) {
+ return true;
+ }
+
+ @Override
+ public boolean onFling(final MotionEvent e1, final MotionEvent e2, final float velocityX,
+ final float velocityY) {
+ final ViewConfiguration mViewConfig = ViewConfiguration.get(getContext());
+ final float mAbsVelocityX = Math.abs(velocityX);
+ final float mAbsVelocityY = Math.abs(velocityY);
+ if (mFlingToExposeMaster && !mMasterVisible && velocityX > 0
+ && mAbsVelocityX >= mAbsVelocityY
+ && mAbsVelocityX > mViewConfig.getScaledMinimumFlingVelocity()
+ && mAbsVelocityX < mViewConfig.getScaledMaximumFlingVelocity()) {
+ showMaster(true, 0);
+ return true;
+ }
+ return super.onFling(e1, e2, velocityX, velocityY);
+ }
+ };
+
+ @Override
+ public void onAnimationStart(final Animator animator) {
+ /* Nothing to do */
+ }
+
+ @Override
+ public void onAnimationRepeat(final Animator animator) {
+ /* Nothing to do */
+ }
+}
diff --git a/src/com/andrew/apollo/widgets/ShuffleButton.java b/src/com/andrew/apollo/widgets/ShuffleButton.java
new file mode 100644
index 0000000..c9d39da
--- /dev/null
+++ b/src/com/andrew/apollo/widgets/ShuffleButton.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.widgets;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnLongClickListener;
+import android.widget.ImageButton;
+
+import com.andrew.apollo.MusicPlaybackService;
+import com.andrew.apollo.R;
+import com.andrew.apollo.utils.ApolloUtils;
+import com.andrew.apollo.utils.MusicUtils;
+import com.andrew.apollo.utils.ThemeUtils;
+import com.andrew.apollo.widgets.theme.HoloSelector;
+
+/**
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class ShuffleButton extends ImageButton implements OnClickListener, OnLongClickListener {
+
+ /**
+ * Shuffle theme resource
+ */
+ private static final String SHUFFLE = "btn_playback_shuffle";
+
+ /**
+ * Shuffle all theme resource
+ */
+ private static final String SHUFFLE_ALL = "btn_playback_shuffle_all";
+
+ /**
+ * The resources to use.
+ */
+ private final ThemeUtils mResources;
+
+ /**
+ * @param context The {@link Context} to use
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ */
+ @SuppressWarnings("deprecation")
+ public ShuffleButton(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ // Initialze the theme resources
+ mResources = new ThemeUtils(context);
+ // Theme the selector
+ setBackgroundDrawable(new HoloSelector(context));
+ // Control playback (cycle shuffle)
+ setOnClickListener(this);
+ // Show the cheat sheet
+ setOnLongClickListener(this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onClick(final View v) {
+ MusicUtils.cycleShuffle();
+ updateShuffleState();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onLongClick(final View view) {
+ if (TextUtils.isEmpty(view.getContentDescription())) {
+ return false;
+ } else {
+ ApolloUtils.showCheatSheet(view);
+ return true;
+ }
+ }
+
+ /**
+ * Sets the correct drawable for the shuffle state.
+ */
+ public void updateShuffleState() {
+ switch (MusicUtils.getShuffleMode()) {
+ case MusicPlaybackService.SHUFFLE_NORMAL:
+ setContentDescription(getResources().getString(R.string.accessibility_shuffle_all));
+ setImageDrawable(mResources.getDrawable(SHUFFLE_ALL));
+ break;
+ case MusicPlaybackService.SHUFFLE_AUTO:
+ setContentDescription(getResources().getString(R.string.accessibility_shuffle_all));
+ setImageDrawable(mResources.getDrawable(SHUFFLE_ALL));
+ break;
+ case MusicPlaybackService.SHUFFLE_NONE:
+ setContentDescription(getResources().getString(R.string.accessibility_shuffle));
+ setImageDrawable(mResources.getDrawable(SHUFFLE));
+ break;
+ default:
+ break;
+ }
+ }
+
+}
diff --git a/src/com/andrew/apollo/widgets/SquareImageView.java b/src/com/andrew/apollo/widgets/SquareImageView.java
new file mode 100644
index 0000000..d3a2ce3
--- /dev/null
+++ b/src/com/andrew/apollo/widgets/SquareImageView.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.widgets;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+
+/**
+ * A custom {@link ImageView} that is sized to be a perfect square, otherwise
+ * functions like a typical {@link ImageView}.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class SquareImageView extends LayoutSuppressingImageView {
+
+ /**
+ * @param context The {@link Context} to use
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ */
+ public SquareImageView(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onMeasure(final int widthSpec, final int heightSpec) {
+ super.onMeasure(widthSpec, heightSpec);
+ final int mSize = Math.min(getMeasuredWidth(), getMeasuredHeight());
+ setMeasuredDimension(mSize, mSize);
+ }
+
+}
diff --git a/src/com/andrew/apollo/widgets/SquareView.java b/src/com/andrew/apollo/widgets/SquareView.java
new file mode 100644
index 0000000..effaf28
--- /dev/null
+++ b/src/com/andrew/apollo/widgets/SquareView.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.widgets;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * A custom {@link ViewGroup} used to make it's children into perfect squares.
+ * Useful when dealing with grid images and especially album art.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class SquareView extends ViewGroup {
+
+ /**
+ * @param context The {@link Context} to use
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ */
+ public SquareView(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
+ final View mChildren = getChildAt(0);
+ mChildren.measure(widthMeasureSpec, widthMeasureSpec);
+ final int mWidth = resolveSize(mChildren.getMeasuredWidth(), widthMeasureSpec);
+ mChildren.measure(mWidth, mWidth);
+ setMeasuredDimension(mWidth, mWidth);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onLayout(final boolean changed, final int l, final int u, final int r,
+ final int d) {
+ getChildAt(0).layout(0, 0, r - l, d - u);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void requestLayout() {
+ forceLayout();
+ }
+}
diff --git a/src/com/andrew/apollo/widgets/VerticalScrollListener.java b/src/com/andrew/apollo/widgets/VerticalScrollListener.java
new file mode 100644
index 0000000..1fca5d2
--- /dev/null
+++ b/src/com/andrew/apollo/widgets/VerticalScrollListener.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.widgets;
+
+import android.annotation.SuppressLint;
+import android.view.View;
+import android.widget.AbsListView;
+import android.widget.AbsListView.OnScrollListener;
+
+import com.andrew.apollo.utils.ApolloUtils;
+
+@SuppressLint("NewApi")
+public class VerticalScrollListener implements OnScrollListener {
+
+ /* Used to determine the off set to scroll the header */
+ private final ScrollableHeader mHeader;
+
+ private final ProfileTabCarousel mTabCarousel;
+
+ private final int mPageIndex;
+
+ public VerticalScrollListener(final ScrollableHeader header, final ProfileTabCarousel carousel,
+ final int pageIndex) {
+ mHeader = header;
+ mTabCarousel = carousel;
+ mPageIndex = pageIndex;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onScroll(final AbsListView view, final int firstVisibleItem,
+ final int visibleItemCount, final int totalItemCount) {
+
+ if (mTabCarousel == null || mTabCarousel.isTabCarouselIsAnimating()) {
+ return;
+ }
+
+ final View top = view.getChildAt(firstVisibleItem);
+ if (top == null) {
+ return;
+ }
+
+ if (firstVisibleItem != 0) {
+ mTabCarousel.moveToYCoordinate(mPageIndex,
+ -mTabCarousel.getAllowedVerticalScrollLength());
+ return;
+ }
+
+ float y;
+ if (ApolloUtils.hasHoneycomb()) {
+ y = view.getChildAt(firstVisibleItem).getY();
+ } else {
+ y = view.getChildAt(firstVisibleItem).getTop();
+ }
+ final float amtToScroll = Math.max(y, -mTabCarousel.getAllowedVerticalScrollLength());
+ mTabCarousel.moveToYCoordinate(mPageIndex, amtToScroll);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onScrollStateChanged(final AbsListView view, final int scrollState) {
+ if (mHeader != null) {
+ mHeader.onScrollStateChanged(view, scrollState);
+ }
+ }
+
+ /** Defines the header to be scrolled. */
+ public interface ScrollableHeader {
+
+ /* Used the pause the disk cache while scrolling */
+ public void onScrollStateChanged(AbsListView view, int scrollState);
+ }
+
+}
diff --git a/src/com/andrew/apollo/widgets/theme/BottomActionBar.java b/src/com/andrew/apollo/widgets/theme/BottomActionBar.java
new file mode 100644
index 0000000..a2fdb17
--- /dev/null
+++ b/src/com/andrew/apollo/widgets/theme/BottomActionBar.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.widgets.theme;
+
+import android.content.Context;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.util.AttributeSet;
+import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
+
+import com.andrew.apollo.R;
+import com.andrew.apollo.utils.ThemeUtils;
+
+/**
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+@SuppressWarnings("deprecation")
+public class BottomActionBar extends RelativeLayout {
+
+ /**
+ * Resource name used to theme the bottom action bar
+ */
+ private static final String BOTTOM_ACTION_BAR = "bottom_action_bar";
+
+ /**
+ * @param context The {@link Context} to use
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ * @throws NameNotFoundException
+ */
+ public BottomActionBar(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ // Initialze the theme resources
+ final ThemeUtils resources = new ThemeUtils(context);
+ // Theme the bottom action bar
+ setBackgroundDrawable(resources.getDrawable(BOTTOM_ACTION_BAR));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ // Theme the selector
+ final LinearLayout bottomActionBar = (LinearLayout)findViewById(R.id.bottom_action_bar);
+ bottomActionBar.setBackgroundDrawable(new HoloSelector(getContext()));
+ }
+}
diff --git a/src/com/andrew/apollo/widgets/theme/Colorstrip.java b/src/com/andrew/apollo/widgets/theme/Colorstrip.java
new file mode 100644
index 0000000..6d649ca
--- /dev/null
+++ b/src/com/andrew/apollo/widgets/theme/Colorstrip.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.widgets.theme;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+
+import com.andrew.apollo.utils.ThemeUtils;
+
+/**
+ * Used as a thin strip placed just above the bottom action bar or just below
+ * the top action bar.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class Colorstrip extends View {
+
+ /**
+ * Resource name used to theme the colorstrip
+ */
+ private static final String COLORSTRIP = "colorstrip";
+
+ /**
+ * @param context The {@link Context} to use
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ */
+ public Colorstrip(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ // Initialze the theme resources
+ final ThemeUtils resources = new ThemeUtils(context);
+ // Theme the colorstrip
+ setBackgroundColor(resources.getColor(COLORSTRIP));
+ }
+}
diff --git a/src/com/andrew/apollo/widgets/theme/HoloSelector.java b/src/com/andrew/apollo/widgets/theme/HoloSelector.java
new file mode 100644
index 0000000..39c3edd
--- /dev/null
+++ b/src/com/andrew/apollo/widgets/theme/HoloSelector.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.widgets.theme;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.StateListDrawable;
+
+import com.andrew.apollo.utils.ApolloUtils;
+import com.andrew.apollo.utils.ThemeUtils;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * A themeable {@link StateListDrawable}.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class HoloSelector extends StateListDrawable {
+
+ /**
+ * Used to theme the touched and focused colors
+ */
+ private static final String RESOURCE_NAME = "holo_selector";
+
+ /**
+ * Focused state
+ */
+ private static final int FOCUSED = android.R.attr.state_focused;
+
+ /**
+ * Pressed state
+ */
+ private static final int PRESSED = android.R.attr.state_pressed;
+
+ /**
+ * Constructor for <code>HoloSelector</code>
+ *
+ * @param context The {@link Context} to use.
+ */
+ @SuppressLint("NewApi")
+ public HoloSelector(final Context context) {
+ final ThemeUtils resources = new ThemeUtils(context);
+ final int themeColor = resources.getColor(RESOURCE_NAME);
+ // Focused
+ addState(new int[] {
+ FOCUSED
+ }, makeColorDrawable(themeColor));
+ // Pressed
+ addState(new int[] {
+ PRESSED
+ }, makeColorDrawable(themeColor));
+ // Default
+ addState(new int[] {}, makeColorDrawable(Color.TRANSPARENT));
+ if (ApolloUtils.hasHoneycomb()) {
+ setExitFadeDuration(400);
+ }
+ }
+
+ /**
+ * @param color The color to use.
+ * @return A new {@link ColorDrawable}.
+ */
+ private static final ColorDrawable makeColorDrawable(final int color) {
+ return new WeakReference<ColorDrawable>(new ColorDrawable(color)).get();
+ }
+}
diff --git a/src/com/andrew/apollo/widgets/theme/ThemeableFrameLayout.java b/src/com/andrew/apollo/widgets/theme/ThemeableFrameLayout.java
new file mode 100644
index 0000000..716ba3f
--- /dev/null
+++ b/src/com/andrew/apollo/widgets/theme/ThemeableFrameLayout.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.widgets.theme;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+
+import com.andrew.apollo.utils.ThemeUtils;
+
+/**
+ * This is a custom {@link FrameLayout} that is used as the main conent when
+ * transacting fragments that is made themeable by allowing developers to change
+ * the background.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class ThemeableFrameLayout extends FrameLayout {
+
+ /**
+ * Used to set the background
+ */
+ public static final String BACKGROUND = "pager_background";
+
+ /**
+ * @param context The {@link Context} to use
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ */
+ @SuppressWarnings("deprecation")
+ public ThemeableFrameLayout(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ // Initialze the theme resources
+ final ThemeUtils resources = new ThemeUtils(context);
+ // Theme the layout
+ setBackgroundDrawable(resources.getDrawable(BACKGROUND));
+ }
+
+}
diff --git a/src/com/andrew/apollo/widgets/theme/ThemeableSeekBar.java b/src/com/andrew/apollo/widgets/theme/ThemeableSeekBar.java
new file mode 100644
index 0000000..18c24ed
--- /dev/null
+++ b/src/com/andrew/apollo/widgets/theme/ThemeableSeekBar.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.widgets.theme;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.SeekBar;
+
+import com.andrew.apollo.utils.ThemeUtils;
+
+/**
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class ThemeableSeekBar extends SeekBar {
+
+ /**
+ * Used to set the progess bar
+ */
+ public static final String PROGESS = "audio_player_seekbar";
+
+ /**
+ * @param context The {@link Context} to use
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ */
+ public ThemeableSeekBar(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ // Initialze the theme resources
+ final ThemeUtils resources = new ThemeUtils(context);
+ // Theme the seek bar
+ setProgressDrawable(resources.getDrawable(PROGESS));
+ }
+
+}
diff --git a/src/com/andrew/apollo/widgets/theme/ThemeableTextView.java b/src/com/andrew/apollo/widgets/theme/ThemeableTextView.java
new file mode 100644
index 0000000..5b4fd9d
--- /dev/null
+++ b/src/com/andrew/apollo/widgets/theme/ThemeableTextView.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.widgets.theme;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Typeface;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.widget.TextView;
+
+import com.andrew.apollo.R;
+import com.andrew.apollo.utils.ThemeUtils;
+
+import java.util.WeakHashMap;
+
+/**
+ * A custom {@link TextView} that is made themeable for developers. It allows a
+ * custom font and color to be set, otherwise functions like normal. Because
+ * different text views may required different colors to be set, the resource
+ * name each can be set in the XML via the attribute {@value themeResource}.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class ThemeableTextView extends TextView {
+
+ /**
+ * @param context The {@link Context} to use
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ */
+ public ThemeableTextView(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ // Initialze the theme resources
+ final ThemeUtils resources = new ThemeUtils(context);
+ // Retrieve styles attributes
+ final TypedArray typedArray = context.obtainStyledAttributes(attrs,
+ R.styleable.ThemeableTextView, 0, 0);
+ // Get the theme resource name
+ final String resourceName = typedArray
+ .getString(R.styleable.ThemeableTextView_themeResource);
+ // Theme the text color
+ if (!TextUtils.isEmpty(resourceName)) {
+ setTextColor(resources.getColor(resourceName));
+ }
+ // Recyle the attrs
+ typedArray.recycle();
+ }
+
+ /**
+ * A small class that holds a weak cache for any typefaces applied to the
+ * text.
+ */
+ public static final class TypefaceCache {
+
+ private static final WeakHashMap<String, Typeface> MAP = new WeakHashMap<String, Typeface>();
+
+ private static TypefaceCache sInstance;
+
+ /**
+ * Constructor for <code>TypefaceCache</code>
+ */
+ public TypefaceCache() {
+ }
+
+ /**
+ * @return A singleton of {@linkTypefaceCache}.
+ */
+ public static final TypefaceCache getInstance() {
+ if (sInstance == null) {
+ sInstance = new TypefaceCache();
+ }
+ return sInstance;
+ }
+
+ /**
+ * @param file The name of the type face asset.
+ * @param context The {@link Context} to use.
+ * @return A new type face.
+ */
+ public Typeface getTypeface(final String file, final Context context) {
+ Typeface result = MAP.get(file);
+ if (result == null) {
+ result = Typeface.createFromAsset(context.getAssets(), file);
+ MAP.put(file, result);
+ }
+ return result;
+ }
+ }
+}
diff --git a/src/com/andrew/apollo/widgets/theme/ThemeableTitlePageIndicator.java b/src/com/andrew/apollo/widgets/theme/ThemeableTitlePageIndicator.java
new file mode 100644
index 0000000..9d866b9
--- /dev/null
+++ b/src/com/andrew/apollo/widgets/theme/ThemeableTitlePageIndicator.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2012 Andrew Neal 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 com.andrew.apollo.widgets.theme;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import com.andrew.apollo.utils.ThemeUtils;
+import com.viewpagerindicator.TitlePageIndicator;
+
+/**
+ * This is a custom {@link TitlePageIndicator} that is made themeable by
+ * allowing developers to choose the background and the selected and unselected
+ * text colors.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class ThemeableTitlePageIndicator extends TitlePageIndicator {
+
+ /**
+ * Resource name used to theme the background
+ */
+ private static final String BACKGROUND = "tpi_background";
+
+ /**
+ * Resource name used to theme the selected text color
+ */
+ private static final String SELECTED_TEXT = "tpi_selected_text_color";
+
+ /**
+ * Resource name used to theme the unselected text color
+ */
+ private static final String TEXT = "tpi_unselected_text_color";
+
+ /**
+ * Resource name used to theme the footer color
+ */
+ private static final String FOOTER = "tpi_footer_color";
+
+ /**
+ * @param context The {@link Context} to use
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ */
+ @SuppressWarnings("deprecation")
+ public ThemeableTitlePageIndicator(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ // Initialze the theme resources
+ final ThemeUtils resources = new ThemeUtils(context);
+ // Theme the background
+ setBackgroundDrawable(resources.getDrawable(BACKGROUND));
+ // Theme the selected text color
+ setSelectedColor(resources.getColor(SELECTED_TEXT));
+ // Theme the unselected text color
+ setTextColor(resources.getColor(TEXT));
+ // Theme the footer
+ setFooterColor(resources.getColor(FOOTER));
+ }
+}